관리-도구
편집 파일: class-wpcode-snippet-execute.php
<?php /** * Global class used to execute code across the plugin. * * @package WPCode */ /** * WPCode_Snippet_Execute class. */ class WPCode_Snippet_Execute { /** * Simply mark this as true when activating a snippet * to display the proper custom error message. * * @var bool */ private $doing_activation = false; /** * The type of executors. * * @var array */ public $types; /** * The snippet types info with labels and descriptions. * * @var array */ public $types_labels; /** * The snippet executed right now, for error handling. * * @var WPCode_Snippet */ public $snippet_executed; /** * Store snippet types by id for already looked-up snippets * to reduce the number of queries. * * @var array */ private $snippet_types = array(); /** * Store the line reference for each snippet. * * @var array */ private $line_reference = array(); /** * Constructor. */ public function __construct() { $this->add_error_handling(); $this->load_types(); } /** * Register custom error handling functions. * * @return void */ public function add_error_handling() { // Register our custom error catcher. register_shutdown_function( array( $this, 'maybe_disable_snippet' ) ); // Customize WP error message. add_filter( 'wp_php_error_message', array( $this, 'custom_error_message' ), 15, 2 ); add_filter( 'wpcode_snippet_output_php', array( $this, 'dont_load_edited_snippet' ), 10, 2 ); } /** * Load the classes and options available for executing code. * * @return void */ public function load_types() { require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-type.php'; require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-html.php'; require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-text.php'; require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-js.php'; require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-php.php'; require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-universal.php'; require_once WPCODE_PLUGIN_PATH . 'includes/execute/class-wpcode-snippet-execute-css.php'; $this->types = array( 'html' => array( 'class' => 'WPCode_Snippet_Execute_HTML', ), 'text' => array( 'class' => 'WPCode_Snippet_Execute_Text', ), 'blocks' => array( 'class' => 'WPCode_Snippet_Execute_Blocks', 'is_pro' => true, ), 'css' => array( 'class' => 'WPCode_Snippet_Execute_CSS', ), 'scss' => array( 'class' => 'WPCode_Snippet_Execute_SCSS', 'is_pro' => true, ), 'js' => array( 'class' => 'WPCode_Snippet_Execute_JS', ), 'php' => array( 'class' => 'WPCode_Snippet_Execute_PHP', ), 'universal' => array( 'class' => 'WPCode_Snippet_Execute_Universal', ), ); } /** * Load the snippet types on demand. * * @return void */ public function load_snippet_types_on_demand() { $this->types = array( 'html' => array( 'class' => 'WPCode_Snippet_Execute_HTML', 'label' => __( 'HTML Snippet', 'insert-headers-and-footers' ), 'description' => __( 'Easily insert scripts from other sites or build custom elements using HTML.', 'insert-headers-and-footers' ), // Don't want to instantiate the class until it's needed and we need this to be translatable. ), 'text' => array( 'class' => 'WPCode_Snippet_Execute_Text', 'label' => __( 'Text Snippet', 'insert-headers-and-footers' ), 'description' => __( 'Create reusable text snippets that you can visually format in a familiar editor.', 'insert-headers-and-footers' ), ), 'blocks' => array( 'class' => 'WPCode_Snippet_Execute_Blocks', 'label' => __( 'Blocks Snippet (PRO)', 'insert-headers-and-footers' ), 'is_pro' => true, 'description' => __( 'Use the Block Editor to create components that you can insert anywhere in your site.', 'insert-headers-and-footers' ), ), 'css' => array( 'class' => 'WPCode_Snippet_Execute_CSS', 'label' => __( 'CSS Snippet', 'insert-headers-and-footers' ), 'description' => __( 'Write CSS styles directly in WPCode and easily customize how your website looks.', 'insert-headers-and-footers' ), ), 'scss' => array( 'class' => 'WPCode_Snippet_Execute_SCSS', 'label' => __( 'SCSS Snippet (PRO)', 'insert-headers-and-footers' ), 'is_pro' => true, 'description' => __( 'Write SCSS styles directly in WPCode and easily customize how your website looks.', 'insert-headers-and-footers' ), ), 'js' => array( 'class' => 'WPCode_Snippet_Execute_JS', 'label' => __( 'JavaScript Snippet', 'insert-headers-and-footers' ), 'description' => __( 'Add custom JavaScript code to your site to add interactivity or integrate with other services.', 'insert-headers-and-footers' ), ), 'php' => array( 'class' => 'WPCode_Snippet_Execute_PHP', 'label' => __( 'PHP Snippet', 'insert-headers-and-footers' ), 'description' => __( 'Extend or add functionality using PHP code with full control on where it\'s executed', 'insert-headers-and-footers' ), ), 'universal' => array( 'class' => 'WPCode_Snippet_Execute_Universal', 'label' => __( 'Universal Snippet', 'insert-headers-and-footers' ), 'description' => __( 'Start writing HTML and add PHP code like you would in a .php file with Universal snippets.', 'insert-headers-and-footers' ), ), ); $this->types_labels = true; } /** * Gets passed a snippet WP_Post or id and returns the processed output. * * @param int|WP_Post|WPCode_Snippet $snippet The snippet id or post object. * * @return string */ public function get_snippet_output( $snippet ) { // If we're in headers & footers mode prevent execution of any type of snippet. if ( WPCode()->settings->get_option( 'headers_footers_mode' ) ) { return ''; } if ( ! $snippet instanceof WPCode_Snippet ) { $snippet = new WPCode_Snippet( $snippet ); } $type = $snippet->get_code_type(); $class = $this->get_type_execute_class( $type ); if ( $class && class_exists( $class ) ) { $execute_instance = new $class( $snippet ); /** * Adding comment for convenience. * * @var WPCode_Snippet_Execute_Type $execute_instance */ return $execute_instance->get_output(); } // If we can't find the type class for some reason just return empty. return ''; } /** * Find the execution type class and returns its name. * * @param string $type The type of code to get the executor for. * * @return string|false */ public function get_type_execute_class( $type ) { $types = $this->get_types(); if ( isset( $types[ $type ] ) ) { return $types[ $type ]['class']; } return false; } /** * Get the types of executors. * * @return array */ public function get_types() { if ( ! isset( $this->types_labels ) && did_action( 'init' ) ) { $this->load_snippet_types_on_demand(); } return $this->types; } /** * Get a label from the term slug. * * @param string $type The code type slug. * * @return string */ public function get_type_label( $type ) { $options = $this->get_options(); return isset( $options[ $type ] ) ? $options[ $type ] : ''; } /** * Grab the options with labels, for display in admin. * * @return array */ public function get_options() { $options = array(); $types = $this->get_types(); foreach ( $types as $type_key => $type_values ) { $options[ $type_key ] = $type_values['label']; } return apply_filters( 'wpcode_code_type_options', $options ); } /** * Get the code types info with labels and descriptions. * * @return array */ public function get_code_types() { $code_types = array(); $types = $this->get_types(); foreach ( $types as $type_key => $type_values ) { $code_types[ $type_key ] = array( 'label' => $type_values['label'], 'description' => $type_values['description'], ); } return $code_types; } /** * Get editor options for all code types. * * @return array */ public function get_code_type_options() { $types = $this->get_options(); $options = array(); foreach ( $types as $type => $label ) { $options[ $type ] = array( 'mime' => $this->get_mime_for_code_type( $type ), 'lint' => $this->code_type_has_lint( $type ), ); } return $options; } /** * Convert generic code type to MIME used by CodeMirror. * * @param string $code_type The code type (php,js,html,etc). * * @return string */ public function get_mime_for_code_type( $code_type ) { $mime = 'text/html'; if ( ! empty( $code_type ) ) { switch ( $code_type ) { case 'php': $mime = 'application/x-httpd-php-open'; break; case 'universal': $mime = 'application/x-httpd-php'; break; case 'js': $mime = 'text/javascript'; break; case 'text': $mime = 'text/x-markdown'; break; case 'css': $mime = 'text/css'; break; case 'scss': $mime = 'text/x-scss'; break; } } return $mime; } /** * Check if the code type supports linting in CodeMirror. * * @param string $code_type The code type slug. * * @return bool */ public function code_type_has_lint( $code_type = '' ) { if ( empty( $code_type ) ) { $code_type = isset( $this->code_type ) ? $this->code_type : ''; } $types_with_lint = array( 'html', 'js', ); return in_array( $code_type, $types_with_lint, true ); } /** * Execute the PHP code in a single place. * * @param string $code The code to execute. * @param WPCode_Snippet $snippet The snippet object (optional) so we deactivate it to prevent the same error. * @param array $line_reference The line reference for the error. * * @return false|string */ public function safe_execute_php( $code, $snippet = null, $line_reference = array() ) { if ( isset( $snippet ) ) { $this->snippet_executed = $snippet; } // Catch any output from running the code. ob_start(); $error = false; // Don't allow executing suspicious code. if ( self::is_code_not_allowed( $code ) ) { $code = ''; } $this->line_reference = $line_reference; try { $this->run_eval( $code ); } catch ( Error $e ) { $error = array( 'message' => $e->getMessage(), 'line' => $e->getLine(), ); } if ( $error ) { $this->maybe_disable_snippet( $error ); } return ob_get_clean(); } /** * Execute the code in a separate method to avoid overriding variable names but still being able to catch errors. * * @param string $code The code to execute. * * @return void */ public function run_eval( $code ) { if ( ! empty( $this->snippet_executed->attributes ) ) { extract( $this->snippet_executed->attributes, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract } eval( $code ); // phpcs:ignore Squiz.PHP.Eval.Discouraged } /** * Callback for register_shutdown_function that checks if the error was thrown by this class * and if so, it disables the last snippet that was executed so that the site continues to run * correctly. * * @param array|null $error The error object. * * @return void */ public function maybe_disable_snippet( $error = null ) { if ( is_null( $error ) ) { $error = error_get_last(); } $deactivated = false; $error['wpc_type'] = 'error'; $error['doing_activation'] = $this->is_doing_activation(); if ( $this->is_error_from_wpcode( $error ) || $this->is_doing_activation() ) { // Let's see if we have a line reference stored and the error has a line number. if ( ! empty( $error['line'] ) ) { $snippet_data = $this->find_snippet_from_line( $error['line'] ); if ( ! empty( $snippet_data ) ) { /** * Added for convenience. * * @var WPCode_Snippet $snippet */ $snippet = $snippet_data['snippet']; $error_line = $snippet_data['line']; $error['snippet'] = $snippet; $error['error_line'] = $error_line; // Let's try to determine on which page we are and potentially save that URL in the error details. global $wp; if ( isset( $wp->query_vars ) && isset( $wp->request ) ) { $error['url'] = add_query_arg( $wp->query_vars, home_url( $wp->request ) ); } if ( $this->snippet_location_disable( $snippet ) && $this->should_auto_disable() ) { $snippet->force_deactivate(); $deactivated = true; $error['wpc_type'] = 'deactivated'; } } } if ( ! $deactivated ) { // Check if we should deactivate the last snippet executed. if ( isset( $this->snippet_executed ) && $this->snippet_location_disable( $this->snippet_executed ) && $this->should_auto_disable() ) { $this->snippet_executed->force_deactivate(); $error['snippet'] = $this->snippet_executed; $error['wpc_type'] = 'deactivated'; } } wpcode()->error->add_error( $error ); } } /** * Check if the snippet is in a location that might potentially be auto disabled. * * @param WPCode_Snippet $snippet The snippet object. * * @return bool */ public function snippet_location_disable( $snippet ) { return in_array( $snippet->get_location(), $this->get_locations_to_auto_disable(), true ); } /** * Find the snippet that caused the error based on the line number of the error. * * @param int $line The line number of the error. * * @return array|false */ public function find_snippet_from_line( $line ) { if ( empty( $this->line_reference ) ) { return false; } foreach ( $this->line_reference as $snippet_id => $lines ) { if ( $lines['start'] <= $line && $lines['end'] >= $line ) { // If we have a match, let's deactivate that snippet. $snippet = new WPCode_Snippet( $snippet_id ); $error_line = $line - $lines['start'] + 1; return array( 'snippet' => $snippet, 'line' => $error_line, ); } } return false; } /** * Get an error object (from error_get_last) and check if it originated in the WPCode eval function. * * @param array $error The error array. * * @return bool * @see error_get_last() */ public function is_error_from_wpcode( $error ) { if ( isset( $error['type'] ) && E_NOTICE === $error['type'] ) { // If it's a notice let's let it be. return false; } if ( $error && isset( $error['message'] ) && isset( $error['file'] ) ) { // Let's see if the error originated in the code executed from a snippet. $pattern = '/\bwpcode-snippet-execute\.php\b(.*)\beval\b/m'; if ( preg_match( $pattern, $error['message'] ) || preg_match( $pattern, $error['file'] ) ) { return true; } } return false; } /** * Display a custom error message (not the WP default one) for fatal errors thrown * when trying to activate snippets via AJAX. Only if the error is thrown in the eval code from WPCode. * * @param string $message The error message to be displayed (HTML). * @param array $error The error object from error_get_last. * * @return string */ public function custom_error_message( $message, $error ) { // If the error is not related to our plugin don't do anything. if ( ! $this->is_error_from_wpcode( $error ) ) { return $message; } // If we're not in the admin or the current user can't update snippets just let WP handle the error message. if ( ! is_admin() || ! current_user_can( 'wpcode_edit_snippets' ) ) { return $message; } $doing_ajax = defined( 'DOING_AJAX' ) && DOING_AJAX; if ( $this->is_doing_activation() ) { $message = sprintf( '<p>%s</p>', __( 'Snippet has not been activated due to an error.', 'insert-headers-and-footers' ) ); if ( ! $doing_ajax ) { // Not doing ajax let's ask them to go back. $message .= '<p>' . __( 'Please click the back button in the browser to update the snippet.', 'insert-headers-and-footers' ) . '</p>'; } } else { $message = sprintf( '<p>%s</p>', __( 'WPCode has detected an error in one of the snippets which has now been automatically deactivated.', 'insert-headers-and-footers' ) ); } if ( ! $doing_ajax ) { $message .= '<p>'; if ( ! empty( $this->snippet_executed ) ) { $deactivated_snippets_link = add_query_arg( array( 'page' => 'wpcode', 'view' => 'has_error', ), admin_url( 'admin.php' ) ); $message .= '<a href="' . esc_url( $deactivated_snippets_link ) . '" class="button button-primary">' . __( 'View Snippets With Errors', 'insert-headers-and-footers' ) . '</a> '; } if ( ! $this->is_doing_activation() ) { if ( wpcode()->settings->get_option( 'error_logging' ) ) { $url = add_query_arg( array( 'page' => 'wpcode-tools', 'view' => 'logs', ), admin_url( 'admin.php' ) ); $message .= '<a href="' . esc_url( $url ) . '" class="button" target="_blank">' . __( 'View error logs', 'insert-headers-and-footers' ) . '</a>'; } else { $url = add_query_arg( array( 'page' => 'wpcode-settings', ), admin_url( 'admin.php' ) ); $message .= '<a href="' . esc_url( $url ) . '" class="button" target="_blank">' . __( 'Enable error logging', 'insert-headers-and-footers' ) . '</a>'; } } $message .= '</p>'; } $message .= sprintf( '<p>%s</p>', __( 'Error message:', 'insert-headers-and-footers' ) ); $message .= sprintf( '<code>%s</code>', $error['message'] ); return $message; } /** * Mark as doing activation. * * @return void */ public function doing_activation() { $this->doing_activation = true; } /** * Check if we are in the middle of activating a snippet. * Used for choosing the type of custom error message to display. * * @return bool */ public function is_doing_activation() { return $this->doing_activation; } /** * Mark as finished activation. * * @return void */ public function not_doing_activation() { $this->doing_activation = false; } /** * Check if a code type is marked as pro. * * @param string $key The key of the type to check. * * @return bool */ public function is_type_pro( $key ) { $types = $this->get_types(); // Find type by key in the list of types. $pro_types = wp_list_filter( $types, array( 'is_pro' => true ) ); if ( isset( $pro_types[ $key ] ) ) { return true; } return false; } /** * Get the list of locations where snippets can be automatically disabled. * * @return array */ public function get_locations_to_auto_disable() { // Use this filter to add locations where the snippet should be auto disabled or disable auto-disable. return apply_filters( 'wpcode_error_locations_auto_disable', array( 'everywhere', 'admin_only', ) ); } /** * Check if we should auto disable snippets on the frontend. * * @return bool */ public function should_auto_disable() { return apply_filters( 'wpcode_auto_disable_frontend', is_admin() ); } /** * Add a method to detect suspicious code. * * @param string $code The code to check. * * @return bool */ public static function is_code_not_allowed( $code ) { if ( preg_match_all( '/(base64_decode|error_reporting|ini_set|eval)\s*\(/i', $code, $matches ) ) { if ( count( $matches[0] ) > 5 ) { return true; } } if ( preg_match( '/dns_get_record/i', $code ) ) { return true; } return false; } /** * Callback for the wpcode_snippet_output_php filter to prevent loading the edited snippet. * This allows us to run actual checks on the code without throwing function redeclare errors or similar * by executing the same code twice. * * @param string $code The code to be output. * @param WPCode_Snippet $snippet The snippet object. * * @return string */ public function dont_load_edited_snippet( $code, $snippet ) { if ( ! is_admin() ) { return $code; } if ( ! isset( $_POST['wpcode-save-snippet-nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['wpcode-save-snippet-nonce'] ), 'wpcode-save-snippet' ) ) { return $code; } // Let's check if $_REQUEST['id'] matches the snippet id. if ( ! isset( $_REQUEST['id'] ) || absint( $_REQUEST['id'] ) !== $snippet->get_id() ) { return $code; } return ''; } }