PluginProbe
Code Snippets / 4.0.0-beta.1
Code Snippets v4.0.0-beta.1
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 4.0.0-beta.1, at php/REST_API/Snippets/Snippets_REST_Controller.php

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