diff --git a/_build/test/Tests/Processors/Workspace/Packages/PackagesPurgeRemoveProcessorsTest.php b/_build/test/Tests/Processors/Workspace/Packages/PackagesPurgeRemoveProcessorsTest.php
new file mode 100644
index 00000000000..147831a32d9
--- /dev/null
+++ b/_build/test/Tests/Processors/Workspace/Packages/PackagesPurgeRemoveProcessorsTest.php
@@ -0,0 +1,284 @@
+ */
+ private array $tempRoots = [];
+
+ protected function tearDown(): void
+ {
+ foreach ($this->tempRoots as $root) {
+ $this->deleteDirectory($root);
+ }
+ $this->tempRoots = [];
+ parent::tearDown();
+ }
+
+ private function deleteDirectory(string $path): void
+ {
+ if (!is_dir($path)) {
+ return;
+ }
+ $path = rtrim($path, '/');
+ foreach (glob($path . '/*') ?: [] as $item) {
+ is_dir($item) ? $this->deleteDirectory($item) : @unlink($item);
+ }
+ @rmdir($path);
+ }
+
+ /**
+ * Creates a workspace directory layout and a mocked package pointing at it.
+ *
+ * @return array{0: modTransportPackage, 1: string, 2: string} package, zip path, unpacked dir path
+ */
+ private function makeWorkspaceWithPackageFiles(
+ bool $withZip,
+ bool $withDir,
+ string $zipFileName = 'demo-1.0.0-pl.transport.zip'
+ ): array {
+ $base = sys_get_temp_dir() . '/modx_pkg_ws_' . uniqid('', true);
+ $workspacePath = $base . '/ws/';
+ $packagesDir = $workspacePath . 'packages';
+ mkdir($packagesDir, 0777, true);
+ $zipPath = $packagesDir . '/' . $zipFileName;
+ $dirPath = $packagesDir . '/' . basename($zipFileName, '.transport.zip') . '/';
+ if ($withZip) {
+ touch($zipPath);
+ }
+ if ($withDir) {
+ mkdir($dirPath, 0777, true);
+ }
+ $this->tempRoots[] = $base;
+
+ $workspace = $this->createMock(modWorkspace::class);
+ $workspace->method('get')->with('path')->willReturn($workspacePath);
+
+ $signature = basename($zipFileName, '.transport.zip');
+ $package = $this->createMock(modTransportPackage::class);
+ $package->method('getOne')->with('Workspace')->willReturn($workspace);
+ $package->method('get')->willReturnCallback(static function ($key) use ($zipFileName, $signature) {
+ if ($key === 'source') {
+ return $zipFileName;
+ }
+ if ($key === 'signature') {
+ return $signature;
+ }
+
+ return null;
+ });
+ $package->method('getPrimaryKey')->willReturn($signature);
+
+ return [$package, $zipPath, $dirPath];
+ }
+
+ private function createModxShell(?modError $error = null): modX
+ {
+ $modx = $this->createMock(modX::class);
+ $modx->method('lexicon')->willReturnCallback(static fn ($key) => $key);
+ $modx->method('log')->willReturn(null);
+ if ($error !== null) {
+ $modx->error = $error;
+ }
+
+ return $modx;
+ }
+
+ // --- Remove::initialize ---
+
+ public function testRemoveInitializeFailsWhenSignatureMissing(): void
+ {
+ $modx = $this->createModxShell($this->createMock(modError::class));
+ $remove = new Remove($modx, ['signature' => '']);
+
+ $this->assertSame('package_err_ns', $remove->initialize());
+ }
+
+ public function testRemoveInitializeFailsWhenPackageNotFound(): void
+ {
+ $modx = $this->createModxShell($this->createMock(modError::class));
+ $modx->expects($this->once())
+ ->method('getObject')
+ ->with(modTransportPackage::class, 'missing')
+ ->willReturn(null);
+ $remove = new Remove($modx, ['signature' => 'missing']);
+
+ $this->assertSame('package_err_nf', $remove->initialize());
+ }
+
+ public function testRemoveInitializeLoadsPackageAndNormalizesForceTrueString(): void
+ {
+ $modx = $this->createModxShell($this->createMock(modError::class));
+ $pkg = $this->createMock(modTransportPackage::class);
+ $modx->method('getObject')->willReturn($pkg);
+ $remove = new Remove($modx, ['signature' => 'demo-1.0.0-pl', 'force' => 'true']);
+
+ $this->assertTrue($remove->initialize());
+ $this->assertSame($pkg, $remove->package);
+ $this->assertTrue($remove->getProperty('force'));
+ }
+
+ // --- Remove::process ---
+
+ public function testRemoveProcessReturnsFailureWhenBothRemovalsFailWithZipAndDirPresent(): void
+ {
+ [$pkg, ,] = $this->makeWorkspaceWithPackageFiles(true, true);
+ $pkg->expects($this->once())->method('removePackage')->with(false)->willReturn(false);
+ $pkg->expects($this->once())->method('remove')->willReturn(false);
+
+ $error = $this->createMock(modError::class);
+ $error->expects($this->once())->method('failure')->willReturn(['success' => false, 'failed' => true]);
+
+ $modx = $this->createModxShell($error);
+ $modx->expects($this->never())->method('invokeEvent');
+
+ $remove = new RemoveProcessorTestDouble($modx, ['signature' => 'x', 'force' => false]);
+ $remove->package = $pkg;
+
+ $result = $remove->process();
+
+ $this->assertIsArray($result);
+ $this->assertFalse($result['success']);
+ $this->assertSame([], $remove->filesystemOperations);
+ }
+
+ public function testRemoveProcessSucceedsWhenRemovePackageFailsButRemoveSucceeds(): void
+ {
+ [$pkg, $zipPath, $dirPath] = $this->makeWorkspaceWithPackageFiles(true, true);
+ $pkg->expects($this->once())->method('removePackage')->with(false)->willReturn(false);
+ $pkg->expects($this->once())->method('remove')->willReturn(true);
+
+ $error = $this->createMock(modError::class);
+ $error->method('success')->willReturn(['success' => true]);
+
+ $modx = $this->createModxShell($error);
+ $modx->expects($this->once())->method('invokeEvent')->with(
+ 'OnPackageRemove',
+ $this->callback(static function (array $payload) use ($pkg) {
+ return isset($payload['package']) && $payload['package'] === $pkg;
+ })
+ );
+
+ $remove = new RemoveProcessorTestDouble($modx, ['signature' => 'x', 'force' => false]);
+ $remove->package = $pkg;
+
+ $result = $remove->process();
+
+ $this->assertIsArray($result);
+ $this->assertTrue($result['success']);
+ $this->assertSame(
+ [['zip', $zipPath], ['dir', $dirPath]],
+ $remove->filesystemOperations
+ );
+ }
+
+ public function testRemoveProcessSucceedsWhenZipAndDirMissingAndRemoveSucceeds(): void
+ {
+ [$pkg, $zipPath, $dirPath] = $this->makeWorkspaceWithPackageFiles(false, false);
+ $pkg->expects($this->never())->method('removePackage');
+ $pkg->expects($this->once())->method('remove')->willReturn(true);
+
+ $error = $this->createMock(modError::class);
+ $error->method('success')->willReturn(['success' => true]);
+
+ $modx = $this->createModxShell($error);
+ $modx->expects($this->once())->method('invokeEvent');
+
+ $remove = new RemoveProcessorTestDouble($modx, ['signature' => 'x', 'force' => false]);
+ $remove->package = $pkg;
+
+ $remove->process();
+
+ $this->assertSame(
+ [['zip', $zipPath], ['dir', $dirPath]],
+ $remove->filesystemOperations
+ );
+ }
+
+ // --- Purge::removePackage ---
+
+ public function testPurgeRemovePackageSkipsFilesystemWhenZipDirPresentAndBothDatabaseRemovalsFail(): void
+ {
+ [$pkg, ,] = $this->makeWorkspaceWithPackageFiles(true, true);
+ $pkg->expects($this->once())->method('removePackage')->with(true)->willReturn(false);
+ $pkg->expects($this->once())->method('remove')->willReturn(false);
+
+ $modx = $this->createModxShell($this->createMock(modError::class));
+ $modx->expects($this->never())->method('invokeEvent');
+
+ $purge = new PurgeProcessorTestDouble($modx, []);
+ $purge->removePackage($pkg);
+
+ $this->assertSame([], $purge->filesystemOperations);
+ }
+
+ public function testPurgeRemovePackageRunsFilesystemCleanupWhenOnlyRemovePackageFailsThenRemoveSucceeds(): void
+ {
+ [$pkg, $zipPath, $dirPath] = $this->makeWorkspaceWithPackageFiles(true, true);
+ $pkg->expects($this->once())->method('removePackage')->with(true)->willReturn(false);
+ $pkg->expects($this->once())->method('remove')->willReturn(true);
+
+ $modx = $this->createModxShell($this->createMock(modError::class));
+ $modx->expects($this->once())->method('invokeEvent')->with(
+ 'OnPackageRemove',
+ $this->callback(static function (array $payload) use ($pkg) {
+ return isset($payload['package']) && $payload['package'] === $pkg;
+ })
+ );
+
+ $purge = new PurgeProcessorTestDouble($modx, []);
+ $purge->removePackage($pkg);
+
+ $this->assertSame(
+ [['zip', $zipPath], ['dir', $dirPath]],
+ $purge->filesystemOperations
+ );
+ }
+
+ public function testPurgeRemovePackageRunsFilesystemCleanupWhenZipAndDirMissing(): void
+ {
+ [$pkg, $zipPath, $dirPath] = $this->makeWorkspaceWithPackageFiles(false, false);
+ $pkg->expects($this->never())->method('removePackage');
+ $pkg->expects($this->once())->method('remove')->willReturn(true);
+
+ $modx = $this->createModxShell($this->createMock(modError::class));
+ $modx->expects($this->once())->method('invokeEvent');
+
+ $purge = new PurgeProcessorTestDouble($modx, []);
+ $purge->removePackage($pkg);
+
+ $this->assertSame(
+ [['zip', $zipPath], ['dir', $dirPath]],
+ $purge->filesystemOperations
+ );
+ }
+}
diff --git a/_build/test/Tests/Processors/Workspace/Packages/Support/PurgeProcessorTestDouble.php b/_build/test/Tests/Processors/Workspace/Packages/Support/PurgeProcessorTestDouble.php
new file mode 100644
index 00000000000..c43a6e85150
--- /dev/null
+++ b/_build/test/Tests/Processors/Workspace/Packages/Support/PurgeProcessorTestDouble.php
@@ -0,0 +1,35 @@
+filesystemOperations[] = ['zip', $transportZip];
+ }
+
+ public function removeTransportDirectory(string $transportDir): void
+ {
+ $this->filesystemOperations[] = ['dir', $transportDir];
+ }
+}
diff --git a/_build/test/Tests/Processors/Workspace/Packages/Support/RemoveProcessorTestDouble.php b/_build/test/Tests/Processors/Workspace/Packages/Support/RemoveProcessorTestDouble.php
new file mode 100644
index 00000000000..66108431132
--- /dev/null
+++ b/_build/test/Tests/Processors/Workspace/Packages/Support/RemoveProcessorTestDouble.php
@@ -0,0 +1,49 @@
+filesystemOperations[] = ['zip', $transportZip];
+ }
+
+ public function removeTransportDirectory(string $transportDir): void
+ {
+ $this->filesystemOperations[] = ['dir', $transportDir];
+ }
+
+ public function clearCache(): void
+ {
+ /* Parent calls sleep(2); skipped for fast unit tests. */
+ }
+
+ public function cleanup(): array
+ {
+ $this->modx->invokeEvent('OnPackageRemove', [
+ 'package' => $this->package,
+ ]);
+
+ return $this->success();
+ }
+}
diff --git a/_build/test/Tests/Processors/Workspace/Packages/Support/TransportPackageFilesystemPathTestProxy.php b/_build/test/Tests/Processors/Workspace/Packages/Support/TransportPackageFilesystemPathTestProxy.php
new file mode 100644
index 00000000000..25d9c3fdd45
--- /dev/null
+++ b/_build/test/Tests/Processors/Workspace/Packages/Support/TransportPackageFilesystemPathTestProxy.php
@@ -0,0 +1,40 @@
+resolveTransportPaths($package);
+ }
+}
diff --git a/_build/test/Tests/Processors/Workspace/Packages/TransportPackageFilesystemTraitTest.php b/_build/test/Tests/Processors/Workspace/Packages/TransportPackageFilesystemTraitTest.php
new file mode 100644
index 00000000000..c9ad2413256
--- /dev/null
+++ b/_build/test/Tests/Processors/Workspace/Packages/TransportPackageFilesystemTraitTest.php
@@ -0,0 +1,157 @@
+createMock(modX::class);
+ if ($corePathForFallback !== null) {
+ $modx->method('getOption')->with('core_path', null, '')->willReturn($corePathForFallback);
+ }
+
+ return $modx;
+ }
+
+ /**
+ * @param string|null $source value from package get('source'); null means omit from callback (simulate empty)
+ * @param string $signature package signature when source is empty
+ */
+ private function mockPackage(
+ ?modWorkspace $workspace,
+ ?string $source,
+ string $signature = 'foo-1.0.0-pl'
+ ): modTransportPackage {
+ $package = $this->createMock(modTransportPackage::class);
+ $package->method('getOne')->with('Workspace')->willReturn($workspace);
+ $package->method('get')->willReturnCallback(function ($key) use ($source, $signature) {
+ if ($key === 'source') {
+ return $source;
+ }
+ if ($key === 'signature') {
+ return $signature;
+ }
+
+ return null;
+ });
+
+ return $package;
+ }
+
+ private function mockWorkspace(string $path): modWorkspace
+ {
+ $workspace = $this->createMock(modWorkspace::class);
+ $workspace->method('get')->with('path')->willReturn($path);
+
+ return $workspace;
+ }
+
+ public function testResolvePathsUsesWorkspaceBaseAndNormalizesTrailingSlash(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx());
+ $ws = $this->mockWorkspace('/var/modx/ws/');
+ $pkg = $this->mockPackage($ws, 'bar-2.0.0-pl.transport.zip');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/var/modx/ws/packages/bar-2.0.0-pl.transport.zip', $paths['transportZip']);
+ $this->assertSame('/var/modx/ws/packages/bar-2.0.0-pl/', $paths['transportDir']);
+ }
+
+ public function testResolvePathsAddsPackagesSegmentWhenWorkspacePathHasNoTrailingSlash(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx());
+ $ws = $this->mockWorkspace('/var/modx/ws');
+ $pkg = $this->mockPackage($ws, 'baz-1.0.0-pl.transport.zip');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/var/modx/ws/packages/baz-1.0.0-pl.transport.zip', $paths['transportZip']);
+ $this->assertSame('/var/modx/ws/packages/baz-1.0.0-pl/', $paths['transportDir']);
+ }
+
+ public function testResolvePathsUsesBasenameWhenSourceContainsSubdirectory(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx());
+ $ws = $this->mockWorkspace('/ws/');
+ $pkg = $this->mockPackage($ws, 'nested/dir/pkg-1.0.0-pl.transport.zip');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/ws/packages/pkg-1.0.0-pl.transport.zip', $paths['transportZip']);
+ $this->assertSame('/ws/packages/pkg-1.0.0-pl/', $paths['transportDir']);
+ }
+
+ public function testResolvePathsDerivesZipNameFromSignatureWhenSourceEmpty(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx());
+ $ws = $this->mockWorkspace('/ws/');
+ $pkg = $this->mockPackage($ws, '', 'myext-3.1.0-pl');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/ws/packages/myext-3.1.0-pl.transport.zip', $paths['transportZip']);
+ $this->assertSame('/ws/packages/myext-3.1.0-pl/', $paths['transportDir']);
+ }
+
+ public function testResolvePathsFallsBackToCorePathWhenNoWorkspace(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx('/test/core'));
+ $pkg = $this->mockPackage(null, 'orphan-1.0.0-pl.transport.zip');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/test/core/packages/orphan-1.0.0-pl.transport.zip', $paths['transportZip']);
+ $this->assertSame('/test/core/packages/orphan-1.0.0-pl/', $paths['transportDir']);
+ }
+
+ public function testResolvePathsCorePathFallbackNormalizesTrailingSlash(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx('/test/core/'));
+ $pkg = $this->mockPackage(null, 'x-1.0.0-pl.transport.zip');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/test/core/packages/x-1.0.0-pl.transport.zip', $paths['transportZip']);
+ }
+
+ public function testResolvePathsNullSourceUsesSignature(): void
+ {
+ $proxy = new TransportPackageFilesystemPathTestProxy($this->mockModx());
+ $ws = $this->mockWorkspace('/ws/');
+ $pkg = $this->mockPackage($ws, null, 'sig-1.0.0-pl');
+
+ $paths = $proxy->resolvePaths($pkg);
+
+ $this->assertSame('/ws/packages/sig-1.0.0-pl.transport.zip', $paths['transportZip']);
+ $this->assertSame('/ws/packages/sig-1.0.0-pl/', $paths['transportDir']);
+ }
+}
diff --git a/_build/test/phpunit.xml b/_build/test/phpunit.xml
index 1eaade10d49..7a7cb29133e 100644
--- a/_build/test/phpunit.xml
+++ b/_build/test/phpunit.xml
@@ -45,6 +45,7 @@
Tests/Processors/Context
Tests/Processors/Element
Tests/Processors/Resource
+ Tests/Processors/Workspace
Tests/Transport
diff --git a/core/src/Revolution/Processors/Workspace/Packages/Purge.php b/core/src/Revolution/Processors/Workspace/Packages/Purge.php
index 02d3ca4159b..62143add823 100644
--- a/core/src/Revolution/Processors/Workspace/Packages/Purge.php
+++ b/core/src/Revolution/Processors/Workspace/Packages/Purge.php
@@ -1,4 +1,5 @@
modx->log(xPDO::LOG_LEVEL_INFO,
- $this->modx->lexicon('packages_purge_info_gpurge', ['signature' => $package->signature]));
-
- $transportZip = $this->modx->getOption('core_path') . 'packages/' . $package->signature . '.transport.zip';
- $transportDir = $this->modx->getOption('core_path') . 'packages/' . $package->signature . '/';
- if (file_exists($transportZip) && file_exists($transportDir)) {
- /* remove transport package */
- if ($package->remove() === false) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR,
- $this->modx->lexicon('package_err_remove', ['signature' => $package->getPrimaryKey()]));
- $this->failure($this->modx->lexicon('package_err_remove', ['signature' => $package->getPrimaryKey()]));
- return;
- }
- } else {
- /* for some reason the files were removed, so just remove the DB object instead */
- $package->remove();
+ $this->modx->log(
+ xPDO::LOG_LEVEL_INFO,
+ $this->modx->lexicon('packages_purge_info_gpurge', ['signature' => $package->get('signature')])
+ );
+
+ $paths = $this->resolveTransportPaths($package);
+ $transportZip = $paths['transportZip'];
+ $transportDir = $paths['transportDir'];
+
+ $zipExists = file_exists($transportZip);
+ $dirExists = is_dir($transportDir);
+
+ $result = $this->attemptRemovePackageFromDatabase($package, $zipExists, $dirExists);
+
+ if ($result['skipFilesystemCleanup']) {
+ return;
}
$this->removeTransportZip($transportZip);
@@ -143,37 +150,45 @@ public function removePackage($package)
}
/**
- * Remove the transport package archive
- * @param string $transportZip
- * @return void
+ * When both zip and unpacked dir exist: removePackage(true), then remove() if needed; if both fail,
+ * skip disk cleanup (legacy purge behaviour).
+ *
+ * When either artifact is missing: only remove(). If that fails, disk cleanup still runs so orphan
+ * transport files under resolved paths are removed even though the DB row may remain.
+ *
+ * @return array{skipFilesystemCleanup: bool}
*/
- public function removeTransportZip($transportZip)
- {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tzip_start'));
- if (!file_exists($transportZip)) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tzip_nf'));
- } else if (!@unlink($transportZip)) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tzip'));
- } else {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tzip'));
+ private function attemptRemovePackageFromDatabase(
+ modTransportPackage $package,
+ bool $zipExists,
+ bool $dirExists
+ ): array {
+ if ($zipExists && $dirExists) {
+ if ($package->removePackage(true) !== false) {
+ return ['skipFilesystemCleanup' => false];
+ }
+ $this->logPackageRemoveError($package);
+ if ($package->remove() !== false) {
+ return ['skipFilesystemCleanup' => false];
+ }
+
+ return ['skipFilesystemCleanup' => true];
}
+
+ if ($package->remove() !== false) {
+ return ['skipFilesystemCleanup' => false];
+ }
+ $this->logPackageRemoveError($package);
+
+ return ['skipFilesystemCleanup' => false];
}
- /**
- * Remove the transport package directory
- * @param string $transportDir
- * @return void
- */
- public function removeTransportDirectory($transportDir)
+ private function logPackageRemoveError(modTransportPackage $package): void
{
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tdir_start'));
- if (!file_exists($transportDir)) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tdir_nf'));
- } else if (!$this->modx->cacheManager->deleteTree($transportDir, true, false, [])) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tdir'));
- } else {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tdir'));
- }
+ $this->modx->log(
+ xPDO::LOG_LEVEL_ERROR,
+ $this->modx->lexicon('package_err_remove', ['signature' => $package->getPrimaryKey()])
+ );
}
/**
diff --git a/core/src/Revolution/Processors/Workspace/Packages/Remove.php b/core/src/Revolution/Processors/Workspace/Packages/Remove.php
index b2919170429..e54ba8656d9 100644
--- a/core/src/Revolution/Processors/Workspace/Packages/Remove.php
+++ b/core/src/Revolution/Processors/Workspace/Packages/Remove.php
@@ -1,4 +1,5 @@
modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_gpack'));
- $transportZip = $this->modx->getOption('core_path') . 'packages/' . $this->package->signature . '.transport.zip';
- $transportDir = $this->modx->getOption('core_path') . 'packages/' . $this->package->signature . '/';
- if (file_exists($transportZip) && file_exists($transportDir)) {
- /* remove transport package */
+ $paths = $this->resolveTransportPaths($this->package);
+ $transportZip = $paths['transportZip'];
+ $transportDir = $paths['transportDir'];
+
+ $zipExists = file_exists($transportZip);
+ $dirExists = is_dir($transportDir);
+
+ $removeFailed = false;
+ if ($zipExists && $dirExists) {
if ($this->package->removePackage($this->getProperty('force')) === false) {
$packageSignature = $this->package->getPrimaryKey();
- $this->modx->log(xPDO::LOG_LEVEL_ERROR,
- $this->modx->lexicon('package_err_remove', ['signature' => $packageSignature]));
- return $this->failure($this->modx->lexicon('package_err_remove', ['signature' => $packageSignature]));
+ $this->modx->log(
+ xPDO::LOG_LEVEL_ERROR,
+ $this->modx->lexicon('package_err_remove', ['signature' => $packageSignature])
+ );
+ if ($this->package->remove() === false) {
+ $removeFailed = true;
+ }
}
- } else {
- /* for some reason the files were removed, so just remove the DB object instead */
- $this->package->remove();
+ } elseif ($this->package->remove() === false) {
+ $removeFailed = true;
+ }
+
+ if ($removeFailed) {
+ return $this->failure($this->modx->lexicon('package_err_remove', [
+ 'signature' => $this->package->getPrimaryKey(),
+ ]));
}
$this->clearCache();
@@ -110,40 +130,6 @@ public function clearCache()
sleep(2);
}
- /**
- * Remove the transport package archive
- * @param string $transportZip
- * @return void
- */
- public function removeTransportZip($transportZip)
- {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tzip_start'));
- if (!file_exists($transportZip)) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tzip_nf'));
- } else if (!@unlink($transportZip)) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tzip'));
- } else {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tzip'));
- }
- }
-
- /**
- * Remove the transport package directory
- * @param string $transportDir
- * @return void
- */
- public function removeTransportDirectory($transportDir)
- {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tdir_start'));
- if (!file_exists($transportDir)) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tdir_nf'));
- } else if (!$this->modx->cacheManager->deleteTree($transportDir, true, false, [])) {
- $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tdir'));
- } else {
- $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tdir'));
- }
- }
-
/**
* Cleanup and return the result
* @return array
diff --git a/core/src/Revolution/Processors/Workspace/Packages/TransportPackageFilesystemTrait.php b/core/src/Revolution/Processors/Workspace/Packages/TransportPackageFilesystemTrait.php
new file mode 100644
index 00000000000..a51a233a730
--- /dev/null
+++ b/core/src/Revolution/Processors/Workspace/Packages/TransportPackageFilesystemTrait.php
@@ -0,0 +1,95 @@
+modx (modX) from Processor.
+ */
+trait TransportPackageFilesystemTrait
+{
+ /**
+ * Absolute paths to the .transport.zip and unpacked transport directory for this package row.
+ *
+ * Unpacked folder name follows xPDOTransport: basename of archive without .transport.zip suffix.
+ *
+ * @return array{transportZip: string, transportDir: string}
+ */
+ protected function resolveTransportPaths(modTransportPackage $package): array
+ {
+ $workspace = $package->getOne('Workspace');
+ if ($workspace !== null) {
+ $base = rtrim($workspace->get('path'), '/') . '/packages/';
+ } else {
+ /* No workspace: still attempt cleanup under core packages dir (orphan / legacy rows). */
+ $base = rtrim((string)$this->modx->getOption('core_path', null, ''), '/') . '/packages/';
+ }
+
+ $source = $package->get('source');
+ if ($source === null || $source === '') {
+ $source = $package->get('signature') . '.transport.zip';
+ }
+ $zipName = basename($source);
+ $transportZip = $base . $zipName;
+ $dirName = basename($zipName, '.transport.zip');
+ $transportDir = $base . $dirName . '/';
+
+ return [
+ 'transportZip' => $transportZip,
+ 'transportDir' => $transportDir,
+ ];
+ }
+
+ /**
+ * Remove the transport package archive
+ *
+ * Public for backward compatibility with callers that invoked these helpers on Purge/Remove.
+ */
+ public function removeTransportZip(string $transportZip): void
+ {
+ $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tzip_start'));
+ if (!file_exists($transportZip)) {
+ $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tzip_nf'));
+ } elseif (!@unlink($transportZip)) {
+ $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tzip'));
+ } else {
+ $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tzip'));
+ }
+ }
+
+ /**
+ * Remove the transport package directory
+ *
+ * Public for backward compatibility with callers that invoked these helpers on Purge/Remove.
+ */
+ public function removeTransportDirectory(string $transportDir): void
+ {
+ /* Same pattern as clearCache(): ensure cacheManager exists before deleteTree. */
+ $this->modx->getCacheManager();
+ $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tdir_start'));
+ if (!is_dir($transportDir)) {
+ $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tdir_nf'));
+ } elseif (!$this->modx->cacheManager->deleteTree($transportDir, true, false, [])) {
+ $this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->lexicon('package_remove_err_tdir'));
+ } else {
+ $this->modx->log(xPDO::LOG_LEVEL_INFO, $this->modx->lexicon('package_remove_info_tdir'));
+ }
+ }
+}