1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00

Merge pull request #940 from TorstenDittmann/feat-265-realtime-support

Feat 265 realtime support - Endpoint implementation
This commit is contained in:
Torsten Dittmann 2021-03-03 09:44:03 +01:00 committed by GitHub
commit e180c7f08c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 2272 additions and 97 deletions

View file

@ -14,7 +14,7 @@ RUN composer update --ignore-platform-reqs --optimize-autoloader \
FROM php:7.4-cli-alpine as step1
ENV PHP_REDIS_VERSION=5.3.0 \
ENV PHP_REDIS_VERSION=5.3.2 \
PHP_SWOOLE_VERSION=v4.5.8 \
PHP_MAXMINDDB_VERSION=v1.8.0 \
PHP_XDEBUG_VERSION=sdebug_2_9-beta
@ -170,6 +170,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/maintenance && \
chmod +x /usr/local/bin/install && \
chmod +x /usr/local/bin/migrate && \
chmod +x /usr/local/bin/realtime && \
chmod +x /usr/local/bin/schedule && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/ssl && \
@ -195,6 +196,7 @@ RUN echo extension=maxminddb.so >> /usr/local/etc/php/conf.d/maxminddb.ini
RUN echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/appwrite.ini
RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /usr/local/etc/php/conf.d/appwrite.ini
RUN echo "opcache.enable_cli=1" >> /usr/local/etc/php/conf.d/appwrite.ini
RUN echo "default_socket_timeout=-1" >> /usr/local/etc/php/conf.d/appwrite.ini
EXPOSE 80

View file

@ -105,10 +105,10 @@ return [
],
[
'name' => '_APP_SYSTEM_SECURITY_EMAIL_ADDRESS',
'description' => 'This is the email address used to issue SSL certificates for custom domains or the user agent in webhooks. The default value is \'security@localhost.test\'.',
'description' => 'This is the email address used to issue SSL certificates for custom domains or the user agent in your webhooks payload.',
'introduction' => '0.7.0',
'default' => 'security@localhost.test',
'required' => false,
'default' => '',
'required' => true,
'question' => '',
],
[
@ -141,6 +141,22 @@ return [
'required' => false,
'question' => '',
],
[
'name' => '_APP_REDIS_USER',
'description' => 'Redis server user.',
'introduction' => '0.7',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_REDIS_PASS',
'description' => 'Redis server password.',
'introduction' => '0.7',
'default' => '',
'required' => false,
'question' => '',
],
],
],
[

View file

@ -33,6 +33,20 @@ App::get('/v1/health/version')
$response->json(['version' => APP_VERSION_STABLE]);
});
App::get('/v1/health/realtime')
->desc('Get Realtime')
->groups(['api', 'health'])
->label('scope', 'public')
->inject('response')
->action(function ($response) {
/** @var Utopia\Response $response */
$redis = new Redis();
$redis->connect('redis', 6379);
$redis->publish('realtime', 'I\'m a live message');
$response->json(['status' => 'OK']);
});
App::get('/v1/health/db')
->desc('Get DB')
->groups(['api', 'health'])

View file

@ -110,7 +110,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e
}, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes'], 'api');
App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $mode) {
App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $realtime, $mode) {
/** @var Utopia\App $utopia */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
@ -119,6 +119,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $usage */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Event\Realtime $realtime */
/** @var Appwrite\Event\Event $functions */
/** @var bool $mode */
@ -139,6 +140,13 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
->setQueue('v1-functions')
->setClass('FunctionsV1')
->trigger();
$realtime
->setEvent($events->getParam('event'))
->setProject($project->getId())
->setPayload($response->getPayload())
->trigger();
}
if (!empty($audits->getParam('event'))) {
@ -162,4 +170,4 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
;
}
}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'mode'], 'api');
}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'realtime', 'mode'], 'api');

View file

