PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.3.3
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.3.3
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.3, at classes/core.php

921 lines 25.8 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" => false,
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 $no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
654
655 // If the ID is null, it means it comes from a Guttenberg block
656 $is_block = empty( $id ) && !empty( $code );
657
658 if( $is_block ) {
659
660 if( $target !== 'js' && $target !== 'php' ) {
661 return '<b>Code Engine:</b> Please provide a valid target (js or php).';
662 }
663
664 if ( $no_js && $target === 'js' ) {
665 return '<b>Code Engine:</b> Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
666 }
667
668 // Because the code from Blocks are sanitized, we need to replace the &quot; with "
669 $code = str_replace( '&quot;', '"', $code );
670
671 if ( $target === 'js' ) {
672 $output = '<script>' . $code . '</script>';
673 }
674
675 if ( $target === 'php' ) {
676 $output = $this->run_non_fn_snippet( null, $code );
677 }
678
679 return $output;
680 }
681
682 // If not a block, we get the snippet by ID
683 // If the ID is not null, it means it comes from a shortcode
684 if ( empty( $id ) && empty( $code ) ) {
685 return '<b>Code Engine:</b> Please provide a snippet ID.';
686 }
687
688 $snippet = $this->get_snippet( $id );
689
690 if ( empty( $snippet ) ) {
691 return '<b>Code Engine:</b> The snippet does not exist.';
692 }
693
694 //Check if the snippet scope is either content_php or content_js
695 $is_content_php = $snippet['scope'] === 'content_php';
696 $is_content_js = $snippet['scope'] === 'content_js';
697
698 if ( !$is_content_php && !$is_content_js ) {
699 return '<b>Code Engine:</b> The snippet is not a content snippet.';
700 }
701
702 if( $no_js && $is_content_js ) {
703 return '<b>Code Engine:</b> Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
704 }
705
706 //Check if the snippet is active
707 if ( !$snippet['active'] ) {
708 return '<b>Code Engine:</b> The snippet is not active.';
709 }
710
711 $output = '<b>Code Engine:</b> No output.';
712
713 if ( $is_content_js ) {
714 $output = '<script>' . $snippet['code'] . '</script>';
715 }
716
717 if ( $is_content_php ) {
718 $output = $this->run_non_fn_snippet( $id );
719 }
720
721 return $output;
722 }
723
724 #endregion
725
726 #region Logs
727
728 function get_logs() {
729 $log_file_path = $this->get_logs_path();
730
731 if ( !file_exists( $log_file_path ) ) {
732 return "Empty log file.";
733 }
734
735 $content = file_get_contents( $log_file_path );
736 $lines = explode( "\n", $content );
737 $lines = array_filter( $lines );
738 $lines = array_reverse( $lines );
739 $content = implode( "\n", $lines );
740 return $content;
741 }
742
743 function clear_logs() {
744 $logPath = $this->get_logs_path();
745 if ( file_exists( $logPath ) ) {
746 unlink( $logPath );
747 }
748
749 $options = $this->get_all_options();
750 $options['logs_path'] = null;
751 $this->update_options( $options );
752 }
753
754 function get_logs_path() {
755 $uploads_dir = wp_upload_dir();
756 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
757
758 $path = $this->get_option( 'logs_path' );
759
760 if ( $path && file_exists( $path ) ) {
761 // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
762 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
763 $path = null;
764 } else {
765 return $path;
766 }
767 }
768
769 if ( !$path ) {
770 $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
771 if ( !file_exists( $path ) ) {
772 touch( $path );
773 }
774 $options = $this->get_all_options();
775 $options['logs_path'] = $path;
776 $this->update_options( $options );
777 }
778
779 return $path;
780 }
781
782 function log( $data = null ) {
783 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
784 $log_file_path = $this->get_logs_path();
785 $fh = @fopen( $log_file_path, 'a' );
786 if ( !$fh ) { return false; }
787 $date = date( "Y-m-d H:i:s" );
788 if ( is_null( $data ) ) {
789 fwrite( $fh, "\n" );
790 }
791 else {
792 fwrite( $fh, "$date: {$data}\n" );
793 //$this->log( "[MWCODE] $data" );
794 }
795 fclose( $fh );
796 return true;
797 }
798
799 private function random_ascii_chars( $length = 8 ) {
800 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
801 $characters_length = count( $characters );
802 $random_string = '';
803
804 for ( $i = 0; $i < $length; $i++ ) {
805 $random_string .= $characters[rand(0, $characters_length - 1)];
806 }
807
808 return $random_string;
809 }
810
811 #endregion
812
813 #region Helpers
814
815 /**
816 * Check if the request is from a white-listed REST route.
817 *
818 * @return bool
819 */
820 public static function is_white_listed_rest() {
821 $options = get_option( 'mwcode_snippet_vault_options', array() );
822
823 // Early return if bypass is enabled
824 if ( !empty( $options['bypass_rest_security'] ) ) {
825 return true;
826 }
827
828 // Early return for admin requests
829 if ( is_admin() ) {
830 return apply_filters( 'mwcode_rest_authorized', true, null );
831 }
832
833 // Get the requested route
834 $requested_route = self::get_requested_rest_route();
835 if ( !$requested_route ) {
836 return apply_filters( 'mwcode_rest_authorized', false, null );
837 }
838
839 // Check against whitelist
840 $white_listed = apply_filters( 'mwcode_rest_whitelist', array(
841 'mwai/v1',
842 'mwai-ui/v1',
843 'media-file-renamer/v1',
844 'media-cleaner/v1',
845 'wplr/v1',
846 'code-engine/v1',
847 'wp/v2',
848 'meow-gallery/v1',
849 'mcp/v1',
850 ));
851
852 $authorized = self::is_route_whitelisted( $requested_route, $white_listed );
853
854 // Log if debug mode is enabled
855 if ( !empty( $options['server_debug_mode'] ) ) {
856 self::log_route_status( $requested_route, $authorized );
857 }
858
859 return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
860 }
861
862 /**
863 * Extract the REST route from the request URI.
864 *
865 * @return string|null
866 */
867 public static function get_requested_rest_route() {
868 if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
869 return null;
870 }
871
872 $route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
873
874 if ( isset( $route_parts[1] ) ) {
875 return trim( $route_parts[1], '/' );
876 }
877
878 return null;
879 }
880
881 /**
882 * Check if a route is in the whitelist.
883 *
884 * @param string $route The route to check
885 * @param array $white_listed The whitelist array
886 * @return bool
887 */
888 private static function is_route_whitelisted( $route, $white_listed ) {
889 foreach ( $white_listed as $white_listed_route ) {
890 if ( strpos( $route, $white_listed_route ) === 0 ) {
891 return true;
892 }
893 }
894 return false;
895 }
896
897 /**
898 * Log the route authorization status.
899 *
900 * @param string $route The route being checked
901 * @param bool $authorized Whether the route is authorized
902 */
903 private static function log_route_status( $route, $authorized ) {
904 global $mwcode_core;
905
906 $message = $authorized
907 ? "�
908 REST route authorized: " . $route
909 : " REST route rejected (not whitelisted): " . $route;
910
911 if ( isset( $mwcode_core ) ) {
912 $mwcode_core->log( $message );
913 } else {
914 error_log( "[Code Engine] " . $message );
915 }
916 }
917
918 #endregion
919 }
920
921 ?>