mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
d2230f8fe7
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:
- `app/controllers/general.php`: path-traversal guard was negating
`\substr(...)` before the strict comparison (`!\substr(...) === $base`
was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
`DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
result as an array with `deviceName/deviceBrand/deviceModel` keys.
Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
key was checking `$resourceKey === 'functions'` when `$resourceKey`
is `'functionId'|'siteId'` — always false. Switched to the intended
`$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
to `string|false` to match `openssl_encrypt`; this lets callers'
`=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
`array_key_exists('from', [])` branch in the Msg91 provider (empty
array literal; branch was unreachable).
Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
`markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
Installer State, `$source` on MigrationsWorker, `$account2` on two
GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
`assertNotNull` assertions with `addToAssertionCount(1)` or
`assertNotEmpty` where the runtime type was already known.
336 lines
9.3 KiB
PHP
336 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\E2E;
|
|
|
|
use Appwrite\Utopia\Fetch\BodyMultipart;
|
|
use Exception;
|
|
|
|
class Client
|
|
{
|
|
public const METHOD_GET = 'GET';
|
|
public const METHOD_POST = 'POST';
|
|
public const METHOD_PUT = 'PUT';
|
|
public const METHOD_PATCH = 'PATCH';
|
|
public const METHOD_DELETE = 'DELETE';
|
|
public const METHOD_HEAD = 'HEAD';
|
|
public const METHOD_OPTIONS = 'OPTIONS';
|
|
public const METHOD_CONNECT = 'CONNECT';
|
|
public const METHOD_TRACE = 'TRACE';
|
|
|
|
/**
|
|
* Is Self Signed Certificates Allowed?
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $selfSigned = false;
|
|
|
|
/**
|
|
* Service host name
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $endpoint = 'https://appwrite.test/v1';
|
|
|
|
/**
|
|
* Global Headers
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $headers = [
|
|
'content-type' => '',
|
|
'x-sdk-version' => 'appwrite:php:v1.0.7',
|
|
];
|
|
|
|
/**
|
|
* SDK constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Set Project
|
|
*
|
|
* Your Appwrite project ID. You can find your project ID in your Appwrite console project settings.
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return self $this
|
|
*/
|
|
public function setProject(string $value): self
|
|
{
|
|
$this->addHeader('X-Appwrite-Project', $value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set Key
|
|
*
|
|
* Your Appwrite project secret key. You can can create a new API key from your Appwrite console API keys dashboard.
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return self $this
|
|
*/
|
|
public function setKey(string $value): self
|
|
{
|
|
$this->addHeader('X-Appwrite-Key', $value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set Locale
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return self $this
|
|
*/
|
|
public function setLocale(string $value): self
|
|
{
|
|
$this->addHeader('X-Appwrite-Locale', $value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set Mode
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return self $this
|
|
*/
|
|
public function setMode(string $value): self
|
|
{
|
|
$this->addHeader('X-Appwrite-Mode', $value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set Response Format
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return self $this
|
|
*/
|
|
public function setResponseFormat(string $value): self
|
|
{
|
|
$this->addHeader('X-Appwrite-Response-Format', $value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param bool $status true
|
|
* @return self $this
|
|
*/
|
|
public function setSelfSigned(bool $status = true): self
|
|
{
|
|
$this->selfSigned = $status;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param string $endpoint
|
|
* @return self $this
|
|
*/
|
|
public function setEndpoint(string $endpoint): self
|
|
{
|
|
$this->endpoint = $endpoint;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getEndpoint(): string
|
|
{
|
|
return $this->endpoint;
|
|
}
|
|
|
|
/**
|
|
* @param string $key
|
|
* @param string $value
|
|
*
|
|
* @return self $this
|
|
*/
|
|
public function addHeader(string $key, string $value): self
|
|
{
|
|
$this->headers[strtolower($key)] = strtolower($value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Call
|
|
*
|
|
* Make an API call
|
|
*
|
|
* @param string $method
|
|
* @param string $path
|
|
* @param array $params
|
|
* @param array $headers
|
|
* @param bool $decode
|
|
* @return array
|
|
* @throws Exception
|
|
*/
|
|
public function call(string $method, string $path = '', array $headers = [], mixed $params = [], bool $decode = true, bool $followRedirects = true): array
|
|
{
|
|
$headers = array_merge($this->headers, $headers);
|
|
$ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
|
|
$responseHeaders = [];
|
|
$cookies = [];
|
|
|
|
if (isset($params['queries'])) {
|
|
foreach ($params['queries'] as $value) {
|
|
if (!is_string($value)) {
|
|
throw new Exception('Queries must be converted to strings');
|
|
}
|
|
}
|
|
}
|
|
|
|
$query = match ($headers['content-type']) {
|
|
'application/json' => json_encode($params),
|
|
'multipart/form-data' => $this->flatten($params),
|
|
'application/graphql' => $params[0],
|
|
'text/plain' => $params,
|
|
default => http_build_query($params),
|
|
};
|
|
|
|
$formattedHeaders = [];
|
|
foreach ($headers as $key => $value) {
|
|
if (strtolower($key) === 'accept-encoding') {
|
|
curl_setopt($ch, CURLOPT_ENCODING, $value);
|
|
continue;
|
|
} else {
|
|
$formattedHeaders[] = $key . ': ' . $value;
|
|
}
|
|
}
|
|
|
|
curl_setopt($ch, CURLOPT_PATH_AS_IS, 1);
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $followRedirects);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36');
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
|
|
curl_setopt($ch, CURLOPT_COOKIEFILE, ''); // enable in-memory RFC 6265 cookie engine
|
|
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
|
$len = strlen($header);
|
|
$header = explode(':', $header, 2);
|
|
|
|
if (count($header) < 2) { // ignore invalid headers
|
|
return $len;
|
|
}
|
|
|
|
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
|
|
|
return $len;
|
|
});
|
|
|
|
|
|
if ($method === self::METHOD_HEAD) {
|
|
curl_setopt($ch, CURLOPT_NOBODY, true); // This is crucial for HEAD requests
|
|
curl_setopt($ch, CURLOPT_HEADER, false);
|
|
} else {
|
|
curl_setopt($ch, CURLOPT_NOBODY, false);
|
|
}
|
|
|
|
if ($method != self::METHOD_GET && $method != self::METHOD_HEAD) {
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
|
|
}
|
|
|
|
// Allow self-signed certificates
|
|
if ($this->selfSigned) {
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
}
|
|
$responseBody = curl_exec($ch);
|
|
$responseType = $responseHeaders['content-type'] ?? '';
|
|
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
foreach (curl_getinfo($ch, CURLINFO_COOKIELIST) as $line) {
|
|
$parts = explode("\t", $line);
|
|
$cookies[$parts[5]] = $parts[6] ?? '';
|
|
}
|
|
|
|
if ($decode && $method !== self::METHOD_HEAD) {
|
|
$strpos = strpos($responseType, ';');
|
|
$strpos = \is_bool($strpos) ? \strlen($responseType) : $strpos;
|
|
switch (substr($responseType, 0, $strpos)) {
|
|
case 'multipart/form-data':
|
|
$boundary = \explode('boundary=', $responseHeaders['content-type'])[1] ?? '';
|
|
$multipartResponse = new BodyMultipart($boundary);
|
|
$multipartResponse->load(\is_bool($responseBody) ? '' : $responseBody);
|
|
|
|
$responseBody = $multipartResponse->getParts();
|
|
break;
|
|
case 'application/json':
|
|
if (\is_bool($responseBody)) {
|
|
throw new Exception('Response is not a valid JSON.');
|
|
}
|
|
|
|
$json = json_decode($responseBody, true);
|
|
|
|
if ($json === null) {
|
|
throw new Exception('Failed to parse response: ' . $responseBody);
|
|
}
|
|
|
|
$responseBody = $json;
|
|
$json = null;
|
|
break;
|
|
}
|
|
} elseif ($method === self::METHOD_HEAD) {
|
|
// For HEAD requests, always set body to empty string regardless of decode flag
|
|
$responseBody = '';
|
|
}
|
|
|
|
if ((curl_errno($ch)/* || 200 != $responseStatus*/)) {
|
|
throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus);
|
|
}
|
|
|
|
curl_close($ch);
|
|
|
|
$responseHeaders['status-code'] = $responseStatus;
|
|
|
|
if ($responseStatus === 500) {
|
|
echo 'Server error(' . $method . ': ' . $path . '. Params: ' . json_encode($params) . '): ' . json_encode($responseBody) . '\n';
|
|
}
|
|
|
|
return [
|
|
'headers' => $responseHeaders,
|
|
'cookies' => $cookies,
|
|
'body' => $responseBody
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Flatten params array to PHP multiple format
|
|
*
|
|
* @param array $data
|
|
* @param string $prefix
|
|
* @return array
|
|
*/
|
|
protected function flatten(array $data, string $prefix = ''): array
|
|
{
|
|
$output = [];
|
|
|
|
foreach ($data as $key => $value) {
|
|
$finalKey = $prefix ? "{$prefix}[{$key}]" : $key;
|
|
|
|
if (is_array($value)) {
|
|
$output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed
|
|
} else {
|
|
$output[$finalKey] = $value;
|
|
}
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
}
|