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

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