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

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