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 / class-snippets-rest-controller.php

class-snippets-rest-controller.php in Code Snippets 4.0.0-beta.1, at php/rest-api/class-snippets-rest-controller.php

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