From 13a1a9f0468f43ae4f06bc23f766df2b8ba4aee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Al=20=E2=8E=8B?= Date: Mon, 29 Jul 2024 22:20:35 +0300 Subject: [PATCH] Feature: Add compatibility layer and compatibility status output (#48) * Feature: Add compatibility layer logic and status output. * qa/address review comments * qa/cleanup * qa/code-style * qa/code style; feat/output compatibility status to main site health tab * fix linting regressions * refactor plugin name retrieval * i18n missed string * update phpdoc on Base class constructor * GH-50/fix missing function namespace * linting --------- Co-authored-by: Chris Reynolds --- README.md | 3 + inc/compatibility/base.php | 276 ++++++ .../class-acceleratedmobilepages.php | 37 + inc/compatibility/class-auth0.php | 37 + inc/compatibility/class-autoptimize.php | 37 + .../class-bettersearchreplace.php | 41 + inc/compatibility/class-brokenlinkchecker.php | 62 ++ .../class-compatibilityfactory.php | 193 +++++ inc/compatibility/class-contactformseven.php | 39 + inc/compatibility/class-eventespresso.php | 35 + .../class-fastvelocityminify.php | 37 + inc/compatibility/class-forcelogin.php | 37 + .../class-officialfacebookpixel.php | 35 + inc/compatibility/class-polylang.php | 36 + inc/compatibility/class-redirection.php | 37 + inc/compatibility/class-sliderrevolution.php | 35 + inc/compatibility/class-tweetoldpost.php | 37 + inc/compatibility/class-woozone.php | 35 + inc/compatibility/class-wprocket.php | 39 + inc/compatibility/class-yithwoocommerce.php | 37 + .../fixes/class-acceleratedmobilepagesfix.php | 75 ++ .../fixes/class-addfilterfix.php | 35 + inc/compatibility/fixes/class-auth0fix.php | 54 ++ .../fixes/class-autoptimizefix.php | 34 + .../fixes/class-defineconstantfix.php | 27 + .../fixes/class-deletefilefix.php | 39 + .../fixes/class-selfupdatingthemesfix.php | 27 + .../fixes/class-setserverportfix.php | 48 ++ .../fixes/class-sliderrevolutionfix.php | 21 + .../fixes/class-updatevaluefix.php | 38 + inc/compatibility/fixes/class-wprocketfix.php | 28 + .../fixes/class-yithchangepdflocationfix.php | 45 + inc/site-health.php | 813 +++++++++++++++++- pantheon.php | 9 +- tests/phpunit/test-compatibility-layer.php | 90 ++ 35 files changed, 2505 insertions(+), 3 deletions(-) create mode 100644 inc/compatibility/base.php create mode 100644 inc/compatibility/class-acceleratedmobilepages.php create mode 100644 inc/compatibility/class-auth0.php create mode 100644 inc/compatibility/class-autoptimize.php create mode 100644 inc/compatibility/class-bettersearchreplace.php create mode 100644 inc/compatibility/class-brokenlinkchecker.php create mode 100644 inc/compatibility/class-compatibilityfactory.php create mode 100644 inc/compatibility/class-contactformseven.php create mode 100644 inc/compatibility/class-eventespresso.php create mode 100644 inc/compatibility/class-fastvelocityminify.php create mode 100644 inc/compatibility/class-forcelogin.php create mode 100644 inc/compatibility/class-officialfacebookpixel.php create mode 100644 inc/compatibility/class-polylang.php create mode 100644 inc/compatibility/class-redirection.php create mode 100644 inc/compatibility/class-sliderrevolution.php create mode 100644 inc/compatibility/class-tweetoldpost.php create mode 100644 inc/compatibility/class-woozone.php create mode 100644 inc/compatibility/class-wprocket.php create mode 100644 inc/compatibility/class-yithwoocommerce.php create mode 100644 inc/compatibility/fixes/class-acceleratedmobilepagesfix.php create mode 100644 inc/compatibility/fixes/class-addfilterfix.php create mode 100644 inc/compatibility/fixes/class-auth0fix.php create mode 100644 inc/compatibility/fixes/class-autoptimizefix.php create mode 100644 inc/compatibility/fixes/class-defineconstantfix.php create mode 100644 inc/compatibility/fixes/class-deletefilefix.php create mode 100644 inc/compatibility/fixes/class-selfupdatingthemesfix.php create mode 100644 inc/compatibility/fixes/class-setserverportfix.php create mode 100644 inc/compatibility/fixes/class-sliderrevolutionfix.php create mode 100644 inc/compatibility/fixes/class-updatevaluefix.php create mode 100644 inc/compatibility/fixes/class-wprocketfix.php create mode 100644 inc/compatibility/fixes/class-yithchangepdflocationfix.php create mode 100644 tests/phpunit/test-compatibility-layer.php diff --git a/README.md b/README.md index 875cc95..2470f9b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ What does that mean? We're glad you asked! ### Maintenance Mode **Put your site into a maintenance mode.** Prevent users from accessing your sites during major updates by enabling Maintenance Mode either in the WordPress admin or via WP-CLI. +### Compatibility Layer +**Ensure your WordPress website is compatible with Pantheon.** Automatically apply & report status of compatibility fixes for common issues that arise when running WordPress on Pantheon. + ## Hooks The Pantheon Must-Use Plugin provides the following hooks that can be used in your code: diff --git a/inc/compatibility/base.php b/inc/compatibility/base.php new file mode 100644 index 0000000..fb19883 --- /dev/null +++ b/inc/compatibility/base.php @@ -0,0 +1,276 @@ +is_plugin_active() ) { + $this->activate(); + } + } + + /** + * Check if the plugin is active by $plugin_slug parameter. + * + * @return bool + */ + protected function is_plugin_active() { + return ( static::$plugin_slug && is_plugin_active( static::$plugin_slug ) ); + } + + /** + * Set up compatibility hooks and persists compatibility status. + * + * @return void + */ + protected function activate() { + $plugin_methods = []; + // Fix will run only one time on plugin activation. + if ( $this->run_on_plugin_activation ) { + $this->run_on_plugin_activation(); + $plugin_methods[] = 'run_on_plugin_activation'; + } + + // Fix will run only one time after plugin activation. + if ( $this->run_after_plugin_activation ) { + $this->add_action_after_plugin_activation(); + $plugin_methods[] = 'run_after_plugin_activation'; + } + + // Fix will run only on dashboard everytime. + if ( $this->run_fix_on_dashboard_only && is_admin() ) { + $this->run_fix_on_dashboard_only(); + $plugin_methods[] = 'run_fix_on_dashboard_only'; + } + + // Fix will run only on frontend everytime. + if ( $this->run_fix_on_frontend_only && ! is_admin() ) { + $this->run_fix_on_frontend_only(); + $plugin_methods[] = 'run_fix_on_frontend_only'; + } + + // Fix will run everytime either frontend or dashboard. + if ( $this->run_fix_everytime ) { + $this->run_fix_everytime(); + $plugin_methods[] = 'run_fix_everytime'; + } + + $this->persist_data( $plugin_methods ); + } + + /** + * Trigger compatibility layer on plugin activation. + * + * @return void + */ + protected function run_on_plugin_activation() { + $this->apply_fix(); + } + + /** + * Apply the compatibility fix. + * + * @return mixed + */ + abstract public function apply_fix(); + + /** + * Register after plugin activation hooks. + * + * @return void + */ + protected function add_action_after_plugin_activation() { + add_action( 'activated_plugin', function ( $plugin ) { + if ( static::$plugin_slug === $plugin ) { + // This code will run after the plugin has been activated. + $this->run_after_plugin_activation(); + } + }, PHP_INT_MAX ); + } + + /** + * Trigger compatibility layer after plugin activation. + * + * @return void + */ + protected function run_after_plugin_activation() { + $this->apply_fix(); + } + + /** + * Trigger compatibility layer on WP Dashboard. + * + * @return void + */ + protected function run_fix_on_dashboard_only() { + $this->apply_fix(); + } + + /** + * Trigger compatibility layer on WP frontend. + * + * @return void + */ + protected function run_fix_on_frontend_only() { + $this->apply_fix(); + } + + /** + * Trigger compatibility layer on each request. + * + * @return void + */ + protected function run_fix_everytime() { + $this->apply_fix(); + } + + /** + * Persist the compatibility layer data to the database. + * + * @return void + */ + protected function persist_data( array $plugin_methods = [] ) { + $pantheon_applied_fixes = get_option( 'pantheon_applied_fixes' ) ?: []; + $old = $pantheon_applied_fixes[ static::$plugin_slug ] ?? []; + $pantheon_applied_fixes[ static::$plugin_slug ] = [ + 'plugin_slug' => static::$plugin_slug, + 'plugin_name' => static::$plugin_name, + 'plugin_status' => $plugin_methods ? 'automated' : 'waiting', + 'plugin_message' => esc_html__( 'Manual fixes can be safely removed.', 'pantheon' ), + 'plugin_class' => static::class, + 'plugin_methods' => implode( ',', $plugin_methods ), + 'plugin_timestamp' => time(), + ]; + // Update the option with the modified array. + if ( $pantheon_applied_fixes[ static::$plugin_slug ] !== $old ) { + update_option( 'pantheon_applied_fixes', $pantheon_applied_fixes ); + } + } + + /** + * Check if the plugin is installed. + * + * @return bool + */ + public function is_plugin_installed() { + return file_exists( WP_PLUGIN_DIR . '/' . static::$plugin_slug ); + } + + /** + * Rollback the compatibility layer. + * + * @return void + */ + public function deactivate() { + $this->remove_fix(); + $this->remove_persisted_data(); + } + + /** + * Remove the compatibility layer fix. + * + * @return mixed + */ + abstract public function remove_fix(); + + /** + * Remove the compatibility layer's persisted data. + * + * @return void + */ + protected function remove_persisted_data() { + // Retrieve the array of persisted fixes. + $pantheon_applied_fixes = get_option( 'pantheon_applied_fixes' ); + + // Check if the plugin's data exists in the persisted fixes. + if ( isset( $pantheon_applied_fixes[ static::$plugin_slug ] ) ) { + // Remove the plugin's data from the array. + unset( $pantheon_applied_fixes[ static::$plugin_slug ] ); + + // Update the option with the modified array. + update_option( 'pantheon_applied_fixes', $pantheon_applied_fixes ); + } + } +} diff --git a/inc/compatibility/class-acceleratedmobilepages.php b/inc/compatibility/class-acceleratedmobilepages.php new file mode 100644 index 0000000..8641100 --- /dev/null +++ b/inc/compatibility/class-acceleratedmobilepages.php @@ -0,0 +1,37 @@ +check_threshold ) ) { + return true; + } + + // bail if the check_threshold is not equal to the default value. + if ( $this->default_threshold_value !== (int) $options->check_threshold ) { + return false; + } + + return true; + } +} diff --git a/inc/compatibility/class-compatibilityfactory.php b/inc/compatibility/class-compatibilityfactory.php new file mode 100644 index 0000000..0312798 --- /dev/null +++ b/inc/compatibility/class-compatibilityfactory.php @@ -0,0 +1,193 @@ +require_files(); + $this->setup_targets(); + + add_action( 'plugins_loaded', [ $this, 'init' ] ); + add_action( 'pantheon_cron', [ $this, 'daily_pantheon_cron' ] ); + } + + /** + * Require all the compatibility layer files. + * + * @return void + */ + private function require_files() { + require_once __DIR__ . '/base.php'; + foreach ( glob( __DIR__ . '/*.php' ) as $file ) { + if ( in_array( $file, [ 'base.php', basename( __FILE__ ) ], true ) ) { + continue; + } + require_once $file; + } + + foreach ( glob( __DIR__ . '/fixes/*.php' ) as $file ) { + require_once $file; + } + } + + /** + * Build the list of target plugins, + * with values for plugins slugs and names, keyed by class names. + * + * @return void + */ + private function setup_targets() { + static::$targets = [ + AcceleratedMobilePages::class => [ 'slug' => 'accelerated-mobile-pages/accelerated-mobile-pages.php' ], + Auth0::class => [ 'slug' => 'auth0/WP_Auth0.php' ], + Autoptimize::class => [ 'slug' => 'autoptimize/autoptimize.php' ], + BetterSearchReplace::class => [ 'slug' => 'better-search-replace/better-search-replace.php' ], + BrokenLinkChecker::class => [ 'slug' => 'broken-link-checker/broken-link-checker.php' ], + ContactFormSeven::class => [ 'slug' => 'contact-form-7/wp-contact-form-7.php' ], + EventEspresso::class => [ 'slug' => 'event-espresso-decaf/espresso.php' ], + FastVelocityMinify::class => [ 'slug' => 'fast-velocity-minify/fvm.php' ], + ForceLogin::class => [ 'slug' => 'wp-force-login/wp-force-login.php' ], + OfficialFacebookPixel::class => [ 'slug' => 'official-facebook-pixel/facebook-for-wordpress.php' ], + Polylang::class => [ 'slug' => 'polylang/polylang.php' ], + Redirection::class => [ 'slug' => 'redirection/redirection.php' ], + SliderRevolution::class => [ 'slug' => 'slider-revolution/slider-revolution.php' ], + TweetOldPost::class => [ 'slug' => 'tweet-old-post/tweet-old-post.php' ], + WPRocket::class => [ 'slug' => 'wp-rocket/wp-rocket.php' ], + WooZone::class => [ 'slug' => 'woozone/plugin.php' ], + YITHWoocommerce::class => [ 'slug' => 'yith-woocommerce-request-a-quote/yith-woocommerce-request-a-quote.php' ], + ]; + $this->add_names_to_targets(); + } + + /** + * Add plugin names to the target plugins array. + * + * @return void + */ + private function add_names_to_targets() { + if ( ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + array_walk( static::$targets, static function ( &$plugin ) { + $file = WP_PLUGIN_DIR . '/' . $plugin['slug']; + if ( ! file_exists( $file ) ) { + $plugin['name'] = $plugin['slug']; + + return; + } + $plugin_data = get_plugin_data( $file, false, false ); + $plugin['name'] = $plugin_data['Name']; + }); + } + + /** + * Get an instance of the class. + * + * @return CompatibilityFactory + */ + public static function get_instance() { + if ( ! self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Instantiate classes and register cron job. + * + * @access public + * + * @return void + */ + public function init() { + if ( ! function_exists( 'is_plugin_active' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $this->instantiate_compatibility_layers(); + + if ( ! wp_next_scheduled( 'pantheon_cron' ) ) { + wp_schedule_event( time(), 'daily', 'pantheon_cron' ); + } + } + + /** + * Instantiate compatibility layer classes. + * + * @return void + */ + private function instantiate_compatibility_layers() { + foreach ( static::$targets as $class => $plugin ) { + new $class( $plugin['slug'] ); + } + } + + /** + * Fallback method to apply fixes hooked to a daily cron job. + * + * @return void + */ + public function daily_pantheon_cron() { + $compat_classes = static::$targets; + // get list of applied fixes. + $pantheon_applied_fixes = get_option( 'pantheon_applied_fixes' ) ?: []; + // filter list of active plugins by fix availability & fix status, then initialize compatibility layers. + array_map( static function ( $plugin ) use ( $compat_classes, $pantheon_applied_fixes ) { + $compat_layer = array_search( + $plugin, + array_combine( array_keys( $compat_classes ), array_column( $compat_classes, 'slug' ) ), + true + ); + if ( + array_key_exists( $plugin, $pantheon_applied_fixes ) || + ! $compat_layer + ) { + return; + } + $instance = new $compat_layer( $plugin ); + $instance->apply_fix(); + }, (array) get_option( 'active_plugins' ) ?: [] ); + } +} diff --git a/inc/compatibility/class-contactformseven.php b/inc/compatibility/class-contactformseven.php new file mode 100644 index 0000000..6812613 --- /dev/null +++ b/inc/compatibility/class-contactformseven.php @@ -0,0 +1,39 @@ +getSdk(); + $config = $sdk->configuration(); + $storage_id = 'STYXKEY_' . $config->getSessionStorageId(); + $transient_id = 'STYXKEY_' . $config->getTransientStorageId(); + $config->setSessionStorageId( $storage_id ); + $config->setTransientStorageId( $transient_id ); + $config->setSessionStorage( new CookieStore( $config, $config->getSessionStorageId() ) ); + $config->setTransientStorage( new CookieStore( $config, $config->getTransientStorageId() ) ); + $sdk->setConfiguration( $config ); + } +} diff --git a/inc/compatibility/fixes/class-autoptimizefix.php b/inc/compatibility/fixes/class-autoptimizefix.php new file mode 100644 index 0000000..7ee8946 --- /dev/null +++ b/inc/compatibility/fixes/class-autoptimizefix.php @@ -0,0 +1,34 @@ +$option_key = $option_value; + update_option( $option_name, json_encode( $options ) ); + } + + /** + * @param string $option_name + * @param string $option_key + * + * @return void + */ + public static function remove( $option_name, $option_key ) { + $options = json_decode( get_option( $option_name ) ); + unset( $options->$option_key ); + update_option( $option_name, json_encode( $options ) ); + } +} diff --git a/inc/compatibility/fixes/class-wprocketfix.php b/inc/compatibility/fixes/class-wprocketfix.php new file mode 100644 index 0000000..06a8398 --- /dev/null +++ b/inc/compatibility/fixes/class-wprocketfix.php @@ -0,0 +1,28 @@ + + +
+

+ +

+ +

+ Known Issues page.', 'pantheon' ) + ), + esc_url( 'https://docs.pantheon.io/plugins-known-issues' ) + ); + ?> +

+ +
+ + [ + 'label' => esc_html__( 'Automatic Fixes', 'pantheon' ), + 'description' => esc_html__( 'Compatibility with the following plugins has been automatically added.', 'pantheon' ), + 'fields' => get_option( 'pantheon_applied_fixes' ), + 'show_count' => true, + ], + 'manual' => [ + 'label' => esc_html__( 'Manual Fixes', 'pantheon' ), + 'description' => esc_html__( 'Compatibility with the following plugins needs to be manually applied.', 'pantheon' ), + 'fields' => get_compatibility_manual_fixes(), + 'show_count' => true, + ], + 'notes' => [ + 'label' => esc_html__( 'Needs Review', 'pantheon' ), + 'description' => esc_html__( 'Compatibility with the following plugins needs to be reviewed.', 'pantheon' ), + 'fields' => get_compatibility_review_fixes(), + 'show_count' => true, + ], + ]; + foreach ( $info as $section => $details ) : + if ( empty( $details['fields'] ) ) { + continue; + } + + ?> +