@ -11,13 +11,18 @@ App::init(function ($utopia, $request, $response, $layout) {
/* AJAX check */
if (!empty($request->getQuery('version', ''))) {
$layout->setPath(__DIR__.'/../../views/layouts/empty.phtml');
$layout->setPath(__DIR__ . '/../../views/layouts/empty.phtml');
}
$port = $request->getPort();
$protocol = $request->getProtocol();
$domain = $request->getHostname();
$layout
->setParam('title', APP_NAME)
->setParam('protocol', $request->getProtocol())
->setParam('domain', $request->getHostname())
->setParam('protocol', $protocol)
->setParam('domain', $domain)
->setParam('endpoint', $protocol . '://' . $domain . ($port != 80 && $port != 443 ? ':' . $port : ''))
->setParam('home', App::getEnv('_APP_HOME'))
->setParam('setup', App::getEnv('_APP_SETUP'))
->setParam('class', 'unknown')
@ -34,10 +39,10 @@ App::init(function ($utopia, $request, $response, $layout) {
$time = (60 * 60 * 24 * 45); // 45 days cache
$response
->addHeader('Cache-Control', 'public, max-age='.$time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url='.\urlencode($request->getURI()))
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
;

View file

@ -20,6 +20,7 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Database\Document;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Event\Event;
use Appwrite\Event\Realtime;
use Appwrite\Extend\PDO;
use Appwrite\OpenSSL\OpenSSL;
use Utopia\App;
@ -34,7 +35,7 @@ use PDO as PDONative;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
const APP_EMAIL_SECURITY = 'security@localhost.test'; // Default security email address
const APP_EMAIL_SECURITY = ''; // Default security email address
const APP_USERAGENT = APP_NAME.'-Server v%s. Please report abuse at %s';
const APP_MODE_DEFAULT = 'default';
const APP_MODE_ADMIN = 'admin';
@ -91,9 +92,13 @@ Config::load('storage-mimes', __DIR__.'/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__.'/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php');
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '')
.':'.App::getEnv('_APP_REDIS_PORT', ''));
$user = App::getEnv('_APP_REDIS_USER','');
$pass = App::getEnv('_APP_REDIS_PASS','');
if(!empty($user) || !empty($pass)) {
Resque::setBackend('redis://'.$user.':'.$pass.'@'.App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
} else {
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
}
/**
* DB Filters
*/
@ -176,6 +181,18 @@ $register->set('statsd', function () { // Register DB connection
$register->set('cache', function () { // Register cache connection
$redis = new Redis();
$redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$user = App::getEnv('_APP_REDIS_USER','');
$pass = App::getEnv('_APP_REDIS_PASS','');
$auth = [];
if(!empty($user)) {
$auth["user"] = $user;
}
if(!empty($pass)) {
$auth["pass"] = $pass;
}
if(!empty($auth)) {
$redis->auth($auth);
}
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
return $redis;
@ -305,6 +322,10 @@ App::setResource('events', function($register) {
return new Event('', '');
}, ['register']);
App::setResource('realtime', function($register) {
return new Realtime('', '', []);
}, ['register']);
App::setResource('audits', function($register) {
return new Event(Event::AUDITS_QUEUE_NAME, Event::AUDITS_CLASS_NAME);
}, ['register']);
@ -378,10 +399,10 @@ App::setResource('user', function($mode, $project, $console, $request, $response
$request->getCookie(Auth::$cookieName.'_legacy', '')));// Get fallback session from old clients (no SameSite support)
// Get fallback session from clients who block 3rd-party cookies
$response->addHeader('X-Debug-Fallback', 'false');
if($response) $response->addHeader('X-Debug-Fallback', 'false');
if(empty($session['id']) && empty($session['secret'])) {
$response->addHeader('X-Debug-Fallback', 'true');
if($response) $response->addHeader('X-Debug-Fallback', 'true');
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
@ -395,7 +416,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response
}
else {
$user = $consoleDB->getDocument(Auth::$unique);
$user
->setAttribute('$id', 'admin-'.$user->getAttribute('$id'))
;
@ -410,7 +431,8 @@ App::setResource('user', function($mode, $project, $console, $request, $response
if (APP_MODE_ADMIN === $mode) {
if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) {
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
} else {
}
else {
$user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]);
}
}

311
app/realtime.php Normal file
View file

@ -0,0 +1,311 @@
<?php
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/../vendor/autoload.php';
use Appwrite\Auth\Auth;
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Network\Validator\Origin;
use Appwrite\Realtime\Realtime;
use Swoole\Database\RedisConfig;
use Swoole\Database\RedisPool;
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\Process;
use Swoole\WebSocket\Frame;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Swoole\Request as SwooleRequest;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
/**
* TODO List
*
* - CORS Validation
* - Limit payload size
* - JWT Authentication (in path / or in message)
*
* Protocols Support:
* - Websocket support: https://www.swoole.co.uk/docs/modules/swoole-websocket-server
* - MQTT support: https://www.swoole.co.uk/docs/modules/swoole-mqtt-server
* - SSE support: https://github.com/hhxsv5/php-sse
* - Socket.io support: https://github.com/shuixn/socket.io-swoole-server
*/
ini_set('default_socket_timeout', -1);
Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
$server = new Server('0.0.0.0', 80);
$server->set([
'websocket_compression' => true,
'package_max_length' => 64000 // Default maximum Package Size (64kb)
]);
$subscriptions = [];
$connections = [];
$register->set('redis', function () {
$user = App::getEnv('_APP_REDIS_USER', '');
$pass = App::getEnv('_APP_REDIS_PASS', '');
$auth = '';
if (!empty($user)) {
$auth += $user;
}
if (!empty($pass)) {
$auth += ':' . $pass;
}
$config = new RedisConfig();
$config
->withHost(App::getEnv('_APP_REDIS_HOST', ''))
->withPort(App::getEnv('_APP_REDIS_PORT', ''))
->withAuth($auth)
->withTimeout(0)
->withReadTimeout(0)
->withRetryInterval(0);
$pool = new RedisPool($config);
return $pool;
});
$server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) {
Console::success('Worker ' . ++$workerId . ' started succefully');
$attempts = 0;
$start = time();
while ($attempts < 300) {
try {
if ($attempts > 0) {
Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . ').
Attempting restart in 5 seconds (attempt #' . $attempts . ')');
sleep(5); // 5 sec delay between connection attempts
}
$redis = $register->get('redis')->get();
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
if ($redis->ping(true)) {
$attempts = 0;
Console::success('Pub/sub connection established (worker: ' . $workerId . ')');
} else {
Console::error('Pub/sub failed (worker: ' . $workerId . ')');
}
$redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, &$connections, &$subscriptions) {
/**
* Supported Resources:
* - Collection
* - Document
* - Bucket
* - File
* - User? / Account? (no permissions)
* - Session? (no permissions)
* - Team? (no permissions)
* - Membership? (no permissions)
* - Function
* - Execution
*/
$event = json_decode($payload, true);
$receivers = Realtime::identifyReceivers($event, $subscriptions);
foreach ($receivers as $receiver) {
if ($server->exist($receiver) && $server->isEstablished($receiver)) {
$server->push(
$receiver,
json_encode($event['data']),
SWOOLE_WEBSOCKET_OPCODE_TEXT,
SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS
);
} else {
$server->close($receiver);
}
}
});
} catch (\Throwable $th) {
Console::error('Pub/sub error: ' . $th->getMessage());
$attempts++;
continue;
}
$attempts++;
}
Console::error('Failed to restart pub/sub...');
});
$server->on('start', function (Server $server) {
Console::success('Server started succefully');
Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}");
// listen ctrl + c
Process::signal(2, function () use ($server) {
Console::log('Stop by Ctrl+C');
$server->shutdown();
});
});
$server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) {
Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})");
$app = new App('');
$connection = $request->fd;
$request = new SwooleRequest($request);
App::setResource('request', function () use ($request) {
return $request;
});
App::setResource('consoleDB', function () use (&$register) {
$consoleDB = new Database();
$consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register, true));
$consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects
$consoleDB->setMocks(Config::getParam('collections', []));
return $consoleDB;
}, ['register']);
App::setResource('project', function ($consoleDB, $request) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Database\Database $consoleDB */
Authorization::disable();
$project = $consoleDB->getDocument($request->getQuery('project'));
Authorization::reset();
return $project;
}, ['consoleDB', 'request']);
App::setResource('console', function ($consoleDB) {
return $consoleDB->getDocument('console');
}, ['consoleDB']);
App::setResource('user', function ($project, $request, $projectDB) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Database $projectDB */
Authorization::setDefaultStatus(true);
Auth::setCookieName('a_session_' . $project->getId());
$session = Auth::decodeSession(
$request->getCookie(
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
)
); // Get fallback session from old clients (no SameSite support)
Auth::$unique = $session['id'];
Auth::$secret = $session['secret'];
$user = $projectDB->getDocument(Auth::$unique);
if (
empty($user->getId()) // Check a document has been found in the DB
|| Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document
|| !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)
) { // Validate user has valid login token
$user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]);
}
return $user;
}, ['project', 'request', 'projectDB']);
/** @var Appwrite\Database\Document $user */
$user = $app->getResource('user');
/** @var Appwrite\Database\Document $project */
$project = $app->getResource('project');
/** @var Appwrite\Database\Document $console */
$console = $app->getResource('console');
/*
* Project Check
*/
if (empty($project->getId())) {
$server->push($connection, 'Missing or unknown project ID');
$server->close($connection);
return;
}
/*
* Abuse Check
*/
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 60, 60, function () use ($register) {
return $register->get('db');
});
$timeLimit
->setNamespace('app_' . $project->getId())
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getURI());
$abuse = new Abuse($timeLimit);
if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$server->push($connection, 'Too many requests');
$server->close($connection);
return;
}
/*
* Validate Client Domain - Check to avoid CSRF attack
* Adding Appwrite API domains to allow XDOMAIN communication
* Skip this check for non-web platforms which are not required to send an origin header
*/
$origin = $request->getOrigin();
$originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
if (!$originValidator->isValid($origin)) {
$server->push($connection, $originValidator->getDescription());
$server->close($connection);
return;
}
Realtime::setUser($user);
$roles = Realtime::getRoles();
$channels = Realtime::parseChannels($request->getQuery('channels', []));
/**
* Channels Check
*/
if (empty($channels)) {
$server->push($connection, 'Missing channels');
$server->close($connection);
return;
}
Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels);
$server->push($connection, json_encode($channels));
});
$server->on('message', function (Server $server, Frame $frame) {
if ($frame->data === 'reload') {
$server->reload();
}
Console::info('Recieved message: ' . $frame->data . ' (user: ' . $frame->fd . ', worker: ' . $server->getWorkerId() . ')');
});
$server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) {
Realtime::unsubscribe($fd, $subscriptions, $connections);
Console::info('Connection close: ' . $fd);
});
$server->start();

