PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.4.0
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.4.0
0.5.7 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 All 33 releases
code-engine / classes / core.php

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

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