diff --git a/includes/Rest/Handler/LanguageLinksHandler.php b/includes/Rest/Handler/LanguageLinksHandler.php index c213a2bf8ba..28fbc4f4b65 100644 --- a/includes/Rest/Handler/LanguageLinksHandler.php +++ b/includes/Rest/Handler/LanguageLinksHandler.php @@ -190,4 +190,7 @@ class LanguageLinksHandler extends SimpleHandler { return (bool)$this->getPage(); } + public function getResponseBodySchemaFileName( string $method ): ?string { + return 'includes/Rest/Handler/Schema/PageLanguageLinks.json'; + } } diff --git a/includes/Rest/Handler/MediaFileHandler.php b/includes/Rest/Handler/MediaFileHandler.php index 3a6b161c1d2..ea55bc01142 100644 --- a/includes/Rest/Handler/MediaFileHandler.php +++ b/includes/Rest/Handler/MediaFileHandler.php @@ -171,4 +171,8 @@ class MediaFileHandler extends SimpleHandler { $file = $this->getFile(); return $file && $file->exists(); } + + public function getResponseBodySchemaFileName( string $method ): ?string { + return 'includes/Rest/Handler/Schema/MediaFile.json'; + } } diff --git a/includes/Rest/Handler/MediaLinksHandler.php b/includes/Rest/Handler/MediaLinksHandler.php index 6e5a97fd1cb..b8ef2546be8 100644 --- a/includes/Rest/Handler/MediaLinksHandler.php +++ b/includes/Rest/Handler/MediaLinksHandler.php @@ -192,4 +192,8 @@ class MediaLinksHandler extends SimpleHandler { protected function getMaxNumLinks(): int { return self::MAX_NUM_LINKS; } + + public function getResponseBodySchemaFileName( string $method ): ?string { + return 'includes/Rest/Handler/Schema/MediaLinks.json'; + } } diff --git a/includes/Rest/Handler/PageHTMLHandler.php b/includes/Rest/Handler/PageHTMLHandler.php index 1e3ad454163..5ced8d940e3 100644 --- a/includes/Rest/Handler/PageHTMLHandler.php +++ b/includes/Rest/Handler/PageHTMLHandler.php @@ -181,4 +181,23 @@ class PageHTMLHandler extends SimpleHandler { HtmlOutputRendererHelper::getParamSettings() ); } + + protected function generateResponseSpec( string $method ): array { + $spec = parent::generateResponseSpec( $method ); + + // TODO: Consider if we prefer something like: + // text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.8.0" + // That would be more specific, but fragile when the profile version changes. It could + // also be inaccurate if the page content was not in fact produced by Parsoid. + if ( $this->getOutputMode() == 'html' ) { + unset( $spec['200']['content']['application/json'] ); + $spec['200']['content']['text/html']['schema']['type'] = 'string'; + } + + return $spec; + } + + public function getResponseBodySchemaFileName( string $method ): ?string { + return 'includes/Rest/Handler/Schema/ExistingPageHtml.json'; + } } diff --git a/includes/Rest/Handler/PageHistoryHandler.php b/includes/Rest/Handler/PageHistoryHandler.php index cc311b61d47..62785c7a395 100644 --- a/includes/Rest/Handler/PageHistoryHandler.php +++ b/includes/Rest/Handler/PageHistoryHandler.php @@ -484,4 +484,8 @@ class PageHistoryHandler extends SimpleHandler { protected function hasRepresentation() { return (bool)$this->getPage(); } + + public function getResponseBodySchemaFileName( string $method ): ?string { + return 'includes/Rest/Handler/Schema/PageHistory.json'; + } } diff --git a/includes/Rest/Handler/PageSourceHandler.php b/includes/Rest/Handler/PageSourceHandler.php index c47d095883f..39d28357e35 100644 --- a/includes/Rest/Handler/PageSourceHandler.php +++ b/includes/Rest/Handler/PageSourceHandler.php @@ -167,12 +167,21 @@ class PageSourceHandler extends SimpleHandler { return $this->contentHelper->hasContent(); } - /** - * This method specifies the JSON schema file for the response body - * - * @return ?string The file path to the ExistingPage JSON schema. - */ public function getResponseBodySchemaFileName( string $method ): ?string { - return 'includes/Rest/Handler/Schema/ExistingPage.json'; + // This does not include restbase compatibility mode, which is triggered by request + // headers. Presumably, such callers will look at the RESTBase spec instead. + switch ( $this->getConfig()['format'] ) { + case 'bare': + $schema = 'includes/Rest/Handler/Schema/ExistingPageBare.json'; + break; + case 'source': + $schema = 'includes/Rest/Handler/Schema/ExistingPageSource.json'; + break; + default: + $schema = null; + break; + } + + return $schema; } } diff --git a/includes/Rest/Handler/Schema/ExistingPageBare.json b/includes/Rest/Handler/Schema/ExistingPageBare.json new file mode 100644 index 00000000000..9847fb9d637 --- /dev/null +++ b/includes/Rest/Handler/Schema/ExistingPageBare.json @@ -0,0 +1,62 @@ +{ + "description": "Page without content", + "required": [ + "id", + "key", + "title", + "latest", + "content_model", + "license", + "html_url" + ], + "properties": { + "id": { + "type": "integer", + "description": "Page identifier" + }, + "key": { + "type": "string", + "description": "Page title in URL-friendly format" + }, + "title": { + "type": "string", + "description": "Page title" + }, + "latest": { + "type": "object", + "description": "Information about the latest revision", + "properties": { + "id": { + "type": "integer", + "description": "Revision identifier for the latest revision" + }, + "timestamp": { + "type": "string", + "description": " Timestamp of the latest revision" + } + } + }, + "content_model": { + "type": "string", + "description": "Page content type" + }, + "license": { + "type": "object", + "description": "Information about the wiki's license", + "properties": { + "url": { + "type": "string", + "description": "URL of the applicable license" + }, + "title": { + "type": "string", + "description": "Name of the applicable license" + } + } + }, + "html_url": { + "type": "string", + "description": "API route to fetch the content of the page in HTML" + } + } +} diff --git a/includes/Rest/Handler/Schema/ExistingPageHtml.json b/includes/Rest/Handler/Schema/ExistingPageHtml.json new file mode 100644 index 00000000000..c60996ee5e0 --- /dev/null +++ b/includes/Rest/Handler/Schema/ExistingPageHtml.json @@ -0,0 +1,62 @@ +{ + "description": "Page with HTML content", + "required": [ + "id", + "key", + "title", + "latest", + "content_model", + "license", + "html" + ], + "properties": { + "id": { + "type": "integer", + "description": "Page identifier" + }, + "key": { + "type": "string", + "description": "Page title in URL-friendly format" + }, + "title": { + "type": "string", + "description": "Page title in reading-friendly format" + }, + "latest": { + "type": "object", + "description": "Information about the latest revision", + "properties": { + "id": { + "type": "integer", + "description": "Revision identifier for the latest revision" + }, + "timestamp": { + "type": "string", + "description": " Timestamp of the latest revision in ISO 8601 format" + } + } + }, + "content_model": { + "type": "string", + "description": "Type of content on the page" + }, + "license": { + "type": "object", + "description": "Information about the wiki's license", + "properties": { + "url": { + "type": "string", + "description": "URL of the applicable license based on the $wgRightsUrl setting" + }, + "title": { + "type": "string", + "description": "Name of the applicable license based on the $wgRightsText setting" + } + } + }, + "html": { + "type": "string", + "description": "Latest page content in HTML, following the HTML specification" + } + } +} diff --git a/includes/Rest/Handler/Schema/ExistingPage.json b/includes/Rest/Handler/Schema/ExistingPageSource.json similarity index 89% rename from includes/Rest/Handler/Schema/ExistingPage.json rename to includes/Rest/Handler/Schema/ExistingPageSource.json index 5790ffd242b..362088f0a6f 100644 --- a/includes/Rest/Handler/Schema/ExistingPage.json +++ b/includes/Rest/Handler/Schema/ExistingPageSource.json @@ -1,17 +1,18 @@ { - "description": "Schema for wiki pages.", + "description": "Page with source (usually wikitext)", "required": [ "id", "key", "title", "latest", "content_model", - "license" + "license", + "source" ], "properties": { "id": { "type": "integer", - "description": "Page identifier." + "description": "Page identifier" }, "key": { "type": "string", @@ -40,7 +41,7 @@ "description": "Page content type" }, "license": { - "type": "string", + "type": "object", "description": "Information about the wiki's license", "properties": { "url": { diff --git a/includes/Rest/Handler/Schema/MediaFile.json b/includes/Rest/Handler/Schema/MediaFile.json new file mode 100644 index 00000000000..3c20ac19a40 --- /dev/null +++ b/includes/Rest/Handler/Schema/MediaFile.json @@ -0,0 +1,165 @@ +{ + "description": "Information about the file", + "required": [ + "title", + "file_description_url", + "latest", + "preferred", + "original", + "thumbnail" + ], + "properties": { + "title": { + "type": "string", + "description": "File title" + }, + "file_description_url": { + "type": "string", + "description": "URL for the page describing the file, including license information and other metadata" + }, + "latest": { + "type": "object", + "nullable": true, + "description": "Information about the latest revision to the file", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Last modified timestamp in ISO 8601 format" + }, + "user": { + "type": "object", + "description": "Information about the user who uploaded the file", + "properties": { + "id": { + "type": "integer", + "nullable": true, + "description": "User identifier" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Username" + } + }, + "required": [ "id", "name" ] + } + }, + "required": [ "timestamp", "user" ] + }, + "preferred": { + "type": "object", + "nullable": true, + "description": "Information about the file's preferred preview format, original format, and thumbnail format", + "properties": { + "mediatype": { + "type": "string", + "enum": [ "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "UNKNOWN", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D" ], + "description": "The file type" + }, + "size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes or null if not available" + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image width in pixels or null if not available" + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image height in pixels or null if not available" + }, + "duration": { + "type": "number", + "nullable": true, + "description": "The length of the video, audio, or multimedia file or null for other media types" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to download the file" + } + }, + "required": [ "mediatype", "size", "width", "height", "duration", "url" ] + }, + "original": { + "type": "object", + "nullable": true, + "description": "Original file details", + "properties": { + "mediatype": { + "type": "string", + "enum": [ "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "UNKNOWN", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D" ], + "description": "The file type" + }, + "size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes or null if not available" + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image width in pixels or null if not available" + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image height in pixels or null if not available" + }, + "duration": { + "type": "number", + "nullable": true, + "description": "The length of the video, audio, or multimedia file or null for other media types" + }, + "url": { + "type": "string", + "format": "url", + "description": "URL to download the file" + } + }, + "required": [ "mediatype", "size", "width", "height", "duration", "url" ] + }, + "thumbnail": { + "type": "object", + "nullable": true, + "description": "Thumbnail information", + "properties": { + "mediatype": { + "type": "string", + "enum": [ "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "UNKNOWN", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D" ], + "description": "The file type" + }, + "size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes or null if not available" + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image width in pixels or null if not available" + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image height in pixels or null if not available" + }, + "duration": { + "type": "number", + "nullable": true, + "description": "The length of the video, audio, or multimedia file or null for other media types" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to download the file" + } + }, + "required": [ "mediatype", "size", "width", "height", "duration", "url" ] + } + } +} diff --git a/includes/Rest/Handler/Schema/MediaLinks.json b/includes/Rest/Handler/Schema/MediaLinks.json new file mode 100644 index 00000000000..fe1dd0fe1ee --- /dev/null +++ b/includes/Rest/Handler/Schema/MediaLinks.json @@ -0,0 +1,182 @@ +{ + "description": "Media links for the page", + "required": [ + "files" + ], + "properties": { + "files": { + "type": "array", + "description": "Array of media used on the page", + "items": { + "type": "object", + "required": [ + "title", + "file_description_url", + "latest", + "preferred", + "original" + ], + "properties": { + "title": { + "type": "string", + "description": "File title" + }, + "file_description_url": { + "type": "string", + "description": "URL for the page describing the file, including license information and other metadata" + }, + "latest": { + "type": "object", + "nullable": true, + "description": "Information about the latest revision to the file", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Last modified timestamp in ISO 8601 format" + }, + "user": { + "type": "object", + "description": "Information about the user who uploaded the file", + "properties": { + "id": { + "type": "integer", + "nullable": true, + "description": "User identifier" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Username" + } + }, + "required": [ + "id", + "name" + ] + } + }, + "required": [ + "timestamp", + "user" + ] + }, + "preferred": { + "type": "object", + "nullable": true, + "description": "Information about the file's preferred preview format, original format, and thumbnail format", + "properties": { + "mediatype": { + "type": "string", + "enum": [ + "BITMAP", + "DRAWING", + "AUDIO", + "VIDEO", + "MULTIMEDIA", + "UNKNOWN", + "OFFICE", + "TEXT", + "EXECUTABLE", + "ARCHIVE", + "3D" + ], + "description": "The file type" + }, + "size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes or null if not available" + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image width in pixels or null if not available" + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image height in pixels or null if not available" + }, + "duration": { + "type": "number", + "nullable": true, + "description": "The length of the video, audio, or multimedia file or null for other media types" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to download the file" + } + }, + "required": [ + "mediatype", + "size", + "width", + "height", + "duration", + "url" + ] + }, + "original": { + "type": "object", + "nullable": true, + "description": "Original file details", + "properties": { + "mediatype": { + "type": "string", + "enum": [ + "BITMAP", + "DRAWING", + "AUDIO", + "VIDEO", + "MULTIMEDIA", + "UNKNOWN", + "OFFICE", + "TEXT", + "EXECUTABLE", + "ARCHIVE", + "3D" + ], + "description": "The file type" + }, + "size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes or null if not available" + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image width in pixels or null if not available" + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image height in pixels or null if not available" + }, + "duration": { + "type": "number", + "nullable": true, + "description": "The length of the video, audio, or multimedia file or null for other media types" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to download the file" + } + }, + "required": [ + "mediatype", + "size", + "width", + "height", + "duration", + "url" + ] + } + } + } + } + } +} diff --git a/includes/Rest/Handler/Schema/NewPage.json b/includes/Rest/Handler/Schema/NewPage.json index ad0e4200766..0b6b56e8347 100644 --- a/includes/Rest/Handler/Schema/NewPage.json +++ b/includes/Rest/Handler/Schema/NewPage.json @@ -1,34 +1,62 @@ { - "description": "Schema for wiki pages.", + "description": "The new page, including source (usually wikitext)", "required": [ - "source", + "id", "title", - "comment", + "key", + "latest", + "license", "content_model", - "token" + "source" ], "properties": { - "source": { - "type": "string", - "description": "Page content in the format specified by the content_model property" + "id": { + "type": "integer", + "description": "Page identifier" }, "title": { "type": "string", "description": "Page title" }, - "comment": { + "key": { "type": "string", - "description": "Reason for creating the page." + "description": "Page title in URL-friendly format" + }, + "latest": { + "type": "object", + "description": "Information about the latest revision", + "properties": { + "id": { + "type": "integer", + "description": "Revision identifier for the latest revision" + }, + "timestamp": { + "type": "string", + "description": " Timestamp of the latest revision" + } + } + }, + "license": { + "type": "object", + "description": "Information about the wiki's license", + "properties": { + "url": { + "type": "string", + "description": "URL of the applicable license" + }, + "title": { + "type": "string", + "description": "Name of the applicable license" + } + } }, "content_model": { "type": "string", - "nullable": true, "description": "Page content type" }, - "token": { + "source": { "type": "string", - "nullable": true, - "description": "CSRF token required when using cookie-based authentication." + "description": "Page content in the format specified by the content_model property" } } } diff --git a/includes/Rest/Handler/Schema/PageHistory.json b/includes/Rest/Handler/Schema/PageHistory.json new file mode 100644 index 00000000000..8d61596b8b5 --- /dev/null +++ b/includes/Rest/Handler/Schema/PageHistory.json @@ -0,0 +1,78 @@ +{ + "description": "Page revision history", + "required": [ + "revisions", + "latest" + ], + "properties": { + "revisions": { + "type": "array", + "description": "List of revisions of the page", + "items": { + "type": "object", + "required": [ + "id", + "timestamp", + "minor", + "size", + "comment", + "user", + "delta" + ], + "properties": { + "id": { + "type": "integer", + "description": "Unique revision identifier" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "minor": { + "type": "boolean", + "description": "True if the edit is marked as minor" + }, + "size": { + "type": "integer", + "description": "Size of the revision in bytes" + }, + "comment": { + "type": "string", + "nullable": true, + "description": "The comment the author associated with the revision" + }, + "user": { + "type": "object", + "nullable": true, + "description": "Information about the user who made the revision", + "properties": { + "id": { + "type": "integer", + "nullable": true, + "description": "Unique identifier for the user; null for anonymous users" + }, + "name": { + "type": "string", + "description": "Username of the editor, or IP address if the user is anonymous" + } + }, + "required": [ + "id", + "name" + ] + }, + "delta": { + "type": "integer", + "nullable": true, + "description": "Change in size between this revision and the preceding one; null if not available" + } + } + } + }, + "latest": { + "type": "string", + "format": "uri", + "description": "URL to the latest revision of the page" + } + } +} diff --git a/includes/Rest/Handler/Schema/PageLanguageLinks.json b/includes/Rest/Handler/Schema/PageLanguageLinks.json new file mode 100644 index 00000000000..c38bee2e529 --- /dev/null +++ b/includes/Rest/Handler/Schema/PageLanguageLinks.json @@ -0,0 +1,30 @@ +{ + "description": "Interlanguage links for the page", + "type": "array", + "items": { + "required": [ + "code", + "name", + "key", + "title" + ], + "properties": { + "code": { + "type": "string", + "description": "Language code" + }, + "name": { + "type": "string", + "description": "Translated language name" + }, + "key": { + "type": "string", + "description": "Translated page title in URL-friendly format" + }, + "title": { + "type": "string", + "description": "Translated page title in reading-friendly format" + } + } + } +} diff --git a/includes/Rest/Handler/Schema/SearchResults.json b/includes/Rest/Handler/Schema/SearchResults.json new file mode 100644 index 00000000000..b7c414a7636 --- /dev/null +++ b/includes/Rest/Handler/Schema/SearchResults.json @@ -0,0 +1,84 @@ +{ + "description": "Search results", + "required": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "description": "List of search result pages", + "items": { + "type": "object", + "required": [ + "id", + "key", + "title", + "excerpt", + "matched_title", + "description", + "thumbnail" + ], + "properties": { + "id": { + "type": "integer", + "description": "Page identifier" + }, + "key": { + "type": "string", + "description": "Page title in URL-friendly format" + }, + "title": { + "type": "string", + "description": "Page title in reading-friendly format" + }, + "excerpt": { + "type": "string", + "description": "Excerpt of the page content matching the search query" + }, + "matched_title": { + "type": "string", + "nullable": true, + "description": "The title of a page redirection from, if applicable, or null" + }, + "description": { + "type": "string", + "nullable": true, + "description": "Short summary of the page topic or null if no summary exists." + }, + "thumbnail": { + "type": "object", + "nullable": true, + "description": "Information about the thumbnail image for the page, or null if no thumbnail exists.", + "properties": { + "mimetype": { + "type": "string", + "description": "The file type" + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image width in pixels or null if not available" + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Maximum recommended image height in pixels or null if not available" + }, + "duration": { + "type": "number", + "nullable": true, + "description": "The length of the video, audio, or multimedia file or null for other media types" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to download the file" + } + }, + "required": [ "mimetype", "width", "height", "duration", "url" ] + } + } + } + } + } +} diff --git a/includes/Rest/Handler/SearchHandler.php b/includes/Rest/Handler/SearchHandler.php index 4c427acf068..3e3c76e8740 100644 --- a/includes/Rest/Handler/SearchHandler.php +++ b/includes/Rest/Handler/SearchHandler.php @@ -414,4 +414,8 @@ class SearchHandler extends Handler { ], ]; } + + public function getResponseBodySchemaFileName( string $method ): ?string { + return 'includes/Rest/Handler/Schema/SearchResults.json'; + } } diff --git a/includes/Rest/Handler/UpdateHandler.php b/includes/Rest/Handler/UpdateHandler.php index 66bdce64de1..b389373e4c7 100644 --- a/includes/Rest/Handler/UpdateHandler.php +++ b/includes/Rest/Handler/UpdateHandler.php @@ -254,13 +254,7 @@ class UpdateHandler extends EditHandler { return FormatJson::decode( $json, true ); } - /** - * This method specifies the JSON schema file for the response - * body when updating an existing page. - * - * @return ?string The file path to the ExistingPage JSON schema. - */ public function getResponseBodySchemaFileName( string $method ): ?string { - return 'includes/Rest/Handler/Schema/ExistingPage.json'; + return 'includes/Rest/Handler/Schema/ExistingPageSource.json'; } }