View file

@ -114,7 +114,7 @@ $maxCells = 10;
<?php if(!$array): ?>
<?php switch($type):
case 'fileId': ?>
<img data-ls-if="{{node.<?php echo $this->escape($key); ?>}} != ''" src="" data-ls-attrs="src=//{{env.DOMAIN}}/v1/storage/files/{{node.<?php echo $this->escape($key); ?>}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="avatar" width="30" height="30" loading="lazy" />
<img data-ls-if="{{node.<?php echo $this->escape($key); ?>}} != ''" src="" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{node.<?php echo $this->escape($key); ?>}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="avatar" width="30" height="30" loading="lazy" />
<?php break; ?>
<?php case 'document': ?>
{...}

View file

@ -72,7 +72,7 @@ $rules = $collection->getAttribute('rules', []);
<?php if(!$array): ?>
<?php switch($type):
case 'fileId': ?>
<img data-ls-if="{{node.<?php echo $this->escape($key); ?>}} != ''" src="" data-ls-attrs="src=//{{env.DOMAIN}}/v1/storage/files/{{node.<?php echo $this->escape($key); ?>}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="avatar" width="30" height="30" loading="lazy" />
<img data-ls-if="{{node.<?php echo $this->escape($key); ?>}} != ''" src="" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{node.<?php echo $this->escape($key); ?>}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="avatar" width="30" height="30" loading="lazy" />
<?php break; ?>
<?php case 'document': ?>
{...}

View file

@ -54,7 +54,7 @@
<input type="radio" name="selected" data-ls-attrs="value={{file.$id}}" data-ls-bind="{{search.selected}}" />
</td>
<td data-title="x" class="">
<img src="" data-ls-attrs="src=//{{env.DOMAIN}}/v1/storage/files/{{file.$id}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="pull-start avatar" width="30" height="30" loading="lazy" />
<img src="" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{file.$id}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="pull-start avatar" width="30" height="30" loading="lazy" />
</td>
<td data-title="Name: " class="text-one-liner">
<span data-ls-bind="{{file.name}}" data-ls-attrs="title={{file.name}}" class="text-fade text-size-small"></span>

View file

@ -596,9 +596,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
<p><b>PowerShell</b></p>
<div class="margin-bottom">
<textarea type="hidden" data-ls-bind="appwrite functions createTag ,
--functionId={{project-function.$id}} ,
--command='mycommand' ,
<textarea type="hidden" data-ls-bind="appwrite functions createTag `
--functionId={{project-function.$id}} `
--command='mycommand' `
--code='/myrepo/myfunction'" data-forms-code="powershell" data-lang="powershell" data-lang-label="PowerShell"></textarea>
</div>

View file

@ -135,7 +135,7 @@ $customDomainsTarget = $this->getParam('customDomainsTarget', false);
<label for="name">API Endpoint</label>
<div class="input-copy">
<input data-forms-copy type="text" disabled data-ls-bind="{{env.PROTOCOL}}://{{env.DOMAIN}}/v1" />
<input data-forms-copy type="text" disabled data-ls-bind="{{env.ENDPOINT}}/v1" />
</div>
<ul class="margin-bottom-large text-fade text-size-small">
@ -453,7 +453,7 @@ $customDomainsTarget = $this->getParam('customDomainsTarget', false);
data-failure-param-alert-classname="error">
<input name="teamId" type="hidden" data-ls-bind="{{member.teamId}}">
<input name="url" type="hidden" data-ls-bind="{{env.PROTOCOL}}://{{env.DOMAIN}}/auth/join?project={{router.params.project}}" />
<input name="url" type="hidden" data-ls-bind="{{env.ENDPOINT}}/auth/join?project={{router.params.project}}" />
<input name="email" type="hidden" data-ls-bind="{{member.email}}">
<input name="name" type="hidden" data-ls-bind="{{member.name}}">
<input name="roles" type="hidden" data-ls-bind="{{member.roles}}" data-cast-to="json">
@ -493,7 +493,7 @@ $customDomainsTarget = $this->getParam('customDomainsTarget', false);
data-failure-param-alert-classname="error">
<input name="teamId" id="team-teamId" type="hidden" data-ls-bind="{{console-project.teamId}}">
<input name="url" type="hidden" data-ls-bind="{{env.PROTOCOL}}://{{env.DOMAIN}}/auth/join?project={{router.params.project}}" />
<input name="url" type="hidden" data-ls-bind="{{env.ENDPOINT}}/auth/join?project={{router.params.project}}" />
<label for="email">Email</label>
<input name="email" id="email" type="email" autocomplete="email" required>

View file

@ -113,7 +113,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<tbody data-ls-loop="project-files.files" data-ls-as="file">
<tr>
<td class="hide">
<img src="" data-ls-attrs="src=//{{env.DOMAIN}}/v1/storage/files/{{file.$id}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="pull-start avatar" width="30" height="30" loading="lazy" />
<img src="" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{file.$id}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="pull-start avatar" width="30" height="30" loading="lazy" />
</td>
<td data-title="Name: " class="text-one-liner" data-ls-attrs="title={{file.name}}" >
<div data-ui-modal class="box modal sticky-footer width-large close" data-button-text="{{file.name}}" data-button-class="link" data-button-element="span">
@ -180,15 +180,15 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<div class="margin-bottom-small">File Preview</div>
<div class="margin-bottom-small">
<img src="" class="file-preview" data-ls-attrs="src=//{{env.DOMAIN}}/v1/storage/files/{{file.$id}}/preview?width=350&height=250&project={{router.params.project}}&mode=admin" loading="lazy" width="225" height="160" />
<img src="" class="file-preview" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{file.$id}}/preview?width=350&height=250&project={{router.params.project}}&mode=admin" loading="lazy" width="225" height="160" />
</div>
<div class="margin-bottom-tiny">
<a href="" data-ls-attrs="href=//{{env.DOMAIN}}/v1/storage/files/{{file.$id}}/view?project={{router.params.project}}&mode=admin" target="_blank" rel="noopener"><i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> New Window <i class="icon-link-ext"></i></a>
<a href="" data-ls-attrs="href={{env.ENDPOINT}}/v1/storage/files/{{file.$id}}/view?project={{router.params.project}}&mode=admin" target="_blank" rel="noopener"><i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> New Window <i class="icon-link-ext"></i></a>
</div>
<div class="margin-bottom">
<a href="" data-ls-attrs="href=//{{env.DOMAIN}}/v1/storage/files/{{file.$id}}/download?project={{router.params.project}}&mode=admin" target="_blank" rel="noopener"><i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Download <i class="icon-link-ext"></i></a>
<a href="" data-ls-attrs="href={{env.ENDPOINT}}/v1/storage/files/{{file.$id}}/download?project={{router.params.project}}&mode=admin" target="_blank" rel="noopener"><i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Download <i class="icon-link-ext"></i></a>
</div>
</div>
</div>