+ +

+ + + +
+
+ + + + + + + + + + '; + $values .= '
  • ' . $status . '
  • '; + $values .= '
  • ' . $message . '
  • '; + $values .= ''; + printf( + /* translators: %s: Plugin's name. */ + '', + esc_html( $field['plugin_name'] ), + wp_kses_post( $values ) + ); + } + ?> + + + [ + 'plugin_status' => esc_html__( 'Manual Fix Required', 'pantheon' ), + 'plugin_slug' => 'tuxedo-big-file-uploads/tuxedo_big_file_uploads.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#big-file-uploads' + ) + ), + ], + 'jetpack' => [ + 'plugin_status' => esc_html__( 'Manual Fix Required', 'pantheon' ), + 'plugin_slug' => 'jetpack/jetpack.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#jetpack' + ) + ), + ], + 'wordfence' => [ + 'plugin_status' => esc_html__( 'Manual Fix Required', 'pantheon' ), + 'plugin_slug' => 'wordfence/wordfence.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wordfence' + ) + ), + ], + 'wpml' => [ + 'plugin_status' => esc_html__( 'Manual Fix Required', 'pantheon' ), + 'plugin_slug' => 'sitepress-multilingual-cms/sitepress.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wpml-the-wordpress-multilingual-plugin' + ) + ), + ], + ]; + + return add_plugin_names_to_known_issues( + array_filter( $plugins, static function ( $plugin ) { + return in_array( $plugin['plugin_slug'], get_option( 'active_plugins' ), true ); + } ) + ); +} + +/** + * Query WP for plugin names to include in compatibility list. + * + * @param array $plugins + * + * @return array[] + */ +function add_plugin_names_to_known_issues( $plugins ) { + if ( ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + foreach ( $plugins as $key => $plugin ) { + $file = WP_PLUGIN_DIR . '/' . $plugin['plugin_slug']; + if ( ! file_exists( $file ) ) { + $plugins[ $key ]['plugin_name'] = ucfirst( $key ); + } else { + $plugin_data = get_plugin_data( $file, false, false ); + $plugins[ $key ]['plugin_name'] = $plugin_data['Name']; + } + } + + return $plugins; +} + +/** + * Get list of plugins that require review. + * + * @return array[] + */ +function get_compatibility_review_fixes() { + $plugins = [ + 'raptive-ads' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'raptive-ads/adthrive-ads.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#adthrive-ads' + ) + ), + ], + 'all-in-one-wp-migration' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'all-in-one-wp-migration/all-in-one-wp-migration.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#all-in-one-wp-migration' + ) + ), + ], + 'bookly' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'bookly-responsive-appointment-booking-tool/main.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#bookly' + ) + ), + ], + 'coming-soon' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'coming-soon/coming-soon.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#coming-soon' + ) + ), + ], + 'disable-json-api' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'disable-json-api/disable-json-api.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#disable-rest-api-and-require-jwt--oauth-authentication' + ) + ), + ], + 'divi-builder' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'divi-builder/divi-builder.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#divi-wordpress-theme--visual-page-builder' + ) + ), + ], + 'elementor' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'elementor/elementor.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#elementor' + ) + ), + ], + 'facetwp' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'facetwp/index.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#facetwp' + ) + ), + ], + 'cookie-law-info' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'cookie-law-info/cookie-law-info.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#gdpr-cookie-consent' + ) + ), + ], + 'h5p' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'h5p/h5p.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#h5p' + ) + ), + ], + 'hm-require-login' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'hm-require-login/hm-require-login.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#hm-require-login' + ) + ), + ], + 'hummingbird-performance' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'hummingbird-performance/wp-hummingbird.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#hummingbird' + ) + ), + ], + 'hyperdb' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'hyperdb/db.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#hyperdb' + ) + ), + ], + 'iwp-client' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'iwp-client/init.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#infinitewp' + ) + ), + ], + 'instashow' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'instashow/instashow.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#instashow' + ) + ), + ], + 'wp-maintenance-mode' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'wp-maintenance-mode/wp-maintenance-mode.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#maintenance-mode' + ) + ), + ], + 'worker' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'worker/init.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#managewp-worker' + ) + ), + ], + 'monarch' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'monarch/monarch.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#monarch-social-sharing' + ) + ), + ], + 'new-relic' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'new-relic/new-relic.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#new-relic-reporting-for-wordpress' + ) + ), + ], + 'object-sync-for-salesforce' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'object-sync-for-salesforce/object-sync-for-salesforce.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#object-sync-for-salesforce' + ) + ), + ], + 'one-click-demo-import' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'one-click-demo-import/one-click-demo-import.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#one-click-demo-import' + ) + ), + ], + 'posts-to-posts' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'posts-to-posts/posts-to-posts.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#posts-2-posts' + ) + ), + ], + 'query-monitor' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'query-monitor/query-monitor.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#query-monitor' + ) + ), + ], + 'site24x7' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'site24x7/site24x7.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#site24x7' + ) + ), + ], + 'wp-smush-pro' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'wp-smush-pro/wp-smush-pro.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#smush-pro' + ) + ), + ], + 'better-wp-security' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'better-wp-security/better-wp-security.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#solid-security-previously-ithemes-security' + ) + ), + ], + 'unbounce' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'unbounce/unbounce.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#unbounce-landing-pages' + ) + ), + ], + 'unyson' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'unyson/unyson.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#unyson-theme-framework' + ) + ), + ], + 'updraftplus' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'updraftplus/updraftplus.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#updraft--updraft-plus-backup' + ) + ), + ], + 'weather-station' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'weather-station/weather-station.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#weather-station' + ) + ), + ], + 'webp-express' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'webp-express/webp-express.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#webp-express' + ) + ), + ], + 'woocommerce' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'woocommerce/woocommerce.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#woocommerce' + ) + ), + ], + 'download-manager' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'download-manager/download-manager.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wordpress-download-manager' + ) + ), + ], + 'wp-all-import' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'wp-all-import/wp-all-import.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wp-all-import--export' + ) + ), + ], + 'wp-migrate-db' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'wp-migrate-db/wp-migrate-db.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wp-migrate-db' + ) + ), + ], + 'wp-phpmyadmin' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'wp-phpmyadmin/wp-phpmyadmin.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wp-phpmyadmin' + ) + ), + ], + 'wp-reset' => [ + 'plugin_status' => esc_html__( 'Incompatible', 'pantheon' ), + 'plugin_slug' => 'wp-reset/wp-reset.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wp-reset' + ) + ), + ], + 'wp-ban' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'wp-ban/wp-ban.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wp-ban' + ) + ), + ], + 'wpfront-notification-bar' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'wpfront-notification-bar/wpfront-notification-bar.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#wpfront-notification-bar' + ) + ), + ], + 'yoast-seo' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'wordpress-seo/wp-seo.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#yoast-seo' + ) + ), + ], + 'yoast-indexables' => [ + 'plugin_status' => esc_html__( 'Partial Compatibility', 'pantheon' ), + 'plugin_slug' => 'yoast-seo/wp-seo.php', + 'plugin_message' => wp_kses_post( + sprintf( + /* translators: %s: the link to relevant documentation. */ + __( 'Read more about the issue here.', 'pantheon' ), + 'https://docs.pantheon.io/plugins-known-issues#yoast-indexables' + ) + ), + ], + ]; + + return add_plugin_names_to_known_issues( + array_filter( $plugins, static function ( $plugin ) { + return in_array( $plugin['plugin_slug'], get_option( 'active_plugins' ), true ); + } ) + ); } /** * Modify the Site Health tests. * * @param array $tests The Site Health tests. + * * @return array */ function site_health_mods( $tests ) { @@ -24,19 +753,101 @@ function site_health_mods( $tests ) { unset( $tests['direct']['update_temp_backup_writable'] ); unset( $tests['direct']['available_updates_disk_space'] ); unset( $tests['async']['background_updates'] ); + + return $tests; +} + +/** + * Add compatibility checks. + * + * @param array $tests The Site Health tests. + * + * @return array + */ +function compatibility_checks( $tests ) { + $tests['async']['compatibility'] = [ + 'label' => __( 'Pantheon Compatibility', 'pantheon' ), + 'test' => 'test-compatibility', + 'has_rest' => false, + 'async_direct_test' => __NAMESPACE__ . '\\test_compatibility', + ]; + return $tests; } +/** + * Check for compatibility issues and return it as a JSON response. + */ +function test_compatibility_ajax() { + wp_send_json_success( test_compatibility() ); +} + +/** + * Check for compatibility issues. + * + * @return array + */ +function test_compatibility() { + $manual_fixes = get_compatibility_manual_fixes(); + $review_fixes = get_compatibility_review_fixes(); + + if ( empty( $manual_fixes ) && empty( $review_fixes ) ) { + return [ + 'label' => __( 'Your site is stable', 'pantheon' ), + 'status' => 'good', + 'badge' => [ + 'label' => __( 'Pantheon', 'pantheon' ), + 'color' => 'green', + ], + 'description' => __( 'There are no known compatibility issues with your active plugins.', 'pantheon' ), + 'test' => 'compatibility', + ]; + } + + if ( ! empty( $manual_fixes ) ) { + return [ + 'label' => __( 'One or more active plugins require a manual compatibility fix', 'pantheon' ), + 'status' => 'critical', + 'badge' => [ + 'label' => __( 'Pantheon', 'pantheon' ), + 'color' => 'red', + ], + 'description' => sprintf( + '

    %s

    %s', + __( 'There are known compatibility issues with your active plugins that require manual fixes.', 'pantheon' ), + output_compatibility_status_table( $manual_fixes, false ) + ), + 'test' => 'compatibility', + ]; + } + + return [ + 'label' => __( 'You should review active plugins for compatibility issues', 'pantheon' ), + 'status' => 'recommended', + 'badge' => [ + 'label' => __( 'Pantheon', 'pantheon' ), + 'color' => 'orange', + ], + 'description' => sprintf( + '

    %s

    %s', + __( 'One or more active plugins may be incompatible or only partially compatible with Pantheon.', 'pantheon' ), + output_compatibility_status_table( $review_fixes, false ) + ), + 'test' => 'compatibility', + ]; +} + /** * Add object cache tests. * * @param array $tests The Site Health tests. + * * @return array */ function object_cache_tests( $tests ) { $tests['direct']['object_cache'] = [ 'label' => __( 'Object Cache', 'pantheon' ), - 'test' => 'test_object_cache', + 'test' => __NAMESPACE__ . '\\test_object_cache', ]; return $tests; diff --git a/pantheon.php b/pantheon.php index 08d756e..61a3e3f 100644 --- a/pantheon.php +++ b/pantheon.php @@ -3,14 +3,14 @@ * Plugin Name: Pantheon * Plugin URI: https://pantheon.io/ * Description: Building on Pantheon's and WordPress's strengths, together. - * Version: 1.4.5 + * Version: 1.5.0 * Author: Pantheon * Author URI: https://pantheon.io/ * * @package pantheon */ -define( 'PANTHEON_MU_PLUGIN_VERSION', '1.4.5' ); +define( 'PANTHEON_MU_PLUGIN_VERSION', '1.5.0' ); if ( isset( $_ENV['PANTHEON_ENVIRONMENT'] ) ) { require_once 'inc/functions.php'; @@ -51,4 +51,9 @@ require_once 'inc/pantheon-multisite-finalize.php'; } } + + if ( ! defined( 'PANTHEON_COMPATIBILITY' ) || PANTHEON_COMPATIBILITY ) { + require_once 'inc/compatibility/class-compatibilityfactory.php'; + Pantheon\Compatibility\CompatibilityFactory::get_instance(); + } } // Ensuring that this is on Pantheon. diff --git a/tests/phpunit/test-compatibility-layer.php b/tests/phpunit/test-compatibility-layer.php new file mode 100644 index 0000000..c47071e --- /dev/null +++ b/tests/phpunit/test-compatibility-layer.php @@ -0,0 +1,90 @@ +original_active_plugins = get_option( 'active_plugins' ); + $this->compatibility_factory = CompatibilityFactory::get_instance(); + } + + public function tearDown(): void { + parent::tearDown(); + update_option( 'active_plugins', $this->original_active_plugins ); + } + + public function test_registered_cron_schedule() { + $this->assertIsObject( wp_get_scheduled_event( 'pantheon_cron' ) ); + } + + public function test_add_names_to_targets() { + foreach ( CompatibilityFactory::$targets as $target ) { + $this->assertArrayHasKey( 'name', $target ); + } + } + + public function test_get_instance() { + $this->assertInstanceOf( CompatibilityFactory::class, $this->compatibility_factory ); + } + + public function test_instantiate_compatibility_layers() { + foreach ( CompatibilityFactory::$targets as $class => $plugin ) { + $this->assertTrue( class_exists( $class ) ); + } + } + + public function test_compatibility_hooks() { + $this->set_active_plugin( 'wp-force-login/wp-force-login.php' ); + CompatibilityFactory::get_instance(); + global $wp_filter; + $this->assertTrue( array_key_exists( 'deactivate_wp-force-login/wp-force-login.php', $wp_filter ) ); + $hooked_functions = array_column($wp_filter['deactivate_wp-force-login/wp-force-login.php']->callbacks[10], + 'function'); + $function_names = array_column( $hooked_functions, 0 ); + $this->assertInstanceOf( ForceLogin::class, $function_names[0] ); + } + + private function set_active_plugin( $plugin ) { + update_option( 'active_plugins', $plugin ); + wp_cache_delete( 'plugins', 'plugins' ); + } + + public function test_daily_pantheon_cron() { + $this->set_active_plugin( 'wp-force-login/wp-force-login.php' ); + $this->compatibility_factory->daily_pantheon_cron(); + $applied_fixes = get_option( 'pantheon_applied_fixes' ); + + $this->assertIsArray( $applied_fixes ); + $this->assertArrayHasKey( 'wp-force-login/wp-force-login.php', $applied_fixes ); + } +}