PluginProbe
Polylang / trunk
Polylang vtrunk
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / src / modules / REST / V1 / Languages.php

Languages.php in Polylang trunk, at src/modules/REST/V1/Languages.php

721 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 namespace WP_Syntex\Polylang\REST\V1;
7
8 use PLL_Language;
9 use PLL_Model;
10 use PLL_Translatable_Objects;
11 use stdClass;
12 use WP_Error;
13 use WP_REST_Request;
14 use WP_REST_Response;
15 use WP_REST_Server;
16 use WP_Syntex\Polylang\Capabilities\Capabilities;
17 use WP_Syntex\Polylang\Model\Languages as Languages_Model;
18 use WP_Syntex\Polylang\REST\Abstract_Controller;
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Languages REST controller.
24 *
25 * @since 3.7
26 */
27 class Languages extends Abstract_Controller {
28 /**
29 * @var Languages_Model
30 */
31 private $languages;
32
33 /**
34 * @var PLL_Translatable_Objects
35 */
36 private $translatable_objects;
37
38 /**
39 * The namespace of this controller's route. Override the parent class property with a default value.
40 *
41 * @var string
42 * @phpstan-var 'pll/v1'
43 */
44 protected $namespace = 'pll/v1';
45
46 /**
47 * The base of this controller's route. Override the parent class property with a default value.
48 *
49 * @var string
50 * @phpstan-var 'languages'
51 */
52 protected $rest_base = 'languages';
53
54 /**
55 * Constructor.
56 *
57 * @since 3.7
58 *
59 * @param PLL_Model $model Polylang's model.
60 */
61 public function __construct( PLL_Model $model ) {
62 $this->languages = $model->languages;
63 $this->translatable_objects = $model->translatable_objects;
64 }
65
66 /**
67 * Registers the routes for languages.
68 *
69 * @since 3.7
70 *
71 * @return void
72 */
73 public function register_routes(): void {
74 register_rest_route(
75 $this->namespace,
76 "/{$this->rest_base}",
77 array(
78 array(
79 'methods' => WP_REST_Server::READABLE,
80 'callback' => array( $this, 'get_items' ),
81 'permission_callback' => array( $this, 'get_items_permissions_check' ),
82 ),
83 array(
84 'methods' => WP_REST_Server::CREATABLE,
85 'callback' => array( $this, 'create_item' ),
86 'permission_callback' => array( $this, 'create_item_permissions_check' ),
87 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
88 ),
89 'schema' => array( $this, 'get_public_item_schema' ),
90 'allow_batch' => array( 'v1' => true ),
91 )
92 );
93
94 $readable = array(
95 'methods' => WP_REST_Server::READABLE,
96 'callback' => array( $this, 'get_item' ),
97 'permission_callback' => array( $this, 'get_item_permissions_check' ),
98 'args' => array(
99 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
100 ),
101 );
102
103 register_rest_route(
104 $this->namespace,
105 "/{$this->rest_base}/(?P<term_id>[\d]+)",
106 array(
107 'args' => array(
108 'term_id' => array(
109 'description' => __( 'Unique identifier for the language.', 'polylang' ),
110 'type' => 'integer',
111 ),
112 ),
113 $readable,
114 array(
115 'methods' => WP_REST_Server::EDITABLE,
116 'callback' => array( $this, 'update_item' ),
117 'permission_callback' => array( $this, 'update_item_permissions_check' ),
118 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
119 ),
120 array(
121 'methods' => WP_REST_Server::DELETABLE,
122 'callback' => array( $this, 'delete_item' ),
123 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
124 ),
125 'schema' => array( $this, 'get_public_item_schema' ),
126 'allow_batch' => array( 'v1' => true ),
127 )
128 );
129 register_rest_route(
130 $this->namespace,
131 sprintf( '/%1$s/(?P<slug>%2$s)', $this->rest_base, Languages_Model::INNER_SLUG_PATTERN ),
132 array(
133 'args' => array(
134 'slug' => array(
135 'description' => __( 'Language code - preferably 2-letters ISO 639-1 (for example: en).', 'polylang' ),
136 'type' => 'string',
137 ),
138 ),
139 $readable,
140 'schema' => array( $this, 'get_public_item_schema' ),
141 'allow_batch' => array( 'v1' => true ),
142 )
143 );
144 }
145
146 /**
147 * Retrieves all languages.
148 *
149 * @since 3.7
150 *
151 * @param WP_REST_Request $request Full details about the request.
152 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
153 *
154 * @phpstan-template T of array
155 * @phpstan-param WP_REST_Request<T> $request
156 */
157 public function get_items( $request ) {
158 $response = array();
159
160 foreach ( $this->languages->get_list() as $language ) {
161 $language = $this->prepare_item_for_response( $language, $request );
162 $response[] = $this->prepare_response_for_collection( $language );
163 }
164
165 return rest_ensure_response( $response );
166 }
167
168 /**
169 * Creates one language from the collection.
170 *
171 * @since 3.7
172 *
173 * @param WP_REST_Request $request Full details about the request.
174 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
175 *
176 * @phpstan-template T of array
177 * @phpstan-param WP_REST_Request<T> $request
178 */
179 public function create_item( $request ) {
180 if ( isset( $request['term_id'] ) ) {
181 return new WP_Error(
182 'rest_exists',
183 __( 'Cannot create existing language.', 'polylang' ),
184 array( 'status' => 400 )
185 );
186 }
187
188 /**
189 * @phpstan-var array{
190 * locale: non-empty-string,
191 * slug?: non-empty-string,
192 * name?: non-empty-string,
193 * is_rtl?: bool,
194 * term_group?: int,
195 * flag?: non-empty-string,
196 * no_default_cat?: bool
197 * } $args
198 */
199 $args = $request->get_params();
200 $language = $this->languages->add( $args );
201
202 if ( is_wp_error( $language ) ) {
203 return $this->add_status_to_error( $language );
204 }
205
206 return $this->prepare_item_for_response( $language, $request );
207 }
208
209 /**
210 * Retrieves one language from the collection.
211 *
212 * @since 3.7
213 *
214 * @param WP_REST_Request $request Full details about the request.
215 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
216 *
217 * @phpstan-template T of array
218 * @phpstan-param WP_REST_Request<T> $request
219 */
220 public function get_item( $request ) {
221 $language = $this->get_language( $request );
222
223 if ( is_wp_error( $language ) ) {
224 return $language;
225 }
226
227 return $this->prepare_item_for_response( $language, $request );
228 }
229
230 /**
231 * Updates one language from the collection.
232 *
233 * @since 3.7
234 *
235 * @param WP_REST_Request $request Full details about the request.
236 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
237 *
238 * @phpstan-template T of array
239 * @phpstan-param WP_REST_Request<T> $request
240 */
241 public function update_item( $request ) {
242 $language = $this->get_language( $request );
243 if ( is_wp_error( $language ) ) {
244 return $language;
245 }
246
247 /**
248 * @phpstan-var array{
249 * term_id: int,
250 * locale?: non-empty-string,
251 * slug?: non-empty-string,
252 * name?: non-empty-string,
253 * is_rtl?: bool,
254 * term_group?: int,
255 * flag?: non-empty-string
256 * } $args
257 */
258 $args = $request->get_params();
259 $args['lang_id'] = $language->term_id;
260 $language = $this->languages->update( $args );
261
262 if ( is_wp_error( $language ) ) {
263 return $this->add_status_to_error( $language );
264 }
265
266 return $this->prepare_item_for_response( $language, $request );
267 }
268
269 /**
270 * Deletes one language from the collection.
271 *
272 * @since 3.7
273 *
274 * @param WP_REST_Request $request Full details about the request.
275 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
276 *
277 * @phpstan-template T of array
278 * @phpstan-param WP_REST_Request<T> $request
279 */
280 public function delete_item( $request ) {
281 $language = $this->get_language( $request );
282
283 if ( is_wp_error( $language ) ) {
284 return $language;
285 }
286
287 $this->languages->delete( $language->term_id );
288
289 $previous = $this->prepare_item_for_response( $language, $request );
290 $response = new WP_REST_Response();
291 $response->set_data(
292 array(
293 'deleted' => true,
294 'previous' => $previous->get_data(),
295 )
296 );
297
298 return $response;
299 }
300
301 /**
302 * Checks if a given request has access to get the languages.
303 *
304 * @since 3.7
305 *
306 * @param WP_REST_Request $request Full details about the request.
307 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
308 *
309 * @phpstan-template T of array
310 * @phpstan-param WP_REST_Request<T> $request
311 */
312 public function get_items_permissions_check( $request ) {
313 if ( 'edit' === $request['context'] && ! $this->check_update_permission() ) {
314 return new WP_Error(
315 'rest_forbidden_context',
316 __( 'Sorry, you are not allowed to edit languages.', 'polylang' ),
317 array( 'status' => rest_authorization_required_code() )
318 );
319 }
320 return true;
321 }
322
323 /**
324 * Checks if a given request has access to create a language.
325 *
326 * @since 3.7
327 *
328 * @param WP_REST_Request $request Full details about the request.
329 * @return true|WP_Error True if the request has access to create languages, WP_Error object otherwise.
330 *
331 * @phpstan-template T of array
332 * @phpstan-param WP_REST_Request<T> $request
333 */
334 public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
335 if ( ! $this->check_update_permission() ) {
336 return new WP_Error(
337 'rest_cannot_create',
338 __( 'Sorry, you are not allowed to create a language.', 'polylang' ),
339 array( 'status' => rest_authorization_required_code() )
340 );
341 }
342 return true;
343 }
344
345 /**
346 * Checks if a given request has access to get a specific language.
347 *
348 * @since 3.7
349 *
350 * @param WP_REST_Request $request Full details about the request.
351 * @return true|WP_Error True if the request has read access for the language, WP_Error object otherwise.
352 *
353 * @phpstan-template T of array
354 * @phpstan-param WP_REST_Request<T> $request
355 */
356 public function get_item_permissions_check( $request ) {
357 return $this->get_items_permissions_check( $request );
358 }
359
360 /**
361 * Checks if a given request has access to update a specific language.
362 *
363 * @since 3.7
364 *
365 * @param WP_REST_Request $request Full details about the request.
366 * @return true|WP_Error True if the request has access to update the language, WP_Error object otherwise.
367 *
368 * @phpstan-template T of array
369 * @phpstan-param WP_REST_Request<T> $request
370 */
371 public function update_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
372 if ( ! $this->check_update_permission() ) {
373 return new WP_Error(
374 'rest_cannot_update',
375 __( 'Sorry, you are not allowed to edit this language.', 'polylang' ),
376 array( 'status' => rest_authorization_required_code() )
377 );
378 }
379 return true;
380 }
381
382 /**
383 * Checks if a given request has access to delete a specific language.
384 *
385 * @since 3.7
386 *
387 * @param WP_REST_Request $request Full details about the request.
388 * @return true|WP_Error True if the request has access to delete the language, WP_Error object otherwise.
389 *
390 * @phpstan-template T of array
391 * @phpstan-param WP_REST_Request<T> $request
392 */
393 public function delete_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
394 if ( ! $this->check_update_permission() ) {
395 return new WP_Error(
396 'rest_cannot_delete',
397 __( 'Sorry, you are not allowed to delete this language.', 'polylang' ),
398 array( 'status' => rest_authorization_required_code() )
399 );
400 }
401 return true;
402 }
403
404 /**
405 * Prepares the language for the REST response.
406 *
407 * @since 3.7
408 *
409 * @param PLL_Language $item Language object.
410 * @param WP_REST_Request $request Request object.
411 * @return WP_REST_Response Response object.
412 *
413 * @phpstan-template T of array
414 * @phpstan-param WP_REST_Request<T> $request
415 */
416 public function prepare_item_for_response( $item, $request ) {
417 $data = $item->to_array();
418 $fields = $this->get_fields_for_response( $request );
419 $response = array();
420
421 $data['is_rtl'] = (bool) $data['is_rtl'];
422 $data['host'] = (string) $data['host'];
423
424 foreach ( $data as $language_prop => $prop_value ) {
425 if ( rest_is_field_included( $language_prop, $fields ) ) {
426 $response[ $language_prop ] = $prop_value;
427 }
428 }
429
430 /** @var WP_REST_Response */
431 return rest_ensure_response( $response );
432 }
433
434 /**
435 * Retrieves the language's schema, conforming to JSON Schema.
436 *
437 * @since 3.7
438 *
439 * @return array Item schema data.
440 */
441 public function get_item_schema(): array {
442 if ( $this->schema ) {
443 return $this->add_additional_fields_schema( $this->schema );
444 }
445
446 $this->schema = array(
447 '$schema' => 'http://json-schema.org/draft-04/schema#',
448 'title' => 'language',
449 'type' => 'object',
450 'properties' => array(
451 'term_id' => array(
452 'description' => __( 'Unique identifier for the language.', 'polylang' ),
453 'type' => 'integer',
454 'minimum' => 1,
455 'context' => array( 'view', 'edit' ),
456 'readonly' => true,
457 ),
458 'name' => array(
459 'description' => __( 'The name is how it is displayed on your site (for example: English).', 'polylang' ),
460 'type' => 'string',
461 'minLength' => 1,
462 'context' => array( 'view', 'edit' ),
463 ),
464 'slug' => array(
465 'description' => __( 'Language code - preferably 2-letters ISO 639-1 (for example: en).', 'polylang' ),
466 'type' => 'string',
467 'pattern' => Languages_Model::SLUG_PATTERN,
468 'context' => array( 'view', 'edit' ),
469 ),
470 'locale' => array(
471 'description' => __( 'WordPress Locale for the language (for example: en_US).', 'polylang' ),
472 'type' => 'string',
473 'pattern' => Languages_Model::LOCALE_PATTERN,
474 'context' => array( 'view', 'edit' ),
475 ),
476 'w3c' => array(
477 'description' => __( 'W3C Locale for the language (for example: en-US).', 'polylang' ),
478 'type' => 'string',
479 'context' => array( 'view', 'edit' ),
480 'readonly' => true,
481 ),
482 'facebook' => array(
483 'description' => __( 'Facebook Locale for the language (for example: en_US).', 'polylang' ),
484 'type' => 'string',
485 'context' => array( 'view', 'edit' ),
486 'readonly' => true,
487 ),
488 'is_rtl' => array(
489 'description' => sprintf(
490 /* translators: %s is a value. */
491 __( 'Text direction. %s for right-to-left.', 'polylang' ),
492 '`true`'
493 ),
494 'type' => 'boolean',
495 'context' => array( 'view', 'edit' ),
496 ),
497 'term_group' => array(
498 'description' => __( 'Position of the language in the language switcher.', 'polylang' ),
499 'type' => 'integer',
500 'context' => array( 'view', 'edit' ),
501 ),
502 'flag_code' => array(
503 'description' => __( 'Flag code corresponding to ISO 3166-1 (for example: us for the United States flag).', 'polylang' ),
504 'type' => 'string',
505 'context' => array( 'view', 'edit' ),
506 ),
507 'flag_url' => array(
508 'description' => __( 'Flag URL.', 'polylang' ),
509 'type' => 'string',
510 'format' => 'uri',
511 'context' => array( 'view', 'edit' ),
512 'readonly' => true,
513 ),
514 'flag' => array(
515 'description' => __( 'HTML tag for the flag.', 'polylang' ),
516 'type' => 'string',
517 'context' => array( 'view', 'edit' ),
518 'readonly' => true,
519 ),
520 'custom_flag_url' => array(
521 'description' => __( 'Custom flag URL.', 'polylang' ),
522 'type' => 'string',
523 'format' => 'uri',
524 'context' => array( 'view', 'edit' ),
525 'readonly' => true,
526 ),
527 'custom_flag' => array(
528 'description' => __( 'HTML tag for the custom flag.', 'polylang' ),
529 'type' => 'string',
530 'context' => array( 'view', 'edit' ),
531 'readonly' => true,
532 ),
533 'is_default' => array(
534 'description' => __( 'Tells whether the language is the default one.', 'polylang' ),
535 'type' => 'boolean',
536 'context' => array( 'view', 'edit' ),
537 'readonly' => true,
538 ),
539 'active' => array(
540 'description' => __( 'Tells whether the language is active.', 'polylang' ),
541 'type' => 'boolean',
542 'context' => array( 'view', 'edit' ),
543 'readonly' => true,
544 ),
545 'home_url' => array(
546 'description' => __( 'Home URL in this language.', 'polylang' ),
547 'type' => 'string',
548 'format' => 'uri',
549 'context' => array( 'view', 'edit' ),
550 'readonly' => true,
551 ),
552 'search_url' => array(
553 'description' => __( 'Search URL in this language.', 'polylang' ),
554 'type' => 'string',
555 'format' => 'uri',
556 'context' => array( 'view', 'edit' ),
557 'readonly' => true,
558 ),
559 'host' => array(
560 'description' => __( 'Host for this language.', 'polylang' ),
561 'type' => 'string',
562 'format' => 'uri',
563 'context' => array( 'view', 'edit' ),
564 'readonly' => true,
565 ),
566 'page_on_front' => array(
567 'description' => __( 'Page on front ID in this language.', 'polylang' ),
568 'type' => 'integer',
569 'minimum' => 0,
570 'context' => array( 'view', 'edit' ),
571 'readonly' => true,
572 ),
573 'page_for_posts' => array(
574 'description' => __( 'Identifier of the page for posts in this language.', 'polylang' ),
575 'type' => 'integer',
576 'minimum' => 0,
577 'context' => array( 'view', 'edit' ),
578 'readonly' => true,
579 ),
580 'fallbacks' => array(
581 'description' => __( 'List of language locale fallbacks.', 'polylang' ),
582 'type' => 'array',
583 'uniqueItems' => true,
584 'items' => array(
585 'type' => 'string',
586 'pattern' => Languages_Model::LOCALE_PATTERN,
587 ),
588 'context' => array( 'view', 'edit' ),
589 'readonly' => true,
590 ),
591 'term_props' => array(
592 'description' => __( 'Language properties.', 'polylang' ),
593 'type' => 'object',
594 'properties' => array(),
595 'context' => array( 'view', 'edit' ),
596 'readonly' => true,
597 ),
598 'no_default_cat' => array(
599 'description' => __( 'Tells whether the default category must be created when creating a new language.', 'polylang' ),
600 'type' => 'boolean',
601 'context' => array( 'edit' ),
602 'default' => false,
603 ),
604 ),
605 );
606
607 foreach ( $this->translatable_objects as $translatable_object ) {
608 $this->schema['properties']['term_props']['properties'][ $translatable_object->get_tax_language() ] = array(
609 'description' => $translatable_object->get_rest_description(),
610 'type' => 'object',
611 'properties' => array(
612 'term_id' => array(
613 /* translators: %s is the name of the term property (`term_id` or `term_taxonomy_id`). */
614 'description' => sprintf( __( 'The %s of the language term for this translatable entity.', 'polylang' ), '`term_id`' ),
615 'type' => 'integer',
616 'minimum' => 1,
617 ),
618 'term_taxonomy_id' => array(
619 /* translators: %s is the name of the term property (`term_id` or `term_taxonomy_id`). */
620 'description' => sprintf( __( 'The %s of the language term for this translatable entity.', 'polylang' ), '`term_taxonomy_id`' ),
621 'type' => 'integer',
622 'minimum' => 1,
623 ),
624 'count' => array(
625 'description' => __( 'Number of items of this type of content in this language.', 'polylang' ),
626 'type' => 'integer',
627 'minimum' => 0,
628 ),
629 ),
630 );
631 }
632
633 return $this->add_additional_fields_schema( $this->schema );
634 }
635
636 /**
637 * Retrieves an array of endpoint arguments from the item schema for the controller.
638 * Ensures that the `no_default_cat` property is returned only for `CREATABLE` requests.
639 *
640 * @since 3.7
641 *
642 * @param string $method Optional. HTTP method of the request. Default WP_REST_Server::CREATABLE.
643 * @return array Endpoint arguments.
644 */
645 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
646 $schema = $this->get_item_schema();
647 if ( WP_REST_Server::CREATABLE !== $method ) {
648 unset( $schema['properties']['no_default_cat'] );
649 } else {
650 $schema['properties']['locale']['required'] = true;
651 }
652
653 return rest_get_endpoint_args_for_schema( $schema, $method );
654 }
655
656 /**
657 * Tells if languages can be edited.
658 *
659 * @since 3.7
660 *
661 * @return bool
662 */
663 protected function check_update_permission(): bool {
664 return current_user_can( Capabilities::LANGUAGES );
665 }
666
667 /**
668 * Returns the language, if the ID is valid.
669 *
670 * @since 3.7
671 *
672 * @param WP_REST_Request $request Full details about the request.
673 * @return PLL_Language|WP_Error Language object if the ID or slug is valid, WP_Error otherwise.
674 *
675 * @phpstan-template T of array
676 * @phpstan-param WP_REST_Request<T> $request
677 */
678 private function get_language( WP_REST_Request $request ) {
679 if ( isset( $request['term_id'] ) ) {
680 $error = new WP_Error(
681 'rest_invalid_id',
682 __( 'Invalid language ID', 'polylang' ),
683 array( 'status' => 404 )
684 );
685
686 if ( $request['term_id'] <= 0 ) {
687 return $error;
688 }
689
690 $language = $this->languages->get( (int) $request['term_id'] );
691
692 if ( ! $language instanceof PLL_Language ) {
693 return $error;
694 }
695
696 return $language;
697 }
698
699 if ( isset( $request['slug'] ) ) {
700 $language = $this->languages->get( (string) $request['slug'] );
701
702 if ( ! $language instanceof PLL_Language ) {
703 return new WP_Error(
704 'rest_invalid_slug',
705 __( 'Invalid language slug', 'polylang' ),
706 array( 'status' => 404 )
707 );
708 }
709
710 return $language;
711 }
712
713 // Should not happen.
714 return new WP_Error(
715 'rest_invalid_identifier',
716 __( 'Invalid language identifier', 'polylang' ),
717 array( 'status' => 404 )
718 );
719 }
720 }
721