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

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