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/migrate-di-container
# Conflicts: # app/init/resources.php # composer.json # composer.lock # phpstan-baseline.neon
This commit is contained in:
@@ -155,7 +155,7 @@ abstract class OAuth2
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return string
|
||||
* @return int
|
||||
*/
|
||||
public function getAccessTokenExpiry(string $code): int
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ class Disqus extends OAuth2
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ class PersonalData extends Password
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $password
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@@ -6,14 +6,8 @@ use DeviceDetector\DeviceDetector;
|
||||
|
||||
class Detector
|
||||
{
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
protected $userAgent = '';
|
||||
|
||||
/**
|
||||
* @param DeviceDetector
|
||||
*/
|
||||
protected $detctor;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,6 @@ class Compose
|
||||
*/
|
||||
protected $compose = [];
|
||||
|
||||
/**
|
||||
* @var string $data
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$this->compose = yaml_parse($data);
|
||||
|
||||
@@ -11,9 +11,6 @@ class Service
|
||||
*/
|
||||
protected $service = [];
|
||||
|
||||
/**
|
||||
* @var string $path
|
||||
*/
|
||||
public function __construct(array $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
|
||||
@@ -9,9 +9,6 @@ class Env
|
||||
*/
|
||||
protected $vars = [];
|
||||
|
||||
/**
|
||||
* @var string $data
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$data = explode("\n", $data);
|
||||
|
||||
@@ -101,7 +101,8 @@ class Mail extends Event
|
||||
/**
|
||||
* Sets preview for the mail event.
|
||||
*
|
||||
* @return string
|
||||
* @param string $preview
|
||||
* @return self
|
||||
*/
|
||||
public function setPreview(string $preview): self
|
||||
{
|
||||
@@ -115,7 +116,7 @@ class Mail extends Event
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview(string $preview): string
|
||||
public function getPreview(): string
|
||||
{
|
||||
return $this->preview;
|
||||
}
|
||||
@@ -181,7 +182,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Set SMTP port
|
||||
*
|
||||
* @param int port
|
||||
* @param int $port
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpPort(int $port): self
|
||||
@@ -217,7 +218,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Set SMTP secure
|
||||
*
|
||||
* @param string $password
|
||||
* @param string $secure
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpSecure(string $secure): self
|
||||
|
||||
@@ -40,7 +40,7 @@ class Usage extends Base
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
return new static(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
|
||||
|
||||
@@ -86,7 +86,7 @@ class Messaging extends Event
|
||||
/**
|
||||
* Returns message document for the messaging event.
|
||||
*
|
||||
* @return string
|
||||
* @return Document
|
||||
*/
|
||||
public function getMessage(): Document
|
||||
{
|
||||
@@ -96,7 +96,7 @@ class Messaging extends Event
|
||||
/**
|
||||
* Sets message ID for the messaging event.
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $messageId
|
||||
* @return self
|
||||
*/
|
||||
public function setMessageId(string $messageId): self
|
||||
|
||||
@@ -8,6 +8,18 @@ use Utopia\Database\Query;
|
||||
|
||||
class EventProcessor
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $events
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
private function getEventMap(array $events): array
|
||||
{
|
||||
return \array_fill_keys(
|
||||
\array_map('strval', \array_unique($events)),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get function events for a project, using Redis cache
|
||||
* @param Document|null $project
|
||||
@@ -26,7 +38,7 @@ class EventProcessor
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$hostname,
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
@@ -36,7 +48,9 @@ class EventProcessor
|
||||
$cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl);
|
||||
|
||||
if ($cachedFunctionEvents !== false) {
|
||||
return \json_decode($cachedFunctionEvents, true) ?? [];
|
||||
$decoded = \json_decode($cachedFunctionEvents, true);
|
||||
|
||||
return \is_array($decoded) ? $this->getEventMap(\array_keys($decoded)) : [];
|
||||
}
|
||||
|
||||
$events = [];
|
||||
@@ -63,7 +77,7 @@ class EventProcessor
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueEvents = \array_flip(\array_unique($events));
|
||||
$uniqueEvents = $this->getEventMap($events);
|
||||
$dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents));
|
||||
|
||||
return $uniqueEvents;
|
||||
@@ -97,6 +111,6 @@ class EventProcessor
|
||||
}
|
||||
}
|
||||
|
||||
return \array_flip(\array_unique($events));
|
||||
return $this->getEventMap($events);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +101,16 @@ class Mapper
|
||||
|
||||
if (\is_array($modelName)) {
|
||||
foreach ($modelName as $name) {
|
||||
$models[] = static::$models[$name];
|
||||
$models[] = self::$models[$name];
|
||||
}
|
||||
} else {
|
||||
$models[] = static::$models[$modelName];
|
||||
$models[] = self::$models[$modelName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If single response, get its model and wrap in array
|
||||
$modelName = $responses->getModel();
|
||||
$models = [static::$models[$modelName]];
|
||||
$models = [self::$models[$modelName]];
|
||||
}
|
||||
|
||||
foreach ($models as $model) {
|
||||
@@ -425,7 +425,7 @@ class Mapper
|
||||
'name' => $unionName,
|
||||
'types' => $types,
|
||||
'resolveType' => static function ($object) use ($unionName) {
|
||||
return static::getUnionImplementation($unionName, $object);
|
||||
return self::getUnionImplementation($unionName, $object);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -440,11 +440,11 @@ class Mapper
|
||||
|
||||
switch ($name) {
|
||||
case 'Attributes':
|
||||
return static::getColumnImplementation($object);
|
||||
return self::getColumnImplementation($object);
|
||||
case 'Columns':
|
||||
return static::getColumnImplementation($object, true);
|
||||
return self::getColumnImplementation($object, true);
|
||||
case 'HashOptions':
|
||||
return static::getHashOptionsImplementation($object);
|
||||
return self::getHashOptionsImplementation($object);
|
||||
}
|
||||
|
||||
throw new Exception('Unknown union type: ' . $name);
|
||||
|
||||
@@ -187,6 +187,20 @@ class V24 extends Migration
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'users':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'impersonator');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"impersonator\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, 'impersonator');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"impersonator\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'teams':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
|
||||
|
||||
@@ -37,7 +37,7 @@ class Action extends UtopiaAction
|
||||
* Foreach Document
|
||||
* Call provided callback for each document in the collection
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param Database $database
|
||||
* @param string $collection
|
||||
* @param array $queries
|
||||
* @param callable $callback
|
||||
|
||||
@@ -43,6 +43,7 @@ class Install extends Action
|
||||
->param('database', '', new WhiteList(['mongodb', 'mariadb', 'postgresql']), 'Database adapter', true)
|
||||
->param('installId', '', new Text(64, 0), 'Installation ID', true)
|
||||
->param('retryStep', null, new Nullable(new WhiteList([Server::STEP_DOCKER_COMPOSE, Server::STEP_ENV_VARS, Server::STEP_DOCKER_CONTAINERS], true)), 'Retry from step', true)
|
||||
->param('migrate', false, new \Utopia\Validator\Boolean(true), 'Run database migration after upgrade', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('swooleResponse')
|
||||
@@ -64,6 +65,7 @@ class Install extends Action
|
||||
string $database,
|
||||
string $installId,
|
||||
?string $retryStep,
|
||||
bool $migrate,
|
||||
Request $request,
|
||||
Response $response,
|
||||
SwooleResponse $swooleResponse,
|
||||
@@ -321,6 +323,28 @@ class Install extends Action
|
||||
}
|
||||
};
|
||||
|
||||
$responseSent = false;
|
||||
$onComplete = function () use ($wantsStream, $swooleResponse, $response, $installId, $state, &$responseSent) {
|
||||
if ($responseSent) {
|
||||
return;
|
||||
}
|
||||
$responseSent = true;
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
$installer->performInstallation(
|
||||
$httpPort ?: $config->getDefaultHttpPort(),
|
||||
$httpsPort ?: $config->getDefaultHttpsPort(),
|
||||
@@ -331,23 +355,12 @@ class Install extends Action
|
||||
$progress,
|
||||
$retryStep,
|
||||
$config->isUpgrade(),
|
||||
$account
|
||||
$account,
|
||||
$onComplete,
|
||||
$migrate,
|
||||
);
|
||||
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
$onComplete();
|
||||
} catch (\Throwable $e) {
|
||||
$this->handleInstallationError($e, $installId, $wantsStream, $response, $swooleResponse, $state);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class View extends Action
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/')
|
||||
->desc('Serve installer UI')
|
||||
->param('step', 1, new Integer(true), 'Step number (1-5)', true)
|
||||
->param('step', 1, new Integer(true), 'Step number (1-6)', true)
|
||||
->param('partial', null, new Nullable(new Text(1, 0)), 'Render partial step only', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
@@ -52,10 +52,13 @@ class View extends Action
|
||||
$defaultEmailCertificates = 'walterobrien@example.com';
|
||||
}
|
||||
|
||||
$step = max(1, min(5, $step));
|
||||
$step = max(1, min(6, $step));
|
||||
if ($isUpgrade && ($step === 2 || $step === 3)) {
|
||||
$step = 4;
|
||||
}
|
||||
if (!$isUpgrade && $step === 6) {
|
||||
$step = 4;
|
||||
}
|
||||
|
||||
$partialFile = $paths['views'] . "/installer/templates/steps/step-{$step}.phtml";
|
||||
if (!is_file($partialFile)) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use Appwrite\Platform\Installer\Http\Installer\Error;
|
||||
use Appwrite\Platform\Installer\Runtime\Config;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Swoole\Http\Server as SwooleServer;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Http\Adapter\Swoole\Request;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Http\Adapter\Swoole\Server as SwooleAdapter;
|
||||
@@ -28,6 +29,7 @@ class Server
|
||||
public const string STEP_DOCKER_COMPOSE = 'docker-compose';
|
||||
public const string STEP_DOCKER_CONTAINERS = 'docker-containers';
|
||||
public const string STEP_ACCOUNT_SETUP = 'account-setup';
|
||||
public const string STEP_MIGRATION = 'migration';
|
||||
public const string STEP_SSL_CERTIFICATE = 'ssl-certificate';
|
||||
|
||||
public const string STATUS_IN_PROGRESS = 'in-progress';
|
||||
@@ -129,6 +131,8 @@ class Server
|
||||
|
||||
private function startSwooleServer(string $host, int $port, ?string $readyFile = null): void
|
||||
{
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
$this->state->clearStaleLock();
|
||||
|
||||
// Preload static files into memory
|
||||
|
||||
+4
-3
@@ -87,13 +87,14 @@ class Decrement extends Action
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+4
-3
@@ -87,13 +87,14 @@ class Increment extends Action
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+3
-3
@@ -139,7 +139,7 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Document $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -183,8 +183,8 @@ class Create extends Action
|
||||
$documents = [$data];
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
|
||||
|
||||
+5
-3
@@ -85,6 +85,7 @@ class Delete extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -101,12 +102,13 @@ class Delete extends Action
|
||||
Context $usage,
|
||||
TransactionState $transactionState,
|
||||
array $plan,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
): void {
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
+4
-3
@@ -72,13 +72,14 @@ class Get extends Action
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+4
-3
@@ -89,10 +89,11 @@ class Update extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -102,8 +103,8 @@ class Update extends Action
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
+3
-3
@@ -96,7 +96,7 @@ class Upsert extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, User $user, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -108,8 +108,8 @@ class Upsert extends Action
|
||||
throw new Exception($this->getMissingPayloadException());
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+3
-3
@@ -83,10 +83,10 @@ class XList extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $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): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+4
-3
@@ -65,17 +65,18 @@ class Create extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
if (empty($operations)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty');
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
// API keys and admins can read any transaction, regular users need permissions
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
|
||||
@@ -91,7 +91,7 @@ class Update extends Action
|
||||
* @param UtopiaResponse $response
|
||||
* @param Database $dbForProject
|
||||
* @param callable $getDatabasesDB
|
||||
* @param Document $user
|
||||
* @param User $user
|
||||
* @param TransactionState $transactionState
|
||||
* @param Delete $queueForDeletes
|
||||
* @param Event $queueForEvents
|
||||
@@ -109,7 +109,7 @@ class Update extends Action
|
||||
* @throws Structure
|
||||
* @throws \Utopia\Http\Exception
|
||||
*/
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
if (!$commit && !$rollback) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
|
||||
@@ -118,8 +118,8 @@ class Update extends Action
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time');
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ class Decrement extends DecrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ class Increment extends IncrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ class Delete extends DocumentDelete
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class Get extends DocumentGet
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ class Update extends DocumentUpdate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class Create extends IndexCreate
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).', false, ['dbForProject'])
|
||||
->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
|
||||
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.')
|
||||
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
|
||||
->param('attributes', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.', false, ['dbForProject'])
|
||||
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
|
||||
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ class Create extends OperationsCreate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ class Decrement extends DecrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ class Increment extends IncrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ class Delete extends DocumentDelete
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class Get extends DocumentGet
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class Update extends DocumentUpdate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ class Create extends OperationsCreate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ class Delete extends DocumentDelete
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class Get extends DocumentGet
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ class Update extends DocumentUpdate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ class Create extends OperationsCreate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class Create extends Base
|
||||
Document $project,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Document $user,
|
||||
User $user,
|
||||
Event $queueForEvents,
|
||||
Context $usage,
|
||||
Func $queueForFunctions,
|
||||
@@ -171,8 +171,8 @@ class Create extends Base
|
||||
/* @var Document $function */
|
||||
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -53,6 +53,7 @@ class Get extends Base
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -61,12 +62,13 @@ class Get extends Base
|
||||
string $executionId,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -62,6 +62,7 @@ class XList extends Base
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -71,12 +72,13 @@ class XList extends Base
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -103,7 +103,7 @@ class Create extends Action
|
||||
Request $request,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Document $user,
|
||||
User $user,
|
||||
Event $queueForEvents,
|
||||
string $mode,
|
||||
Device $deviceForFiles,
|
||||
@@ -112,8 +112,8 @@ class Create extends Action
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -66,6 +66,7 @@ class Delete extends Action
|
||||
->inject('deviceForFiles')
|
||||
->inject('queueForDeletes')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -77,12 +78,13 @@ class Delete extends Action
|
||||
Event $queueForEvents,
|
||||
Device $deviceForFiles,
|
||||
DeleteEvent $queueForDeletes,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -70,6 +70,7 @@ class Get extends Action
|
||||
->inject('resourceToken')
|
||||
->inject('deviceForFiles')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -84,12 +85,13 @@ class Get extends Action
|
||||
Document $resourceToken,
|
||||
Device $deviceForFiles,
|
||||
Authorization $authorization,
|
||||
User $user,
|
||||
) {
|
||||
/* @type Document $bucket */
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -51,6 +51,7 @@ class Get extends Action
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -59,12 +60,13 @@ class Get extends Action
|
||||
string $fileId,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -92,6 +92,7 @@ class Get extends Action
|
||||
->inject('deviceForLocal')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -117,7 +118,8 @@ class Get extends Action
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForLocal,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
@@ -127,8 +129,8 @@ class Get extends Action
|
||||
/* @type Document $bucket */
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -271,7 +273,7 @@ class Get extends Action
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
|
||||
|
||||
//Do not update transformedAt if it's a console user
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
if (!$user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
|
||||
@@ -52,6 +52,7 @@ class Get extends Action
|
||||
->inject('mode')
|
||||
->inject('deviceForFiles')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -66,7 +67,8 @@ class Get extends Action
|
||||
Document $project,
|
||||
string $mode,
|
||||
Device $deviceForFiles,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
@@ -88,8 +90,8 @@ class Get extends Action
|
||||
$disposition = $decoded['disposition'] ?? 'inline';
|
||||
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
@@ -64,6 +64,7 @@ class Update extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -75,12 +76,13 @@ class Update extends Action
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -108,7 +110,7 @@ class Update extends Action
|
||||
|
||||
// Users can only manage their own roles, API keys and Admin users can manage any
|
||||
$roles = $authorization->getRoles();
|
||||
if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) {
|
||||
if (!$user->isApp($roles) && !$user->isPrivileged($roles) && !\is_null($permissions)) {
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
|
||||
@@ -71,6 +71,7 @@ class Get extends Action
|
||||
->inject('resourceToken')
|
||||
->inject('deviceForFiles')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -84,13 +85,14 @@ class Get extends Action
|
||||
string $mode,
|
||||
Document $resourceToken,
|
||||
Device $deviceForFiles,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
/* @type Document $bucket */
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -63,6 +63,7 @@ class XList extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -74,12 +75,13 @@ class XList extends Action
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
string $mode,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -98,10 +98,10 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken)
|
||||
public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken)
|
||||
{
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if (empty($url)) {
|
||||
if (! $isAppUser && ! $isPrivilegedUser) {
|
||||
|
||||
@@ -52,10 +52,11 @@ class Get extends Action
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization)
|
||||
public function action(string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization, User $user)
|
||||
{
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
|
||||
@@ -76,25 +77,25 @@ class Get extends Action
|
||||
];
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
}, $membershipsPrivacy);
|
||||
|
||||
$user = !empty(array_filter($membershipsPrivacy))
|
||||
$memberUser = !empty(array_filter($membershipsPrivacy))
|
||||
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
|
||||
: new Document();
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$mfa = $user->getAttribute('mfa', false);
|
||||
$mfa = $memberUser->getAttribute('mfa', false);
|
||||
|
||||
if ($mfa) {
|
||||
$totp = TOTP::getAuthenticatorFromUser($user);
|
||||
$totp = TOTP::getAuthenticatorFromUser($memberUser);
|
||||
$totpEnabled = $totp && $totp->getAttribute('verified', false);
|
||||
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
|
||||
$emailEnabled = $memberUser->getAttribute('email', false) && $memberUser->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $memberUser->getAttribute('phone', false) && $memberUser->getAttribute('phoneVerification', false);
|
||||
|
||||
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
|
||||
$mfa = false;
|
||||
@@ -105,11 +106,11 @@ class Get extends Action
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userName']) {
|
||||
$membership->setAttribute('userName', $user->getAttribute('name'));
|
||||
$membership->setAttribute('userName', $memberUser->getAttribute('name'));
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userEmail']) {
|
||||
$membership->setAttribute('userEmail', $user->getAttribute('email'));
|
||||
$membership->setAttribute('userEmail', $memberUser->getAttribute('email'));
|
||||
}
|
||||
|
||||
$membership->setAttribute('teamName', $team->getAttribute('name'));
|
||||
|
||||
@@ -66,7 +66,7 @@ class Update extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
public function action(string $teamId, string $membershipId, array $roles, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
{
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
if ($team->isEmpty()) {
|
||||
@@ -83,8 +83,8 @@ class Update extends Action
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
|
||||
@@ -61,10 +61,11 @@ class XList extends Action
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization)
|
||||
public function action(string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization, User $user)
|
||||
{
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
|
||||
@@ -129,26 +130,26 @@ class XList extends Action
|
||||
];
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
}, $membershipsPrivacy);
|
||||
|
||||
$memberships = array_map(function ($membership) use ($dbForProject, $team, $membershipsPrivacy) {
|
||||
$user = !empty(array_filter($membershipsPrivacy))
|
||||
$memberUser = !empty(array_filter($membershipsPrivacy))
|
||||
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
|
||||
: new Document();
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$mfa = $user->getAttribute('mfa', false);
|
||||
$mfa = $memberUser->getAttribute('mfa', false);
|
||||
|
||||
if ($mfa) {
|
||||
$totp = TOTP::getAuthenticatorFromUser($user);
|
||||
$totp = TOTP::getAuthenticatorFromUser($memberUser);
|
||||
$totpEnabled = $totp && $totp->getAttribute('verified', false);
|
||||
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
|
||||
$emailEnabled = $memberUser->getAttribute('email', false) && $memberUser->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $memberUser->getAttribute('phone', false) && $memberUser->getAttribute('phoneVerification', false);
|
||||
|
||||
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
|
||||
$mfa = false;
|
||||
@@ -159,11 +160,11 @@ class XList extends Action
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userName']) {
|
||||
$membership->setAttribute('userName', $user->getAttribute('name'));
|
||||
$membership->setAttribute('userName', $memberUser->getAttribute('name'));
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userEmail']) {
|
||||
$membership->setAttribute('userEmail', $user->getAttribute('email'));
|
||||
$membership->setAttribute('userEmail', $memberUser->getAttribute('email'));
|
||||
}
|
||||
|
||||
$membership->setAttribute('teamName', $team->getAttribute('name'));
|
||||
|
||||
@@ -68,10 +68,10 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
public function action(string $teamId, string $name, array $roles, Response $response, User $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
{
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
|
||||
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ use Utopia\Platform\Action as UtopiaAction;
|
||||
|
||||
class Action extends UtopiaAction
|
||||
{
|
||||
protected function getFileAndBucket(Database $dbForProject, Authorization $authorization, string $bucketId, string $fileId): array
|
||||
protected function getFileAndBucket(Database $dbForProject, Authorization $authorization, User $user, string $bucketId, string $fileId): array
|
||||
{
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -8,6 +8,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\Database;
|
||||
@@ -64,19 +65,20 @@ class Create extends Action
|
||||
->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File unique ID.', false, ['dbForProject'])
|
||||
->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Token expiry date', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
|
||||
public function action(string $bucketId, string $fileId, ?string $expire, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
|
||||
{
|
||||
/**
|
||||
* @var Document $bucket
|
||||
* @var Document $file
|
||||
*/
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $bucketId, $fileId);
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $user, $bucketId, $fileId);
|
||||
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$bucketPermission = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate()));
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\FileTokens;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
@@ -57,14 +58,15 @@ class XList extends Action
|
||||
->param('queries', [], new FileTokens(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', FileTokens::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Authorization $authorization)
|
||||
public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, User $user, Database $dbForProject, Authorization $authorization)
|
||||
{
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $bucketId, $fileId);
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $user, $bucketId, $fileId);
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
$queries[] = Query::equal('resourceType', [TOKENS_RESOURCE_TYPE_FILES]);
|
||||
|
||||
@@ -70,6 +70,8 @@ trait Deployment
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project');
|
||||
}
|
||||
|
||||
$this->beforeCreateGitDeployment($project, $repository, $dbForPlatform, $authorization);
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
$databaseName = $dsn->getHost();
|
||||
@@ -561,6 +563,10 @@ trait Deployment
|
||||
}
|
||||
}
|
||||
|
||||
protected function beforeCreateGitDeployment(Document $project, Document $repository, Database $dbForPlatform, Authorization $authorization): void
|
||||
{
|
||||
}
|
||||
|
||||
protected function getBuildQueueName(Document $project, Database $dbForPlatform, Authorization $authorization): string
|
||||
{
|
||||
return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME);
|
||||
|
||||
+4
-5
@@ -82,12 +82,11 @@ class Create extends Action
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_DETECTION_RUNTIME,
|
||||
model: [
|
||||
Response::MODEL_DETECTION_RUNTIME,
|
||||
Response::MODEL_DETECTION_FRAMEWORK,
|
||||
],
|
||||
),
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_DETECTION_FRAMEWORK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
|
||||
@@ -86,12 +86,11 @@ class XList extends Action
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
|
||||
model: [
|
||||
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
|
||||
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
|
||||
],
|
||||
),
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\Docker\Env;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Appwrite\Platform\Installer\Server as InstallerServer;
|
||||
use Appwrite\Utopia\View;
|
||||
use Swoole\Coroutine;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Config\Config;
|
||||
@@ -35,6 +36,7 @@ class Install extends Action
|
||||
private const string GROWTH_API_URL = 'https://growth.appwrite.io/v1';
|
||||
|
||||
protected bool $isUpgrade = false;
|
||||
protected bool $migrate = false;
|
||||
protected string $hostPath = '';
|
||||
protected ?bool $isLocalInstall = null;
|
||||
protected ?array $installerConfig = null;
|
||||
@@ -211,13 +213,14 @@ class Install extends Action
|
||||
}
|
||||
|
||||
// If interactive and web mode enabled, start web server
|
||||
if ($interactive === 'Y' && Console::isInteractive()) {
|
||||
// Skip the web installer when explicit CLI params are provided
|
||||
if ($interactive === 'Y' && Console::isInteractive() && !$this->hasExplicitCliParams()) {
|
||||
Console::success('Starting web installer...');
|
||||
Console::info('Open your browser at: http://localhost:' . InstallerServer::INSTALLER_WEB_PORT);
|
||||
Console::info('Press Ctrl+C to cancel installation');
|
||||
|
||||
$detectedDb = ($existingInstallation && isset($existingDatabase)) ? $existingDatabase : null;
|
||||
$this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade, $detectedDb);
|
||||
$this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade || $existingInstallation, $detectedDb);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -321,7 +324,7 @@ class Install extends Action
|
||||
|
||||
$shouldGenerateSecrets = !$existingInstallation && !$isUpgrade;
|
||||
$input = $this->prepareEnvironmentVariables($userInput, $vars, $shouldGenerateSecrets);
|
||||
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade);
|
||||
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade, migrate: $this->migrate);
|
||||
}
|
||||
|
||||
|
||||
@@ -510,7 +513,9 @@ class Install extends Action
|
||||
?callable $progress = null,
|
||||
?string $resumeFromStep = null,
|
||||
bool $isUpgrade = false,
|
||||
array $account = []
|
||||
array $account = [],
|
||||
?callable $onComplete = null,
|
||||
bool $migrate = false,
|
||||
): void {
|
||||
$isLocalInstall = $this->isLocalInstall();
|
||||
$this->applyLocalPaths($isLocalInstall, false);
|
||||
@@ -633,8 +638,34 @@ class Install extends Action
|
||||
$this->createInitialAdminAccount($account, $progress, $apiUrl, $domain);
|
||||
}
|
||||
|
||||
// Track installs
|
||||
$this->trackSelfHostedInstall($input, $isUpgrade, $version, $account);
|
||||
if ($isUpgrade && $migrate) {
|
||||
// Allow the containers-completed SSE event to flush
|
||||
// before blocking on migration exec
|
||||
usleep(200_000);
|
||||
$currentStep = InstallerServer::STEP_MIGRATION;
|
||||
$this->runDatabaseMigration($progress, $isLocalInstall);
|
||||
} elseif ($isUpgrade) {
|
||||
$this->updateProgress($progress, InstallerServer::STEP_MIGRATION, InstallerServer::STATUS_COMPLETED, messageOverride: 'Migration skipped');
|
||||
}
|
||||
|
||||
// Signal completion before tracking so the SSE stream
|
||||
// finishes and the frontend can redirect immediately.
|
||||
if ($onComplete) {
|
||||
try {
|
||||
$onComplete();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
// Run tracking in a coroutine when inside a Swoole
|
||||
// request so it doesn't block the worker.
|
||||
if (Coroutine::getCid() !== -1) {
|
||||
go(function () use ($input, $isUpgrade, $version, $account) {
|
||||
$this->trackSelfHostedInstall($input, $isUpgrade, $version, $account);
|
||||
});
|
||||
} else {
|
||||
$this->trackSelfHostedInstall($input, $isUpgrade, $version, $account);
|
||||
}
|
||||
|
||||
if ($isCLI) {
|
||||
Console::success('Appwrite installed successfully');
|
||||
@@ -726,6 +757,46 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
private function runDatabaseMigration(?callable $progress, bool $isLocalInstall): void
|
||||
{
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_MIGRATION,
|
||||
InstallerServer::STATUS_IN_PROGRESS,
|
||||
messageOverride: 'Running database migration...'
|
||||
);
|
||||
|
||||
// Allow the SSE chunk to flush before the blocking exec
|
||||
usleep(100_000);
|
||||
|
||||
// Static command — no user input involved
|
||||
$command = $isLocalInstall
|
||||
? 'docker compose exec appwrite migrate 2>&1'
|
||||
: 'docker exec appwrite migrate 2>&1';
|
||||
|
||||
$output = [];
|
||||
\exec($command, $output, $exit);
|
||||
|
||||
if ($exit !== 0) {
|
||||
$message = trim(implode("\n", $output));
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_MIGRATION,
|
||||
InstallerServer::STATUS_ERROR,
|
||||
details: ['output' => $message],
|
||||
messageOverride: 'Migration failed: ' . ($message ?: 'exit code ' . $exit)
|
||||
);
|
||||
throw new \RuntimeException('Database migration failed', 0, $message !== '' ? new \RuntimeException($message) : null);
|
||||
}
|
||||
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_MIGRATION,
|
||||
InstallerServer::STATUS_COMPLETED,
|
||||
messageOverride: 'Database migration completed'
|
||||
);
|
||||
}
|
||||
|
||||
private function trackSelfHostedInstall(array $input, bool $isUpgrade, string $version, array $account): void
|
||||
{
|
||||
if ($this->isLocalInstall()) {
|
||||
@@ -753,7 +824,7 @@ class Install extends Action
|
||||
$name = $account['name'] ?? 'Admin';
|
||||
$email = $account['email'] ?? 'admin@selfhosted.local';
|
||||
|
||||
$hostIp = gethostbyname($domain);
|
||||
$hostIp = @gethostbyname($domain);
|
||||
|
||||
$payload = [
|
||||
'action' => $type,
|
||||
@@ -767,7 +838,7 @@ class Install extends Action
|
||||
'email' => $email,
|
||||
'domain' => $domain,
|
||||
'database' => $database,
|
||||
'hostIp' => $hostIp !== $domain ? $hostIp : null,
|
||||
'ip' => ($hostIp !== false && $hostIp !== $domain) ? $hostIp : null,
|
||||
'os' => php_uname('s') . ' ' . php_uname('r'),
|
||||
'arch' => php_uname('m'),
|
||||
'cpus' => ((int) trim((string) \shell_exec('nproc'))) ?: null,
|
||||
@@ -778,6 +849,8 @@ class Install extends Action
|
||||
try {
|
||||
$client = new Client();
|
||||
$client
|
||||
->setConnectTimeout(5000)
|
||||
->setTimeout(5000)
|
||||
->addHeader('Content-Type', 'application/json')
|
||||
->fetch(self::GROWTH_API_URL . '/analytics', Client::METHOD_POST, $payload);
|
||||
} catch (\Throwable) {
|
||||
@@ -1261,6 +1334,22 @@ class Install extends Action
|
||||
$this->hostPath = $this->getInstallerHostPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any installer-specific CLI params were explicitly passed.
|
||||
* When params like --database or --http-port are provided, the user
|
||||
* intends to run in CLI mode rather than launching the web installer.
|
||||
*/
|
||||
private function hasExplicitCliParams(): bool
|
||||
{
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
foreach ($argv as $arg) {
|
||||
if (\str_starts_with($arg, '--')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the database adapter from a pre-1.9.0 compose file by
|
||||
* checking which DB service exists or reading _APP_DB_HOST.
|
||||
|
||||
@@ -102,11 +102,14 @@ class SDKs extends Action
|
||||
} else {
|
||||
$sdks = explode(',', $sdks);
|
||||
}
|
||||
$version ??= Console::confirm('Choose an Appwrite version');
|
||||
|
||||
$createRelease = ($release === 'yes');
|
||||
$commitRelease = ($commit === 'yes');
|
||||
|
||||
if ($createRelease && $examplesOnly) {
|
||||
throw new \Exception('Cannot use --release=yes with --mode=examples');
|
||||
}
|
||||
|
||||
if (! $createRelease && ! $examplesOnly) {
|
||||
$git ??= Console::confirm('Should we use git push? (yes/no)');
|
||||
$git = ($git === 'yes');
|
||||
@@ -118,30 +121,34 @@ class SDKs extends Action
|
||||
$prUrls = [];
|
||||
}
|
||||
|
||||
if (! \in_array($version, [
|
||||
'0.6.x',
|
||||
'0.7.x',
|
||||
'0.8.x',
|
||||
'0.9.x',
|
||||
'0.10.x',
|
||||
'0.11.x',
|
||||
'0.12.x',
|
||||
'0.13.x',
|
||||
'0.14.x',
|
||||
'0.15.x',
|
||||
'1.0.x',
|
||||
'1.1.x',
|
||||
'1.2.x',
|
||||
'1.3.x',
|
||||
'1.4.x',
|
||||
'1.5.x',
|
||||
'1.6.x',
|
||||
'1.7.x',
|
||||
'1.8.x',
|
||||
'1.9.x',
|
||||
'latest',
|
||||
])) {
|
||||
throw new \Exception('Unknown version given');
|
||||
if (! $createRelease) {
|
||||
$version ??= Console::confirm('Choose an Appwrite version');
|
||||
|
||||
if (! \in_array($version, [
|
||||
'0.6.x',
|
||||
'0.7.x',
|
||||
'0.8.x',
|
||||
'0.9.x',
|
||||
'0.10.x',
|
||||
'0.11.x',
|
||||
'0.12.x',
|
||||
'0.13.x',
|
||||
'0.14.x',
|
||||
'0.15.x',
|
||||
'1.0.x',
|
||||
'1.1.x',
|
||||
'1.2.x',
|
||||
'1.3.x',
|
||||
'1.4.x',
|
||||
'1.5.x',
|
||||
'1.6.x',
|
||||
'1.7.x',
|
||||
'1.8.x',
|
||||
'1.9.x',
|
||||
'latest',
|
||||
])) {
|
||||
throw new \Exception('Unknown version given');
|
||||
}
|
||||
}
|
||||
|
||||
$selectedPlatforms = ($selectedPlatform === '*' || $selectedPlatform === null) ? null : \array_map('trim', \explode(',', $selectedPlatform));
|
||||
@@ -173,6 +180,124 @@ class SDKs extends Action
|
||||
}
|
||||
|
||||
Console::log('');
|
||||
|
||||
if ($createRelease && ! $examplesOnly) {
|
||||
Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$language['version']}) ━━━");
|
||||
$changelog = $language['changelog'] ?? '';
|
||||
$changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log';
|
||||
|
||||
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
|
||||
$releaseVersion = $language['version'];
|
||||
$releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion);
|
||||
|
||||
if (empty($releaseNotes)) {
|
||||
$releaseNotes = "Release version {$releaseVersion}";
|
||||
}
|
||||
|
||||
$releaseTitle = $releaseVersion;
|
||||
$releaseTarget = $language['repoBranch'] ?? 'main';
|
||||
|
||||
if ($repoName === '/') {
|
||||
Console::warning(' Not a releasable SDK, skipping');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if release already exists
|
||||
$checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null';
|
||||
$existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? '');
|
||||
|
||||
if (! empty($existingReleaseUrl)) {
|
||||
Console::warning(" Release {$releaseVersion} already exists, skipping");
|
||||
Console::log(" {$existingReleaseUrl}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the latest commit on the target branch already has a release
|
||||
$latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null';
|
||||
$latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? '');
|
||||
|
||||
if (! empty($latestCommitSha)) {
|
||||
$latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null';
|
||||
$latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? '');
|
||||
|
||||
if (! empty($latestReleaseTag)) {
|
||||
$tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null';
|
||||
$tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? '');
|
||||
|
||||
if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
|
||||
Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$previousVersion = '';
|
||||
$tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
|
||||
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
|
||||
|
||||
$formattedNotes = "## What's Changed\n\n";
|
||||
$formattedNotes .= $releaseNotes . "\n\n";
|
||||
|
||||
if (! empty($previousVersion)) {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/compare/' . $previousVersion . '...' . $releaseVersion;
|
||||
} else {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/releases/tag/' . $releaseVersion;
|
||||
}
|
||||
|
||||
if (! $commitRelease) {
|
||||
Console::info(' [DRY RUN] Would create release:');
|
||||
Console::log(" Repository: {$repoName}");
|
||||
Console::log(" Version: {$releaseVersion}");
|
||||
Console::log(" Title: {$releaseTitle}");
|
||||
Console::log(" Target Branch: {$releaseTarget}");
|
||||
Console::log(' Previous Version: ' . ($previousVersion ?: 'N/A'));
|
||||
Console::log(' Release Notes:');
|
||||
Console::log(' ' . str_replace("\n", "\n ", $formattedNotes));
|
||||
} else {
|
||||
Console::log(" Creating release {$releaseVersion}...");
|
||||
|
||||
$tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_');
|
||||
\file_put_contents($tempNotesFile, $formattedNotes);
|
||||
|
||||
$releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \
|
||||
--repo ' . \escapeshellarg($repoName) . ' \
|
||||
--title ' . \escapeshellarg($releaseTitle) . ' \
|
||||
--notes-file ' . \escapeshellarg($tempNotesFile) . ' \
|
||||
--target ' . \escapeshellarg($releaseTarget) . ' \
|
||||
2>&1';
|
||||
|
||||
$releaseOutput = [];
|
||||
$releaseReturnCode = 0;
|
||||
\exec($releaseCommand, $releaseOutput, $releaseReturnCode);
|
||||
|
||||
\unlink($tempNotesFile);
|
||||
|
||||
if ($releaseReturnCode === 0) {
|
||||
// Extract release URL from output
|
||||
$releaseUrl = '';
|
||||
foreach ($releaseOutput as $line) {
|
||||
if (strpos($line, 'https://github.com/') !== false) {
|
||||
$releaseUrl = trim($line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console::success(" Release {$releaseVersion} created");
|
||||
if (! empty($releaseUrl)) {
|
||||
Console::log(" {$releaseUrl}");
|
||||
}
|
||||
} else {
|
||||
$errorMessage = implode("\n", $releaseOutput);
|
||||
Console::error(" Failed to create release: " . $errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$version}) ━━━");
|
||||
$specFormat = $language['spec'] ?? 'swagger2';
|
||||
$spec = null;
|
||||
@@ -330,119 +455,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
throw new \Exception('Language "' . $language['key'] . '" not supported');
|
||||
}
|
||||
|
||||
if ($createRelease && ! $examplesOnly) {
|
||||
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
|
||||
$releaseVersion = $language['version'];
|
||||
$releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion);
|
||||
|
||||
if (empty($releaseNotes)) {
|
||||
$releaseNotes = "Release version {$releaseVersion}";
|
||||
}
|
||||
|
||||
$releaseTitle = $releaseVersion;
|
||||
$releaseTarget = $language['repoBranch'] ?? 'main';
|
||||
|
||||
if ($repoName === '/') {
|
||||
Console::warning(' Not a releasable SDK, skipping');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if release already exists
|
||||
$checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null';
|
||||
$existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? '');
|
||||
|
||||
if (! empty($existingReleaseUrl)) {
|
||||
Console::warning(" Release {$releaseVersion} already exists, skipping");
|
||||
Console::log(" {$existingReleaseUrl}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the latest commit on the target branch already has a release
|
||||
$latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null';
|
||||
$latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? '');
|
||||
|
||||
if (! empty($latestCommitSha)) {
|
||||
$latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null';
|
||||
$latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? '');
|
||||
|
||||
if (! empty($latestReleaseTag)) {
|
||||
$tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null';
|
||||
$tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? '');
|
||||
|
||||
if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
|
||||
Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$previousVersion = '';
|
||||
$tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
|
||||
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
|
||||
|
||||
$formattedNotes = "## What's Changed\n\n";
|
||||
$formattedNotes .= $releaseNotes . "\n\n";
|
||||
|
||||
if (! empty($previousVersion)) {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/compare/' . $previousVersion . '...' . $releaseVersion;
|
||||
} else {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/releases/tag/' . $releaseVersion;
|
||||
}
|
||||
|
||||
if (! $commitRelease) {
|
||||
Console::info(' [DRY RUN] Would create release:');
|
||||
Console::log(" Repository: {$repoName}");
|
||||
Console::log(" Version: {$releaseVersion}");
|
||||
Console::log(" Title: {$releaseTitle}");
|
||||
Console::log(" Target Branch: {$releaseTarget}");
|
||||
Console::log(' Previous Version: ' . ($previousVersion ?: 'N/A'));
|
||||
Console::log(' Release Notes:');
|
||||
Console::log(' ' . str_replace("\n", "\n ", $formattedNotes));
|
||||
} else {
|
||||
Console::log(" Creating release {$releaseVersion}...");
|
||||
|
||||
$tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_');
|
||||
\file_put_contents($tempNotesFile, $formattedNotes);
|
||||
|
||||
$releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \
|
||||
--repo ' . \escapeshellarg($repoName) . ' \
|
||||
--title ' . \escapeshellarg($releaseTitle) . ' \
|
||||
--notes-file ' . \escapeshellarg($tempNotesFile) . ' \
|
||||
--target ' . \escapeshellarg($releaseTarget) . ' \
|
||||
2>&1';
|
||||
|
||||
$releaseOutput = [];
|
||||
$releaseReturnCode = 0;
|
||||
\exec($releaseCommand, $releaseOutput, $releaseReturnCode);
|
||||
|
||||
\unlink($tempNotesFile);
|
||||
|
||||
if ($releaseReturnCode === 0) {
|
||||
// Extract release URL from output
|
||||
$releaseUrl = '';
|
||||
foreach ($releaseOutput as $line) {
|
||||
if (strpos($line, 'https://github.com/') !== false) {
|
||||
$releaseUrl = trim($line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console::success(" Release {$releaseVersion} created");
|
||||
if (! empty($releaseUrl)) {
|
||||
Console::log(" {$releaseUrl}");
|
||||
}
|
||||
} else {
|
||||
$errorMessage = implode("\n", $releaseOutput);
|
||||
Console::error(" Failed to create release: " . $errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::log($examplesOnly
|
||||
? ' Generating examples...'
|
||||
: ' Generating SDK...');
|
||||
|
||||
@@ -357,6 +357,13 @@ class Specs extends Action
|
||||
$keys = $this->getKeys();
|
||||
|
||||
$generatedFiles = [];
|
||||
$endpoint = System::getEnv('_APP_HOME', 'https://appwrite.io');
|
||||
$email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', 'team@appwrite.io');
|
||||
$specsDir = __DIR__ . '/../../../../app/config/specs';
|
||||
|
||||
if (!is_dir($specsDir) && !@mkdir($specsDir, 0755, true) && !is_dir($specsDir)) {
|
||||
throw new Exception('Failed to create specs directory: ' . $specsDir);
|
||||
}
|
||||
|
||||
foreach ($platforms as $platform) {
|
||||
$routes = [];
|
||||
@@ -453,8 +460,6 @@ class Specs extends Action
|
||||
foreach (['swagger2', 'open-api3'] as $format) {
|
||||
$formatInstance = $this->getFormatInstance($format, $arguments);
|
||||
$specs = new Specification($formatInstance);
|
||||
$endpoint = System::getEnv('_APP_HOME', '[HOSTNAME]');
|
||||
$email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', APP_EMAIL_TEAM);
|
||||
|
||||
$formatInstance
|
||||
->setParam('name', APP_NAME)
|
||||
@@ -473,36 +478,30 @@ class Specs extends Action
|
||||
->setParam('docs.description', 'Full API docs, specs and tutorials')
|
||||
->setParam('docs.url', $endpoint . '/docs');
|
||||
|
||||
$specsDir = __DIR__ . '/../../../../app/config/specs';
|
||||
$path = $mocks
|
||||
? $specsDir . '/' . $format . '-mocks-' . $platform . '.json'
|
||||
: $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
|
||||
|
||||
if (!is_dir($specsDir)) {
|
||||
if (!mkdir($specsDir, 0755, true)) {
|
||||
throw new Exception('Failed to create specs directory: ' . $specsDir);
|
||||
}
|
||||
$parsedSpecs = $specs->parse();
|
||||
$encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT);
|
||||
|
||||
unset($parsedSpecs);
|
||||
|
||||
if ($encodedSpecs === false) {
|
||||
throw new Exception('Failed to encode ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . \json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($mocks) {
|
||||
$path = $specsDir . '/' . $format . '-mocks-' . $platform . '.json';
|
||||
|
||||
if (!file_put_contents($path, json_encode($specs->parse(), JSON_PRETTY_PRINT))) {
|
||||
throw new Exception('Failed to save mocks spec file: ' . $path);
|
||||
}
|
||||
|
||||
$generatedFiles[] = realpath($path);
|
||||
Console::success('Saved mocks spec file: ' . realpath($path));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
|
||||
|
||||
if (!file_put_contents($path, json_encode($specs->parse(), JSON_PRETTY_PRINT))) {
|
||||
throw new Exception('Failed to save spec file: ' . $path);
|
||||
if (\file_put_contents($path, $encodedSpecs) === false) {
|
||||
throw new Exception('Failed to save ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . $path);
|
||||
}
|
||||
|
||||
$generatedFiles[] = realpath($path);
|
||||
Console::success('Saved spec file: ' . realpath($path));
|
||||
Console::success('Saved ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . realpath($path));
|
||||
|
||||
unset($encodedSpecs, $specs, $formatInstance);
|
||||
}
|
||||
|
||||
unset($arguments, $models, $routes, $services);
|
||||
}
|
||||
|
||||
if ($git === 'yes') {
|
||||
|
||||
@@ -30,6 +30,7 @@ class Upgrade extends Install
|
||||
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
|
||||
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
|
||||
->param('database', 'mongodb', new Text(length: 0), 'Database to use (mongodb|mariadb|postgresql)', true)
|
||||
->param('migrate', false, new Boolean(true), 'Run database migration after upgrade', true)
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -40,9 +41,11 @@ class Upgrade extends Install
|
||||
string $image,
|
||||
string $interactive,
|
||||
bool $noStart,
|
||||
string $database
|
||||
string $database,
|
||||
bool $migrate = false,
|
||||
): void {
|
||||
$this->isUpgrade = true;
|
||||
$this->migrate = $migrate;
|
||||
$isLocalInstall = $this->isLocalInstall();
|
||||
$this->applyLocalPaths($isLocalInstall, true);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ use Utopia\Locale\Locale;
|
||||
use Utopia\Migration\Destination;
|
||||
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
|
||||
use Utopia\Migration\Destinations\CSV as DestinationCSV;
|
||||
use Utopia\Migration\Destinations\JSON as DestinationJSON;
|
||||
use Utopia\Migration\Exception as MigrationException;
|
||||
use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Resources\Database\Database as ResourceDatabase;
|
||||
@@ -37,6 +38,7 @@ use Utopia\Migration\Source;
|
||||
use Utopia\Migration\Sources\Appwrite as SourceAppwrite;
|
||||
use Utopia\Migration\Sources\CSV;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\JSON;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
@@ -208,8 +210,8 @@ class Migrations extends Action
|
||||
$getDatabasesDB = fn (Document $database): Database =>
|
||||
$this->getDatabasesDBForProject($database);
|
||||
$queries = [];
|
||||
if ($source === SourceAppwrite::getName() && $destination === DestinationCSV::getName()) {
|
||||
$queries = Query::parseQueries($migrationOptions['queries']);
|
||||
if ($source === SourceAppwrite::getName() && in_array($destination, [DestinationCSV::getName(), DestinationJSON::getName()])) {
|
||||
$queries = Query::parseQueries($migrationOptions['queries'] ?? []);
|
||||
}
|
||||
|
||||
$migrationSource = match ($source) {
|
||||
@@ -250,6 +252,12 @@ class Migrations extends Action
|
||||
$this->dbForProject,
|
||||
$getDatabasesDB
|
||||
),
|
||||
JSON::getName() => new JSON(
|
||||
$resourceId,
|
||||
$migrationOptions['path'],
|
||||
$this->deviceForMigrations,
|
||||
$this->dbForProject,
|
||||
),
|
||||
default => throw new \Exception('Invalid source type'),
|
||||
};
|
||||
|
||||
@@ -288,6 +296,13 @@ class Migrations extends Action
|
||||
$options['escape'],
|
||||
$options['header'],
|
||||
),
|
||||
DestinationJSON::getName() => new DestinationJSON(
|
||||
$this->deviceForFiles,
|
||||
$migration->getAttribute('resourceId'),
|
||||
$options['bucketId'] ?? 'default',
|
||||
$options['filename'],
|
||||
$options['columns'] ?? [],
|
||||
),
|
||||
default => throw new \Exception('Invalid destination type'),
|
||||
};
|
||||
}
|
||||
@@ -550,8 +565,9 @@ class Migrations extends Action
|
||||
$destination?->success();
|
||||
$source?->success();
|
||||
}
|
||||
if ($migration->getAttribute('destination') === DestinationCSV::getName()) {
|
||||
$this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
|
||||
$destination_type = $migration->getAttribute('destination');
|
||||
if ($destination_type === DestinationCSV::getName() || $destination_type === DestinationJSON::getName()) {
|
||||
$this->handleDataExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
|
||||
}
|
||||
} finally {
|
||||
$source?->cleanup();
|
||||
@@ -583,7 +599,7 @@ class Migrations extends Action
|
||||
* @param Authorization $authorization
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCSVExportComplete(
|
||||
protected function handleDataExportComplete(
|
||||
Document $project,
|
||||
Document $migration,
|
||||
Mail $queueForMails,
|
||||
@@ -608,7 +624,8 @@ class Migrations extends Action
|
||||
throw new \Exception('Bucket not found');
|
||||
}
|
||||
|
||||
$path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . '.csv');
|
||||
$extension = $migration->getAttribute('destination') === DestinationJSON::getName() ? '.json' : '.csv';
|
||||
$path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . $extension);
|
||||
$size = $this->deviceForFiles->getFileSize($path);
|
||||
$mime = $this->deviceForFiles->getFileMimeType($path);
|
||||
$hash = $this->deviceForFiles->getFileHash($path);
|
||||
@@ -632,13 +649,14 @@ class Migrations extends Action
|
||||
$migration->setAttribute('errors', $errors);
|
||||
$migration = $this->updateMigrationDocument($migration, $project, $queueForRealtime);
|
||||
|
||||
$this->sendCSVEmail(
|
||||
$this->sendExportEmail(
|
||||
success: false,
|
||||
project: $project,
|
||||
user: $user,
|
||||
options: $options,
|
||||
queueForMails: $queueForMails,
|
||||
platform: $platform,
|
||||
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
|
||||
sizeMB: $sizeMB
|
||||
);
|
||||
|
||||
@@ -694,13 +712,14 @@ class Migrations extends Action
|
||||
$migration->setAttribute('options', $options);
|
||||
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
|
||||
|
||||
$this->sendCSVEmail(
|
||||
$this->sendExportEmail(
|
||||
success: true,
|
||||
project: $project,
|
||||
user: $user,
|
||||
options: $options,
|
||||
queueForMails: $queueForMails,
|
||||
platform: $platform,
|
||||
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
|
||||
downloadUrl: $downloadUrl
|
||||
);
|
||||
}
|
||||
@@ -719,13 +738,14 @@ class Migrations extends Action
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function sendCSVEmail(
|
||||
protected function sendExportEmail(
|
||||
bool $success,
|
||||
Document $project,
|
||||
Document $user,
|
||||
array $options,
|
||||
Mail $queueForMails,
|
||||
array $platform,
|
||||
string $exportType = 'CSV',
|
||||
string $downloadUrl = '',
|
||||
float $sizeMB = 0.0,
|
||||
): void {
|
||||
@@ -745,15 +765,15 @@ class Migrations extends Action
|
||||
? 'success'
|
||||
: 'failure';
|
||||
|
||||
// Get localized email content
|
||||
$subject = $locale->getText("emails.csvExport.{$emailType}.subject");
|
||||
$preview = $locale->getText("emails.csvExport.{$emailType}.preview");
|
||||
$hello = $locale->getText("emails.csvExport.{$emailType}.hello");
|
||||
$body = $locale->getText("emails.csvExport.{$emailType}.body");
|
||||
$footer = $locale->getText("emails.csvExport.{$emailType}.footer");
|
||||
$thanks = $locale->getText("emails.csvExport.{$emailType}.thanks");
|
||||
$signature = $locale->getText("emails.csvExport.{$emailType}.signature");
|
||||
$buttonText = $success ? $locale->getText("emails.csvExport.{$emailType}.buttonText") : '';
|
||||
// Get localized email content — replace {{type}} with export format (CSV/JSON)
|
||||
$subject = \str_replace('{{type}}', $exportType, $locale->getText("emails.dataExport.{$emailType}.subject"));
|
||||
$preview = \str_replace('{{type}}', $exportType, $locale->getText("emails.dataExport.{$emailType}.preview"));
|
||||
$hello = $locale->getText("emails.dataExport.{$emailType}.hello");
|
||||
$body = $locale->getText("emails.dataExport.{$emailType}.body");
|
||||
$footer = $locale->getText("emails.dataExport.{$emailType}.footer");
|
||||
$thanks = $locale->getText("emails.dataExport.{$emailType}.thanks");
|
||||
$signature = $locale->getText("emails.dataExport.{$emailType}.signature");
|
||||
$buttonText = $success ? $locale->getText("emails.dataExport.{$emailType}.buttonText") : '';
|
||||
|
||||
// Build email body using appropriate template
|
||||
$templatePath = $success
|
||||
@@ -769,6 +789,7 @@ class Migrations extends Action
|
||||
->setParam('{{direction}}', $locale->getText('settings.direction'))
|
||||
->setParam('{{project}}', $project->getAttribute('name'))
|
||||
->setParam('{{user}}', $user->getAttribute('name', $user->getAttribute('email')))
|
||||
->setParam('{{type}}', $exportType)
|
||||
->setParam('{{size}}', $success ? '' : (string)$sizeMB);
|
||||
|
||||
if ($success) {
|
||||
@@ -789,6 +810,7 @@ class Migrations extends Action
|
||||
'terms' => $platform['termsUrl'],
|
||||
'privacy' => $platform['privacyUrl'],
|
||||
'platform' => $platform['platformName'],
|
||||
'type' => $exportType,
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
|
||||
@@ -149,7 +149,7 @@ class Template extends View
|
||||
/**
|
||||
* From Camel Case
|
||||
*
|
||||
* @var string $input
|
||||
* @param string $input
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -167,7 +167,7 @@ class Template extends View
|
||||
/**
|
||||
* From Camel Case to Dash Case
|
||||
*
|
||||
* @var string $input
|
||||
* @param string $input
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -102,7 +102,7 @@ class User extends Document
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPrivileged(array $roles): bool
|
||||
public function isPrivileged(array $roles): bool
|
||||
{
|
||||
if (
|
||||
in_array(self::ROLE_OWNER, $roles) ||
|
||||
@@ -122,7 +122,7 @@ class User extends Document
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isApp(array $roles): bool
|
||||
public function isApp(array $roles): bool
|
||||
{
|
||||
if (in_array(self::ROLE_APPS, $roles)) {
|
||||
return true;
|
||||
@@ -175,7 +175,5 @@ class User extends Document
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class Request extends UtopiaRequest
|
||||
$forwardedUserAgent = $this->getHeader('x-forwarded-user-agent');
|
||||
if (!empty($forwardedUserAgent)) {
|
||||
$roles = $this->authorization->getRoles();
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isAppUser = $this->user?->isApp($roles) ?? false;
|
||||
|
||||
if ($isAppUser) {
|
||||
return $forwardedUserAgent;
|
||||
@@ -239,9 +239,15 @@ class Request extends UtopiaRequest
|
||||
}
|
||||
|
||||
private ?Authorization $authorization = null;
|
||||
private ?User $user = null;
|
||||
|
||||
public function setAuthorization(Authorization $authorization): void
|
||||
{
|
||||
$this->authorization = $authorization;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,11 +120,11 @@ class V17 extends Filter
|
||||
$isArrayStack = !$isStringStack && $stackCount > 0;
|
||||
|
||||
if ($char === static::CHAR_BACKSLASH) {
|
||||
if (!(static::isSpecialChar($filter[$i + 1]))) {
|
||||
static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam);
|
||||
if (!(self::isSpecialChar($filter[$i + 1]))) {
|
||||
self::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam);
|
||||
}
|
||||
|
||||
static::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam);
|
||||
self::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam);
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
@@ -147,7 +147,7 @@ class V17 extends Filter
|
||||
}
|
||||
|
||||
// Either way, add symbol to builder
|
||||
static::appendSymbol(
|
||||
self::appendSymbol(
|
||||
$isStringStack,
|
||||
$char,
|
||||
$i,
|
||||
@@ -199,7 +199,7 @@ class V17 extends Filter
|
||||
}
|
||||
|
||||
// Value, not relevant to syntax
|
||||
static::appendSymbol(
|
||||
self::appendSymbol(
|
||||
$isStringStack,
|
||||
$char,
|
||||
$i,
|
||||
|
||||
@@ -505,8 +505,9 @@ class Response extends SwooleResponse
|
||||
|
||||
if ($rule['sensitive']) {
|
||||
$roles = $this->authorization->getRoles();
|
||||
$isPrivilegedUser = DBUser::isPrivileged($roles);
|
||||
$isAppUser = DBUser::isApp($roles);
|
||||
$user = $this->user ?? new DBUser();
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) {
|
||||
$data->setAttribute($key, '');
|
||||
@@ -628,9 +629,9 @@ class Response extends SwooleResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently set filter
|
||||
* Return the currently set filters
|
||||
*
|
||||
* @return Filter
|
||||
* @return array<Filter>
|
||||
*/
|
||||
public function getFilters(): array
|
||||
{
|
||||
@@ -660,7 +661,7 @@ class Response extends SwooleResponse
|
||||
/**
|
||||
* Static wrapper to show sensitive data in response
|
||||
*
|
||||
* @param callable The callback to show sensitive information for
|
||||
* @param callable(): array $callback The callback to show sensitive information for
|
||||
* @return array
|
||||
*/
|
||||
public static function showSensitive(callable $callback): array
|
||||
@@ -674,9 +675,15 @@ class Response extends SwooleResponse
|
||||
}
|
||||
|
||||
private ?Authorization $authorization = null;
|
||||
private ?DBUser $user = null;
|
||||
|
||||
public function setAuthorization(Authorization $authorization): void
|
||||
{
|
||||
$this->authorization = $authorization;
|
||||
}
|
||||
|
||||
public function setUser(DBUser $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,34 +11,48 @@ class V21 extends Filter
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
return match ($model) {
|
||||
Response::MODEL_USER => $this->parseUser($content),
|
||||
Response::MODEL_USER_LIST => $this->handleList(
|
||||
$content,
|
||||
'users',
|
||||
fn ($item) => $this->parseUser($item),
|
||||
),
|
||||
Response::MODEL_ACCOUNT => $this->parseUser($content),
|
||||
Response::MODEL_SITE => $this->parseSite($content),
|
||||
Response::MODEL_SITE_LIST => $this->handleList(
|
||||
$content,
|
||||
"sites",
|
||||
'sites',
|
||||
fn ($item) => $this->parseSite($item),
|
||||
),
|
||||
Response::MODEL_FUNCTION => $this->parseFunction($content),
|
||||
Response::MODEL_FUNCTION_LIST => $this->handleList(
|
||||
$content,
|
||||
"functions",
|
||||
'functions',
|
||||
fn ($item) => $this->parseFunction($item),
|
||||
),
|
||||
Response::MODEL_DOCUMENT => $this->parseDocument($content),
|
||||
Response::MODEL_DOCUMENT_LIST => $this->handleList(
|
||||
$content,
|
||||
"documents",
|
||||
'documents',
|
||||
fn ($item) => $this->parseDocument($item),
|
||||
),
|
||||
Response::MODEL_ROW => $this->parseRow($content),
|
||||
Response::MODEL_ROW_LIST => $this->handleList(
|
||||
$content,
|
||||
"rows",
|
||||
'rows',
|
||||
fn ($item) => $this->parseRow($item),
|
||||
),
|
||||
default => $content,
|
||||
};
|
||||
}
|
||||
|
||||
protected function parseUser(array $content): array
|
||||
{
|
||||
unset($content['impersonator']);
|
||||
unset($content['impersonatorUserId']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseSite(array $content): array
|
||||
{
|
||||
$content = $this->parseSpecs($content);
|
||||
|
||||
@@ -157,9 +157,9 @@ class User extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Collection
|
||||
* Filter user document attributes for response output.
|
||||
*
|
||||
* @return string
|
||||
* @return Document
|
||||
*/
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user