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

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