PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.8
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.8
2.7.9 2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 trunk 1.3 2.0.4 2.0.6 2.1.91 2.2.4 2.2.7 2.2.9 2.3.1 2.3.10 2.4.10 2.4.2 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.6.0 All 30 releases
insert-php / includes / class.abilities.php

class.abilities.php in Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts 2.7.8, at includes/class.abilities.php

1,296 lines 42.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Registers the plugin abilities with the WordPress Abilities API.
4 *
5 * @package Woody_Code_Snippets
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * WINP_Abilities class.
15 */
16 class WINP_Abilities {
17
18 /**
19 * Ability category slug.
20 */
21 const CATEGORY = 'woody-snippets';
22
23 /**
24 * Maximum number of snippets returned per page.
25 */
26 const MAX_PER_PAGE = 100;
27
28 /**
29 * Maximum length of the conditions JSON string.
30 */
31 const MAX_CONDITIONS_LENGTH = 20000;
32
33 /**
34 * Maximum nesting depth of a condition value.
35 */
36 const MAX_VALUE_DEPTH = 4;
37
38 /**
39 * Constructor.
40 */
41 public function __construct() {
42 add_action( 'wp_abilities_api_categories_init', [ $this, 'register_category' ] );
43 add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
44 }
45
46 /**
47 * Register the ability category.
48 *
49 * @return void
50 */
51 public function register_category() {
52 if ( ! function_exists( 'wp_register_ability_category' ) ) {
53 return;
54 }
55
56 wp_register_ability_category(
57 self::CATEGORY,
58 [
59 'label' => __( 'Woody Code Snippets', 'insert-php' ),
60 'description' => __( 'Manage Woody code snippets: code, insertion location, display conditions and activation state.', 'insert-php' ),
61 ]
62 );
63 }
64
65 /**
66 * Register the abilities.
67 *
68 * @return void
69 */
70 public function register_abilities() {
71 if ( ! function_exists( 'wp_register_ability' ) ) {
72 return;
73 }
74
75 wp_register_ability(
76 'woody/list-snippets',
77 [
78 'label' => __( 'List snippets', 'insert-php' ),
79 'description' => __( 'Lists Woody snippets with their type, insertion scope, location and activation state. Snippet code is not included; use woody/get-snippet to read it.', 'insert-php' ),
80 'category' => self::CATEGORY,
81 'input_schema' => $this->get_list_input_schema(),
82 'output_schema' => [
83 'type' => 'object',
84 'properties' => [
85 'snippets' => [
86 'type' => 'array',
87 'items' => $this->get_snippet_schema( false ),
88 ],
89 'total' => [ 'type' => 'integer' ],
90 'page' => [ 'type' => 'integer' ],
91 'per_page' => [ 'type' => 'integer' ],
92 'total_pages' => [ 'type' => 'integer' ],
93 ],
94 ],
95 'execute_callback' => [ $this, 'list_snippets' ],
96 'permission_callback' => [ $this, 'can_manage_snippets' ],
97 'meta' => [
98 'annotations' => [
99 'readonly' => true,
100 'destructive' => false,
101 'idempotent' => true,
102 ],
103 'show_in_rest' => true,
104 ],
105 ]
106 );
107
108 wp_register_ability(
109 'woody/get-snippet',
110 [
111 'label' => __( 'Get snippet', 'insert-php' ),
112 'description' => __( 'Returns one Woody snippet with its code, type, insertion scope, location, display conditions and activation state.', 'insert-php' ),
113 'category' => self::CATEGORY,
114 'input_schema' => [
115 'type' => 'object',
116 'properties' => [
117 'snippet_id' => [
118 'type' => 'integer',
119 'description' => __( 'Snippet ID.', 'insert-php' ),
120 'minimum' => 1,
121 ],
122 ],
123 'required' => [ 'snippet_id' ],
124 'additionalProperties' => false,
125 ],
126 'output_schema' => $this->get_snippet_schema( true ),
127 'execute_callback' => [ $this, 'get_snippet' ],
128 'permission_callback' => [ $this, 'can_manage_snippets' ],
129 'meta' => [
130 'annotations' => [
131 'readonly' => true,
132 'destructive' => false,
133 'idempotent' => true,
134 ],
135 'show_in_rest' => true,
136 ],
137 ]
138 );
139
140 wp_register_ability(
141 'woody/upsert-snippet',
142 [
143 'label' => __( 'Create or update snippet', 'insert-php' ),
144 'description' => __( 'Creates a snippet (always inactive) or updates the code and execution settings of an existing one. PHP and universal snippets that run everywhere or are inserted automatically are validated first by evaluating the code once, exactly as the snippet editor does; nothing is saved when validation fails. With dry_run=true only the validation runs and nothing is saved. After an update an active snippet stays active only when the "activate by default" setting is on; this ability never activates an inactive snippet, use woody/set-snippet-state for that. CSS and JS snippet code cannot be written through this ability.', 'insert-php' ),
145 'category' => self::CATEGORY,
146 'input_schema' => $this->get_upsert_input_schema(),
147 'output_schema' => [
148 'type' => 'object',
149 'properties' => [
150 'dry_run' => [ 'type' => 'boolean' ],
151 'operation' => [
152 'type' => 'string',
153 'enum' => [ 'create', 'update' ],
154 ],
155 'valid' => [ 'type' => 'boolean' ],
156 'validated' => [
157 'type' => 'boolean',
158 'description' => __( 'Whether the code was evaluated. False for snippet types or scopes the editor does not validate.', 'insert-php' ),
159 ],
160 'message' => [ 'type' => 'string' ],
161 'revision_id' => [ 'type' => 'integer' ],
162 'snippet' => $this->get_snippet_schema( true ),
163 ],
164 ],
165 'execute_callback' => [ $this, 'upsert_snippet' ],
166 'permission_callback' => [ $this, 'can_manage_snippets' ],
167 'meta' => [
168 'annotations' => [
169 'readonly' => false,
170 'destructive' => true,
171 'idempotent' => false,
172 ],
173 'show_in_rest' => true,
174 'ai_connect' => false,
175 ],
176 ]
177 );
178
179 wp_register_ability(
180 'woody/set-snippet-state',
181 [
182 'label' => __( 'Activate or deactivate snippet', 'insert-php' ),
183 'description' => __( 'Activates or deactivates one snippet. Activating a PHP, universal or HTML snippet that runs everywhere or is inserted automatically first executes it once to check for errors, exactly as the activation switch in the snippets list does, and is refused when the check fails.', 'insert-php' ),
184 'category' => self::CATEGORY,
185 'input_schema' => [
186 'type' => 'object',
187 'properties' => [
188 'snippet_id' => [
189 'type' => 'integer',
190 'description' => __( 'Snippet ID.', 'insert-php' ),
191 'minimum' => 1,
192 ],
193 'state' => [
194 'type' => 'string',
195 'description' => __( 'Target state.', 'insert-php' ),
196 'enum' => [ 'active', 'inactive' ],
197 ],
198 ],
199 'required' => [ 'snippet_id', 'state' ],
200 'additionalProperties' => false,
201 ],
202 'output_schema' => [
203 'type' => 'object',
204 'properties' => [
205 'changed' => [ 'type' => 'boolean' ],
206 'snippet' => $this->get_snippet_schema( false ),
207 ],
208 ],
209 'execute_callback' => [ $this, 'set_snippet_state' ],
210 'permission_callback' => [ $this, 'can_manage_snippets' ],
211 'meta' => [
212 'annotations' => [
213 'readonly' => false,
214 'destructive' => true,
215 'idempotent' => true,
216 ],
217 'show_in_rest' => true,
218 'ai_connect' => false,
219 ],
220 ]
221 );
222 }
223
224 /**
225 * Permission check shared by all abilities.
226 *
227 * Mirrors the snippets admin screens: the plugin capability gate plus the
228 * capability of the snippets post type.
229 *
230 * @return bool
231 */
232 public function can_manage_snippets() {
233 if ( ! WINP_Plugin::app()->current_user_car() ) {
234 return false;
235 }
236
237 return current_user_can( $this->get_post_type_cap( 'edit_posts' ) );
238 }
239
240 /**
241 * Get a capability of the snippets post type.
242 *
243 * @param string $cap Post type capability key.
244 *
245 * @return string
246 */
247 private function get_post_type_cap( $cap ) {
248 $post_type = get_post_type_object( WINP_SNIPPETS_POST_TYPE );
249
250 if ( $post_type && isset( $post_type->cap->$cap ) ) {
251 return (string) $post_type->cap->$cap;
252 }
253
254 // Same value the post type registration maps 'edit_posts' and 'create_posts' to.
255 return 'edit_' . WINP_SNIPPETS_POST_TYPE . 's';
256 }
257
258 /**
259 * Execute callback: list snippets.
260 *
261 * @param array<string, mixed>|null $input Ability input.
262 *
263 * @return array<string, mixed>
264 */
265 public function list_snippets( $input = null ) {
266 $input = is_array( $input ) ? $input : [];
267 $page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1;
268 $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 20;
269 $per_page = min( self::MAX_PER_PAGE, max( 1, $per_page ) );
270 $status = isset( $input['status'] ) ? sanitize_key( (string) $input['status'] ) : 'any';
271
272 $statuses = [
273 'any' => [ 'publish', 'draft', 'pending', 'private', 'future' ],
274 'publish' => [ 'publish' ],
275 'draft' => [ 'draft' ],
276 'trash' => [ 'trash' ],
277 ];
278
279 $args = [
280 'post_type' => WINP_SNIPPETS_POST_TYPE,
281 'post_status' => isset( $statuses[ $status ] ) ? $statuses[ $status ] : $statuses['any'],
282 // As on the snippets list screen: private snippets of other users need read_private_posts.
283 'perm' => 'readable',
284 'posts_per_page' => $per_page,
285 'paged' => $page,
286 'orderby' => 'ID',
287 'order' => 'DESC',
288 ];
289
290 if ( ! empty( $input['search'] ) ) {
291 $args['s'] = sanitize_text_field( (string) $input['search'] );
292 }
293
294 $meta_query = [];
295
296 if ( ! empty( $input['type'] ) ) {
297 $type = sanitize_key( (string) $input['type'] );
298
299 if ( WINP_SNIPPET_TYPE_PHP === $type ) {
300 // Snippets without a stored type are PHP snippets.
301 $meta_query[] = [
302 'relation' => 'OR',
303 [
304 'key' => 'wbcr_inp_snippet_type',
305 'value' => $type,
306 ],
307 [
308 'key' => 'wbcr_inp_snippet_type',
309 'compare' => 'NOT EXISTS',
310 ],
311 ];
312 } else {
313 $meta_query[] = [
314 'key' => 'wbcr_inp_snippet_type',
315 'value' => $type,
316 ];
317 }
318 }
319
320 if ( ! empty( $input['scope'] ) ) {
321 $meta_query[] = [
322 'key' => 'wbcr_inp_snippet_scope',
323 'value' => $this->scope_to_meta( sanitize_key( (string) $input['scope'] ) ),
324 ];
325 }
326
327 if ( isset( $input['active'] ) ) {
328 if ( $input['active'] ) {
329 $meta_query[] = [
330 'key' => 'wbcr_inp_snippet_activate',
331 'value' => '1',
332 ];
333 } else {
334 $meta_query[] = [
335 'relation' => 'OR',
336 [
337 'key' => 'wbcr_inp_snippet_activate',
338 'value' => '1',
339 'compare' => '!=',
340 ],
341 [
342 'key' => 'wbcr_inp_snippet_activate',
343 'compare' => 'NOT EXISTS',
344 ],
345 ];
346 }
347 }
348
349 if ( ! empty( $meta_query ) ) {
350 $meta_query['relation'] = 'AND';
351 $args['meta_query'] = $meta_query;
352 }
353
354 $query = new WP_Query( $args );
355 $snippets = [];
356
357 foreach ( $query->posts as $post ) {
358 if ( $post instanceof WP_Post ) {
359 $snippets[] = $this->format_snippet( $post, false );
360 }
361 }
362
363 return [
364 'snippets' => $snippets,
365 'total' => (int) $query->found_posts,
366 'page' => $page,
367 'per_page' => $per_page,
368 'total_pages' => (int) $query->max_num_pages,
369 ];
370 }
371
372 /**
373 * Execute callback: get one snippet.
374 *
375 * @param array<string, mixed>|null $input Ability input.
376 *
377 * @return array<string, mixed>|WP_Error
378 */
379 public function get_snippet( $input = null ) {
380 $input = is_array( $input ) ? $input : [];
381 $post = $this->get_snippet_post( isset( $input['snippet_id'] ) ? (int) $input['snippet_id'] : 0 );
382
383 if ( is_wp_error( $post ) ) {
384 return $post;
385 }
386
387 // Same check the snippet editor applies before showing the code (post.php, edit_post).
388 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
389 return new WP_Error( 'woody_forbidden', __( 'You are not allowed to edit this snippet.', 'insert-php' ) );
390 }
391
392 return $this->format_snippet( $post, true );
393 }
394
395 /**
396 * Execute callback: create or update a snippet.
397 *
398 * @param array<string, mixed>|null $input Ability input.
399 *
400 * @return array<string, mixed>|WP_Error
401 */
402 public function upsert_snippet( $input = null ) {
403 $input = is_array( $input ) ? $input : [];
404 $dry_run = ! empty( $input['dry_run'] );
405 $snippet_id = isset( $input['snippet_id'] ) ? (int) $input['snippet_id'] : 0;
406 $post = null;
407
408 if ( ! post_type_exists( WINP_SNIPPETS_POST_TYPE ) ) {
409 return new WP_Error( 'woody_unavailable', __( 'The snippets post type is not registered in this context.', 'insert-php' ) );
410 }
411
412 if ( $snippet_id ) {
413 $post = $this->get_snippet_post( $snippet_id );
414
415 if ( is_wp_error( $post ) ) {
416 return $post;
417 }
418
419 if ( 'trash' === $post->post_status ) {
420 return new WP_Error( 'woody_snippet_trashed', __( 'The snippet is in the trash and cannot be edited.', 'insert-php' ) );
421 }
422
423 if ( ! current_user_can( 'edit_post', $snippet_id ) ) {
424 return new WP_Error( 'woody_forbidden', __( 'You are not allowed to edit this snippet.', 'insert-php' ) );
425 }
426 } elseif ( ! current_user_can( $this->get_post_type_cap( 'create_posts' ) ) ) {
427 return new WP_Error( 'woody_forbidden', __( 'You are not allowed to create snippets.', 'insert-php' ) );
428 }
429
430 $data = $this->resolve_snippet_data( $input, $post );
431
432 if ( is_wp_error( $data ) ) {
433 return $data;
434 }
435
436 $was_active = $post ? $this->is_active( $post->ID ) : false;
437
438 // Same rule the editor uses to decide whether the code has to be evaluated before saving.
439 $is_executable = in_array( $data['type'], [ WINP_SNIPPET_TYPE_PHP, WINP_SNIPPET_TYPE_UNIVERSAL ], true );
440 $needs_validation = $is_executable && 'shortcode' !== $data['scope'] && '' !== trim( $data['code'] ) && $data['execution_changed'];
441 $validation = [
442 'valid' => true,
443 'message' => '',
444 ];
445
446 if ( $needs_validation ) {
447 if ( WINP_Helper::is_safe_mode() ) {
448 return new WP_Error( 'woody_safe_mode', __( 'Safe mode is enabled, snippet code cannot be evaluated. Disable safe mode first.', 'insert-php' ) );
449 }
450
451 $validation = WINP_Code_Validator::validate_code( $this->prepare_code( $data['code'], $data['type'] ), $data['type'] );
452
453 if ( ! $validation['valid'] ) {
454 $validation['message'] = wp_strip_all_tags( str_replace( '<br>', "\n", $validation['message'] ) );
455
456 if ( $was_active ) {
457 $validation['message'] .= ' ' . __( 'The snippet is currently active and has already run in this request; deactivate it first if the error is caused by its own declarations.', 'insert-php' );
458 }
459 }
460 }
461
462 $operation = $post ? 'update' : 'create';
463
464 if ( $dry_run ) {
465 return [
466 'dry_run' => true,
467 'operation' => $operation,
468 'valid' => $validation['valid'],
469 'validated' => $needs_validation,
470 'message' => $validation['message'],
471 ];
472 }
473
474 if ( ! $validation['valid'] ) {
475 return new WP_Error( 'woody_invalid_code', $validation['message'] );
476 }
477
478 $saved_id = $this->save_snippet( $data, $post, $was_active );
479
480 if ( is_wp_error( $saved_id ) ) {
481 return $saved_id;
482 }
483
484 $saved = get_post( $saved_id );
485
486 if ( ! $saved instanceof WP_Post ) {
487 return new WP_Error( 'woody_save_failed', __( 'The snippet could not be saved.', 'insert-php' ) );
488 }
489
490 $revisions = wp_get_post_revisions(
491 $saved_id,
492 [
493 'posts_per_page' => 1,
494 'fields' => 'ids',
495 ]
496 );
497
498 $latest = reset( $revisions );
499 $revision_id = $latest instanceof WP_Post ? $latest->ID : ( is_int( $latest ) ? $latest : 0 );
500
501 return [
502 'dry_run' => false,
503 'operation' => $operation,
504 'valid' => true,
505 'validated' => $needs_validation,
506 'message' => '',
507 'revision_id' => $revision_id,
508 'snippet' => $this->format_snippet( $saved, true ),
509 ];
510 }
511
512 /**
513 * Execute callback: activate or deactivate a snippet.
514 *
515 * @param array<string, mixed>|null $input Ability input.
516 *
517 * @return array<string, mixed>|WP_Error
518 */
519 public function set_snippet_state( $input = null ) {
520 $input = is_array( $input ) ? $input : [];
521 $snippet_id = isset( $input['snippet_id'] ) ? (int) $input['snippet_id'] : 0;
522 $state = isset( $input['state'] ) ? sanitize_key( (string) $input['state'] ) : '';
523
524 if ( ! in_array( $state, [ 'active', 'inactive' ], true ) ) {
525 return new WP_Error( 'woody_invalid_state', __( 'State must be "active" or "inactive".', 'insert-php' ) );
526 }
527
528 $post = $this->get_snippet_post( $snippet_id );
529
530 if ( is_wp_error( $post ) ) {
531 return $post;
532 }
533
534 if ( post_type_exists( WINP_SNIPPETS_POST_TYPE ) && ! current_user_can( 'edit_post', $snippet_id ) ) {
535 return new WP_Error( 'woody_forbidden', __( 'You are not allowed to edit this snippet.', 'insert-php' ) );
536 }
537
538 $is_active = $this->is_active( $snippet_id );
539 $activate = 'active' === $state;
540
541 if ( $activate === $is_active ) {
542 return [
543 'changed' => false,
544 'snippet' => $this->format_snippet( $post, false ),
545 ];
546 }
547
548 if ( $activate ) {
549 if ( 'trash' === $post->post_status ) {
550 return new WP_Error( 'woody_snippet_trashed', __( 'The snippet is in the trash and cannot be activated.', 'insert-php' ) );
551 }
552
553 $error = $this->check_before_activation( $post );
554
555 if ( is_wp_error( $error ) ) {
556 return $error;
557 }
558 }
559
560 update_post_meta( $snippet_id, 'wbcr_inp_snippet_activate', $activate ? 1 : 0 );
561
562 return [
563 'changed' => true,
564 'snippet' => $this->format_snippet( $post, false ),
565 ];
566 }
567
568 /**
569 * Run the checks the activation switch runs before a snippet is activated.
570 *
571 * @param WP_Post $post Snippet post.
572 *
573 * @return true|WP_Error
574 */
575 private function check_before_activation( WP_Post $post ) {
576 $scope = (string) get_post_meta( $post->ID, 'wbcr_inp_snippet_scope', true );
577 $type = $this->get_type( $post->ID );
578
579 $skipped_types = [ WINP_SNIPPET_TYPE_TEXT, WINP_SNIPPET_TYPE_AD, WINP_SNIPPET_TYPE_CSS, WINP_SNIPPET_TYPE_JS ];
580
581 if ( ! in_array( $scope, [ 'evrywhere', 'auto' ], true ) || in_array( $type, $skipped_types, true ) ) {
582 return true;
583 }
584
585 if ( WINP_Helper::is_safe_mode() ) {
586 return new WP_Error( 'woody_safe_mode', __( 'Safe mode is enabled, snippet code cannot be executed. Disable safe mode first.', 'insert-php' ) );
587 }
588
589 if ( in_array( $type, [ WINP_SNIPPET_TYPE_PHP, WINP_SNIPPET_TYPE_UNIVERSAL ], true ) ) {
590 // Redeclaring a function inside eval() ends the request, so detect it before executing the snippet.
591 $code = $this->prepare_code( (string) WINP_Helper::get_snippet_code( $post ), $type );
592 $redeclaration = WINP_Code_Validator::find_function_redeclaration( $code, $type );
593
594 if ( null !== $redeclaration ) {
595 return new WP_Error(
596 'woody_invalid_code',
597 // translators: %1$d is the line number, %2$s is the fully qualified function name.
598 sprintf( __( 'Line %1$d: Cannot redeclare function %2$s(). Rename the function or guard its declaration with function_exists().', 'insert-php' ), $redeclaration['line'], $redeclaration['name'] )
599 );
600 }
601 }
602
603 $error = WINP_Plugin::app()->get_execute_object()->getSnippetError( $post->ID );
604
605 if ( $error ) {
606 $message = is_array( $error ) && isset( $error['message'] ) ? wp_strip_all_tags( (string) $error['message'] ) : '';
607
608 return new WP_Error(
609 'woody_invalid_code',
610 trim( __( 'The snippet was not activated because its code contains an error.', 'insert-php' ) . ' ' . $message )
611 );
612 }
613
614 return true;
615 }
616
617 /**
618 * Merge the input over the stored snippet and validate the result.
619 *
620 * @param array<string, mixed> $input Ability input.
621 * @param WP_Post|null $post Existing snippet, null when creating.
622 *
623 * @return array<string, mixed>|WP_Error
624 */
625 private function resolve_snippet_data( $input, $post ) {
626 $old_type = $post ? $this->get_type( $post->ID ) : '';
627 $old_scope = $post ? (string) get_post_meta( $post->ID, 'wbcr_inp_snippet_scope', true ) : '';
628 $old_scope = '' === $old_scope ? 'shortcode' : $old_scope;
629 $old_code = $post ? (string) WINP_Helper::get_snippet_code( $post ) : '';
630
631 if ( ! $post ) {
632 foreach ( [ 'title', 'code', 'type' ] as $required ) {
633 if ( ! isset( $input[ $required ] ) || '' === trim( (string) $input[ $required ] ) ) {
634 // translators: %s is the name of the missing input field.
635 return new WP_Error( 'woody_missing_field', sprintf( __( 'The "%s" field is required to create a snippet.', 'insert-php' ), $required ) );
636 }
637 }
638 }
639
640 $type = isset( $input['type'] ) ? sanitize_key( (string) $input['type'] ) : $old_type;
641
642 if ( $post && $type !== $old_type ) {
643 return new WP_Error( 'woody_type_immutable', __( 'The type of an existing snippet cannot be changed.', 'insert-php' ) );
644 }
645
646 $writable_types = [ WINP_SNIPPET_TYPE_PHP, WINP_SNIPPET_TYPE_UNIVERSAL, WINP_SNIPPET_TYPE_HTML, WINP_SNIPPET_TYPE_TEXT, WINP_SNIPPET_TYPE_AD ];
647 $file_types = [ WINP_SNIPPET_TYPE_CSS, WINP_SNIPPET_TYPE_JS ];
648
649 if ( in_array( $type, $file_types, true ) ) {
650 // CSS and JS snippets are served from a file that only the editor save handler generates.
651 if ( ! $post || isset( $input['code'] ) ) {
652 return new WP_Error( 'woody_type_not_supported', __( 'The code of CSS and JS snippets can only be saved from the snippet editor.', 'insert-php' ) );
653 }
654 } elseif ( ! in_array( $type, $writable_types, true ) ) {
655 return new WP_Error( 'woody_invalid_type', __( 'Unknown snippet type.', 'insert-php' ) );
656 }
657
658 $title = $post ? $post->post_title : '';
659
660 if ( isset( $input['title'] ) ) {
661 $title = sanitize_text_field( (string) $input['title'] );
662
663 if ( '' === $title ) {
664 return new WP_Error( 'woody_missing_field', __( 'The snippet title cannot be empty.', 'insert-php' ) );
665 }
666 }
667
668 $code = isset( $input['code'] ) ? (string) $input['code'] : $old_code;
669
670 // Scope: PHP snippets run everywhere or by shortcode, the other types are inserted automatically or by shortcode.
671 $scope = isset( $input['scope'] ) ? $this->scope_to_meta( sanitize_key( (string) $input['scope'] ) ) : $old_scope;
672
673 $allowed_scopes = WINP_SNIPPET_TYPE_PHP === $type ? [ 'evrywhere', 'shortcode' ] : [ 'auto', 'shortcode' ];
674
675 if ( ! in_array( $scope, $allowed_scopes, true ) ) {
676 return new WP_Error(
677 'woody_invalid_scope',
678 WINP_SNIPPET_TYPE_PHP === $type
679 ? __( 'PHP snippets support the "everywhere" and "shortcode" scopes.', 'insert-php' )
680 : __( 'This snippet type supports the "auto" and "shortcode" scopes.', 'insert-php' )
681 );
682 }
683
684 $data = [
685 'type' => $type,
686 'title' => $title,
687 'code' => $code,
688 'scope' => $scope,
689 'execution_changed' => ! $post || $code !== $old_code || $scope !== $old_scope,
690 ];
691
692 if ( isset( $input['description'] ) ) {
693 $data['description'] = sanitize_text_field( (string) $input['description'] );
694 }
695
696 $has_placement = isset( $input['location'] ) || isset( $input['location_number'] );
697
698 if ( WINP_SNIPPET_TYPE_PHP === $type ) {
699 if ( $has_placement || isset( $input['conditions'] ) ) {
700 return new WP_Error( 'woody_not_applicable', __( 'PHP snippets do not support an insertion location or display conditions.', 'insert-php' ) );
701 }
702
703 return $data;
704 }
705
706 if ( $has_placement || ! $post ) {
707 $placement = $this->resolve_placement( $input, $post );
708
709 if ( is_wp_error( $placement ) ) {
710 return $placement;
711 }
712
713 $data['location'] = $placement['location'];
714 $data['location_number'] = $placement['location_number'];
715 }
716
717 if ( isset( $input['conditions'] ) ) {
718 $conditions = $this->parse_conditions( (string) $input['conditions'] );
719
720 if ( is_wp_error( $conditions ) ) {
721 return $conditions;
722 }
723
724 $data['conditions'] = $conditions;
725 }
726
727 return $data;
728 }
729
730 /**
731 * Validate the automatic insertion location.
732 *
733 * @param array<string, mixed> $input Ability input.
734 * @param WP_Post|null $post Existing snippet, null when creating.
735 *
736 * @return array{location:string,location_number:int}|WP_Error
737 */
738 private function resolve_placement( $input, $post ) {
739 $location = $post ? (string) get_post_meta( $post->ID, 'wbcr_inp_snippet_location', true ) : '';
740 $number = $post ? (int) get_post_meta( $post->ID, 'wbcr_inp_snippet_p_number', true ) : 0;
741
742 if ( isset( $input['location'] ) ) {
743 $location = sanitize_key( (string) $input['location'] );
744 }
745
746 if ( isset( $input['location_number'] ) ) {
747 $number = max( 0, (int) $input['location_number'] );
748 }
749
750 // The editor default.
751 $location = '' === $location ? 'header' : $location;
752 $locations = ( new WINP_Insertion_Locations() )->getList();
753
754 if ( ! isset( $locations[ $location ] ) ) {
755 return new WP_Error(
756 'woody_invalid_location',
757 // translators: %s is a comma separated list of location slugs.
758 sprintf( __( 'Unknown insertion location. Available locations: %s.', 'insert-php' ), implode( ', ', array_keys( $locations ) ) )
759 );
760 }
761
762 // The editor disables the WooCommerce locations when WooCommerce is not active.
763 if ( 0 === strpos( $location, 'woo_' ) && ! class_exists( 'WooCommerce' ) ) {
764 return new WP_Error( 'woody_woocommerce_required', __( 'WooCommerce locations require WooCommerce to be active.', 'insert-php' ) );
765 }
766
767 $settings = isset( $locations[ $location ][2] ) && is_array( $locations[ $location ][2] ) ? $locations[ $location ][2] : [];
768 $requires_number = ! empty( $settings['requiresLocationNumber'] );
769
770 if ( $requires_number && $number < 1 ) {
771 return new WP_Error( 'woody_location_number_required', __( 'This insertion location requires a location_number of 1 or more.', 'insert-php' ) );
772 }
773
774 return [
775 'location' => $location,
776 'location_number' => $number,
777 ];
778 }
779
780 /**
781 * Get the upgrade link reported by the abilities that need the premium plugin.
782 *
783 * @param string $area Gated feature, used as the campaign.
784 *
785 * @return string
786 */
787 private function get_upgrade_url( $area ) {
788 return tsdk_utmify( WINP_UPGRADE, $area, 'mcp' );
789 }
790
791 /**
792 * Parse and validate the display conditions JSON.
793 *
794 * The stored structure is the one the conditions editor posts: a list of
795 * groups, each holding AND-ed scopes that hold OR-ed conditions.
796 *
797 * @param string $json Conditions JSON.
798 *
799 * @return array<int, stdClass>|WP_Error
800 */
801 private function parse_conditions( $json ) {
802 if ( strlen( $json ) > self::MAX_CONDITIONS_LENGTH ) {
803 return new WP_Error( 'woody_invalid_conditions', __( 'The conditions value is too long.', 'insert-php' ) );
804 }
805
806 $json = trim( $json );
807
808 if ( '' === $json ) {
809 return [];
810 }
811
812 $groups = json_decode( $json );
813
814 if ( ! is_array( $groups ) ) {
815 return new WP_Error( 'woody_invalid_conditions', __( 'Conditions must be a JSON array of condition groups.', 'insert-php' ) );
816 }
817
818 $free_params = [
819 'user-role',
820 'user-registered',
821 'user-cookie-name',
822 'location-page',
823 'location-referrer',
824 'location-post-type',
825 'location-taxonomy',
826 'page-taxonomy',
827 'location-some-page',
828 ];
829 $operators = [ 'equals', 'notequal', 'less', 'older', 'greater', 'younger', 'contains', 'notcontain', 'between' ];
830 $is_premium = WINP_Plugin::app()->is_premium();
831 $result = [];
832
833 foreach ( $groups as $group ) {
834 if ( ! is_object( $group ) || ! isset( $group->conditions ) || ! is_array( $group->conditions ) ) {
835 return new WP_Error( 'woody_invalid_conditions', __( 'Each condition group needs a "conditions" array.', 'insert-php' ) );
836 }
837
838 $group_type = isset( $group->type ) ? sanitize_key( (string) $group->type ) : 'showif';
839
840 if ( ! in_array( $group_type, [ 'showif', 'hideif' ], true ) ) {
841 return new WP_Error( 'woody_invalid_conditions', __( 'A condition group type must be "showif" or "hideif".', 'insert-php' ) );
842 }
843
844 $scopes = [];
845
846 foreach ( $group->conditions as $scope ) {
847 if ( ! is_object( $scope ) || ! isset( $scope->conditions ) || ! is_array( $scope->conditions ) ) {
848 return new WP_Error( 'woody_invalid_conditions', __( 'Each condition scope needs a "conditions" array.', 'insert-php' ) );
849 }
850
851 $conditions = [];
852
853 foreach ( $scope->conditions as $condition ) {
854 if ( ! is_object( $condition ) || ! isset( $condition->param, $condition->operator ) ) {
855 return new WP_Error( 'woody_invalid_conditions', __( 'Each condition needs a "param" and an "operator".', 'insert-php' ) );
856 }
857
858 $param = sanitize_key( (string) $condition->param );
859 $operator = sanitize_key( (string) $condition->operator );
860
861 if ( ! in_array( $operator, $operators, true ) ) {
862 // translators: %s is a comma separated list of operators.
863 return new WP_Error( 'woody_invalid_conditions', sprintf( __( 'Unknown condition operator. Available operators: %s.', 'insert-php' ), implode( ', ', $operators ) ) );
864 }
865
866 $is_premium_param = 0 === strpos( $param, 'technology-' ) || 0 === strpos( $param, 'auditory-' );
867
868 if ( $is_premium_param && ! $is_premium ) {
869 $upgrade_url = $this->get_upgrade_url( 'display-conditions' );
870
871 return new WP_Error(
872 'woody_premium_required',
873 // translators: %1$s is the condition parameter, %2$s is the upgrade URL.
874 sprintf( __( 'The "%1$s" condition requires Woody Code Snippets Premium. Upgrade: %2$s', 'insert-php' ), $param, $upgrade_url ),
875 [ 'upgrade_url' => $upgrade_url ]
876 );
877 }
878
879 if ( ! $is_premium_param && ! in_array( $param, $free_params, true ) ) {
880 // translators: %s is a comma separated list of condition parameters.
881 return new WP_Error( 'woody_invalid_conditions', sprintf( __( 'Unknown condition param. Available params: %s.', 'insert-php' ), implode( ', ', $free_params ) ) );
882 }
883
884 $clean = new stdClass();
885 $clean->param = $param;
886 $clean->operator = $operator;
887 $clean->type = isset( $condition->type ) ? sanitize_key( (string) $condition->type ) : 'text';
888 $clean->value = $this->sanitize_condition_value( isset( $condition->value ) ? $condition->value : '', 0 );
889
890 $conditions[] = $clean;
891 }
892
893 $clean_scope = new stdClass();
894 $clean_scope->type = 'scope';
895 $clean_scope->conditions = $conditions;
896
897 $scopes[] = $clean_scope;
898 }
899
900 $clean_group = new stdClass();
901 $clean_group->conditions = $scopes;
902 $clean_group->type = $group_type;
903
904 $result[] = $clean_group;
905 }
906
907 return $result;
908 }
909
910 /**
911 * Sanitize a condition value, keeping its scalar, list or object shape.
912 *
913 * @param mixed $value Raw value.
914 * @param int $depth Current nesting depth.
915 *
916 * @return mixed
917 */
918 private function sanitize_condition_value( $value, $depth ) {
919 if ( $depth > self::MAX_VALUE_DEPTH ) {
920 return '';
921 }
922
923 if ( is_object( $value ) ) {
924 $clean = new stdClass();
925
926 foreach ( get_object_vars( $value ) as $key => $item ) {
927 $key = preg_replace( '/[^A-Za-z0-9_\-]/', '', (string) $key );
928
929 if ( '' !== $key && null !== $key ) {
930 $clean->$key = $this->sanitize_condition_value( $item, $depth + 1 );
931 }
932 }
933
934 return $clean;
935 }
936
937 if ( is_array( $value ) ) {
938 $clean = [];
939
940 foreach ( $value as $item ) {
941 $clean[] = $this->sanitize_condition_value( $item, $depth + 1 );
942 }
943
944 return $clean;
945 }
946
947 if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
948 return $value;
949 }
950
951 return sanitize_text_field( (string) $value );
952 }
953
954 /**
955 * Persist the snippet.
956 *
957 * @param array<string, mixed> $data Validated snippet data.
958 * @param WP_Post|null $post Existing snippet, null when creating.
959 * @param bool $was_active Whether the snippet was active before the update.
960 *
961 * @return int|WP_Error Snippet ID.
962 */
963 private function save_snippet( $data, $post, $was_active ) {
964 $postarr = [
965 'post_title' => $data['title'],
966 'post_content' => $data['code'],
967 'post_type' => WINP_SNIPPETS_POST_TYPE,
968 ];
969
970 if ( $post ) {
971 $postarr['ID'] = $post->ID;
972 } else {
973 $postarr['post_status'] = 'publish';
974 }
975
976 // Like the editor, keep the content filters away from code; text and ad snippets stay filtered.
977 $is_code = ! in_array( $data['type'], [ WINP_SNIPPET_TYPE_TEXT, WINP_SNIPPET_TYPE_AD ], true );
978 $had_kses = $is_code && false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
979 $had_rel = $is_code && false !== has_filter( 'content_save_pre', 'wp_targeted_link_rel' );
980
981 if ( $had_kses ) {
982 kses_remove_filters();
983 }
984
985 if ( $had_rel ) {
986 remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
987 }
988
989 $saved_id = $post ? wp_update_post( wp_slash( $postarr ), true ) : wp_insert_post( wp_slash( $postarr ), true );
990
991 if ( $had_kses ) {
992 kses_init_filters();
993 }
994
995 if ( $had_rel ) {
996 add_filter( 'content_save_pre', 'wp_targeted_link_rel' );
997 }
998
999 if ( is_wp_error( $saved_id ) ) {
1000 return $saved_id;
1001 }
1002
1003 update_post_meta( $saved_id, 'wbcr_inp_snippet_type', $data['type'] );
1004 update_post_meta( $saved_id, 'wbcr_inp_snippet_scope', $data['scope'] );
1005
1006 if ( isset( $data['location'] ) ) {
1007 update_post_meta( $saved_id, 'wbcr_inp_snippet_location', $data['location'] );
1008 update_post_meta( $saved_id, 'wbcr_inp_snippet_p_number', (string) $data['location_number'] );
1009 }
1010
1011 if ( isset( $data['description'] ) ) {
1012 update_post_meta( $saved_id, 'wbcr_inp_snippet_description', $data['description'] );
1013 }
1014
1015 if ( isset( $data['conditions'] ) ) {
1016 update_post_meta( $saved_id, 'wbcr_inp_snippet_filters', empty( $data['conditions'] ) ? '' : $data['conditions'] );
1017 update_post_meta( $saved_id, 'wbcr_inp_changed_filters', 1 );
1018 }
1019
1020 if ( ! $post ) {
1021 update_post_meta( $saved_id, 'wbcr_inp_snippet_priority', WINP_Helper::get_next_snippet_priority() );
1022 }
1023
1024 // New snippets are always inactive. Like the editor, an update keeps an active
1025 // snippet active only when the "activate by default" setting is on.
1026 $keep_active = $post && $was_active && get_option( 'wbcr_inp_activate_by_default', true );
1027
1028 update_post_meta( $saved_id, 'wbcr_inp_snippet_activate', $keep_active ? 1 : 0 );
1029
1030 return $saved_id;
1031 }
1032
1033 /**
1034 * Remove the PHP tags the executor removes before evaluating a PHP snippet.
1035 *
1036 * Same replacements as WINP_Execute_Snippet::prepareCode(), which needs a
1037 * saved snippet to resolve the type.
1038 *
1039 * @param string $code Snippet code.
1040 * @param string $type Snippet type.
1041 *
1042 * @return string
1043 */
1044 private function prepare_code( $code, $type ) {
1045 if ( WINP_SNIPPET_TYPE_PHP !== $type ) {
1046 return $code;
1047 }
1048
1049 $code = (string) preg_replace( '|^[\s]*<\?(php)?|', '', $code );
1050
1051 return (string) preg_replace( '|\?>[\s]*$|', '', $code );
1052 }
1053
1054 /**
1055 * Get a snippet post by ID.
1056 *
1057 * @param int $snippet_id Snippet ID.
1058 *
1059 * @return WP_Post|WP_Error
1060 */
1061 private function get_snippet_post( $snippet_id ) {
1062 $post = $snippet_id > 0 ? get_post( $snippet_id ) : null;
1063
1064 if ( ! $post instanceof WP_Post || WINP_SNIPPETS_POST_TYPE !== $post->post_type || 'auto-draft' === $post->post_status ) {
1065 return new WP_Error( 'woody_snippet_not_found', __( 'Snippet not found.', 'insert-php' ) );
1066 }
1067
1068 return $post;
1069 }
1070
1071 /**
1072 * Get the stored snippet type.
1073 *
1074 * @param int $snippet_id Snippet ID.
1075 *
1076 * @return string
1077 */
1078 private function get_type( $snippet_id ) {
1079 $type = get_post_meta( $snippet_id, 'wbcr_inp_snippet_type', true );
1080
1081 return is_string( $type ) && '' !== $type ? $type : WINP_SNIPPET_TYPE_PHP;
1082 }
1083
1084 /**
1085 * Whether the snippet is activated.
1086 *
1087 * @param int $snippet_id Snippet ID.
1088 *
1089 * @return bool
1090 */
1091 private function is_active( $snippet_id ) {
1092 return (bool) (int) get_post_meta( $snippet_id, 'wbcr_inp_snippet_activate', true );
1093 }
1094
1095 /**
1096 * Convert the public scope name to the stored value.
1097 *
1098 * @param string $scope Public scope name.
1099 *
1100 * @return string
1101 */
1102 private function scope_to_meta( $scope ) {
1103 return 'everywhere' === $scope ? 'evrywhere' : $scope;
1104 }
1105
1106 /**
1107 * Build the public representation of a snippet.
1108 *
1109 * @param WP_Post $post Snippet post.
1110 * @param bool $with_code Whether to include the code and display conditions.
1111 *
1112 * @return array<string, mixed>
1113 */
1114 private function format_snippet( WP_Post $post, $with_code ) {
1115 require_once WINP_PLUGIN_DIR . '/includes/class.snippet.php';
1116
1117 $type = $this->get_type( $post->ID );
1118 $scope = (string) get_post_meta( $post->ID, 'wbcr_inp_snippet_scope', true );
1119 $scope = '' === $scope ? 'shortcode' : $scope;
1120
1121 $snippet = [
1122 'id' => $post->ID,
1123 'title' => $post->post_title,
1124 'type' => $type,
1125 'status' => $post->post_status,
1126 'active' => $this->is_active( $post->ID ),
1127 'scope' => 'evrywhere' === $scope ? 'everywhere' : $scope,
1128 'location' => (string) get_post_meta( $post->ID, 'wbcr_inp_snippet_location', true ),
1129 'location_number' => (int) get_post_meta( $post->ID, 'wbcr_inp_snippet_p_number', true ),
1130 'priority' => (int) get_post_meta( $post->ID, 'wbcr_inp_snippet_priority', true ),
1131 'description' => (string) get_post_meta( $post->ID, 'wbcr_inp_snippet_description', true ),
1132 'shortcode' => wp_specialchars_decode( WINP_Helper::get_shortcode_text( $post ), ENT_QUOTES ),
1133 // False when DISALLOW_UNFILTERED_HTML stops the plugin from running this snippet type.
1134 'execution_allowed' => ( new WINP_Snippet( $post ) )->is_allowed(),
1135 'modified_gmt' => $post->post_modified_gmt,
1136 ];
1137
1138 if ( $with_code ) {
1139 $filters = get_post_meta( $post->ID, 'wbcr_inp_snippet_filters', true );
1140 $encoded = empty( $filters ) ? false : wp_json_encode( $filters );
1141 $decoded = false === $encoded ? [] : json_decode( $encoded, true );
1142
1143 $snippet['code'] = (string) WINP_Helper::get_snippet_code( $post );
1144 $snippet['conditions'] = is_array( $decoded ) ? array_values( $decoded ) : [];
1145 }
1146
1147 return $snippet;
1148 }
1149
1150 /**
1151 * Input schema of woody/list-snippets.
1152 *
1153 * @return array<string, mixed>
1154 */
1155 private function get_list_input_schema() {
1156 return [
1157 'type' => 'object',
1158 'properties' => [
1159 'type' => [
1160 'type' => 'string',
1161 'description' => __( 'Only snippets of this type.', 'insert-php' ),
1162 'enum' => [ WINP_SNIPPET_TYPE_PHP, WINP_SNIPPET_TYPE_UNIVERSAL, WINP_SNIPPET_TYPE_HTML, WINP_SNIPPET_TYPE_CSS, WINP_SNIPPET_TYPE_JS, WINP_SNIPPET_TYPE_TEXT, WINP_SNIPPET_TYPE_AD ],
1163 ],
1164 'scope' => [
1165 'type' => 'string',
1166 'description' => __( 'Only snippets with this insertion scope.', 'insert-php' ),
1167 'enum' => [ 'everywhere', 'auto', 'shortcode' ],
1168 ],
1169 'active' => [
1170 'type' => 'boolean',
1171 'description' => __( 'Only active (true) or inactive (false) snippets.', 'insert-php' ),
1172 ],
1173 'status' => [
1174 'type' => 'string',
1175 'description' => __( 'Post status filter. "any" excludes the trash.', 'insert-php' ),
1176 'enum' => [ 'any', 'publish', 'draft', 'trash' ],
1177 'default' => 'any',
1178 ],
1179 'search' => [
1180 'type' => 'string',
1181 'description' => __( 'Search term matched against the snippet title and code.', 'insert-php' ),
1182 ],
1183 'page' => [
1184 'type' => 'integer',
1185 'minimum' => 1,
1186 'default' => 1,
1187 ],
1188 'per_page' => [
1189 'type' => 'integer',
1190 'minimum' => 1,
1191 'maximum' => self::MAX_PER_PAGE,
1192 'default' => 20,
1193 ],
1194 ],
1195 'additionalProperties' => false,
1196 ];
1197 }
1198
1199 /**
1200 * Input schema of woody/upsert-snippet.
1201 *
1202 * @return array<string, mixed>
1203 */
1204 private function get_upsert_input_schema() {
1205 return [
1206 'type' => 'object',
1207 'properties' => [
1208 'snippet_id' => [
1209 'type' => 'integer',
1210 'description' => __( 'ID of the snippet to update. Omit to create a new, inactive snippet.', 'insert-php' ),
1211 'minimum' => 1,
1212 ],
1213 'title' => [
1214 'type' => 'string',
1215 'description' => __( 'Snippet title. Required when creating.', 'insert-php' ),
1216 ],
1217 'code' => [
1218 'type' => 'string',
1219 'description' => __( 'Snippet code or content. Required when creating.', 'insert-php' ),
1220 ],
1221 'type' => [
1222 'type' => 'string',
1223 'description' => __( 'Snippet type. Required when creating; it cannot be changed afterwards.', 'insert-php' ),
1224 'enum' => [ WINP_SNIPPET_TYPE_PHP, WINP_SNIPPET_TYPE_UNIVERSAL, WINP_SNIPPET_TYPE_HTML, WINP_SNIPPET_TYPE_TEXT, WINP_SNIPPET_TYPE_AD ],
1225 ],
1226 'scope' => [
1227 'type' => 'string',
1228 'description' => __( 'Where the snippet runs. PHP snippets: "everywhere" or "shortcode". Other types: "auto" (automatic insertion at "location") or "shortcode". Defaults to "shortcode".', 'insert-php' ),
1229 'enum' => [ 'everywhere', 'auto', 'shortcode' ],
1230 ],
1231 'location' => [
1232 'type' => 'string',
1233 'description' => __( 'Automatic insertion location for non-PHP snippets, for example header, footer, before_content, after_content, before_paragraph, after_paragraph, before_post, after_post. Defaults to "header".', 'insert-php' ),
1234 ],
1235 'location_number' => [
1236 'type' => 'integer',
1237 'description' => __( 'Paragraph or post number for locations that need one, such as before_paragraph.', 'insert-php' ),
1238 'minimum' => 0,
1239 ],
1240 'description' => [
1241 'type' => 'string',
1242 'description' => __( 'Snippet description.', 'insert-php' ),
1243 ],
1244 'conditions' => [
1245 'type' => 'string',
1246 'description' => __( 'Display conditions for non-PHP snippets as a JSON string, in the format the conditions editor stores: [{"type":"showif","conditions":[{"type":"scope","conditions":[{"param":"location-some-page","operator":"equals","type":"select","value":"base_web"}]}]}]. Groups hold AND-ed scopes, scopes hold OR-ed conditions. Pass "[]" to clear. Params starting with technology- or auditory- require the premium plugin.', 'insert-php' ),
1247 ],
1248 'dry_run' => [
1249 'type' => 'boolean',
1250 'description' => __( 'Validate only; nothing is saved. Validation of PHP and universal snippets evaluates the code once.', 'insert-php' ),
1251 'default' => false,
1252 ],
1253 ],
1254 'additionalProperties' => false,
1255 ];
1256 }
1257
1258 /**
1259 * Output schema of a snippet.
1260 *
1261 * @param bool $with_code Whether the code and display conditions are included.
1262 *
1263 * @return array<string, mixed>
1264 */
1265 private function get_snippet_schema( $with_code ) {
1266 $properties = [
1267 'id' => [ 'type' => 'integer' ],
1268 'title' => [ 'type' => 'string' ],
1269 'type' => [ 'type' => 'string' ],
1270 'status' => [ 'type' => 'string' ],
1271 'active' => [ 'type' => 'boolean' ],
1272 'scope' => [ 'type' => 'string' ],
1273 'location' => [ 'type' => 'string' ],
1274 'location_number' => [ 'type' => 'integer' ],
1275 'priority' => [ 'type' => 'integer' ],
1276 'description' => [ 'type' => 'string' ],
1277 'shortcode' => [ 'type' => 'string' ],
1278 'execution_allowed' => [ 'type' => 'boolean' ],
1279 'modified_gmt' => [ 'type' => 'string' ],
1280 ];
1281
1282 if ( $with_code ) {
1283 $properties['code'] = [ 'type' => 'string' ];
1284 $properties['conditions'] = [
1285 'type' => 'array',
1286 'items' => [ 'type' => 'object' ],
1287 ];
1288 }
1289
1290 return [
1291 'type' => 'object',
1292 'properties' => $properties,
1293 ];
1294 }
1295 }
1296