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

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