PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.2.9
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.2.9
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
code-engine / classes / core.php

core.php in Code Engine – PHP Snippets, AI Functions & Automation for WordPress 0.2.9, at classes/core.php

761 lines 20.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once ( MWCODE_PATH . '/vendor/autoload.php' );
4 use PhpParser\ParserFactory;
5 use PhpParser\NodeDumper;
6 use PhpParser\Error;
7
8 class Meow_MWCODE_Core
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
17 private $option_name = 'mwcode_options';
18
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;
29
30 // Create API before plugins_loaded
31 $this->mwcode = new Meow_MWCODE_API( $this, $snippet );
32 $mwcode = $this->mwcode;
33
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 }
39
40 function init() {
41 // Part of the core, settings and stuff
42 $this->admin = new Meow_MWCODE_Admin( $this );
43
44 // Only for REST
45 if ( $this->is_rest ) {
46 new Meow_MWCODE_Rest( $this, $this->admin, $this->snippet );
47 }
48 }
49
50
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 }
60
61 public function can_access_features() {
62 return apply_filters( 'mwcode_allow_usage', current_user_can( 'administrator' ) );
63 }
64
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
70
71 #region Options
72
73 function get_option( $option, $default = null ) {
74 $options = $this->get_all_options();
75 return $options[$option] ?? $default;
76 }
77
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,
86
87 //UI
88 "ui_show_preview" => true,
89
90 //AI
91 "ai_suggestions" => false,
92 "ai_engine_status"=> false,
93 "ai_engine_message" => "",
94
95 //API
96 "api_endpoint" => false,
97 "api_token" => md5( time() . rand() ),
98 ];
99 }
100
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 }
107
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 }
122
123 function update_option( $option, $value ) {
124 $options = $this->get_all_options();
125 $options[$option] = $value;
126 return $this->update_options( $options );
127 }
128
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 }
135
136 // Validate and keep the options clean and logical.
137 function sanitize_options( $options ) {
138 $options_modified = false;
139
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 }
145
146 // Update AI Engine status
147 $options_modified = $this->updateAIEngineStatus( $options ) || $options_modified;
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 }
154
155 if ( $options_modified ) {
156 update_option( $this->option_name, $options, false );
157 }
158
159 return $options;
160 }
161
162 private function updateAIEngineStatus( &$options ) {
163 global $mwai;
164
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 }
170
171 try {
172 $status = $mwai->checkStatus();
173
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 }
186
187 return false;
188 }
189
190 // #endregion
191
192 #region Snippets
193
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 );
206 }
207
208 private function sanitize_arg( $name, $value ) {
209 if ( $name[0] !== '$' ) { $name = '$' . $name; }
210
211 if ( !empty( $value ) && !is_numeric( $value ) && $value[0] !== '"' && $value[strlen( $value ) - 1] !== '"' ) {
212 $value = '"' . esc_sql( $value ) . '"';
213 }
214
215 return [ $name, $value ];
216 }
217
218 function run_non_fn_snippet( $id, $code = null, $test = false ) {
219 // Retrieve the snippet code from the provided code or via the snippet ID.
220 if ( $code ) {
221 $snippet = [ 'code' => $code ];
222 } else {
223 $snippet = $this->get_snippet( $id );
224 }
225
226 // Remove any PHP opening tag.
227 $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
228
229
230 if ( $test ) {
231 $snippet['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $snippet['code'] );
232 }
233
234 $error = null;
235 $output = null;
236
237 try {
238 ob_start();
239 eval( $snippet['code'] );
240 $output = ob_get_clean();
241 } catch ( Throwable $e ) {
242 $error = new Exception( 'Error executing the snippet, ' . $e->getMessage() );
243 ob_clean();
244 } finally {
245 restore_error_handler();
246 }
247
248 // If in test mode, return output as an array of lines with an 'error' key if needed.
249 if ( $test ) {
250 $output = explode( "\n", trim( $output ) );
251 if ( $error !== null ) {
252 $output['error'] = $error->getMessage();
253 }
254 } else {
255 if ( $error !== null ) {
256 throw $error;
257 }
258 }
259
260 return $output;
261 }
262
263 function run_snippet( $id, $args = [], $params = [] )
264 {
265 // Static array to track defined functions
266 static $defined_functions = array();
267
268 if ( $id ) { // If there is an ID, we get the snippet, if not we get the data from the params
269 $snippet = $this->get_snippet( $id );
270 $this->snippet->get_function_snippets_data( $snippet ); // adds the function data to the snippet
271
272 $params = [ // We set the params according to the snippet we fetched
273 'test' => false, // If we pass an ID to the function, we are not testing the snippet
274 // 'test' => $params['test'] ?? false if needed we can still use ID and test at the same time (should not happen)
275 'code' => $snippet['code'],
276 'name' => $snippet['functionName'],
277 'args' => $snippet['functionArgs'],
278 'values' => $snippet['functionArgsDict'] // Contains the default values of the arguments
279 ];
280 }
281
282 // Sanitize all the arguments if the option is enabled
283 if ( $this->get_option( 'sanitize_arguments', true ) ) {
284
285 if ( $args ) {
286 foreach ( $args as $name => $value ) {
287 list( $sanitizedName, $sanitizedValue ) = $this->sanitize_arg( $name, $value );
288 unset( $args[$name] );
289
290 $args[$sanitizedName] = $sanitizedValue;
291 }
292 }
293
294 foreach ( $params['values'] as $name => $value ) {
295
296 if( array_key_exists( 'input', $value) ) {
297 list( $sanitizedInputName, $sanitizedInputValue ) = $this->sanitize_arg( $name, $value['input'] );
298 $params['values'][$sanitizedInputName]['input'] = $sanitizedInputValue;
299 }
300
301 if( array_key_exists( 'default', $value) ) {
302 list( $sanitizedDefaultValueName, $sanitizedDefaultValue ) = $this->sanitize_arg( $name, $value['default'] );
303 $params['values'][$sanitizedDefaultValueName]['default'] = $sanitizedDefaultValue;
304 }
305 }
306
307 }
308
309
310
311 // Make sure the function is existing and is the one in the snippet
312 if ( empty( $params['code'] ) ) {
313 throw new Exception( 'Code Engine: The snippet code appears to be empty.' );
314 }
315
316 if ( empty( $params['name'] ) || ! str_contains( $params['code'], $params['name'] ) ) {
317 throw new Exception( "Code Engine: Function name does not match. The name should be {$params['name']}." );
318 }
319
320 // Overwrite the default values with the provided ones
321 if ( $args ) {
322 foreach ( $args as $name => $value ) {
323 $params['values'][$name]['input'] = $value;
324 }
325
326 $this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
327 }
328
329 // Check if the function has already been defined
330 if ( !in_array( $params['name'], $defined_functions ) ) {
331
332 // If not, proceed with modification and definition
333 if ( $params['test'] ) { // Make sure the echo statement uses a line break
334 $params['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $params['code'] );
335 } else { // Remove all echo statements
336 $params['code'] = preg_replace( '/echo\s+(.+?);/s', '', $params['code'] );
337 }
338
339 $params['code'] = "if (!function_exists('{$params['name']}')) {\n" . $params['code'] . "\n}\n";
340
341 // Add the function name to the array to avoid redefinition
342 $defined_functions[] = $params['name'];
343 } else {
344 // If already defined, just prepare to call the function without redefining it
345 $params['code'] = '';
346 }
347
348 // Prepare the code to be executed
349 $params['code'] .= "\n\$mwcode_result = {$params['name']}(";
350 foreach ( $params['args'] as $index => $arg ) {
351 $value = 'null'; // In case the argument is not provided it will be null
352
353 if ( array_key_exists( $arg, $params['values'] ) ) { // Avoid warnings if the argument is not provided
354
355 // If the argument is provided, use it, if not use the default value
356 if ( !empty( $params['values'][$arg]['input'] ) ) {
357 $value = $params['values'][$arg]['input'];
358
359 } else if ( !empty( $params['values'][$arg]['default'] ) ) {
360 $value = $params['values'][$arg]['default'];
361 }
362 }
363
364 $params['code'] .= "{$value}";
365 if ( $index < count( $params['args'] ) - 1 ) {
366 $params['code'] .= ', ';
367 }
368 }
369 $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
370
371 $error = null;
372 $output = null;
373
374 try {
375 ob_start();
376 eval( $params['code'] );
377 $output = ob_get_clean();
378
379 if ( $params['test'] ){
380 $output = explode( "\n", $output );
381 }
382
383 } catch ( Throwable $e ) {
384 //$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
385 $error = new Exception(' Error executing the function, ' . $e->getMessage());
386
387 ob_clean();
388 } finally {
389 restore_error_handler();
390 }
391
392 if ( $error !== null ) {
393 if( $params['test'] ){
394 $output['error'] = $error->getMessage();
395 } else {
396 throw $error;
397 }
398 }
399
400 return $output;
401 }
402
403
404 function parse_snippet( $code, $new_snippet = false ){
405 $parser = ( new ParserFactory( ) )->createForNewestSupportedVersion( );
406
407 if( !$this->snippet ){
408 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
409 }
410
411 // First we check the function names are unique
412 $fn = $this->snippet->sanitize_and_check_functions( $code, $new_snippet );
413 if ( ! $fn['is_valid'] ) {
414
415 $lint = [
416 'line' => 1,
417 'attributes' => $fn['attributes'][0],
418 'raw_message' => implode(', ', $fn['errors'][0]),
419 'message' => implode(', ', $fn['errors'][0]),
420 ];
421
422 return $lint;
423 }
424
425 try {
426 $stmts = $parser->parse( $code );
427 $result = $stmts;
428 } catch ( PhpParser\Error $e ) {
429
430 $lint = [
431 'line' => $e->getStartLine(),
432 'attributes' => $e->getAttributes(),
433 'raw_message' => $e->getRawMessage(),
434 'message' => $e->getMessage(),
435 ];
436
437 return $lint;
438 }
439
440 return null;
441 }
442
443 public function get_js_functions_to_push() {
444 $functions = $this->snippet->get_functions();
445 $js_functions = [];
446 foreach ( $functions as &$function ) {
447 if ( !isset( $function['target'] ) ) {
448 $function['target'] = 'php';
449 }
450 if ( $function['target'] == 'js' ) {
451 $js_functions[] = $function;
452 }
453 }
454 $snippets = [];
455 foreach ( $js_functions as $function ) {
456 $snippet = $this->snippet->select_one( $function['snippetId'] );
457 $snippet['function_info'] = $function; // Add function info to snippet
458 $snippets[] = $snippet;
459 }
460
461 return $this->generate_js_functions_code( $snippets );
462 }
463
464 function generate_js_functions_code ($snippets ) {
465 $code = "";
466 foreach ( $snippets as $snippet ) {
467 $function_code = $snippet['code'];
468 $function_info = $snippet['function_info'];
469
470 // Extract function name and arguments
471 preg_match( '/(?:const|let|var)?\s*(\w+)\s*=\s*\((.*?)\)\s*=>/', $function_code, $matches );
472 $function_name = $matches[1] ?? $function_info['name'];
473 $function_args = $matches[2] ?? '';
474
475 // Prepare default values
476 $default_args = [];
477 foreach ( $function_info['args'] as $arg ) {
478 if ( isset( $arg['default'] ) && $arg['default'] !== '' ) {
479 $default_args[$arg['name']] = $arg['default'];
480 }
481 }
482
483 // Modify function to use default values
484 if ( !empty( $default_args ) ) {
485 $new_args = explode( ',', $function_args );
486 foreach ( $new_args as &$arg ) {
487 $arg = trim( $arg );
488 if ( isset( $default_args[$arg] ) ) {
489 $arg .= " = " . json_encode( $default_args[$arg] );
490 }
491 }
492 $new_args_string = implode( ', ', $new_args );
493 $function_code = preg_replace(
494 '/(\w+)\s*=\s*\((.*?)\)\s*=>/',
495 "$1 = ($new_args_string) =>",
496 $function_code
497 );
498 }
499
500 $code .= $function_code . "\n\n";
501 }
502
503 return $code;
504 }
505
506
507 /**
508 * [STATIC] Execute active snippets.
509 *
510 * @return array
511 */
512 public function execute_active_snippets() {
513
514 $blocked = false;
515 $page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
516 if ( $page === 'mwcode_settings' || !Meow_MWCODE_Core::is_white_listed_rest() ) {
517 $blocked = true;
518 }
519
520 if ( empty( $this->snippet ) ) {
521 $this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
522 }
523
524 $ts = $this->get_option( 'thrown_snippet', null );
525 if ( !empty( $ts ) ) {
526 $this->log( "⚠️ Your snippet \"{$ts['name']}\" has thrown a fatal error last time, so we disabled it. Please check the logs for more information." );
527 $this->snippet->force_disable( $ts['id'] );
528 $this->update_option( 'thrown_snippet', null );
529 }
530
531 $scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
532 // Get all active snippets
533
534
535
536 $snippets = $this->snippet->select(
537 null, // offset
538 -1, // limit
539 [
540 [ 'accessor' => 'active', 'value' => 1 ],
541 [ 'accessor' => 'scope', 'value' => $scope ],
542 ], // filter
543 [ 'accessor' => 'priority', 'by' => 'DESC' ] // sort
544 )['data'];
545
546
547 if ( empty( $snippets ) ) {
548 return;
549 }
550
551 $snippets = array_map( function ( $snippet ) use ( $blocked ) {
552 $snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
553 $snippet['blocked'] = $blocked;
554
555 // If the snippet must be executed only in the frontend, we bypass the block
556 if ( !is_admin() && $snippet['scope'] === 'frontend' ) {
557 $snippet['blocked'] = false;
558 }
559
560 return $snippet;
561 }, $snippets );
562
563
564
565 return $snippets;
566 }
567
568
569 #endregion
570
571 #reion Shortcodes
572
573 function content_shortcode( $atts ) {
574
575 $atts = shortcode_atts( array(
576 'id' => null,
577 ), $atts );
578
579 $id = $atts['id'];
580 if ( empty( $id ) ) {
581 return '<b>Code Engine:</b> Please provide a snippet ID.';
582 }
583
584 $snippet = $this->get_snippet( $id );
585
586 if ( empty( $snippet ) ) {
587 return '<b>Code Engine:</b> The snippet does not exist.';
588 }
589
590 //Check if the snippet scope is either content_php or content_js
591 $is_content_php = $snippet['scope'] === 'content_php';
592 $is_content_js = $snippet['scope'] === 'content_js';
593
594 if ( !$is_content_php && !$is_content_js ) {
595 return '<b>Code Engine:</b> The snippet is not a content snippet.';
596 }
597
598 //Check if the snippet is active
599 if ( !$snippet['active'] ) {
600 return '<b>Code Engine:</b> The snippet is not active.';
601 }
602
603 $output = '<b>Code Engine:</b> No output.';
604
605 if ( $is_content_js ) {
606 $output = '<script>' . $snippet['code'] . '</script>';
607 }
608
609 if ( $is_content_php ) {
610 $output = $this->run_non_fn_snippet( $id );
611 }
612
613 return $output;
614 }
615
616 #endregion
617
618 #region Logs
619
620 function get_logs() {
621 $log_file_path = $this->get_logs_path();
622
623 if ( !file_exists( $log_file_path ) ) {
624 return "Empty log file.";
625 }
626
627 $content = file_get_contents( $log_file_path );
628 $lines = explode( "\n", $content );
629 $lines = array_filter( $lines );
630 $lines = array_reverse( $lines );
631 $content = implode( "\n", $lines );
632 return $content;
633 }
634
635 function clear_logs() {
636 $logPath = $this->get_logs_path();
637 if ( file_exists( $logPath ) ) {
638 unlink( $logPath );
639 }
640
641 $options = $this->get_all_options();
642 $options['logs_path'] = null;
643 $this->update_options( $options );
644 }
645
646 function get_logs_path() {
647 $uploads_dir = wp_upload_dir();
648 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
649
650 $path = $this->get_option( 'logs_path' );
651
652 if ( $path && file_exists( $path ) ) {
653 // make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
654 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
655 $path = null;
656 } else {
657 return $path;
658 }
659 }
660
661 if ( !$path ) {
662 $path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
663 if ( !file_exists( $path ) ) {
664 touch( $path );
665 }
666 $options = $this->get_all_options();
667 $options['logs_path'] = $path;
668 $this->update_options( $options );
669 }
670
671 return $path;
672 }
673
674 function log( $data = null ) {
675 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
676 $log_file_path = $this->get_logs_path();
677 $fh = @fopen( $log_file_path, 'a' );
678 if ( !$fh ) { return false; }
679 $date = date( "Y-m-d H:i:s" );
680 if ( is_null( $data ) ) {
681 fwrite( $fh, "\n" );
682 }
683 else {
684 fwrite( $fh, "$date: {$data}\n" );
685 //$this->log( "[MWCODE] $data" );
686 }
687 fclose( $fh );
688 return true;
689 }
690
691 private function random_ascii_chars( $length = 8 ) {
692 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
693 $characters_length = count( $characters );
694 $random_string = '';
695
696 for ( $i = 0; $i < $length; $i++ ) {
697 $random_string .= $characters[rand(0, $characters_length - 1)];
698 }
699
700 return $random_string;
701 }
702
703 #endregion
704
705 #region Helpers
706
707 /**
708 * Check if the request is from a white-listed REST route.
709 *
710 * @return bool
711 */
712 public static function is_white_listed_rest() {
713 $authorized = false;
714 $white_listed = array(
715 'mwai/v1',
716 'mwai-ui/v1',
717 'media-file-renamer/v1',
718 'media-cleaner/v1',
719 'wplr/v1',
720 'code-engine/v1',
721 'wp/v2',
722 'meow-gallery/v1',
723 );
724
725 $white_listed = apply_filters( 'meow_mwcode_white_listed_rest', $white_listed );
726
727 $route = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : null;
728 $requested_route = null;
729
730 if ( $route ) {
731 $route_parts = explode( '/wp-json/', $route );
732
733 if ( isset( $route_parts[1] ) ) {
734 $requested_route = trim( $route_parts[1], '/' );
735 foreach ( $white_listed as $white_listed_route ) {
736 if ( strpos( $requested_route, $white_listed_route ) === 0 ) {
737 $authorized = true;
738 $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
739 return $authorized;
740 }
741 }
742 }
743
744 if ( is_admin() ) {
745 $authorized = true;
746
747 $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
748 return $authorized;
749 }
750
751
752 }
753
754 $authorized = apply_filters( 'meow_mwcode_white_listed_rest_authorized', $authorized, $requested_route );
755 return $authorized;
756 }
757
758 #endregion
759 }
760
761 ?>