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