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 -781 0.3.80.3.0 View file →
@@ -6,942 +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 - // Convert to PHP array format instead of JSON
290 - $value = var_export( $value, true );
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 - }
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'] ) {
445 462
446 - $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
463 + $lint = [
464 + 'line' => 1,
465 + 'attributes' => $fn['attributes'][0],
466 + 'raw_message' => implode(', ', $fn['errors'][0]),
467 + 'message' => implode(', ', $fn['errors'][0]),
468 + ];
447 469
448 - $error = null;
449 - $output = null;
450 -
451 - try {
452 - ob_start();
453 - eval( $params['code'] );
454 - $output = ob_get_clean();
455 -
456 - if ( $params['test'] ){
457 - $output = explode( "\n", $output );
458 - }
459 -
460 - } catch ( Throwable $e ) {
461 - //$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
462 - $error = new Exception(' Error executing the function, ' . $e->getMessage());
470 + return $lint;
471 + }
463 472
464 - ob_clean();
465 - } finally {
466 - restore_error_handler();
467 - }
473 + try {
474 + $stmts = $parser->parse( $code );
475 + $result = $stmts;
476 + } catch ( PhpParser\Error $e ) {
468 477
469 - if ( $error !== null ) {
470 - if( $params['test'] ){
471 - $output['error'] = $error->getMessage();
472 - } else {
473 - throw $error;
474 - }
475 - }
478 + $lint = [
479 + 'line' => $e->getStartLine(),
480 + 'attributes' => $e->getAttributes(),
481 + 'raw_message' => $e->getRawMessage(),
482 + 'message' => $e->getMessage(),
483 + ];
476 484
477 - return $output;
478 - }
485 + return $lint;
486 + }
479 487
488 + return null;
489 + }
480 490
481 - function parse_snippet( $code, $new_snippet = false ){
482 - $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 + }
483 550
484 - if( !$this->snippet ){
485 - $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
486 - }
551 + return $code;
552 + }
487 553
488 - // First we check the function names are unique
489 - $fn = $this->snippet->sanitize_and_check_functions( $code, $new_snippet );
490 - if ( ! $fn['is_valid'] ) {
491 554
492 - $lint = [
493 - 'line' => 1,
494 - 'attributes' => $fn['attributes'][0],
495 - 'raw_message' => implode(', ', $fn['errors'][0]),
496 - 'message' => implode(', ', $fn['errors'][0]),
497 - ];
555 + /**
556 + * [STATIC] Execute active snippets.
557 + *
558 + * @return array
559 + */
560 + public function execute_active_snippets() {
498 561
499 - return $lint;
500 - }
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 + }
501 567
502 - try {
503 - $stmts = $parser->parse( $code );
504 - $result = $stmts;
505 - } catch ( PhpParser\Error $e ) {
568 + if ( empty( $this->snippet ) ) {
569 + $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
570 + }
506 571
507 - $lint = [
508 - 'line' => $e->getStartLine(),
509 - 'attributes' => $e->getAttributes(),
510 - 'raw_message' => $e->getRawMessage(),
511 - 'message' => $e->getMessage(),
512 - ];
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 + }
513 578
514 - return $lint;
515 - }
579 + $scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
580 + // Get all active snippets
516 581
517 - return null;
518 - }
582 +
519 583
520 - public function get_js_functions_to_push() {
521 - $functions = $this->snippet->get_functions();
522 - $js_functions = [];
523 - foreach ( $functions as &$function ) {
524 - if ( !isset( $function['target'] ) ) {
525 - $function['target'] = 'php';
526 - }
527 - if ( $function['target'] == 'js' ) {
528 - $js_functions[] = $function;
529 - }
530 - }
531 - $snippets = [];
532 - foreach ( $js_functions as $function ) {
533 - $snippet = $this->snippet->select_one( $function['snippetId'] );
534 - $snippet['function_info'] = $function; // Add function info to snippet
535 - $snippets[] = $snippet;
536 - }
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'];
537 593
538 - return $this->generate_js_functions_code( $snippets );
539 - }
540 -
541 - function generate_js_functions_code ($snippets ) {
542 - $code = "";
543 - foreach ( $snippets as $snippet ) {
544 - $function_code = $snippet['code'];
545 - $function_info = $snippet['function_info'];
546 594
547 - // Extract function name and arguments
548 - preg_match( '/(?:const|let|var)?\s*(\w+)\s*=\s*\((.*?)\)\s*=>/', $function_code, $matches );
549 - $function_name = $matches[1] ?? $function_info['name'];
550 - $function_args = $matches[2] ?? '';
551 -
552 - // Prepare default values
553 - $default_args = [];
554 - foreach ( $function_info['args'] as $arg ) {
555 - if ( isset( $arg['default'] ) && $arg['default'] !== '' ) {
556 - $default_args[$arg['name']] = $arg['default'];
595 + if ( empty( $snippets ) ) {
596 + return;
557 597 }
558 - }
559 598
560 - // Modify function to use default values
561 - if ( !empty( $default_args ) ) {
562 - $new_args = explode( ',', $function_args );
563 - foreach ( $new_args as &$arg ) {
564 - $arg = trim( $arg );
565 - if ( isset( $default_args[$arg] ) ) {
566 - $arg .= " = " . json_encode( $default_args[$arg] );
567 - }
568 - }
569 - $new_args_string = implode( ', ', $new_args );
570 - $function_code = preg_replace(
571 - '/(\w+)\s*=\s*\((.*?)\)\s*=>/',
572 - "$1 = ($new_args_string) =>",
573 - $function_code
574 - );
575 - }
599 + $snippets = array_map( function ( $snippet ) use ( $blocked ) {
600 + $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
601 + $snippet['blocked'] = $blocked;
576 602
577 - $code .= $function_code . "\n\n";
578 - }
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 + }
579 607
580 - return $code;
581 - }
608 + return $snippet;
609 + }, $snippets );
582 610
611 +
583 612
584 - /**
585 - * [STATIC] Execute active snippets.
586 - *
587 - * @return array
588 - */
589 - public function execute_active_snippets() {
590 -
591 - $blocked = false;
592 - $page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
593 -
594 -
595 - if ( $page === 'mwcode_settings' ) {
596 - // If we blocks global snippets like nonce_life filter, we would block the settings page so let's remove the block for this page
597 -
598 - $blocked = false;
599 - //$blocked = true;
613 + return $snippets;
600 614 }
601 - // Block REST requests that aren't whitelisted
602 - elseif ( MeowCommon_Helpers::is_rest() && !Meow_MWCODE_Core::is_white_listed_rest() ) {
603 - $blocked = true;
604 - }
605 615
606 - if ( empty( $this->snippet ) ) {
607 - $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
608 - }
609 616
610 - $ts = $this->get_option( 'thrown_snippet', null );
611 - if ( !empty( $ts ) ) {
612 - $this->log( "⚠️ Your snippet \"{$ts['name']}\" has thrown a fatal error last time, so we disabled it. Please check the logs for more information." );
613 - $this->snippet->force_disable( $ts['id'] );
614 - $this->update_option( 'thrown_snippet', null );
615 - }
617 + #endregion
616 618
617 - $scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
618 - // Get all active snippets
619 + #reion Shortcodes
619 620
620 - $snippets = $this->snippet->select(
621 - null, // offset
622 - -1, // limit
623 - [
624 - [ 'accessor' => 'active', 'value' => 1 ],
625 - [ 'accessor' => 'scope', 'value' => $scope ],
626 - ], // filter
627 - [ 'accessor' => 'priority', 'by' => 'DESC' ] // sort
628 - )['data'];
621 + function content_shortcode( $atts ) {
629 622
630 - if ( empty( $snippets ) ) {
631 - return;
632 - }
623 + $atts = shortcode_atts( array(
624 + 'id' => null,
625 + 'target' => null,
626 + 'code' => null,
627 + ), $atts );
633 628
634 - $snippets = array_map( function ( $snippet ) use ( $blocked ) {
635 - $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
636 - $snippet['blocked'] = $blocked;
629 + $id = $atts['id'];
630 + $target = $atts['target'];
631 + $code = $atts['code'];
637 632
638 - // If the snippet must be executed only in the frontend, we bypass the block
639 - if ( !is_admin() && $snippet['scope'] === 'frontend' ) {
640 - $snippet['blocked'] = false;
641 - }
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 ){
642 636
643 - return $snippet;
644 - }, $snippets );
637 + // Because the code from Blocks are sanitized, we need to replace the &quot; with "
638 + $code = str_replace( '&quot;', '"', $code );
645 639
646 - return $snippets;
647 - }
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 + }
648 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 + }
649 655
650 - #endregion
656 + $snippet = $this->get_snippet( $id );
651 657
652 - #region Shortcodes
658 + if ( empty( $snippet ) ) {
659 + return '<b>Code Engine:</b> The snippet does not exist.';
660 + }
653 661
654 - 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';
655 665
656 - $atts = shortcode_atts( array(
657 - 'id' => null,
658 - 'target' => null,
659 - 'code' => null,
660 - ), $atts );
666 + if ( !$is_content_php && !$is_content_js ) {
667 + return '<b>Code Engine:</b> The snippet is not a content snippet.';
668 + }
661 669
662 - $id = $atts['id'];
663 - $target = $atts['target'];
664 - $code = $atts['code'];
665 - $current_post = get_post();
666 -
667 - $no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
668 - $allow_php = $this->get_option( 'code_blocks', false );
669 - $allow_php_whitelist = $this->get_option( 'code_blocks_whitelist', [] );
670 -
671 - // If the ID is null, it means it comes from a Guttenberg block
672 - $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 + }
673 674
674 - if( $is_block ) {
675 + $output = '<b>Code Engine:</b> No output.';
675 676
676 - if( $target !== 'js' && $target !== 'php' ) {
677 - return '<b>Code Engine:</b> Please provide a valid target (js or php).';
678 - }
677 + if ( $is_content_js ) {
678 + $output = '<script>' . $snippet['code'] . '</script>';
679 + }
679 680
680 - if ( $no_js && $target === 'js' ) {
681 - return '<b>Code Engine:</b> Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
682 - }
681 + if ( $is_content_php ) {
682 + $output = $this->run_non_fn_snippet( $id );
683 + }
683 684
684 - if ( $target === 'php' ) {
685 + return $output;
686 + }
685 687
686 - if ( !$allow_php ) {
687 - 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.';
688 - }
688 + #endregion
689 689
690 - if ( !empty( $allow_php_whitelist ) && !in_array( $current_post->ID, $allow_php_whitelist ) ) {
691 - 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.';
692 - }
693 - }
690 + #region Logs
694 691
695 - // Because the code from Blocks are sanitized, we need to replace the &quot; with "
696 - $code = str_replace( '&quot;', '"', $code );
692 + function get_logs() {
693 + $log_file_path = $this->get_logs_path();
697 694
698 - if ( $target === 'js' ) {
699 - $output = '<script>' . $code . '</script>';
700 - }
695 + if ( !file_exists( $log_file_path ) ) {
696 + return "Empty log file.";
697 + }
701 698
702 - if ( $target === 'php' ) {
703 - $output = $this->run_non_fn_snippet( null, $code );
704 - }
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 + }
705 706
706 - return $output;
707 - }
707 + function clear_logs() {
708 + $logPath = $this->get_logs_path();
709 + if ( file_exists( $logPath ) ) {
710 + unlink( $logPath );
711 + }
708 712
709 - // If not a block, we get the snippet by ID
710 - // If the ID is not null, it means it comes from a shortcode
711 - if ( empty( $id ) && empty( $code ) ) {
712 - return '<b>Code Engine:</b> Please provide a snippet ID.';
713 - }
713 + $options = $this->get_all_options();
714 + $options['logs_path'] = null;
715 + $this->update_options( $options );
716 + }
714 717
715 - $snippet = $this->get_snippet( $id );
718 + function get_logs_path() {
719 + $uploads_dir = wp_upload_dir();
720 + $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
716 721
717 - if ( empty( $snippet ) ) {
718 - return '<b>Code Engine:</b> The snippet does not exist.';
719 - }
722 + $path = $this->get_option( 'logs_path' );
720 723
721 - //Check if the snippet scope is either content_php or content_js
722 - $is_content_php = $snippet['scope'] === 'content_php';
723 - $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 + }
724 732
725 - if ( !$is_content_php && !$is_content_js ) {
726 - return '<b>Code Engine:</b> The snippet is not a content snippet.';
727 - }
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 + }
728 742
729 - if( $no_js && $is_content_js ) {
730 - return '<b>Code Engine:</b> Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
731 - }
743 + return $path;
744 + }
732 745
733 - //Check if the snippet is active
734 - if ( !$snippet['active'] ) {
735 - return '<b>Code Engine:</b> The snippet is not active.';
736 - }
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 + }
737 762
738 - $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 = '';
739 767
740 - if ( $is_content_js ) {
741 - $output = '<script>' . $snippet['code'] . '</script>';
742 - }
768 + for ( $i = 0; $i < $length; $i++ ) {
769 + $random_string .= $characters[rand(0, $characters_length - 1)];
770 + }
743 771
744 - if ( $is_content_php ) {
745 - $output = $this->run_non_fn_snippet( $id );
746 - }
772 + return $random_string;
773 + }
747 774
748 - return $output;
749 - }
775 + #endregion
750 776
751 - #endregion
777 + #region Helpers
752 778
753 - #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 + );
754 796
755 - function get_logs() {
756 - $log_file_path = $this->get_logs_path();
797 + $white_listed = apply_filters( 'meow_mwcode_white_listed_rest', $white_listed );
757 798
758 - if ( !file_exists( $log_file_path ) ) {
759 - return "Empty log file.";
760 - }
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;
761 818
762 - $content = file_get_contents( $log_file_path );
763 - $lines = explode( "\n", $content );
764 - $lines = array_filter( $lines );
765 - $lines = array_reverse( $lines );
766 - $content = implode( "\n", $lines );
767 - return $content;
768 - }
819 + $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
820 + return $authorized;
821 + }
769 822
770 - function clear_logs() {
771 - $logPath = $this->get_logs_path();
772 - if ( file_exists( $logPath ) ) {
773 - unlink( $logPath );
774 - }
775 823
776 - $options = $this->get_all_options();
777 - $options['logs_path'] = null;
778 - $this->update_options( $options );
779 - }
824 + }
780 825
781 - function get_logs_path() {
782 - $uploads_dir = wp_upload_dir();
783 - $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
784 -
785 - $path = $this->get_option( 'logs_path' );
786 -
787 - if ( $path && file_exists( $path ) ) {
788 - // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
789 - if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
790 - $path = null;
791 - } else {
792 - return $path;
793 - }
826 + $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
827 + return $authorized;
794 828 }
795 829
796 - if ( !$path ) {
797 - $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
798 - if ( !file_exists( $path ) ) {
799 - touch( $path );
800 - }
801 - $options = $this->get_all_options();
802 - $options['logs_path'] = $path;
803 - $this->update_options( $options );
804 - }
805 -
806 - return $path;
807 - }
808 -
809 - function log( $data = null ) {
810 - if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
811 - $log_file_path = $this->get_logs_path();
812 - $fh = @fopen( $log_file_path, 'a' );
813 - if ( !$fh ) { return false; }
814 - $date = date( "Y-m-d H:i:s" );
815 - if ( is_null( $data ) ) {
816 - fwrite( $fh, "\n" );
817 - }
818 - else {
819 - fwrite( $fh, "$date: {$data}\n" );
820 - //$this->log( "[MWCODE] $data" );
821 - }
822 - fclose( $fh );
823 - return true;
824 - }
825 -
826 - private function random_ascii_chars( $length = 8 ) {
827 - $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
828 - $characters_length = count( $characters );
829 - $random_string = '';
830 -
831 - for ( $i = 0; $i < $length; $i++ ) {
832 - $random_string .= $characters[rand(0, $characters_length - 1)];
833 - }
834 -
835 - return $random_string;
836 - }
837 -
838 - #endregion
839 -
840 - #region Helpers
841 -
842 - /**
843 - * Check if the request is from a white-listed REST route.
844 - *
845 - * @return bool
846 - */
847 - public static function is_white_listed_rest() {
848 - $options = get_option( 'mwcode_snippet_vault_options', array() );
849 -
850 - // Early return if bypass is enabled
851 - if ( !empty( $options['bypass_rest_security'] ) ) {
852 - return true;
853 - }
854 -
855 - // Early return for admin requests
856 - if ( is_admin() ) {
857 - return apply_filters( 'mwcode_rest_authorized', true, null );
858 - }
859 -
860 - // Get the requested route
861 - $requested_route = self::get_requested_rest_route();
862 - if ( !$requested_route ) {
863 - return apply_filters( 'mwcode_rest_authorized', false, null );
864 - }
865 -
866 - // Check against whitelist
867 - $white_listed = apply_filters( 'mwcode_rest_whitelist', array(
868 - 'mwai/v1',
869 - 'mwai-ui/v1',
870 - 'media-file-renamer/v1',
871 - 'media-cleaner/v1',
872 - 'wplr/v1',
873 - 'code-engine/v1',
874 - 'wp/v2',
875 - 'meow-gallery/v1',
876 - 'mcp/v1',
877 - ));
878 -
879 - $authorized = self::is_route_whitelisted( $requested_route, $white_listed );
880 -
881 - // Log if debug mode is enabled
882 - if ( !empty( $options['server_debug_mode'] ) ) {
883 - self::log_route_status( $requested_route, $authorized );
884 - }
885 -
886 - return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
887 - }
888 -
889 - /**
890 - * Extract the REST route from the request URI.
891 - *
892 - * @return string|null
893 - */
894 - public static function get_requested_rest_route() {
895 - if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
896 - return null;
897 - }
898 -
899 - $route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
900 -
901 - if ( isset( $route_parts[1] ) ) {
902 - return trim( $route_parts[1], '/' );
903 - }
904 -
905 - return null;
906 - }
907 -
908 - /**
909 - * Check if a route is in the whitelist.
910 - *
911 - * @param string $route The route to check
912 - * @param array $white_listed The whitelist array
913 - * @return bool
914 - */
915 - private static function is_route_whitelisted( $route, $white_listed ) {
916 - foreach ( $white_listed as $white_listed_route ) {
917 - if ( strpos( $route, $white_listed_route ) === 0 ) {
918 - return true;
919 - }
920 - }
921 - return false;
922 - }
923 -
924 - /**
925 - * Log the route authorization status.
926 - *
927 - * @param string $route The route being checked
928 - * @param bool $authorized Whether the route is authorized
929 - */
930 - private static function log_route_status( $route, $authorized ) {
931 - global $mwcode_core;
932 -
933 - $message = $authorized
934 - ? "✅ REST route authorized: " . $route
935 - : "❌ REST route rejected (not whitelisted): " . $route;
936 -
937 - if ( isset( $mwcode_core ) ) {
938 - $mwcode_core->log( $message );
939 - } else {
940 - error_log( "[Code Engine] " . $message );
941 - }
942 - }
943 -
944 - #endregion
830 + #endregion
945 831 }
946 832
947 833 ?>