From e21d17f2783f81efa29557a6c1cdac680f8a7d9c Mon Sep 17 00:00:00 2001 From: rivanuff <16398970+rivanuff@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:04:27 +0100 Subject: [PATCH 1/3] feat: declare rest route args --- .../RestAPI/Controllers/BaseController.php | 43 ++++++++- .../RestAPI/Controllers/ItemController.php | 89 +++++++++++-------- .../RestAPI/Controllers/SearchController.php | 17 ++++ .../RestAPI/Controllers/ThemeController.php | 10 +++ src/Base/RestAPI/RestAPIServiceProvider.php | 26 ++++-- 5 files changed, 140 insertions(+), 45 deletions(-) diff --git a/src/Base/RestAPI/Controllers/BaseController.php b/src/Base/RestAPI/Controllers/BaseController.php index 2a88948..1a084a9 100644 --- a/src/Base/RestAPI/Controllers/BaseController.php +++ b/src/Base/RestAPI/Controllers/BaseController.php @@ -58,8 +58,47 @@ protected function getPaginatorParams(WP_REST_Request $request, int $limit = 10) */ protected function getTaxonomyParams(WP_REST_Request $request): array { - $queryVars = array_filter(wp_list_pluck(get_object_taxonomies('openpub-item', 'objects'), 'query_var')); + return array_intersect_key($request->get_params(), $this->taxonomyArguments()); + } + + public function paginatorArguments(): array + { + return [ + 'limit' => [ + 'description' => __('Number of items per page, -1 for all items.', 'openpub-base'), + 'type' => 'integer', + 'minimum' => -1, + 'default' => 10, + ], + 'page' => [ + 'description' => __('Page number.', 'openpub-base'), + 'type' => 'integer', + 'minimum' => 1, + 'default' => 1, + ], + ]; + } + + /** + * Route args for the taxonomy query vars getTaxonomyParams() passes to WP_Query. + */ + public function taxonomyArguments(): array + { + $args = []; + + foreach (get_object_taxonomies('openpub-item', 'objects') as $taxonomy) { + if (empty($taxonomy->query_var)) { + continue; + } + + // String first, so values reach WP_Query unsplit and keep its "a+b" (AND) syntax. + $args[$taxonomy->query_var] = [ + 'description' => sprintf(__('Filter on %s term slugs.', 'openpub-base'), $taxonomy->name), + 'type' => ['string', 'array'], + 'items' => ['type' => 'string'], + ]; + } - return array_intersect_key($request->get_params(), array_flip($queryVars)); + return $args; } } diff --git a/src/Base/RestAPI/Controllers/ItemController.php b/src/Base/RestAPI/Controllers/ItemController.php index 3529d1d..46ffe20 100644 --- a/src/Base/RestAPI/Controllers/ItemController.php +++ b/src/Base/RestAPI/Controllers/ItemController.php @@ -34,21 +34,67 @@ public function getActiveItems(WP_REST_Request $request): array return $this->response($items); } + /** + * Route args for the items and items/active endpoints. + */ + public function collectionArguments(): array + { + return array_merge($this->paginatorArguments(), $this->filterArguments(), [ + 'highlighted' => [ + 'description' => __('Only highlighted items (true) or only non-highlighted items (false).', 'openpub-base'), + 'type' => 'boolean', + ], + ], $this->taxonomyArguments()); + } + + /** + * Route args for the single item endpoints; the filters apply to the related items. + */ + public function singleArguments(): array + { + return array_merge($this->filterArguments(), [ + 'draft-preview' => [ + 'description' => __('Include draft and scheduled items, for logged-in users.', 'openpub-base'), + 'type' => 'boolean', + ], + ]); + } + + protected function filterArguments(): array + { + return [ + 'type' => [ + 'description' => __('Filter on openpub-type term slugs.', 'openpub-base'), + 'type' => 'array', + 'items' => ['type' => 'string'], + ], + 'audience' => [ + 'description' => __('Filter on openpub-audience term slugs.', 'openpub-base'), + 'type' => 'array', + 'items' => ['type' => 'string'], + ], + 'source' => [ + 'description' => __('Filter on the blog ID the items are shown on, when "show on" is enabled.', 'openpub-base'), + 'type' => 'integer', + ], + ]; + } + protected function itemQueryBuilder(WP_REST_Request $request = null): Item { $items = new Item(); $items = $items->query(apply_filters('owc/openpub/rest-api/items/query', $this->getPaginatorParams($request))); - if ($this->hasHighlightedParam($request)) { - $items->query(Item::addHighlightedParameters($this->getHighlightedParam($request))); + if (null !== $request->get_param('highlighted')) { + $items->query(Item::addHighlightedParameters(rest_sanitize_boolean($request->get_param('highlighted')))); } if ($this->getTypeParam($request)) { $items->query(Item::addTypeParameter($this->getTypeParam($request))); } - if ($this->showOnParamIsValid($request) && $this->plugin->settings->useShowOn()) { - $items->query(Item::addShowOnParameter($request->get_param('source'))); + if ($request->get_param('source') && $this->plugin->settings->useShowOn()) { + $items->query(Item::addShowOnParameter((string) $request->get_param('source'))); } if ($this->getAudienceParam($request)) { @@ -145,8 +191,8 @@ protected function addRelated(array $item, WP_REST_Request $request): array $items->query(Item::addTypeParameter($type)); } - if ($this->showOnParamIsValid($request) && $this->plugin->settings->useShowOn()) { - $items->query(Item::addShowOnParameter($request->get_param('source'))); + if ($request->get_param('source') && $this->plugin->settings->useShowOn()) { + $items->query(Item::addShowOnParameter((string) $request->get_param('source'))); } if ($this->getAudienceParam($request)) { @@ -183,24 +229,6 @@ public function getImageUrl(WP_Post $post): array return (new FeaturedImageField($this->plugin))->create($post); } - protected function getHighlightedParam(WP_REST_Request $request): bool - { - return filter_var($request->get_param('highlighted'), FILTER_VALIDATE_BOOLEAN); - } - - protected function hasHighlightedParam(WP_REST_Request $request): bool - { - if (empty($request->get_param('highlighted'))) { - return false; - } - - if (! $this->validateBoolean($request->get_param('highlighted'))) { - return false; - }; - - return true; - } - /** * Get the type slugs already attached to an item, comma-separated. * @@ -236,20 +264,11 @@ protected function getTypeParam(WP_REST_Request $request): string return is_string($typeParam) ? $typeParam : ''; } - protected function validateBoolean(string $value): bool - { - $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - - if (null === $value) { - return false; - } - - return true; - } - /** * Validate if show on param is valid. * Param should be a numeric value. + * + * @deprecated 3.10.0 The source route arg is validated as an integer; kept for child plugins built against older versions. */ protected function showOnParamIsValid(WP_REST_Request $request): bool { diff --git a/src/Base/RestAPI/Controllers/SearchController.php b/src/Base/RestAPI/Controllers/SearchController.php index 613e883..bf57bee 100644 --- a/src/Base/RestAPI/Controllers/SearchController.php +++ b/src/Base/RestAPI/Controllers/SearchController.php @@ -20,4 +20,21 @@ public function search(WP_REST_Request $request): array return $this->response($search); } + + public function arguments(): array + { + return array_merge([ + 's' => [ + 'description' => __('The search term.', 'openpub-base'), + 'type' => 'string', + ], + 'posts_per_page' => [ + 'description' => __('Number of results per page, at most 10.', 'openpub-base'), + 'type' => 'integer', + 'minimum' => 1, + 'default' => 10, + ], + 'page' => $this->paginatorArguments()['page'], + ], $this->taxonomyArguments()); + } } diff --git a/src/Base/RestAPI/Controllers/ThemeController.php b/src/Base/RestAPI/Controllers/ThemeController.php index 75b74e7..2cc93d1 100644 --- a/src/Base/RestAPI/Controllers/ThemeController.php +++ b/src/Base/RestAPI/Controllers/ThemeController.php @@ -28,6 +28,16 @@ public function getThemes(WP_REST_Request $request) return $this->response($themes); } + public function singleArguments(): array + { + return [ + 'draft-preview' => [ + 'description' => __('Include draft and scheduled themes.', 'openpub-base'), + 'type' => 'boolean', + ], + ]; + } + /** * Get an individual post theme by slug. * diff --git a/src/Base/RestAPI/RestAPIServiceProvider.php b/src/Base/RestAPI/RestAPIServiceProvider.php index 4e10d25..c190e9b 100644 --- a/src/Base/RestAPI/RestAPIServiceProvider.php +++ b/src/Base/RestAPI/RestAPIServiceProvider.php @@ -65,46 +65,56 @@ public function register(): void */ public function registerRoutes(): void { + $itemController = new ItemController($this->plugin); + $themeController = new ThemeController($this->plugin); + $searchController = new SearchController($this->plugin); + register_rest_route($this->namespace, 'items', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new ItemController($this->plugin), 'getItems'], + 'callback' => [$itemController, 'getItems'], + 'args' => $itemController->collectionArguments(), 'permission_callback' => '__return_true', ]); register_rest_route($this->namespace, 'items/active', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new ItemController($this->plugin), 'getActiveItems'], + 'callback' => [$itemController, 'getActiveItems'], + 'args' => $itemController->collectionArguments(), 'permission_callback' => '__return_true', ]); register_rest_route($this->namespace, 'items/(?P\d+)', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new ItemController($this->plugin), 'getItem'], + 'callback' => [$itemController, 'getItem'], + 'args' => $itemController->singleArguments(), 'permission_callback' => '__return_true', ]); register_rest_route($this->namespace, 'items/(?P[\w-]+)', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new ItemController($this->plugin), 'getItemBySlug'], + 'callback' => [$itemController, 'getItemBySlug'], + 'args' => $itemController->singleArguments(), 'permission_callback' => '__return_true', ]); register_rest_route($this->namespace, 'themes', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new ThemeController($this->plugin), 'getThemes'], + 'callback' => [$themeController, 'getThemes'], + 'args' => $themeController->paginatorArguments(), 'permission_callback' => '__return_true', ]); register_rest_route($this->namespace, 'themes/(?P[\w-]+)', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new ThemeController($this->plugin), 'getThemeBySlug'], + 'callback' => [$themeController, 'getThemeBySlug'], + 'args' => $themeController->singleArguments(), 'permission_callback' => '__return_true', ]); register_rest_route($this->namespace, 'search', [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [new SearchController($this->plugin), 'search'], - 'args' => [], + 'callback' => [$searchController, 'search'], + 'args' => $searchController->arguments(), 'permission_callback' => '__return_true', ]); From 9f751c464c1afc07a294a84e8dab779944a8adcb Mon Sep 17 00:00:00 2001 From: rivanuff <16398970+rivanuff@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:04:32 +0100 Subject: [PATCH 2/3] release: v3.10.0 --- CHANGELOG.md | 4 ++++ openpub-base.php | 2 +- src/Base/Foundation/Plugin.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab08cc0..80eeebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [v3.10.0] - 2026-11-12 + +- Feat: declare REST API route args, so params are documented and validated by WordPress (malformed values now return a 400) + ## [v3.9.6] - 2026-11-12 - Fix: allow OpenPub taxonomy filter params in REST API queries diff --git a/openpub-base.php b/openpub-base.php index 328d2bc..9069a96 100644 --- a/openpub-base.php +++ b/openpub-base.php @@ -4,7 +4,7 @@ * Plugin Name: Yard | OpenPub Base * Plugin URI: https://www.openwebconcept.nl/ * Description: Acts as foundation for other OpenPub related content plugins. This plugin implements actions to allow for other plugins to add and/or change Custom Posttypes, Metaboxes, Taxonomies, en Posts 2 posts relations. - * Version: 3.9.6 + * Version: 3.10.0 * Author: Yard | Digital Agency * Author URI: https://www.yard.nl/ * License: GPL-3.0 diff --git a/src/Base/Foundation/Plugin.php b/src/Base/Foundation/Plugin.php index cc8f059..c6752db 100644 --- a/src/Base/Foundation/Plugin.php +++ b/src/Base/Foundation/Plugin.php @@ -8,7 +8,7 @@ class Plugin { public const NAME = 'openpub-base'; - public const VERSION = '3.9.6'; + public const VERSION = '3.10.0'; protected string $rootPath; public Config $config; From fdcda2ca0250a4e421caba8896f47f814a133397 Mon Sep 17 00:00:00 2001 From: rivanuff <16398970+rivanuff@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:29:09 +0100 Subject: [PATCH 3/3] fix: require login for theme draft preview --- CHANGELOG.md | 1 + src/Base/RestAPI/Controllers/ThemeController.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80eeebf..02f0d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [v3.10.0] - 2026-11-12 - Feat: declare REST API route args, so params are documented and validated by WordPress (malformed values now return a 400) +- Fix: only show draft and scheduled themes through draft-preview to logged-in users ## [v3.9.6] - 2026-11-12 diff --git a/src/Base/RestAPI/Controllers/ThemeController.php b/src/Base/RestAPI/Controllers/ThemeController.php index 2cc93d1..3773dd4 100644 --- a/src/Base/RestAPI/Controllers/ThemeController.php +++ b/src/Base/RestAPI/Controllers/ThemeController.php @@ -32,7 +32,7 @@ public function singleArguments(): array { return [ 'draft-preview' => [ - 'description' => __('Include draft and scheduled themes.', 'openpub-base'), + 'description' => __('Include draft and scheduled themes, for logged-in users.', 'openpub-base'), 'type' => 'boolean', ], ]; @@ -68,7 +68,7 @@ public function singleThemeQueryBuilder(WP_REST_Request $request): Theme $preview = filter_var($request->get_param('draft-preview'), FILTER_VALIDATE_BOOLEAN); - if (true === $preview) { + if (true === $preview && is_user_logged_in()) { $theme->query(['post_status' => ['publish', 'draft', 'future']]); }