diff --git a/.github/workflows/github-action-publish-compat-checker.yml b/.github/workflows/github-action-publish-compat-checker.yml new file mode 100644 index 00000000..589495dc --- /dev/null +++ b/.github/workflows/github-action-publish-compat-checker.yml @@ -0,0 +1,20 @@ +name: Publish Compat Checker package +on: + push: + branches: + - trunk + +jobs: + deploy: + name: Publish Compat Checker package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Deploy + uses: s0/git-publish-subdir-action@develop + env: + REPO: self + BRANCH: compat-checker + FOLDER: packages/php/compat-checker + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c173028b..98aa1de4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ vendor/ # All zip files *.zip + +# IDE files +.vscode/* \ No newline at end of file diff --git a/README.md b/README.md index 6e589125..524611f3 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The packages here are too experimental or too Grow-specific to be shared Woo-wid - [`/packages/js/generator-grow`](packages/js/generator-grow/README.md) - Yeoman Generator for extension repository boilerplate. - [`/packages/js/storybook`](packages/js/storybook/README.md) - Storybook dependencies and basic scripts - [`/packages/js/tracking-jsdoc`](packages/js/tracking-jsdoc/README.md) - `jsdoc` plugin to document Tracking Events in markdown +- [`/packages/php/compat-checker`](packages/php/compat-checker/README.md) - Library to run compatibility checks for WooCommerce extensions. ## List of plugins diff --git a/packages/php/compat-checker/README.md b/packages/php/compat-checker/README.md new file mode 100644 index 00000000..52c1ecce --- /dev/null +++ b/packages/php/compat-checker/README.md @@ -0,0 +1,42 @@ +# Compat Checker for WooCommerce Extensions + +A simple library to run compatibility checks for WooCommerce extensions. + +## Getting Started + +1. Include this library in your WooCommerce plugin's `composer.json` like shown below: + +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/woocommerce/grow" + } + ], + "require": { + "woocommerce/grow": "dev-compat-checker" + } +} +``` +2. Run `composer update` to include the `woocommerce/grow` repo in the `vendor` folder. + +3. In the main plugin file that contains the plugin header, add the compatibility check like the below example: + +```php +require __DIR__ . '/vendor/autoload.php'; + +use Automattic\WooCommerce\Grow\Tools\CompatChecker\v0_0_1\Checker; + +add_action( 'plugins_loaded', 'wc_plugin_init' ); + +function wc_plugin_init() { + define( 'WC_BRANDS_VERSION', '1.6.56' ); // WRCS: DEFINED_VERSION. + + if ( ! Checker::instance()->is_compatible( __FILE__, WC_BRANDS_VERSION ) ) { + return; + } + + // Continue initializing the plugin. +} +``` \ No newline at end of file diff --git a/packages/php/compat-checker/composer.json b/packages/php/compat-checker/composer.json new file mode 100644 index 00000000..a97c6755 --- /dev/null +++ b/packages/php/compat-checker/composer.json @@ -0,0 +1,21 @@ +{ + "name": "woogrow/compat-checker", + "description": "Compatibility checker for Woo Grow extensions", + "version": "0.0.1", + "license": "GPL-3.0", + "minimum-stability": "stable", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Automattic\\WooCommerce\\Grow\\Tools\\CompatChecker\\v0_0_1\\": "src/" + } + }, + "require-dev": { + "woocommerce/woocommerce-sniffs": "^0.1.3" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/packages/php/compat-checker/composer.lock b/packages/php/compat-checker/composer.lock new file mode 100644 index 00000000..92a1d159 --- /dev/null +++ b/packages/php/compat-checker/composer.lock @@ -0,0 +1,420 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d2bf099e9e6b95c169258509897c9198", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.2", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2022-02-04T12:51:07+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "time": "2022-10-25T01:46:02+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "time": "2022-10-24T09:00:36+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2023-02-22T23:07:41+00:00" + }, + { + "name": "woocommerce/woocommerce-sniffs", + "version": "0.1.3", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-sniffs.git", + "reference": "4576d54595614d689bc4436acff8baaece3c5bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/4576d54595614d689bc4436acff8baaece3c5bb0", + "reference": "4576d54595614d689bc4436acff8baaece3c5bb0", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "php": ">=7.0", + "phpcompatibility/phpcompatibility-wp": "^2.1.0", + "wp-coding-standards/wpcs": "^2.3.0" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Claudio Sanches", + "email": "claudio@automattic.com" + } + ], + "description": "WooCommerce sniffs", + "keywords": [ + "phpcs", + "standards", + "woocommerce", + "wordpress" + ], + "support": { + "issues": "https://github.com/woocommerce/woocommerce-sniffs/issues", + "source": "https://github.com/woocommerce/woocommerce-sniffs/tree/0.1.3" + }, + "time": "2022-02-17T15:34:51+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7da1894633f168fe244afc6de00d141f27517b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", + "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.3.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", + "phpcompatibility/php-compatibility": "^9.0", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "time": "2020-05-13T23:57:56+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/packages/php/compat-checker/phpcs.xml b/packages/php/compat-checker/phpcs.xml new file mode 100644 index 00000000..473e1ad4 --- /dev/null +++ b/packages/php/compat-checker/phpcs.xml @@ -0,0 +1,20 @@ + + + Compat Checker Tool ruleset. + + + + + + + + + + + + + + + tests/ + + \ No newline at end of file diff --git a/packages/php/compat-checker/src/Checker.php b/packages/php/compat-checker/src/Checker.php new file mode 100644 index 00000000..8ded58ae --- /dev/null +++ b/packages/php/compat-checker/src/Checker.php @@ -0,0 +1,87 @@ + 'Plugin Name', + 'Version' => 'Version', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'RequiresWC' => 'WC requires at least', + 'TestedWP' => 'Tested up to', + 'TestedWC' => 'WC tested up to', + ); + + $transient_key = 'wc_grow_compat_checker_' . plugin_basename( $plugin_file ) . $file_version; + $plugin_data = get_transient( $transient_key ); + + if ( false === $plugin_data ) { + $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' ); + set_transient( $transient_key, $plugin_data, MONTH_IN_SECONDS ); + } + + return $plugin_data; + } + + /** + * Runs all compatibility checks. + * + * @param string $plugin_file_path The Absolute path to the main plugin file. + * @param string $file_version The plugin file version. Can be the same as the plugin version. + * + * @return bool + */ + public function is_compatible( $plugin_file_path, $file_version ) { + $checks = array( + WPCompatibility::class, + WCCompatibility::class, + ); + $plugin_data = $this->get_plugin_data( $plugin_file_path, $file_version ); + + foreach ( $checks as $compatibility ) { + if ( ! $compatibility::instance()->is_compatible( $plugin_data ) ) { + return false; + } + } + + return true; + } +} diff --git a/packages/php/compat-checker/src/Checks/CompatCheck.php b/packages/php/compat-checker/src/Checks/CompatCheck.php new file mode 100644 index 00000000..2733cef7 --- /dev/null +++ b/packages/php/compat-checker/src/Checks/CompatCheck.php @@ -0,0 +1,167 @@ +id ) && ! in_array( $screen->id, $hidden, true ); + + /** + * The Compat Check filter to show an admin notice. + * + * @since 0.0.1 + * + * @param bool $show Whether to show the admin notice. + * @param string $slug The slug for the notice. + */ + if ( ! apply_filters( 'wc_grow_compat_check_show_admin_notice', $show, $slug ) ) { + return; + } + + $this->notices[ $slug ] = array( + 'class' => $class, + 'message' => $message, + ); + } + + /** + * Compares major version. + * + * @param string $left First version number. + * @param string $right Second version number. + * @param string $operator An optional operator. The possible operators are: <, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne respectively. + * + * @return int|bool + */ + protected function compare_major_version( $left, $right, $operator = null ) { + $pattern = '/^(\d+\.\d+).*/'; + $replace = '$1.0'; + + $left = preg_replace( $pattern, $replace, $left ); + $right = preg_replace( $pattern, $replace, $right ); + + return version_compare( $left, $right, $operator ); + } + + /** + * Display admin notices generated by the checker. + */ + public function display_admin_notices() { + $allowed_tags = array( + 'a' => array( + 'class' => array(), + 'href' => array(), + 'target' => array(), + ), + 'strong' => array(), + ); + + foreach ( $this->notices as $key => $notice ) { + $class = $notice['class']; + $message = $notice['message']; + echo sprintf( + '

