PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / REST_API / Snippets / Snippets_REST_Controller.php

Snippets_REST_Controller.php in Code Snippets trunk, at php/REST_API/Snippets/Snippets_REST_Controller.php

975 lines 29.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\REST_API\Snippets;
4
5 use Code_Snippets\Migration\Export\Export;
6 use Code_Snippets\Migration\Export\Export_Code;
7 use Code_Snippets\Migration\Export\Export_JSON;
8 use Code_Snippets\Model\Snippet;
9 use Code_Snippets\REST_API\REST_Collection_Controller;
10 use WP_Error;
11 use WP_REST_Request;
12 use WP_REST_Response;
13 use WP_REST_Server;
14 use function Code_Snippets\activate_snippet;
15 use function Code_Snippets\clean_active_snippets_cache;
16 use function Code_Snippets\code_snippets;
17 use function Code_Snippets\deactivate_snippet;
18 use function Code_Snippets\delete_snippet;
19 use function Code_Snippets\get_snippet_author;
20 use function Code_Snippets\get_snippet;
21 use function Code_Snippets\get_snippets;
22 use function Code_Snippets\restore_snippet;
23 use function Code_Snippets\save_snippet;
24 use function Code_Snippets\trash_snippet;
25
26 /**
27 * Allows fetching snippet data through the WordPress REST API.
28 *
29 * @since 3.4.0
30 * @package Code_Snippets
31 */
32 final class Snippets_REST_Controller extends REST_Collection_Controller {
33
34 /**
35 * Current API version.
36 */
37 public const VERSION = 1;
38
39 /**
40 * The base of this controller's route.
41 */
42 public const BASE_ROUTE = 'snippets';
43
44 /**
45 * Register REST routes.
46 */
47 public function register_routes() {
48 $route = '/' . self::BASE_ROUTE;
49 $id_route = $route . '/(?P<id>[\d]+)';
50
51 $network_args = array_intersect_key(
52 $this->get_endpoint_args_for_item_schema(),
53 [ 'network' ]
54 );
55
56 // Allow standard collection parameters (page, per_page, etc.) on the collection route.
57 $collection_args = array_merge( $network_args, $this->get_collection_params() );
58
59 $collection_args['status'] = [
60 'description' => esc_html__( 'Filter snippets by activation status.', 'code-snippets' ),
61 'type' => 'string',
62 'enum' => [ 'all', 'active', 'inactive' ],
63 'default' => 'all',
64 'sanitize_callback' => 'sanitize_key',
65 ];
66
67 $collection_args['exclude_types'] = [
68 'description' => esc_html__( 'List of snippet types to exclude from the response.', 'code-snippets' ),
69 'type' => 'array',
70 'items' => [
71 'type' => 'string',
72 ],
73 'default' => [],
74 'sanitize_callback' => static function ( $value ): array {
75 $values = is_array( $value ) ? $value : [ $value ];
76 return array_values( array_filter( array_map( 'sanitize_key', $values ) ) );
77 },
78 ];
79
80 $collection_args['orderby'] = [
81 'description' => esc_html__( 'Sort collection by object attribute.', 'code-snippets' ),
82 'type' => 'string',
83 'enum' => [ 'id', 'name', 'display_name' ],
84 'sanitize_callback' => 'sanitize_key',
85 ];
86
87 $collection_args['order'] = [
88 'description' => esc_html__( 'Sort direction.', 'code-snippets' ),
89 'type' => 'string',
90 'enum' => [ 'asc', 'desc' ],
91 'default' => 'asc',
92 'sanitize_callback' => 'sanitize_key',
93 ];
94
95 register_rest_route(
96 $this->namespace,
97 $route,
98 [
99 [
100 'methods' => WP_REST_Server::READABLE,
101 'callback' => [ $this, 'get_items' ],
102 'permission_callback' => [ $this, 'get_items_permissions_check' ],
103 'args' => $collection_args,
104 ],
105 [
106 'methods' => WP_REST_Server::CREATABLE,
107 'callback' => [ $this, 'create_item' ],
108 'permission_callback' => [ $this, 'create_item_permissions_check' ],
109 'args' => $this->get_endpoint_args_for_item_schema( true ),
110 ],
111 'schema' => [ $this, 'get_item_schema' ],
112 ]
113 );
114
115 register_rest_route(
116 $this->namespace,
117 $id_route,
118 [
119 [
120 'methods' => WP_REST_Server::READABLE,
121 'callback' => [ $this, 'get_item' ],
122 'permission_callback' => [ $this, 'get_item_permissions_check' ],
123 'args' => $network_args,
124 ],
125 [
126 'methods' => WP_REST_Server::EDITABLE,
127 'callback' => [ $this, 'update_item' ],
128 'permission_callback' => [ $this, 'update_item_permissions_check' ],
129 'args' => $this->get_endpoint_args_for_item_schema( false ),
130 ],
131 [
132 'methods' => WP_REST_Server::DELETABLE,
133 'callback' => [ $this, 'delete_item' ],
134 'permission_callback' => [ $this, 'delete_item_permissions_check' ],
135 'args' => $network_args,
136 ],
137 'schema' => [ $this, 'get_item_schema' ],
138 ]
139 );
140
141 register_rest_route(
142 $this->namespace,
143 $id_route . '/restore',
144 [
145 'methods' => WP_REST_Server::EDITABLE,
146 'callback' => [ $this, 'restore_item' ],
147 'permission_callback' => [ $this, 'update_item_permissions_check' ],
148 'args' => $network_args,
149 ]
150 );
151
152 register_rest_route(
153 $this->namespace,
154 $route . '/schema',
155 [
156 'methods' => WP_REST_Server::READABLE,
157 'callback' => [ $this, 'get_public_item_schema' ],
158 'permission_callback' => '__return_true',
159 ]
160 );
161
162 register_rest_route(
163 $this->namespace,
164 $id_route . '/activate',
165 [
166 'methods' => WP_REST_Server::EDITABLE,
167 'callback' => [ $this, 'activate_item' ],
168 'permission_callback' => [ $this, 'toggle_item_permissions_check' ],
169 'schema' => [ $this, 'get_item_schema' ],
170 'args' => $network_args,
171 ]
172 );
173
174 register_rest_route(
175 $this->namespace,
176 $id_route . '/deactivate',
177 [
178 'methods' => WP_REST_Server::EDITABLE,
179 'callback' => [ $this, 'deactivate_item' ],
180 'permission_callback' => [ $this, 'toggle_item_permissions_check' ],
181 'schema' => [ $this, 'get_item_schema' ],
182 'args' => $network_args,
183 ]
184 );
185
186 register_rest_route(
187 $this->namespace,
188 $id_route . '/export',
189 [
190 'methods' => WP_REST_Server::READABLE,
191 'callback' => [ $this, 'export_item' ],
192 'permission_callback' => [ $this, 'get_item_permissions_check' ],
193 'schema' => [ $this, 'get_item_schema' ],
194 'args' => $network_args,
195 ]
196 );
197
198 register_rest_route(
199 $this->namespace,
200 $id_route . '/export-code',
201 [
202 'methods' => WP_REST_Server::READABLE,
203 'callback' => [ $this, 'export_item_code' ],
204 'permission_callback' => [ $this, 'get_item_permissions_check' ],
205 'schema' => [ $this, 'get_item_schema' ],
206 'args' => $network_args,
207 ]
208 );
209 }
210
211 /**
212 * Determine whether the request has permission to access snippets.
213 *
214 * @param WP_REST_Request $request Incoming HTTP request.
215 *
216 * @return bool
217 */
218 public function permission_callback( WP_REST_Request $request ): bool {
219 return code_snippets()->current_user_can();
220 }
221
222 /**
223 * Determine whether a request targets network-scoped snippets.
224 *
225 * Only the literal boolean `true` (or its common string/integer equivalents)
226 * is treated as a network-scoped request. A missing or null `network` param
227 * means "site-scoped", and must not be escalated to the network capability.
228 *
229 * @param WP_REST_Request|WP_Error $request Full data about the request.
230 *
231 * @return bool
232 */
233 private function is_network_scoped_request( $request ): bool {
234 if ( ! is_multisite() || ! $request instanceof WP_REST_Request || ! $request->has_param( 'network' ) ) {
235 return false;
236 }
237
238 $network = $request->get_param( 'network' );
239
240 if ( is_bool( $network ) ) {
241 return $network;
242 }
243
244 if ( is_string( $network ) ) {
245 return ! in_array( strtolower( $network ), [ '0', 'false', 'no', '' ], true );
246 }
247
248 return (bool) $network;
249 }
250
251 /**
252 * Verify the current user has permission for the scope implied by the request.
253 *
254 * @param WP_REST_Request $request Full data about the request.
255 *
256 * @return bool
257 */
258 private function check_request_capability( WP_REST_Request $request ): bool {
259 return $this->is_network_scoped_request( $request )
260 ? code_snippets()->user_can_manage_network_snippets()
261 : code_snippets()->current_user_can();
262 }
263
264 /**
265 * Determine whether the request targets a shared network snippet.
266 *
267 * Shared network snippets are stored network-wide but each site decides whether
268 * to activate them via the per-site `active_shared_network_snippets` option. The
269 * `id` route parameter is used to look up the snippet so the result reflects the
270 * actual stored row rather than a value supplied in the request payload.
271 *
272 * @param WP_REST_Request|WP_Error $request Full data about the request.
273 *
274 * @return bool
275 */
276 private function is_shared_network_snippet_request( $request ): bool {
277 if ( ! is_multisite() || ! $request instanceof WP_REST_Request ) {
278 return false;
279 }
280
281 $snippet_id = absint( $request->get_param( 'id' ) );
282
283 if ( ! $snippet_id ) {
284 return false;
285 }
286
287 $snippet = get_snippet( $snippet_id, true );
288 return $snippet->id && $snippet->shared_network;
289 }
290
291 /**
292 * Check if a given request has access to get items.
293 *
294 * @param WP_REST_Request $request Full data about the request.
295 *
296 * @return bool
297 */
298 public function get_items_permissions_check( $request ): bool {
299 return $this->check_request_capability( $request );
300 }
301
302 /**
303 * Check if a given request has access to get a specific item.
304 *
305 * Shared network snippets are readable by any user who can manage snippets on
306 * the current site, since the snippet is intentionally exposed to subsites.
307 *
308 * @param WP_REST_Request $request Full data about the request.
309 *
310 * @return bool
311 */
312 public function get_item_permissions_check( $request ): bool {
313 if ( $this->is_shared_network_snippet_request( $request ) ) {
314 return code_snippets()->current_user_can();
315 }
316
317 return $this->check_request_capability( $request );
318 }
319
320 /**
321 * Check if a given request has access to create items.
322 *
323 * @param WP_REST_Request $request Full data about the request.
324 *
325 * @return bool
326 */
327 public function create_item_permissions_check( $request ): bool {
328 return $this->check_request_capability( $request );
329 }
330
331 /**
332 * Check if a given request has access to update a specific item.
333 *
334 * @param WP_REST_Request $request Full data about the request.
335 *
336 * @return bool
337 */
338 public function update_item_permissions_check( $request ): bool {
339 return $this->check_request_capability( $request );
340 }
341
342 /**
343 * Check if a given request has access to delete a specific item.
344 *
345 * @param WP_REST_Request $request Full data about the request.
346 *
347 * @return bool
348 */
349 public function delete_item_permissions_check( $request ): bool {
350 return $this->check_request_capability( $request );
351 }
352
353 /**
354 * Check if a given request has access to toggle a snippet's activation.
355 *
356 * For shared network snippets the activation toggle only writes to the
357 * per-site `active_shared_network_snippets` option, so the site capability
358 * is sufficient. For all other snippets we keep the strict capability check
359 * that prevents a subsite admin from forging `network=true` to operate on
360 * exclusive network-scoped snippets.
361 *
362 * @param WP_REST_Request $request Full data about the request.
363 *
364 * @return bool
365 */
366 public function toggle_item_permissions_check( WP_REST_Request $request ): bool {
367 if ( $this->is_shared_network_snippet_request( $request ) ) {
368 return code_snippets()->current_user_can();
369 }
370
371 return $this->check_request_capability( $request );
372 }
373
374 /**
375 * Retrieves a collection of snippets, with pagination.
376 *
377 * @param WP_REST_Request $request Full details about the request.
378 *
379 * @return WP_REST_Response Response object on success.
380 */
381 public function get_items( $request ): WP_REST_Response {
382 $network = $request->get_param( 'network' );
383 $all_snippets = $this->get_network_items( get_snippets( [], $network ), $network );
384
385 $status = sanitize_key( (string) $request->get_param( 'status' ) );
386
387 $exclude_types = $request->get_param( 'exclude_types' );
388 $exclude_types = is_array( $exclude_types ) ? array_map( 'sanitize_key', $exclude_types ) : [];
389
390 if ( $exclude_types || 'all' !== $status ) {
391 $all_snippets = array_filter(
392 $all_snippets,
393 static function ( Snippet $snippet ) use ( $exclude_types, $status ): bool {
394 if ( $exclude_types && in_array( $snippet->type, $exclude_types, true ) ) {
395 return false;
396 }
397
398 if ( 'active' === $status ) {
399 return ! $snippet->trashed && $snippet->active;
400 }
401
402 if ( 'inactive' === $status ) {
403 return ! $snippet->trashed && ! $snippet->active;
404 }
405
406 return true;
407 }
408 );
409 }
410
411 $orderby = sanitize_key( (string) $request->get_param( 'orderby' ) );
412 $order = sanitize_key( (string) $request->get_param( 'order' ) );
413
414 if ( $orderby ) {
415 $direction = 'desc' === $order ? -1 : 1;
416
417 usort(
418 $all_snippets,
419 static function ( Snippet $a, Snippet $b ) use ( $orderby, $direction ): int {
420 switch ( $orderby ) {
421 case 'display_name':
422 $cmp = strcasecmp( $a->display_name, $b->display_name );
423 break;
424 case 'name':
425 $cmp = strcasecmp( $a->name, $b->name );
426 break;
427 case 'id':
428 default:
429 $cmp = $a->id <=> $b->id;
430 break;
431 }
432
433 return 0 === $cmp ? ( $a->id <=> $b->id ) * $direction : $cmp * $direction;
434 }
435 );
436 }
437
438 $total_items = count( $all_snippets );
439 $query_params = $request->get_query_params();
440
441 if ( isset( $query_params['per_page'] ) || isset( $query_params['page'] ) ) {
442 $collection_params = $this->get_collection_params();
443 $per_page = isset( $query_params['per_page'] )
444 ? max( 1, (int) $query_params['per_page'] )
445 : (int) $collection_params['per_page']['default'];
446
447 $page_request = (int) $request->get_param( 'page' );
448 $page = max( 1, $page_request ? $page_request : (int) $collection_params['page']['default'] );
449 $total_pages = (int) ceil( $total_items / $per_page );
450
451 $offset = ( $page - 1 ) * $per_page;
452 $snippets = array_slice( $all_snippets, $offset, $per_page );
453 } else {
454 $snippets = $all_snippets;
455 $total_pages = 1;
456 }
457
458 $response = rest_ensure_response(
459 array_map(
460 function ( $snippet ) use ( $request ) {
461 $response_item = $this->prepare_item_for_response( $snippet, $request );
462 return $this->prepare_response_for_collection( $response_item );
463 },
464 $snippets
465 )
466 );
467
468 $response->header( 'X-WP-Total', (string) $total_items );
469 $response->header( 'X-WP-TotalPages', (string) $total_pages );
470
471 return $response;
472 }
473
474 /**
475 * Retrieve and merge shared network snippets.
476 *
477 * @param Snippet[] $all_snippets List of snippets to merge with.
478 * @param bool|null $network Whether fetching network snippets.
479 *
480 * @return Snippet[] Modified list of snippets.
481 */
482 private function get_network_items( array $all_snippets, ?bool $network ): array {
483 if ( ! is_multisite() || $network ) {
484 return $all_snippets;
485 }
486
487 $shared_ids = get_site_option( 'shared_network_snippets' );
488
489 if ( ! $shared_ids || ! is_array( $shared_ids ) ) {
490 return $all_snippets;
491 }
492
493 $active_shared_snippets = get_option( 'active_shared_network_snippets', [] );
494 $shared_snippets = get_snippets( $shared_ids, true );
495
496 foreach ( $shared_snippets as $snippet ) {
497 $snippet->shared_network = true;
498 $snippet->active = in_array( $snippet->id, $active_shared_snippets, true );
499 }
500
501 return array_merge( $all_snippets, $shared_snippets );
502 }
503
504 /**
505 * Retrieves one item from the collection.
506 *
507 * @param WP_REST_Request $request Full details about the request.
508 *
509 * @return WP_REST_Response|WP_Error Response object on success.
510 */
511 public function get_item( $request ) {
512 $snippet_id = $request->get_param( 'id' );
513 $item = get_snippet( $snippet_id, $request->get_param( 'network' ) );
514
515 if ( ! $item->id && 0 !== $snippet_id && '0' !== $snippet_id ) {
516 return new WP_Error(
517 'rest_cannot_get',
518 __( 'The snippet could not be found.', 'code-snippets' ),
519 [ 'status' => 404 ]
520 );
521 }
522
523 $data = $this->prepare_item_for_response( $item, $request );
524 return rest_ensure_response( $data );
525 }
526
527 /**
528 * Create one item from the collection
529 *
530 * @param WP_REST_Request|array $request Full data about the request.
531 *
532 * @return WP_REST_Response|WP_Error
533 */
534 public function create_item( $request ) {
535 $snippet = $this->prepare_item_for_database( $request );
536 $result = save_snippet( $snippet );
537
538 return $result
539 ? $this->prepare_item_for_response( $result, $request )
540 : new WP_Error(
541 'rest_cannot_create',
542 __( 'The snippet could not be created.', 'code-snippets' ),
543 [ 'status' => 500 ]
544 );
545 }
546
547 /**
548 * Update one item from the collection
549 *
550 * @param WP_REST_Request $request Full data about the request.
551 *
552 * @return WP_Error|WP_REST_Response
553 */
554 public function update_item( $request ) {
555 $snippet_id = absint( $request->get_param( 'id' ) );
556 $snippet = $snippet_id ? get_snippet( $snippet_id, $request->get_param( 'network' ) ) : null;
557
558 if ( ! $snippet_id || ! $snippet || ! $snippet->id ) {
559 return new WP_Error(
560 'rest_cannot_update',
561 __( 'Cannot update a snippet without a valid ID.', 'code-snippets' ),
562 [ 'status' => 400 ]
563 );
564 }
565
566 $item = $this->prepare_item_for_database( $request, $snippet );
567 $result = save_snippet( $item );
568
569 return $result
570 ? rest_ensure_response( $this->prepare_item_for_response( $result, $request ) )
571 : new WP_Error(
572 'rest_cannot_update',
573 __( 'The snippet could not be updated.', 'code-snippets' ),
574 [ 'status' => 500 ]
575 );
576 }
577
578 /**
579 * Delete one item from the collection, or trash it if not already trashed.
580 *
581 * @param WP_REST_Request $request Full data about the request.
582 *
583 * @return WP_Error|WP_REST_Response
584 */
585 public function delete_item( $request ) {
586 $item = $this->prepare_item_for_database( $request );
587 $snippet = get_snippet( $item->id, $item->network );
588
589 if ( ! $snippet || ! $snippet->id ) {
590 return new WP_Error(
591 'rest_cannot_delete',
592 __( 'The snippet could not be found.', 'code-snippets' ),
593 [ 'status' => 404 ]
594 );
595 }
596
597 if ( $snippet->trashed ) {
598 return delete_snippet( $snippet->id, $snippet->network )
599 ? new WP_REST_Response( null, 204 )
600 : new WP_Error(
601 'rest_cannot_delete',
602 __( 'The snippet could not be deleted.', 'code-snippets' ),
603 [ 'status' => 500 ]
604 );
605 } else {
606 return trash_snippet( $snippet->id, $snippet->network )
607 ? $this->get_item( $request )
608 : new WP_Error(
609 'rest_cannot_trash',
610 __( 'The snippet could not be trashed.', 'code-snippets' ),
611 [ 'status' => 500 ]
612 );
613 }
614 }
615
616 /**
617 * Restore a deleted item from the trash.
618 *
619 * @param WP_REST_Request $request Snippet information.
620 *
621 * @return WP_Error|WP_REST_Response
622 */
623 public function restore_item( WP_REST_Request $request ) {
624 $item = $this->prepare_item_for_database( $request );
625
626 return restore_snippet( $item->id, $item->network )
627 ? new WP_REST_Response( null, 204 )
628 : new WP_Error(
629 'rest_cannot_restore',
630 __( 'The snippet could not be restored.', 'code-snippets' ),
631 [ 'status' => 500 ]
632 );
633 }
634
635 /**
636 * Fetch snippet using data from request.
637 *
638 * @param WP_REST_Request $request Request containing 'id' and 'network' parameters.
639 *
640 * @return Snippet|WP_Error
641 */
642 private function get_requested_snippet( WP_REST_Request $request ) {
643 $id = $request->get_param( 'id' );
644
645 $snippet = $id && is_numeric( $id )
646 ? get_snippet( $id, $request->get_param( 'network' ) )
647 : null;
648
649 if ( ! $snippet || ! $snippet->id ) {
650 return new WP_Error(
651 'rest_cannot_activate',
652 __( 'The snippet could not be found.', 'code-snippets' ),
653 [ 'status' => 404 ]
654 );
655 }
656
657 return $snippet;
658 }
659
660 /**
661 * Activate one item in the collection.
662 *
663 * @param WP_REST_Request $request Full data about the request.
664 *
665 * @return WP_Error|WP_REST_Response
666 */
667 public function activate_item( WP_REST_Request $request ) {
668 $snippet = $this->get_requested_snippet( $request );
669
670 if ( is_wp_error( $snippet ) ) {
671 return rest_ensure_response( $snippet );
672 }
673
674 if ( $snippet->shared_network ) {
675 $this->set_shared_network_active( $snippet->id, true );
676 $snippet->active = true;
677 return rest_ensure_response( $snippet );
678 }
679
680 $result = activate_snippet( $snippet->id, $snippet->network );
681
682 return $result instanceof Snippet
683 ? rest_ensure_response( $result )
684 : new WP_Error(
685 'rest_cannot_activate',
686 $result,
687 [ 'status' => 500 ]
688 );
689 }
690
691 /**
692 * Deactivate one item in the collection.
693 *
694 * @param WP_REST_Request $request Full data about the request.
695 *
696 * @return WP_Error|WP_REST_Response
697 */
698 public function deactivate_item( WP_REST_Request $request ) {
699 $snippet = $this->get_requested_snippet( $request );
700
701 if ( is_wp_error( $snippet ) ) {
702 return rest_ensure_response( $snippet );
703 }
704
705 if ( $snippet->shared_network ) {
706 $this->set_shared_network_active( $snippet->id, false );
707 $snippet->active = false;
708 return rest_ensure_response( $snippet );
709 }
710
711 $result = deactivate_snippet( $snippet->id, $snippet->network );
712
713 return $result instanceof Snippet
714 ? rest_ensure_response( $result )
715 : new WP_Error(
716 'rest_cannot_activate',
717 __( 'The snippet could not be deactivated.', 'code-snippets' ),
718 [ 'status' => 500 ]
719 );
720 }
721
722 /**
723 * Toggle a shared network snippet's active state for the current site only.
724 *
725 * @param int $snippet_id Snippet identifier.
726 * @param bool $active Whether the snippet should be active on the current site.
727 *
728 * @return void
729 */
730 private function set_shared_network_active( int $snippet_id, bool $active ): void {
731 $active_shared_snippets = get_option( 'active_shared_network_snippets', [] );
732
733 if ( ! is_array( $active_shared_snippets ) ) {
734 $active_shared_snippets = [];
735 }
736
737 $already_active = in_array( $snippet_id, $active_shared_snippets, true );
738
739 if ( $active === $already_active ) {
740 return;
741 }
742
743 $active_shared_snippets = $active
744 ? array_merge( $active_shared_snippets, [ $snippet_id ] )
745 : array_values( array_diff( $active_shared_snippets, [ $snippet_id ] ) );
746
747 update_option( 'active_shared_network_snippets', $active_shared_snippets );
748 clean_active_snippets_cache( code_snippets()->db->ms_table );
749 }
750
751 /**
752 * Prepare an instance of the Export class from a request.
753 *
754 * @param Export $export Instance of Export class to use for generating response.
755 *
756 * @return WP_REST_Response
757 */
758 protected function build_export_response( Export $export ): WP_REST_Response {
759 $response = rest_ensure_response( $export->generate_export() );
760 $response->header( 'X-Suggested-Filename', $export->build_filename() );
761 return $response;
762 }
763
764 /**
765 * Retrieve one item in the collection in JSON export format.
766 *
767 * @param WP_REST_Request $request Full data about the request.
768 *
769 * @return WP_REST_Response
770 */
771 public function export_item( WP_REST_Request $request ): WP_REST_Response {
772 $item = $this->prepare_item_for_database( $request );
773 $export = new Export_JSON( [ $item->id ], $item->network );
774
775 return $this->build_export_response( $export );
776 }
777
778 /**
779 * Retrieve one item in the collection in the code export format.
780 *
781 * @param WP_REST_Request $request Full data about the request.
782 *
783 * @return WP_REST_Response
784 */
785 public function export_item_code( WP_REST_Request $request ): WP_REST_Response {
786 $item = $this->prepare_item_for_database( $request );
787 $export = new Export_Code( [ $item->id ], $item->network );
788
789 return $this->build_export_response( $export );
790 }
791
792 /**
793 * Prepares one item for create or update operation.
794 *
795 * @param WP_REST_Request $request Request object.
796 * @param Snippet|null $item Existing item to augment.
797 *
798 * @return Snippet The prepared item.
799 */
800 protected function prepare_item_for_database( $request, ?Snippet $item = null ): Snippet {
801 if ( ! $item instanceof Snippet ) {
802 $item = new Snippet();
803 }
804
805 foreach ( $item->get_allowed_fields() as $field ) {
806 if ( $request->has_param( $field ) ) {
807 $item->set_field( $field, $request->get_param( $field ) );
808 }
809 }
810
811 return $item;
812 }
813
814 /**
815 * Prepare the item for the REST response.
816 *
817 * @param Snippet $item Snippet object.
818 * @param WP_REST_Request $request Request object.
819 *
820 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
821 */
822 public function prepare_item_for_response( $item, $request ) {
823 $schema = $this->get_item_schema();
824
825 $properties = array_keys( $schema['properties'] );
826
827 $response_data = array_map(
828 function ( $property ) use ( $item ) {
829 switch ( $property ) {
830 case 'created_by':
831 return get_snippet_author( $item->created_by );
832
833 case 'updated_by':
834 return get_snippet_author( $item->updated_by );
835
836 default:
837 return $item->$property;
838 }
839 },
840 $properties
841 );
842
843 $response = array_combine( $properties, $response_data );
844
845 // The schema declares this as a date-time, so send one: the stored value
846 // is UTC without an offset, which clients read as local time.
847 $response['modified'] = $item->modified_iso;
848
849 return rest_ensure_response( $response );
850 }
851
852 /**
853 * Get our sample schema for a post.
854 *
855 * @return array<string, mixed> The sample schema for a post
856 */
857 public function get_item_schema(): array {
858 if ( $this->schema ) {
859 return $this->schema;
860 }
861
862 $this->schema = [
863 '$schema' => 'http://json-schema.org/draft-04/schema#',
864 'title' => 'snippet',
865 'type' => 'object',
866 'properties' => [
867 'id' => [
868 'description' => esc_html__( 'Unique identifier for the snippet.', 'code-snippets' ),
869 'type' => 'integer',
870 'readonly' => true,
871 ],
872 'name' => [
873 'description' => esc_html__( 'Descriptive title for the snippet.', 'code-snippets' ),
874 'type' => 'string',
875 ],
876 'desc' => [
877 'description' => esc_html__( 'Descriptive text associated with snippet.', 'code-snippets' ),
878 'type' => 'string',
879 ],
880 'code' => [
881 'description' => esc_html__( 'Executable snippet code.', 'code-snippets' ),
882 'type' => 'string',
883 ],
884 'tags' => [
885 'description' => esc_html__( 'List of tag categories the snippet belongs to.', 'code-snippets' ),
886 'type' => 'array',
887 'items' => [
888 'type' => 'string',
889 ],
890 ],
891 'scope' => [
892 'description' => esc_html__( 'Context in which the snippet is executable.', 'code-snippets' ),
893 'type' => 'string',
894 ],
895 'condition_id' => [
896 'description' => esc_html__( 'Identifier of condition linked to this snippet.', 'code-snippets' ),
897 'type' => 'integer',
898 ],
899 'active' => [
900 'description' => esc_html__( 'Snippet activation status.', 'code-snippets' ),
901 'type' => 'boolean',
902 ],
903 'trashed' => [
904 'description' => esc_html__( 'Whether the snippet is marked as deleted.', 'code-snippets' ),
905 'type' => 'boolean',
906 'readonly' => true,
907 ],
908 'locked' => [
909 'description' => esc_html__( 'Whether the snippet is locked from modification or deletion.', 'code-snippets' ),
910 'type' => 'boolean',
911 ],
912 'priority' => [
913 'description' => esc_html__( 'Relative priority in which the snippet is executed.', 'code-snippets' ),
914 'type' => 'integer',
915 ],
916 'network' => [
917 'description' => esc_html__( 'Whether the snippet is network-wide instead of site-wide.', 'code-snippets' ),
918 'type' => [ 'boolean', 'null' ],
919 'default' => null,
920 ],
921 'shared_network' => [
922 'description' => esc_html__( 'If a network snippet, whether can be activated on discrete sites instead of network-wide.', 'code-snippets' ),
923 'type' => [ 'boolean', 'null' ],
924 ],
925 'modified' => [
926 'description' => esc_html__( 'Date and time when the snippet was last modified, in ISO format.', 'code-snippets' ),
927 'type' => 'string',
928 'format' => 'date-time',
929 'readonly' => true,
930 ],
931 'last_active' => [
932 'description' => esc_html__( 'Timestamp of when the snippet was last active, if available.', 'code-snippets' ),
933 'type' => 'integer',
934 'readonly' => true,
935 ],
936 'code_error' => [
937 'description' => esc_html__( 'Error message if the snippet code could not be parsed.', 'code-snippets' ),
938 'type' => [ 'array', 'null' ],
939 'items' => [
940 'type' => [ 'string', 'integer' ],
941 ],
942 'readonly' => true,
943 ],
944 'code_error_trace' => [
945 'description' => esc_html__( 'Stack trace for the most recent snippet code error.', 'code-snippets' ),
946 'type' => [ 'string', 'null' ],
947 'readonly' => true,
948 ],
949 'created_by' => [
950 'description' => esc_html__( 'The snippet author.', 'code-snippets' ),
951 'type' => [ 'object', 'null' ],
952 'readonly' => true,
953 'properties' => [
954 'id' => [ 'type' => 'integer' ],
955 'display_name' => [ 'type' => 'string' ],
956 'avatar_url' => [ 'type' => 'string' ],
957 ],
958 ],
959 'updated_by' => [
960 'description' => esc_html__( 'The most recent editor.', 'code-snippets' ),
961 'type' => [ 'object', 'null' ],
962 'readonly' => true,
963 'properties' => [
964 'id' => [ 'type' => 'integer' ],
965 'display_name' => [ 'type' => 'string' ],
966 'avatar_url' => [ 'type' => 'string' ],
967 ],
968 ],
969 ],
970 ];
971
972 return $this->schema;
973 }
974 }
975