site_url = get_site_url();
$this->is_rest = MeowCommon_Helpers::is_rest();
$this->is_cli = defined( 'WP_CLI' ) && WP_CLI;
// Snippets
$snippet = new Meow_MWCODE_Modules_Snippet( $this );
$this->snippet = $snippet;
// Create API before plugins_loaded
$this->mwcode = new Meow_MWCODE_API( $this, $snippet );
$mwcode = $this->mwcode;
// Add the shortcode for the "content" snippets
add_shortcode( 'code-engine', [ $this, 'content_shortcode' ] );
add_action( 'plugins_loaded', array( $this, 'init' ) );
}
function init() {
// Part of the core, settings and stuff
$this->admin = new Meow_MWCODE_Admin( $this );
// Only for REST
if ( $this->is_rest ) {
new Meow_MWCODE_Rest( $this, $this->admin, $this->snippet );
}
// MCP integration - check both class and global variable
if ( class_exists( 'Meow_MWAI_Core' ) || isset( $GLOBALS['mwai'] ) ) {
new Meow_MWCODE_MCP( $this );
}
}
/**
*
* Roles & Access Rights
*
*/
#region Roles & Access Rights
public function can_access_settings() {
return apply_filters( 'mwcode_allow_setup', current_user_can( 'manage_options' ) );
}
public function can_access_features() {
return apply_filters( 'mwcode_allow_usage', current_user_can( 'administrator' ) );
}
public function check_rest_nonce( $request ) {
$nonce = $request->get_header( 'X-WP-Nonce' );
return wp_verify_nonce( $nonce, 'wp_rest' );
}
#endregion
#region Options
function get_option( $option, $default = null ) {
$options = $this->get_all_options();
return $options[$option] ?? $default;
}
function list_options() {
return [
//Safemode
"safe_mode_status" => "on", // on, off, whitelist
"safe_mode_whitelist" => [],
"disallow_block_php" => true, // Do not allow PHP code to be execute through Blocks "code" parameter
//LOGS
"server_debug_mode" => false,
//UI
"ui_show_preview" => false,
//AI
"ai_suggestions" => false,
"ai_engine_status"=> false,
"ai_engine_message" => "",
//API
"api_endpoint" => false,
"api_token" => md5( time() . rand() ),
//MCP
"mcp_support" => false,
];
}
function get_all_options( ) {
$options = get_option( $this->option_name, [] );
$defaults = $this->list_options();
// Merge with defaults to ensure all options exist
$options = array_merge( $defaults, $options );
$options = $this->sanitize_options( $options );
return $options;
}
function update_options( $options ) {
$current_options = get_option($this->option_name);
if ($current_options === $options) {
// $this->log('💾 The options are already the expected value.');
} else {
if ( !update_option( $this->option_name, $options, false ) ) {
$this->log( '💾 There was an issue updating the options.' );
}
}
$options = $this->sanitize_options( $options );
return $options;
}
function update_option( $option, $value ) {
$options = $this->get_all_options();
$options[$option] = $value;
return $this->update_options( $options );
}
function reset_options() {
if ( $this->get_all_options() === $this->list_options() ) {
return true;
}
return $this->update_options( $this->list_options() );
}
// Validate and keep the options clean and logical.
function sanitize_options( $options ) {
$options_modified = false;
// Ensure mcp_support exists in options
if ( !isset( $options['mcp_support'] ) ) {
$options['mcp_support'] = false;
}
// Make sure safe mode whitelist is an array
if ( ! is_array( $options['safe_mode_whitelist'] ) ) {
$options['safe_mode_whitelist'] = explode( ",", $options['safe_mode_whitelist'] );
$options_modified = true;
}
// Update AI Engine status
$options_modified = $this->updateAIEngineStatus( $options ) || $options_modified;
// Disable AI related features if AI Engine is not available
if ( ! $options['ai_engine_status'] ) {
if ( $options['ai_suggestions'] !== false ) {
$options['ai_suggestions'] = false;
$options_modified = true;
}
// Note: We don't disable MCP support here anymore
// It will be checked at runtime in the MCP class
}
if ( $options_modified ) {
update_option( $this->option_name, $options, false );
}
return $options;
}
private function updateAIEngineStatus( &$options ) {
global $mwai;
if ( is_null( $mwai ) || ! isset( $mwai ) ) {
$options['ai_engine_status'] = false;
$options['ai_engine_message'] = 'AI Engine is not available.';
return true;
}
try {
$status = $mwai->checkStatus();
if ( $options['ai_engine_status'] != true || $options['ai_engine_message'] != $status ) {
$options['ai_engine_status'] = true;
$options['ai_engine_message'] = $status;
return true;
}
} catch ( Exception $e ) {
if ( $options['ai_engine_status'] != false || $options['ai_engine_message'] != $e->getMessage() ) {
$options['ai_engine_status'] = false;
$options['ai_engine_message'] = $e->getMessage();
return true;
}
}
return false;
}
#endregion
#region Snippets
/**
* Get snippet.
*
* @param $id
* @return mixed
*/
protected function get_snippet( $id ) {
if ( $this->snippet === null ) {
$this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
}
return $this->snippet->select_one( $id );
}
function add_snippet( $params ) {
$response = [
"snippet" => null,
"result" => false,
];
$this->snippet->validate( $params );
$params = $this->snippet->formatParamsForDatabase( $params );
$result = $this->snippet->insert( $params );
$snippet = $this->snippet->select_one( $result );
if( $result ) {
$params['id'] = (string)$result;
$this->snippet->create_or_update_function_snippet( $params );
$this->snippet->create_or_update_interval_snippet( $params );
$this->snippet->get_function_snippets_data( $snippet );
}
$response['snippet'] = $snippet;
$response['result'] = $result;
return $response;
}
private function sanitize_arg( $name, $value, $type = null) {
$real_type = gettype( $value );
if ( $name[0] !== '$' ) { $name = '$' . $name; }
if ( $type == null ) {
$type = $real_type;
}
if ( $type != 'array' && !empty( $value ) && !is_numeric( $value ) && $value[0] !== '"' && $value[strlen( $value ) - 1] !== '"' ) {
$value = '"' . esc_sql( $value ) . '"';
}
if ( $type === 'array' && $real_type === 'string' ) {
// We got a string like this: "["a", "b", "c"]" or "[ 1, 2, 3 ]"
// We need to convert it to an array
$value = str_replace( '"', '', $value );
$value = str_replace( '[', '', $value );
$value = str_replace( ']', '', $value );
$value = explode( ',', $value );
$value = array_map( 'trim', $value );
}
if ( $type === 'array' ) {
$value = json_encode( $value );
$value = str_replace( '\\', '', $value );
}
return [ $name, $value ];
}
function run_non_fn_snippet( $id, $code = null, $test = false ) {
// Retrieve the snippet code from the provided code or via the snippet ID.
if ( $code ) {
$snippet = [ 'code' => $code ];
} else {
$snippet = $this->get_snippet( $id );
}
// Remove any PHP opening tag.
$snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
if ( $test ) {
$snippet['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $snippet['code'] );
}
$error = null;
$output = null;
try {
ob_start();
eval( $snippet['code'] );
$output = ob_get_clean();
} catch ( Throwable $e ) {
$snippet_id = $id ? " ( ID: $id )" : '(Content Gutenberg Block)';
$this->log( '🔴 Error executing the snippet ' . $snippet_id . ' : ' . $e->getMessage() );
ob_clean();
} finally {
restore_error_handler();
}
// If in test mode, return output as an array of lines with an 'error' key if needed.
if ( $test ) {
$output = explode( "\n", trim( $output ) );
if ( $error !== null ) {
$output['error'] = $error->getMessage();
}
} else {
if ( $error !== null ) {
throw $error;
}
}
return $output;
}
function run_snippet( $id, $args = [], $params = [] )
{
// Static array to track defined functions
static $defined_functions = array();
if ( $id ) { // If there is an ID, we get the snippet, if not we get the data from the params
$snippet = $this->get_snippet( $id );
$this->snippet->get_function_snippets_data( $snippet ); // adds the function data to the snippet
$params = [ // We set the params according to the snippet we fetched
'test' => false, // If we pass an ID to the function, we are not testing the snippet
// 'test' => $params['test'] ?? false if needed we can still use ID and test at the same time (should not happen)
'code' => $snippet['code'],
'name' => $snippet['functionName'],
'args' => $snippet['functionArgs'],
'values' => $snippet['functionArgsDict'] // Contains the default values of the arguments
];
}
// Sanitize all the arguments if the option is enabled
if ( $this->get_option( 'sanitize_arguments', true ) ) {
if ( $args ) {
foreach ( $args as $name => $value ) {
list( $sanitizedName, $sanitizedValue ) = $this->sanitize_arg( $name, $value );
unset( $args[$name] );
$args[$sanitizedName] = $sanitizedValue;
}
}
foreach ( $params['values'] as $name => $value ) {
if( array_key_exists( 'input', $value) ) {
list( $sanitizedInputName, $sanitizedInputValue ) = $this->sanitize_arg( $name, $value['input'], $value['type'] );
$params['values'][$sanitizedInputName]['input'] = $sanitizedInputValue;
}
if( array_key_exists( 'default', $value) ) {
list( $sanitizedDefaultValueName, $sanitizedDefaultValue ) = $this->sanitize_arg( $name, $value['default'], $value['type'] );
$params['values'][$sanitizedDefaultValueName]['default'] = $sanitizedDefaultValue;
}
}
}
// Make sure the function is existing and is the one in the snippet
if ( empty( $params['code'] ) ) {
throw new Exception( 'Code Engine: The snippet code appears to be empty.' );
}
if ( empty( $params['name'] ) || ! str_contains( $params['code'], $params['name'] ) ) {
throw new Exception( "Code Engine: Function name does not match. The name should be {$params['name']}." );
}
// Overwrite the default values with the provided ones
if ( $args ) {
foreach ( $args as $name => $value ) {
$params['values'][$name]['input'] = $value;
}
$this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
}
// Check if the function has already been defined
if ( !in_array( $params['name'], $defined_functions ) ) {
// If not, proceed with modification and definition
if ( $params['test'] ) { // Make sure the echo statement uses a line break
$params['code'] = preg_replace( '/echo\s+(.+?);/s', 'echo $1 . "\n";', $params['code'] );
} else { // Remove all echo statements
$params['code'] = preg_replace( '/echo\s+(.+?);/s', '', $params['code'] );
}
$params['code'] = "if (!function_exists('{$params['name']}')) {\n" . $params['code'] . "\n}\n";
// Add the function name to the array to avoid redefinition
$defined_functions[] = $params['name'];
} else {
// If already defined, just prepare to call the function without redefining it
$params['code'] = '';
}
// Prepare the code to be executed
$params['code'] .= "\n\$mwcode_result = {$params['name']}(";
foreach ( $params['args'] as $index => $arg ) {
$value = 'null'; // In case the argument is not provided it will be null
if ( array_key_exists( $arg, $params['values'] ) ) { // Avoid warnings if the argument is not provided
// If the argument is provided, use it, if not use the default value
if ( !empty( $params['values'][$arg]['input'] ) ) {
$value = $params['values'][$arg]['input'];
} else if ( !empty( $params['values'][$arg]['default'] ) ) {
$value = $params['values'][$arg]['default'];
}
}
$params['code'] .= "{$value}";
if ( $index < count( $params['args'] ) - 1 ) {
$params['code'] .= ', ';
}
}
$params['code'] .= ");\necho print_r(\$mwcode_result, true);";
$error = null;
$output = null;
try {
ob_start();
eval( $params['code'] );
$output = ob_get_clean();
if ( $params['test'] ){
$output = explode( "\n", $output );
}
} catch ( Throwable $e ) {
//$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
$error = new Exception(' Error executing the function, ' . $e->getMessage());
ob_clean();
} finally {
restore_error_handler();
}
if ( $error !== null ) {
if( $params['test'] ){
$output['error'] = $error->getMessage();
} else {
throw $error;
}
}
return $output;
}
function parse_snippet( $code, $new_snippet = false ){
$parser = ( new ParserFactory( ) )->createForNewestSupportedVersion( );
if( !$this->snippet ){
$this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
}
// First we check the function names are unique
$fn = $this->snippet->sanitize_and_check_functions( $code, $new_snippet );
if ( ! $fn['is_valid'] ) {
$lint = [
'line' => 1,
'attributes' => $fn['attributes'][0],
'raw_message' => implode(', ', $fn['errors'][0]),
'message' => implode(', ', $fn['errors'][0]),
];
return $lint;
}
try {
$stmts = $parser->parse( $code );
$result = $stmts;
} catch ( PhpParser\Error $e ) {
$lint = [
'line' => $e->getStartLine(),
'attributes' => $e->getAttributes(),
'raw_message' => $e->getRawMessage(),
'message' => $e->getMessage(),
];
return $lint;
}
return null;
}
public function get_js_functions_to_push() {
$functions = $this->snippet->get_functions();
$js_functions = [];
foreach ( $functions as &$function ) {
if ( !isset( $function['target'] ) ) {
$function['target'] = 'php';
}
if ( $function['target'] == 'js' ) {
$js_functions[] = $function;
}
}
$snippets = [];
foreach ( $js_functions as $function ) {
$snippet = $this->snippet->select_one( $function['snippetId'] );
$snippet['function_info'] = $function; // Add function info to snippet
$snippets[] = $snippet;
}
return $this->generate_js_functions_code( $snippets );
}
function generate_js_functions_code ($snippets ) {
$code = "";
foreach ( $snippets as $snippet ) {
$function_code = $snippet['code'];
$function_info = $snippet['function_info'];
// Extract function name and arguments
preg_match( '/(?:const|let|var)?\s*(\w+)\s*=\s*\((.*?)\)\s*=>/', $function_code, $matches );
$function_name = $matches[1] ?? $function_info['name'];
$function_args = $matches[2] ?? '';
// Prepare default values
$default_args = [];
foreach ( $function_info['args'] as $arg ) {
if ( isset( $arg['default'] ) && $arg['default'] !== '' ) {
$default_args[$arg['name']] = $arg['default'];
}
}
// Modify function to use default values
if ( !empty( $default_args ) ) {
$new_args = explode( ',', $function_args );
foreach ( $new_args as &$arg ) {
$arg = trim( $arg );
if ( isset( $default_args[$arg] ) ) {
$arg .= " = " . json_encode( $default_args[$arg] );
}
}
$new_args_string = implode( ', ', $new_args );
$function_code = preg_replace(
'/(\w+)\s*=\s*\((.*?)\)\s*=>/',
"$1 = ($new_args_string) =>",
$function_code
);
}
$code .= $function_code . "\n\n";
}
return $code;
}
/**
* [STATIC] Execute active snippets.
*
* @return array
*/
public function execute_active_snippets() {
$blocked = false;
$page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
if ( $page === 'mwcode_settings' ) {
// If we blocks global snippets like nonce_life filter, we would block the settings page so let's remove the block for this page
$blocked = false;
//$blocked = true;
}
// Block REST requests that aren't whitelisted
elseif ( MeowCommon_Helpers::is_rest() && !Meow_MWCODE_Core::is_white_listed_rest() ) {
$blocked = true;
}
if ( empty( $this->snippet ) ) {
$this->snippet = new Meow_MWCODE_Modules_Snippet( $this );
}
$ts = $this->get_option( 'thrown_snippet', null );
if ( !empty( $ts ) ) {
$this->log( "⚠️ Your snippet \"{$ts['name']}\" has thrown a fatal error last time, so we disabled it. Please check the logs for more information." );
$this->snippet->force_disable( $ts['id'] );
$this->update_option( 'thrown_snippet', null );
}
$scope = is_admin() ? [ 'backend', 'persistent' ] : [ 'frontend', 'persistent' ];
// Get all active snippets
$snippets = $this->snippet->select(
null, // offset
-1, // limit
[
[ 'accessor' => 'active', 'value' => 1 ],
[ 'accessor' => 'scope', 'value' => $scope ],
], // filter
[ 'accessor' => 'priority', 'by' => 'DESC' ] // sort
)['data'];
if ( empty( $snippets ) ) {
return;
}
$snippets = array_map( function ( $snippet ) use ( $blocked ) {
$snippet['code'] = preg_replace( '/<\?php/', '', $snippet['code'], 1 );
$snippet['blocked'] = $blocked;
// If the snippet must be executed only in the frontend, we bypass the block
if ( !is_admin() && $snippet['scope'] === 'frontend' ) {
$snippet['blocked'] = false;
}
return $snippet;
}, $snippets );
return $snippets;
}
#endregion
#region Shortcodes
function content_shortcode( $atts ) {
$atts = shortcode_atts( array(
'id' => null,
'target' => null,
'code' => null,
), $atts );
$id = $atts['id'];
$target = $atts['target'];
$code = $atts['code'];
$no_js = defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML;
$no_php = $this->get_option( 'disallow_block_php', true );
// If the ID is null, it means it comes from a Guttenberg block
$is_block = empty( $id ) && !empty( $code );
if( $is_block ) {
if( $target !== 'js' && $target !== 'php' ) {
return 'Code Engine: Please provide a valid target (js or php).';
}
if ( $no_js && $target === 'js' ) {
return 'Code Engine: Code Block JS are disabled because unfiltered HTML is not allowed on your server.';
}
if ( $no_php && $target === 'php' ) {
return 'Code Engine: 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.';
}
// Because the code from Blocks are sanitized, we need to replace the " with "
$code = str_replace( '"', '"', $code );
if ( $target === 'js' ) {
$output = '';
}
if ( $target === 'php' ) {
$output = $this->run_non_fn_snippet( null, $code );
}
return $output;
}
// If not a block, we get the snippet by ID
// If the ID is not null, it means it comes from a shortcode
if ( empty( $id ) && empty( $code ) ) {
return 'Code Engine: Please provide a snippet ID.';
}
$snippet = $this->get_snippet( $id );
if ( empty( $snippet ) ) {
return 'Code Engine: The snippet does not exist.';
}
//Check if the snippet scope is either content_php or content_js
$is_content_php = $snippet['scope'] === 'content_php';
$is_content_js = $snippet['scope'] === 'content_js';
if ( !$is_content_php && !$is_content_js ) {
return 'Code Engine: The snippet is not a content snippet.';
}
if( $no_js && $is_content_js ) {
return 'Code Engine: Code Engine JS snippets are disabled because unfiltered HTML is not allowed on your server.';
}
//Check if the snippet is active
if ( !$snippet['active'] ) {
return 'Code Engine: The snippet is not active.';
}
$output = 'Code Engine: No output.';
if ( $is_content_js ) {
$output = '';
}
if ( $is_content_php ) {
$output = $this->run_non_fn_snippet( $id );
}
return $output;
}
#endregion
#region Logs
function get_logs() {
$log_file_path = $this->get_logs_path();
if ( !file_exists( $log_file_path ) ) {
return "Empty log file.";
}
$content = file_get_contents( $log_file_path );
$lines = explode( "\n", $content );
$lines = array_filter( $lines );
$lines = array_reverse( $lines );
$content = implode( "\n", $lines );
return $content;
}
function clear_logs() {
$logPath = $this->get_logs_path();
if ( file_exists( $logPath ) ) {
unlink( $logPath );
}
$options = $this->get_all_options();
$options['logs_path'] = null;
$this->update_options( $options );
}
function get_logs_path() {
$uploads_dir = wp_upload_dir();
$uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
$path = $this->get_option( 'logs_path' );
if ( $path && file_exists( $path ) ) {
// make sure the path is legal (within the uploads directory with the MWCODE_PREFIX and log extension)
if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWCODE_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
$path = null;
} else {
return $path;
}
}
if ( !$path ) {
$path = $uploads_dir_path . MWCODE_PREFIX . "_" . $this->random_ascii_chars() . ".log";
if ( !file_exists( $path ) ) {
touch( $path );
}
$options = $this->get_all_options();
$options['logs_path'] = $path;
$this->update_options( $options );
}
return $path;
}
function log( $data = null ) {
if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
$log_file_path = $this->get_logs_path();
$fh = @fopen( $log_file_path, 'a' );
if ( !$fh ) { return false; }
$date = date( "Y-m-d H:i:s" );
if ( is_null( $data ) ) {
fwrite( $fh, "\n" );
}
else {
fwrite( $fh, "$date: {$data}\n" );
//$this->log( "[MWCODE] $data" );
}
fclose( $fh );
return true;
}
private function random_ascii_chars( $length = 8 ) {
$characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
$characters_length = count( $characters );
$random_string = '';
for ( $i = 0; $i < $length; $i++ ) {
$random_string .= $characters[rand(0, $characters_length - 1)];
}
return $random_string;
}
#endregion
#region Helpers
/**
* Check if the request is from a white-listed REST route.
*
* @return bool
*/
public static function is_white_listed_rest() {
$options = get_option( 'mwcode_snippet_vault_options', array() );
// Early return if bypass is enabled
if ( !empty( $options['bypass_rest_security'] ) ) {
return true;
}
// Early return for admin requests
if ( is_admin() ) {
return apply_filters( 'mwcode_rest_authorized', true, null );
}
// Get the requested route
$requested_route = self::get_requested_rest_route();
if ( !$requested_route ) {
return apply_filters( 'mwcode_rest_authorized', false, null );
}
// Check against whitelist
$white_listed = apply_filters( 'mwcode_rest_whitelist', array(
'mwai/v1',
'mwai-ui/v1',
'media-file-renamer/v1',
'media-cleaner/v1',
'wplr/v1',
'code-engine/v1',
'wp/v2',
'meow-gallery/v1',
'mcp/v1',
));
$authorized = self::is_route_whitelisted( $requested_route, $white_listed );
// Log if debug mode is enabled
if ( !empty( $options['server_debug_mode'] ) ) {
self::log_route_status( $requested_route, $authorized );
}
return apply_filters( 'mwcode_rest_authorized', $authorized, $requested_route );
}
/**
* Extract the REST route from the request URI.
*
* @return string|null
*/
public static function get_requested_rest_route() {
if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
return null;
}
$route_parts = explode( '/wp-json/', $_SERVER['REQUEST_URI'] );
if ( isset( $route_parts[1] ) ) {
return trim( $route_parts[1], '/' );
}
return null;
}
/**
* Check if a route is in the whitelist.
*
* @param string $route The route to check
* @param array $white_listed The whitelist array
* @return bool
*/
private static function is_route_whitelisted( $route, $white_listed ) {
foreach ( $white_listed as $white_listed_route ) {
if ( strpos( $route, $white_listed_route ) === 0 ) {
return true;
}
}
return false;
}
/**
* Log the route authorization status.
*
* @param string $route The route being checked
* @param bool $authorized Whether the route is authorized
*/
private static function log_route_status( $route, $authorized ) {
global $mwcode_core;
$message = $authorized
? "✅ REST route authorized: " . $route
: "❌ REST route rejected (not whitelisted): " . $route;
if ( isset( $mwcode_core ) ) {
$mwcode_core->log( $message );
} else {
error_log( "[Code Engine] " . $message );
}
}
#endregion
}
?>