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

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