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

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