├── composer.json ├── index.php ├── readme.md ├── readme.txt └── wp_query-route-to-rest-api.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aucor/wp_query-route-to-rest-api", 3 | "description": "Adds new route /wp-json/wp_query/args/ to REST API", 4 | "license": "GPL-2.0-or-later", 5 | "type": "wordpress-plugin", 6 | "homepage": "https://github.com/aucor/wp_query-route-to-rest-api", 7 | "authors": [ 8 | { 9 | "name": "Teemu Suoranta", 10 | "email": "teemu@aucor.fi" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | - [Description](#description) 12 | - [How to use](#how-to-use) 13 | - [Basic usage](#basic-usage) 14 | - [Use with PHP](#use-with-php) 15 | - [Use with JS](#use-with-js) 16 | - [Advanced examples](#advanced-examples) 17 | - [Advanced example: tax_query](#advanced-example-taxquery) 18 | - [Advanced example: tax_query with relation](#advanced-example-taxquery-with-relation) 19 | - [Advanced example: modifying existing WP_Query \(post archive, term archive, search etc\)](#advanced-example-modifying-existing-wpquery-post-archive-term-archive-search-etc) 20 | - [Restrictions](#restrictions) 21 | - [Allowed args](#allowed-args) 22 | - [Post types](#post-types) 23 | - [Post status](#post-status) 24 | - [Restriction fail-safe](#restriction-fail-safe) 25 | - [Default WP_Query](#default-wpquery) 26 | - [Extra plugin compatibility features](#extra-plugin-compatibility-features) 27 | - [Filters](#filters) 28 | - [Hooks](#hooks) 29 | - [Install](#install) 30 | - [Issues and feature whishlist](#issues-and-feature-whishlist) 31 | - [Changelog](#changelog) 32 | - [1.3.2](#132) 33 | - [1.3.1](#131) 34 | - [1.3.0](#130) 35 | - [1.2.0](#120) 36 | - [1.1.1](#111) 37 | - [1.1](#11) 38 | 39 | 40 | 41 | 42 | 43 | ## Description 44 | 45 | Adds new route `/wp-json/wp_query/args/` to REST API. You can query content with WP_Query args. There's extensive filters and actions to limit or extend functionality. 46 | 47 | 48 | ## How to use 49 | 50 | 51 | ### Basic usage 52 | 53 | **Route**: `/wp-json/wp_query/args/` 54 | 55 | **Get three projects**: `/wp-json/wp_query/args/?post_type=project&posts_per_page=3` 56 | 57 | **You shoudn't write query args by hand!** It gets very complicated when you want to pass arrays for example with meta_query. 58 | 59 | 60 | ### Use with PHP 61 | **1. Create $args** 62 | ```php 63 | $args = array( 64 | 'post_type' => 'post', 65 | 'orderby' => 'title', 66 | 'order' => 'ASC' 67 | ); 68 | ``` 69 | **2. Turn $args into query string** [(Reference)](https://codex.wordpress.org/Function_Reference/build_query) 70 | ```php 71 | $query_str = build_query( $args ); 72 | ``` 73 | 74 | **3. Make the call** 75 | ```php 76 | $response = wp_remote_get( 'https://your-site.local/wp-json/wp_query/args/?' . $query_str ); 77 | 78 | // Get array of "post objects" 79 | $posts = json_decode( wp_remote_retrieve_body( $response ) ); 80 | ``` 81 | 82 | 83 | ### Use with JS 84 | **1. Create args** 85 | ```js 86 | var args = { 87 | 'post_type': 'post', 88 | 'orderby': 'title', 89 | 'order': 'ASC' 90 | }; 91 | ``` 92 | 93 | **2 a) Create params with for example using `@wordpress/url` package** 94 | ```js 95 | import { addQueryArgs } from '@wordpress/url'; 96 | 97 | const endpointURL = addQueryArgs( '/wp-json/wp_query/args/', args ); 98 | ``` 99 | 100 | **2 b) Some other JS solution** 101 | [query-string](https://www.npmjs.com/package/query-string) handles most use cases, but as query strings aren't really standardized, YMMV. 102 | 103 | One example of where it falls short: 104 | ```javascript 105 | const params = { 106 | "paged": 1, 107 | "order": "desc", 108 | "posts_per_page": 1, 109 | "tax_query": [ 110 | { 111 | "taxonomy": "category", 112 | "field": "term_id", 113 | "terms": [ 114 | 1 115 | ] 116 | }, 117 | { 118 | "taxonomy": "category", 119 | "field": "term_id", 120 | "terms": [ 121 | 2 122 | ] 123 | } 124 | ] 125 | } 126 | ``` 127 | 128 | 129 | One possible solution, ES2015: 130 | ```javascript 131 | let qsAdditions = '' 132 | 133 | if (params.tax_query) { 134 | // Define a helper method for getting a querystring part 135 | const part = (i, key, value) => Array.isArray(value) 136 | ? value.reduce((acc, v, i2) => ( 137 | acc += `&tax_query[${i}][${key}][${i2}]=${v}` 138 | ), '') 139 | : `&tax_query[${i}][${key}]=${value}` 140 | 141 | // Loop the params and glue pieces of querystrings together 142 | qsAdditions += params_tax_query.reduce((acc, cond, i) => ( 143 | acc += part(i, 'taxonomy', cond.taxonomy || 'category') + 144 | part(i, 'field', cond.field || 'term_id') + 145 | part(i, 'terms', cond.terms) 146 | ), '') 147 | 148 | // Delete value from object so query-string won't parse it 149 | delete params.tax_query 150 | } 151 | 152 | const query_str = querystring.stringify(params) + qsAdditions 153 | ``` 154 | 155 | **2 c) Create params with jQuery** 156 | ```js 157 | var query_str = jQuery.param( args ); 158 | ``` 159 | 160 | **3. Make the call** 161 | ```js 162 | fetch( addQueryArgs( '/wp-json/wp_query/args/', args ) ) 163 | .then( function ( response ) { 164 | // The API call was succesful. 165 | if ( response.ok ) { 166 | return response.json(); 167 | } else { 168 | return Promise.reject( response ); 169 | } 170 | } ).then( function ( data ) { 171 | // Do something with data. 172 | console.log( data ); 173 | } ).catch( function ( err ) { 174 | // There was an error. 175 | console.warn( 'Something went wrong.', err ); 176 | } ); 177 | ``` 178 | 179 | Or with jQuery. 180 | ```js 181 | $.ajax({ 182 | url: 'https://your.site.local/wp-json/wp_query/args/?' + query_str, 183 | }).done(function( data ) { 184 | console.log( data ); 185 | }); 186 | 187 | ``` 188 | 189 | 190 | ## Advanced examples 191 | 192 | 193 | ### Advanced example: tax_query 194 | 195 | Get posts that have **both** tags "wordpress" and "woocommerce" 196 | 197 | **PHP:** 198 | ```php 199 | $args = array( 200 | 'post_type' => 'post', 201 | 'tax_query' => array( 202 | array( 203 | 'taxonomy' => 'post_tag', 204 | 'field' => 'slug', 205 | 'terms' => array( 'wordpress' ), 206 | ), 207 | array( 208 | 'taxonomy' => 'post_tag', 209 | 'field' => 'slug', 210 | 'terms' => array( 'woocommerce' ), 211 | ), 212 | ), 213 | ); 214 | ``` 215 | 216 | **JS:** 217 | 218 | ```js 219 | var args = { 220 | 'post_type': 'post', 221 | 'tax_query': [ 222 | { 223 | 'taxonomy': 'post_tag', 224 | 'field': 'slug', 225 | 'terms': [ 'wordpress' ] 226 | }, 227 | { 228 | 'taxonomy': 'post_tag', 229 | 'field': 'slug', 230 | 'terms': [ 'woocommerce' ] 231 | } 232 | ] 233 | }; 234 | ``` 235 | 236 | 237 | ### Advanced example: tax_query with relation 238 | 239 | Get posts that have **either** "wordpress" **or** "woocommerce" tag. This gets tricky because JS doesn't support completely the same array structure as PHP. If you only need PHP, this is a piece of cake. 240 | 241 | **PHP:** 242 | ```php 243 | $args = array( 244 | 'post_type' => 'post', 245 | 'tax_query' => array( 246 | 'relation' => 'OR', 247 | array( 248 | 'taxonomy' => 'post_tag', 249 | 'field' => 'slug', 250 | 'terms' => array( 'wordpress' ), 251 | ), 252 | array( 253 | 'taxonomy' => 'post_tag', 254 | 'field' => 'slug', 255 | 'terms' => array( 'woocommerce' ), 256 | ), 257 | ), 258 | ); 259 | ``` 260 | 261 | **JS:** 262 | ```js 263 | var args = { 264 | 'post_type': 'post', 265 | 'tax_query': { 266 | 'relation': 'OR', 267 | 0: { 268 | 'taxonomy': 'post_tag', 269 | 'field': 'slug', 270 | 'terms': [ 'wordpress' ] 271 | }, 272 | 1: { 273 | 'taxonomy': 'post_tag', 274 | 'field': 'slug', 275 | 'terms': [ 'woocommerce' ] 276 | } 277 | } 278 | }; 279 | ``` 280 | 281 | For other uses, keep in mind JS object/array syntax. If there's key + value, use object `{}`. If theres only value, use array `[]`. 282 | 283 | 284 | ### Advanced example: modifying existing WP_Query (post archive, term archive, search etc) 285 | 286 | Sometimes you need to create features that add small tweaks to current query that WordPress, theme or plugins has already defined. These include "load more" buttons, filters etc. You can create that query from scratch if you want, but there is a neat way to get the current query for JS. 287 | 288 | You can add this to your `archive.php` or whatever PHP template you need: 289 | 290 | ```php 291 | 295 | 296 | ``` 297 | 298 | 299 | Now you can access the query in JS from this var `wp_query`. Props @timiwahalahti for this idea. 300 | 301 | 302 | ## Restrictions 303 | 304 | The route `/wp-json/wp_query/args/` sets some restrictions by default for queries. These restrictions can be lifted or hardened with filters and actions. 305 | 306 | 307 | ### Allowed args 308 | ``` 309 | 'p', 310 | 'name', 311 | 'title', 312 | 'page_id', 313 | 'pagename', 314 | 'post_parent', 315 | 'post_parent__in', 316 | 'post_parent__not_in', 317 | 'post__in', 318 | 'post__not_in', 319 | 'post_name__in', 320 | 'post_type', // With restrictions 321 | 'posts_per_page', // With restrictions 322 | 'offset', 323 | 'paged', 324 | 'page', 325 | 'ignore_sticky_posts', 326 | 'order', 327 | 'orderby', 328 | 'year', 329 | 'monthnum', 330 | 'w', 331 | 'day', 332 | 'hour', 333 | 'minute', 334 | 'second', 335 | 'm', 336 | 'date_query', 337 | 'inclusive', 338 | 'compare', 339 | 'column', 340 | 'relation', 341 | 'post_mime_type', 342 | 'author', 343 | 'author_name', 344 | 'author__in', 345 | 'author__not_in', 346 | 'meta_key', 347 | 'meta_value', 348 | 'meta_value_num', 349 | 'meta_compare', 350 | 'meta_query', 351 | 's', 352 | 'cat', 353 | 'category_name', 354 | 'category__and', 355 | 'category__in', 356 | 'category__not_in', 357 | 'tag', 358 | 'tag_id', 359 | 'tag__and', 360 | 'tag__in', 361 | 'tag__not_in', 362 | 'tag_slug__and', 363 | 'tag_slug__in', 364 | 'tax_query', 365 | 'lang', // Polylang 366 | ``` 367 | So biggest ones missing have something to do with getting content that you might not want to get like `post_status` drafts (add this argument to the list with filter if you need it). By default, no querying `post_passwords` or having your way with cache settings. 368 | 369 | 370 | ### Post types 371 | 372 | By default all the post types marked `'show_in_rest' => true` are available. `'post_type' => 'any'` falls back to these post types. You can change post types with filter to what you want. 373 | 374 | 375 | ### Post status 376 | 377 | By default, only "publish" is allowed. Add other post_status as needed with filter. 378 | 379 | 380 | ### Restriction fail-safe 381 | 382 | Addition to restriction of WP_Query args, there is check after the query that queried posts will not be forbidden post types or post_status. 383 | 384 | 385 | ### Default WP_Query 386 | 387 | ```php 388 | $default_args = array( 389 | 'post_status' => 'publish', 390 | 'posts_per_page' => 10, 391 | 'has_password' => false 392 | ); 393 | ``` 394 | In addition to the normal defaults from WP_Query. 395 | 396 | 397 | ## Extra plugin compatibility features 398 | 399 | This plugin has built-in compatibility for [Relevanssi ('s' argument)](https://wordpress.org/plugins/relevanssi/) and [Polylang ('lang' argument)](https://wordpress.org/plugins/polylang/) 400 | 401 | 402 | ## Filters 403 | 404 | **Add more allowed args:** 405 | ```php 406 | function my_allowed_args($args) { 407 | $args[] = 'post_status'; 408 | return $args; 409 | } 410 | add_filter( 'wp_query_route_to_rest_api_allowed_args', 'my_allowed_args' ); 411 | 412 | ``` 413 | 414 | **Add more default args:** 415 | ```php 416 | function my_default_args($args) { 417 | $args['posts_per_page'] = 5; 418 | return $args; 419 | } 420 | add_filter( 'wp_query_route_to_rest_api_default_args', 'my_default_args' ); 421 | 422 | ``` 423 | 424 | **Add allowed post types:** 425 | 426 | You can also add post types by setting `'show_in_rest' => true` when registering post type. 427 | ```php 428 | function my_allowed_post_types($post_types) { 429 | $post_types[] = 'projects'; 430 | return $post_types; 431 | } 432 | add_filter( 'wp_query_route_to_rest_api_allowed_post_types', 'my_allowed_post_types' ); 433 | 434 | ``` 435 | 436 | **Add allowed post status:** 437 | 438 | ```php 439 | function my_allowed_post_status($post_status) { 440 | $post_status[] = 'draft'; 441 | return $post_status; 442 | } 443 | add_filter( 'wp_query_route_to_rest_api_allowed_post_status', 'my_allowed_post_status' ); 444 | 445 | ``` 446 | 447 | **Is current post allowed:** 448 | ```php 449 | function my_post_is_allowed($is_allowed, $post) { 450 | if($post->ID == 123) { 451 | $is_allowed = false; 452 | } 453 | return $is_allowed; 454 | } 455 | add_filter( 'wp_query_route_to_rest_api_post_is_allowed', 'my_post_is_allowed', 10, 2 ); 456 | ``` 457 | **Alter any argument value:** 458 | ```php 459 | function my_arg_value($value, $key, $args) { 460 | if($key == 'posts_per_page' && $value > 10) { 461 | $value = 10; 462 | } 463 | return $value; 464 | } 465 | add_filter( 'wp_query_route_to_rest_api_arg_value', 'my_arg_value', 10, 3 ); 466 | ``` 467 | 468 | **Check permissions:** 469 | ```php 470 | function my_permission_check($is_allowed, $request) { 471 | return true; 472 | } 473 | add_filter( 'wp_query_route_to_rest_api_permissions_check', 'my_permission_check', 10, 2 ); 474 | 475 | ``` 476 | 477 | **Limit max posts per page:** 478 | ```php 479 | function my_max_posts_per_page($max) { 480 | return 100; // Default 50 481 | } 482 | add_filter( 'wp_query_route_to_rest_api_max_posts_per_page', 'my_max_posts_per_page' ); 483 | 484 | ``` 485 | 486 | **Modify default $data:** 487 | ```php 488 | function my_default_data($data) { 489 | $data = array( 490 | 'html' => false, 491 | 'messages' => array( 492 | 'empty' => esc_html__( 'No results found.', 'text-domain' ), 493 | ), 494 | ); 495 | 496 | return $data; 497 | } 498 | add_filter( 'wp_query_route_to_rest_api_default_data', 'my_default_data' ); 499 | ``` 500 | 501 | **Modify $data after loop:** 502 | ```php 503 | function my_default_data($data, $wp_query, $args) { 504 | // Do something with the data. 505 | 506 | return $data; 507 | } 508 | add_filter( 'wp_query_route_to_rest_api_after_loop_data', 'my_default_data', 10, 3 ); 509 | ``` 510 | 511 | **Remove post type meta:** 512 | ```php 513 | add_filter( 'wp_query_route_to_rest_api_update_post_type_meta', '__return_false' ); 514 | ``` 515 | 516 | **Remove parent class:** 517 | ```php 518 | add_filter( 'wp_query_route_to_rest_api_use_parent_class', '__return_false' ); 519 | ``` 520 | 521 | 522 | ## Hooks 523 | 524 | **Before WP_Query:** 525 | ```php 526 | function my_before_query($args) { 527 | // do whatever 528 | } 529 | add_action( 'wp_query_route_to_rest_api_before_query', 'my_before_query' ); 530 | ``` 531 | **After WP_Query:** 532 | ```php 533 | function my_after_query($wp_query) { 534 | // do whatever 535 | } 536 | add_action( 'wp_query_route_to_rest_api_after_query', 'my_after_query' ); 537 | ``` 538 | 539 | 540 | ## Install 541 | 542 | Download and activate. That's it. 543 | 544 | **Composer:** 545 | ``` 546 | $ composer require aucor/wp_query-route-to-rest-api 547 | ``` 548 | **With composer.json:** 549 | ``` 550 | { 551 | "require": { 552 | "aucor/wp_query-route-to-rest-api": "*" 553 | }, 554 | "extra": { 555 | "installer-paths": { 556 | "htdocs/wp-content/plugins/{$name}/": ["type:wordpress-plugin"] 557 | } 558 | } 559 | } 560 | ``` 561 | 562 | 563 | ## Issues and feature whishlist 564 | 565 | This is a WordPress plugin by 3rd party developer. WordPress.org or Automattic has nothing to do with this plugin. There's no warranty or quarantees. Thread carefully. 566 | 567 | If you see a critical functionality missing, please contribute! 568 | 569 | **Looking for similar API to WP_User_Query?** 570 | 571 | Install also [MEOM/meom-user-query](https://github.com/MEOM/meom-user-query) 572 | 573 | 574 | ## Changelog 575 | 576 | 577 | ### 1.3.2 578 | 579 | Fix PHP warning caused by 1.3.0 argument sanitizing refactoring. 580 | 581 | 582 | ### 1.3.1 583 | 584 | Fix composer license to a valid license. 585 | 586 | 587 | ### 1.3.0 588 | 589 | Compatibility release with a few new features. 100% backwards compatible. 590 | 591 | * Custom HTML output is now allowed (#10) 592 | * Instance and argument sanitizing can be now reused (#11) 593 | * Typos in filter names are fixed while keeping old names also working (#5) 594 | * Updates WP version, plugin version and some readme tweaks 595 | 596 | 597 | ### 1.2.0 598 | 599 | WordPress.org release. 600 | 601 | 602 | ### 1.1.1 603 | 604 | Added advanced example in readme for getting PHP WP_Query for JS. Added table of contents. Made the title hierarchy more logical. 605 | 606 | 607 | ### 1.1 608 | 609 | Make the return data structure same as /wp-json/wp/posts/. The data schema was missing some data before. Now the structure is inherited from the WP_REST_Posts_Controller as it should have from the start. 610 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === WP_Query Route To REST API === 2 | Contributors: teemusuoranta, samikeijonen, christian-nikkanen 3 | Tags: WordPress, REST API, WP_Query 4 | Requires at least: 4.7.3 5 | Tested up to: 5.9.3 6 | Stable tag: 1.3.2 7 | Requires PHP: 7.0 8 | License: GPLv2+ 9 | 10 | Adds new route /wp-json/wp_query/args/ to REST API. 11 | 12 | == Description == 13 | 14 | = Features = 15 | 16 | * Adds new route /wp-json/wp_query/args/ to REST API. 17 | * You can query content with WP_Query args. 18 | * There's extensive filters and actions to limit or extend functionality. 19 | * Built-in compatibility for [Relevanssi ('s' argument)](https://wordpress.org/plugins/relevanssi/) and [Polylang ('lang' argument)](https://wordpress.org/plugins/polylang/) 20 | 21 | 22 | == Installation == 23 | 24 | Download and activate. That's it. 25 | 26 | == Changelog == 27 | 28 | = 1.3.2 = 29 | *Release Date - 12 April 2022* 30 | 31 | Fix PHP warning caused by 1.3.0 argument sanitizing refactoring. 32 | 33 | = 1.3.1 = 34 | *Release Date - 7 April 2022* 35 | 36 | Fix composer license to a valid license. 37 | 38 | = 1.3.0 = 39 | *Release Date - 5 April 2022* 40 | 41 | Compatibility release with a few new features. 100% backwards compatible. 42 | 43 | * Custom HTML output is now allowed (#10) 44 | * Instance and argument sanitizing can be now reused (#11) 45 | * Typos in filter names are fixed while keeping old names also working (#5) 46 | * Updates WP version, plugin version and some readme tweaks 47 | 48 | = 1.2.0 = 49 | *Release Date - 25 July 2019* 50 | 51 | *WordPress.org release 52 | 53 | = 1.1.1 = 54 | *Release Date - 3 June 2017* 55 | 56 | * Added advanced example in readme for getting PHP WP_Query for JS. Added table of contents. Made the title hierarchy more logical. 57 | 58 | = 1.1 = 59 | *Release Date - 5 April 2017* 60 | 61 | * Make the return data structure same as /wp-json/wp/posts/. The data schema was missing some data before. Now the structure is inherited from the WP_REST_Posts_Controller as it should have from the start. 62 | -------------------------------------------------------------------------------- /wp_query-route-to-rest-api.php: -------------------------------------------------------------------------------- 1 | register_routes(); 27 | 28 | } 29 | 30 | /** 31 | * Register read-only /wp_query/args/ route 32 | */ 33 | 34 | public function register_routes() { 35 | register_rest_route( 'wp_query', 'args', array( 36 | 'methods' => WP_REST_Server::READABLE, 37 | 'callback' => array( $this, 'get_items' ), 38 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 39 | ) ); 40 | } 41 | 42 | /** 43 | * Check if a given request has access to get items 44 | * 45 | * @param WP_REST_Request $request Full data about the request. 46 | * 47 | * @return WP_Error|bool 48 | */ 49 | 50 | public function get_items_permissions_check( $request ) { 51 | return apply_filters( 'wp_query_route_to_rest_api_permissions_check', true, $request ); 52 | } 53 | 54 | public function sanitize_query_parameters ( $parameters ) { 55 | $default_args = array( 56 | 'post_status' => 'publish', 57 | 'posts_per_page' => 10, 58 | 'has_password' => false 59 | ); 60 | $default_args = apply_filters( 'wp_query_route_to_rest_api_default_args', $default_args ); 61 | 62 | // allow these args => what isn't explicitly allowed, is forbidden 63 | $allowed_args = array( 64 | 'p', 65 | 'name', 66 | 'title', 67 | 'page_id', 68 | 'pagename', 69 | 'post_parent', 70 | 'post_parent__in', 71 | 'post_parent__not_in', 72 | 'post__in', 73 | 'post__not_in', 74 | 'post_name__in', 75 | 'post_type', // With restrictions 76 | 'posts_per_page', // With restrictions 77 | 'offset', 78 | 'paged', 79 | 'page', 80 | 'ignore_sticky_posts', 81 | 'order', 82 | 'orderby', 83 | 'year', 84 | 'monthnum', 85 | 'w', 86 | 'day', 87 | 'hour', 88 | 'minute', 89 | 'second', 90 | 'm', 91 | 'date_query', 92 | 'inclusive', 93 | 'compare', 94 | 'column', 95 | 'relation', 96 | 'post_mime_type', 97 | 'lang', // Polylang 98 | ); 99 | 100 | 101 | // Allow filtering by author: default yes (backwards compatibility: legacy filter with typo) 102 | $allow_authors = apply_filters( 'wp_query_route_to_rest_api_allow_authors', apply_filters( 'wp_query_toute_to_rest_api_allow_authors', true ) ); 103 | if ( $allow_authors ) { 104 | $allowed_args[] = 'author'; 105 | $allowed_args[] = 'author_name'; 106 | $allowed_args[] = 'author__in'; 107 | $allowed_args[] = 'author__not_in'; 108 | } 109 | 110 | // Allow filtering by meta: default yes (backwards compatibility: legacy filter with typo) 111 | $allow_meta = apply_filters( 'wp_query_route_to_rest_api_allow_meta', apply_filters( 'wp_query_toute_to_rest_api_allow_meta', true ) ); 112 | if ( $allow_meta ) { 113 | $allowed_args[] = 'meta_key'; 114 | $allowed_args[] = 'meta_value'; 115 | $allowed_args[] = 'meta_value_num'; 116 | $allowed_args[] = 'meta_compare'; 117 | $allowed_args[] = 'meta_query'; 118 | } 119 | 120 | // Allow search: default yes (backwards compatibility: legacy filter with typo) 121 | $allow_search = apply_filters( 'wp_query_route_to_rest_api_allow_search', apply_filters( 'wp_query_toute_to_rest_api_allow_search', true ) ); 122 | if ( $allow_search ) { 123 | $allowed_args[] = 's'; 124 | } 125 | 126 | // Allow filtering by taxonomies: default yes (backwards compatibility: legacy filter with typo) 127 | $allow_taxonomies = apply_filters( 'wp_query_route_to_rest_api_allow_taxonomies', apply_filters( 'wp_query_toute_to_rest_api_allow_taxonomies', true ) ); 128 | if ( $allow_taxonomies ) { 129 | $allowed_args[] = 'cat'; 130 | $allowed_args[] = 'category_name'; 131 | $allowed_args[] = 'category__and'; 132 | $allowed_args[] = 'category__in'; 133 | $allowed_args[] = 'category__not_in'; 134 | $allowed_args[] = 'tag'; 135 | $allowed_args[] = 'tag_id'; 136 | $allowed_args[] = 'tag__and'; 137 | $allowed_args[] = 'tag__in'; 138 | $allowed_args[] = 'tag__not_in'; 139 | $allowed_args[] = 'tag_slug__and'; 140 | $allowed_args[] = 'tag_slug__in'; 141 | $allowed_args[] = 'tax_query'; 142 | } 143 | 144 | // let themes and plugins ultimately decide what to allow 145 | $allowed_args = apply_filters( 'wp_query_route_to_rest_api_allowed_args', $allowed_args ); 146 | 147 | // args from url 148 | $query_args = array(); 149 | 150 | foreach ( $parameters as $key => $value ) { 151 | 152 | // skip keys that are not explicitly allowed 153 | if( in_array( $key, $allowed_args ) ) { 154 | 155 | switch ( $key ) { 156 | 157 | // Posts type restrictions 158 | case 'post_type': 159 | 160 | // Multiple values 161 | if( is_array( $value ) ) { 162 | foreach ( $value as $sub_key => $sub_value ) { 163 | // Bail if there's even one post type that's not allowed 164 | if( !$this->check_is_post_type_allowed( $sub_value ) ) { 165 | $query_args[ $key ] = 'post'; 166 | break; 167 | } 168 | } 169 | 170 | // Value "any" 171 | } elseif ( $value == 'any' ) { 172 | $query_args[ $key ] = $this->_get_allowed_post_types(); 173 | break; 174 | 175 | // Single value 176 | } elseif ( !$this->check_is_post_type_allowed( $value ) ) { 177 | $query_args[ $key ] = 'post'; 178 | break; 179 | } 180 | 181 | $query_args[ $key ] = $value; 182 | break; 183 | 184 | // Posts per page restrictions 185 | case 'posts_per_page': 186 | 187 | $max_pages = apply_filters( 'wp_query_route_to_rest_api_max_posts_per_page', 50 ); 188 | if( $value <= 0 || $value > $max_pages ) { 189 | $query_args[ $key ] = $max_pages; 190 | break; 191 | } 192 | $query_args[ $key ] = $value; 193 | break; 194 | 195 | // Posts per page restrictions 196 | case 'posts_status': 197 | 198 | // Multiple values 199 | if( is_array( $value ) ) { 200 | foreach ( $value as $sub_key => $sub_value ) { 201 | // Bail if there's even one post status that's not allowed 202 | if( !$this->check_is_post_status_allowed( $sub_value ) ) { 203 | $query_args[ $key ] = 'publish'; 204 | break; 205 | } 206 | } 207 | 208 | // Value "any" 209 | } elseif ( $value == 'any' ) { 210 | $query_args[ $key ] = $this->_get_allowed_post_status(); 211 | break; 212 | 213 | // Single value 214 | } elseif ( !$this->check_is_post_status_allowed( $value ) ) { 215 | $query_args[ $key ] = 'publish'; 216 | break; 217 | } 218 | 219 | $query_args[ $key ] = $value; 220 | break; 221 | 222 | // Set given value 223 | default: 224 | $query_args[ $key ] = $value; 225 | break; 226 | } 227 | } 228 | } 229 | 230 | // Combine defaults and query_args 231 | $args = wp_parse_args( $query_args, $default_args ); 232 | 233 | // Make all the values filterable 234 | foreach ($args as $key => $value) { 235 | $args[$key] = apply_filters( 'wp_query_route_to_rest_api_arg_value', $value, $key, $args ); 236 | } 237 | 238 | return $args; 239 | } 240 | 241 | public function build_query ( $parameters ) { 242 | $args = $this->sanitize_query_parameters( $parameters ); 243 | 244 | // Before query: hook your plugins here 245 | do_action( 'wp_query_route_to_rest_api_before_query', $args ); 246 | 247 | // Run query 248 | $wp_query = new WP_Query( $args ); 249 | 250 | // After query: hook your plugins here 251 | do_action( 'wp_query_route_to_rest_api_after_query', $wp_query ); 252 | 253 | return $wp_query; 254 | } 255 | 256 | /** 257 | * Get a collection of items 258 | * 259 | * @param WP_REST_Request $request Full data about the request. 260 | */ 261 | 262 | public function get_items( $request ) { 263 | $parameters = $request->get_query_params(); 264 | $args = $this->sanitize_query_parameters( $parameters ); 265 | $wp_query = $this->build_query( $parameters ); 266 | 267 | $data = array(); 268 | $data = apply_filters( 'wp_query_route_to_rest_api_default_data', $data ); 269 | 270 | while ( $wp_query->have_posts() ) : $wp_query->the_post(); 271 | 272 | // Extra safety check for unallowed posts 273 | if ( $this->check_is_post_allowed( $wp_query->post ) ) { 274 | // After loop hook 275 | $data = apply_filters( 'wp_query_route_to_rest_api_after_loop_data', $data, $wp_query, $args ); 276 | 277 | // Update properties post_type and meta to match current post_type 278 | // This is kind of hacky, but the parent WP_REST_Posts_Controller 279 | // does all kinds of assumptions from properties $post_type and 280 | // $meta so we need to update it several times. 281 | // Allow filtering by meta: default yes 282 | if( apply_filters( 'wp_query_route_to_rest_api_update_post_type_meta', true ) ) { 283 | $this->post_type = $wp_query->post->post_type; 284 | $this->meta = new WP_REST_Post_Meta_Fields( $wp_query->post->post_type ); 285 | } 286 | 287 | // Use parent class functions to prepare the post 288 | if( apply_filters( 'wp_query_route_to_rest_api_use_parent_class', true ) ) { 289 | $itemdata = parent::prepare_item_for_response( $wp_query->post, $request ); 290 | $data[] = parent::prepare_response_for_collection( $itemdata ); 291 | } 292 | } 293 | 294 | endwhile; 295 | 296 | return $this->get_response( $request, $args, $wp_query, $data ); 297 | } 298 | 299 | /** 300 | * Get response 301 | * 302 | * @access protected 303 | * 304 | * @param WP_REST_Request $request Full details about the request 305 | * @param array $args WP_Query args 306 | * @param WP_Query $wp_query 307 | * @param array $data response data 308 | * 309 | * @return WP_REST_Response 310 | */ 311 | 312 | protected function get_response( $request, $args, $wp_query, $data ) { 313 | 314 | // Prepare data 315 | $response = new WP_REST_Response( $data, 200 ); 316 | 317 | // Total amount of posts 318 | $response->header( 'X-WP-Total', intval( $wp_query->found_posts ) ); 319 | 320 | // Total number of pages 321 | $max_pages = ( absint( $args[ 'posts_per_page' ] ) == 0 ) ? 1 : ceil( $wp_query->found_posts / $args[ 'posts_per_page' ] ); 322 | $response->header( 'X-WP-TotalPages', intval( $max_pages ) ); 323 | 324 | return $response; 325 | } 326 | 327 | /** 328 | * Get allowed post status 329 | * 330 | * @access protected 331 | * 332 | * @return array $post_status 333 | */ 334 | 335 | protected function _get_allowed_post_status() { 336 | $post_status = array( 'publish' ); 337 | return apply_filters( 'wp_query_route_to_rest_api_allowed_post_status', $post_status ); 338 | } 339 | 340 | /** 341 | * Check is post status allowed 342 | * 343 | * @access protected 344 | * 345 | * @return abool 346 | */ 347 | 348 | protected function check_is_post_status_allowed( $post_status ) { 349 | return in_array( $post_status, $this->_get_allowed_post_status() ); 350 | } 351 | 352 | /** 353 | * Get allowed post types 354 | * 355 | * @access protected 356 | * 357 | * @return array $post_types 358 | */ 359 | 360 | protected function _get_allowed_post_types() { 361 | $post_types = get_post_types( array( 'show_in_rest' => true ) ); 362 | return apply_filters( 'wp_query_route_to_rest_api_allowed_post_types', $post_types ); 363 | } 364 | 365 | /** 366 | * Check is post type allowed 367 | * 368 | * @access protected 369 | * 370 | * @return abool 371 | */ 372 | 373 | protected function check_is_post_type_allowed( $post_type ) { 374 | return in_array( $post_type, $this->_get_allowed_post_types() ); 375 | } 376 | 377 | /** 378 | * Post is allowed 379 | * 380 | * @access protected 381 | * 382 | * @return bool 383 | */ 384 | 385 | protected function check_is_post_allowed( $post ) { 386 | 387 | // Is allowed post_status 388 | if( !$this->check_is_post_status_allowed( $post->post_status ) ) { 389 | return false; 390 | } 391 | 392 | // Is allowed post_type 393 | if( !$this->check_is_post_type_allowed( $post->post_type ) ) { 394 | return false; 395 | } 396 | 397 | return apply_filters( 'wp_query_route_to_rest_api_post_is_allowed', true, $post ); 398 | 399 | } 400 | 401 | /** 402 | * Plugin compatibility args 403 | * 404 | * @param array $args 405 | * 406 | * @return array $args 407 | */ 408 | 409 | public function plugin_compatibility_args( $args ) { 410 | 411 | // Polylang compatibility 412 | $args[] = 'lang'; 413 | 414 | return $args; 415 | } 416 | 417 | /** 418 | * Plugin compatibility after query 419 | * 420 | * @param WP_Query $wp_query 421 | */ 422 | 423 | public function plugin_compatibility_after_query( $wp_query ) { 424 | 425 | // Relevanssi compatibility 426 | if( function_exists( 'relevanssi_do_query' ) && !empty( $wp_query->query_vars[ 's' ] ) ) { 427 | relevanssi_do_query( $wp_query ); 428 | } 429 | 430 | } 431 | 432 | } 433 | 434 | /** 435 | * This allows access to the class instance from other places. 436 | */ 437 | 438 | function wp_query_route_to_rest_api_get_instance() { 439 | static $instance; 440 | 441 | if ( ! $instance ) { 442 | $instance = new WP_Query_Route_To_REST_API(); 443 | } 444 | 445 | return $instance; 446 | } 447 | 448 | /** 449 | * Init only when needed 450 | */ 451 | 452 | function wp_query_route_to_rest_api_init() { 453 | wp_query_route_to_rest_api_get_instance(); 454 | } 455 | add_action( 'rest_api_init', 'wp_query_route_to_rest_api_init' ); 456 | 457 | --------------------------------------------------------------------------------