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

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