Server IP : 52.91.253.208 / Your IP : 3.138.34.80 [ Web Server : Apache System : Linux ip-172-26-9-9 4.19.0-25-cloud-amd64 #1 SMP Debian 4.19.289-1 (2023-07-24) x86_64 User : daemon ( 1) PHP Version : 7.3.18 Disable Function : NONE Domains : 3 Domains MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /opt/bitnami/apps/wordpress/htdocs/wp-content/plugins/amp/src/ |
Upload File : |
<?php /** * Class PluginSuppression. * * @package AmpProject\AmpWP */ namespace AmpProject\AmpWP; use AMP_Options_Manager; use AMP_Theme_Support; use AMP_Validation_Error_Taxonomy; use AMP_Validation_Manager; use AMP_Validated_URL_Post_Type; use AmpProject\AmpWP\Admin\ReaderThemes; use AmpProject\AmpWP\DevTools\CallbackReflection; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use WP_Block_Type_Registry; use WP_Hook; use WP_Term; use WP_Widget; use WP_User; /** * Suppress plugins from running by removing their hooks and nullifying their shortcodes, widgets, and blocks. * * @package AmpProject\AmpWP * @internal * @since 2.0 */ final class PluginSuppression implements Service, Registerable { /** * Plugin registry to use. * * @var PluginRegistry */ private $plugin_registry; /** * Callback reflector to use. * * @var CallbackReflection */ private $callback_reflection; /** * Instantiate the plugin suppression service. * * @param PluginRegistry $plugin_registry Plugin registry to use. * @param CallbackReflection $callback_reflection Callback reflector to use. */ public function __construct( PluginRegistry $plugin_registry, CallbackReflection $callback_reflection ) { $this->plugin_registry = $plugin_registry; $this->callback_reflection = $callback_reflection; } /** * Register the service with the system. */ public function register() { add_filter( 'amp_default_options', [ $this, 'filter_default_options' ] ); add_filter( 'amp_options_updating', [ $this, 'sanitize_options' ], 10, 2 ); // When a Reader theme is selected and an AMP request is being made, start suppressing as early as possible. // This can be done because we know it is an AMP page due to the query parameter, but it also _has_ to be done // specifically for the case of accessing the AMP Customizer (in which customize.php is requested with the query // parameter) in order to prevent the registration of Customizer controls from suppressed plugins. Suppression // could be done early for Transitional mode as well since a query parameter is also used for frontend requests // but there is no similar need to suppress the registration of Customizer controls in Transitional mode since // there is no separate Customizer for AMP in Transitional mode (or legacy Reader mode). if ( $this->is_reader_theme_request() ) { $this->suppress_plugins(); } else { $min_priority = defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : ~PHP_INT_MAX; // phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound // In Standard mode we _have_ to wait for the wp action because with the absence of a query parameter // we have to rely on amp_is_request() and the WP_Query to determine whether a plugin should be suppressed. add_action( 'wp', [ $this, 'maybe_suppress_plugins' ], $min_priority ); } } /** * Is reader theme request. * * @return bool */ public function is_reader_theme_request() { return ( AMP_Theme_Support::READER_MODE_SLUG === AMP_Options_Manager::get_option( Option::THEME_SUPPORT ) && ReaderThemes::DEFAULT_READER_THEME !== AMP_Options_Manager::get_option( Option::READER_THEME ) && isset( $_GET[ amp_get_slug() ] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended ); } /** * Add default option. * * @param array $defaults Default options. * @return array Defaults. */ public function filter_default_options( $defaults ) { $defaults[ Option::SUPPRESSED_PLUGINS ] = []; return $defaults; } /** * Suppress plugins if on an AMP endpoint. * * @return bool Whether plugins are being suppressed. */ public function maybe_suppress_plugins() { if ( amp_is_request() ) { return $this->suppress_plugins(); } return false; } /** * Suppress plugins. * * @return bool Whether plugins are being suppressed. */ public function suppress_plugins() { $suppressed = AMP_Options_Manager::get_option( Option::SUPPRESSED_PLUGINS ); if ( empty( $suppressed ) ) { return false; } $suppressed_plugin_slugs = array_keys( $suppressed ); $this->suppress_hooks( $suppressed_plugin_slugs ); $this->suppress_shortcodes( $suppressed_plugin_slugs ); $this->suppress_blocks( $suppressed_plugin_slugs ); $this->suppress_widgets( $suppressed_plugin_slugs ); return true; } /** * Sanitize options. * * @param array $options Existing options with already-sanitized values for updating. * @param array $new_options Unsanitized options being submitted for updating. * * @return array Sanitized options. */ public function sanitize_options( $options, $new_options ) { if ( ! isset( $new_options[ Option::SUPPRESSED_PLUGINS ] ) ) { return $options; } $option = $options[ Option::SUPPRESSED_PLUGINS ]; $posted_suppressed_plugins = $new_options[ Option::SUPPRESSED_PLUGINS ]; $plugins = $this->plugin_registry->get_plugins( true ); $errors_by_source = AMP_Validated_URL_Post_Type::get_recent_validation_errors_by_source(); $urls_by_frequency = []; $changes = 0; foreach ( $posted_suppressed_plugins as $plugin_slug => $suppressed ) { if ( ! isset( $plugins[ $plugin_slug ] ) ) { unset( $option[ $plugin_slug ] ); continue; } $suppressed = rest_sanitize_boolean( $suppressed ); if ( isset( $option[ $plugin_slug ] ) && ! $suppressed ) { // Gather the URLs on which the error occurred, keeping track of the frequency so that we can use the URL with the most errors to re-validate. if ( ! empty( $option[ $plugin_slug ][ Option::SUPPRESSED_PLUGINS_ERRORING_URLS ] ) ) { foreach ( $option[ $plugin_slug ][ Option::SUPPRESSED_PLUGINS_ERRORING_URLS ] as $url ) { if ( ! isset( $urls_by_frequency[ $url ] ) ) { $urls_by_frequency[ $url ] = 0; } $urls_by_frequency[ $url ]++; } } // Remove the plugin from being suppressed. unset( $option[ $plugin_slug ] ); $changes++; } elseif ( ! isset( $option[ $plugin_slug ] ) && $suppressed && array_key_exists( $plugin_slug, $plugins ) ) { // Capture the URLs that the error occurred on so we can check them again when the plugin is re-activated. $urls = []; if ( isset( $errors_by_source['plugin'][ $plugin_slug ] ) ) { foreach ( $errors_by_source['plugin'][ $plugin_slug ] as $validation_error ) { $urls = array_merge( $urls, array_map( [ AMP_Validated_URL_Post_Type::class, 'get_url_from_post' ], $validation_error['post_ids'] ) ); } } $user = wp_get_current_user(); $option[ $plugin_slug ] = [ // Note that we store the version that was suppressed so that we can alert the user when to check again. Option::SUPPRESSED_PLUGINS_LAST_VERSION => $plugins[ $plugin_slug ]['Version'], Option::SUPPRESSED_PLUGINS_TIMESTAMP => time(), Option::SUPPRESSED_PLUGINS_USERNAME => $user instanceof WP_User ? $user->user_nicename : null, Option::SUPPRESSED_PLUGINS_ERRORING_URLS => array_unique( array_filter( $urls ) ), ]; $changes++; } } // When the suppressed plugins changed, re-validate so validation errors can be re-computed with the plugins newly-suppressed or un-suppressed. if ( $changes > 0 ) { add_action( 'update_option_' . AMP_Options_Manager::OPTION_NAME, static function () use ( $urls_by_frequency ) { $url = null; if ( count( $urls_by_frequency ) > 0 ) { arsort( $urls_by_frequency ); $url = key( $urls_by_frequency ); } else { $validated_url_posts = get_posts( [ 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG, 'posts_per_page' => 1, ] ); if ( count( $validated_url_posts ) > 0 ) { $url = AMP_Validated_URL_Post_Type::get_url_from_post( $validated_url_posts[0] ); } } if ( $url ) { AMP_Validation_Manager::validate_url_and_store( $url ); } } ); } $options[ Option::SUPPRESSED_PLUGINS ] = $option; return $options; } /** * Provides validation errors for a plugin specified by slug. * * @param string $plugin_slug Plugin slug. * @return array Validation errors. */ private function get_sorted_plugin_validation_errors( $plugin_slug ) { $errors_by_source = AMP_Validated_URL_Post_Type::get_recent_validation_errors_by_source(); if ( ! isset( $errors_by_source['plugin'][ $plugin_slug ] ) ) { return []; } $validation_errors = $errors_by_source['plugin'][ $plugin_slug ]; usort( $validation_errors, static function ( $a, $b ) { /** @var WP_Term */ $a_term = $a['term']; /** @var WP_Term */ $b_term = $b['term']; $a_reviewed = ( (int) $a_term->term_group & AMP_Validation_Error_Taxonomy::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ); $b_reviewed = ( (int) $b_term->term_group & AMP_Validation_Error_Taxonomy::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ); if ( $a_reviewed !== $b_reviewed ) { return (int) $a_reviewed - (int) $b_reviewed; } $a_removed = ( (int) $a_term->term_group & AMP_Validation_Error_Taxonomy::ACCEPTED_VALIDATION_ERROR_BIT_MASK ); $b_removed = ( (int) $b_term->term_group & AMP_Validation_Error_Taxonomy::ACCEPTED_VALIDATION_ERROR_BIT_MASK ); return (int) $a_removed - (int) $b_removed; } ); // Because this data will be accessed via REST, add additional fields that are not easily rendered in JS. $validation_errors = array_map( static function( $validation_error ) { $term = $validation_error['term']; $validation_error['edit_url'] = admin_url( add_query_arg( [ AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG => $term->name, 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG, ], 'edit.php' ) ); $validation_error['title'] = AMP_Validation_Error_Taxonomy::get_error_title_from_code( $validation_error['data'] ); $validation_error['is_removed'] = (bool) ( (int) $term->term_group & AMP_Validation_Error_Taxonomy::ACCEPTED_VALIDATION_ERROR_BIT_MASK ); $validation_error['is_reviewed'] = (bool) ( (int) $term->term_group & AMP_Validation_Error_Taxonomy::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ); $validation_error['tooltip'] = sprintf( /* translators: %1 is whether validation error is 'removed' or 'kept', %2 is whether validation error is 'reviewed' or 'unreviewed' */ __( 'Invalid markup causing the validation error is %1$s and %2$s. See all validated URL(s) with this validation error.', 'amp' ), $validation_error['is_removed'] ? __( 'removed', 'amp' ) : __( 'kept', 'amp' ), $validation_error['is_reviewed'] ? __( 'reviewed', 'amp' ) : __( 'unreviewed', 'amp' ) ); return $validation_error; }, $validation_errors ); return $validation_errors; } /** * Provides a keyed array of active plugins with keys being slugs and values being plugin info plus validation error details. * * Plugins are sorted by validation error count, in descending order. * * @return array Plugins. */ public function get_suppressible_plugins_with_details() { $plugins = []; foreach ( $this->plugin_registry->get_plugins( true ) as $slug => $plugin ) { $plugin['validation_errors'] = $this->get_sorted_plugin_validation_errors( $slug ); $plugins[ $slug ] = $plugin; } return $plugins; } /** * Prepare suppressed plugins for response. * * Augment the suppressed plugins data with additional information. * * @param array $suppressed_plugins Suppressed plugins. * @return array Prepared suppressed plugins. */ public function prepare_suppressed_plugins_for_response( $suppressed_plugins ) { return array_map( function ( $suppressed_plugin ) { if ( ! is_array( $suppressed_plugin ) ) { return $suppressed_plugin; } if ( isset( $suppressed_plugin['username'] ) ) { $username = $suppressed_plugin['username']; unset( $suppressed_plugin['username'] ); $suppressed_plugin['user'] = $this->prepare_user_for_response( $username ); } return $suppressed_plugin; }, $suppressed_plugins ); } /** * Prepare user for response. * * @param string $username Username. * @return array User data. */ private function prepare_user_for_response( $username ) { $response = [ 'slug' => $username, ]; $user = get_user_by( 'slug', $username ); if ( $user ) { $response['name'] = $user->display_name; } return $response; } /** * Suppress plugin hooks. * * @param string[] $suppressed_plugins Suppressed plugins. * @global WP_Hook[] $wp_filter */ private function suppress_hooks( $suppressed_plugins ) { global $wp_filter; foreach ( $wp_filter as $tag => $filter ) { foreach ( $filter->callbacks as $priority => $prioritized_callbacks ) { foreach ( $prioritized_callbacks as $callback ) { if ( $this->is_callback_plugin_suppressed( $callback['function'], $suppressed_plugins ) ) { $filter->remove_filter( $tag, $callback['function'], $priority ); } } } } } /** * Suppress plugin shortcodes. * * @param string[] $suppressed_plugins Suppressed plugins. * @global array $shortcode_tags */ private function suppress_shortcodes( $suppressed_plugins ) { global $shortcode_tags; foreach ( array_keys( $shortcode_tags ) as $tag ) { if ( $this->is_callback_plugin_suppressed( $shortcode_tags[ $tag ], $suppressed_plugins ) ) { add_shortcode( $tag, '__return_empty_string' ); } } } /** * Suppress plugin blocks. * * @todo What about static blocks added? * * @param string[] $suppressed_plugins Suppressed plugins. */ private function suppress_blocks( $suppressed_plugins ) { if ( ! class_exists( 'WP_Block_Type_Registry' ) ) { return; } $registry = WP_Block_Type_Registry::get_instance(); foreach ( $registry->get_all_registered() as $block_type ) { if ( ! $block_type->is_dynamic() || ! $this->is_callback_plugin_suppressed( $block_type->render_callback, $suppressed_plugins ) ) { continue; } unset( $block_type->script, $block_type->style ); $block_type->render_callback = '__return_empty_string'; } } /** * Suppress plugin widgets. * * @see AMP_Validation_Manager::wrap_widget_callbacks() Which needs to run after this. * * @param string[] $suppressed_plugins Suppressed plugins. * @global array $wp_registered_widgets */ private function suppress_widgets( $suppressed_plugins ) { global $wp_registered_widgets; foreach ( $wp_registered_widgets as &$registered_widget ) { if ( $this->is_callback_plugin_suppressed( $registered_widget['callback'], $suppressed_plugins ) ) { // This is primarily needed for widgets registered without WP_Widget. $registered_widget['callback'] = '__return_null'; } } // The above will ensure that widgets registered via WP_Widget or wp_register_sidebar_widget() will both be // suppressed from being output. One additional case, which also applies to WP_Widget, is when the_widget() // is used to render a widget. For that, the 'widget_display_callback' filter below is used (in WP>=5.3). add_filter( 'widget_display_callback', /** * Prevent WP_Widgets from suppressed plugins from being rendered in sidebars and via the_widget(). * * @param array $instance The current widget instance's settings. * @param WP_Widget $widget_obj The current widget instance. * @return array|false Instance or false if suppressed. */ function ( $instance, $widget_obj ) use ( $suppressed_plugins ) { if ( $this->is_callback_plugin_suppressed( [ $widget_obj, 'display_callback' ], $suppressed_plugins ) ) { $instance = false; } return $instance; }, PHP_INT_MAX, 2 ); } /** * Determine whether callback is from a suppressed plugin. * * @param callable $callback Callback. * @param string[] $suppressed_plugins Suppressed plugins. * @return bool Whether from suppressed plugin. */ private function is_callback_plugin_suppressed( $callback, $suppressed_plugins ) { $source = $this->callback_reflection->get_source( $callback ); return ( isset( $source['type'], $source['name'] ) && 'plugin' === $source['type'] && in_array( $source['name'], $suppressed_plugins, true ) ); } }