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