PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.5
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.5
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 2.7.1 All 27 releases
insert-php / includes / class.rest.php

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

691 lines 17.3 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 update_option( 'wbcr_inp_' . $key, $value );
312 }
313
314 return new \WP_REST_Response(
315 [
316 'success' => true,
317 'message' => __( 'Settings saved successfully.', 'insert-php' ),
318 ]
319 );
320 }
321
322 /**
323 * Handle snippet import.
324 *
325 * @param \WP_REST_Request<array<string, mixed>> $request Rest request.
326 *
327 * @return \WP_REST_Response
328 */
329 public function import_snippets( \WP_REST_Request $request ) {
330 $files = $request->get_file_params();
331 $duplicate_action = $request->get_param( 'duplicate_action' );
332
333 // Validate duplicate action.
334 if ( ! in_array( $duplicate_action, [ 'ignore', 'replace', 'skip' ], true ) ) {
335 return new \WP_REST_Response(
336 [
337 // translators: %s is the invalid duplicate action.
338 'message' => sprintf( __( 'Invalid duplicate action: "%s". Expected: ignore, replace, or skip.', 'insert-php' ), $duplicate_action ),
339 'success' => false,
340 ],
341 400
342 );
343 }
344
345 // Check if files were uploaded.
346 if ( empty( $files ) ) {
347 return new \WP_REST_Response(
348 [
349 'message' => __( 'No files were uploaded. Please select a file and try again.', 'insert-php' ),
350 'success' => false,
351 ],
352 400
353 );
354 }
355
356 $max_file_size = 2 * 1024 * 1024; // 2MB in bytes.
357 $errors = [];
358
359 // Normalize file array structure (WordPress may structure it differently).
360 $normalized_files = [];
361 if ( isset( $files['files'] ) ) {
362 // files[] format - need to normalize.
363 $file_count = count( $files['files']['name'] );
364 for ( $i = 0; $i < $file_count; $i++ ) {
365 $normalized_files[] = [
366 'name' => $files['files']['name'][ $i ],
367 'type' => $files['files']['type'][ $i ],
368 'tmp_name' => $files['files']['tmp_name'][ $i ],
369 'error' => $files['files']['error'][ $i ],
370 'size' => $files['files']['size'][ $i ],
371 ];
372 }
373 } else {
374 $normalized_files = $files;
375 }
376
377 $validated_files = [];
378 foreach ( $normalized_files as $file ) {
379 if ( ! isset( $file['error'] ) || is_array( $file['error'] ) ) {
380 $errors[] = __( 'The file could not be uploaded. Please ensure it\'s a valid .json or .zip file.', 'insert-php' );
381 continue;
382 }
383
384 if ( UPLOAD_ERR_OK !== $file['error'] ) {
385 // translators: %s is the file name.
386 $errors[] = sprintf( __( 'Upload error for file: %s', 'insert-php' ), $file['name'] );
387 continue;
388 }
389
390 if ( $file['size'] > $max_file_size ) {
391 // translators: %s is the file name.
392 $errors[] = sprintf( __( 'File too large: %s (maximum 2MB)', 'insert-php' ), $file['name'] );
393 continue;
394 }
395
396 $file_extension = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
397 if ( ! in_array( $file_extension, [ 'json', 'zip' ], true ) ) {
398 // translators: %s is the file name.
399 $errors[] = sprintf( __( 'Invalid file type: %s (only .json and .zip allowed)', 'insert-php' ), $file['name'] );
400 continue;
401 }
402
403 // Additional MIME type validation.
404 $finfo = finfo_open( FILEINFO_MIME_TYPE );
405
406 if ( false === $finfo ) {
407 // translators: %s is the file name.
408 $errors[] = sprintf( __( 'Could not verify the file type for "%s". Please use a .json or .zip file.', 'insert-php' ), $file['name'] );
409 continue;
410 }
411
412 $mime_type = finfo_file( $finfo, $file['tmp_name'] );
413 finfo_close( $finfo );
414
415 $allowed_mime_types = [
416 'application/json',
417 'text/plain',
418 'application/zip',
419 'application/x-zip-compressed',
420 ];
421
422 if ( ! in_array( $mime_type, $allowed_mime_types, true ) ) {
423 // translators: %s is the file name.
424 $errors[] = sprintf( __( 'Invalid file MIME type: %s', 'insert-php' ), $file['name'] );
425 continue;
426 }
427
428 $validated_files[] = $file;
429 }
430
431 // If no valid files, return error.
432 if ( empty( $validated_files ) ) {
433 return new \WP_REST_Response(
434 [
435 'message' => __( 'No valid files to import.', 'insert-php' ),
436 'success' => false,
437 'errors' => $errors,
438 ],
439 400
440 );
441 }
442
443 // Process import using the import snippet class.
444 if ( ! class_exists( 'WINP_Import_Snippet' ) ) {
445 require_once WINP_PLUGIN_DIR . '/admin/includes/class.import.snippet.php';
446 }
447
448 $import_handler = new WINP_Import_Snippet();
449 $result = $import_handler->process_import_files( $validated_files, $duplicate_action );
450
451 // Merge validation errors with import errors.
452 $all_errors = array_merge( $errors, $result['errors'] );
453
454 if ( $result['count'] > 0 ) {
455 // translators: %d is the number of imported snippets.
456 $message = sprintf(
457 // translators: %d is the number of imported snippets.
458 _n(
459 'Successfully imported %d snippet.',
460 'Successfully imported %d snippets.',
461 $result['count'],
462 'insert-php'
463 ),
464 $result['count']
465 );
466
467 if ( ! empty( $all_errors ) ) {
468 $message .= ' ' . __( 'Some files had errors.', 'insert-php' );
469 }
470
471 return new \WP_REST_Response(
472 [
473 'success' => true,
474 'message' => $message,
475 'errors' => $all_errors,
476 'count' => $result['count'],
477 ]
478 );
479 }
480
481 return new \WP_REST_Response(
482 [
483 'message' => __( 'No snippets were imported. Please check your file contains valid snippet data.', 'insert-php' ),
484 'success' => false,
485 'errors' => $all_errors,
486 ]
487 );
488 }
489
490 /**
491 * Handle snippet export.
492 *
493 * @param \WP_REST_Request<array<string, mixed>> $request Rest request.
494 *
495 * @return \WP_REST_Response
496 */
497 public function export_snippets( \WP_REST_Request $request ) {
498 $status = $request->get_param( 'status' );
499 $types = $request->get_param( 'types' );
500 $tags = $request->get_param( 'tags' );
501
502 $status = sanitize_text_field( $status );
503 $types = array_map( 'sanitize_text_field', (array) $types );
504 $tags = array_map( 'sanitize_text_field', (array) $tags );
505
506 // Build query conditions.
507 $meta_query_conditions = [];
508 $tax_query_conditions = [];
509
510 // Status filter.
511 if ( 'all' !== $status ) {
512 if ( 'active' === $status ) {
513 // Active: wbcr_inp_snippet_activate = 1.
514 $meta_query_conditions[] = [
515 'key' => 'wbcr_inp_snippet_activate',
516 'value' => 1,
517 ];
518 } else {
519 // Inactive: wbcr_inp_snippet_activate != 1 OR doesn't exist.
520 $meta_query_conditions[] = [
521 'relation' => 'OR',
522 [
523 'key' => 'wbcr_inp_snippet_activate',
524 'value' => 1,
525 'compare' => '!=',
526 ],
527 [
528 'key' => 'wbcr_inp_snippet_activate',
529 'compare' => 'NOT EXISTS',
530 ],
531 ];
532 }
533 }
534
535 // Types filter.
536 if ( ! empty( $types ) ) {
537 if ( count( $types ) > 1 ) {
538 $type_condition = [ 'relation' => 'OR' ];
539 foreach ( $types as $type ) {
540 $type_condition[] = [
541 'key' => 'wbcr_inp_snippet_type',
542 'value' => $type,
543 ];
544 }
545 } else {
546 $type_condition = [
547 'key' => 'wbcr_inp_snippet_type',
548 'value' => $types[0],
549 ];
550 }
551
552 $meta_query_conditions[] = $type_condition;
553 }
554
555 // Tags filter.
556 if ( ! empty( $tags ) ) {
557 // Ensure taxonomy exists before querying.
558 if ( ! taxonomy_exists( WINP_SNIPPETS_TAXONOMY ) ) {
559 register_taxonomy( WINP_SNIPPETS_TAXONOMY, WINP_SNIPPETS_POST_TYPE, [] );
560 }
561
562 $tax_query_conditions = [
563 [
564 'taxonomy' => WINP_SNIPPETS_TAXONOMY,
565 'field' => 'slug',
566 'terms' => $tags,
567 'operator' => 'IN',
568 ],
569 ];
570 }
571
572 if ( count( $meta_query_conditions ) > 1 ) {
573 $meta_query_conditions['relation'] = 'AND';
574 }
575
576 // Build final query.
577 $conditions = [
578 'post_type' => WINP_SNIPPETS_POST_TYPE,
579 'post_status' => 'publish',
580 'numberposts' => -1,
581 ];
582
583 if ( ! empty( $meta_query_conditions ) ) {
584 $conditions['meta_query'] = $meta_query_conditions;
585 }
586
587 if ( ! empty( $tax_query_conditions ) ) {
588 $conditions['tax_query'] = $tax_query_conditions;
589 }
590
591 // Query snippets.
592 $snippets = get_posts( $conditions );
593
594 if ( empty( $snippets ) ) {
595 return new \WP_REST_Response(
596 [
597 'message' => __( 'No snippets found. Try adjusting your filters or search terms.', 'insert-php' ),
598 'success' => false,
599 ],
600 404
601 );
602 }
603
604 $ids = wp_list_pluck( $snippets, 'ID' );
605
606 require_once WINP_PLUGIN_DIR . '/admin/includes/class.actions.snippet.php';
607 $exporter = new WINP_Actions_Snippet();
608
609 $result = $exporter->export_snippets( $ids, true );
610
611 if ( $result['is_zip'] ) {
612 // For ZIP files, encode as base64 for JSON transport.
613 $data = base64_encode( $result['data'] );
614 } else {
615 // For JSON files, encode as pretty-printed JSON.
616 $data = wp_json_encode( $result['data'], JSON_PRETTY_PRINT );
617 }
618
619 return new \WP_REST_Response(
620 [
621 'success' => true,
622 'filename' => $result['filename'],
623 'data' => $data,
624 'count' => $result['count'],
625 'is_zip' => $result['is_zip'],
626 ]
627 );
628 }
629
630 /**
631 * Handle snippet sync to cloud.
632 *
633 * @param \WP_REST_Request $request Rest request.
634 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
635 *
636 * @return \WP_REST_Response
637 */
638 public function sync_snippet( \WP_REST_Request $request ) {
639 $title = $request->get_param( 'title' );
640 $snippet_id = absint( $request->get_param( 'id' ) );
641
642 // Verify the snippet exists.
643 $snippet = get_post( $snippet_id );
644 if ( ! $snippet || WINP_SNIPPETS_POST_TYPE !== $snippet->post_type ) {
645 return new \WP_REST_Response(
646 [
647 'message' => __( 'Snippet not found. It may have been deleted or moved.', 'insert-php' ),
648 'success' => false,
649 ],
650 404
651 );
652 }
653
654 // Verify the current user has permission to edit this specific snippet.
655 if ( ! current_user_can( 'edit_post', $snippet_id ) ) {
656 return new \WP_REST_Response(
657 [
658 'message' => __( 'You do not have permission to sync this snippet.', 'insert-php' ),
659 'success' => false,
660 ],
661 403
662 );
663 }
664
665 // Sync snippet using the API object.
666 $result = WINP_Plugin::app()->get_api_object()->synchronization( $snippet_id, $title );
667
668 // synchronization() returns true on success, error string on failure, or false if post doesn't exist.
669 if ( true === $result ) {
670 return new \WP_REST_Response(
671 [
672 'success' => true,
673 'message' => __( 'Snippet saved as template successfully.', 'insert-php' ),
674 ],
675 200
676 );
677 }
678
679 // If result is a string, it's an error message. If false, it's a generic error.
680 $error_message = is_string( $result ) ? $result : __( 'Failed to sync snippet. Please check your connection and try again.', 'insert-php' );
681
682 return new \WP_REST_Response(
683 [
684 'success' => false,
685 'message' => $error_message,
686 ],
687 500
688 );
689 }
690 }
691