Merge branch '1.8.x' into fix-smtp-auth-check

This commit is contained in:
Chirag Aggarwal
2025-12-16 10:51:01 +05:30
committed by GitHub
20 changed files with 292 additions and 44 deletions
+1
View File
@@ -125,3 +125,4 @@ _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
_APP_TRUSTED_HEADERS=x-forwarded-for
+1
View File
@@ -42,6 +42,7 @@ jobs:
with:
context: .
platforms: linux/amd64,linux/arm64
target: production
build-args: |
VERSION=${{ steps.meta.outputs.version }}
VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1
+3 -2
View File
@@ -20,10 +20,10 @@ jobs:
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -46,6 +46,7 @@ jobs:
with:
context: .
platforms: linux/amd64,linux/arm64
target: production
build-args: |
VERSION=${{ steps.meta.outputs.version }}
push: true
+23 -10
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.10.6 AS final
FROM appwrite/base:0.10.6 AS base
LABEL maintainer="team@appwrite.io"
@@ -36,9 +36,7 @@ COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
COPY ./app /usr/src/code/app
COPY ./public /usr/src/code/public
COPY ./bin /usr/local/bin
COPY ./docs /usr/src/code/docs
COPY ./src /usr/src/code/src
COPY ./dev /usr/src/code/dev
# Set Volumes
RUN mkdir -p /storage/uploads && \
@@ -90,15 +88,30 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/stats-resources && \
chmod +x /usr/local/bin/worker-stats-resources
# Letsencrypt Permissions
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "true" ]; then apk add --update --no-cache openssh-client github-cli; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi
FROM base AS production
RUN rm -rf /usr/src/code/app/config/specs && \
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so && \
find /usr -name '*.a' -delete 2>/dev/null || true && \
find /usr -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
find /usr -name '*.pyc' -delete 2>/dev/null || true
EXPOSE 80
CMD [ "php", "app/http.php" ]
FROM base AS development
COPY ./docs /usr/src/code/docs
COPY ./dev /usr/src/code/dev
RUN if [ "$DEBUG" = "true" ]; then \
cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && \
mkdir -p /tmp/xdebug && \
apk add --update --no-cache openssh-client github-cli; \
fi
EXPOSE 80
+9
View File
@@ -357,6 +357,15 @@ return [
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_TRUSTED_HEADERS',
'description' => 'This option allows you to set the list of trusted headers, the value is a comma‑separated list of HTTP header names, evaluated left-to-right for the first valid IP. Header names are treated case-insensitively.',
'introduction' => '1.8.0',
'default' => 'x-forwarded-for',
'required' => false,
'question' => '',
'filter' => ''
]
],
],
-1
View File
@@ -628,7 +628,6 @@ App::init()
$queueForFunctions->setPlatform($platform);
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
$queueForMigrations->setPlatform($platform);
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
+7 -1
View File
@@ -198,8 +198,14 @@ App::setResource('allowedHostnames', function (array $platform, Document $projec
$allowed[] = $request->getHostname();
}
/* Allow the request origin if a dev key or rule is found */
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
/* Add request hostname for preflight requests */
if ($request->getMethod() === 'OPTIONS') {
$allowed[] = $originHostname;
}
/* Allow the request origin if a dev key or rule is found */
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
$allowed[] = $originHostname;
}
Generated
+15 -15
View File
@@ -4264,16 +4264,16 @@
},
{
"name": "utopia-php/framework",
"version": "0.33.34",
"version": "0.33.35",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "76def92594c32504ec80eaacdb60ff8fad73c856"
"reference": "82b139fb04f30045db51b0d322224f222da32313"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/76def92594c32504ec80eaacdb60ff8fad73c856",
"reference": "76def92594c32504ec80eaacdb60ff8fad73c856",
"url": "https://api.github.com/repos/utopia-php/http/zipball/82b139fb04f30045db51b0d322224f222da32313",
"reference": "82b139fb04f30045db51b0d322224f222da32313",
"shasum": ""
},
"require": {
@@ -4306,9 +4306,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.33.34"
"source": "https://github.com/utopia-php/http/tree/0.33.35"
},
"time": "2025-12-08T07:55:31+00:00"
"time": "2025-12-12T08:33:52+00:00"
},
{
"name": "utopia-php/image",
@@ -5011,22 +5011,22 @@
},
{
"name": "utopia-php/swoole",
"version": "0.8.4",
"version": "0.8.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/swoole.git",
"reference": "150c30700e738c52348cce9ed0e0f0ff96872081"
"reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/150c30700e738c52348cce9ed0e0f0ff96872081",
"reference": "150c30700e738c52348cce9ed0e0f0ff96872081",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/e42b6b8e44c457a7b35d8a857d7af1d67d667c58",
"reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58",
"shasum": ""
},
"require": {
"ext-swoole": "*",
"php": ">=8.0",
"utopia-php/framework": "0.33.*"
"utopia-php/framework": "0.33.35"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -5056,9 +5056,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/swoole/issues",
"source": "https://github.com/utopia-php/swoole/tree/0.8.4"
"source": "https://github.com/utopia-php/swoole/tree/0.8.5"
},
"time": "2025-09-07T09:39:46+00:00"
"time": "2025-12-15T14:03:23+00:00"
},
{
"name": "utopia-php/system",
@@ -8943,7 +8943,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -8967,5 +8967,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+3
View File
@@ -54,6 +54,7 @@ services:
image: appwrite-dev
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
@@ -219,6 +220,7 @@ services:
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
- _APP_CUSTOM_DOMAIN_DENY_LIST
- _APP_TRUSTED_HEADERS
extra_hosts:
- "host.docker.internal:host-gateway"
@@ -735,6 +737,7 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_DATABASE_SHARED_TABLES
- _APP_OPTIONS_FORCE_HTTPS
appwrite-task-maintenance:
entrypoint: maintenance
+7 -1
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
@@ -110,13 +111,18 @@ class Build extends Event
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'resource' => $this->resource,
'deployment' => $this->deployment,
'type' => $this->type,
'template' => $this->template,
'platform' => $this->platform
'platform' => $platform,
];
}
+7 -1
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
@@ -202,6 +203,11 @@ class Func extends Event
{
$events = $this->getEvent() ? Event::generateEvents($this->getEvent(), $this->getParams()) : null;
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'user' => $this->user,
@@ -217,7 +223,7 @@ class Func extends Event
'path' => $this->path,
'headers' => $this->headers,
'method' => $this->method,
'platform' => $this->platform
'platform' => $platform,
];
}
}
+100 -1
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Queue\Publisher;
class Mail extends Event
@@ -16,6 +17,8 @@ class Mail extends Event
protected string $bodyTemplate = '';
protected array $attachment = [];
protected array $customMailOptions = [];
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
@@ -400,6 +403,94 @@ class Mail extends Event
return $this;
}
/**
* Set sender email
*
* @param string $email
* @return self
*/
public function setSenderEmail(string $email): self
{
$this->customMailOptions['senderEmail'] = $email;
return $this;
}
/**
* Get sender email
*
* @return string
*/
public function getSenderEmail(): string
{
return $this->customMailOptions['senderEmail'] ?? '';
}
/**
* Set sender name
*
* @param string $name
* @return self
*/
public function setSenderName(string $name): self
{
$this->customMailOptions['senderName'] = $name;
return $this;
}
/**
* Get sender name
*
* @return string
*/
public function getSenderName(): string
{
return $this->customMailOptions['senderName'] ?? '';
}
/**
* Set reply-to email
*
* @param string $email
* @return self
*/
public function setReplyToEmail(string $email): self
{
$this->customMailOptions['replyToEmail'] = $email;
return $this;
}
/**
* Get reply-to email
*
* @return string
*/
public function getReplyToEmail(): string
{
return $this->customMailOptions['replyToEmail'] ?? '';
}
/**
* Set reply-to name
*
* @param string $name
* @return self
*/
public function setReplyToName(string $name): self
{
$this->customMailOptions['replyToName'] = $name;
return $this;
}
/**
* Get reply-to name
*
* @return string
*/
public function getReplyToName(): string
{
return $this->customMailOptions['replyToName'] ?? '';
}
/**
* Reset
*
@@ -415,6 +506,7 @@ class Mail extends Event
$this->variables = [];
$this->bodyTemplate = '';
$this->attachment = [];
$this->customMailOptions = [];
return $this;
}
@@ -425,6 +517,11 @@ class Mail extends Event
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'recipient' => $this->recipient,
@@ -436,7 +533,9 @@ class Mail extends Event
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
'customMailOptions' => $this->customMailOptions,
'events' => Event::generateEvents($this->getEvent(), $this->getParams()),
'platform' => $platform,
];
}
}
+7 -1
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
@@ -73,11 +74,16 @@ class Migration extends Event
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'user' => $this->user,
'migration' => $this->migration,
'platform' => $this->platform,
'platform' => $platform,
];
}
}
+4 -1
View File
@@ -2,6 +2,8 @@
namespace Appwrite\Network;
use Utopia\Validator\Hostname;
/**
* Generate CORS response headers for an incoming request.
*
@@ -76,7 +78,8 @@ final class Cors
}
// Match only by host
if (!\in_array($host, $this->allowedHosts, true)) {
$validator = new Hostname($this->allowedHosts);
if (!$validator->isValid($host)) {
return $headers;
}
+15 -3
View File
@@ -152,9 +152,21 @@ class Mails extends Action
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
if (!empty($smtp)) {
$replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : $smtp['senderEmail'];
$replyToName = $smtp['senderName'];
$customMailOptions = $payload['customMailOptions'] ?? [];
// fallback hierarchy: Custom options > SMTP config > Defaults.
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
$mail->setFrom($fromEmail, $fromName);
}
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
$replyTo = $customMailOptions['replyToEmail'] ?? $replyTo;
$replyToName = $customMailOptions['replyToName'] ?? $replyToName;
} elseif (!empty($smtp)) {
$replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : ($smtp['senderEmail'] ?? $replyTo);
$replyToName = $smtp['senderName'] ?? $replyToName;
}
$mail->addReplyTo($replyTo, $replyToName);
+19 -7
View File
@@ -52,8 +52,6 @@ class Migrations extends Action
protected array $plan;
protected array $platform;
/**
* @var array<string, int>
*/
@@ -108,7 +106,6 @@ class Migrations extends Action
$this->deviceForMigrations = $deviceForMigrations;
$this->deviceForFiles = $deviceForFiles;
$this->plan = $plan;
$this->platform = $payload['platform'] ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
@@ -144,10 +141,15 @@ class Migrations extends Action
$credentials = $migration->getAttribute('credentials');
$migrationOptions = $migration->getAttribute('options');
$dataSource = Appwrite::SOURCE_API;
$endpoint = $this->platform['endpoint'] ?: ($credentials['endpoint'] ?? 'http://appwrite.test/v1');
$database = null;
$queries = [];
if ($credentials['endpoint'] === 'http://localhost/v1') {
$platform = Config::getParam('platform', []);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$credentials['endpoint'] = $protocol . '://' . $platform['apiHostname'] . '/v1';
}
if ($source === Appwrite::getName() && $destination === DestinationCSV::getName()) {
$dataSource = Appwrite::SOURCE_DATABASE;
$database = $this->dbForProject;
@@ -178,7 +180,7 @@ class Migrations extends Action
),
SourceAppwrite::getName() => new SourceAppwrite(
$credentials['projectId'],
$endpoint,
$credentials['endpoint'],
$credentials['apiKey'],
$dataSource,
$database,
@@ -206,10 +208,13 @@ class Migrations extends Action
$destination = $migration->getAttribute('destination');
$options = $migration->getAttribute('options', []);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$platform = Config::getParam('platform', []);
return match ($destination) {
DestinationAppwrite::getName() => new DestinationAppwrite(
$this->project->getId(),
$this->platform['endpoint'],
$protocol . '://' . $platform['apiHostname'] . '/v1',
$apiKey,
$this->dbForProject,
Config::getParam('collections', [])['databases']['collections'],
@@ -306,6 +311,8 @@ class Migrations extends Action
$transfer = $source = $destination = null;
try {
if (
$migration->getAttribute('source') === SourceAppwrite::getName() &&
@@ -313,8 +320,13 @@ class Migrations extends Action
) {
$credentials = $migration->getAttribute('credentials', []);
$credentials['projectId'] = $credentials['projectId'] ?? $project->getId();
$credentials['endpoint'] = $credentials['endpoint'] ?? $this->platform['endpoint'];
$credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey;
if (empty($credentials['endpoint'])) {
$platform = Config::getParam('platform', []);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$credentials['endpoint'] = $protocol . '://' . $platform['apiHostname'] . '/v1';
}
$migration->setAttribute('credentials', $credentials);
}
+4
View File
@@ -9,6 +9,7 @@ use Swoole\Http\Request as SwooleRequest;
use Utopia\Database\Validator\Authorization;
use Utopia\Route;
use Utopia\Swoole\Request as UtopiaRequest;
use Utopia\System\System;
class Request extends UtopiaRequest
{
@@ -20,6 +21,9 @@ class Request extends UtopiaRequest
public function __construct(SwooleRequest $request)
{
$trustedHeaders = System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for');
$this->setTrustedIpHeaders(explode(',', $trustedHeaders));
parent::__construct($request);
}
+15
View File
@@ -184,7 +184,22 @@ class HTTPTest extends Scope
'origin' => 'http://google.com',
]);
$this->assertNull($response['headers']['access-control-allow-origin'] ?? null);
}
public function testPreflight()
{
$endpoint = '/v1/projects'; // Can be any non-404 route
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_OPTIONS, $endpoint, [
'origin' => 'http://random.com',
'access-control-request-headers' => 'X-Appwrite-Project',
'access-control-request-method' => 'GET'
]);
$this->assertEquals('http://random.com', $response['headers']['access-control-allow-origin']);
}
public function testConsoleRedirect()
@@ -326,4 +326,41 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 204);
}
public function testFallbackForTrustedIp(): void
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
// call appwrite directly to avoid proxy stripping the headers
$this->client->setEndpoint('http://localhost/v1');
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-forwarded-for' => '191.0.113.195',
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-forwarded-for' => '191.0.113.195',
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals('191.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? '');
}
}
+15
View File
@@ -36,6 +36,21 @@ final class CorsTest extends TestCase
$this->assertSame('https://foo.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
}
public function testSubdomainWildcardAllowsAnySubdomain(): void
{
$cors = new Cors(
allowedHosts: ['*.example.com'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false
);
$result = $cors->headers('https://foo.example.com');
$this->assertSame('https://foo.example.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
}
public function testEmptyOriginReturnsStaticHeadersOnly(): void
{
$cors = new Cors(