oont-contents/plugins/insert-headers-and-footers/includes/class-wpcode-snippet-execute.php
2025-04-06 08:34:48 +02:00

739 lines
20 KiB
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',
'filter_label' => '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',
'filter_label' => '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',
'filter_label' => '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',
'filter_label' => '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',
'filter_label' => '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',
'filter_label' => 'JavaScript',
'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',
'filter_label' => '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',
'filter_label' => '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 apply_filters( 'wpcode_code_types_for_display', $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 Generic.PHP.ForbiddenFunctions.Found
}
/**
* 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>&nbsp;';
}
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 '';
}
}