Author: Shivam Mathur (shivammathur)
Date: 2026-10-02T22:04:55+05:30
Commit: https://github.com/php/web-downloads/commit/165a1ad444b0d3125e1df6bbbf61a616754c79ef
Raw diff: https://github.com/php/web-downloads/commit/165a1ad444b0d3125e1df6bbbf61a616754c79ef.diff
Allow deleting legacy Winlibs deletion jobs
Changed paths:
M API.md
M src/Http/Controllers/DeletePendingJobController.php
M tests/Http/Controllers/DeletePendingJobControllerTest.php
Diff:
diff --git a/API.md b/API.md
index 779e438..d3577cb 100644
--- a/API.md
+++ b/API.md
@@ -64,8 +64,8 @@ curl -i -X GET \
- Auth: Required
- Purpose: Remove a queued build job before it is processed.
- Request body (JSON):
- - type (string, required): One of php, pecl, or
winlibs.
- - job (string, required): The job filename (for
php/pecl) or directory name (for winlibs).
+ - type (string, required): One of php, pecl,
winlibs, or winlibs-delete.
+ - job (string, required): The job filename (for
php/pecl), directory name (for winlibs), or
winlibs-delete-*.json filename (for legacy winlibs-delete jobs).
- Success: 200 OK with { "status": "deleted" }.
- Errors:
- 400 if validation fails (missing/invalid fields).
@@ -85,6 +85,12 @@ curl -i -X POST \
https://downloads.php.net/api/delete-pending-job
```
+To remove a legacy Winlibs deletion request, pass the filename from GET
/api/list-builds without the winlibs-delete/ prefix:
+
+```json
+{"type":"winlibs-delete","job":"winlibs-delete-7coij6somubocMYxf6G.json"}
+```
+
---
### POST /api/php
diff --git a/src/Http/Controllers/DeletePendingJobController.php
b/src/Http/Controllers/DeletePendingJobController.php
index 0162aa9..b275fef 100644
--- a/src/Http/Controllers/DeletePendingJobController.php
+++ b/src/Http/Controllers/DeletePendingJobController.php
@@ -23,8 +23,8 @@ public function __construct(string $inputPath = 'php://input', ?string
$buildsDi
protected function validate(array $data): bool
{
$validator = new Validator([
- 'type' => 'required|string|regex:/^(php|pecl|winlibs)$/i',
- 'job' => 'required|string|regex:/^[A-Za-z0-9._-]+$/',
+ 'type' =>
'required|string|regex:/\A(?:php|pecl|winlibs|winlibs-delete)\z/i',
+ 'job' =>
'required|string|regex:/\A(?!\.{1,2}\z)[A-Za-z0-9._-]+\z/',
]);
$validator->validate($data);
@@ -34,6 +34,11 @@ protected function validate(array $data): bool
if (!$valid) {
http_response_code(400);
echo 'Invalid request: ' . $validator;
+ } elseif (strtolower($data['type']) === 'winlibs-delete'
+ && preg_match('/\Awinlibs-delete-[A-Za-z0-9]+\.json\z/',
$data['job']) !== 1) {
+ http_response_code(400);
+ echo 'Invalid request: job must be a winlibs-delete-*.json filename.';
+ return false;
}
return $valid;
@@ -78,7 +83,7 @@ private function deleteJob(string $type, string $jobName): void
private function resolvePath(string $type, string $jobName): string
{
return match ($type) {
- 'php', 'pecl' => $this->buildsDirectory . '/' .
$type . '/' . $jobName,
+ 'php', 'pecl', 'winlibs-delete' =>
$this->buildsDirectory . '/' . $type . '/' . $jobName,
'winlibs' => $this->buildsDirectory . '/winlibs/' . $jobName,
default => $this->buildsDirectory,
};
diff --git a/tests/Http/Controllers/DeletePendingJobControllerTest.php
b/tests/Http/Controllers/DeletePendingJobControllerTest.php
index a90a2eb..81064b4 100644
--- a/tests/Http/Controllers/DeletePendingJobControllerTest.php
+++ b/tests/Http/Controllers/DeletePendingJobControllerTest.php
@@ -82,6 +82,73 @@ public function testDeletesWinlibsJobDirectory(): void
unlink($inputFile);
}
+ /**
+ * @throws JsonException
+ */
+ public function testDeletesLegacyWinlibsDeleteFileAndLockWithoutTouchingActiveQueue(): void
+ {
+ $legacyDir = $this->tempDir . '/winlibs-delete';
+ $activeDir = $this->tempDir . '/winlibs/delete-active';
+ mkdir($legacyDir, 0755, true);
+ mkdir($activeDir, 0755, true);
+
+ $jobName = 'winlibs-delete-7coij6somubocMYxf6G.json';
+ $jobFile = $legacyDir . '/' . $jobName;
+ file_put_contents($jobFile, '{}');
+ file_put_contents($jobFile . '.lock', '');
+ file_put_contents($activeDir . '/data.json', '{}');
+
+ $inputFile = $this->createInputFile(json_encode([
+ 'type' => 'WINLIBS-DELETE',
+ 'job' => $jobName,
+ ], JSON_THROW_ON_ERROR));
+
+ http_response_code(200);
+ $controller = new DeletePendingJobController($inputFile, $this->tempDir);
+ ob_start();
+ $controller->handle();
+ $output = ob_get_clean();
+
+ static::assertSame(200, http_response_code());
+
static::assertJsonStringEqualsJsonString('{"status":"deleted"}',
$output);
+ static::assertFileDoesNotExist($jobFile);
+ static::assertFileDoesNotExist($jobFile . '.lock');
+ static::assertFileExists($activeDir . '/data.json');
+
+ unlink($inputFile);
+ }
+
+ /**
+ * @throws JsonException
+ */
+ public function testRejectsOtherFilesAndParentDirectoryNames(): void
+ {
+ $legacyDir = $this->tempDir . '/winlibs-delete';
+ mkdir($legacyDir, 0755, true);
+ file_put_contents($legacyDir . '/other.json', '{}');
+
+ foreach ([
+ ['type' => 'winlibs-delete', 'job' =>
'other.json'],
+ ['type' => 'winlibs-delete', 'job' =>
'winlibs-delete-..json'],
+ ['type' => 'winlibs', 'job' => '..'],
+ ] as $request) {
+ $inputFile = $this->createInputFile(json_encode($request, JSON_THROW_ON_ERROR));
+
+ http_response_code(200);
+ $controller = new DeletePendingJobController($inputFile, $this->tempDir);
+ ob_start();
+ $controller->handle();
+ $output = ob_get_clean();
+
+ static::assertSame(400, http_response_code());
+ static::assertStringContainsString('Invalid request', $output);
+ static::assertDirectoryExists($this->tempDir);
+ static::assertFileExists($legacyDir . '/other.json');
+
+ unlink($inputFile);
+ }
+ }
+
/**
* @throws JsonException
*/
@@ -109,4 +176,4 @@ private function createInputFile(string $json): string
return $tempFile;
}
-}
\ No newline at end of file
+}