View file

@ -361,7 +361,7 @@ $providers = $this->getParam('providers', []);
<p>To complete set up, add this OAuth2 redirect URI to your <?php echo $this->escape(ucfirst($provider)); ?> app configuration.</p>
<div class="input-copy">
<input data-forms-copy type="text" disabled data-ls-bind="{{env.PROTOCOL}}://{{env.DOMAIN}}/v1/account/sessions/oauth2/callback/<?php echo $this->escape($provider); ?>/{{router.params.project}}" class="margin-bottom-no" />
<input data-forms-copy type="text" disabled data-ls-bind="{{env.ENDPOINT}}/v1/account/sessions/oauth2/callback/<?php echo $this->escape($provider); ?>/{{router.params.project}}" class="margin-bottom-no" />
</div>
</div>
</div>

View file

@ -145,7 +145,7 @@
data-failure-param-alert-classname="error">
<input name="teamId" id="team-teamId" type="hidden" data-ls-bind="{{team.$id}}">
<input name="url" type="hidden" data-ls-bind="{{env.PROTOCOL}}://{{env.DOMAIN}}" />
<input name="url" type="hidden" data-ls-bind="{{env.ENDPOINT}}" />
<label for="email">Email</label>
<input name="email" id="email" type="email" autocomplete="email" required>

View file

@ -23,7 +23,7 @@
<label>Email</label>
<input name="email" type="email" class="full-width" autocomplete="email" placeholder="me@example.com" required>
<input name="url" type="hidden" data-ls-bind="{{env.PROTOCOL}}://{{env.DOMAIN}}/auth/recovery/reset" />
<input name="url" type="hidden" data-ls-bind="{{env.ENDPOINT}}/auth/recovery/reset" />
<button type="submit" class="btn btn-primary"><i class="fa fa-sign-in"></i> Recover</button>
</form>

View file

@ -66,6 +66,8 @@ services:
- _APP_DOMAIN_TARGET
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -104,6 +106,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_STATSD_HOST
- _APP_STATSD_PORT
@ -121,6 +125,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -142,6 +148,8 @@ services:
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -163,6 +171,8 @@ services:
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -187,6 +197,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -211,6 +223,8 @@ services:
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DOMAIN_TARGET
- _APP_DB_HOST
- _APP_DB_PORT
@ -236,6 +250,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -264,6 +280,8 @@ services:
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@ -283,6 +301,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
@ -302,6 +322,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
mariadb:
image: appwrite/mariadb:1.2.0 # fix issues when upgrading using: mysql_upgrade -u root -p

View file

@ -2,6 +2,7 @@
$protocol = $this->getParam('protocol', '');
$domain = $this->getParam('domain', '');
$endpoint = $this->getParam('endpoint', '');
$platforms = $this->getParam('platforms', []);
$version = $this->getParam('version', '0.0.0');
$isDev = $this->getParam('isDev', false);
@ -56,7 +57,7 @@ if(!empty($platforms)) {
<?php if (!empty($canonical)): ?>
<meta property="og:url" content="<?php echo $canonical; ?>" />
<?php endif; ?>
<meta property="og:image" content="<?php echo $protocol; ?>://<?php echo $domain; ?>/images/logo.png?v=<?php echo APP_CACHE_BUSTER; ?>" />
<meta property="og:image" content="<?php echo $endpoint; ?>/images/logo.png?v=<?php echo APP_CACHE_BUSTER; ?>" />
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
@ -71,6 +72,7 @@ if(!empty($platforms)) {
VERSION: '<?php echo $version; ?>',
CACHEBUSTER: '<?php echo $version; ?>/<?php echo APP_CACHE_BUSTER; ?>',
PROTOCOL: '<?php echo $protocol; ?>',
ENDPOINT: '<?php echo $endpoint; ?>',
DOMAIN: '<?php echo $domain; ?>',
HOME: '<?php echo $this->escape($this->getParam('home')); ?>',
SETUP: '<?php echo $this->escape($this->getParam('setup')); ?>',
@ -142,4 +144,4 @@ if(!empty($platforms)) {
<!-- Version <?php echo $version; ?> -->
</body>
</html>
</html>

View file

@ -110,14 +110,22 @@ class CertificatesV1
}
$staging = (App::isProduction()) ? '' : ' --dry-run';
$email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS');
$response = \shell_exec("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
." --email ".App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', 'security@localhost.test')
if(empty($email)) {
throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate');
}
$stdout = '';
$stderr = '';
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
." --email ".$email
." -w ".APP_STORAGE_CERTIFICATES
." -d {$domain->get()}");
." -d {$domain->get()}", '', $stdout, $stderr);
if(!$response) {
throw new Exception('Failed to issue a certificate');
if($stderr || $exit !== 0) {
throw new Exception('Failed to issue a certificate with message: '.$stderr);
}
$path = APP_STORAGE_CERTIFICATES.'/'.$domain->get();
@ -129,19 +137,19 @@ class CertificatesV1
}
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($response));
throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout));
}
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/chain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem: '.\json_encode($response));
throw new Exception('Failed to rename certificate chain.pem: '.\json_encode($stdout));
}
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/fullchain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem: '.\json_encode($response));
throw new Exception('Failed to rename certificate fullchain.pem: '.\json_encode($stdout));
}
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/privkey.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem: '.\json_encode($response));
throw new Exception('Failed to rename certificate privkey.pem: '.\json_encode($stdout));
}
$certificate = \array_merge($certificate, [
@ -154,7 +162,7 @@ class CertificatesV1
'issueDate' => \time(),
'renewDate' => $renew,
'attempts' => 0,
'log' => \json_encode($response),
'log' => \json_encode($stdout),
]);
$certificate = $consoleDB->createDocument($certificate);

3
bin/realtime Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/realtime.php $@

View file

@ -1,3 +1,10 @@
#!/bin/sh
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" RESQUE_PHP='/usr/src/code/vendor/autoload.php' php /usr/src/code/vendor/bin/resque-scheduler
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
REDIS_BACKEND=$REDIS_BACKEND RESQUE_PHP='/usr/src/code/vendor/autoload.php' php /usr/src/code/vendor/bin/resque-scheduler

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-audits' APP_INCLUDE='/usr/src/code/app/workers/audits.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-audits' APP_INCLUDE='/usr/src/code/app/workers/audits.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

