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
+}

Reply via email to