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

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