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

958 lines 27.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once ( MWCODE_PATH . '/vendor/autoload.php' );
4 use PhpParser\ParserFactory;
5 use PhpParser\NodeDumper;
6 use PhpParser\Error;
7
8 class Meow_MWCODE_Core
9 {
10 public $admin = null;
11 public $snippet = null;
12 public $is_rest = false;
13 public $is_cli = false;
14 public $site_url = null;
15 public $mwcode = null;
16 public $licenser = null;
17
18 private $option_name = 'mwcode_options';
19
20 public function __construct() {
21 global $mwcode;
22
23 $this->site_url = get_site_url();
24 $this->is_rest = MeowKit_MWCODE_Helpers::is_rest();
25 $this->is_cli = defined( 'WP_CLI' ) && WP_CLI;
26
27 // Snippets
28 $snippet = new Meow_MWCODE_Modules_Snippet( $this );
29 $this->snippet = $snippet;
30
31 // Create API before plugins_loaded
32 $this->mwcode = new Meow_MWCODE_API( $this, $snippet );
33 $mwcode = $this->mwcode;
34
35 // Add the shortcode for the "content" snippets
36 add_shortcode( 'code-engine', [ $this, 'content_shortcode' ] );
37
38 add_action( 'plugins_loaded', array( $this, 'init' ) );
39 }
40
41 function init() {
42 // Initialize the licenser for Pro version
43 if ( class_exists( 'MeowKitPro_MWCODE_Licenser' ) ) {
44 $this->licenser = new MeowKitPro_MWCODE_Licenser( MWCODE_PREFIX, MWCODE_ENTRY, MWCODE_DOMAIN, MWCODE_ITEM_ID, MWCODE_VERSION );
45 }
46
47 // Part of the core, settings and stuff
48 $this->admin = new Meow_MWCODE_Admin( $this );
49
50 // Only for REST
51 if ( $this->is_rest ) {
52 new Meow_MWCODE_Rest( $this, $this->admin, $this->snippet );
53 }
54
55 // MCP integration - check both class and global variable
56 if ( class_exists( 'Meow_MWAI_Core' ) || isset( $GLOBALS['mwai'] ) ) {
57 new Meow_MWCODE_MCP( $this );
58 }
59 }
60
61 /**
62 *
63 * Roles & Access Rights
64 *
65 */
66 #region Roles & Access Rights
67 public function can_access_settings() {
68 return apply_filters( 'mwcode_allow_setup', current_user_can( 'manage_options' ) );
69 }
70
71 public function can_access_features() {
72 return apply_filters( 'mwcode_allow_usage', current_user_can( 'administrator' ) );
73 }
74
75 public function check_rest_nonce( $request ) {
76 $nonce = $request->get_header( 'X-WP-Nonce' );
77 return wp_verify_nonce( $nonce, 'wp_rest' );
78 }
79 #endregion
80
81 #region Options
82
83 function get_option( $option, $default = null ) {
84 $options = $this->get_all_options();
85 return $options[$option] ?? $default;
86 }
87
88 function list_options() {
89 return [
90 //Safemode
91 "safe_mode_status" => "on", // on, off, whitelist
92 "safe_mode_whitelist" => [],
93 //"disallow_block_php" => true, // Do not allow PHP code to be execute through Blocks "code" parameter
94 "code_blocks" => false,
95 "code_blocks_whitelist" => [], // Whitelist for code blocks, if empty, all code blocks are allowed
96
97 //LOGS
98 "server_debug_mode" => false,
99
100 //UI
101 "ui_show_preview" => false,
102
103 //AI
104 "ai_suggestions" => false,
105 "ai_engine_status"=> false,
106 "ai_engine_message" => "",
107
108 //API
109 "api_endpoint" => false,
110 "api_token" => md5( time() . rand() ),
111
112 //MCP
113 "mcp_support" => false,
114
115 //MAINTENANCE
116 "clean_uninstall" => false,
117 ];
118 }
119
120 function get_all_options( ) {
121 $options = get_option( $this->option_name, [] );
122 $defaults = $this->list_options();
123
124 // Merge with defaults to ensure all options exist
125 $options = array_merge( $defaults, $options );
126
127 $options = $this->sanitize_options( $options );
128 return $options;
129 }
130
131 function update_options( $options ) {
132
133 $options = $this->sanitize_options( $options );
134
135 if ( !update_option( $this->option_name, $options, false ) ) {
136 $this->log( '💾 There was an issue updating the options.' );
137 }
138
139 return $options;
140 }
141
142 function update_option( $option, $value ) {
143 $options = $this->get_all_options();
144 $options[$option] = $value;
145 return $this->update_options( $options );
146 }
147
148 function reset_options() {
149 if ( $this->get_all_options() === $this->list_options() ) {
150 return true;
151 }
152 return $this->update_options( $this->list_options() );
153 }
154
155 // Validate and keep the options clean and logical.
156 function sanitize_options( $options ) {
157 $options_modified = false;
158
159 // Ensure mcp_support exists in options
160 if ( !isset( $options['mcp_support'] ) ) {
161 $options['mcp_support'] = false;
162 }
163
164 // Make sure safe mode whitelist is an array
165 if ( ! is_array( $options['safe_mode_whitelist'] ) ) {
166 $options['safe_mode_whitelist'] = explode( ",", $options['safe_mode_whitelist'] );
167 $options_modified = true;
168 }
169
170 // Update AI Engine status
171 $options_modified = $this->updateAIEngineStatus( $options ) || $options_modified;
172
173 // Disable AI related features if AI Engine is not available
174 if ( ! $options['ai_engine_status'] ) {
175 if ( $options['ai_suggestions'] !== false ) {
176 $options['ai_suggestions'] = false;
177 $options_modified = true;
178 }
179 // Note: We don't disable MCP support here anymore
180 // It will be checked at runtime in the MCP class
181 }
182
183 return $options;
184 }
185
186 private function updateAIEngineStatus( &$options ) {
187 global $mwai;
188
189 if ( is_null( $mwai ) || ! isset( $mwai ) ) {
190 $options['ai_engine_status'] = false;
191 $options['ai_engine_message'] = 'AI Engine is not available.';
192 return true;
193 }
194
195 try {
196 $status = $mwai->checkStatus();
197
198 if ( $options['ai_engine_status'] != true || $options['ai_engine_message'] != $status ) {
199 $options['ai_engine_status'] = true;
200 $options['ai_engine_message'] = $status;
201 return true;
202 }
203 } catch ( Exception $e ) {
204 if ( $options['ai_engine_status'] != false || $options['ai_engine_message'] != $e->getMessage() ) {
205 $options['ai_engine_status'] = false;
206 $options['ai_engine_message'] = $e->getMessage();
207 return true;
208 }
209 }
210
211 return false;
212 }
213
214 #endregion
215
216 #region Snippets
217
218 /**
219 * Get snippet.
220 *
221 * @param $id
222 * @return mixed
223 */
224 protected function get_snippet( $id ) {
225 if ( $this->snippet === null ) {
226 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
227 }
228
229 return $this->snippet->select_one( $id );
230 }
231
232 function add_snippet( $params ) {
233
234 $response = [
235 "snippet" => null,
236 "result" => false,
237 ];
238
239 $this->snippet->validate( $params );
240
241 $params = $this->snippet->formatParamsForDatabase( $params );
242 $result = $this->snippet->insert( $params );
243 $snippet = $this->snippet->select_one( $result );
244
245 if( $result ) {
246 $params['id'] = (string)$result;
247
248 $this->snippet->create_or_update_function_snippet( $params );
249 $this->snippet->create_or_update_interval_snippet( $params );
250
251 $this->snippet->get_function_snippets_data( $snippet );
252 }
253
254 $response['snippet'] = $snippet;
255 $response['result'] = $result;
256
257 return $response;
258 }
259
260 private function sanitize_arg( $name, $value, $type = null) {
261 $real_type = gettype( $value );
262
263 if ( $name[0] !== '$' ) { $name = '$' . $name; }
264
265 if ( $type == null ) {
266 $type = $real_type;
267 }
268
269 if ( $type != 'array' && !empty( $value ) && !is_numeric( $value ) && $value[0] !== '"' && $value[strlen( $value ) - 1] !== '"' ) {
270 $value = '"' . esc_sql( $value ) . '"';
271 }
272
273 if ( $type === 'array' && $real_type === 'string' ) {
274 // We got a string like this: "["a", "b", "c"]" or "[ 1, 2, 3 ]"
275 // We need to convert it to an array
276 $value = str_replace( '"', '', $value );
277 $value = str_replace( '[', '', $value );
278 $value = str_replace( ']', '', $value );
279 $value = explode( ',', $value );
280 $value = array_map( 'trim', $value );
281 }
282
283 if ( $type === 'array' ) {
284 // Convert to PHP array format instead of JSON
285 $value = var_export( $value, true );
286 }
287
288 return [ $name, $value ];
289 }
290
291 function run_non_fn_snippet( $id, $code = null, $test = false, $prefix = '' ) {
292 // Retrieve the snippet code from the provided code or via the snippet ID.
293 if ( $code ) {
294 $snippet = [ 'code' => $code ];
295 } else {
296 $snippet = $this->get_snippet( $id );
297 }
298
299 // Remove any PHP opening tag.
300 $snippet['code'] = $this->snippet->sanitize_code( $snippet['code'] );
301
302 if ( $test ) {
303 $snippet['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $snippet['code'] );
304 }
305
306 if( $prefix ) {
307 $snippet['code'] = $prefix . "\n" . $snippet['code'];
308 }
309
310 $error = null;
311 $output = null;
312
313 try {
314 ob_start();
315 eval( $snippet['code'] );
316 $output = ob_get_clean();
317 } catch ( Throwable $e ) {
318 $snippet_id = $id ? " ( ID: $id )" : '(Content Gutenberg Block)';
319 $this->log( '🔴 Error executing the snippet ' . $snippet_id . ' : ' . $e->getMessage() );
320 ob_clean();
321 } finally {
322 restore_error_handler();
323 }
324
325 // If in test mode, return output as an array of lines with an 'error' key if needed.
326 if ( $test ) {
327 $output = explode( "\n", trim( $output ) );
328 if ( $error !== null ) {
329 $output['error'] = $error->getMessage();
330 }
331 } else {
332 if ( $error !== null ) {
333 throw $error;
334 }
335 }
336
337 return $output;
338 }
339
340 function run_snippet( $id, $args = [], $params = [] )
341 {
342 // Static array to track defined functions
343 static $defined_functions = array();
344
345 if ( $id ) { // If there is an ID, we get the snippet, if not we get the data from the params
346 $snippet = $this->get_snippet( $id );
347 $this->snippet->get_function_snippets_data( $snippet ); // adds the function data to the snippet
348
349 $params = [ // We set the params according to the snippet we fetched
350 'test' => false, // If we pass an ID to the function, we are not testing the snippet
351 // 'test' => $params['test'] ?? false if needed we can still use ID and test at the same time (should not happen)
352 'code' => $snippet['code'],
353 'name' => $snippet['functionName'],
354 'args' => $snippet['functionArgs'],
355 'values' => $snippet['functionArgsDict'] // Contains the default values of the arguments
356 ];
357 }
358
359 // Sanitize all the arguments if the option is enabled
360 if ( $this->get_option( 'sanitize_arguments', true ) ) {
361
362 if ( $args ) {
363 foreach ( $args as $name => $value ) {
364 list( $sanitizedName, $sanitizedValue ) = $this->sanitize_arg( $name, $value );
365 unset( $args[$name] );
366
367 $args[$sanitizedName] = $sanitizedValue;
368 }
369 }
370
371 foreach ( $params['values'] as $name => $value ) {
372
373 if( array_key_exists( 'input', $value) ) {
374 list( $sanitizedInputName, $sanitizedInputValue ) = $this->sanitize_arg( $name, $value['input'], $value['type'] );
375 $params['values'][$sanitizedInputName]['input'] = $sanitizedInputValue;
376 }
377
378 if( array_key_exists( 'default', $value) ) {
379 list( $sanitizedDefaultValueName, $sanitizedDefaultValue ) = $this->sanitize_arg( $name, $value['default'], $value['type'] );
380 $params['values'][$sanitizedDefaultValueName]['default'] = $sanitizedDefaultValue;
381 }
382 }
383
384 }
385
386 // Make sure the function is existing and is the one in the snippet
387 if ( empty( $params['code'] ) ) {
388 throw new Exception( 'Code Engine: The snippet code appears to be empty.' );
389 }
390
391 if ( empty( $params['name'] ) || ! str_contains( $params['code'], $params['name'] ) ) {
392 throw new Exception( "Code Engine: Function name does not match. The name should be {$params['name']}." );
393 }
394
395 // Overwrite the default values with the provided ones
396 if ( $args ) {
397 foreach ( $args as $name => $value ) {
398 $params['values'][$name]['input'] = $value;
399 }
400
401 $this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
402 }
403
404 // Check if the function has already been defined
405 if ( !in_array( $params['name'], $defined_functions ) ) {
406
407 // If not, proceed with modification and definition
408 if ( $params['test'] ) { // Make sure the echo statement uses a line break
409 $params['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $params['code'] );
410 } else { // Remove all echo statements
411 $params['code'] = preg_replace( '/echo\s+(.+?);/s', '', $params['code'] );
412 }
413
414 $params['code'] = "if (!function_exists('{$params['name']}')) {\n" . $params['code'] . "\n}\n";
415
416 // Add the function name to the array to avoid redefinition
417 $defined_functions[] = $params['name'];
418 } else {
419 // If already defined, just prepare to call the function without redefining it
420 $params['code'] = '';
421 }
422
423 // Prepare the code to be executed
424 $params['code'] .= "\n\$mwcode_result = {$params['name']}(";
425 foreach ( $params['args'] as $index => $arg ) {
426 $value = 'null'; // In case the argument is not provided it will be null
427
428 if ( array_key_exists( $arg, $params['values'] ) ) { // Avoid warnings if the argument is not provided
429
430 // If the argument is provided, use it, if not use the default value
431 if ( !empty( $params['values'][$arg]['input'] ) ) {
432 $value = $params['values'][$arg]['input'];
433
434 } else if ( !empty( $params['values'][$arg]['default'] ) ) {
435 $value = $params['values'][$arg]['default'];
436 }
437 }
438
439 $params['code'] .= "{$value}";
440 if ( $index < count( $params['args'] ) - 1 ) {
441 $params['code'] .= ', ';
442 }
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 ( MeowKit_MWCODE_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'] = $this->snippet->sanitize_code( $snippet['code'] );
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 function separate_mwcode_atts( $atts ) {
653
654 if( array_key_exists( 'id', $atts ) ) unset( $atts['id'] );
655 if( array_key_exists( 'target', $atts ) ) unset( $atts['target'] );
656 if( array_key_exists( 'code', $atts ) ) unset( $atts['code'] );
657
658 return $atts;
659 }
660
661 function content_shortcode( $atts ) {
662
663 $user_atts = $this->separate_mwcode_atts( $atts );
664
665 $atts = shortcode_atts( array(
666 'id' => null,
667 'target' => null, // js or php
668 'code' => null, // For Guttenberg block usage
669 ), $atts, 'code-engine' );
670
671 $id = $atts['id'];
672 $target = $atts['target'];
673 $code = $atts['code'];
674 $current_post = get_post();
675
676 $no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
677 $allow_php = $this->get_option( 'code_blocks', false );
678 $allow_php_whitelist = $this->get_option( 'code_blocks_whitelist', [] );
679
680 // If the ID is null, it means it comes from a Guttenberg block
681 $is_block = empty( $id ) && !empty( $code );
682
683 if( $is_block ) {
684
685 if( $target !== 'js' && $target !== 'php' ) {
686 return '<b>Code Engine:</b> Please provide a valid target (js or php).';
687 }
688
689 if ( $no_js && $target === 'js' ) {
690 return '<b>Code Engine:</b> Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
691 }
692
693 if ( $target === 'php' ) {
694
695 if ( !$allow_php ) {
696 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.';
697 }
698
699 if ( !empty( $allow_php_whitelist ) && !in_array( $current_post->ID, $allow_php_whitelist ) ) {
700 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.';
701 }
702 }
703
704 // Because the code from Blocks are sanitized, we need to replace the &quot; with "
705 $code = str_replace( '&quot;', '"', $code );
706
707 if ( $target === 'js' ) {
708 $output = '<script>' . $code . '</script>';
709 }
710
711 if ( $target === 'php' ) {
712 $output = $this->run_non_fn_snippet( null, $code );
713 }
714
715 return $output;
716 }
717
718 // If not a block, we get the snippet by ID
719 // If the ID is not null, it means it comes from a shortcode
720 if ( empty( $id ) && empty( $code ) ) {
721 return '<b>Code Engine:</b> Please provide a snippet ID.';
722 }
723
724 $snippet = $this->get_snippet( $id );
725
726 if ( empty( $snippet ) ) {
727 return '<b>Code Engine:</b> The snippet does not exist.';
728 }
729
730 //Check if the snippet scope is either content_php or content_js
731 $is_content_php = $snippet['scope'] === 'content_php';
732 $is_content_js = $snippet['scope'] === 'content_js';
733
734 if ( !$is_content_php && !$is_content_js ) {
735 return '<b>Code Engine:</b> The snippet is not a content snippet.';
736 }
737
738 if( $no_js && $is_content_js ) {
739 return '<b>Code Engine:</b> Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
740 }
741
742 //Check if the snippet is active
743 if ( !$snippet['active'] ) {
744 return '<b>Code Engine:</b> The snippet is not active.';
745 }
746
747 $output = '<b>Code Engine:</b> No output.';
748
749 if ( $is_content_js ) {
750 $output = '<script>' . $snippet['code'] . '</script>';
751 }
752
753 if ( $is_content_php ) {
754 $prefix = "\$mwcode_atts = unserialize( '" . serialize( $user_atts ) . "' );";
755 $output = $this->run_non_fn_snippet( $id, null, false, $prefix );
756 }
757
758 return $output;
759 }
760
761 #endregion
762
763 #region Logs
764
765 function get_logs() {
766 $log_file_path = $this->get_logs_path();
767
768 if ( !file_exists( $log_file_path ) ) {
769 return "Empty log file.";
770 }
771
772 $content = file_get_contents( $log_file_path );
773 $lines = explode( "\n", $content );
774 $lines = array_filter( $lines );
775 $lines = array_reverse( $lines );
776 $content = implode( "\n", $lines );
777 return $content;
778 }
779
780 function clear_logs() {
781 $logPath = $this->get_logs_path();
782 if ( file_exists( $logPath ) ) {
783 unlink( $logPath );
784 }
785
786 $options = $this->get_all_options();
787 $options['logs_path'] = null;
788 $this->update_options( $options );
789 }
790
791 function get_logs_path() {
792 $uploads_dir = wp_upload_dir();
793 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
794
795 $path = $this->get_option( 'logs_path' );
796
797 if ( $path && file_exists( $path ) ) {
798 // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
799 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
800 $path = null;
801 } else {
802 return $path;
803 }
804 }
805
806 if ( !$path ) {
807 $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
808 if ( !file_exists( $path ) ) {
809 touch( $path );
810 }
811 $options = $this->get_all_options();
812 $options['logs_path'] = $path;
813 $this->update_options( $options );
814 }
815
816 return $path;
817 }
818
819 function log( $data = null ) {
820 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
821 $log_file_path = $this->get_logs_path();
822 $fh = @fopen( $log_file_path, 'a' );
823 if ( !$fh ) { return false; }
824 $date = date( "Y-m-d H:i:s" );
825 if ( is_null( $data ) ) {
826 fwrite( $fh, "\n" );
827 }
828 else {
829 fwrite( $fh, "$date: {$data}\n" );
830 //$this->log( "[MWCODE] $data" );
831 }
832 fclose( $fh );
833 return true;
834 }
835
836 private function random_ascii_chars( $length = 8 ) {
837 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
838 $characters_length = count( $characters );
839 $random_string = '';
840
841 for ( $i = 0; $i < $length; $i++ ) {
842 $random_string .= $characters[rand(0, $characters_length - 1)];
843 }
844
845 return $random_string;
846 }
847
848 #endregion
849
850 #region Helpers
851
852 /**
853 * Check if the request is from a white-listed REST route.
854 *
855 * @return bool
856 */
857 public static function is_white_listed_rest() {
858 $options = get_option( 'mwcode_snippet_vault_options', array() );
859
860 // Early return if bypass is enabled
861 if ( !empty( $options['bypass_rest_security'] ) ) {
862 return true;
863 }
864
865 // Early return for admin requests
866 if ( is_admin() ) {
867 return apply_filters( 'mwcode_rest_authorized', true, null );
868 }
869
870 // Get the requested route
871 $requested_route = self::get_requested_rest_route();
872 if ( !$requested_route ) {
873 return apply_filters( 'mwcode_rest_authorized', false, null );
874 }
875
876 // Check against whitelist
877 $white_listed = apply_filters( 'mwcode_rest_whitelist', array(
878 'mwai/v1',
879 'mwai-ui/v1',
880 'media-file-renamer/v1',
881 'media-cleaner/v1',
882 'wplr/v1',
883 'code-engine/v1',
884 'wp/v2',
885 'meow-gallery/v1',
886 'mcp/v1',
887 ));
888
889 $authorized = self::is_route_whitelisted( $requested_route, $white_listed );
890
891 // Log if debug mode is enabled
892 if ( !empty( $options['server_debug_mode'] ) ) {
893 self::log_route_status( $requested_route, $authorized );
894 }
895
896 return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
897 }
898
899 /**
900 * Extract the REST route from the request URI.
901 *
902 * @return string|null
903 */
904 public static function get_requested_rest_route() {
905 if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
906 return null;
907 }
908
909 $route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
910
911 if ( isset( $route_parts[1] ) ) {
912 return trim( $route_parts[1], '/' );
913 }
914
915 return null;
916 }
917
918 /**
919 * Check if a route is in the whitelist.
920 *
921 * @param string $route The route to check
922 * @param array $white_listed The whitelist array
923 * @return bool
924 */
925 private static function is_route_whitelisted( $route, $white_listed ) {
926 foreach ( $white_listed as $white_listed_route ) {
927 if ( strpos( $route, $white_listed_route ) === 0 ) {
928 return true;
929 }
930 }
931 return false;
932 }
933
934 /**
935 * Log the route authorization status.
936 *
937 * @param string $route The route being checked
938 * @param bool $authorized Whether the route is authorized
939 */
940 private static function log_route_status( $route, $authorized ) {
941 global $mwcode_core;
942
943 $message = $authorized
944 ? "�
945 REST route authorized: " . $route
946 : " REST route rejected (not whitelisted): " . $route;
947
948 if ( isset( $mwcode_core ) ) {
949 $mwcode_core->log( $message );
950 } else {
951 error_log( "[Code Engine] " . $message );
952 }
953 }
954
955 #endregion
956 }
957
958 ?>