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 -755 0.3.30.3.0 View file →
@@ -6,915 +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" => false,
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'];
652 -
653 - $no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
654 -
655 - // If the ID is null, it means it comes from a Guttenberg block
656 - $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 + }
657 674
658 - if( $is_block ) {
675 + $output = '<b>Code Engine:</b> No output.';
659 676
660 - if( $target !== 'js' && $target !== 'php' ) {
661 - return '<b>Code Engine:</b> Please provide a valid target (js or php).';
662 - }
677 + if ( $is_content_js ) {
678 + $output = '<script>' . $snippet['code'] . '</script>';
679 + }
663 680
664 - if ( $no_js && $target === 'js' ) {
665 - return '<b>Code Engine:</b> Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
666 - }
681 + if ( $is_content_php ) {
682 + $output = $this->run_non_fn_snippet( $id );
683 + }
667 684
668 - // Because the code from Blocks are sanitized, we need to replace the &quot; with "
669 - $code = str_replace( '&quot;', '"', $code );
685 + return $output;
686 + }
670 687
671 - if ( $target === 'js' ) {
672 - $output = '<script>' . $code . '</script>';
673 - }
688 + #endregion
674 689
675 - if ( $target === 'php' ) {
676 - $output = $this->run_non_fn_snippet( null, $code );
677 - }
690 + #region Logs
678 691
679 - return $output;
680 - }
692 + function get_logs() {
693 + $log_file_path = $this->get_logs_path();
681 694
682 - // If not a block, we get the snippet by ID
683 - // If the ID is not null, it means it comes from a shortcode
684 - if ( empty( $id ) && empty( $code ) ) {
685 - return '<b>Code Engine:</b> Please provide a snippet ID.';
686 - }
695 + if ( !file_exists( $log_file_path ) ) {
696 + return "Empty log file.";
697 + }
687 698
688 - $snippet = $this->get_snippet( $id );
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 + }
689 706
690 - if ( empty( $snippet ) ) {
691 - return '<b>Code Engine:</b> The snippet does not exist.';
692 - }
707 + function clear_logs() {
708 + $logPath = $this->get_logs_path();
709 + if ( file_exists( $logPath ) ) {
710 + unlink( $logPath );
711 + }
693 712
694 - //Check if the snippet scope is either content_php or content_js
695 - $is_content_php = $snippet['scope'] === 'content_php';
696 - $is_content_js = $snippet['scope'] === 'content_js';
713 + $options = $this->get_all_options();
714 + $options['logs_path'] = null;
715 + $this->update_options( $options );
716 + }
697 717
698 - if ( !$is_content_php && !$is_content_js ) {
699 - return '<b>Code Engine:</b> The snippet is not a content snippet.';
700 - }
718 + function get_logs_path() {
719 + $uploads_dir = wp_upload_dir();
720 + $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
701 721
702 - if( $no_js && $is_content_js ) {
703 - return '<b>Code Engine:</b> Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
704 - }
722 + $path = $this->get_option( 'logs_path' );
705 723
706 - //Check if the snippet is active
707 - if ( !$snippet['active'] ) {
708 - return '<b>Code Engine:</b> The snippet is not active.';
709 - }
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 + }
710 732
711 - $output = '<b>Code Engine:</b> No output.';
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 + }
712 742
713 - if ( $is_content_js ) {
714 - $output = '<script>' . $snippet['code'] . '</script>';
715 - }
743 + return $path;
744 + }
716 745
717 - if ( $is_content_php ) {
718 - $output = $this->run_non_fn_snippet( $id );
719 - }
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 + }
720 762
721 - return $output;
722 - }
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 = '';
723 767
724 - #endregion
768 + for ( $i = 0; $i < $length; $i++ ) {
769 + $random_string .= $characters[rand(0, $characters_length - 1)];
770 + }
725 771
726 - #region Logs
772 + return $random_string;
773 + }
727 774
728 - function get_logs() {
729 - $log_file_path = $this->get_logs_path();
775 + #endregion
730 776
731 - if ( !file_exists( $log_file_path ) ) {
732 - return "Empty log file.";
733 - }
777 + #region Helpers
734 778
735 - $content = file_get_contents( $log_file_path );
736 - $lines = explode( "\n", $content );
737 - $lines = array_filter( $lines );
738 - $lines = array_reverse( $lines );
739 - $content = implode( "\n", $lines );
740 - return $content;
741 - }
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 + );
742 796
743 - function clear_logs() {
744 - $logPath = $this->get_logs_path();
745 - if ( file_exists( $logPath ) ) {
746 - unlink( $logPath );
747 - }
797 + $white_listed = apply_filters( 'meow_mwcode_white_listed_rest', $white_listed );
748 798
749 - $options = $this->get_all_options();
750 - $options['logs_path'] = null;
751 - $this->update_options( $options );
752 - }
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;
753 818
754 - function get_logs_path() {
755 - $uploads_dir = wp_upload_dir();
756 - $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
819 + $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
820 + return $authorized;
821 + }
757 822
758 - $path = $this->get_option( 'logs_path' );
759 823
760 - if ( $path && file_exists( $path ) ) {
761 - // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
762 - if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
763 - $path = null;
764 - } else {
765 - return $path;
766 - }
767 - }
824 + }
768 825
769 - if ( !$path ) {
770 - $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
771 - if ( !file_exists( $path ) ) {
772 - touch( $path );
773 - }
774 - $options = $this->get_all_options();
775 - $options['logs_path'] = $path;
776 - $this->update_options( $options );
826 + $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
827 + return $authorized;
777 828 }
778 829
779 - return $path;
780 - }
781 -
782 - function log( $data = null ) {
783 - if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
784 - $log_file_path = $this->get_logs_path();
785 - $fh = @fopen( $log_file_path, 'a' );
786 - if ( !$fh ) { return false; }
787 - $date = date( "Y-m-d H:i:s" );
788 - if ( is_null( $data ) ) {
789 - fwrite( $fh, "\n" );
790 - }
791 - else {
792 - fwrite( $fh, "$date: {$data}\n" );
793 - //$this->log( "[MWCODE] $data" );
794 - }
795 - fclose( $fh );
796 - return true;
797 - }
798 -
799 - private function random_ascii_chars( $length = 8 ) {
800 - $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
801 - $characters_length = count( $characters );
802 - $random_string = '';
803 -
804 - for ( $i = 0; $i < $length; $i++ ) {
805 - $random_string .= $characters[rand(0, $characters_length - 1)];
806 - }
807 -
808 - return $random_string;
809 - }
810 -
811 - #endregion
812 -
813 - #region Helpers
814 -
815 - /**
816 - * Check if the request is from a white-listed REST route.
817 - *
818 - * @return bool
819 - */
820 - public static function is_white_listed_rest() {
821 - $options = get_option( 'mwcode_snippet_vault_options', array() );
822 -
823 - // Early return if bypass is enabled
824 - if ( !empty( $options['bypass_rest_security'] ) ) {
825 - return true;
826 - }
827 -
828 - // Early return for admin requests
829 - if ( is_admin() ) {
830 - return apply_filters( 'mwcode_rest_authorized', true, null );
831 - }
832 -
833 - // Get the requested route
834 - $requested_route = self::get_requested_rest_route();
835 - if ( !$requested_route ) {
836 - return apply_filters( 'mwcode_rest_authorized', false, null );
837 - }
838 -
839 - // Check against whitelist
840 - $white_listed = apply_filters( 'mwcode_rest_whitelist', array(
841 - 'mwai/v1',
842 - 'mwai-ui/v1',
843 - 'media-file-renamer/v1',
844 - 'media-cleaner/v1',
845 - 'wplr/v1',
846 - 'code-engine/v1',
847 - 'wp/v2',
848 - 'meow-gallery/v1',
849 - 'mcp/v1',
850 - ));
851 -
852 - $authorized = self::is_route_whitelisted( $requested_route, $white_listed );
853 -
854 - // Log if debug mode is enabled
855 - if ( !empty( $options['server_debug_mode'] ) ) {
856 - self::log_route_status( $requested_route, $authorized );
857 - }
858 -
859 - return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
860 - }
861 -
862 - /**
863 - * Extract the REST route from the request URI.
864 - *
865 - * @return string|null
866 - */
867 - public static function get_requested_rest_route() {
868 - if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
869 - return null;
870 - }
871 -
872 - $route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
873 -
874 - if ( isset( $route_parts[1] ) ) {
875 - return trim( $route_parts[1], '/' );
876 - }
877 -
878 - return null;
879 - }
880 -
881 - /**
882 - * Check if a route is in the whitelist.
883 - *
884 - * @param string $route The route to check
885 - * @param array $white_listed The whitelist array
886 - * @return bool
887 - */
888 - private static function is_route_whitelisted( $route, $white_listed ) {
889 - foreach ( $white_listed as $white_listed_route ) {
890 - if ( strpos( $route, $white_listed_route ) === 0 ) {
891 - return true;
892 - }
893 - }
894 - return false;
895 - }
896 -
897 - /**
898 - * Log the route authorization status.
899 - *
900 - * @param string $route The route being checked
901 - * @param bool $authorized Whether the route is authorized
902 - */
903 - private static function log_route_status( $route, $authorized ) {
904 - global $mwcode_core;
905 -
906 - $message = $authorized
907 - ? "✅ REST route authorized: " . $route
908 - : "❌ REST route rejected (not whitelisted): " . $route;
909 -
910 - if ( isset( $mwcode_core ) ) {
911 - $mwcode_core->log( $message );
912 - } else {
913 - error_log( "[Code Engine] " . $message );
914 - }
915 - }
916 -
917 - #endregion
830 + #endregion
918 831 }
919 832
920 833 ?>