PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.7
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.7
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 2.6.1 2.7.0 All 28 releases
insert-php / includes / class.rest.php

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

698 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST Class
4 *
5 * @package Woody_Code_Snippets
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * WINP_Rest Class
15 */
16 class WINP_Rest {
17
18 /**
19 * WINP_Rest constructor.
20 */
21 public function __construct() {
22 add_action( 'rest_api_init', [ $this, 'register_routes' ] );
23 }
24
25 /**
26 * Register the license REST route.
27 *
28 * @return void
29 */
30 public function register_routes() {
31 $namespace = 'woody/v1';
32
33 register_rest_route(
34 $namespace,
35 '/license',
36 [
37 [
38 'methods' => \WP_REST_Server::CREATABLE,
39 'args' => [
40 'key' => [
41 'type' => 'string',
42 'sanitize_callback' => function ( $param ) {
43 return (string) esc_attr( $param );
44 },
45 'validate_callback' => function ( $param ) {
46 return is_string( $param );
47 },
48 ],
49 'action' => [
50 'type' => 'string',
51 'sanitize_callback' => function ( $param ) {
52 return (string) esc_attr( $param );
53 },
54 'validate_callback' => function ( $param ) {
55 return in_array( $param, [ 'activate', 'deactivate' ], true );
56 },
57 ],
58 ],
59 'permission_callback' => function () {
60 return current_user_can( 'manage_options' );
61 },
62 'callback' => [ $this, 'license' ],
63 ],
64 ]
65 );
66
67 register_rest_route(
68 $namespace,
69 '/settings',
70 [
71 [
72 'methods' => \WP_REST_Server::CREATABLE,
73 'args' => [
74 'data' => [
75 'type' => 'object',
76 'required' => true,
77 'sanitize_callback' => [ $this, 'sanitize_settings' ],
78 'validate_callback' => function ( $param ) {
79 if ( ! is_array( $param ) ) {
80 return false;
81 }
82
83 $schema = $this->get_settings_schema();
84
85 foreach ( array_keys( $param ) as $key ) {
86 if ( ! isset( $schema[ $key ] ) ) {
87 return false;
88 }
89 }
90 return true;
91 },
92 ],
93 ],
94 'permission_callback' => function () {
95 return current_user_can( 'manage_options' );
96 },
97 'callback' => [ $this, 'save_settings' ],
98 ],
99 ]
100 );
101
102 register_rest_route(
103 $namespace,
104 '/import',
105 [
106 [
107 'methods' => \WP_REST_Server::CREATABLE,
108 'permission_callback' => function () {
109 return current_user_can( 'manage_options' );
110 },
111 'callback' => [ $this, 'import_snippets' ],
112 ],
113 ]
114 );
115
116 register_rest_route(
117 $namespace,
118 '/export',
119 [
120 [
121 'methods' => \WP_REST_Server::CREATABLE,
122 'args' => [
123 'status' => [
124 'type' => 'string',
125 'default' => 'all',
126 'sanitize_callback' => 'sanitize_text_field',
127 ],
128 'types' => [
129 'type' => 'array',
130 'default' => [],
131 'items' => [
132 'type' => 'string',
133 ],
134 ],
135 'tags' => [
136 'type' => 'array',
137 'default' => [],
138 'items' => [
139 'type' => 'string',
140 ],
141 ],
142 ],
143 'permission_callback' => function () {
144 return current_user_can( 'manage_options' );
145 },
146 'callback' => [ $this, 'export_snippets' ],
147 ],
148 ]
149 );
150
151 register_rest_route(
152 $namespace,
153 '/sync',
154 [
155 [
156 'methods' => \WP_REST_Server::CREATABLE,
157 'args' => [
158 'title' => [
159 'type' => 'string',
160 'required' => true,
161 'sanitize_callback' => 'sanitize_text_field',
162 'validate_callback' => function ( $param ) {
163 return is_string( $param ) && ! empty( trim( $param ) );
164 },
165 ],
166 'id' => [
167 'type' => 'integer',
168 'required' => true,
169 'sanitize_callback' => 'absint',
170 'validate_callback' => function ( $param ) {
171 return is_numeric( $param ) && $param > 0;
172 },
173 ],
174 ],
175 'permission_callback' => function () {
176 return current_user_can( 'manage_options' );
177 },
178 'callback' => [ $this, 'sync_snippet' ],
179 ],
180 ]
181 );
182 }
183
184 /**
185 * Get settings schema (name => type mapping)
186 *
187 * @return array<string, string>
188 */
189 private function get_settings_schema() {
190 $settings = WINP_Settings::get_settings();
191 $schema = [];
192
193 foreach ( $settings as $setting ) {
194 if ( isset( $setting['name'] ) && isset( $setting['type'] ) ) {
195 $schema[ $setting['name'] ] = $setting['type'];
196 }
197 }
198
199 return $schema;
200 }
201
202 /**
203 * Sanitize settings data based on schema
204 *
205 * @param mixed $data Raw settings data.
206 *
207 * @return array<string, mixed> Sanitized settings data.
208 */
209 public function sanitize_settings( $data ) {
210 if ( ! is_array( $data ) ) {
211 return [];
212 }
213
214 $schema = $this->get_settings_schema();
215 $sanitized = [];
216
217 foreach ( $data as $key => $value ) {
218 if ( ! isset( $schema[ $key ] ) ) {
219 continue;
220 }
221
222 switch ( $schema[ $key ] ) {
223 case 'checkbox':
224 if ( is_bool( $value ) ) {
225 $sanitized[ $key ] = $value;
226 } elseif ( is_numeric( $value ) ) {
227 $sanitized[ $key ] = (bool) (int) $value;
228 } elseif ( is_string( $value ) ) {
229 $sanitized[ $key ] = in_array( strtolower( $value ), [ 'true', '1', 'yes', 'on' ], true );
230 } else {
231 $sanitized[ $key ] = (bool) $value;
232 }
233 break;
234
235 case 'integer':
236 $sanitized[ $key ] = absint( $value );
237 break;
238
239 case 'email':
240 $sanitized_email = sanitize_email( $value );
241 if ( is_email( $sanitized_email ) ) {
242 $sanitized[ $key ] = $sanitized_email;
243 }
244 break;
245
246 case 'dropdown':
247 case 'text':
248 case 'textbox':
249 default:
250 $sanitized[ $key ] = sanitize_text_field( $value );
251 break;
252 }
253 }
254
255 return $sanitized;
256 }
257
258 /**
259 * Handle license activation/deactivation.
260 *
261 * @param \WP_REST_Request<array<string, mixed>> $request Rest request.
262 *
263 * @return \WP_REST_Response
264 */
265 public function license( \WP_REST_Request $request ) {
266 $data = $request->get_param( 'data' );
267
268 if ( ! isset( $data['key'] ) || ! isset( $data['action'] ) ) {
269 return new \WP_REST_Response(
270 [
271 'message' => __( 'This action is no longer valid. Please refresh the page and try again.', 'insert-php' ),
272 'success' => false,
273 ]
274 );
275 }
276
277 $response = WINP_Plugin::app()->premium->toggle_license( $data['action'], $data['key'] );
278
279 if ( is_wp_error( $response ) ) {
280 return new \WP_REST_Response(
281 [
282 'message' => $response->get_error_message(),
283 'success' => false,
284 ]
285 );
286 }
287
288 return new \WP_REST_Response( $response );
289 }
290
291 /**
292 * Handle settings save.
293 *
294 * @param \WP_REST_Request<array<string, mixed>> $request Rest request.
295 *
296 * @return \WP_REST_Response
297 */
298 public function save_settings( \WP_REST_Request $request ) {
299 $data = $request->get_param( 'data' );
300
301 if ( empty( $data ) ) {
302 return new \WP_REST_Response(
303 [
304 'message' => __( 'No changes detected. Modify at least one setting before saving.', 'insert-php' ),
305 'success' => false,
306 ]
307 );
308 }
309
310 foreach ( $data as $key => $value ) {
311 if ( false === $value ) {
312 // update_option() short-circuits when storing `false` over a
313 // missing option (both compare equal), so a default-enabled
314 // checkbox could never be persisted as disabled.
315 $value = '';
316 }
317
318 update_option( 'wbcr_inp_' . $key, $value );
319 }
320
321 return new \WP_REST_Response(
322 [
323 'success' => true,
324 'message' => __( 'Settings saved successfully.', 'insert-php' ),
325 ]
326 );
327 }
328
329 /**
330 * Handle snippet import.
331 *
332 * @param \WP_REST_Request<array<string, mixed>> $request Rest request.
333 *
334 * @return \WP_REST_Response
335 */
336 public function import_snippets( \WP_REST_Request $request ) {
337 $files = $request->get_file_params();
338 $duplicate_action = $request->get_param( 'duplicate_action' );
339
340 // Validate duplicate action.
341 if ( ! in_array( $duplicate_action, [ 'ignore', 'replace', 'skip' ], true ) ) {
342 return new \WP_REST_Response(
343 [
344 // translators: %s is the invalid duplicate action.
345 'message' => sprintf( __( 'Invalid duplicate action: "%s". Expected: ignore, replace, or skip.', 'insert-php' ), $duplicate_action ),
346 'success' => false,
347 ],
348 400
349 );
350 }
351
352 // Check if files were uploaded.
353 if ( empty( $files ) ) {
354 return new \WP_REST_Response(
355 [
356 'message' => __( 'No files were uploaded. Please select a file and try again.', 'insert-php' ),
357 'success' => false,
358 ],
359 400
360 );
361 }
362
363 $max_file_size = 2 * 1024 * 1024; // 2MB in bytes.
364 $errors = [];
365
366 // Normalize file array structure (WordPress may structure it differently).
367 $normalized_files = [];
368 if ( isset( $files['files'] ) ) {
369 // files[] format - need to normalize.
370 $file_count = count( $files['files']['name'] );
371 for ( $i = 0; $i < $file_count; $i++ ) {
372 $normalized_files[] = [
373 'name' => $files['files']['name'][ $i ],
374 'type' => $files['files']['type'][ $i ],
375 'tmp_name' => $files['files']['tmp_name'][ $i ],
376 'error' => $files['files']['error'][ $i ],
377 'size' => $files['files']['size'][ $i ],
378 ];
379 }
380 } else {
381 $normalized_files = $files;
382 }
383
384 $validated_files = [];
385 foreach ( $normalized_files as $file ) {
386 if ( ! isset( $file['error'] ) || is_array( $file['error'] ) ) {
387 $errors[] = __( 'The file could not be uploaded. Please ensure it\'s a valid .json or .zip file.', 'insert-php' );
388 continue;
389 }
390
391 if ( UPLOAD_ERR_OK !== $file['error'] ) {
392 // translators: %s is the file name.
393 $errors[] = sprintf( __( 'Upload error for file: %s', 'insert-php' ), $file['name'] );
394 continue;
395 }
396
397 if ( $file['size'] > $max_file_size ) {
398 // translators: %s is the file name.
399 $errors[] = sprintf( __( 'File too large: %s (maximum 2MB)', 'insert-php' ), $file['name'] );
400 continue;
401 }
402
403 $file_extension = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
404 if ( ! in_array( $file_extension, [ 'json', 'zip' ], true ) ) {
405 // translators: %s is the file name.
406 $errors[] = sprintf( __( 'Invalid file type: %s (only .json and .zip allowed)', 'insert-php' ), $file['name'] );
407 continue;
408 }
409
410 // Additional MIME type validation.
411 $finfo = finfo_open( FILEINFO_MIME_TYPE );
412
413 if ( false === $finfo ) {
414 // translators: %s is the file name.
415 $errors[] = sprintf( __( 'Could not verify the file type for "%s". Please use a .json or .zip file.', 'insert-php' ), $file['name'] );
416 continue;
417 }
418
419 $mime_type = finfo_file( $finfo, $file['tmp_name'] );
420 finfo_close( $finfo );
421
422 $allowed_mime_types = [
423 'application/json',
424 'text/plain',
425 'application/zip',
426 'application/x-zip-compressed',
427 ];
428
429 if ( ! in_array( $mime_type, $allowed_mime_types, true ) ) {
430 // translators: %s is the file name.
431 $errors[] = sprintf( __( 'Invalid file MIME type: %s', 'insert-php' ), $file['name'] );
432 continue;
433 }
434
435 $validated_files[] = $file;
436 }
437
438 // If no valid files, return error.
439 if ( empty( $validated_files ) ) {
440 return new \WP_REST_Response(
441 [
442 'message' => __( 'No valid files to import.', 'insert-php' ),
443 'success' => false,
444 'errors' => $errors,
445 ],
446 400
447 );
448 }
449
450 // Process import using the import snippet class.
451 if ( ! class_exists( 'WINP_Import_Snippet' ) ) {
452 require_once WINP_PLUGIN_DIR . '/admin/includes/class.import.snippet.php';
453 }
454
455 $import_handler = new WINP_Import_Snippet();
456 $result = $import_handler->process_import_files( $validated_files, $duplicate_action );
457
458 // Merge validation errors with import errors.
459 $all_errors = array_merge( $errors, $result['errors'] );
460
461 if ( $result['count'] > 0 ) {
462 // translators: %d is the number of imported snippets.
463 $message = sprintf(
464 // translators: %d is the number of imported snippets.
465 _n(
466 'Successfully imported %d snippet.',
467 'Successfully imported %d snippets.',
468 $result['count'],
469 'insert-php'
470 ),
471 $result['count']
472 );
473
474 if ( ! empty( $all_errors ) ) {
475 $message .= ' ' . __( 'Some files had errors.', 'insert-php' );
476 }
477
478 return new \WP_REST_Response(
479 [
480 'success' => true,
481 'message' => $message,
482 'errors' => $all_errors,
483 'count' => $result['count'],
484 ]
485 );
486 }
487
488 return new \WP_REST_Response(
489 [
490 'message' => __( 'No snippets were imported. Please check your file contains valid snippet data.', 'insert-php' ),
491 'success' => false,
492 'errors' => $all_errors,
493 ]
494 );
495 }
496
497 /**
498 * Handle snippet export.
499 *
500 * @param \WP_REST_Request<array<string, mixed>> $request Rest request.
501 *
502 * @return \WP_REST_Response
503 */
504 public function export_snippets( \WP_REST_Request $request ) {
505 $status = $request->get_param( 'status' );
506 $types = $request->get_param( 'types' );
507 $tags = $request->get_param( 'tags' );
508
509 $status = sanitize_text_field( $status );
510 $types = array_map( 'sanitize_text_field', (array) $types );
511 $tags = array_map( 'sanitize_text_field', (array) $tags );
512
513 // Build query conditions.
514 $meta_query_conditions = [];
515 $tax_query_conditions = [];
516
517 // Status filter.
518 if ( 'all' !== $status ) {
519 if ( 'active' === $status ) {
520 // Active: wbcr_inp_snippet_activate = 1.
521 $meta_query_conditions[] = [
522 'key' => 'wbcr_inp_snippet_activate',
523 'value' => 1,
524 ];
525 } else {
526 // Inactive: wbcr_inp_snippet_activate != 1 OR doesn't exist.
527 $meta_query_conditions[] = [
528 'relation' => 'OR',
529 [
530 'key' => 'wbcr_inp_snippet_activate',
531 'value' => 1,
532 'compare' => '!=',
533 ],
534 [
535 'key' => 'wbcr_inp_snippet_activate',
536 'compare' => 'NOT EXISTS',
537 ],
538 ];
539 }
540 }
541
542 // Types filter.
543 if ( ! empty( $types ) ) {
544 if ( count( $types ) > 1 ) {
545 $type_condition = [ 'relation' => 'OR' ];
546 foreach ( $types as $type ) {
547 $type_condition[] = [
548 'key' => 'wbcr_inp_snippet_type',
549 'value' => $type,
550 ];
551 }
552 } else {
553 $type_condition = [
554 'key' => 'wbcr_inp_snippet_type',
555 'value' => $types[0],
556 ];
557 }
558
559 $meta_query_conditions[] = $type_condition;
560 }
561
562 // Tags filter.
563 if ( ! empty( $tags ) ) {
564 // Ensure taxonomy exists before querying.
565 if ( ! taxonomy_exists( WINP_SNIPPETS_TAXONOMY ) ) {
566 register_taxonomy( WINP_SNIPPETS_TAXONOMY, WINP_SNIPPETS_POST_TYPE, [] );
567 }
568
569 $tax_query_conditions = [
570 [
571 'taxonomy' => WINP_SNIPPETS_TAXONOMY,
572 'field' => 'slug',
573 'terms' => $tags,
574 'operator' => 'IN',
575 ],
576 ];
577 }
578
579 if ( count( $meta_query_conditions ) > 1 ) {
580 $meta_query_conditions['relation'] = 'AND';
581 }
582
583 // Build final query.
584 $conditions = [
585 'post_type' => WINP_SNIPPETS_POST_TYPE,
586 'post_status' => 'publish',
587 'numberposts' => -1,
588 ];
589
590 if ( ! empty( $meta_query_conditions ) ) {
591 $conditions['meta_query'] = $meta_query_conditions;
592 }
593
594 if ( ! empty( $tax_query_conditions ) ) {
595 $conditions['tax_query'] = $tax_query_conditions;
596 }
597
598 // Query snippets.
599 $snippets = get_posts( $conditions );
600
601 if ( empty( $snippets ) ) {
602 return new \WP_REST_Response(
603 [
604 'message' => __( 'No snippets found. Try adjusting your filters or search terms.', 'insert-php' ),
605 'success' => false,
606 ],
607 404
608 );
609 }
610
611 $ids = wp_list_pluck( $snippets, 'ID' );
612
613 require_once WINP_PLUGIN_DIR . '/admin/includes/class.actions.snippet.php';
614 $exporter = new WINP_Actions_Snippet();
615
616 $result = $exporter->export_snippets( $ids, true );
617
618 if ( $result['is_zip'] ) {
619 // For ZIP files, encode as base64 for JSON transport.
620 $data = base64_encode( $result['data'] );
621 } else {
622 // For JSON files, encode as pretty-printed JSON.
623 $data = wp_json_encode( $result['data'], JSON_PRETTY_PRINT );
624 }
625
626 return new \WP_REST_Response(
627 [
628 'success' => true,
629 'filename' => $result['filename'],
630 'data' => $data,
631 'count' => $result['count'],
632 'is_zip' => $result['is_zip'],
633 ]
634 );
635 }
636
637 /**
638 * Handle snippet sync to cloud.
639 *
640 * @param \WP_REST_Request $request Rest request.
641 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
642 *
643 * @return \WP_REST_Response
644 */
645 public function sync_snippet( \WP_REST_Request $request ) {
646 $title = $request->get_param( 'title' );
647 $snippet_id = absint( $request->get_param( 'id' ) );
648
649 // Verify the snippet exists.
650 $snippet = get_post( $snippet_id );
651 if ( ! $snippet || WINP_SNIPPETS_POST_TYPE !== $snippet->post_type ) {
652 return new \WP_REST_Response(
653 [
654 'message' => __( 'Snippet not found. It may have been deleted or moved.', 'insert-php' ),
655 'success' => false,
656 ],
657 404
658 );
659 }
660
661 // Verify the current user has permission to edit this specific snippet.
662 if ( ! current_user_can( 'edit_post', $snippet_id ) ) {
663 return new \WP_REST_Response(
664 [
665 'message' => __( 'You do not have permission to sync this snippet.', 'insert-php' ),
666 'success' => false,
667 ],
668 403
669 );
670 }
671
672 // Sync snippet using the API object.
673 $result = WINP_Plugin::app()->get_api_object()->synchronization( $snippet_id, $title );
674
675 // synchronization() returns true on success, error string on failure, or false if post doesn't exist.
676 if ( true === $result ) {
677 return new \WP_REST_Response(
678 [
679 'success' => true,
680 'message' => __( 'Snippet saved as template successfully.', 'insert-php' ),
681 ],
682 200
683 );
684 }
685
686 // If result is a string, it's an error message. If false, it's a generic error.
687 $error_message = is_string( $result ) ? $result : __( 'Failed to sync snippet. Please check your connection and try again.', 'insert-php' );
688
689 return new \WP_REST_Response(
690 [
691 'success' => false,
692 'message' => $error_message,
693 ],
694 500
695 );
696 }
697 }
698