mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.9.x' into feat-docker-geo-18x
# Conflicts: # src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php # src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
This commit is contained in:
@@ -165,9 +165,9 @@ class Apple extends OAuth2
|
||||
|
||||
protected function getAppSecret(): string
|
||||
{
|
||||
try {
|
||||
$secret = \json_decode($this->appSecret, true);
|
||||
} catch (\Throwable $th) {
|
||||
$secret = \json_decode($this->appSecret, true);
|
||||
|
||||
if (!\is_array($secret)) {
|
||||
throw new Exception('Invalid secret');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,6 @@ class Etsy extends OAuth2
|
||||
*/
|
||||
private string $endpoint = 'https://api.etsy.com/v3/public';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $version = '2022-07-14';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
|
||||
@@ -121,7 +121,7 @@ class Podio extends OAuth2
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return \strval($user['user_id']) ?? '';
|
||||
return \strval($user['user_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,11 +11,6 @@ class Zoom extends OAuth2
|
||||
*/
|
||||
private string $endpoint = 'https://zoom.us';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $version = '2022-03-26';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
|
||||
@@ -59,7 +59,7 @@ class PersonalData extends Password
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->email && strpos($password, explode('@', $this->email)[0] ?? '') !== false) {
|
||||
if ($this->email && strpos($password, explode('@', $this->email)[0]) !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,8 @@ class Mails extends Listener
|
||||
->setSmtpUsername($smtp['username'] ?? '')
|
||||
->setSmtpPassword($smtp['password'] ?? '')
|
||||
->setSmtpSecure($smtp['secure'] ?? '')
|
||||
->setSmtpReplyTo($customTemplate['replyTo'] ?? $smtp['replyTo'] ?? '')
|
||||
->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility
|
||||
->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '')
|
||||
->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
|
||||
->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class Service
|
||||
array_walk($ports, function (&$value, $key) {
|
||||
$split = explode(':', $value);
|
||||
$this->service['ports'][
|
||||
(isset($split[0])) ? $split[0] : ''
|
||||
$split[0]
|
||||
] = (isset($split[1])) ? $split[1] : '';
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class Env
|
||||
|
||||
foreach ($data as &$row) {
|
||||
$row = explode('=', $row, 2);
|
||||
$key = (isset($row[0])) ? trim($row[0]) : null;
|
||||
$key = trim($row[0]);
|
||||
$value = (isset($row[1])) ? (function (string $v): string {
|
||||
$v = trim($v);
|
||||
if (
|
||||
|
||||
@@ -459,7 +459,7 @@ class Event
|
||||
/**
|
||||
* Identify all sections of the pattern.
|
||||
*/
|
||||
$type = $parts[0] ?? false;
|
||||
$type = $parts[0];
|
||||
$resource = $parts[1] ?? false;
|
||||
$hasSubResource = $count > 3 && \str_starts_with($parts[3], '[');
|
||||
$hasSubSubResource = $count > 5 && \str_starts_with($parts[5], '[') && $hasSubResource;
|
||||
|
||||
@@ -251,14 +251,26 @@ class Mail extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Set SMTP reply to
|
||||
* Set SMTP reply-to email
|
||||
*
|
||||
* @param string $replyTo
|
||||
* @param string $email
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpReplyTo(string $replyTo): self
|
||||
public function setSmtpReplyToEmail(string $email): self
|
||||
{
|
||||
$this->smtp['replyTo'] = $replyTo;
|
||||
$this->smtp['replyToEmail'] = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set SMTP reply-to name
|
||||
*
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpReplyToName(string $name): self
|
||||
{
|
||||
$this->smtp['replyToName'] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -333,13 +345,23 @@ class Mail extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SMTP reply to
|
||||
* Get SMTP reply-to email
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSmtpReplyTo(): string
|
||||
public function getSmtpReplyToEmail(): string
|
||||
{
|
||||
return $this->smtp['replyTo'] ?? '';
|
||||
return $this->smtp['replyToEmail'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SMTP reply-to name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSmtpReplyToName(): string
|
||||
{
|
||||
return $this->smtp['replyToName'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,7 @@ class Event extends Validator
|
||||
/**
|
||||
* Identify all sections of the pattern.
|
||||
*/
|
||||
$type = $parts[0] ?? false;
|
||||
$type = $parts[0];
|
||||
$resource = $parts[1] ?? false;
|
||||
$hasSubResource = $count > 3 && ($events[$type]['$resource'] ?? false) && ($events[$type][$parts[2]]['$resource'] ?? false);
|
||||
$hasSubSubResource = $count > 5 && $hasSubResource && ($events[$type][$parts[2]][$parts[4]]['$resource'] ?? false);
|
||||
@@ -61,9 +61,6 @@ class Event extends Validator
|
||||
if ($hasSubSubResource) {
|
||||
$subSubType = $parts[4];
|
||||
$subSubResource = $parts[5];
|
||||
if ($count === 8) {
|
||||
$attribute = $parts[7];
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasSubResource && !$hasSubSubResource) {
|
||||
|
||||
@@ -24,7 +24,7 @@ class Webhook extends Event
|
||||
public function trimPayload(): array
|
||||
{
|
||||
$trimmed = parent::trimPayload();
|
||||
if (isset($this->context)) {
|
||||
if (!empty($this->context)) {
|
||||
$trimmed['context'] = [];
|
||||
}
|
||||
return $trimmed;
|
||||
|
||||
@@ -91,26 +91,20 @@ class Mapper
|
||||
}
|
||||
}
|
||||
|
||||
$responses = $method->getResponses() ?? [];
|
||||
$responses = $method->getResponses();
|
||||
|
||||
// If responses is an array, map each response to its model
|
||||
if (\is_array($responses)) {
|
||||
$models = [];
|
||||
foreach ($responses as $response) {
|
||||
$modelName = $response->getModel();
|
||||
// Map each response to its model
|
||||
$models = [];
|
||||
foreach ($responses as $response) {
|
||||
$modelName = $response->getModel();
|
||||
|
||||
if (\is_array($modelName)) {
|
||||
foreach ($modelName as $name) {
|
||||
$models[] = self::$models[$name];
|
||||
}
|
||||
} else {
|
||||
$models[] = self::$models[$modelName];
|
||||
if (\is_array($modelName)) {
|
||||
foreach ($modelName as $name) {
|
||||
$models[] = self::$models[$name];
|
||||
}
|
||||
} else {
|
||||
$models[] = self::$models[$modelName];
|
||||
}
|
||||
} else {
|
||||
// If single response, get its model and wrap in array
|
||||
$modelName = $responses->getModel();
|
||||
$models = [self::$models[$modelName]];
|
||||
}
|
||||
|
||||
foreach ($models as $model) {
|
||||
|
||||
@@ -452,6 +452,7 @@ class Realtime extends MessagingAdapter
|
||||
* Reserved channel params with expected type
|
||||
* If matched the expected type then skip the query parsing like in project
|
||||
*/
|
||||
/** @var array<string, 'array'|'string'> $reservedParamExpectedTypes */
|
||||
$reservedParamExpectedTypes = [
|
||||
'project' => 'string',
|
||||
];
|
||||
@@ -465,7 +466,6 @@ class Realtime extends MessagingAdapter
|
||||
$isExpectedType = match ($expectedType) {
|
||||
'array' => \is_array($params),
|
||||
'string' => \is_string($params),
|
||||
default => false,
|
||||
};
|
||||
|
||||
// If the value matches the expected type dont use it the queries
|
||||
|
||||
@@ -94,6 +94,7 @@ abstract class Migration
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
'1.9.2' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -262,7 +262,7 @@ class V17 extends Migration
|
||||
* Set default maxSessions
|
||||
*/
|
||||
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT
|
||||
'maxSessions' => 10
|
||||
]));
|
||||
break;
|
||||
case 'users':
|
||||
|
||||
@@ -16,7 +16,7 @@ class OpenSSL
|
||||
* @param string $aad
|
||||
* @param int $tag_length
|
||||
*
|
||||
* @return string
|
||||
* @return string|false
|
||||
*/
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
|
||||
{
|
||||
|
||||
@@ -240,9 +240,7 @@ class Install extends Action
|
||||
$inputValue = trim($inputValue);
|
||||
}
|
||||
if ($storedValue !== $inputValue) {
|
||||
if ($installId !== '') {
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Installation payload mismatch');
|
||||
return;
|
||||
}
|
||||
@@ -262,16 +260,12 @@ class Install extends Action
|
||||
$incomingHash = $state->hashSensitiveValue($incomingValue);
|
||||
if (isset($stored[$hashField])) {
|
||||
if (!hash_equals((string) $stored[$hashField], $incomingHash)) {
|
||||
if ($installId !== '') {
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Installation payload mismatch');
|
||||
return;
|
||||
}
|
||||
} elseif (isset($stored[$field]) && $incomingValue !== '' && (string) $stored[$field] !== $incomingValue) {
|
||||
if ($installId !== '') {
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Installation payload mismatch');
|
||||
return;
|
||||
}
|
||||
@@ -430,7 +424,7 @@ class Install extends Action
|
||||
private function deriveNameFromEmail(string $email): string
|
||||
{
|
||||
$parts = explode('@', $email);
|
||||
$username = $parts[0] ?? '';
|
||||
$username = $parts[0];
|
||||
$cleaned = preg_replace('/[^a-zA-Z0-9]/', '', $username);
|
||||
return ucfirst($cleaned);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class Status extends Action
|
||||
}
|
||||
|
||||
$data = $state->readProgressFile($installId);
|
||||
if (is_array($data) && isset($data['payload']) && is_array($data['payload'])) {
|
||||
if (isset($data['payload']) && is_array($data['payload'])) {
|
||||
unset(
|
||||
$data['payload']['opensslKey'],
|
||||
$data['payload']['assistantOpenAIKey'],
|
||||
@@ -54,7 +54,7 @@ class Status extends Action
|
||||
);
|
||||
}
|
||||
// Strip sensitive data from step details
|
||||
if (is_array($data) && isset($data['details']) && is_array($data['details'])) {
|
||||
if (isset($data['details']) && is_array($data['details'])) {
|
||||
foreach ($data['details'] as $stepKey => &$stepDetails) {
|
||||
if (is_array($stepDetails)) {
|
||||
unset($stepDetails['sessionSecret'], $stepDetails['trace']);
|
||||
|
||||
@@ -218,7 +218,7 @@ final class Config
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $value
|
||||
* @param array<mixed> $value
|
||||
*/
|
||||
public function setEnabledDatabases(array $value): void
|
||||
{
|
||||
|
||||
@@ -19,13 +19,11 @@ class State
|
||||
private const int PORT_MIN = 1;
|
||||
private const int PORT_MAX = 65535;
|
||||
|
||||
private array $paths;
|
||||
private bool $bootstrapped = false;
|
||||
private int $lastStaleLockClearAt = 0;
|
||||
|
||||
public function __construct(array $paths)
|
||||
public function __construct()
|
||||
{
|
||||
$this->paths = $paths;
|
||||
}
|
||||
|
||||
public function buildConfig(array $overrides = [], bool $useEnv = true): Config
|
||||
@@ -180,7 +178,7 @@ class State
|
||||
if (!preg_match(self::PATTERN_IPV6_WITH_PORT, $value, $matches)) {
|
||||
return false;
|
||||
}
|
||||
$host = $matches[1] ?? '';
|
||||
$host = $matches[1];
|
||||
$port = $matches[2] ?? null;
|
||||
} else {
|
||||
$parts = explode(':', $value);
|
||||
|
||||
@@ -60,7 +60,7 @@ class Server
|
||||
{
|
||||
$this->initPaths();
|
||||
|
||||
$this->state = new State($this->paths);
|
||||
$this->state = new State();
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$this->runCli();
|
||||
|
||||
@@ -47,7 +47,7 @@ class AppDomain extends Validator
|
||||
if (!preg_match(self::PATTERN_IPV6_WITH_PORT, $value, $matches)) {
|
||||
return false;
|
||||
}
|
||||
$host = $matches[1] ?? '';
|
||||
$host = $matches[1];
|
||||
$port = $matches[2] ?? null;
|
||||
} else {
|
||||
$parts = explode(':', $value);
|
||||
|
||||
@@ -37,8 +37,8 @@ class Delete extends Action
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('audits.userId', '{user.$id}')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
||||
@@ -250,7 +250,8 @@ class Create extends Action
|
||||
|
||||
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
$replyTo = "";
|
||||
$replyToEmail = '';
|
||||
$replyToName = '';
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
@@ -259,8 +260,13 @@ class Create extends Action
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
if (!empty($smtpReplyToEmail)) {
|
||||
$replyToEmail = $smtpReplyToEmail;
|
||||
}
|
||||
if (!empty($smtp['replyToName'])) {
|
||||
$replyToName = $smtp['replyToName'];
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
@@ -277,8 +283,13 @@ class Create extends Action
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
|
||||
if (!empty($customReplyToEmail)) {
|
||||
$replyToEmail = $customReplyToEmail;
|
||||
}
|
||||
if (!empty($customTemplate['replyToName'])) {
|
||||
$replyToName = $customTemplate['replyToName'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
@@ -286,7 +297,8 @@ class Create extends Action
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ class Get extends Action
|
||||
}
|
||||
|
||||
if (!$isEmployee && !empty($githubName)) {
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub'] ?? ''), $employees));
|
||||
if (!empty($employeeGitHub)) {
|
||||
$isEmployee = true;
|
||||
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
|
||||
$employeeNumber = $employees[$employeeGitHub]['spot'];
|
||||
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,10 @@ class Get extends Action
|
||||
}
|
||||
|
||||
if (!$isEmployee && !empty($githubName)) {
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub'] ?? ''), $employees));
|
||||
if (!empty($employeeGitHub)) {
|
||||
$isEmployee = true;
|
||||
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
|
||||
$employeeNumber = $employees[$employeeGitHub]['spot'];
|
||||
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class Get extends Action
|
||||
$doc->strictErrorChecking = false;
|
||||
@$doc->loadHTML($res->getBody());
|
||||
|
||||
$links = $doc->getElementsByTagName('link') ?? [];
|
||||
$links = $doc->getElementsByTagName('link');
|
||||
$outputHref = '';
|
||||
$outputExt = '';
|
||||
$space = 0;
|
||||
@@ -128,7 +128,7 @@ class Get extends Action
|
||||
case 'jpeg':
|
||||
$size = \explode('x', \strtolower($sizes));
|
||||
|
||||
$sizeWidth = (int) ($size[0] ?? 0);
|
||||
$sizeWidth = (int) $size[0];
|
||||
$sizeHeight = (int) ($size[1] ?? 0);
|
||||
|
||||
if (($sizeWidth * $sizeHeight) >= $space) {
|
||||
|
||||
@@ -60,7 +60,6 @@ class Get extends Action
|
||||
|
||||
public function action(string $text, int $size, int $margin, bool $download, Response $response)
|
||||
{
|
||||
$download = ($download === '1' || $download === 'true' || $download === 1 || $download === true);
|
||||
$options = new QROptions([
|
||||
'addQuietzone' => true,
|
||||
'quietzoneSize' => $margin,
|
||||
|
||||
@@ -105,7 +105,7 @@ class Get extends Action
|
||||
$client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
|
||||
|
||||
// Convert indexed array to empty array (should not happen due to Assoc validator)
|
||||
if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) {
|
||||
if (count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) {
|
||||
$headers = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class Base extends Action
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$providerRepositoryId = $function->getAttribute('providerRepositoryId', '');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class Base extends Action
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$providerRepositoryId = $site->getAttribute('providerRepositoryId', '');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ abstract class Action extends DatabasesAction
|
||||
/**
|
||||
* The current API context (either 'table' or 'collection').
|
||||
*/
|
||||
private ?string $context = COLLECTIONS;
|
||||
private string $context = COLLECTIONS;
|
||||
|
||||
/**
|
||||
* Get the response model used in the SDK and HTTP responses.
|
||||
|
||||
+2
-2
@@ -26,9 +26,9 @@ use Utopia\Validator\Range;
|
||||
abstract class Action extends UtopiaAction
|
||||
{
|
||||
/**
|
||||
* @var string|null The current context (either 'column' or 'attribute')
|
||||
* @var string The current context (either 'column' or 'attribute')
|
||||
*/
|
||||
private ?string $context = ATTRIBUTES;
|
||||
private string $context = ATTRIBUTES;
|
||||
|
||||
/**
|
||||
* Get the correct response model.
|
||||
|
||||
+3
-3
@@ -14,10 +14,10 @@ use Utopia\Database\Validator\Authorization;
|
||||
abstract class Action extends DatabasesAction
|
||||
{
|
||||
/**
|
||||
* @var string|null The current context (either 'row' or 'document')
|
||||
* @var string The current context (either 'row' or 'document')
|
||||
*/
|
||||
private ?string $context = DOCUMENTS;
|
||||
private ?string $databaseType = DATABASE_TYPE_LEGACY;
|
||||
private string $context = DOCUMENTS;
|
||||
private string $databaseType = DATABASE_TYPE_LEGACY;
|
||||
|
||||
/**
|
||||
* Get the response model used in the SDK and HTTP responses.
|
||||
|
||||
-10
@@ -293,16 +293,6 @@ class Create extends Action
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
|
||||
}
|
||||
|
||||
if ($permission === Database::PERMISSION_UPDATE) {
|
||||
$validDocument = $authorization->isValid(
|
||||
new Input($permission, $document->getUpdate())
|
||||
);
|
||||
$valid = $validCollection || $validDocument;
|
||||
if ($documentSecurity && !$valid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
$relationships = \array_filter(
|
||||
$collection->getAttribute('attributes', []),
|
||||
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ class Get extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$selects = Query::groupByType($queries)['selections'] ?? [];
|
||||
$selects = Query::groupByType($queries)['selections'];
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
|
||||
+1
-6
@@ -353,12 +353,7 @@ class Upsert extends Action
|
||||
$collectionsCache = [];
|
||||
|
||||
if (empty($upserted[0])) {
|
||||
if ($transactionId !== null) {
|
||||
// For transactions, get the document with transaction changes applied
|
||||
$upserted[0] = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$upserted[0] = $dbForDatabases->getDocument($collectionTableId, $documentId);
|
||||
}
|
||||
$upserted[0] = $dbForDatabases->getDocument($collectionTableId, $documentId);
|
||||
}
|
||||
|
||||
$document = $upserted[0];
|
||||
|
||||
+24
-3
@@ -22,6 +22,7 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
@@ -80,10 +81,11 @@ class XList extends Action
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('utopia')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, User $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, User $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, ?Http $utopia = null): void
|
||||
{
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
@@ -126,8 +128,10 @@ class XList extends Action
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$dbStart = \microtime(true);
|
||||
|
||||
try {
|
||||
$hasSelects = ! empty(Query::groupByType($queries)['selections'] ?? []);
|
||||
$hasSelects = ! empty(Query::groupByType($queries)['selections']);
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
// When there are no select queries, relationship loading is skipped on the
|
||||
// underlying find() to avoid pulling related documents the caller did not ask for.
|
||||
@@ -178,7 +182,7 @@ class XList extends Action
|
||||
$cachedTotal = null;
|
||||
}
|
||||
if ($cachedTotal !== null && $cachedTotal !== false) {
|
||||
$total = $cachedTotal;
|
||||
$total = (int) $cachedTotal;
|
||||
} else {
|
||||
$total = $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
try {
|
||||
@@ -206,6 +210,8 @@ class XList extends Action
|
||||
throw new Exception(Exception::DATABASE_TIMEOUT);
|
||||
}
|
||||
|
||||
$dbDurationMs = (\microtime(true) - $dbStart) * 1000;
|
||||
|
||||
$operations = 0;
|
||||
$collectionsCache = [];
|
||||
foreach ($documents as $document) {
|
||||
@@ -229,5 +235,20 @@ class XList extends Action
|
||||
// rows or documents
|
||||
$this->getSDKGroup() => $documents,
|
||||
]), $this->getResponseModel());
|
||||
|
||||
try {
|
||||
$this->afterQuery($dbDurationMs, $database, $collection, $queries, $utopia);
|
||||
} catch (\Throwable) {
|
||||
// Observers must never break the response.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After query hook.
|
||||
*
|
||||
* @param array<Query> $queries
|
||||
*/
|
||||
protected function afterQuery(float $dbDurationMs, Document $database, Document $collection, array $queries, ?Http $utopia): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ abstract class Action extends UtopiaAction
|
||||
/**
|
||||
* The current API context (either 'columnIndex' or 'index').
|
||||
*/
|
||||
private ?string $context = INDEX;
|
||||
private string $context = INDEX;
|
||||
|
||||
/**
|
||||
* Get the response model used in the SDK and HTTP responses.
|
||||
|
||||
@@ -119,6 +119,7 @@ class Get extends Action
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new \LogicException('Unexpected period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
@@ -100,9 +100,9 @@ class XList extends Action
|
||||
$os = $detector->getOS();
|
||||
$client = $detector->getClient();
|
||||
$device = $detector->getDevice();
|
||||
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
|
||||
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
|
||||
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
|
||||
$deviceName = $device['deviceName'] ?? '';
|
||||
$deviceBrand = $device['deviceBrand'] ?? '';
|
||||
$deviceModel = $device['deviceModel'] ?? '';
|
||||
|
||||
$output[$i] = new Document([
|
||||
'event' => $log['event'],
|
||||
|
||||
@@ -9,8 +9,8 @@ abstract class Action extends DatabasesAction
|
||||
/**
|
||||
* The current API context (either 'table' or 'collection').
|
||||
*/
|
||||
private ?string $context = COLLECTIONS;
|
||||
private ?string $databaseType = LEGACY;
|
||||
private string $context = COLLECTIONS;
|
||||
private string $databaseType = LEGACY;
|
||||
|
||||
public function getDatabaseType(): string
|
||||
{
|
||||
|
||||
@@ -144,6 +144,7 @@ class Get extends Action
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new \LogicException('Unexpected period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
@@ -133,6 +133,7 @@ class XList extends Action
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new \LogicException('Unexpected period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
+1
@@ -63,6 +63,7 @@ class XList extends DocumentXList
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('utopia')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ class XList extends Action
|
||||
$os = $detector->getOS();
|
||||
$client = $detector->getClient();
|
||||
$device = $detector->getDevice();
|
||||
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
|
||||
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
|
||||
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
|
||||
$deviceName = $device['deviceName'] ?? '';
|
||||
$deviceBrand = $device['deviceBrand'] ?? '';
|
||||
$deviceModel = $device['deviceModel'] ?? '';
|
||||
|
||||
$output[$i] = new Document([
|
||||
'event' => $log['event'],
|
||||
|
||||
@@ -65,6 +65,7 @@ class XList extends DocumentXList
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('utopia')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class Create extends CreateDocumentAction
|
||||
$error = '';
|
||||
try {
|
||||
$embedResult = $embeddingAgent->embed($text);
|
||||
$embedding = $embedResult['embedding'] ?? [];
|
||||
$embedding = $embedResult['embedding'];
|
||||
$totalDuration += $embedResult['totalDuration'] ?? 0;
|
||||
$totalTokens += $embedResult['tokensProcessed'] ?? 0;
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -54,7 +54,7 @@ class Databases extends Action
|
||||
*/
|
||||
public function action(Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, callable $getDatabasesDB, Realtime $queueForRealtime, Log $log): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$payload = $message->getPayload();
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
|
||||
@@ -206,6 +206,12 @@ class Create extends Action
|
||||
if ($chunk === -1) {
|
||||
$chunk = $chunks;
|
||||
}
|
||||
} else {
|
||||
// Guard against manually setting range header for single chunk upload
|
||||
if ($chunks === -1) {
|
||||
$chunks = 1;
|
||||
$chunk = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
|
||||
|
||||
@@ -116,7 +116,7 @@ class XList extends Base
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$filterQueries = $grouped['filters'];
|
||||
$selectQueries = $grouped['selections'] ?? [];
|
||||
$selectQueries = $grouped['selections'];
|
||||
|
||||
try {
|
||||
$results = $dbForProject->find('deployments', $queries);
|
||||
|
||||
@@ -145,21 +145,8 @@ class Create extends Base
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array<string, mixed> $headers
|
||||
*/
|
||||
$assocParams = ['headers'];
|
||||
foreach ($assocParams as $assocParam) {
|
||||
if (!empty('headers') && !is_array($$assocParam)) {
|
||||
$$assocParam = \json_decode($$assocParam, true);
|
||||
}
|
||||
}
|
||||
|
||||
$booleanParams = ['async'];
|
||||
foreach ($booleanParams as $booleamParam) {
|
||||
if (!empty($$booleamParam) && !is_bool($$booleamParam)) {
|
||||
$$booleamParam = $$booleamParam === "true" ? true : false;
|
||||
}
|
||||
if (!is_array($headers)) {
|
||||
$headers = \json_decode($headers, true);
|
||||
}
|
||||
|
||||
// 'headers' validator
|
||||
@@ -366,10 +353,10 @@ class Create extends Base
|
||||
// V2 vars
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'],
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'],
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ class Create extends Base
|
||||
}
|
||||
|
||||
$functionsDomain = $platform['functionsDomain'];
|
||||
if (!empty($functionsDomain)) {
|
||||
if (!empty($functionsDomain) && isset($deployment) && !$deployment->isEmpty()) {
|
||||
$routeSubdomain = ID::unique();
|
||||
$domain = "{$routeSubdomain}.{$functionsDomain}";
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
@@ -391,8 +391,8 @@ class Create extends Base
|
||||
'status' => 'verified',
|
||||
'type' => 'deployment',
|
||||
'trigger' => 'manual',
|
||||
'deploymentId' => !isset($deployment) || $deployment->isEmpty() ? '' : $deployment->getId(),
|
||||
'deploymentInternalId' => !isset($deployment) || $deployment->isEmpty() ? '' : $deployment->getSequence(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
'deploymentResourceType' => 'function',
|
||||
'deploymentResourceId' => $function->getId(),
|
||||
'deploymentResourceInternalId' => $function->getSequence(),
|
||||
|
||||
@@ -162,10 +162,6 @@ class Update extends Base
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($runtime)) {
|
||||
$runtime = $function->getAttribute('runtime');
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ class Get extends Base
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unsupported period "' . $days['period'] . '".'),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Usage;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -104,6 +105,7 @@ class XList extends Base
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unsupported period "' . $days['period'] . '".'),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
@@ -77,11 +77,7 @@ class Delete extends Base
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getSequence() || $variable->getAttribute('resourceType') !== 'function') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
if ($variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getSequence() || $variable->getAttribute('resourceType') !== 'function') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ class Get extends Base
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if (
|
||||
$variable === false ||
|
||||
$variable->isEmpty() ||
|
||||
$variable->getAttribute('resourceInternalId') !== $function->getSequence() ||
|
||||
$variable->getAttribute('resourceType') !== 'function'
|
||||
@@ -74,10 +73,6 @@ class Get extends Base
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class Update extends Base
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getSequence() || $variable->getAttribute('resourceType') !== 'function') {
|
||||
if ($variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getSequence() || $variable->getAttribute('resourceType') !== 'function') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class Builds extends Action
|
||||
): void {
|
||||
Console::log('Build action started');
|
||||
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$payload = $message->getPayload();
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new \Exception('Missing payload');
|
||||
@@ -206,7 +206,7 @@ class Builds extends Action
|
||||
throw new \Exception('Resource not found');
|
||||
}
|
||||
|
||||
if ($isResourceBlocked($project, $resourceKey === 'functions' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) {
|
||||
if ($isResourceBlocked($project, $resource->getCollection() === 'functions' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) {
|
||||
throw new \Exception('Resource is blocked');
|
||||
}
|
||||
|
||||
@@ -226,10 +226,6 @@ class Builds extends Action
|
||||
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
if ($resource->getCollection() === 'functions' && \is_null($runtime)) {
|
||||
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
// Realtime preparation
|
||||
$event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update";
|
||||
$queueForRealtime
|
||||
@@ -829,7 +825,8 @@ class Builds extends Action
|
||||
|
||||
Console::log('Runtime creation finished');
|
||||
|
||||
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
|
||||
$latestDeployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
if ($latestDeployment->getAttribute('status') === 'canceled') {
|
||||
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
|
||||
|
||||
return;
|
||||
@@ -1259,21 +1256,6 @@ class Builds extends Action
|
||||
*/
|
||||
protected function afterBuildSuccess(Realtime $queueForRealtime, Database $dbForProject, Document &$deployment, array $runtime, ?string $adapter): void
|
||||
{
|
||||
if (! ($queueForRealtime instanceof Realtime)) {
|
||||
throw new Exception('queueForRealtime must be an instance of Realtime');
|
||||
}
|
||||
if (! ($dbForProject instanceof Database)) {
|
||||
throw new Exception('dbForProject must be an instance of Database');
|
||||
}
|
||||
if (! ($deployment instanceof Document)) {
|
||||
throw new Exception('deployment must be an instance of Document');
|
||||
}
|
||||
if (! is_array($runtime)) {
|
||||
throw new Exception('runtime must be an array');
|
||||
}
|
||||
if (! is_string($adapter) && ! is_null($adapter)) {
|
||||
throw new Exception('adapter must be a string or null');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1283,13 +1265,6 @@ class Builds extends Action
|
||||
Document $project,
|
||||
Document $deployment,
|
||||
): void {
|
||||
if (! ($project instanceof Document)) {
|
||||
throw new Exception('project must be an instance of Document');
|
||||
}
|
||||
|
||||
if (! ($deployment instanceof Document)) {
|
||||
throw new Exception('deployment must be an instance of Document');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRuntime(Document $resource, string $version): array
|
||||
@@ -1313,6 +1288,7 @@ class Builds extends Action
|
||||
return match ($resource->getCollection()) {
|
||||
'functions' => $resource->getAttribute('version', 'v2'),
|
||||
'sites' => 'v5',
|
||||
default => throw new \Exception('Unsupported resource type "' . $resource->getCollection() . '".'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1445,11 +1421,10 @@ class Builds extends Action
|
||||
]);
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = match ($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
$previewUrl = '';
|
||||
if ($resource->getCollection() === 'sites' && !$rule->isEmpty()) {
|
||||
$previewUrl = "{$protocol}://" . $rule->getAttribute('domain', '');
|
||||
}
|
||||
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
|
||||
|
||||
@@ -20,6 +20,8 @@ use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Counter;
|
||||
|
||||
use function Swoole\Coroutine\batch;
|
||||
|
||||
@@ -44,6 +46,7 @@ class Screenshots extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('deviceForFiles')
|
||||
->inject('telemetry')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -53,17 +56,19 @@ class Screenshots extends Action
|
||||
Database $dbForPlatform,
|
||||
Database $dbForProject,
|
||||
Document $project,
|
||||
Device $deviceForFiles
|
||||
Device $deviceForFiles,
|
||||
Telemetry $telemetry
|
||||
): void {
|
||||
Console::log('Screenshot action started');
|
||||
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$payload = $message->getPayload();
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new \Exception('Missing payload');
|
||||
}
|
||||
|
||||
$screenshotMessage = Screenshot::fromArray($payload);
|
||||
$counter = $telemetry->createCounter('worker.screenshots.capture');
|
||||
|
||||
Console::log('Site screenshot started');
|
||||
|
||||
@@ -162,7 +167,7 @@ class Screenshots extends Action
|
||||
try {
|
||||
$config = $configs[$key];
|
||||
|
||||
$config['headers'] = \array_merge($config['headers'] ?? [], [
|
||||
$config['headers'] = \array_merge($config['headers'], [
|
||||
'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey
|
||||
]);
|
||||
$config['sleep'] = 3000;
|
||||
@@ -268,8 +273,24 @@ class Screenshots extends Action
|
||||
$date = \date('H:i:s');
|
||||
$this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[90m[$date] [90m[[0mappwrite[90m][33m Screenshot capturing failed. Deployment will continue. [0m\n");
|
||||
|
||||
$this->recordTelemetry($counter, 'failure');
|
||||
|
||||
throw $th;
|
||||
}
|
||||
|
||||
$this->recordTelemetry($counter, 'success');
|
||||
}
|
||||
|
||||
protected function recordTelemetry(Counter $counter, string $result): void
|
||||
{
|
||||
try {
|
||||
$counter->add(1, [
|
||||
'resourceType' => RESOURCE_TYPE_SITES,
|
||||
'result' => $result,
|
||||
]);
|
||||
} catch (\Throwable) {
|
||||
// Telemetry should never affect screenshot processing.
|
||||
}
|
||||
}
|
||||
|
||||
protected function appendToLogs(Database $dbForProject, string $deploymentId, Realtime $queueForRealtime, string $logs)
|
||||
|
||||
@@ -82,7 +82,7 @@ class Get extends Action
|
||||
}
|
||||
|
||||
$certificatePayload = @openssl_x509_parse($peerCertificate);
|
||||
if ($certificatePayload === false || !\is_array($certificatePayload)) {
|
||||
if ($certificatePayload === false) {
|
||||
throw new Exception(Exception::HEALTH_INVALID_HOST, 'Failed to parse peer certificate for ' . $domain);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use Appwrite\Event\Publisher\Screenshot;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -123,6 +124,7 @@ class Get extends Base
|
||||
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots,
|
||||
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
|
||||
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unknown queue name: ' . $name),
|
||||
};
|
||||
$failed = $queue->getSize(failed: true);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -31,7 +32,7 @@ class Update extends Action
|
||||
->desc('Update project labels')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'labels.*.update')
|
||||
// ->label('event', 'project.labels.update')
|
||||
->label('audits.event', 'project.labels.update')
|
||||
->label('audits.resource', 'project.labels/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
@@ -53,6 +54,7 @@ class Update extends Action
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -63,11 +65,12 @@ class Update extends Action
|
||||
array $labels,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
): void {
|
||||
$labels = (array) \array_values(\array_unique($labels));
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class Delete extends Action
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].delete')
|
||||
->label('audits.event', 'project.platform.delete')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('audits.resource', 'project.platform/{request.platformId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
|
||||
@@ -139,7 +139,7 @@ class Create extends Action
|
||||
if (empty($key) && empty($type)) {
|
||||
// Modern request, validate hostname
|
||||
if (empty($hostname)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "hostname" is not optional.');
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Param "hostname" is not optional.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\MembershipPrivacy;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectMembershipPrivacyPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/membership-privacy')
|
||||
->httpAlias('/v1/projects/:projectId/auth/memberships-privacy')
|
||||
->desc('Update membership privacy policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updateMembershipPrivacyPolicy',
|
||||
description: <<<EOT
|
||||
Updating this policy allows you to control if team members can see other members information. When enabled, all team members can see ID, name, email, phone number, and MFA status of other members..
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('userId', null, new Boolean(), 'Set to true if you want make user ID visible to all team members, or false to hide it.', optional: true)
|
||||
->param('userEmail', null, new Boolean(), 'Set to true if you want make user email visible to all team members, or false to hide it.', optional: true)
|
||||
->param('userPhone', null, new Boolean(), 'Set to true if you want make user phone number visible to all team members, or false to hide it.', optional: true)
|
||||
->param('userName', null, new Boolean(), 'Set to true if you want make user name visible to all team members, or false to hide it.', optional: true)
|
||||
->param('userMFA', null, new Boolean(), 'Set to true if you want make user MFA status visible to all team members, or false to hide it.', optional: true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
?bool $userId,
|
||||
?bool $userEmail,
|
||||
?bool $userPhone,
|
||||
?bool $userName,
|
||||
?bool $userMFA,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
|
||||
if ($userId !== null) {
|
||||
$auths['membershipsUserId'] = $userId;
|
||||
}
|
||||
if ($userEmail !== null) {
|
||||
$auths['membershipsUserEmail'] = $userEmail;
|
||||
}
|
||||
if ($userPhone !== null) {
|
||||
$auths['membershipsUserPhone'] = $userPhone;
|
||||
}
|
||||
if ($userName !== null) {
|
||||
$auths['membershipsUserName'] = $userName;
|
||||
}
|
||||
if ($userMFA !== null) {
|
||||
$auths['membershipsMfa'] = $userMFA;
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'membership-privacy');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\PasswordDictionary;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectPasswordDictionaryPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/password-dictionary')
|
||||
->httpAlias('/v1/projects/:projectId/auth/password-dictionary')
|
||||
->desc('Update password dictionary policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updatePasswordDictionaryPolicy',
|
||||
description: <<<EOT
|
||||
Updating this policy allows you to control if new passwords are checked against most common passwords dictionary. When enabled, and user changes their password, password must not be contained in the dictionary.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('enabled', null, new Boolean(), 'Toggle password dictionary policy. Set to true if you want password change to block passwords in the dictionary, or false to allow them. When changing this policy, existing passwords remain valid.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['passwordDictionary'] = $enabled;
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'password-dictionary');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\PasswordHistory;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectPasswordHistoryPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/password-history')
|
||||
->httpAlias('/v1/projects/:projectId/auth/password-history')
|
||||
->desc('Update password history policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updatePasswordHistoryPolicy',
|
||||
description: <<<EOT
|
||||
Updates one of password strength policies. Based on total length configured, previous password hashes are stored, and users cannot choose a new password that is already stored in the passwird history list, when updating an user password, or setting new one through password recovery.
|
||||
|
||||
Keep in mind, while password history policy is disabled, the history is not being stored. Enabling the policy will not have any history on existing users, and it will only start to collect and enforce the policy on password changes since the policy is enabled.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('total', null, new Nullable(new Range(1, APP_LIMIT_COUNT)), 'Set the password history length per user. Value can be between 1 and ' . APP_LIMIT_COUNT . ', or null to disable the limit.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
?int $total,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
|
||||
if (\is_null($total)) {
|
||||
$auths['passwordHistory'] = 0;
|
||||
} else {
|
||||
$auths['passwordHistory'] = $total;
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'password-history');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\PasswordPersonalData;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectPasswordPersonalDataPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/password-personal-data')
|
||||
->httpAlias('/v1/projects/:projectId/auth/personal-data')
|
||||
->desc('Update password personal data policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updatePasswordPersonalDataPolicy',
|
||||
description: <<<EOT
|
||||
Updating this policy allows you to control if password strength is checked against personal data. When enabled, and user sets or changes their password, the password must not contain user ID, name, email or phone number.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
// TODO: Split into more toggles, simiplar to membership privacy policy
|
||||
->param('enabled', null, new Boolean(), 'Toggle password personal data policy. Set to true if you want to block passwords including user\'s personal data, or false to allow it. When changing this policy, existing passwords remain valid.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['personalDataCheck'] = $enabled;
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'password-personal-data');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionAlert;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectSessionAlertPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/session-alert')
|
||||
->httpAlias('/v1/projects/:projectId/auth/session-alerts')
|
||||
->desc('Update session alert policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updateSessionAlertPolicy',
|
||||
description: <<<EOT
|
||||
Updating this policy allows you to control if email alert is sent upon session creation. When enabled, and user signs into their account, they will be sent an email notification. There is an exception, the first session after a new sign up does not trigger an alert, even if the policy is enabled.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('enabled', null, new Boolean(), 'Toggle session alert policy. Set to true if you want users to receive email notifications when a sessions are created for their users, or false to not send email alerts.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['sessionAlerts'] = $enabled;
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'session-alert');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionDuration;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectSessionDurationPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/session-duration')
|
||||
->httpAlias('/v1/projects/:projectId/auth/duration')
|
||||
->desc('Update session duration policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updateSessionDurationPolicy',
|
||||
description: <<<EOT
|
||||
Update maximum duration how long sessions created within a project should stay active for.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('duration', null, new Range(5, 31536000), 'Maximum session length in seconds. Minium allowed value is 5 second, and maximum is 1 year, which is 31536000 seconds.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
int $duration,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['duration'] = $duration;
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'session-duration');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionInvalidation;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectSessionInvalidationPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/session-invalidation')
|
||||
->httpAlias('/v1/projects/:projectId/auth/session-invalidation')
|
||||
->desc('Update session invalidation policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updateSessionInvalidationPolicy',
|
||||
description: <<<EOT
|
||||
Updating this policy allows you to control if existing sessions should be invalidated when a password of a user is changed. When enabled, and user changes their password, they will be logged out of all their devices.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('enabled', null, new Boolean(), 'Toggle session invalidation policy. Set to true if you want password change to invalidate all sessions of an user, or false to keep sessions active.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['invalidateSessions'] = $enabled;
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'session-invalidation');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionLimit;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectSessionLimitPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/session-limit')
|
||||
->httpAlias('/v1/projects/:projectId/auth/max-sessions')
|
||||
->desc('Update session limit policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updateSessionLimitPolicy',
|
||||
description: <<<EOT
|
||||
Update the maximum number of sessions allowed per user. When the limit is hit, the oldest session will be deleted to make room for new one.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('total', null, new Nullable(new Range(1, APP_LIMIT_COUNT)), 'Set the maximum number of sessions allowed per user. Value can be between 1 and ' . APP_LIMIT_COUNT . ', or null to disable the limit.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
?int $total,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
|
||||
if (\is_null($total)) {
|
||||
$auths['maxSessions'] = 0;
|
||||
} else {
|
||||
$auths['maxSessions'] = $total;
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'session-limit');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies\UserLimit;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectUserLimitPolicy';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/policies/user-limit')
|
||||
->httpAlias('/v1/projects/:projectId/auth/limit')
|
||||
->desc('Update user limit policy')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'policies.write')
|
||||
->label('event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.event', 'projects.[projectId].policies.[policy].update')
|
||||
->label('audits.resource', 'project/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'policies',
|
||||
name: 'updateUserLimitPolicy',
|
||||
description: <<<EOT
|
||||
Update the maximum number of users in the project. When the limit is hit or amount of existing users already exceeded the limit, all users remain active, but new user sign up will be prohibited.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('total', null, new Nullable(new Range(1, APP_LIMIT_COUNT)), 'Set the maximum number of users allowed in the project. Value can be between 1 and ' . APP_LIMIT_COUNT . ', or null to disable the limit.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
?int $total,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
|
||||
if (\is_null($total)) {
|
||||
$auths['limit'] = 0;
|
||||
} else {
|
||||
$auths['limit'] = $total;
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'auths' => $auths,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('policy', 'user-limit');
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests;
|
||||
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception as Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectSMTPTest';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/smtp/tests')
|
||||
->httpAlias('/v1/projects/:projectId/smtp/tests')
|
||||
->desc('Create project SMTP test')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'smtp',
|
||||
name: 'createSMTPTest',
|
||||
description: <<<EOT
|
||||
Send a test email to verify SMTP configuration.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_NOCONTENT,
|
||||
model: UtopiaResponse::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE,
|
||||
))
|
||||
->param('emails', [], new ArrayList(new Email(), 10), 'Array of emails to send test email to. Maximum of 10 emails are allowed.')
|
||||
->param('senderName', '', new Text(256), 'Name of the email sender', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('senderEmail', '', new Email(), 'Email of the sender', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('replyTo', '', new Email(), 'Reply to email', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('host', '', new Hostname(), 'SMTP server host name', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('port', null, new Integer(), 'SMTP server port', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('username', '', new Text(256), 'SMTP server username', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('password', '', new Text(256), 'SMTP server password', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', optional: true, deprecated: true) // Backwards compatibility
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForMails')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $emails
|
||||
*/
|
||||
public function action(
|
||||
array $emails,
|
||||
string $paramSenderName, // Backwards compatibility
|
||||
string $paramSenderEmail, // Backwards compatibility
|
||||
string $paramReplyTo, // Backwards compatibility
|
||||
string $paramHost, // Backwards compatibility
|
||||
?int $paramPort, // Backwards compatibility
|
||||
string $paramUsername, // Backwards compatibility
|
||||
string $paramPassword, // Backwards compatibility
|
||||
string $paramSecure, // Backwards compatibility
|
||||
Response $response,
|
||||
Document $project,
|
||||
Mail $queueForMails,
|
||||
array $plan
|
||||
): void {
|
||||
// Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config.
|
||||
// When inline params are provided they are treated as self-contained — project config is ignored
|
||||
// so legacy (1.9.1) callers do not get project state (e.g. replyToName) leaked into their request.
|
||||
$hasInlineParams = !empty($paramHost);
|
||||
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
|
||||
if (!$hasInlineParams && ($smtp['enabled'] ?? false) !== true) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP must be enabled on the project to send a test email.');
|
||||
}
|
||||
|
||||
if ($hasInlineParams) {
|
||||
$senderName = $paramSenderName;
|
||||
$senderEmail = $paramSenderEmail;
|
||||
$replyToEmail = $paramReplyTo;
|
||||
$replyToName = ''; // 1.9.1 inline params did not include replyToName
|
||||
$host = $paramHost;
|
||||
$port = $paramPort ?? 0;
|
||||
$username = $paramUsername;
|
||||
$password = $paramPassword;
|
||||
$secure = $paramSecure;
|
||||
} else {
|
||||
$senderName = $smtp['senderName'] ?? '';
|
||||
$senderEmail = $smtp['senderEmail'] ?? '';
|
||||
$replyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? ''; // Includes backwards compatibility
|
||||
$replyToName = $smtp['replyToName'] ?? '';
|
||||
$host = $smtp['host'] ?? '';
|
||||
$port = $smtp['port'] ?? 0;
|
||||
$username = $smtp['username'] ?? '';
|
||||
$password = $smtp['password'] ?? '';
|
||||
$secure = $smtp['secure'] ?? '';
|
||||
}
|
||||
|
||||
if (empty($senderEmail)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP sender email must be configured on the project to send a test email.');
|
||||
}
|
||||
|
||||
if (empty($host)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP host must be configured on the project to send a test email.');
|
||||
}
|
||||
|
||||
if (empty($port)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP port must be configured on the project to send a test email.');
|
||||
}
|
||||
|
||||
// Fallback to sender details when reply-to is not explicitly configured
|
||||
$replyToEmailDisplay = !empty($replyToEmail) ? $replyToEmail : $senderEmail;
|
||||
$replyToNameDisplay = !empty($replyToName) ? $replyToName : $senderName;
|
||||
|
||||
$subject = 'Custom SMTP email sample';
|
||||
$template = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-smtp-test.tpl');
|
||||
$template
|
||||
->setParam('{{from}}', "{$senderName} ({$senderEmail})")
|
||||
->setParam('{{replyTo}}', "{$replyToNameDisplay} ({$replyToEmailDisplay})")
|
||||
->setParam('{{logoUrl}}', $plan['logoUrl'] ?? APP_EMAIL_LOGO_URL)
|
||||
->setParam('{{accentColor}}', $plan['accentColor'] ?? APP_EMAIL_ACCENT_COLOR)
|
||||
->setParam('{{twitterUrl}}', $plan['twitterUrl'] ?? APP_SOCIAL_TWITTER)
|
||||
->setParam('{{discordUrl}}', $plan['discordUrl'] ?? APP_SOCIAL_DISCORD)
|
||||
->setParam('{{githubUrl}}', $plan['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE)
|
||||
->setParam('{{termsUrl}}', $plan['termsUrl'] ?? APP_EMAIL_TERMS_URL)
|
||||
->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$queueForMails
|
||||
->setSmtpHost($host)
|
||||
->setSmtpPort($port)
|
||||
->setSmtpUsername($username)
|
||||
->setSmtpPassword($password)
|
||||
->setSmtpSecure($secure)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName)
|
||||
->setRecipient($email)
|
||||
->setName('')
|
||||
->setBodyTemplate(APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl')
|
||||
->setBody($template->render())
|
||||
->setVariables([])
|
||||
->setSubject($subject)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Throwable;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectSMTP';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/smtp')
|
||||
->httpAlias('/v1/projects/:projectId/smtp')
|
||||
->desc('Update project SMTP configuration')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
// ->label('event', 'project.smtp.update')
|
||||
->label('audits.event', 'project.smtp.update')
|
||||
->label('audits.resource', 'project.smtp/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'smtp',
|
||||
name: 'updateSMTP',
|
||||
description: <<<EOT
|
||||
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('host', null, new Nullable(new Hostname()), 'SMTP server hostname (domain)', optional: true)
|
||||
->param('port', null, new Nullable(new Integer()), 'SMTP server port', optional: true)
|
||||
->param('username', null, new Nullable(new Text(256)), 'SMTP server username. Leave empty for no authorization.', optional: true)
|
||||
->param('password', null, new Nullable(new Text(256)), 'SMTP server password. Leave empty for no authorization. This property is stored securely and cannot be read in future (write-only).', optional: true)
|
||||
->param('senderEmail', null, new Nullable(new Email()), 'Email address shown in inbox as the sender of the email.', optional: true)
|
||||
->param('senderName', null, new Nullable(new Text(256)), 'Name shown in inbox as the sender of the email.', optional: true)
|
||||
->param('replyToEmail', null, new Nullable(new Email()), 'Email used when user replies to the email.', optional: true)
|
||||
->param('replyToName', null, new Nullable(new Text(256)), 'Name used when user replies to the email.', optional: true)
|
||||
->param('secure', null, new Nullable(new WhiteList(['tls', 'ssl'], true)), 'Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption.', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates.', optional: true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
|
||||
public function action(
|
||||
?string $host,
|
||||
?int $port,
|
||||
?string $username,
|
||||
?string $password,
|
||||
?string $senderEmail,
|
||||
?string $senderName,
|
||||
?string $replyToEmail,
|
||||
?string $replyToName,
|
||||
?string $secure,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
): void {
|
||||
// Fetch current configuration
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
|
||||
// Apply changes
|
||||
$keys = ['host', 'port', 'username', 'password', 'senderEmail', 'senderName', 'replyToEmail', 'replyToName', 'secure', 'enabled'];
|
||||
foreach ($keys as $key) {
|
||||
if (!\is_null(${$key})) {
|
||||
$smtp[$key] = ${$key};
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
$smtp['replyToEmail'] = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
|
||||
if (($smtp['enabled'] ?? false) === true) {
|
||||
// Ensure required fields are set
|
||||
$requiredKeys = ['host', 'port', 'senderEmail'];
|
||||
foreach ($requiredKeys as $key) {
|
||||
if (empty($smtp[$key])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Param "' . $key . '" is not optional.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate SMTP credentials
|
||||
// Validate when the caller is explicitly enabling or hasn't expressed a preference
|
||||
// (so a credentials-only PATCH can auto-enable). Skip only when the caller is
|
||||
// explicitly keeping/turning SMTP off.
|
||||
if (\is_null($enabled) || $enabled === true) {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
|
||||
$mail->Host = $smtp['host'] ?? '';
|
||||
$mail->Port = $smtp['port'] ?? '';
|
||||
$mail->SMTPSecure = $smtp['secure'] ?? '';
|
||||
$mail->setFrom($smtp['senderEmail'], $smtp['senderName'] ?? '');
|
||||
|
||||
if (!empty($smtp['username'] ?? '')) {
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $smtp['username'];
|
||||
$mail->Password = $smtp['password'] ?? '';
|
||||
}
|
||||
|
||||
if (!empty($smtp['replyToEmail'] ?? '')) {
|
||||
$mail->addReplyTo($smtp['replyToEmail'], $smtp['replyToName'] ?? '');
|
||||
}
|
||||
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->Timeout = 5;
|
||||
|
||||
try {
|
||||
$valid = $mail->SmtpConnect();
|
||||
|
||||
if (!$valid) {
|
||||
throw new \Exception('Connection is not valid.');
|
||||
}
|
||||
|
||||
// Auto-enable if configuration is valid
|
||||
// Dont do this if specifically request to mark disabled
|
||||
if (\is_null($enabled)) {
|
||||
$smtp['enabled'] = true;
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
if (($smtp['enabled'] ?? null) === true) {
|
||||
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, $error->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
$updates = new Document([
|
||||
'smtp' => $smtp,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/templates/email/:templateId')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Get project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'getEmailTemplate',
|
||||
description: <<<EOT
|
||||
Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
Response $response,
|
||||
Document $project,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
// Get custom template if available
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $templateId . '-' . $locale] ?? [];
|
||||
|
||||
// Enforced params
|
||||
$template['templateId'] = $templateId;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
// Prepare default tempaltes
|
||||
$localeObj = new Locale($locale);
|
||||
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
|
||||
|
||||
$defaultSubject = $localeObj->getText('emails.' . $templateId . '.subject');
|
||||
$defaultMessage = $this->getDefaultMessage($templateId, $localeObj);
|
||||
|
||||
// Apply defaults if needed
|
||||
if (\is_null($template['message'] ?? null)) {
|
||||
$template['message'] = $defaultMessage;
|
||||
}
|
||||
|
||||
if (\is_null($template['subject'] ?? null)) {
|
||||
$template['subject'] = $defaultSubject;
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (!\is_null($template['replyTo'] ?? null)) {
|
||||
$template['replyToEmail'] = $template['replyToEmail'] ?? $template['replyTo'] ?? '';
|
||||
}
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
|
||||
protected function getDefaultMessage(string $templateId, Locale $localeObj): string
|
||||
{
|
||||
$templateConfigs = [
|
||||
'magicSession' => [
|
||||
'file' => 'email-magic-url.tpl',
|
||||
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
|
||||
],
|
||||
'mfaChallenge' => [
|
||||
'file' => 'email-mfa-challenge.tpl',
|
||||
'placeholders' => ['description', 'clientInfo']
|
||||
],
|
||||
'otpSession' => [
|
||||
'file' => 'email-otp.tpl',
|
||||
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
|
||||
],
|
||||
'sessionAlert' => [
|
||||
'file' => 'email-session-alert.tpl',
|
||||
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
|
||||
],
|
||||
];
|
||||
|
||||
// fallback to the base template.
|
||||
$config = $templateConfigs[$templateId] ?? [
|
||||
'file' => 'email-inner-base.tpl',
|
||||
'placeholders' => ['buttonText', 'body', 'footer']
|
||||
];
|
||||
|
||||
$templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']);
|
||||
$message = Template::fromString($templateString);
|
||||
|
||||
// Set type-specific parameters
|
||||
foreach ($config['placeholders'] as $param) {
|
||||
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
|
||||
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$templateId}.{$param}"), escapeHtml: $escapeHtml);
|
||||
}
|
||||
|
||||
$message
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello"))
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature"));
|
||||
|
||||
$message = $message->render(useContent: true);
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Update project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.write')
|
||||
->label('event', 'templates.[templateId].update')
|
||||
->label('audits.event', 'project.template.update')
|
||||
->label('audits.resource', 'project.template/{response.templateId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'updateEmailTemplate',
|
||||
description: <<<EOT
|
||||
Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->param('subject', null, new Nullable(new Text(255)), 'Subject of the email template. Can be up to 255 characters.', optional: true)
|
||||
->param('message', null, new Nullable(new Text(10485760)), 'Plain or HTML body of the email template message. Can be up to 10MB of content.', optional: true)
|
||||
->param('senderName', null, new Nullable(new Text(255, 0)), 'Name of the email sender.', optional: true)
|
||||
->param('senderEmail', null, new Nullable(new Email()), 'Email of the sender.', optional: true)
|
||||
->param('replyToEmail', null, new Nullable(new Email()), 'Reply to email.', optional: true)
|
||||
->param('replyToName', null, new Nullable(new Text(255, 0)), 'Reply to name.', optional: true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
?string $subject,
|
||||
?string $message,
|
||||
?string $senderName,
|
||||
?string $senderEmail,
|
||||
?string $replyToEmail,
|
||||
?string $replyToName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
// Prevent template update if custom SMTP is not configured
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
if (($smtp['enabled'] ?? false) !== true) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP must be enabled on the project to configure custom email templates.');
|
||||
}
|
||||
|
||||
// Fetch current configuration
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $templateId . '-' . $locale] ?? [];
|
||||
|
||||
// Apply changes
|
||||
$keys = ['senderName', 'senderEmail', 'replyToEmail', 'replyToName', 'message', 'subject'];
|
||||
foreach ($keys as $key) {
|
||||
if (!\is_null(${$key})) {
|
||||
$template[$key] = ${$key};
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (!\is_null($template['replyTo'] ?? null)) {
|
||||
$template['replyToEmail'] = $template['replyToEmail'] ?? $template['replyTo'] ?? '';
|
||||
}
|
||||
|
||||
// Ensure required fields are set
|
||||
$requiredKeys = ['subject', 'message'];
|
||||
foreach ($requiredKeys as $key) {
|
||||
if (empty($template[$key])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Param "' . $key . '" is not optional.');
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
$templates['email.' . $templateId . '-' . $locale] = $template;
|
||||
$updates = new Document([
|
||||
'templates' => $templates,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents->setParam('templateId', $templateId);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'templateId' => $templateId,
|
||||
'locale' => $locale,
|
||||
'subject' => $template['subject'],
|
||||
'message' => $template['message'],
|
||||
'senderName' => $template['senderName'] ?? '',
|
||||
'senderEmail' => $template['senderEmail'] ?? '',
|
||||
'replyToEmail' => $template['replyToEmail'] ?? '',
|
||||
'replyToName' => $template['replyToName'] ?? '',
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,21 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\MembershipPrivacy\Update as UpdateMembershipPrivacyPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\PasswordDictionary\Update as UpdatePasswordDictionaryPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\PasswordHistory\Update as UpdatePasswordHistoryPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\PasswordPersonalData\Update as UpdatePasswordPersonalDataPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionAlert\Update as UpdateSessionAlertPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionDuration\Update as UpdateSessionDurationPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionInvalidation\Update as UpdateSessionInvalidationPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\SessionLimit\Update as UpdateSessionLimitPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Policies\UserLimit\Update as UpdateUserLimitPolicy;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Update as UpdateProjectProtocol;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Services\Update as UpdateProjectService;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests\Create as CreateSMTPTest;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Update as UpdateSMTP;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Get as GetTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Update as UpdateTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
|
||||
@@ -45,6 +58,14 @@ class Http extends Service
|
||||
$this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol());
|
||||
$this->addAction(UpdateProjectService::getName(), new UpdateProjectService());
|
||||
|
||||
// SMTP
|
||||
$this->addAction(UpdateSMTP::getName(), new UpdateSMTP());
|
||||
$this->addAction(CreateSMTPTest::getName(), new CreateSMTPTest());
|
||||
|
||||
// Templates
|
||||
$this->addAction(GetTemplate::getName(), new GetTemplate());
|
||||
$this->addAction(UpdateTemplate::getName(), new UpdateTemplate());
|
||||
|
||||
// Variables
|
||||
$this->addAction(CreateVariable::getName(), new CreateVariable());
|
||||
$this->addAction(ListVariables::getName(), new ListVariables());
|
||||
@@ -73,5 +94,16 @@ class Http extends Service
|
||||
$this->addAction(CreateLinuxPlatform::getName(), new CreateLinuxPlatform());
|
||||
$this->addAction(GetPlatform::getName(), new GetPlatform());
|
||||
$this->addAction(ListPlatforms::getName(), new ListPlatforms());
|
||||
|
||||
// Policies
|
||||
$this->addAction(UpdateMembershipPrivacyPolicy::getName(), new UpdateMembershipPrivacyPolicy());
|
||||
$this->addAction(UpdatePasswordDictionaryPolicy::getName(), new UpdatePasswordDictionaryPolicy());
|
||||
$this->addAction(UpdatePasswordHistoryPolicy::getName(), new UpdatePasswordHistoryPolicy());
|
||||
$this->addAction(UpdatePasswordPersonalDataPolicy::getName(), new UpdatePasswordPersonalDataPolicy());
|
||||
$this->addAction(UpdateSessionAlertPolicy::getName(), new UpdateSessionAlertPolicy());
|
||||
$this->addAction(UpdateSessionDurationPolicy::getName(), new UpdateSessionDurationPolicy());
|
||||
$this->addAction(UpdateSessionInvalidationPolicy::getName(), new UpdateSessionInvalidationPolicy());
|
||||
$this->addAction(UpdateSessionLimitPolicy::getName(), new UpdateSessionLimitPolicy());
|
||||
$this->addAction(UpdateUserLimitPolicy::getName(), new UpdateUserLimitPolicy());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class Delete extends Action
|
||||
|
||||
$key = $dbForPlatform->getDocument('devKeys', $keyId);
|
||||
|
||||
if ($key === false || $key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
if ($key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class Get extends Action
|
||||
|
||||
$key = $dbForPlatform->getDocument('devKeys', $keyId);
|
||||
|
||||
if ($key === false || $key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
if ($key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class Update extends Action
|
||||
|
||||
$key = $dbForPlatform->getDocument('devKeys', $keyId);
|
||||
|
||||
if ($key === false || $key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
if ($key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ class Create extends Action
|
||||
$auth = Config::getParam('auth', []);
|
||||
$auths = [
|
||||
'limit' => 0,
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
|
||||
'maxSessions' => 0,
|
||||
'passwordHistory' => 0,
|
||||
'passwordDictionary' => false,
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
@@ -120,6 +120,8 @@ class Create extends Action
|
||||
'membershipsUserName' => false,
|
||||
'membershipsUserEmail' => false,
|
||||
'membershipsMfa' => false,
|
||||
'membershipsUserId' => false,
|
||||
'membershipsUserPhone' => false,
|
||||
'invalidateSessions' => true
|
||||
];
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ class XList extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
|
||||
$selectQueries = Query::groupByType($queries)['selections'];
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$projects = $this->find($dbForPlatform, $queries, $selectQueries);
|
||||
|
||||
@@ -164,9 +164,7 @@ class Action extends PlatformAction
|
||||
$validator = new AnyOf($cnameValidators);
|
||||
$validators[] = $validator;
|
||||
|
||||
if (\is_null($mainValidator)) {
|
||||
$mainValidator = $validator;
|
||||
}
|
||||
$mainValidator = $validator;
|
||||
}
|
||||
|
||||
// Ensure at least one of CNAME/A/AAAA record points to our servers properly
|
||||
|
||||
@@ -84,7 +84,8 @@ class Create extends Action
|
||||
|
||||
$collection = match ($resourceType) {
|
||||
'site' => 'sites',
|
||||
'function' => 'functions'
|
||||
'function' => 'functions',
|
||||
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid resource type: ' . $resourceType),
|
||||
};
|
||||
$resource = $dbForProject->getDocument($collection, $resourceId);
|
||||
if ($resource->isEmpty()) {
|
||||
|
||||
@@ -208,6 +208,12 @@ class Create extends Action
|
||||
if ($chunk === -1) {
|
||||
$chunk = $chunks;
|
||||
}
|
||||
} else {
|
||||
// Guard against manually setting range header for single chunk upload
|
||||
if ($chunks === -1) {
|
||||
$chunks = 1;
|
||||
$chunk = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
|
||||
|
||||
@@ -116,7 +116,7 @@ class XList extends Base
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$filterQueries = $grouped['filters'];
|
||||
$selectQueries = $grouped['selections'] ?? [];
|
||||
$selectQueries = $grouped['selections'];
|
||||
|
||||
try {
|
||||
$results = $dbForProject->find('deployments', $queries);
|
||||
|
||||
@@ -164,10 +164,6 @@ class Update extends Base
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($framework)) {
|
||||
$framework = $site->getAttribute('framework');
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ class Get extends Base
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unsupported period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Usage;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -107,6 +108,7 @@ class XList extends Base
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unsupported period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
@@ -67,11 +67,7 @@ class Delete extends Base
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getSequence() || $variable->getAttribute('resourceType') !== 'site') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
if ($variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getSequence() || $variable->getAttribute('resourceType') !== 'site') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ class Get extends Base
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if (
|
||||
$variable === false ||
|
||||
$variable->isEmpty() ||
|
||||
$variable->getAttribute('resourceInternalId') !== $site->getSequence() ||
|
||||
$variable->getAttribute('resourceType') !== 'site'
|
||||
@@ -74,10 +73,6 @@ class Get extends Base
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class Update extends Base
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getSequence() || $variable->getAttribute('resourceType') !== 'site') {
|
||||
if ($variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getSequence() || $variable->getAttribute('resourceType') !== 'site') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -384,14 +384,11 @@ class Create extends Action
|
||||
->setAttribute('chunksUploaded', $chunksUploaded);
|
||||
|
||||
/**
|
||||
* Validate create permission and skip authorization in updateDocument
|
||||
* Without this, the file creation will fail when user doesn't have update permission
|
||||
* Skip authorization in updateDocument.
|
||||
* Without this, the file creation will fail when user doesn't have update permission.
|
||||
* However as with chunk upload even if we are updating, we are essentially creating a file
|
||||
* adding it's new chunk so we validate create permission instead of update
|
||||
* adding it's new chunk so we rely on the create-permission check performed earlier.
|
||||
*/
|
||||
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
|
||||
}
|
||||
|
||||
@@ -431,15 +428,11 @@ class Create extends Action
|
||||
->setAttribute('metadata', $metadata);
|
||||
|
||||
/**
|
||||
* Validate create permission and skip authorization in updateDocument
|
||||
* Without this, the file creation will fail when user doesn't have update permission
|
||||
* Skip authorization in updateDocument.
|
||||
* Without this, the file creation will fail when user doesn't have update permission.
|
||||
* However as with chunk upload even if we are updating, we are essentially creating a file
|
||||
* adding it's new chunk so we validate create permission instead of update
|
||||
* adding it's new chunk so we rely on the create-permission check performed earlier.
|
||||
*/
|
||||
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
try {
|
||||
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
|
||||
} catch (NotFoundException) {
|
||||
@@ -468,8 +461,5 @@ class Create extends Action
|
||||
*/
|
||||
protected function afterCreateSuccess(Document $file)
|
||||
{
|
||||
if (!($file instanceof Document)) {
|
||||
throw new Exception('file must be an instance of document');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ class Get extends Action
|
||||
|
||||
// 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;
|
||||
$output = empty($type) ? (array_search($mime, $outputs) ?: 'jpg') : $type;
|
||||
}
|
||||
|
||||
$startTime = \microtime(true);
|
||||
@@ -243,7 +243,7 @@ class Get extends Action
|
||||
|
||||
$image->crop((int) $width, (int) $height, $gravity);
|
||||
|
||||
if (!empty($opacity) || $opacity === 0) {
|
||||
if (!empty($opacity)) {
|
||||
$image->setOpacity($opacity);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class Update extends Action
|
||||
}
|
||||
|
||||
if (\is_null($permissions)) {
|
||||
$permissions = $file->getPermissions() ?? [];
|
||||
$permissions = $file->getPermissions();
|
||||
}
|
||||
|
||||
$file->setAttribute('$permissions', $permissions);
|
||||
|
||||
@@ -143,11 +143,12 @@ class XList extends Action
|
||||
});
|
||||
|
||||
foreach ($stats as $stat) {
|
||||
$bucket = $bucketByStatsId[$stat->getId()];
|
||||
|
||||
if ($bucket) {
|
||||
$bucket->setAttribute('totalSize', $stat->getAttribute('value', 0));
|
||||
if (!isset($bucketByStatsId[$stat->getId()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bucket = $bucketByStatsId[$stat->getId()];
|
||||
$bucket->setAttribute('totalSize', $stat->getAttribute('value', 0));
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Stats may not be available, default to 0
|
||||
|
||||
@@ -109,6 +109,7 @@ class Get extends Action
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\\T00:00:00.000P',
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unsupported period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user