%2$s

', + esc_attr( $class ), + wp_kses( $message, $allowed_tags ) + ); + } + } + + /** + * Sets the plugin data. + * + * @param array $plugin_data The plugin data. + */ + protected function set_plugin_data( $plugin_data ) { + $defaults = array( + 'Name' => '', + 'Version' => '', + 'RequiresWP' => '', + 'RequiresPHP' => '', + 'RequiresWC' => '', + 'TestedWP' => '', + 'TestedWC' => '', + ); + $this->plugin_data = wp_parse_args( $plugin_data, $defaults ); + } + + /** + * Determines if the plugin is WooCommerce compatible. + * + * @param array $plugin_data The plugin data. + * + * @return bool + */ + public function is_compatible( $plugin_data ) { + $this->set_plugin_data( $plugin_data ); + add_action( 'admin_notices', array( $this, 'display_admin_notices' ), 20 ); + return $this->run_checks(); + } +} diff --git a/packages/php/compat-checker/src/Checks/WCCompatibility.php b/packages/php/compat-checker/src/Checks/WCCompatibility.php new file mode 100644 index 00000000..3f345e7d --- /dev/null +++ b/packages/php/compat-checker/src/Checks/WCCompatibility.php @@ -0,0 +1,388 @@ +get_wc_version(); + $wc_version_required = $this->plugin_data['RequiresWC']; + + if ( empty( $wc_version_required ) ) { + return true; + } + + return version_compare( $wc_version, $wc_version_required, '>=' ); + } + + /** + * Determines if the WooCommerce version is untested. + * + * @return bool + */ + private function is_wc_untested() { + if ( empty( $this->plugin_data['TestedWC'] ) ) { + return false; + } + + $wc_version = $this->get_wc_version(); + $wc_version_tested_upto = $this->plugin_data['TestedWC']; + + return $this->compare_major_version( $wc_version, $wc_version_tested_upto, '<=' ); + } + + /** + * Check WooCommerce installation and activation. + * + * @return bool + * @throws IncompatibleException If WooCommerce is not activated. + */ + private function check_wc_installation_and_activation() { + if ( ! $this->is_wc_activated() ) { + add_action( 'admin_notices', array( $this, 'wc_fail_load' ) ); + throw new IncompatibleException( esc_html__( 'WooCommerce is not installed or activated.', 'woogrow-compat-checker' ) ); + } + return true; + } + + /** + * Check WooCommerce version. + * + * @return bool + * @throws IncompatibleException If WooCommerce version is not compatible. + */ + private function check_wc_version() { + if ( ! $this->is_wc_compatible() ) { + add_action( 'admin_notices', array( $this, 'wc_out_of_date' ) ); + throw new IncompatibleException( esc_html__( 'WooCommerce version not compatible.', 'woogrow-compat-checker' ) ); + } + + if ( ! $this->is_wc_untested() ) { + add_action( 'admin_notices', array( $this, 'wc_untested' ) ); + } + + return true; + } + + /** + * Retrieves a list of the latest available WooCommerce versions. + * + * Excludes betas, release candidates and development versions. + * Versions are sorted from most recent to least recent. + * + * @return string[] Array of semver strings. + */ + private function get_latest_wc_versions() { + $latest_wc_versions = get_transient( 'compat_checker_wc_versions' ); + + if ( ! is_array( $latest_wc_versions ) ) { + + /** + * The endpoint to fetch the latest WooCommerce versions. + * + * @link https://codex.wordpress.org/WordPress.org_API + */ + $wp_org_request = wp_remote_get( 'https://api.wordpress.org/plugins/info/1.0/woocommerce.json', array( 'timeout' => 1 ) ); + + if ( is_array( $wp_org_request ) && isset( $wp_org_request['body'] ) ) { + + $plugin_info = json_decode( $wp_org_request['body'], true ); + + if ( is_array( $plugin_info ) && ! empty( $plugin_info['versions'] ) && is_array( $plugin_info['versions'] ) ) { + + $latest_wc_versions = array(); + + // Reverse the array as WordPress supplies oldest version first, newest last. + foreach ( array_keys( array_reverse( $plugin_info['versions'] ) ) as $wc_version ) { + + // Skip trunk, release candidates, betas and other non-final or irregular versions. + if ( + is_string( $wc_version ) + && '' !== $wc_version + && is_numeric( $wc_version[0] ) + && false === strpos( $wc_version, '-' ) + ) { + $latest_wc_versions[] = $wc_version; + } + } + + set_transient( 'compat_checker_wc_versions', $latest_wc_versions, WEEK_IN_SECONDS ); + } + } + } + + return is_array( $latest_wc_versions ) ? $latest_wc_versions : array(); + } + + /** + * Get the L-n version of WooCommerce. + * + * @return string + */ + private function get_supported_wc_version() { + + $latest_wc_versions = $this->get_latest_wc_versions(); + + if ( empty( $latest_wc_versions ) ) { + return ''; + } + + $latest_wc_version = current( $latest_wc_versions ); + $supported_wc_version = $latest_wc_version; + + $latest_semver = explode( '.', $latest_wc_version ); + $supported_semver = explode( '.', (string) $this->min_wc_semver ); + $supported_major = max( 0, (int) $latest_semver[0] - (int) $supported_semver[0] ); + $supported_minor = isset( $supported_semver[1] ) ? (int) $supported_semver[1] : 0; + $previous_minor = null; + + // Loop known WooCommerce versions from the most recent until we get the oldest supported one. + foreach ( $latest_wc_versions as $older_wc_version ) { + // As we loop through the versions, the latest one before we break the loop will be the minimum supported version. + $supported_wc_version = $older_wc_version; + + $older_semver = explode( '.', $older_wc_version ); + $older_major = (int) $older_semver[0]; + $older_minor = isset( $older_semver[1] ) ? (int) $older_semver[1]: 0; + + // if major is ignored, skip; if the minor hasn't changed (patch must be), skip. + if ( $older_major > $supported_major || $older_minor === $previous_minor ) { + continue; + } + + // We reached the maximum number of supported minor versions. + if ( $supported_minor <= 0 ) { + break; + } + + // Store the previous minor while we loop patch versions, which we ignore. + $previous_minor = $older_minor; + + $supported_minor--; + } + + return $supported_wc_version; + } + + /** + * Check for WooCommerce upgrade recommendation. + * + * @return bool + */ + private function check_wc_upgrade_recommendation() { + // Bail on frontend requests or if there is no definied versions to compare. + if ( ! is_admin() || empty( $this->min_wc_semver ) || ! is_numeric( $this->min_wc_semver ) ) { + return; + } + + $current_wc_version = $this->get_wc_version(); + $supported_wc_version = $this->get_supported_wc_version(); + + if ( ! $this->compare_major_version( $current_wc_version, $supported_wc_version, '>=' ) ) { + add_action( 'admin_notices', array( $this, 'make_upgrade_recommendation' ) ); + } + return true; + } + + /** + * Add notices for WooCommerce not being installed or activated. + */ + public function wc_fail_load() { + $plugin_name = $this->plugin_data['Name']; + + if ( $this->is_wc_installed() ) { + if ( ! current_user_can( 'activate_plugins' ) ) { + return; + } + + $activation_url = wp_nonce_url( 'plugins.php?action=activate&plugin=' . self::WC_PLUGIN_FILE . '&plugin_status=all&paged=1&s', 'activate-plugin_' . self::WC_PLUGIN_FILE ); + $message = sprintf( + /* translators: %1$s - Plugin Name, %2$s - activate WooCommerce link open, %3$s - activate WooCommerce link close. */ + esc_html__( '%1$s requires WooCommerce to be activated. Please %2$sactivate WooCommerce%3$s.', 'woogrow-compat-checker' ), + '' . $plugin_name . '', + '', + '' + ); + $this->add_admin_notice( + 'woocommerce-not-activated', + 'error', + $message + ); + } else { + if ( ! current_user_can( 'install_plugins' ) ) { + return; + } + + $install_url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=woocommerce' ), 'install-plugin_woocommerce' ); + $message = sprintf( + /* translators: %1$s - Plugin Name, %2$s - install WooCommerce link open, %3$s - install WooCommerce link close. */ + esc_html__( '%1$s requires WooCommerce to be installed and activated. Please %2$sinstall WooCommerce%3$s.', 'woogrow-compat-checker' ), + '' . $plugin_name . '', + '', + '' + ); + + $this->add_admin_notice( + 'woocommerce-not-installed', + 'error', + $message + ); + } + } + + /** + * Add notices for out of date WooCommerce. + */ + public function wc_out_of_date() { + if ( ! current_user_can( 'update_plugins' ) ) { + return; + } + + $plugin_name = $this->plugin_data['Name']; + $wc_version_required = ( 1 === substr_count( $this->plugin_data['RequiresWC'], '.' ) ) ? $this->plugin_data['RequiresWC'] . '.0' : $this->plugin_data['RequiresWC'] ; // Pad .0 if the min required WC version is not in semvar format. + + $message = sprintf( + /* translators: %1$s - Plugin Name, %2$s - minimum WooCommerce version, %3$s - update WooCommerce link open, %4$s - update WooCommerce link close, %5$s - download minimum WooCommerce link open, %6$s - download minimum WooCommerce link close. */ + esc_html__( '%1$s requires WooCommerce version %2$s or higher. Please %3$supdate WooCommerce%4$s to the latest version, or %5$sdownload the minimum required version »%6$s', 'woogrow-compat-checker' ), + '' . $plugin_name . '', + $wc_version_required, + '', + '', + '', + '' + ); + + $this->add_admin_notice( + 'woocommerce-out-of-date', + 'error', + $message + ); + } + + /** + * Adds notice for untested WooCommerce. + */ + public function wc_untested() { + $plugin_name = $this->plugin_data['Name']; + $wc_version = $this->get_wc_version(); + $plugin_version = $this->plugin_data['Version']; + + $message = sprintf( + /* translators: %1$s - Plugin Name, %2$s - Plugin version, %3$s - WooCommerce version number */ + esc_html__( '%1$s - version %2$s is untested with WooCommerce %3$s.', 'woogrow-compat-checker' ), + '' . $plugin_name . '', + $plugin_version, + $wc_version + ); + + $this->add_admin_notice( + 'woocommerce-untested', + 'warning is-dismissible', + $message + ); + } + + /** + * Adds notice for WooCommerce upgrade recommendation. + */ + public function make_upgrade_recommendation() { + // Bail if the user does not have the update plugins capability. + if ( ! current_user_can( 'update_plugins' ) ) { + return; + } + + $plugin_name = $this->plugin_data['Name']; + $current_wc_version = $this->get_wc_version(); + + $message = sprintf( + /* translators: Placeholders: %1$s - plugin name, %2$s - WooCommerce version number, %3$s - opening HTML link tag, %4$s - closing HTML link tag */ + esc_html__( 'Heads up! %1$s will soon discontinue support for WooCommerce %2$s. Please %3$supdate WooCommerce%4$s to take advantage of the latest updates and features.', 'woogrow-compat-checker' ), + '' . $plugin_name . '', + $current_wc_version, + '', + '' + ); + + $this->add_admin_notice( + 'woocommerce-upgrade-recommendation', + 'warning is-dismissible', + $message + ); + } + + /** + * Run all compatibility checks. + */ + protected function run_checks() { + try { + $this->check_wc_installation_and_activation(); + $this->check_wc_version(); + $this->check_wc_upgrade_recommendation(); + return true; + } catch ( IncompatibleException $e ) { + return false; + } + } +} diff --git a/packages/php/compat-checker/src/Checks/WPCompatibility.php b/packages/php/compat-checker/src/Checks/WPCompatibility.php new file mode 100644 index 00000000..ead034c2 --- /dev/null +++ b/packages/php/compat-checker/src/Checks/WPCompatibility.php @@ -0,0 +1,59 @@ +compare_major_version( $this->plugin_data['TestedWP'], $wp_version, '<' ) ) { + add_action( 'admin_notices', array( $this, 'wp_not_tested' ) ); + } + return true; + } + + /** + * Display WordPress version not tested warning. + */ + public function wp_not_tested() { + global $wp_version; + + $plugin_name = $this->plugin_data['Name']; + $plugin_version = $this->plugin_data['Version']; + + $message = sprintf( + /* translators: %1$s - Plugin Name, %2$s - Plugin version, %3$s - WordPress version number */ + esc_html__( '%1$s - %2$s is untested with WordPress %3$s.', 'woogrow-compat-checker' ), + '' . $plugin_name . '', + $plugin_version, + $wp_version + ); + + $this->add_admin_notice( + 'wordpress-untested', + 'warning is-dismissible', + $message + ); + } + + /** + * Run all compatibility checks. + */ + protected function run_checks() { + $this->check_wp_version(); + return true; + } +} diff --git a/packages/php/compat-checker/src/Exception/IncompatibleException.php b/packages/php/compat-checker/src/Exception/IncompatibleException.php new file mode 100644 index 00000000..fb3d602e --- /dev/null +++ b/packages/php/compat-checker/src/Exception/IncompatibleException.php @@ -0,0 +1,17 @@ +