PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
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 3.8.6, at src/modules/REST/V1/Languages.php

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