diff --git a/app/config/locale/translations/tr.json b/app/config/locale/translations/tr.json index 6a94aeacae..e82317de01 100644 --- a/app/config/locale/translations/tr.json +++ b/app/config/locale/translations/tr.json @@ -3,30 +3,36 @@ "settings.locale": "tr", "settings.direction": "ltr", "emails.sender": "%s Takımı", - "emails.verification.subject": "", - "emails.verification.hello": "", - "emails.verification.body": "", - "emails.verification.footer": "", - "emails.verification.thanks": "", - "emails.verification.signature": "", - "emails.magicSession.subject": "", - "emails.magicSession.hello": "", - "emails.magicSession.body": "", - "emails.magicSession.footer": "", - "emails.magicSession.thanks": "", - "emails.magicSession.signature": "", - "emails.recovery.subject": "", - "emails.recovery.hello": "", - "emails.recovery.body": "", - "emails.recovery.footer": "", - "emails.recovery.thanks": "", - "emails.recovery.signature": "", - "emails.invitation.subject": "", - "emails.invitation.hello": "", - "emails.invitation.body": "", - "emails.invitation.footer": "", - "emails.invitation.thanks": "", - "emails.invitation.signature": "", + "emails.verification.subject": "Hesabını Doğrula", + "emails.verification.hello": "Merhaba {{user}}", + "emails.verification.body": "Eposta adresini doğrulamak için bu bağlantıyı kullanın.", + "emails.verification.footer": "Eğer bu eposta adresini doğrulamak isteyen siz değilseniz devam etmeyin.", + "emails.verification.thanks": "Teşekkürler", + "emails.verification.signature": "{{project}} takımı", + "emails.magicSession.subject": "Giriş", + "emails.magicSession.hello": "Merhaba,", + "emails.magicSession.body": "Giriş yapmak için tıklayın.", + "emails.magicSession.footer": "Eğer bu eposta adresini kullanarak giriş yapmak istemediyseniz devam etmeyin.", + "emails.magicSession.thanks": "Teşekkürler", + "emails.magicSession.signature": "{{project}} takımı", + "emails.recovery.subject": "Şifremi Sıfırla", + "emails.recovery.hello": "Merhaba {{user}}", + "emails.recovery.body": "{{project}} şifrenizi sıfırlamak için bu bağlantıyı kullanın.", + "emails.recovery.footer": "Eğer şifre sıfırlama talebinde bulunmadıysanız devam etmeyin.", + "emails.recovery.thanks": "Teşekkürler", + "emails.recovery.signature": "{{project}} takımı", + "emails.invitation.subject": "%s üzerinde %s Takımına Davet", + "emails.invitation.hello": "Merhaba", + "emails.invitation.body": "Bu epostayı aldınız, çünkü {{owner}} sizi {{project}} üzerinde {{team}} takımının üyesi olmaya davet etti.", + "emails.invitation.footer": "Eğer ilgilenmiyorsanız devam etmeyin.", + "emails.invitation.thanks": "Teşekkürler", + "emails.invitation.signature": "{{project}} takımı", + "emails.certificate.subject": "%s için sertifika hatası", + "emails.certificate.hello": "Merhaba", + "emails.certificate.body": "Alan adınız '{{domain}}' için sertifika oluşturulamadı. Deneme sayısı {{attempt}} ve hata sebebi: {{error}}", + "emails.certificate.footer": "Geçmiş sertifikanız ilk denemeden sonra 30 gün daha geçerli kalacaktır. Bu konuyu araştırmanızı öneriyoruz, aksi taktirde alan adınız SSL sertifikasız kalacaktır.", + "emails.certificate.thanks": "Teşekkürler", + "emails.certificate.signature": "{{project}} takımı", "locale.country.unknown": "Bilinmeyen", "countries.af": "Afganistan", "countries.ao": "Angola", @@ -229,4 +235,4 @@ "continents.na": "Kuzey Amerika", "continents.oc": "Okyanusya", "continents.sa": "Güney Amerika" -} \ No newline at end of file +} diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c210b19f4f..54967fb502 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -550,11 +550,19 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if (!$user->isEmpty()) { $userId = $user->getId(); - $identitiesWithMatchingEmail = $dbForProject->find('identities', [ + $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), Query::notEqual('userId', $userId), ]); - if (!empty($identitiesWithMatchingEmail)) { + if (!empty($identityWithMatchingEmail)) { + throw new Exception(Exception::USER_ALREADY_EXISTS); + } + + $userWithMatchingEmail = $dbForProject->find('users', [ + Query::equal('email', [$email]), + Query::notEqual('$id', $userId), + ]); + if (!empty($userWithMatchingEmail)) { throw new Exception(Exception::USER_ALREADY_EXISTS); } } diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 1fae48dae0..aaa1df9d32 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -551,6 +551,11 @@ App::post('/v1/storage/buckets/:bucketId/files') break; } $data = $compressor->compress($data); + } else { + // reset the algorithm to none as we do not compress the file + // if file size exceedes the APP_STORAGE_READ_BUFFER + // regardless the bucket compression algoorithm + $algorithm = COMPRESSION_TYPE_NONE; } if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { @@ -872,14 +877,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::USER_UNAUTHORIZED); } - if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support - $output = 'jpg'; - } - - $inputs = Config::getParam('storage-inputs'); - $outputs = Config::getParam('storage-outputs'); - $fileLogos = Config::getParam('storage-logos'); - if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { @@ -890,9 +887,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } + if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support + $output = 'jpg'; + } + + $inputs = Config::getParam('storage-inputs'); + $outputs = Config::getParam('storage-outputs'); + $fileLogos = Config::getParam('storage-logos'); + $path = $file->getAttribute('path'); $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $algorithm = $file->getAttribute('algorithm', 'none'); + $algorithm = $file->getAttribute('algorithm', COMPRESSION_TYPE_NONE); $cipher = $file->getAttribute('openSSLCipher'); $mime = $file->getAttribute('mimeType'); if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) { @@ -903,7 +908,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $path = $fileLogos['default_image']; } - $algorithm = 'none'; + $algorithm = COMPRESSION_TYPE_NONE; $cipher = null; $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); @@ -915,12 +920,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } if (empty($output)) { + // when file extension is provided but it's not one of our + // supported outputs we fallback to `jpg` + if (!empty($type) && !array_key_exists($type, $outputs)) { + $type = 'jpg'; + } + // when file extension is not provided and the mime type is not one of our supported outputs // we fallback to `jpg` output format $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; } - $source = $deviceFiles->read($path); if (!empty($cipher)) { // Decrypt @@ -935,11 +945,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } switch ($algorithm) { - case 'zstd': + case COMPRESSION_TYPE_ZSTD: $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: $compressor = new GZIP(); $source = $compressor->decompress($source); break; @@ -1080,15 +1090,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ); } - switch ($file->getAttribute('algorithm', 'none')) { - case 'zstd': + switch ($file->getAttribute('algorithm', COMPRESSION_TYPE_NONE)) { + case COMPRESSION_TYPE_ZSTD: if (empty($source)) { $source = $deviceFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: if (empty($source)) { $source = $deviceFiles->read($path); } @@ -1231,15 +1241,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ); } - switch ($file->getAttribute('algorithm', 'none')) { - case 'zstd': + switch ($file->getAttribute('algorithm', COMPRESSION_TYPE_NONE)) { + case COMPRESSION_TYPE_ZSTD: if (empty($source)) { $source = $deviceFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: if (empty($source)) { $source = $deviceFiles->read($path); } @@ -1253,10 +1263,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') $response->send(substr($source, $start, ($end - $start + 1))); } $response->send($source); + return; } if (!empty($rangeHeader)) { $response->send($deviceFiles->read($path, $start, ($end - $start + 1))); + return; } $size = $deviceFiles->getFileSize($path); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 4ea122e3c5..05cfddd276 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -556,6 +556,22 @@ App::shutdown() ->setParam('project.{scope}.network.outbound', $response->getSize()) ->submit(); } + + /** + * Update user last activity + */ + if (!$user->isEmpty()) { + $accessedAt = $user->getAttribute('accessedAt', ''); + if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCCESS)) > $accessedAt) { + $user->setAttribute('accessedAt', DateTime::now()); + + if (APP_MODE_ADMIN !== $mode) { + $dbForProject->updateDocument('users', $user->getId(), $user); + } else { + $dbForConsole->updateDocument('users', $user->getId(), $user); + } + } + } }); App::init() diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 5463740761..36d2ee2365 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -27,7 +27,7 @@ trait StorageBase 'name' => 'Test Bucket', 'fileSecurity' => true, 'maximumFileSize' => 2000000, //2MB - 'allowedFileExtensions' => ["jpg", "png"], + 'allowedFileExtensions' => ["jpg", "png", 'jfif'], 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -462,6 +462,32 @@ trait StorageBase $this->assertEquals('image/png', $file2['headers']['content-type']); $this->assertNotEmpty($file2['body']); + // upload JXL file for preview + $fileJfif = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/preview-test.jfif'), 'image/jxl', 'preview-test.jfif'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $fileJfif['headers']['status-code']); + $this->assertNotEmpty($fileJfif['body']['$id']); + + // TEST preview JXL + $preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileJfif['body']['$id'] . '/preview', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $preview['headers']['status-code']); + $this->assertEquals('image/jpeg', $preview['headers']['content-type']); + $this->assertNotEmpty($preview['body']); + //new image preview features $file3 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([ 'content-type' => 'application/json', diff --git a/tests/resources/disk-a/preview-test.jfif b/tests/resources/disk-a/preview-test.jfif new file mode 100644 index 0000000000..e50021f95d Binary files /dev/null and b/tests/resources/disk-a/preview-test.jfif differ