9
bin/worker-certificates Normal file → Executable file
View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-certificates' APP_INCLUDE='/usr/src/code/app/workers/certificates.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-certificates' APP_INCLUDE='/usr/src/code/app/workers/certificates.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-deletes' APP_INCLUDE='/usr/src/code/app/workers/deletes.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-deletes' APP_INCLUDE='/usr/src/code/app/workers/deletes.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-functions' APP_INCLUDE='/usr/src/code/app/workers/functions.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-functions' APP_INCLUDE='/usr/src/code/app/workers/functions.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-mails' APP_INCLUDE='/usr/src/code/app/workers/mails.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-mails' APP_INCLUDE='/usr/src/code/app/workers/mails.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-tasks' APP_INCLUDE='/usr/src/code/app/workers/tasks.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-tasks' APP_INCLUDE='/usr/src/code/app/workers/tasks.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-usage' APP_INCLUDE='/usr/src/code/app/workers/usage.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-usage' APP_INCLUDE='/usr/src/code/app/workers/usage.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -1,3 +1,10 @@
#!/bin/sh
QUEUE='v1-webhooks' APP_INCLUDE='/usr/src/code/app/workers/webhooks.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
if [ -z "$_APP_REDIS_USER" ] || [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
QUEUE='v1-webhooks' APP_INCLUDE='/usr/src/code/app/workers/webhooks.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -18,8 +18,8 @@ services:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`appwrite`)
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.appwrite_web.address=:80
- --entrypoints.appwrite_websecure.address=:443
- --accesslog=true
ports:
- 80:80
@ -47,11 +47,19 @@ services:
networks:
- appwrite
labels:
- traefik.enable=true
- traefik.constraint-label-stack=appwrite
- traefik.http.routers.appwrite.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite-secure.tls=true
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=appwrite"
- "traefik.http.services.appwrite_api.loadbalancer.server.port=80"
#http
- traefik.http.routers.appwrite_api_http.entrypoints=appwrite_web
- traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_api_http.service=appwrite_api
# https
- traefik.http.routers.appwrite_api_https.entrypoints=appwrite_websecure
- traefik.http.routers.appwrite_api_https.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-uploads:/storage/uploads:rw
@ -86,6 +94,8 @@ services:
- _APP_DOMAIN_TARGET
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -110,6 +120,48 @@ services:
- _APP_FUNCTIONS_MEMORY_SWAP
- _APP_FUNCTIONS_ENVS
appwrite-realtime:
entrypoint: realtime
container_name: appwrite-realtime
build:
context: .
restart: unless-stopped
ports:
- 9505:80
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=appwrite"
- "traefik.http.services.appwrite_realtime.loadbalancer.server.port=80"
#ws
- traefik.http.routers.appwrite_realtime_ws.entrypoints=appwrite_web
- traefik.http.routers.appwrite_realtime_ws.rule=PathPrefix(`/v1/realtime`)
- traefik.http.routers.appwrite_realtime_ws.service=appwrite_realtime
# wss
- traefik.http.routers.appwrite_realtime_wss.entrypoints=appwrite_websecure
- traefik.http.routers.appwrite_realtime_wss.rule=PathPrefix(`/v1/realtime`)
- traefik.http.routers.appwrite_realtime_wss.service=appwrite_realtime
- traefik.http.routers.appwrite_realtime_wss.tls=true
- traefik.http.routers.appwrite_realtime_wss.tls.certresolver=dns
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
environment:
- _APP_ENV
- _APP_OPTIONS_ABUSE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
appwrite-worker-usage:
entrypoint: worker-usage
container_name: appwrite-worker-usage
@ -127,6 +179,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_STATSD_HOST
- _APP_STATSD_PORT
@ -147,6 +201,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -172,6 +228,8 @@ services:
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -196,6 +254,8 @@ services:
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -223,6 +283,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -249,6 +311,8 @@ services:
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -276,6 +340,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
@ -310,6 +376,8 @@ services:
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@ -332,6 +400,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
@ -353,6 +423,8 @@ services:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
mariadb:
image: appwrite/mariadb:1.2.0 # fix issues when upgrading using: mysql_upgrade -u root -p
@ -446,7 +518,7 @@ services:
image: adminer
restart: always
ports:
- 9505:8080
- 9506:8080
networks:
- appwrite

View file

@ -1,4 +1,4 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1108px" height="839px" viewBox="-0.5 -0.5 1108 839" content="&lt;mxfile host=&quot;f62fd6c3-483a-4a80-b2dc-315a36eb41a5&quot; modified=&quot;2021-01-16T10:33:38.685Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 11_0_0) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36&quot; etag=&quot;OZW_7vXQqOQ9aLjWoWdU&quot; version=&quot;13.10.0&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;WOshqXSVd2VkRfcggtcB&quot; name=&quot;Page-1&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;">
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1108px" height="839px" viewBox="-0.5 -0.5 1108 839" content="&lt;mxfile host=&quot;612c770d-d23c-4897-ac0d-72478d1c5aef&quot; modified=&quot;2021-02-22T07:17:54.650Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 11_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.53.2 Chrome/87.0.4280.141 Electron/11.2.1 Safari/537.36&quot; etag=&quot;sAOb0L0gpVpMeEycjwCe&quot; version=&quot;14.2.4&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;WOshqXSVd2VkRfcggtcB&quot; name=&quot;Page-1&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<path d="M 77 40 L 77 80 L 397 80 L 397 113.63" fill="none" stroke="#23445d" stroke-miterlimit="10" pointer-events="stroke"/>
@ -117,10 +117,12 @@
</g>
<path d="M 457 220 L 500.63 220" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 505.88 220 L 498.88 223.5 L 500.63 220 L 498.88 216.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 397 240 L 397 275 L 597 275 L 597 303.63" fill="none" stroke="#10739e" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 308.88 L 593.5 301.88 L 597 303.63 L 600.5 301.88 Z" fill="#10739e" stroke="#10739e" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 397 240 L 397 275 L 437 275 L 437 303.63" fill="none" stroke="#10739e" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 437 308.88 L 433.5 301.88 L 437 303.63 L 440.5 301.88 Z" fill="#10739e" stroke="#10739e" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 397 240 L 397 275 L 97 275 L 97 303.63" fill="none" stroke="#10739e" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 97 308.88 L 93.5 301.88 L 97 303.63 L 100.5 301.88 Z" fill="#10739e" stroke="#10739e" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 397 240 L 397 275 L 597 275 L 597 303.63" fill="none" stroke="#006eaf" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 308.88 L 593.5 301.88 L 597 303.63 L 600.5 301.88 Z" fill="#006eaf" stroke="#006eaf" stroke-miterlimit="10" pointer-events="all"/>
<rect x="337" y="200" width="120" height="40" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@ -138,8 +140,6 @@
</text>
</switch>
</g>
<path d="M 97 350 L 97 380 L 597 380 L 597 356.37" fill="none" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 351.12 L 600.5 358.12 L 597 356.37 L 593.5 358.12 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="all"/>
<rect x="37" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@ -157,42 +157,40 @@
</text>
</switch>
</g>
<path d="M 657 330 L 897 330 L 897 250 L 960.63 250" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 250 L 960.63 250" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 250 L 958.88 253.5 L 960.63 250 L 958.88 246.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 300 L 960.63 300" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 300 L 960.63 300" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 300 L 958.88 303.5 L 960.63 300 L 958.88 296.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 400 L 960.63 400" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 400 L 960.63 400" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 400 L 958.88 403.5 L 960.63 400 L 958.88 396.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 450 L 960.63 450" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 450 L 960.63 450" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 450 L 958.88 453.5 L 960.63 450 L 958.88 446.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 500 L 960.63 500" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 500 L 960.63 500" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 500 L 958.88 503.5 L 960.63 500 L 958.88 496.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 550 L 960.63 550" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 550 L 960.63 550" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 550 L 958.88 553.5 L 960.63 550 L 958.88 546.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 600 L 960.63 600" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 497 330 L 897 330 L 897 600 L 960.63 600" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 600 L 958.88 603.5 L 960.63 600 L 958.88 596.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 537 330 L 347 330 L 347 493.63" fill="none" stroke="#56517e" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 347 498.88 L 343.5 491.88 L 347 493.63 L 350.5 491.88 Z" fill="#56517e" stroke="#56517e" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 537 330 L 207 330 L 207 493.63" fill="none" stroke="#56517e" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 377 330 L 207 330 L 207 493.63" fill="none" stroke="#56517e" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 207 498.88 L 203.5 491.88 L 207 493.63 L 210.5 491.88 Z" fill="#56517e" stroke="#56517e" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 597 350 L 597 403.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 408.88 L 593.5 401.88 L 597 403.63 L 600.5 401.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 657 330 L 897 330 L 897 350 L 960.63 350" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 437 350 L 437 380 L 597 380 L 597 403.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 408.88 L 593.5 401.88 L 597 403.63 L 600.5 401.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 497 330 L 897 330 L 897 350 L 960.63 350" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 965.88 350 L 958.88 353.5 L 960.63 350 L 958.88 346.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
<rect x="537" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
<rect x="377" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 330px; margin-left: 538px;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 330px; margin-left: 378px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
APIs
REST API
</div>
</div>
</div>
</foreignObject>
<text x="597" y="334" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
APIs
<text x="437" y="334" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
REST API
</text>
</switch>
</g>
@ -425,10 +423,12 @@
</text>
</switch>
</g>
<path d="M 597 430 L 597 465 L 527 465 L 527 493.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 527 498.88 L 523.5 491.88 L 527 493.63 L 530.5 491.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 597 430 L 597 465 L 667 465 L 667 493.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 667 498.88 L 663.5 491.88 L 667 493.63 L 670.5 491.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 597 430 L 597 465 L 527 465 L 527 493.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 527 498.88 L 523.5 491.88 L 527 493.63 L 530.5 491.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 597 430 L 597 465 L 667 465 L 667 493.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 667 498.88 L 663.5 491.88 L 667 493.63 L 670.5 491.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 597 430 L 597 465 L 807 465 L 807 493.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 807 498.88 L 803.5 491.88 L 807 493.63 L 810.5 491.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
<rect x="437" y="410" width="320" height="20" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@ -724,10 +724,29 @@
</text>
</switch>
</g>
<path d="M 597 350 L 597 403.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 408.88 L 593.5 401.88 L 597 403.63 L 600.5 401.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
<rect x="537" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 330px; margin-left: 538px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
REALTIME API
</div>
</div>
</div>
</foreignObject>
<text x="597" y="334" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
REALTIME API
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank">
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -19,16 +19,23 @@ class Redis extends Adapter
*/
protected $adapter;
/**
* @var bool
*/
protected $isPool = false;
/**
* Redis constructor.
*
* @param Adapter $adapter
* @param Registry $register
* @param bool $isPool
*/
public function __construct(Adapter $adapter, Registry $register)
public function __construct(Adapter $adapter, Registry $register, bool $isPool = false)
{
$this->register = $register;
$this->adapter = $adapter;
$this->isPool = $isPool;
}
/**
@ -261,7 +268,7 @@ class Redis extends Adapter
*/
protected function getRedis(): Client
{
return $this->register->get('cache');
return $this->isPool ? $this->register->get('redis')->get() : $this->register->get('cache');
}
/**

View file

@ -0,0 +1,179 @@
<?php
namespace Appwrite\Event;
use Appwrite\Database\Document;
use Utopia\App;
class Realtime
{
/**
* @var string
*/
protected $project = '';
/**
* @var string
*/
protected $event = '';
/**
* @var array
*/
protected $channels = [];
/**
* @var array
*/
protected $permissions = [];
/**
* @var Document
*/
protected $payload;
/**
* Event constructor.
*
* @param string $project
* @param string $event
* @param array $payload
*/
public function __construct(string $project, string $event, array $payload)
{
$this->project = $project;
$this->event = $event;
$this->payload = new Document($payload);
}
/**
* @param string $project
* return $this
*/
public function setProject(string $project): self
{
$this->project = $project;
return $this;
}
/**
* @return string
*/
public function getProject(): string
{
return $this->project;
}
/**
* @param string $event
* return $this
*/
public function setEvent(string $event): self
{
$this->event = $event;
return $this;
}
/**
* @return string
*/
public function getEvent(): string
{
return $this->event;
}
/**
* @param array $payload
* return $this
*/
public function setPayload(array $payload): self
{
$this->payload = new Document($payload);
return $this;
}
/**
* @return Document
*/
public function getPayload(): Document
{
return $this->payload;
}
/**
* Populate channels array based on the event name and payload.
*
* @return void
*/
private function prepareChannels(): void
{
switch (true) {
case strpos($this->event, 'account.recovery.') === 0:
case strpos($this->event, 'account.sessions.') === 0:
case strpos($this->event, 'account.verification.') === 0:
$this->channels[] = 'account.' . $this->payload->getAttribute('userId');
$this->permissions = ['user:' . $this->payload->getAttribute('userId')];
break;
case strpos($this->event, 'account.') === 0:
$this->channels[] = 'account.' . $this->payload->getId();
$this->permissions = ['user:' . $this->payload->getId()];
break;
case strpos($this->event, 'database.collections.') === 0:
$this->channels[] = 'collections';
$this->channels[] = 'collections.' . $this->payload->getId();
$this->permissions = $this->payload->getAttribute('$permissions.read');
break;
case strpos($this->event, 'database.documents.') === 0:
$this->channels[] = 'documents';
$this->channels[] = 'collections.' . $this->payload->getAttribute('$collection') . '.documents';
$this->channels[] = 'documents.' . $this->payload->getId();
$this->permissions = $this->payload->getAttribute('$permissions.read');
break;
case strpos($this->event, 'storage.') === 0:
$this->channels[] = 'files';
$this->channels[] = 'files.' . $this->payload->getId();
$this->permissions = $this->payload->getAttribute('$permissions.read');
break;
}
}
/**
* Execute Event.
*
* @return void
*/
public function trigger(): void
{
$this->prepareChannels();
if (empty($this->channels)) return;
$redis = new \Redis();
$redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$redis->publish('realtime', json_encode([
'project' => $this->project,
'permissions' => $this->permissions,
'data' => [
'event' => $this->event,
'channels' => $this->channels,
'timestamp' => time(),
'payload' => $this->payload->getArrayCopy()
]
]));
$this->reset();
}
public function reset(): self
{
$this->event = '';
$this->payload = $this->channels = [];
return $this;
}
}

