PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.4.8
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.4.8
0.5.7 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 All 33 releases
← All changes | classes/core.php +228 -55 0.3.20.4.8 View file →
@@ -12,9 +12,14 @@
12 12 public $is_rest = false;
13 13 public $is_cli = false;
14 14 public $site_url = null;
15 15 public $mwcode = null;
16 + public $licenser = null;
16 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 +
17 22 private $option_name = 'mwcode_options';
18 23
19 24 public function __construct() {
20 25 global $mwcode;
@@ -19,9 +24,9 @@
19 24 public function __construct() {
20 25 global $mwcode;
21 26
22 27 $this->site_url = get_site_url();
23 - $this->is_rest = MeowCommon_Helpers::is_rest();
28 + $this->is_rest = MeowKit_MWCODE_Helpers::is_rest();
24 29 $this->is_cli = defined( 'WP_CLI' ) && WP_CLI;
25 30
26 31 // Snippets
27 32 $snippet = new Meow_MWCODE_Modules_Snippet( $this );
@@ -37,8 +42,13 @@
37 42 add_action( 'plugins_loaded', array( $this, 'init' ) );
38 43 }
39 44
40 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 +
41 51 // Part of the core, settings and stuff
42 52 $this->admin = new Meow_MWCODE_Admin( $this );
43 53
44 54 // Only for REST
@@ -83,14 +93,17 @@
83 93 return [
84 94 //Safemode
85 95 "safe_mode_status" => "on", // on, off, whitelist
86 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
87 100
88 101 //LOGS
89 102 "server_debug_mode" => false,
90 103
91 104 //UI
92 - "ui_show_preview" => true,
105 + "ui_show_preview" => false,
93 106
94 107 //AI
95 108 "ai_suggestions" => false,
96 109 "ai_engine_status"=> false,
@@ -101,8 +114,11 @@
101 114 "api_token" => md5( time() . rand() ),
102 115
103 116 //MCP
104 117 "mcp_support" => false,
118 +
119 + //MAINTENANCE
120 + "clean_uninstall" => false,
105 121 ];
106 122 }
107 123
108 124 function get_all_options( ) {
@@ -116,19 +132,15 @@
116 132 return $options;
117 133 }
118 134
119 135 function update_options( $options ) {
120 - $current_options = get_option($this->option_name);
121 136
122 - if ($current_options === $options) {
123 - // $this->log('💾 The options are already the expected value.');
124 - } else {
125 - if ( !update_option( $this->option_name, $options, false ) ) {
126 - $this->log( '💾 There was an issue updating the options.' );
127 - }
137 + $options = $this->sanitize_options( $options );
138 +
139 + if ( !update_option( $this->option_name, $options, false ) ) {
140 + //$this->log( '💾 There was an issue updating the options.' );
128 141 }
129 -
130 - $options = $this->sanitize_options( $options );
142 +
131 143 return $options;
132 144 }
133 145
134 146 function update_option( $option, $value ) {
@@ -159,9 +171,9 @@
159 171 $options_modified = true;
160 172 }
161 173
162 174 // Update AI Engine status
163 - $options_modified = $this->updateAIEngineStatus( $options ) || $options_modified;
175 + $options = $this->updateAIEngineStatus( $options );
164 176
165 177 // Disable AI related features if AI Engine is not available
166 178 if ( ! $options['ai_engine_status'] ) {
167 179 if ( $options['ai_suggestions'] !== false ) {
@@ -171,12 +183,8 @@
171 183 // Note: We don't disable MCP support here anymore
172 184 // It will be checked at runtime in the MCP class
173 185 }
174 186
175 - if ( $options_modified ) {
176 - update_option( $this->option_name, $options, false );
177 - }
178 -
179 187 return $options;
180 188 }
181 189
182 190 private function updateAIEngineStatus( &$options ) {
@@ -181,31 +189,13 @@
181 189
182 190 private function updateAIEngineStatus( &$options ) {
183 191 global $mwai;
184 192
185 - if ( is_null( $mwai ) || ! isset( $mwai ) ) {
186 - $options['ai_engine_status'] = false;
187 - $options['ai_engine_message'] = 'AI Engine is not available.';
188 - return true;
189 - }
193 + $options['mwai_has_ai'] = !empty( $mwai ) && method_exists( $mwai, 'hasAI' ) && $mwai->hasAI();
194 + // Legacy
195 + $options['ai_engine_status'] = $options['mwai_has_ai'];
190 196
191 - try {
192 - $status = $mwai->checkStatus();
193 -
194 - if ( $options['ai_engine_status'] != true || $options['ai_engine_message'] != $status ) {
195 - $options['ai_engine_status'] = true;
196 - $options['ai_engine_message'] = $status;
197 - return true;
198 - }
199 - } catch ( Exception $e ) {
200 - if ( $options['ai_engine_status'] != false || $options['ai_engine_message'] != $e->getMessage() ) {
201 - $options['ai_engine_status'] = false;
202 - $options['ai_engine_message'] = $e->getMessage();
203 - return true;
204 - }
205 - }
206 -
207 - return false;
197 + return $options;
208 198 }
209 199
210 200 #endregion
211 201
@@ -276,16 +266,16 @@
276 266 $value = array_map( 'trim', $value );
277 267 }
278 268
279 269 if ( $type === 'array' ) {
280 - $value = json_encode( $value );
281 - $value = str_replace( '\\', '', $value );
270 + // Convert to PHP array format instead of JSON
271 + $value = var_export( $value, true );
282 272 }
283 273
284 274 return [ $name, $value ];
285 275 }
286 276
287 - function run_non_fn_snippet( $id, $code = null, $test = false ) {
277 + function run_non_fn_snippet( $id, $code = null, $test = false, $prefix = '' ) {
288 278 // Retrieve the snippet code from the provided code or via the snippet ID.
289 279 if ( $code ) {
290 280 $snippet = [ 'code' => $code ];
291 281 } else {
@@ -292,13 +282,17 @@
292 282 $snippet = $this->get_snippet( $id );
293 283 }
294 284
295 285 // Remove any PHP opening tag.
296 - $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
286 + $snippet['code'] = $this->snippet->sanitize_code( $snippet['code'] );
297 287
298 288 if ( $test ) {
299 289 $snippet['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $snippet['code'] );
300 290 }
291 +
292 + if( $prefix ) {
293 + $snippet['code'] = $prefix . "\n" . $snippet['code'];
294 + }
301 295
302 296 $error = null;
303 297 $output = null;
304 298
@@ -392,8 +386,18 @@
392 386
393 387 $this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
394 388 }
395 389
390 + // Global snippets are meant to be always accessible. On non-whitelisted REST routes
391 + // (Workflow Engine, MCP, AI function-calling) the plugins_loaded pass blocks them, so
392 + // make sure their helper library is loaded before we run a function that may call it.
393 + $this->load_global_snippets();
394 +
395 + // Make every *other* active PHP function snippet available so this function can
396 + // call its siblings. We pass the current name as the exception so the target is
397 + // still defined below (with the edited/test code when testing), not pre-defined here.
398 + $this->define_all_functions( $params['name'] );
399 +
396 400 // Check if the function has already been defined
397 401 if ( !in_array( $params['name'], $defined_functions ) ) {
398 402
399 403 // If not, proceed with modification and definition
@@ -432,13 +436,14 @@
432 436 if ( $index < count( $params['args'] ) - 1 ) {
433 437 $params['code'] .= ', ';
434 438 }
435 439 }
440 +
436 441 $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
437 442
438 443 $error = null;
439 444 $output = null;
440 -
445 +
441 446 try {
442 447 ob_start();
443 448 eval( $params['code'] );
444 449 $output = ob_get_clean();
@@ -506,8 +511,132 @@
506 511
507 512 return null;
508 513 }
509 514
515 + /**
516 + * Load the active global snippets (persistent + backend/frontend for this context)
517 + * that haven't already run this request, so on-demand function execution has the same
518 + * always-available helper library a normal page load would. Callable functions are
519 + * typically small wrappers around these globals.
520 + *
521 + * On non-whitelisted REST routes (Workflow Engine, MCP, AI function-calling) the
522 + * plugins_loaded pass blocks global snippets for safety; this restores them for the
523 + * deliberate, authorized act of executing a snippet. The loaded-id registry guarantees
524 + * each global runs at most once per request, so nothing is ever re-declared.
525 + */
526 + function load_global_snippets() {
527 + global $current_mwcode_snippet;
528 + static $done = false;
529 + if ( $done ) {
530 + return;
531 + }
532 + $done = true;
533 +
534 + if ( empty( $this->snippet ) ) {
535 + $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
536 + }
537 +
538 + $scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
539 +
540 + $snippets = $this->snippet->select(
541 + null, // offset
542 + -1, // limit (all)
543 + [
544 + [ 'accessor' => 'active', 'value' => 1 ],
545 + [ 'accessor' => 'scope', 'value' => $scope ],
546 + ],
547 + [ 'accessor' => 'priority', 'by' => 'DESC' ]
548 + )['data'] ?? [];
549 +
550 + foreach ( $snippets as $snippet ) {
551 + // Skip globals already executed this request (e.g. by the plugins_loaded pass).
552 + if ( in_array( $snippet['id'], $this->loaded_global_ids ) ) {
553 + continue;
554 + }
555 + $this->loaded_global_ids[] = $snippet['id'];
556 +
557 + $code = $this->snippet->sanitize_code( $snippet['code'] );
558 + $current_mwcode_snippet = $snippet;
559 + try {
560 + ob_start();
561 + eval( $code );
562 + ob_end_clean();
563 + } catch ( Throwable $e ) {
564 + ob_end_clean();
565 + $this->log( "⚠️ Code Engine: Failed to load global snippet \"{$snippet['name']}\": " . $e->getMessage() );
566 + }
567 + }
568 + $current_mwcode_snippet = null;
569 + }
570 +
571 + /**
572 + * Declare every active PHP function snippet in the current request, without
573 + * invoking any of them, so function snippets can call one another.
574 + *
575 + * Function snippets are not auto-loaded on every request (unlike global/backend/
576 + * frontend scopes) — they are meant to run on demand. This is the PHP counterpart
577 + * to get_js_functions_to_push(): it makes the whole library of functions callable
578 + * before a function is executed (via REST, MCP, AI function-calling, Workflow Engine).
579 + *
580 + * Idempotent: a static guard runs the full pass only once per request, and each
581 + * definition is wrapped in function_exists() so nothing is ever redefined.
582 + *
583 + * @param string|null $except Function name to skip (the one run_snippet is about to
584 + * define itself, so edited/test code keeps priority).
585 + */
586 + function define_all_functions( $except = null ) {
587 + static $loaded = false;
588 + if ( $loaded ) {
589 + return;
590 + }
591 + $loaded = true;
592 +
593 + if ( empty( $this->snippet ) ) {
594 + $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
595 + }
596 +
597 + // One query for every active function snippet (code included), then enrich with
598 + // the function metadata (name + target) the same way run_snippet does.
599 + $snippets = $this->snippet->select(
600 + null, // offset
601 + -1, // limit (all)
602 + [
603 + [ 'accessor' => 'active', 'value' => 1 ],
604 + [ 'accessor' => 'scope', 'value' => 'function' ],
605 + ],
606 + [] // sort
607 + )['data'] ?? [];
608 +
609 + if ( empty( $snippets ) ) {
610 + return;
611 + }
612 +
613 + $this->snippet->get_function_snippets_data( $snippets );
614 +
615 + foreach ( $snippets as $snippet ) {
616 + $name = $snippet['functionName'] ?? '';
617 + $target = strtolower( $snippet['functionTarget'] ?? 'php' );
618 +
619 + // Skip JS functions (pushed to the front-end separately), the function the
620 + // caller will define itself, and anything already declared in this request.
621 + if ( $name === '' || $target === 'js' || $name === $except || function_exists( $name ) ) {
622 + continue;
623 + }
624 +
625 + // Mirror run_snippet()'s non-test handling: drop echo statements, then declare
626 + // (never call) the function, guarded so a later run_snippet() call is a no-op.
627 + $code = $this->snippet->sanitize_code( $snippet['code'] );
628 + $code = preg_replace( '/echo\s+(.+?);/s', '', $code );
629 + $code = "if (!function_exists('{$name}')) {\n{$code}\n}\n";
630 +
631 + try {
632 + eval( $code );
633 + } catch ( Throwable $e ) {
634 + $this->log( "⚠️ Code Engine: Failed to pre-define function \"{$name}\": " . $e->getMessage() );
635 + }
636 + }
637 + }
638 +
510 639 public function get_js_functions_to_push() {
511 640 $functions = $this->snippet->get_functions();
512 641 $js_functions = [];
513 642 foreach ( $functions as &$function ) {
@@ -580,14 +709,17 @@
580 709
581 710 $blocked = false;
582 711 $page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
583 712
584 - // Block on settings page for safety
713 +
585 714 if ( $page === 'mwcode_settings' ) {
586 - $blocked = true;
715 + // If we blocks global snippets like nonce_life filter, we would block the settings page so let's remove the block for this page
716 +
717 + $blocked = false;
718 + //$blocked = true;
587 719 }
588 720 // Block REST requests that aren't whitelisted
589 - elseif ( MeowCommon_Helpers::is_rest() && !Meow_MWCODE_Core::is_white_listed_rest() ) {
721 + elseif ( MeowKit_MWCODE_Helpers::is_rest() && !Meow_MWCODE_Core::is_white_listed_rest() ) {
590 722 $blocked = true;
591 723 }
592 724
593 725 if ( empty( $this->snippet ) ) {
@@ -618,9 +750,9 @@
618 750 return;
619 751 }
620 752
621 753 $snippets = array_map( function ( $snippet ) use ( $blocked ) {
622 - $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
754 + $snippet['code'] = $this->snippet->sanitize_code( $snippet['code'] );
623 755 $snippet['blocked'] = $blocked;
624 756
625 757 // If the snippet must be executed only in the frontend, we bypass the block
626 758 if ( !is_admin() && $snippet['scope'] === 'frontend' ) {
@@ -636,25 +768,60 @@
636 768
637 769 #endregion
638 770
639 771 #region Shortcodes
772 + function separate_mwcode_atts( $atts ) {
640 773
774 + if( array_key_exists( 'id', $atts ) ) unset( $atts['id'] );
775 + if( array_key_exists( 'target', $atts ) ) unset( $atts['target'] );
776 + if( array_key_exists( 'code', $atts ) ) unset( $atts['code'] );
777 +
778 + return $atts;
779 + }
780 +
641 781 function content_shortcode( $atts ) {
642 782
783 + $user_atts = $this->separate_mwcode_atts( $atts );
784 +
643 785 $atts = shortcode_atts( array(
644 - 'id' => null,
645 - 'target' => null,
646 - 'code' => null,
647 - ), $atts );
786 + 'id' => null,
787 + 'target' => null, // js or php
788 + 'code' => null, // For Guttenberg block usage
789 + ), $atts, 'code-engine' );
648 790
649 791 $id = $atts['id'];
650 792 $target = $atts['target'];
651 793 $code = $atts['code'];
652 -
794 + $current_post = get_post();
795 +
796 + $no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
797 + $allow_php = $this->get_option( 'code_blocks', false );
798 + $allow_php_whitelist = $this->get_option( 'code_blocks_whitelist', [] );
799 +
653 800 // If the ID is null, it means it comes from a Guttenberg block
654 801 $is_block = empty( $id ) && !empty( $code );
655 - if( $is_block ){
656 802
803 + if( $is_block ) {
804 +
805 + if( $target !== 'js' && $target !== 'php' ) {
806 + return '<b>Code Engine:</b> Please provide a valid target (js or php).';
807 + }
808 +
809 + if ( $no_js && $target === 'js' ) {
810 + return '<b>Code Engine:</b> Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
811 + }
812 +
813 + if ( $target === 'php' ) {
814 +
815 + if ( !$allow_php ) {
816 + return '<b>Code Engine:</b> Code Block PHP are disabled. If you are an administrator, you can enable it in the settings, this is not recommended. Please use a Content Snippet ( PHP ) instead.';
817 + }
818 +
819 + if ( !empty( $allow_php_whitelist ) && !in_array( $current_post->ID, $allow_php_whitelist ) ) {
820 + return '<b>Code Engine:</b> Code Block PHP are disabled for this post. If you are an administrator, you can enable it in the settings, this is not recommended. Please use a Content Snippet ( PHP ) instead.';
821 + }
822 + }
823 +
657 824 // Because the code from Blocks are sanitized, we need to replace the &quot; with "
658 825 $code = str_replace( '&quot;', '"', $code );
659 826
660 827 if ( $target === 'js' ) {
@@ -667,8 +834,9 @@
667 834
668 835 return $output;
669 836 }
670 837
838 + // If not a block, we get the snippet by ID
671 839 // If the ID is not null, it means it comes from a shortcode
672 840 if ( empty( $id ) && empty( $code ) ) {
673 841 return '<b>Code Engine:</b> Please provide a snippet ID.';
674 842 }
@@ -680,14 +848,18 @@
680 848 }
681 849
682 850 //Check if the snippet scope is either content_php or content_js
683 851 $is_content_php = $snippet['scope'] === 'content_php';
684 - $is_content_js = $snippet['scope'] === 'content_js';
852 + $is_content_js = $snippet['scope'] === 'content_js';
685 853
686 854 if ( !$is_content_php && !$is_content_js ) {
687 855 return '<b>Code Engine:</b> The snippet is not a content snippet.';
688 856 }
689 857
858 + if( $no_js && $is_content_js ) {
859 + return '<b>Code Engine:</b> Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
860 + }
861 +
690 862 //Check if the snippet is active
691 863 if ( !$snippet['active'] ) {
692 864 return '<b>Code Engine:</b> The snippet is not active.';
693 865 }
@@ -698,9 +870,10 @@
698 870 $output = '<script>' . $snippet['code'] . '</script>';
699 871 }
700 872
701 873 if ( $is_content_php ) {
702 - $output = $this->run_non_fn_snippet( $id );
874 + $prefix = "\$mwcode_atts = unserialize( '" . serialize( $user_atts ) . "' );";
875 + $output = $this->run_non_fn_snippet( $id, null, false, $prefix );
703 876 }
704 877
705 878 return $output;
706 879 }