From da40d6c504964f37518d63134604b2c8eb2341e8 Mon Sep 17 00:00:00 2001 From: Lachlan Reynolds Date: Mon, 13 Apr 2026 12:09:56 +1200 Subject: [PATCH 1/3] Added simple script to fetch API specs --- fetch-openapi-specs.php | 81 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 fetch-openapi-specs.php diff --git a/fetch-openapi-specs.php b/fetch-openapi-specs.php new file mode 100644 index 000000000..0700fc6b1 --- /dev/null +++ b/fetch-openapi-specs.php @@ -0,0 +1,81 @@ + 'OpenApiDocs.getPluginWhitelist', + ]); +} catch (Throwable $e) { + fwrite(STDERR, "Failed to fetch plugin whitelist: {$e->getMessage()}\n"); + exit(1); +} + +$written = 0; + +foreach ($plugins as $plugin) { + if (!is_string($plugin) || $plugin === '') { + continue; + } + + try { + $spec = fetchJson($baseUrl, $tokenAuth, [ + 'method' => 'OpenApiDocs.getGeneratedOpenApiSpec', + 'plugin' => $plugin, + ]); + + $path = sprintf('%s/%s_openapi_spec_v%s.json', $targetDirectory, $plugin, $version); + $json = json_encode($spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($json === false) { + throw new RuntimeException("Failed to encode JSON for plugin $plugin"); + } + + $json .= "\n"; + + if (file_put_contents($path, $json) === false) { + throw new RuntimeException("Failed to write file: $path"); + } + + $written++; + } catch (Throwable $e) { + fwrite(STDERR, "Failed for $plugin: {$e->getMessage()}\n"); + } +} + +echo "Wrote $written spec file(s) to $targetDirectory\n"; From bc7020cf972fd2eedc7dbb25f646e16ab959e491 Mon Sep 17 00:00:00 2001 From: Lachlan Reynolds Date: Tue, 14 Apr 2026 13:30:52 +1200 Subject: [PATCH 2/3] Using curl, using tmp file, better error handling --- fetch-openapi-specs.php | 42 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/fetch-openapi-specs.php b/fetch-openapi-specs.php index 0700fc6b1..c1d0a0b87 100644 --- a/fetch-openapi-specs.php +++ b/fetch-openapi-specs.php @@ -12,6 +12,11 @@ exit(1); } +if (!function_exists('curl_init')) { + fwrite(STDERR, "The curl extension is required to fetch OpenAPI specs.\n"); + exit(1); +} + function fetchJson(string $baseUrl, string $tokenAuth, array $params): array { $params['module'] = 'API'; @@ -19,10 +24,30 @@ function fetchJson(string $baseUrl, string $tokenAuth, array $params): array $params['token_auth'] = $tokenAuth; $url = $baseUrl . '/index.php?' . http_build_query($params); - $response = @file_get_contents($url); + $ch = curl_init($url); + if ($ch === false) { + throw new RuntimeException("Failed to initialize curl for $url"); + } + + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 60, + ]); + $response = curl_exec($ch); if ($response === false) { - throw new RuntimeException("Request failed: $url"); + $error = curl_error($ch); + curl_close($ch); + throw new RuntimeException("Request failed for $url: $error"); + } + + $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + curl_close($ch); + + if ($statusCode < 200 || $statusCode >= 300) { + throw new RuntimeException("Request failed for $url with HTTP status $statusCode"); } $decoded = json_decode($response, true); @@ -67,14 +92,25 @@ function fetchJson(string $baseUrl, string $tokenAuth, array $params): array } $json .= "\n"; + $tempPath = tempnam($targetDirectory, $plugin . '_openapi_'); + if ($tempPath === false) { + throw new RuntimeException("Failed to create temporary file for plugin $plugin"); + } - if (file_put_contents($path, $json) === false) { + if (file_put_contents($tempPath, $json) === false) { + @unlink($tempPath); throw new RuntimeException("Failed to write file: $path"); } + if (!rename($tempPath, $path)) { + @unlink($tempPath); + throw new RuntimeException("Failed to move temporary file into place: $path"); + } + $written++; } catch (Throwable $e) { fwrite(STDERR, "Failed for $plugin: {$e->getMessage()}\n"); + exit(1); } } From 6ef6801aac06716a7bc0cf2025afac36391aadeb Mon Sep 17 00:00:00 2001 From: Lachlan Reynolds Date: Tue, 14 Apr 2026 13:52:52 +1200 Subject: [PATCH 3/3] hide token and removed stale spec files --- fetch-openapi-specs.php | 61 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/fetch-openapi-specs.php b/fetch-openapi-specs.php index c1d0a0b87..7e270883e 100644 --- a/fetch-openapi-specs.php +++ b/fetch-openapi-specs.php @@ -17,16 +17,58 @@ exit(1); } +function buildRequestUrl(string $baseUrl, array $params): string +{ + return $baseUrl . '/index.php?' . http_build_query($params); +} + +function describeRequest(string $baseUrl, array $params): string +{ + $descriptionParams = $params; + unset($descriptionParams['token_auth']); + + return buildRequestUrl($baseUrl, $descriptionParams); +} + +/** + * @return list + */ +function findExistingSpecFiles(string $targetDirectory): array +{ + $files = glob($targetDirectory . '/*_openapi_spec_v*.json'); + if ($files === false) { + throw new RuntimeException("Failed to list existing OpenAPI spec files in $targetDirectory"); + } + + return array_values($files); +} + +function removeStaleSpecFiles(string $targetDirectory, array $expectedPaths): void +{ + $expectedPaths = array_fill_keys($expectedPaths, true); + + foreach (findExistingSpecFiles($targetDirectory) as $existingPath) { + if (isset($expectedPaths[$existingPath])) { + continue; + } + + if (!unlink($existingPath)) { + throw new RuntimeException("Failed to remove stale file: $existingPath"); + } + } +} + function fetchJson(string $baseUrl, string $tokenAuth, array $params): array { $params['module'] = 'API'; $params['format'] = 'JSON'; $params['token_auth'] = $tokenAuth; - $url = $baseUrl . '/index.php?' . http_build_query($params); + $url = buildRequestUrl($baseUrl, $params); + $requestDescription = describeRequest($baseUrl, $params); $ch = curl_init($url); if ($ch === false) { - throw new RuntimeException("Failed to initialize curl for $url"); + throw new RuntimeException("Failed to initialize curl for $requestDescription"); } curl_setopt_array($ch, [ @@ -40,19 +82,19 @@ function fetchJson(string $baseUrl, string $tokenAuth, array $params): array if ($response === false) { $error = curl_error($ch); curl_close($ch); - throw new RuntimeException("Request failed for $url: $error"); + throw new RuntimeException("Request failed for $requestDescription: $error"); } $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($statusCode < 200 || $statusCode >= 300) { - throw new RuntimeException("Request failed for $url with HTTP status $statusCode"); + throw new RuntimeException("Request failed for $requestDescription with HTTP status $statusCode"); } $decoded = json_decode($response, true); if (!is_array($decoded)) { - throw new RuntimeException("Invalid JSON response: $url"); + throw new RuntimeException("Invalid JSON response for $requestDescription"); } if (isset($decoded['result']) && $decoded['result'] === 'error') { @@ -73,6 +115,7 @@ function fetchJson(string $baseUrl, string $tokenAuth, array $params): array } $written = 0; +$expectedPaths = []; foreach ($plugins as $plugin) { if (!is_string($plugin) || $plugin === '') { @@ -86,6 +129,7 @@ function fetchJson(string $baseUrl, string $tokenAuth, array $params): array ]); $path = sprintf('%s/%s_openapi_spec_v%s.json', $targetDirectory, $plugin, $version); + $expectedPaths[] = $path; $json = json_encode($spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if ($json === false) { throw new RuntimeException("Failed to encode JSON for plugin $plugin"); @@ -114,4 +158,11 @@ function fetchJson(string $baseUrl, string $tokenAuth, array $params): array } } +try { + removeStaleSpecFiles($targetDirectory, $expectedPaths); +} catch (Throwable $e) { + fwrite(STDERR, "Failed to clean up stale spec files: {$e->getMessage()}\n"); + exit(1); +} + echo "Wrote $written spec file(s) to $targetDirectory\n";