View file

@ -0,0 +1,182 @@
<?php
namespace Appwrite\Realtime;
use Appwrite\Auth\Auth;
use Appwrite\Database\Document;
class Realtime
{
/**
* @var Document $user
*/
static $user;
/**
* @param Document $user
*/
static function setUser(Document $user)
{
self::$user = $user;
}
/**
* @return array
*/
static function getRoles()
{
$roles = ['role:' . ((self::$user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)];
if (!(self::$user->isEmpty())) {
$roles[] = 'user:' . self::$user->getId();
}
foreach (self::$user->getAttribute('memberships', []) as $node) {
if (isset($node['teamId']) && isset($node['roles'])) {
$roles[] = 'team:' . $node['teamId'];
foreach ($node['roles'] as $nodeRole) { // Set all team roles
$roles[] = 'team:' . $node['teamId'] . '/' . $nodeRole;
}
}
}
return $roles;
}
/**
* @param array $channels
*/
static function parseChannels(array $channels)
{
$channels = array_flip($channels);
foreach ($channels as $key => $value) {
switch (true) {
case strpos($key, 'account.') === 0:
unset($channels[$key]);
break;
case $key === 'account':
if (!empty(self::$user->getId())) {
$channels['account.' . self::$user->getId()] = $value;
}
unset($channels['account']);
break;
}
}
if (\array_key_exists('account', $channels)) {
if (self::$user->getId()) {
$channels['account.' . self::$user->getId()] = $channels['account'];
}
unset($channels['account']);
}
return $channels;
}
/**
* Identifies the receivers of all subscriptions, based on the permissions and event.
*
* @param array $event
* @param array $connections
* @param array $subscriptions
*/
static function identifyReceivers(array &$event, array &$subscriptions)
{
$receivers = [];
if ($subscriptions[$event['project']]) {
foreach ($subscriptions[$event['project']] as $role => $subscription) {
foreach ($event['data']['channels'] as $channel) {
if (
\array_key_exists($channel, $subscriptions[$event['project']][$role])
&& (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions']))
) {
foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) {
$receivers[] = $ids;
}
break;
}
}
}
}
return array_keys(array_flip($receivers));
}
/**
* Adds Subscription.
*
* @param string $projectId
* @param mixed $connection
* @param array $subscriptions
* @param array $roles
* @param array $channels
*/
static function subscribe($projectId, $connection, $roles, &$subscriptions, &$connections, &$channels)
{
/**
* Build Subscriptions Tree
*
* [PROJECT_ID] ->
* [ROLE_X] ->
* [CHANNEL_NAME_X] -> [CONNECTION_ID]
* [CHANNEL_NAME_Y] -> [CONNECTION_ID]
* [CHANNEL_NAME_Z] -> [CONNECTION_ID]
* [ROLE_Y] ->
* [CHANNEL_NAME_X] -> [CONNECTION_ID]
* [CHANNEL_NAME_Y] -> [CONNECTION_ID]
* [CHANNEL_NAME_Z] -> [CONNECTION_ID]
*/
if (!isset($subscriptions[$projectId])) { // Init Project
$subscriptions[$projectId] = [];
}
foreach ($roles as $role) {
if (!isset($subscriptions[$projectId][$role])) { // Add user first connection
$subscriptions[$projectId][$role] = [];
}
foreach ($channels as $channel => $list) {
$subscriptions[$projectId][$role][$channel][$connection] = true;
}
}
$connections[$connection] = [
'projectId' => $projectId,
'roles' => $roles,
];
}
/**
* Remove Subscription.
*
* @param mixed $connection
* @param array $subscriptions
* @param array $connections
*/
static function unsubscribe($connection, &$subscriptions, &$connections)
{
$projectId = $connections[$connection]['projectId'] ?? '';
$roles = $connections[$connection]['roles'] ?? [];
foreach ($roles as $role) {
foreach ($subscriptions[$projectId][$role] as $channel => $list) {
unset($subscriptions[$projectId][$role][$channel][$connection]); // Remove connection
if (empty($subscriptions[$projectId][$role][$channel])) {
unset($subscriptions[$projectId][$role][$channel]); // Remove channel when no connections
}
}
if (empty($subscriptions[$projectId][$role])) {
unset($subscriptions[$projectId][$role]); // Remove role when no channels
}
}
if (empty($subscriptions[$projectId])) { // Remove project when no roles
unset($subscriptions[$projectId]);
}
unset($connections[$connection]);
}
}

View file

@ -0,0 +1,318 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Database\Document;
use Appwrite\Realtime\Realtime;
use PHPUnit\Framework\TestCase;
class RealtimeChannelsTest extends TestCase
{
/**
* Configures how many Connections the Test should Mock.
*/
public $connectionsPerChannel = 10;
public $connections = [];
public $subscriptions = [];
public $connectionsCount = 0;
public $connectionsAuthenticated = 0;
public $connectionsGuest = 0;
public $connectionsTotal = 0;
public $allChannels = [
'files',
'files.1',
'collections',
'collections.1',
'collections.1.documents',
'collections.2',
'collections.2.documents',
'documents',
'documents.1',
'documents.2',
];
public function setUp(): void
{
/**
* Setup global Counts
*/
$this->connectionsAuthenticated = count($this->allChannels) * $this->connectionsPerChannel;
$this->connectionsGuest = count($this->allChannels) * $this->connectionsPerChannel;
$this->connectionsTotal = $this->connectionsAuthenticated + $this->connectionsGuest;
/**
* Add Authenticated Clients
*/
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
foreach ($this->allChannels as $index => $channel) {
Realtime::setUser(new Document([
'$id' => 'user' . $this->connectionsCount,
'memberships' => [
[
'teamId' => 'team' . $i,
'roles' => [
empty($index % 2) ? 'admin' : 'member'
]
]
]
]));
Realtime::subscribe(
'1',
$this->connectionsCount,
Realtime::getRoles(),
$this->subscriptions,
$this->connections,
Realtime::parseChannels([0 => $channel])
);
$this->connectionsCount++;
}
}
/**
* Add Guest Clients
*/
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
foreach ($this->allChannels as $index => $channel) {
Realtime::setUser(new Document([
'$id' => ''
]));
Realtime::subscribe(
'1',
$this->connectionsCount,
Realtime::getRoles(),
$this->subscriptions,
$this->connections,
Realtime::parseChannels([0 => $channel])
);
$this->connectionsCount++;
}
}
}
public function tearDown(): void
{
$this->connections = [];
$this->subscriptions = [];
$this->connectionsCount = 0;
}
public function testSubscriptions()
{
/**
* Check for 1 project.
*/
$this->assertCount(1, $this->subscriptions);
/**
* Check for correct amount of subscriptions:
* - XXX users
* - XXX teams
* - XXX team roles (2 roles per team)
* - 1 role:guest
* - 1 role:member
*/
$this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']);
/**
* Check for connections
* - Authenticated
* - Guests
*/
$this->assertCount($this->connectionsTotal, $this->connections);
Realtime::unsubscribe(-1, $this->subscriptions, $this->connections);
$this->assertCount($this->connectionsTotal, $this->connections);
$this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']);
for ($i = 0; $i < $this->connectionsCount; $i++) {
Realtime::unsubscribe($i, $this->subscriptions, $this->connections);
$this->assertCount(($this->connectionsCount - $i - 1), $this->connections);
}
$this->assertEmpty($this->connections);
$this->assertEmpty($this->subscriptions);
}
/**
* Tests Wildcard (*) Permissions on every channel.
*/
public function testWildcardPermission()
{
foreach ($this->allChannels as $index => $channel) {
$event = [
'project' => '1',
'permissions' => ['*'],
'data' => [
'channels' => [
0 => $channel,
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
/**
* Every Client subscribed to the Wildcard should receive this event.
*/
$this->assertCount($this->connectionsTotal / count($this->allChannels), $receivers, $channel);
foreach ($receivers as $receiver) {
/**
* Making sure the right clients receive the event.
*/
$this->assertStringEndsWith($index, $receiver);
}
}
}
public function testRolePermissions()
{
$roles = ['role:guest', 'role:member'];
foreach ($this->allChannels as $index => $channel) {
foreach ($roles as $role) {
$permissions = [$role];
$event = [
'project' => '1',
'permissions' => $permissions,
'data' => [
'channels' => [
0 => $channel,
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
/**
* Every Role subscribed to a Channel should receive this event.
*/
$this->assertCount($this->connectionsPerChannel, $receivers, $channel);
foreach ($receivers as $receiver) {
/**
* Making sure the right clients receive the event.
*/
$this->assertStringEndsWith($index, $receiver);
}
}
}
}
public function testUserPermissions()
{
foreach ($this->allChannels as $index => $channel) {
$permissions = [];
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
$permissions[] = 'user:user' . (!empty($i) ? $i : '') . $index;
}
$event = [
'project' => '1',
'permissions' => $permissions,
'data' => [
'channels' => [
0 => $channel,
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
/**
* Every Client subscribed to a Channel should receive this event.
*/
$this->assertCount($this->connectionsAuthenticated / count($this->allChannels), $receivers, $channel);
foreach ($receivers as $receiver) {
/**
* Making sure the right clients receive the event.
*/
$this->assertStringEndsWith($index, $receiver);
}
}
}
public function testTeamPermissions()
{
foreach ($this->allChannels as $index => $channel) {
$permissions = [];
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
$permissions[] = 'team:team' . $i;
}
$event = [
'project' => '1',
'permissions' => $permissions,
'data' => [
'channels' => [
0 => $channel,
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
/**
* Every Team Member should receive this event.
*/
$this->assertCount($this->connectionsAuthenticated / count($this->allChannels), $receivers, $channel);
foreach ($receivers as $receiver) {
/**
* Making sure the right clients receive the event.
*/
$this->assertStringEndsWith($index, $receiver);
}
$permissions = ['team:team' . $index . '/' . (empty($index % 2) ? 'admin' : 'member')];
$event = [
'project' => '1',
'permissions' => $permissions,
'data' => [
'channels' => [
0 => $channel,
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
/**
* Only 1 Team Member of a role should have access to a specific channel.
*/
$this->assertCount(1, $receivers, $channel);
foreach ($receivers as $receiver) {
/**
* Making sure the right clients receive the event.
*/
$this->assertStringEndsWith($index, $receiver);
}
}
}
}

View file

@ -0,0 +1,201 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Database\Document;
use Appwrite\Realtime\Realtime;
use PHPUnit\Framework\TestCase;
class RealtimeGuestTest extends TestCase
{
public $connections = [];
public $subscriptions = [];
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testGuest()
{
Realtime::setUser(new Document([
'$id' => ''
]));
$roles = Realtime::getRoles();
$this->assertCount(1, $roles);
$this->assertContains('role:guest', $roles);
$channels = [
0 => 'files',
1 => 'documents',
2 => 'documents.789',
3 => 'account',
4 => 'account.456'
];
$channels = Realtime::parseChannels($channels);
$this->assertCount(3, $channels);
$this->assertArrayHasKey('files', $channels);
$this->assertArrayHasKey('documents', $channels);
$this->assertArrayHasKey('documents.789', $channels);
$this->assertArrayNotHasKey('account', $channels);
$this->assertArrayNotHasKey('account.456', $channels);
Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels);
$event = [
'project' => '1',
'permissions' => ['*'],
'data' => [
'channels' => [
0 => 'documents',
1 => 'documents',
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['role:guest'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['role:member'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['user:123'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:abc'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:abc/administrator'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:abc/god'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:def'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:def/guest'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['user:456'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:def/member'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['*'];
$event['data']['channels'] = ['documents.123'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['data']['channels'] = ['documents.789'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['project'] = '2';
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
Realtime::unsubscribe(2, $this->subscriptions, $this->connections);
$this->assertCount(1, $this->connections);
$this->assertCount(1, $this->subscriptions['1']);
Realtime::unsubscribe(1, $this->subscriptions, $this->connections);
$this->assertEmpty($this->connections);
$this->assertEmpty($this->subscriptions);
}
}

View file

@ -0,0 +1,220 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Database\Document;
use Appwrite\Realtime\Realtime;
use PHPUnit\Framework\TestCase;
class RealtimeTest extends TestCase
{
public $connections = [];
public $subscriptions = [];
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testUser()
{
Realtime::setUser(new Document([
'$id' => '123',
'memberships' => [
[
'teamId' => 'abc',
'roles' => [
'administrator',
'god'
]
],
[
'teamId' => 'def',
'roles' => [
'guest'
]
]
]
]));
$roles = Realtime::getRoles();
$this->assertCount(7, $roles);
$this->assertContains('user:123', $roles);
$this->assertContains('role:member', $roles);
$this->assertContains('team:abc', $roles);
$this->assertContains('team:abc/administrator', $roles);
$this->assertContains('team:abc/god', $roles);
$this->assertContains('team:def', $roles);
$this->assertContains('team:def/guest', $roles);
$channels = [
0 => 'files',
1 => 'documents',
2 => 'documents.789',
3 => 'account',
4 => 'account.456'
];
$channels = Realtime::parseChannels($channels);
$this->assertCount(4, $channels);
$this->assertArrayHasKey('files', $channels);
$this->assertArrayHasKey('documents', $channels);
$this->assertArrayHasKey('documents.789', $channels);
$this->assertArrayHasKey('account.123', $channels);
$this->assertArrayNotHasKey('account', $channels);
$this->assertArrayNotHasKey('account.456', $channels);
Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels);
$event = [
'project' => '1',
'permissions' => ['*'],
'data' => [
'channels' => [
0 => 'account.123',
]
]
];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['role:member'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['user:123'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['team:abc'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['team:abc/administrator'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['team:abc/god'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['team:def'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['team:def/guest'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['permissions'] = ['user:456'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['team:def/member'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['permissions'] = ['*'];
$event['data']['channels'] = ['documents.123'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
$event['data']['channels'] = ['documents.789'];
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['project'] = '2';
$receivers = Realtime::identifyReceivers(
$event,
$this->subscriptions
);
$this->assertEmpty($receivers);
Realtime::unsubscribe(2, $this->subscriptions, $this->connections);
$this->assertCount(1, $this->connections);
$this->assertCount(7, $this->subscriptions['1']);
Realtime::unsubscribe(1, $this->subscriptions, $this->connections);
$this->assertEmpty($this->connections);
$this->assertEmpty($this->subscriptions);
}
}