Leantime/leantime · error · Exception
notification.plugin_zip_cant_open
Error message
notification.plugin_zip_cant_open
What it means
The match maps ZipArchive::ER_OPEN to "Zip: Can't open file". Distinct from ER_NOENT (file absent): here the path exists but the OS refused to open it - a permission or policy problem on the temp file itself, since libzip opens it read/write for verification.
Source
Thrown at app/Domain/Plugins/Services/Plugins.php:741
&& ! File::deleteDirectory($pluginDir)
) {
throw new \Exception(__('notification.plugin_cant_remove'));
}
if (! mkdir($pluginDir) && ! is_dir($pluginDir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $pluginDir));
}
$zip = new \ZipArchive;
match ($zip->open($temporaryFile)) {
\ZipArchive::ER_EXISTS => throw new \Exception(__('notification.plugin_zip_exists')),
\ZipArchive::ER_INCONS => throw new \Exception(__('notification.plugin_zip_inconsistent')),
\ZipArchive::ER_INVAL => throw new \Exception(__('notification.plugin_zip_invalid_arg')),
\ZipArchive::ER_MEMORY => throw new \Exception(__('notification.plugin_zip_malloc')),
\ZipArchive::ER_NOENT => throw new \Exception(__('notification.plugin_zip_no_file')),
\ZipArchive::ER_NOZIP => throw new \Exception(__('notification.plugin_zip_not_zip')),
\ZipArchive::ER_OPEN => throw new \Exception(__('notification.plugin_zip_cant_open')),
\ZipArchive::ER_READ => throw new \Exception(__('notification.plugin_zip_read_err')),
\ZipArchive::ER_SEEK => throw new \Exception(__('notification.plugin_zip_seek_err')),
default => throw new \Exception(__('notification.plugin_zip_unknown_err')),
true => null,
};
if (! $zip->extractTo($pluginDir)) {
throw new \Exception(__('notification.plugin_zip_cant_extract'));
}
$zip->close();
unlink($temporaryFile);
// read the composer.json content from the plugin phar file
$pluginModel = $this->createPluginFromComposer($foldername, $plugin->license);
if (! $this->pluginRepository->addPlugin($pluginModel)) {View on GitHub (pinned to 9a9f49f100)
Solutions
- ls -l /tmp/{Folder}.zip and confirm the web server user can read AND write it (libzip reopens the archive).
- Check audit logs for SELinux denials: ausearch -m avc -ts recent; restore context or adjust policy.
- Ensure /tmp is mounted read-write (mount | grep tmp).
- Remove any stale archive (rm -f) so a fresh, properly-owned file is written, then retry.
Defensive patterns
Strategy: validation
Validate before calling
if (! is_readable($temporaryFile)) {
throw new RuntimeException("Temp archive {$temporaryFile} exists but is not readable - permissions/SELinux");
} Prevention
- Ensure the web user can read files it just wrote into the temp dir (sane umask).
- On SELinux hosts, allow the web context read access to /tmp files.
- Mount /tmp read-write.
When it happens
Trigger: installMarketplacePlugin() where the temp zip was written but cannot be opened: restrictive umask making the file unreadable to the effective user in edge setups, SELinux/AppArmor denying open on /tmp files, a read-only filesystem mount for /tmp, or file attributes (immutable) set on a leftover archive.
Common situations: SELinux targeted policy blocking httpd from reading user-created /tmp files; hardened /tmp mounts (ro); leftover temp file with immutable attribute from a previous attempt.
Related errors
- notification.plugin_cant_remove
- Directory "%s" was not created
- notification.plugin_zip_exists
- notification.plugin_zip_inconsistent
- notification.plugin_zip_invalid_arg
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/a3ca26a2ec082cbc.
Report an issue: GitHub.