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