PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.3.2
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.3.2
0.5.6 0.5.5 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.9 0.4.8 0.4.7 0.4.6 trunk 0.0.1 0.0.2 0.2.8 0.2.9 0.3.0 0.3.1 0.3.2 0.3.3 0.3.4 0.3.5 0.3.6 0.3.7 0.3.8 All 32 releases
code-engine / classes / core.php

core.php in Code Engine – PHP Snippets, AI Functions & Automation for WordPress 0.3.2, at classes/core.php

905 lines 25.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once ( MWCODE_PATH . '/vendor/autoload.php' );
4 use PhpParser\ParserFactory;
5 use PhpParser\NodeDumper;
6 use PhpParser\Error;
7
8 class Meow_MWCODE_Core
9 {
10 public $admin = null;
11 public $snippet = null;
12 public $is_rest = false;
13 public $is_cli = false;
14 public $site_url = null;
15 public $mwcode = null;
16
17 private $option_name = 'mwcode_options';
18
19 public function __construct() {
20 global $mwcode;
21
22 $this->site_url = get_site_url();
23 $this->is_rest = MeowCommon_Helpers::is_rest();
24 $this->is_cli = defined( 'WP_CLI' ) && WP_CLI;
25
26 // Snippets
27 $snippet = new Meow_MWCODE_Modules_Snippet( $this );
28 $this->snippet = $snippet;
29
30 // Create API before plugins_loaded
31 $this->mwcode = new Meow_MWCODE_API( $this, $snippet );
32 $mwcode = $this->mwcode;
33
34 // Add the shortcode for the "content" snippets
35 add_shortcode( 'code-engine', [ $this, 'content_shortcode' ] );
36
37 add_action( 'plugins_loaded', array( $this, 'init' ) );
38 }
39
40 function init() {
41 // Part of the core, settings and stuff
42 $this->admin = new Meow_MWCODE_Admin( $this );
43
44 // Only for REST
45 if ( $this->is_rest ) {
46 new Meow_MWCODE_Rest( $this, $this->admin, $this->snippet );
47 }
48
49 // MCP integration - check both class and global variable
50 if ( class_exists( 'Meow_MWAI_Core' ) || isset( $GLOBALS['mwai'] ) ) {
51 new Meow_MWCODE_MCP( $this );
52 }
53 }
54
55 /**
56 *
57 * Roles & Access Rights
58 *
59 */
60 #region Roles & Access Rights
61 public function can_access_settings() {
62 return apply_filters( 'mwcode_allow_setup', current_user_can( 'manage_options' ) );
63 }
64
65 public function can_access_features() {
66 return apply_filters( 'mwcode_allow_usage', current_user_can( 'administrator' ) );
67 }
68
69 public function check_rest_nonce( $request ) {
70 $nonce = $request->get_header( 'X-WP-Nonce' );
71 return wp_verify_nonce( $nonce, 'wp_rest' );
72 }
73 #endregion
74
75 #region Options
76
77 function get_option( $option, $default = null ) {
78 $options = $this->get_all_options();
79 return $options[$option] ?? $default;
80 }
81
82 function list_options() {
83 return [
84 //Safemode
85 "safe_mode_status" => "on", // on, off, whitelist
86 "safe_mode_whitelist" => [],
87
88 //LOGS
89 "server_debug_mode" => false,
90
91 //UI
92 "ui_show_preview" => true,
93
94 //AI
95 "ai_suggestions" => false,
96 "ai_engine_status"=> false,
97 "ai_engine_message" => "",
98
99 //API
100 "api_endpoint" => false,
101 "api_token" => md5( time() . rand() ),
102
103 //MCP
104 "mcp_support" => false,
105 ];
106 }
107
108 function get_all_options( ) {
109 $options = get_option( $this->option_name, [] );
110 $defaults = $this->list_options();
111
112 // Merge with defaults to ensure all options exist
113 $options = array_merge( $defaults, $options );
114
115 $options = $this->sanitize_options( $options );
116 return $options;
117 }
118
119 function update_options( $options ) {
120 $current_options = get_option($this->option_name);
121
122 if ($current_options === $options) {
123 // $this->log('💾 The options are already the expected value.');
124 } else {
125 if ( !update_option( $this->option_name, $options, false ) ) {
126 $this->log( '💾 There was an issue updating the options.' );
127 }
128 }
129
130 $options = $this->sanitize_options( $options );
131 return $options;
132 }
133
134 function update_option( $option, $value ) {
135 $options = $this->get_all_options();
136 $options[$option] = $value;
137 return $this->update_options( $options );
138 }
139
140 function reset_options() {
141 if ( $this->get_all_options() === $this->list_options() ) {
142 return true;
143 }
144 return $this->update_options( $this->list_options() );
145 }
146
147 // Validate and keep the options clean and logical.
148 function sanitize_options( $options ) {
149 $options_modified = false;
150
151 // Ensure mcp_support exists in options
152 if ( !isset( $options['mcp_support'] ) ) {
153 $options['mcp_support'] = false;
154 }
155
156 // Make sure safe mode whitelist is an array
157 if ( ! is_array( $options['safe_mode_whitelist'] ) ) {
158 $options['safe_mode_whitelist'] = explode( ",", $options['safe_mode_whitelist'] );
159 $options_modified = true;
160 }
161
162 // Update AI Engine status
163 $options_modified = $this->updateAIEngineStatus( $options ) || $options_modified;
164
165 // Disable AI related features if AI Engine is not available
166 if ( ! $options['ai_engine_status'] ) {
167 if ( $options['ai_suggestions'] !== false ) {
168 $options['ai_suggestions'] = false;
169 $options_modified = true;
170 }
171 // Note: We don't disable MCP support here anymore
172 // It will be checked at runtime in the MCP class
173 }
174
175 if ( $options_modified ) {
176 update_option( $this->option_name, $options, false );
177 }
178
179 return $options;
180 }
181
182 private function updateAIEngineStatus( &$options ) {
183 global $mwai;
184
185 if ( is_null( $mwai ) || ! isset( $mwai ) ) {
186 $options['ai_engine_status'] = false;
187 $options['ai_engine_message'] = 'AI Engine is not available.';
188 return true;
189 }
190
191 try {
192 $status = $mwai->checkStatus();
193
194 if ( $options['ai_engine_status'] != true || $options['ai_engine_message'] != $status ) {
195 $options['ai_engine_status'] = true;
196 $options['ai_engine_message'] = $status;
197 return true;
198 }
199 } catch ( Exception $e ) {
200 if ( $options['ai_engine_status'] != false || $options['ai_engine_message'] != $e->getMessage() ) {
201 $options['ai_engine_status'] = false;
202 $options['ai_engine_message'] = $e->getMessage();
203 return true;
204 }
205 }
206
207 return false;
208 }
209
210 #endregion
211
212 #region Snippets
213
214 /**
215 * Get snippet.
216 *
217 * @param $id
218 * @return mixed
219 */
220 protected function get_snippet( $id ) {
221 if ( $this->snippet === null ) {
222 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
223 }
224
225 return $this->snippet->select_one( $id );
226 }
227
228 function add_snippet( $params ) {
229
230 $response = [
231 "snippet" => null,
232 "result" => false,
233 ];
234
235 $this->snippet->validate( $params );
236
237 $params = $this->snippet->formatParamsForDatabase( $params );
238 $result = $this->snippet->insert( $params );
239 $snippet = $this->snippet->select_one( $result );
240
241 if( $result ) {
242 $params['id'] = (string)$result;
243
244 $this->snippet->create_or_update_function_snippet( $params );
245 $this->snippet->create_or_update_interval_snippet( $params );
246
247 $this->snippet->get_function_snippets_data( $snippet );
248 }
249
250 $response['snippet'] = $snippet;
251 $response['result'] = $result;
252
253 return $response;
254 }
255
256 private function sanitize_arg( $name, $value, $type = null) {
257 $real_type = gettype( $value );
258
259 if ( $name[0] !== '$' ) { $name = '$' . $name; }
260
261 if ( $type == null ) {
262 $type = $real_type;
263 }
264
265 if ( $type != 'array' && !empty( $value ) && !is_numeric( $value ) && $value[0] !== '"' && $value[strlen( $value ) - 1] !== '"' ) {
266 $value = '"' . esc_sql( $value ) . '"';
267 }
268
269 if ( $type === 'array' && $real_type === 'string' ) {
270 // We got a string like this: "["a", "b", "c"]" or "[ 1, 2, 3 ]"
271 // We need to convert it to an array
272 $value = str_replace( '"', '', $value );
273 $value = str_replace( '[', '', $value );
274 $value = str_replace( ']', '', $value );
275 $value = explode( ',', $value );
276 $value = array_map( 'trim', $value );
277 }
278
279 if ( $type === 'array' ) {
280 $value = json_encode( $value );
281 $value = str_replace( '\\', '', $value );
282 }
283
284 return [ $name, $value ];
285 }
286
287 function run_non_fn_snippet( $id, $code = null, $test = false ) {
288 // Retrieve the snippet code from the provided code or via the snippet ID.
289 if ( $code ) {
290 $snippet = [ 'code' => $code ];
291 } else {
292 $snippet = $this->get_snippet( $id );
293 }
294
295 // Remove any PHP opening tag.
296 $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
297
298 if ( $test ) {
299 $snippet['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $snippet['code'] );
300 }
301
302 $error = null;
303 $output = null;
304
305 try {
306 ob_start();
307 eval( $snippet['code'] );
308 $output = ob_get_clean();
309 } catch ( Throwable $e ) {
310 $snippet_id = $id ? " ( ID: $id )" : '(Content Gutenberg Block)';
311 $this->log( '🔴 Error executing the snippet ' . $snippet_id . ' : ' . $e->getMessage() );
312 ob_clean();
313 } finally {
314 restore_error_handler();
315 }
316
317 // If in test mode, return output as an array of lines with an 'error' key if needed.
318 if ( $test ) {
319 $output = explode( "\n", trim( $output ) );
320 if ( $error !== null ) {
321 $output['error'] = $error->getMessage();
322 }
323 } else {
324 if ( $error !== null ) {
325 throw $error;
326 }
327 }
328
329 return $output;
330 }
331
332 function run_snippet( $id, $args = [], $params = [] )
333 {
334 // Static array to track defined functions
335 static $defined_functions = array();
336
337 if ( $id ) { // If there is an ID, we get the snippet, if not we get the data from the params
338 $snippet = $this->get_snippet( $id );
339 $this->snippet->get_function_snippets_data( $snippet ); // adds the function data to the snippet
340
341 $params = [ // We set the params according to the snippet we fetched
342 'test' => false, // If we pass an ID to the function, we are not testing the snippet
343 // 'test' => $params['test'] ?? false if needed we can still use ID and test at the same time (should not happen)
344 'code' => $snippet['code'],
345 'name' => $snippet['functionName'],
346 'args' => $snippet['functionArgs'],
347 'values' => $snippet['functionArgsDict'] // Contains the default values of the arguments
348 ];
349 }
350
351 // Sanitize all the arguments if the option is enabled
352 if ( $this->get_option( 'sanitize_arguments', true ) ) {
353
354 if ( $args ) {
355 foreach ( $args as $name => $value ) {
356 list( $sanitizedName, $sanitizedValue ) = $this->sanitize_arg( $name, $value );
357 unset( $args[$name] );
358
359 $args[$sanitizedName] = $sanitizedValue;
360 }
361 }
362
363 foreach ( $params['values'] as $name => $value ) {
364
365 if( array_key_exists( 'input', $value) ) {
366 list( $sanitizedInputName, $sanitizedInputValue ) = $this->sanitize_arg( $name, $value['input'], $value['type'] );
367 $params['values'][$sanitizedInputName]['input'] = $sanitizedInputValue;
368 }
369
370 if( array_key_exists( 'default', $value) ) {
371 list( $sanitizedDefaultValueName, $sanitizedDefaultValue ) = $this->sanitize_arg( $name, $value['default'], $value['type'] );
372 $params['values'][$sanitizedDefaultValueName]['default'] = $sanitizedDefaultValue;
373 }
374 }
375
376 }
377
378 // Make sure the function is existing and is the one in the snippet
379 if ( empty( $params['code'] ) ) {
380 throw new Exception( 'Code Engine: The snippet code appears to be empty.' );
381 }
382
383 if ( empty( $params['name'] ) || ! str_contains( $params['code'], $params['name'] ) ) {
384 throw new Exception( "Code Engine: Function name does not match. The name should be {$params['name']}." );
385 }
386
387 // Overwrite the default values with the provided ones
388 if ( $args ) {
389 foreach ( $args as $name => $value ) {
390 $params['values'][$name]['input'] = $value;
391 }
392
393 $this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
394 }
395
396 // Check if the function has already been defined
397 if ( !in_array( $params['name'], $defined_functions ) ) {
398
399 // If not, proceed with modification and definition
400 if ( $params['test'] ) { // Make sure the echo statement uses a line break
401 $params['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $params['code'] );
402 } else { // Remove all echo statements
403 $params['code'] = preg_replace( '/echo\s+(.+?);/s', '', $params['code'] );
404 }
405
406 $params['code'] = "if (!function_exists('{$params['name']}')) {\n" . $params['code'] . "\n}\n";
407
408 // Add the function name to the array to avoid redefinition
409 $defined_functions[] = $params['name'];
410 } else {
411 // If already defined, just prepare to call the function without redefining it
412 $params['code'] = '';
413 }
414
415 // Prepare the code to be executed
416 $params['code'] .= "\n\$mwcode_result = {$params['name']}(";
417 foreach ( $params['args'] as $index => $arg ) {
418 $value = 'null'; // In case the argument is not provided it will be null
419
420 if ( array_key_exists( $arg, $params['values'] ) ) { // Avoid warnings if the argument is not provided
421
422 // If the argument is provided, use it, if not use the default value
423 if ( !empty( $params['values'][$arg]['input'] ) ) {
424 $value = $params['values'][$arg]['input'];
425
426 } else if ( !empty( $params['values'][$arg]['default'] ) ) {
427 $value = $params['values'][$arg]['default'];
428 }
429 }
430
431 $params['code'] .= "{$value}";
432 if ( $index < count( $params['args'] ) - 1 ) {
433 $params['code'] .= ', ';
434 }
435 }
436 $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
437
438 $error = null;
439 $output = null;
440
441 try {
442 ob_start();
443 eval( $params['code'] );
444 $output = ob_get_clean();
445
446 if ( $params['test'] ){
447 $output = explode( "\n", $output );
448 }
449
450 } catch ( Throwable $e ) {
451 //$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
452 $error = new Exception(' Error executing the function, ' . $e->getMessage());
453
454 ob_clean();
455 } finally {
456 restore_error_handler();
457 }
458
459 if ( $error !== null ) {
460 if( $params['test'] ){
461 $output['error'] = $error->getMessage();
462 } else {
463 throw $error;
464 }
465 }
466
467 return $output;
468 }
469
470
471 function parse_snippet( $code, $new_snippet = false ){
472 $parser = ( new ParserFactory( ) )->createForNewestSupportedVersion( );
473
474 if( !$this->snippet ){
475 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
476 }
477
478 // First we check the function names are unique
479 $fn = $this->snippet->sanitize_and_check_functions( $code, $new_snippet );
480 if ( ! $fn['is_valid'] ) {
481
482 $lint = [
483 'line' => 1,
484 'attributes' => $fn['attributes'][0],
485 'raw_message' => implode(', ', $fn['errors'][0]),
486 'message' => implode(', ', $fn['errors'][0]),
487 ];
488
489 return $lint;
490 }
491
492 try {
493 $stmts = $parser->parse( $code );
494 $result = $stmts;
495 } catch ( PhpParser\Error $e ) {
496
497 $lint = [
498 'line' => $e->getStartLine(),
499 'attributes' => $e->getAttributes(),
500 'raw_message' => $e->getRawMessage(),
501 'message' => $e->getMessage(),
502 ];
503
504 return $lint;
505 }
506
507 return null;
508 }
509
510 public function get_js_functions_to_push() {
511 $functions = $this->snippet->get_functions();
512 $js_functions = [];
513 foreach ( $functions as &$function ) {
514 if ( !isset( $function['target'] ) ) {
515 $function['target'] = 'php';
516 }
517 if ( $function['target'] == 'js' ) {
518 $js_functions[] = $function;
519 }
520 }
521 $snippets = [];
522 foreach ( $js_functions as $function ) {
523 $snippet = $this->snippet->select_one( $function['snippetId'] );
524 $snippet['function_info'] = $function; // Add function info to snippet
525 $snippets[] = $snippet;
526 }
527
528 return $this->generate_js_functions_code( $snippets );
529 }
530
531 function generate_js_functions_code ($snippets ) {
532 $code = "";
533 foreach ( $snippets as $snippet ) {
534 $function_code = $snippet['code'];
535 $function_info = $snippet['function_info'];
536
537 // Extract function name and arguments
538 preg_match( '/(?:const|let|var)?\s*(\w+)\s*=\s*\((.*?)\)\s*=>/', $function_code, $matches );
539 $function_name = $matches[1] ?? $function_info['name'];
540 $function_args = $matches[2] ?? '';
541
542 // Prepare default values
543 $default_args = [];
544 foreach ( $function_info['args'] as $arg ) {
545 if ( isset( $arg['default'] ) && $arg['default'] !== '' ) {
546 $default_args[$arg['name']] = $arg['default'];
547 }
548 }
549
550 // Modify function to use default values
551 if ( !empty( $default_args ) ) {
552 $new_args = explode( ',', $function_args );
553 foreach ( $new_args as &$arg ) {
554 $arg = trim( $arg );
555 if ( isset( $default_args[$arg] ) ) {
556 $arg .= " = " . json_encode( $default_args[$arg] );
557 }
558 }
559 $new_args_string = implode( ', ', $new_args );
560 $function_code = preg_replace(
561 '/(\w+)\s*=\s*\((.*?)\)\s*=>/',
562 "$1 = ($new_args_string) =>",
563 $function_code
564 );
565 }
566
567 $code .= $function_code . "\n\n";
568 }
569
570 return $code;
571 }
572
573
574 /**
575 * [STATIC] Execute active snippets.
576 *
577 * @return array
578 */
579 public function execute_active_snippets() {
580
581 $blocked = false;
582 $page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
583
584 // Block on settings page for safety
585 if ( $page === 'mwcode_settings' ) {
586 $blocked = true;
587 }
588 // Block REST requests that aren't whitelisted
589 elseif ( MeowCommon_Helpers::is_rest() && !Meow_MWCODE_Core::is_white_listed_rest() ) {
590 $blocked = true;
591 }
592
593 if ( empty( $this->snippet ) ) {
594 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
595 }
596
597 $ts = $this->get_option( 'thrown_snippet', null );
598 if ( !empty( $ts ) ) {
599 $this->log( "⚠️ Your snippet \"{$ts['name']}\" has thrown a fatal error last time, so we disabled it. Please check the logs for more information." );
600 $this->snippet->force_disable( $ts['id'] );
601 $this->update_option( 'thrown_snippet', null );
602 }
603
604 $scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
605 // Get all active snippets
606
607 $snippets = $this->snippet->select(
608 null, // offset
609 -1, // limit
610 [
611 [ 'accessor' => 'active', 'value' => 1 ],
612 [ 'accessor' => 'scope', 'value' => $scope ],
613 ], // filter
614 [ 'accessor' => 'priority', 'by' => 'DESC' ] // sort
615 )['data'];
616
617 if ( empty( $snippets ) ) {
618 return;
619 }
620
621 $snippets = array_map( function ( $snippet ) use ( $blocked ) {
622 $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
623 $snippet['blocked'] = $blocked;
624
625 // If the snippet must be executed only in the frontend, we bypass the block
626 if ( !is_admin() && $snippet['scope'] === 'frontend' ) {
627 $snippet['blocked'] = false;
628 }
629
630 return $snippet;
631 }, $snippets );
632
633 return $snippets;
634 }
635
636
637 #endregion
638
639 #region Shortcodes
640
641 function content_shortcode( $atts ) {
642
643 $atts = shortcode_atts( array(
644 'id' => null,
645 'target' => null,
646 'code' => null,
647 ), $atts );
648
649 $id = $atts['id'];
650 $target = $atts['target'];
651 $code = $atts['code'];
652
653 // If the ID is null, it means it comes from a Guttenberg block
654 $is_block = empty( $id ) && !empty( $code );
655 if( $is_block ){
656
657 // Because the code from Blocks are sanitized, we need to replace the &quot; with "
658 $code = str_replace( '&quot;', '"', $code );
659
660 if ( $target === 'js' ) {
661 $output = '<script>' . $code . '</script>';
662 }
663
664 if ( $target === 'php' ) {
665 $output = $this->run_non_fn_snippet( null, $code );
666 }
667
668 return $output;
669 }
670
671 // If the ID is not null, it means it comes from a shortcode
672 if ( empty( $id ) && empty( $code ) ) {
673 return '<b>Code Engine:</b> Please provide a snippet ID.';
674 }
675
676 $snippet = $this->get_snippet( $id );
677
678 if ( empty( $snippet ) ) {
679 return '<b>Code Engine:</b> The snippet does not exist.';
680 }
681
682 //Check if the snippet scope is either content_php or content_js
683 $is_content_php = $snippet['scope'] === 'content_php';
684 $is_content_js = $snippet['scope'] === 'content_js';
685
686 if ( !$is_content_php && !$is_content_js ) {
687 return '<b>Code Engine:</b> The snippet is not a content snippet.';
688 }
689
690 //Check if the snippet is active
691 if ( !$snippet['active'] ) {
692 return '<b>Code Engine:</b> The snippet is not active.';
693 }
694
695 $output = '<b>Code Engine:</b> No output.';
696
697 if ( $is_content_js ) {
698 $output = '<script>' . $snippet['code'] . '</script>';
699 }
700
701 if ( $is_content_php ) {
702 $output = $this->run_non_fn_snippet( $id );
703 }
704
705 return $output;
706 }
707
708 #endregion
709
710 #region Logs
711
712 function get_logs() {
713 $log_file_path = $this->get_logs_path();
714
715 if ( !file_exists( $log_file_path ) ) {
716 return "Empty log file.";
717 }
718
719 $content = file_get_contents( $log_file_path );
720 $lines = explode( "\n", $content );
721 $lines = array_filter( $lines );
722 $lines = array_reverse( $lines );
723 $content = implode( "\n", $lines );
724 return $content;
725 }
726
727 function clear_logs() {
728 $logPath = $this->get_logs_path();
729 if ( file_exists( $logPath ) ) {
730 unlink( $logPath );
731 }
732
733 $options = $this->get_all_options();
734 $options['logs_path'] = null;
735 $this->update_options( $options );
736 }
737
738 function get_logs_path() {
739 $uploads_dir = wp_upload_dir();
740 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
741
742 $path = $this->get_option( 'logs_path' );
743
744 if ( $path && file_exists( $path ) ) {
745 // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
746 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
747 $path = null;
748 } else {
749 return $path;
750 }
751 }
752
753 if ( !$path ) {
754 $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
755 if ( !file_exists( $path ) ) {
756 touch( $path );
757 }
758 $options = $this->get_all_options();
759 $options['logs_path'] = $path;
760 $this->update_options( $options );
761 }
762
763 return $path;
764 }
765
766 function log( $data = null ) {
767 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
768 $log_file_path = $this->get_logs_path();
769 $fh = @fopen( $log_file_path, 'a' );
770 if ( !$fh ) { return false; }
771 $date = date( "Y-m-d H:i:s" );
772 if ( is_null( $data ) ) {
773 fwrite( $fh, "\n" );
774 }
775 else {
776 fwrite( $fh, "$date: {$data}\n" );
777 //$this->log( "[MWCODE] $data" );
778 }
779 fclose( $fh );
780 return true;
781 }
782
783 private function random_ascii_chars( $length = 8 ) {
784 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
785 $characters_length = count( $characters );
786 $random_string = '';
787
788 for ( $i = 0; $i < $length; $i++ ) {
789 $random_string .= $characters[rand(0, $characters_length - 1)];
790 }
791
792 return $random_string;
793 }
794
795 #endregion
796
797 #region Helpers
798
799 /**
800 * Check if the request is from a white-listed REST route.
801 *
802 * @return bool
803 */
804 public static function is_white_listed_rest() {
805 $options = get_option( 'mwcode_snippet_vault_options', array() );
806
807 // Early return if bypass is enabled
808 if ( !empty( $options['bypass_rest_security'] ) ) {
809 return true;
810 }
811
812 // Early return for admin requests
813 if ( is_admin() ) {
814 return apply_filters( 'mwcode_rest_authorized', true, null );
815 }
816
817 // Get the requested route
818 $requested_route = self::get_requested_rest_route();
819 if ( !$requested_route ) {
820 return apply_filters( 'mwcode_rest_authorized', false, null );
821 }
822
823 // Check against whitelist
824 $white_listed = apply_filters( 'mwcode_rest_whitelist', array(
825 'mwai/v1',
826 'mwai-ui/v1',
827 'media-file-renamer/v1',
828 'media-cleaner/v1',
829 'wplr/v1',
830 'code-engine/v1',
831 'wp/v2',
832 'meow-gallery/v1',
833 'mcp/v1',
834 ));
835
836 $authorized = self::is_route_whitelisted( $requested_route, $white_listed );
837
838 // Log if debug mode is enabled
839 if ( !empty( $options['server_debug_mode'] ) ) {
840 self::log_route_status( $requested_route, $authorized );
841 }
842
843 return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
844 }
845
846 /**
847 * Extract the REST route from the request URI.
848 *
849 * @return string|null
850 */
851 public static function get_requested_rest_route() {
852 if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
853 return null;
854 }
855
856 $route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
857
858 if ( isset( $route_parts[1] ) ) {
859 return trim( $route_parts[1], '/' );
860 }
861
862 return null;
863 }
864
865 /**
866 * Check if a route is in the whitelist.
867 *
868 * @param string $route The route to check
869 * @param array $white_listed The whitelist array
870 * @return bool
871 */
872 private static function is_route_whitelisted( $route, $white_listed ) {
873 foreach ( $white_listed as $white_listed_route ) {
874 if ( strpos( $route, $white_listed_route ) === 0 ) {
875 return true;
876 }
877 }
878 return false;
879 }
880
881 /**
882 * Log the route authorization status.
883 *
884 * @param string $route The route being checked
885 * @param bool $authorized Whether the route is authorized
886 */
887 private static function log_route_status( $route, $authorized ) {
888 global $mwcode_core;
889
890 $message = $authorized
891 ? "�
892 REST route authorized: " . $route
893 : " REST route rejected (not whitelisted): " . $route;
894
895 if ( isset( $mwcode_core ) ) {
896 $mwcode_core->log( $message );
897 } else {
898 error_log( "[Code Engine] " . $message );
899 }
900 }
901
902 #endregion
903 }
904
905 ?>