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

  1. ls -l /tmp/{Folder}.zip and confirm the web server user can read AND write it (libzip reopens the archive).
  2. Check audit logs for SELinux denials: ausearch -m avc -ts recent; restore context or adjust policy.
  3. Ensure /tmp is mounted read-write (mount | grep tmp).
  4. 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

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


AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21). Data as JSON: /api/errors/a3ca26a2ec082cbc. Report an issue: GitHub.