I performed an investigation and found a reproducible interaction between Ftp.php and Chevereto’s global error-reporting configuration.
Environment
- Chevereto: 4.5.7
- PHP: 8.3.6
- PHP package: php8.3-common 8.3.6-0ubuntu0.24.04.10
- FTP extension enabled
- FTP server reports UNIX
- Passive FTP enabled
- Date-based upload path generated by Chevereto: 2026/09/07/
Observed FTP behavior
Using the active storage credentials and read-only FTP operations:
bucket_cwd_ok=true
year_cwd_ok=true
month_cwd_ok=true
day_cwd_ok=false
pwd_leaf=09
The FTP server behaves according to the PHP FTP contract: attempting to enter the missing 07 directory returns false, emits a warning and leaves the working directory at 09.
Relevant Chevereto code
app/src/Legacy/Classes/Ftp.php, approximately lines 71–78:
public function chdir(string $path): void
{
try {
ftp_chdir($this->ftp, $path);
} catch (Throwable $e) {
throw new Exception("Unable to change dir '{$path}'", 600, $e);
}
}
The return value of ftp_chdir() is not checked.
According to PHP documentation, ftp_chdir() returns false and emits an E_WARNING when the directory change fails:
https://www.php.net/manual/en/function.ftp-chdir.php
The same pattern exists in mkdirRecursive():
try {
ftp_chdir($this->ftp, $cwd);
} catch (Throwable $e) {
try {
ftp_mkdir($this->ftp, $part);
} catch (Throwable $e) {
// ...
}
try {
ftp_chdir($this->ftp, $part);
} catch (Throwable $e) {
// ...
}
}
Neither ftp_chdir() nor ftp_mkdir() has its return value checked.
Interaction with
During application bootstrap, Chevereto executes this in app/src/Legacy/functions.php, approximately line 1600:
if (cheveretoVersionInstalled() !== '') {
error_reporting(0);
Chevereto registers ThrowableHandler::ERROR_AS_EXCEPTION, but that handler contains logic equivalent to:
if (! (error_reporting() & $severity)) {
return;
}
Therefore, after error_reporting(0), FTP warnings are not converted into exceptions.
As a result, the catch blocks in Ftp.php do not run when an FTP function returns false.
Direct reproduction using Chevereto’s installed
I reproduced the issue without creating directories or uploading files:
- Registered Chevereto’s ERROR_AS_EXCEPTION handler.
- Set error_reporting(0), matching the application runtime.
- Connected using the installed Chevereto\Legacy\Classes\Ftp.
- Entered the existing 2026/09/ directory.
- Called Ftp::chdir() for the missing 2026/09/07/.
- Inspected the resulting FTP working directory.
Result:
error_reporting=0
missing_day_threw=false
pwd_leaf_after_missing_day=09
Ftp::chdir() returned normally even though the requested directory did not exist, and the connection remained inside 09.
Resulting upload flow
The likely execution path is:
- Image.php correctly generates 2026/09/07/.
- Storage.php calls:
$API->mkdirRecursive($pathPrefix);
- ftp_chdir() for 07 returns false.
- Because warnings are disabled, no exception is thrown.
- The catch block containing ftp_mkdir() is skipped.
- Ftp:😛ut() again attempts chdir() but ignores its false result.
- The FTP connection remains in 2026/09/.
- ftp_put() successfully uploads the file into the month directory.
- Chevereto stores the expected date path in the database and later tries to load the file from 2026/09/07/.
This exactly matches the observed symptom.
Version comparison
The following logic appears unchanged between the public 4.5.5 and 4.5.7 sources:
- Generation of the Y/m/d/ prefix in Image.php
- Passing keyprefix to Storage::uploadFiles()
- Global error_reporting(0)
- Throwable-handler registration
Therefore, this may be a latent defect exposed after the upgrade rather than a PHP FTP extension regression introduced at the same time.
The FTP extension behaves correctly according to its documented return contract.
Suggested fix
Please check the return values explicitly instead of relying on warnings being converted into exceptions:
public function chdir(string $path): void
{
if (@ftp_chdir($this->ftp, $path) === false) {
throw new Exception("Unable to change dir '{$path}'", 600);
}
}
Inside mkdirRecursive():
if (@ftp_chdir($this->ftp, $cwd)) {
continue;
}
if (@ftp_mkdir($this->ftp, $part) === false) {
throw new Exception(
"Can't make recursive dir for {$part}",
600
);
}
if (@ftp_chdir($this->ftp, $part) === false) {
throw new Exception(
"Unable to change dir to {$part}",
600
);
}
The return value of ftp_put() should also be checked so that a failed transfer cannot be reported as successful.
I used OpenAI Codex, to assist with this diagnosis. 😉