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