diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e3e6fcd81..8d8ddadda6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -497,6 +497,18 @@ If you are in PHP Storm you don't need any plugin. Below are the settings requir 2. If needed edit the **dev/xdebug.ini** file to your needs. 3. Launch your Appwrite instance while your debugger is listening for connections. +## Profiling +Appwrite uses XDebug [Profiler](https://xdebug.org/docs/profiler) for generating **CacheGrind** files. The generated file would be located in each of the `appwrite` containers inside the `/tmp/xdebug` folder. + +To disable the profiler while debugging remove the `,profiler` mode from the `xdebug.ini` file +```diff +zend_extension=xdebug + +[xdebug] +-xdebug.mode=develop,debug,profile ++xdebug.mode=develop,debug +``` + ### VS Code Launch Configuration ```json diff --git a/Dockerfile b/Dockerfile index 6f0fdcb7b7..fc501701ba 100755 --- a/Dockerfile +++ b/Dockerfile @@ -91,9 +91,10 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ # Enable Extensions RUN if [ "$DEBUG" == "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi +RUN if [ "$DEBUG" == "true" ]; then mkdir -p /tmp/xdebug; fi RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so; fi EXPOSE 80 -CMD [ "php", "app/http.php" ] \ No newline at end of file +CMD [ "php", "app/http.php" ] diff --git a/app/config/collections.php b/app/config/collections.php index b4dc1049fb..b8667d0b8d 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -3848,6 +3848,39 @@ $projectCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('scheduledAt'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + [ + '$id' => ID::custom('scheduleInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('scheduleId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/config/locale/translations/ar-ma.json b/app/config/locale/translations/ar-ma.json new file mode 100644 index 0000000000..453de25c80 --- /dev/null +++ b/app/config/locale/translations/ar-ma.json @@ -0,0 +1,238 @@ +{ + "settings.inspire": "\"الفن ديال الحكمة هو الفن ديال أنك تعرف أش تنخّل.\"", + "settings.locale": "ar-ma", + "settings.direction": "rtl", + "emails.sender": "فرقة %s", + "emails.verification.subject": "التيْقان ديال الحساب", + "emails.verification.hello": "السلام {{user}}", + "emails.verification.body": "تبّع هاد الوصلة باش تيقّن لادريسة تاع ليميل ديالك.", + "emails.verification.footer": "إلا ماشي نتا اللي طلبتي تيقّن هاد لادريسة تاع ليميل، ممكن تنخّل هاد البرية.", + "emails.verification.thanks": "شكرا", + "emails.verification.signature": "فرقة {{project}}", + "emails.magicSession.subject": "تكونيكطا", + "emails.magicSession.hello": "السلام,", + "emails.magicSession.body": "تبّع هاد الوصلة باش تتكونيكطا.", + "emails.magicSession.footer": "إلا ماشي نتا اللي طلبتي تتكونيكطا بهاد ليميل، ممكن تنخّل هاد البرية.", + "emails.magicSession.thanks": "شكرا", + "emails.magicSession.signature": "فرقة {{project}}", + "emails.recovery.subject": "تبدال كلمة السر", + "emails.recovery.hello": "السلام {{user}}", + "emails.recovery.body": "تبّع هاد الوصلة باش تبدّل كلمة السر تاع {{project}}.", + "emails.recovery.footer": "إلا ماشي نتا اللي طلبتي تبدّل كلمة السر، ممكن تنخّل هاد البرية.", + "emails.recovery.thanks": "شكرا", + "emails.recovery.signature": "فرقة {{project}}", + "emails.invitation.subject": "عراضة ل فرقة %s ف %s", + "emails.invitation.hello": "السلام", + "emails.invitation.body": "هاد البرية تصيفطات ليك حيت {{owner}} بغى يعرض عليك تولّي عضو ف فرقة {{team}} عند {{project}}.", + "emails.invitation.footer": "إلا كنتي ما مسوّقش, ممكن تنخّل هاد البرية.", + "emails.invitation.thanks": "شكرا", + "emails.invitation.signature": "فرقة {{project}}", + "emails.certificate.subject": "السرتافيكة فشلات ل %s", + "emails.certificate.hello": "السلام", + "emails.certificate.body": "السرتافيكة ديال الضومين ديالك '{{domain}}' ما قدّاتش تجينيرا. هادي هي المحاولة نمرة {{attempt}}, السبب ديال هاد الفشل هو: {{error}}", + "emails.certificate.footer": "السرتافيكة الفايتة ديالك غاتبقى مزيانة لمدة 30 يوم من عند أول فشل. كانشجعوك بزاف أنك تبقشش فهاد الموضوع, وا إلّا الضومين ديالك ما غايبقاش خدّام فيه الـ SSL.", + "emails.certificate.thanks": "شكرا", + "emails.certificate.signature": "فرقة {{project}}", + "locale.country.unknown": "ما معروفش", + "countries.af": "أفغانستان", + "countries.ao": "أنڭولا", + "countries.al": "ألبانيا", + "countries.ad": "أندورا", + "countries.ae": "الإمارات العربية المتّاحدة", + "countries.ar": "الأرجنتين", + "countries.am": "أرمينيا", + "countries.ag": "أنتيڭوا وبربودا", + "countries.au": "ؤسطراليا", + "countries.at": "النامسا", + "countries.az": "أديربيجان", + "countries.bi": "بوروندي", + "countries.be": "بلجيكا", + "countries.bj": "بينين", + "countries.bf": "بوركينا فاصو", + "countries.bd": "بنڭلاديش", + "countries.bg": "بلڭاريا", + "countries.bh": "البحرين", + "countries.bs": "دزيرات البهاما", + "countries.ba": "البوسنة ؤ الهرسك", + "countries.by": "بيلاروسيا", + "countries.bz": "بيليز", + "countries.bo": "بوليڤيا", + "countries.br": "البرازيل", + "countries.bb": "باربادوس", + "countries.bn": "بروناي", + "countries.bt": "بوتان", + "countries.bw": "بوتسوانا", + "countries.cf": "جمهورية إفريقيا الوسطانية", + "countries.ca": "كانادا", + "countries.ch": "سويسرا", + "countries.cl": "تشيلي", + "countries.cn": "الشينوا", + "countries.ci": "ساحل العاج", + "countries.cm": "الكاميرون", + "countries.cd": "جمهورية الكونڭو الديمقراطية", + "countries.cg": "جمهورية الكونڭو", + "countries.co": "كولومبيا", + "countries.km": "دزيرات القومور", + "countries.cv": "الراس الخضر", + "countries.cr": "كوسطاريكا", + "countries.cu": "كوبا", + "countries.cy": "قوبروص", + "countries.cz": "التشيك", + "countries.de": "ألمانيا", + "countries.dj": "دجيبوتي", + "countries.dm": "ضومينيكا", + "countries.dk": "الدنمارك", + "countries.do": "جمهورية الضومينيكان", + "countries.dz": "الدزاير", + "countries.ec": "إكوادور", + "countries.eg": "مصر", + "countries.er": "إريتريا", + "countries.es": "سبانيا", + "countries.ee": "إسطونيا", + "countries.et": "إتيوپيا", + "countries.fi": "فينلاندا", + "countries.fj": "فيدجي", + "countries.fr": "فرانسا", + "countries.fm": "ميكرونيزيا", + "countries.ga": "الڭابون", + "countries.gb": "المملكة المتّاحدة", + "countries.ge": "تجورجيا", + "countries.gh": "غانا", + "countries.gn": "غينيا", + "countries.gm": "ڭامبيا", + "countries.gw": "غينيا بيساو", + "countries.gq": "غينيا الستوائية", + "countries.gr": "اليونان", + "countries.gd": "ڭرينادا", + "countries.gt": "ڭواتيمالا", + "countries.gy": "ڭيانا", + "countries.hn": "هوندوراس", + "countries.hr": "كرواتيا", + "countries.ht": "هايتي", + "countries.hu": "الماجر", + "countries.id": "إندونيسيا", + "countries.in": "الهند", + "countries.ie": "إرلاندا", + "countries.ir": "إران", + "countries.iq": "العراق", + "countries.is": "إسلاندا", + "countries.il": "إسرائيل", + "countries.it": "الطاليان", + "countries.jm": "جامايكا", + "countries.jo": "الأردن", + "countries.jp": "الجاپون", + "countries.kz": "كازاخستان", + "countries.ke": "كينيا", + "countries.kg": "قيرغيزستان", + "countries.kh": "كمبوديا", + "countries.ki": "كيريباتي", + "countries.kn": "سانت كيتس ؤ نيفيس", + "countries.kr": "كوريا الجنوبية", + "countries.kw": "الكويت", + "countries.la": "لاوس", + "countries.lb": "لبنان", + "countries.lr": "ليبيريا", + "countries.ly": "ليبيا", + "countries.lc": "سانت لوسيا", + "countries.li": "ليختنشتاين", + "countries.lk": "سري لانكا", + "countries.ls": "ليسوتو", + "countries.lt": "ليتوانيا", + "countries.lu": "لوكسمبورڭ", + "countries.lv": "لاتفيا", + "countries.ma": "المغريب", + "countries.mc": "موناكو", + "countries.md": "مولضوڤا", + "countries.mg": "ماداغشقار", + "countries.mv": "دزيرات المالديڤ", + "countries.mx": "الميكسيك", + "countries.mh": "دزيرات مارشال", + "countries.mk": "مقدونيا", + "countries.ml": "مالي", + "countries.mt": "مالطا", + "countries.mm": "ميانمار", + "countries.me": "مونطينيڭرو", + "countries.mn": "منغوليا", + "countries.mz": "الموزمبيق", + "countries.mr": "موريتانيا", + "countries.mu": "موريشيوس", + "countries.mw": "مالاوي", + "countries.my": "ماليزيا", + "countries.na": "ناميبيا", + "countries.ne": "النيجر", + "countries.ng": "نيجيريا", + "countries.ni": "نيكاراڭوا", + "countries.nl": "هولاندا", + "countries.no": "النرويج", + "countries.np": "نيپال", + "countries.nr": "ناورو", + "countries.nz": "نيوزيلاندا", + "countries.om": "عمّان", + "countries.pk": "پاكيستان", + "countries.pa": "پاناما", + "countries.pe": "الپيرو", + "countries.ph": "الفيليپين", + "countries.pw": "پالاو", + "countries.pg": "پاپوا غينيا الجديدة", + "countries.pl": "پولاندا", + "countries.kp": "كوريا الشمالية", + "countries.pt": "البرطقيز", + "countries.py": "الپاراڭواي", + "countries.qa": "قطر", + "countries.ro": "رومانيا", + "countries.ru": "روسيا", + "countries.rw": "روّاندا", + "countries.sa": "المملكة العربية السعودية", + "countries.sd": "السودان", + "countries.sn": "السينيڭال", + "countries.sg": "سنغافورة", + "countries.sb": "دزيرات سليمان", + "countries.sl": "صييراليون", + "countries.sv": "السالڤاضور", + "countries.sm": "سان مارينو", + "countries.so": "الصومال", + "countries.rs": "صيربيا", + "countries.ss": "جنوب السودان", + "countries.st": "صاو طومي ؤ پرينسيپي", + "countries.sr": "سورينام", + "countries.sk": "صلوڤاكيا", + "countries.si": "صلوڤينيا", + "countries.se": "السويد", + "countries.sz": "سوازيلاند", + "countries.sc": "السيشيل", + "countries.sy": "سوريا", + "countries.td": "تشاد", + "countries.tg": "الطوڭو", + "countries.th": "الطايلوند", + "countries.tj": "طادجيكيستان", + "countries.tm": "تركمانيستان", + "countries.tl": "تيمور الشرقية", + "countries.to": "تونڭا", + "countries.tt": "ترينيداد ؤ طوباڭو", + "countries.tn": "تونس", + "countries.tr": "توركيا", + "countries.tv": "توڤالو", + "countries.tz": "طنزانيا", + "countries.ug": "ؤڭاندا", + "countries.ua": "ؤكرانيا", + "countries.uy": "ؤروڭواي", + "countries.us": "ميريكان", + "countries.uz": "ؤزباكيستان", + "countries.va": "مدينة الڤاتيكان", + "countries.vc": "سانت ڤانسون ؤ دزيرات ڭرينادين", + "countries.ve": "ڤينيزويلا", + "countries.vn": "ڤيطنام", + "countries.vu": "ڤانواتو", + "countries.ws": "ساموا", + "countries.ye": "اليمن", + "countries.za": "جنوب إفريقيا", + "countries.zm": "زامبيا", + "countries.zw": "زيمبابوي", + "continents.af": "أفريقيا", + "continents.an": "القارة القطبية الجنوبية", + "continents.as": "أسيا", + "continents.eu": "ؤروپا", + "continents.na": "ميريكان الشمالية", + "continents.oc": "ؤقيانوسيا", + "continents.sa": "ميريكان الجنوبية" +} diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 7a9a217ad4..d294d5e4ee 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1744,9 +1744,8 @@ App::post('/v1/functions/:functionId/executions') ->setContext('function', $function); if ($async) { - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - if(is_null($scheduledAt)) { + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); $queueForFunctions ->setType('http') ->setExecution($execution) @@ -1770,7 +1769,7 @@ App::post('/v1/functions/:functionId/executions') 'jwt' => $jwt, ]; - $dbForConsole->createDocument('schedules', new Document([ + $schedule = $dbForConsole->createDocument('schedules', new Document([ 'region' => System::getEnv('_APP_REGION', 'default'), 'resourceType' => ScheduleExecutions::getSupportedResource(), 'resourceId' => $execution->getId(), @@ -1781,6 +1780,13 @@ App::post('/v1/functions/:functionId/executions') 'data' => $data, 'active' => true, ])); + + $execution = $execution + ->setAttribute('scheduleId', $schedule->getId()) + ->setAttribute('scheduleInternalId', $schedule->getInternalId()) + ->setAttribute('scheduledAt', $scheduledAt); + + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); } return $response @@ -1825,7 +1831,8 @@ App::post('/v1/functions/:functionId/executions') 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_VERSION' => APP_VERSION_STABLE + 'APPWRITE_VERSION' => APP_VERSION_STABLE, + 'APPWRITE_REGION' => $project->getAttribute('region'), ]); /** Execute function */ diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 652644d44b..b080066653 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -63,7 +63,7 @@ App::post('/v1/storage/buckets') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true) - ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan']) + ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan']) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) @@ -240,7 +240,7 @@ App::put('/v1/storage/buckets/:bucketId') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true) - ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan']) + ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan']) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) diff --git a/app/controllers/general.php b/app/controllers/general.php index 70acd9d482..d532110df3 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -2,6 +2,7 @@ require_once __DIR__ . '/../init.php'; +use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; use Appwrite\Event\Certificate; use Appwrite\Event\Event; @@ -165,7 +166,15 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); } + $jwtExpiry = $function->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $apiKey = $jwtObj->encode([ + 'projectId' => $project->getId(), + 'scopes' => $function->getAttribute('scopes', []) + ]); + $headers = \array_merge([], $requestHeaders); + $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-id'] = ''; $headers['x-appwrite-user-jwt'] = ''; @@ -244,15 +253,21 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); } + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; + $hostname = System::getEnv('_APP_DOMAIN'); + $endpoint = $protocol . '://' . $hostname . "/v1"; + // Appwrite vars $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, 'APPWRITE_FUNCTION_ID' => $functionId, 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_VERSION' => APP_VERSION_STABLE + 'APPWRITE_VERSION' => APP_VERSION_STABLE, + 'APPWRITE_REGION' => $project->getAttribute('region'), ]); /** Execute function */ diff --git a/dev/xdebug.ini b/dev/xdebug.ini index e29c8bd46e..f9c535019f 100644 --- a/dev/xdebug.ini +++ b/dev/xdebug.ini @@ -1,6 +1,8 @@ zend_extension=xdebug [xdebug] -xdebug.mode=develop,debug +xdebug.mode=develop,debug,profile xdebug.client_host=host.docker.internal -xdebug.start_with_request=yes \ No newline at end of file +xdebug.start_with_request=yes +xdebug.output_dir=/tmp/xdebug +xdebug.use_compression=false diff --git a/docs/references/avatars/get-favicon.md b/docs/references/avatars/get-favicon.md index b571e0af93..c3a63dd83f 100644 --- a/docs/references/avatars/get-favicon.md +++ b/docs/references/avatars/get-favicon.md @@ -1 +1,3 @@ Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL. + +This endpoint does not follow HTTP redirects. \ No newline at end of file diff --git a/docs/references/avatars/get-image.md b/docs/references/avatars/get-image.md index efd654b362..ca9cc5e418 100644 --- a/docs/references/avatars/get-image.md +++ b/docs/references/avatars/get-image.md @@ -1,3 +1,5 @@ Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol. When one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px. + +This endpoint does not follow HTTP redirects. \ No newline at end of file diff --git a/src/Appwrite/Migration/Version/V21.php b/src/Appwrite/Migration/Version/V21.php index be6c38be67..7dd2912234 100644 --- a/src/Appwrite/Migration/Version/V21.php +++ b/src/Appwrite/Migration/Version/V21.php @@ -116,6 +116,33 @@ class V21 extends Migration } catch (\Throwable $th) { Console::warning("'_key_deployment' from {$id}: {$th->getMessage()}"); } + + try { + /** + * Create 'scheduledAt' attribute + */ + $this->createAttributeFromCollection($this->projectDB, $id, 'scheduledAt'); + } catch (\Throwable $th) { + Console::warning("'scheduledAt' from {$id}: {$th->getMessage()}"); + } + + try { + /** + * Create 'scheduleInternalId' attribute + */ + $this->createAttributeFromCollection($this->projectDB, $id, 'scheduleInternalId'); + } catch (\Throwable $th) { + Console::warning("'scheduleInternalId' from {$id}: {$th->getMessage()}"); + } + + try { + /** + * Create 'scheduleId' attribute + */ + $this->createAttributeFromCollection($this->projectDB, $id, 'scheduleId'); + } catch (\Throwable $th) { + Console::warning("'scheduleId' from {$id}: {$th->getMessage()}"); + } } usleep(50000); diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index 922498c3fa..b712edd402 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -400,7 +400,8 @@ class Builds extends Action 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_VERSION' => APP_VERSION_STABLE + 'APPWRITE_VERSION' => APP_VERSION_STABLE, + 'APPWRITE_REGION' => $project->getAttribute('region'), ]); $command = $deployment->getAttribute('commands', ''); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 5dfdc0a63a..78671bfeb0 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -373,6 +373,9 @@ class Functions extends Action $headers['x-appwrite-event'] = $event ?? ''; $headers['x-appwrite-user-id'] = $user->getId() ?? ''; $headers['x-appwrite-user-jwt'] = $jwt ?? ''; + $headers['x-appwrite-country-code'] = ''; + $headers['x-appwrite-continent-code'] = ''; + $headers['x-appwrite-continent-eu'] = 'false'; /** Create execution or update execution status */ $execution = $dbForProject->getDocument('executions', $executionId ?? ''); @@ -464,7 +467,8 @@ class Functions extends Action 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_VERSION' => APP_VERSION_STABLE + 'APPWRITE_VERSION' => APP_VERSION_STABLE, + 'APPWRITE_REGION' => $project->getAttribute('region'), ]); /** Execute function */ diff --git a/src/Appwrite/Utopia/Response/Filters/V18.php b/src/Appwrite/Utopia/Response/Filters/V18.php index b32856bdb4..d0aa680e3b 100644 --- a/src/Appwrite/Utopia/Response/Filters/V18.php +++ b/src/Appwrite/Utopia/Response/Filters/V18.php @@ -14,6 +14,7 @@ class V18 extends Filter $parsedResponse = match($model) { Response::MODEL_FUNCTION => $this->parseFunction($content), + Response::MODEL_EXECUTION => $this->parseExecution($content), Response::MODEL_PROJECT => $this->parseProject($content), default => $parsedResponse, }; @@ -21,6 +22,12 @@ class V18 extends Filter return $parsedResponse; } + protected function parseExecution(array $content) + { + unset($content['scheduledAt']); + return $content; + } + protected function parseFunction(array $content) { unset($content['scopes']); diff --git a/src/Appwrite/Utopia/Response/Model/Execution.php b/src/Appwrite/Utopia/Response/Model/Execution.php index fbd9619a40..90fbdc9689 100644 --- a/src/Appwrite/Utopia/Response/Model/Execution.php +++ b/src/Appwrite/Utopia/Response/Model/Execution.php @@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; +use Utopia\Database\DateTime; use Utopia\Database\Helpers\Role; class Execution extends Model @@ -110,6 +111,13 @@ class Execution extends Model 'default' => 0, 'example' => 0.400, ]) + ->addRule('scheduledAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'The scheduled time for execution. If left empty, execution will be queued immediately.', + 'required' => false, + 'default' => DateTime::now(), + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) ; } diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index bf969d388a..4e501692fa 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -8,6 +8,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; +use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; @@ -269,6 +270,7 @@ class FunctionsCustomClientTest extends Scope // Schedule execution for the future \date_default_timezone_set('UTC'); $futureTime = (new \DateTime())->add(new \DateInterval('PT10S'))->format('Y-m-d H:i:s'); + $futureTimeIso = DateTime::formatTz($futureTime); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ 'content-type' => 'application/json', @@ -286,9 +288,23 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(202, $execution['headers']['status-code']); $this->assertEquals('scheduled', $execution['body']['status']); + $this->assertEquals($futureTimeIso, $execution['body']['scheduledAt']); $executionId = $execution['body']['$id']; + // List executions and ensure it has schedule date + $response = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, \count($response['body']['executions'])); + $recentExecution = $response['body']['executions'][0]; + $this->assertEquals($executionId, $recentExecution['$id']); + $this->assertEquals($futureTimeIso, $recentExecution['scheduledAt']); + sleep(20); $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions/' . $executionId, [ @@ -303,6 +319,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('/custom', $execution['body']['requestPath']); $this->assertEquals('GET', $execution['body']['requestMethod']); $this->assertGreaterThan(0, $execution['body']['duration']); + $this->assertEquals($futureTimeIso, $execution['body']['scheduledAt']); /* Test for FAILURE */ @@ -449,6 +466,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']); $this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']); $this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']); + $this->assertEquals('default', $output['APPWRITE_REGION']); $this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']); $this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']); $this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']); @@ -834,6 +852,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']); $this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']); $this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']); + $this->assertEquals('default', $output['APPWRITE_REGION']); $this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']); $this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']); $this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']); diff --git a/tests/resources/functions/php-fn/index.php b/tests/resources/functions/php-fn/index.php index b353ad2ca4..0b6e3d206c 100644 --- a/tests/resources/functions/php-fn/index.php +++ b/tests/resources/functions/php-fn/index.php @@ -11,6 +11,7 @@ return function ($context) { 'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '', 'APPWRITE_VERSION' => \getenv('APPWRITE_VERSION') ?: '', + 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '', 'APPWRITE_FUNCTION_EVENT' => $context->req->headers['x-appwrite-event'] ?? '', 'APPWRITE_FUNCTION_EVENT_DATA' => $context->req->bodyRaw ?? '', 'APPWRITE_FUNCTION_DATA' => $context->req->bodyRaw ?? '', diff --git a/tests/resources/functions/php-large/index.php b/tests/resources/functions/php-large/index.php index 5a9666488e..ab3d050245 100644 --- a/tests/resources/functions/php-large/index.php +++ b/tests/resources/functions/php-large/index.php @@ -8,6 +8,7 @@ return function ($context) { 'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '', + 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '', 'UNICODE_TEST' => "êä" ]); }; diff --git a/tests/resources/functions/php/index.php b/tests/resources/functions/php/index.php index ac7b85a43a..d5328c40e1 100644 --- a/tests/resources/functions/php/index.php +++ b/tests/resources/functions/php/index.php @@ -8,6 +8,7 @@ return function ($context) { 'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '', + 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '', 'UNICODE_TEST' => "êä", 'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: '' ]); diff --git a/tests/unit/Utopia/Response/Filters/V18Test.php b/tests/unit/Utopia/Response/Filters/V18Test.php index 36719a7620..c4011c08a1 100644 --- a/tests/unit/Utopia/Response/Filters/V18Test.php +++ b/tests/unit/Utopia/Response/Filters/V18Test.php @@ -50,6 +50,32 @@ class V18Test extends TestCase $this->assertEquals($expected, $result); } + + public function executionProvider(): array + { + return [ + 'remove scheduledAt' => [ + [ + 'scheduledAt' => '2024-07-13T09:00:00.000Z', + ], + [ + ] + ] + ]; + } + + /** + * @dataProvider executionProvider + */ + public function testExecution(array $content, array $expected): void + { + $model = Response::MODEL_EXECUTION; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + public function projectProvider(): array { return [