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

948 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 // Convert to PHP array format instead of JSON
290 $value = var_export( $value, true );
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
446 $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
447
448 $error = null;
449 $output = null;
450
451 try {
452 ob_start();
453 eval( $params['code'] );
454 $output = ob_get_clean();
455
456 if ( $params['test'] ){
457 $output = explode( "\n", $output );
458 }
459
460 } catch ( Throwable $e ) {
461 //$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
462 $error = new Exception(' Error executing the function, ' . $e->getMessage());
463
464 ob_clean();
465 } finally {
466 restore_error_handler();
467 }
468
469 if ( $error !== null ) {
470 if( $params['test'] ){
471 $output['error'] = $error->getMessage();
472 } else {
473 throw $error;
474 }
475 }
476
477 return $output;
478 }
479
480
481 function parse_snippet( $code, $new_snippet = false ){
482 $parser = ( new ParserFactory( ) )->createForNewestSupportedVersion( );
483
484 if( !$this->snippet ){
485 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
486 }
487
488 // First we check the function names are unique
489 $fn = $this->snippet->sanitize_and_check_functions( $code, $new_snippet );
490 if ( ! $fn['is_valid'] ) {
491
492 $lint = [
493 'line' => 1,
494 'attributes' => $fn['attributes'][0],
495 'raw_message' => implode(', ', $fn['errors'][0]),
496 'message' => implode(', ', $fn['errors'][0]),
497 ];
498
499 return $lint;
500 }
501
502 try {
503 $stmts = $parser->parse( $code );
504 $result = $stmts;
505 } catch ( PhpParser\Error $e ) {
506
507 $lint = [
508 'line' => $e->getStartLine(),
509 'attributes' => $e->getAttributes(),
510 'raw_message' => $e->getRawMessage(),
511 'message' => $e->getMessage(),
512 ];
513
514 return $lint;
515 }
516
517 return null;
518 }
519
520 public function get_js_functions_to_push() {
521 $functions = $this->snippet->get_functions();
522 $js_functions = [];
523 foreach ( $functions as &$function ) {
524 if ( !isset( $function['target'] ) ) {
525 $function['target'] = 'php';
526 }
527 if ( $function['target'] == 'js' ) {
528 $js_functions[] = $function;
529 }
530 }
531 $snippets = [];
532 foreach ( $js_functions as $function ) {
533 $snippet = $this->snippet->select_one( $function['snippetId'] );
534 $snippet['function_info'] = $function; // Add function info to snippet
535 $snippets[] = $snippet;
536 }
537
538 return $this->generate_js_functions_code( $snippets );
539 }
540
541 function generate_js_functions_code ($snippets ) {
542 $code = "";
543 foreach ( $snippets as $snippet ) {
544 $function_code = $snippet['code'];
545 $function_info = $snippet['function_info'];
546
547 // Extract function name and arguments
548 preg_match( '/(?:const|let|var)?\s*(\w+)\s*=\s*\((.*?)\)\s*=>/', $function_code, $matches );
549 $function_name = $matches[1] ?? $function_info['name'];
550 $function_args = $matches[2] ?? '';
551
552 // Prepare default values
553 $default_args = [];
554 foreach ( $function_info['args'] as $arg ) {
555 if ( isset( $arg['default'] ) && $arg['default'] !== '' ) {
556 $default_args[$arg['name']] = $arg['default'];
557 }
558 }
559
560 // Modify function to use default values
561 if ( !empty( $default_args ) ) {
562 $new_args = explode( ',', $function_args );
563 foreach ( $new_args as &$arg ) {
564 $arg = trim( $arg );
565 if ( isset( $default_args[$arg] ) ) {
566 $arg .= " = " . json_encode( $default_args[$arg] );
567 }
568 }
569 $new_args_string = implode( ', ', $new_args );
570 $function_code = preg_replace(
571 '/(\w+)\s*=\s*\((.*?)\)\s*=>/',
572 "$1 = ($new_args_string) =>",
573 $function_code
574 );
575 }
576
577 $code .= $function_code . "\n\n";
578 }
579
580 return $code;
581 }
582
583
584 /**
585 * [STATIC] Execute active snippets.
586 *
587 * @return array
588 */
589 public function execute_active_snippets() {
590
591 $blocked = false;
592 $page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
593
594
595 if ( $page === 'mwcode_settings' ) {
596 // If we blocks global snippets like nonce_life filter, we would block the settings page so let's remove the block for this page
597
598 $blocked = false;
599 //$blocked = true;
600 }
601 // Block REST requests that aren't whitelisted
602 elseif ( MeowCommon_Helpers::is_rest() && !Meow_MWCODE_Core::is_white_listed_rest() ) {
603 $blocked = true;
604 }
605
606 if ( empty( $this->snippet ) ) {
607 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
608 }
609
610 $ts = $this->get_option( 'thrown_snippet', null );
611 if ( !empty( $ts ) ) {
612 $this->log( "⚠️ Your snippet \"{$ts['name']}\" has thrown a fatal error last time, so we disabled it. Please check the logs for more information." );
613 $this->snippet->force_disable( $ts['id'] );
614 $this->update_option( 'thrown_snippet', null );
615 }
616
617 $scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
618 // Get all active snippets
619
620 $snippets = $this->snippet->select(
621 null, // offset
622 -1, // limit
623 [
624 [ 'accessor' => 'active', 'value' => 1 ],
625 [ 'accessor' => 'scope', 'value' => $scope ],
626 ], // filter
627 [ 'accessor' => 'priority', 'by' => 'DESC' ] // sort
628 )['data'];
629
630 if ( empty( $snippets ) ) {
631 return;
632 }
633
634 $snippets = array_map( function ( $snippet ) use ( $blocked ) {
635 $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
636 $snippet['blocked'] = $blocked;
637
638 // If the snippet must be executed only in the frontend, we bypass the block
639 if ( !is_admin() && $snippet['scope'] === 'frontend' ) {
640 $snippet['blocked'] = false;
641 }
642
643 return $snippet;
644 }, $snippets );
645
646 return $snippets;
647 }
648
649
650 #endregion
651
652 #region Shortcodes
653
654 function content_shortcode( $atts ) {
655
656 $atts = shortcode_atts( array(
657 'id' => null,
658 'target' => null,
659 'code' => null,
660 ), $atts );
661
662 $id = $atts['id'];
663 $target = $atts['target'];
664 $code = $atts['code'];
665 $current_post = get_post();
666
667 $no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
668 $allow_php = $this->get_option( 'code_blocks', false );
669 $allow_php_whitelist = $this->get_option( 'code_blocks_whitelist', [] );
670
671 // If the ID is null, it means it comes from a Guttenberg block
672 $is_block = empty( $id ) && !empty( $code );
673
674 if( $is_block ) {
675
676 if( $target !== 'js' && $target !== 'php' ) {
677 return '<b>Code Engine:</b> Please provide a valid target (js or php).';
678 }
679
680 if ( $no_js && $target === 'js' ) {
681 return '<b>Code Engine:</b> Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
682 }
683
684 if ( $target === 'php' ) {
685
686 if ( !$allow_php ) {
687 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.';
688 }
689
690 if ( !empty( $allow_php_whitelist ) && !in_array( $current_post->ID, $allow_php_whitelist ) ) {
691 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.';
692 }
693 }
694
695 // Because the code from Blocks are sanitized, we need to replace the &quot; with "
696 $code = str_replace( '&quot;', '"', $code );
697
698 if ( $target === 'js' ) {
699 $output = '<script>' . $code . '</script>';
700 }
701
702 if ( $target === 'php' ) {
703 $output = $this->run_non_fn_snippet( null, $code );
704 }
705
706 return $output;
707 }
708
709 // If not a block, we get the snippet by ID
710 // If the ID is not null, it means it comes from a shortcode
711 if ( empty( $id ) && empty( $code ) ) {
712 return '<b>Code Engine:</b> Please provide a snippet ID.';
713 }
714
715 $snippet = $this->get_snippet( $id );
716
717 if ( empty( $snippet ) ) {
718 return '<b>Code Engine:</b> The snippet does not exist.';
719 }
720
721 //Check if the snippet scope is either content_php or content_js
722 $is_content_php = $snippet['scope'] === 'content_php';
723 $is_content_js = $snippet['scope'] === 'content_js';
724
725 if ( !$is_content_php && !$is_content_js ) {
726 return '<b>Code Engine:</b> The snippet is not a content snippet.';
727 }
728
729 if( $no_js && $is_content_js ) {
730 return '<b>Code Engine:</b> Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
731 }
732
733 //Check if the snippet is active
734 if ( !$snippet['active'] ) {
735 return '<b>Code Engine:</b> The snippet is not active.';
736 }
737
738 $output = '<b>Code Engine:</b> No output.';
739
740 if ( $is_content_js ) {
741 $output = '<script>' . $snippet['code'] . '</script>';
742 }
743
744 if ( $is_content_php ) {
745 $output = $this->run_non_fn_snippet( $id );
746 }
747
748 return $output;
749 }
750
751 #endregion
752
753 #region Logs
754
755 function get_logs() {
756 $log_file_path = $this->get_logs_path();
757
758 if ( !file_exists( $log_file_path ) ) {
759 return "Empty log file.";
760 }
761
762 $content = file_get_contents( $log_file_path );
763 $lines = explode( "\n", $content );
764 $lines = array_filter( $lines );
765 $lines = array_reverse( $lines );
766 $content = implode( "\n", $lines );
767 return $content;
768 }
769
770 function clear_logs() {
771 $logPath = $this->get_logs_path();
772 if ( file_exists( $logPath ) ) {
773 unlink( $logPath );
774 }
775
776 $options = $this->get_all_options();
777 $options['logs_path'] = null;
778 $this->update_options( $options );
779 }
780
781 function get_logs_path() {
782 $uploads_dir = wp_upload_dir();
783 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
784
785 $path = $this->get_option( 'logs_path' );
786
787 if ( $path && file_exists( $path ) ) {
788 // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
789 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
790 $path = null;
791 } else {
792 return $path;
793 }
794 }
795
796 if ( !$path ) {
797 $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
798 if ( !file_exists( $path ) ) {
799 touch( $path );
800 }
801 $options = $this->get_all_options();
802 $options['logs_path'] = $path;
803 $this->update_options( $options );
804 }
805
806 return $path;
807 }
808
809 function log( $data = null ) {
810 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
811 $log_file_path = $this->get_logs_path();
812 $fh = @fopen( $log_file_path, 'a' );
813 if ( !$fh ) { return false; }
814 $date = date( "Y-m-d H:i:s" );
815 if ( is_null( $data ) ) {
816 fwrite( $fh, "\n" );
817 }
818 else {
819 fwrite( $fh, "$date: {$data}\n" );
820 //$this->log( "[MWCODE] $data" );
821 }
822 fclose( $fh );
823 return true;
824 }
825
826 private function random_ascii_chars( $length = 8 ) {
827 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
828 $characters_length = count( $characters );
829 $random_string = '';
830
831 for ( $i = 0; $i < $length; $i++ ) {
832 $random_string .= $characters[rand(0, $characters_length - 1)];
833 }
834
835 return $random_string;
836 }
837
838 #endregion
839
840 #region Helpers
841
842 /**
843 * Check if the request is from a white-listed REST route.
844 *
845 * @return bool
846 */
847 public static function is_white_listed_rest() {
848 $options = get_option( 'mwcode_snippet_vault_options', array() );
849
850 // Early return if bypass is enabled
851 if ( !empty( $options['bypass_rest_security'] ) ) {
852 return true;
853 }
854
855 // Early return for admin requests
856 if ( is_admin() ) {
857 return apply_filters( 'mwcode_rest_authorized', true, null );
858 }
859
860 // Get the requested route
861 $requested_route = self::get_requested_rest_route();
862 if ( !$requested_route ) {
863 return apply_filters( 'mwcode_rest_authorized', false, null );
864 }
865
866 // Check against whitelist
867 $white_listed = apply_filters( 'mwcode_rest_whitelist', array(
868 'mwai/v1',
869 'mwai-ui/v1',
870 'media-file-renamer/v1',
871 'media-cleaner/v1',
872 'wplr/v1',
873 'code-engine/v1',
874 'wp/v2',
875 'meow-gallery/v1',
876 'mcp/v1',
877 ));
878
879 $authorized = self::is_route_whitelisted( $requested_route, $white_listed );
880
881 // Log if debug mode is enabled
882 if ( !empty( $options['server_debug_mode'] ) ) {
883 self::log_route_status( $requested_route, $authorized );
884 }
885
886 return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
887 }
888
889 /**
890 * Extract the REST route from the request URI.
891 *
892 * @return string|null
893 */
894 public static function get_requested_rest_route() {
895 if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
896 return null;
897 }
898
899 $route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
900
901 if ( isset( $route_parts[1] ) ) {
902 return trim( $route_parts[1], '/' );
903 }
904
905 return null;
906 }
907
908 /**
909 * Check if a route is in the whitelist.
910 *
911 * @param string $route The route to check
912 * @param array $white_listed The whitelist array
913 * @return bool
914 */
915 private static function is_route_whitelisted( $route, $white_listed ) {
916 foreach ( $white_listed as $white_listed_route ) {
917 if ( strpos( $route, $white_listed_route ) === 0 ) {
918 return true;
919 }
920 }
921 return false;
922 }
923
924 /**
925 * Log the route authorization status.
926 *
927 * @param string $route The route being checked
928 * @param bool $authorized Whether the route is authorized
929 */
930 private static function log_route_status( $route, $authorized ) {
931 global $mwcode_core;
932
933 $message = $authorized
934 ? "�
935 REST route authorized: " . $route
936 : " REST route rejected (not whitelisted): " . $route;
937
938 if ( isset( $mwcode_core ) ) {
939 $mwcode_core->log( $message );
940 } else {
941 error_log( "[Code Engine] " . $message );
942 }
943 }
944
945 #endregion
946 }
947
948 ?>