diff --git a/composer.json b/composer.json index 2b54815..a5d8d2a 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "package": { "name": "bluehost/bluehost-wordpress-plugin", "type": "wordpress-plugin", - "version": "3.1.0", + "version": "3.14.8", "dist": { "url": "https://github.com/bluehost/bluehost-wordpress-plugin/releases/latest/download/bluehost-wordpress-plugin.zip", "type": "zip" @@ -105,6 +105,7 @@ }, "require": { "ext-json": "*", + "newfold-labs/wp-module-context": "^1.0", "newfold-labs/wp-module-loader": "^1.0.10", "wp-forge/helpers": "^2.0", "wp-forge/wp-query-builder": "^1.0.4", diff --git a/composer.lock b/composer.lock index 051fa58..9d16ab6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "487baf4658a4c1edb478db67f77eeebc", + "content-hash": "4bbcb858d87850fac8c58931c178bed8", "packages": [ { "name": "doctrine/inflector", @@ -140,6 +140,60 @@ }, "time": "2022-08-26T17:23:54+00:00" }, + { + "name": "newfold-labs/wp-module-context", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/newfold-labs/wp-module-context.git", + "reference": "fb57e927df45ef33573fe7f429ef1e088b0a6df0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/newfold-labs/wp-module-context/zipball/fb57e927df45ef33573fe7f429ef1e088b0a6df0", + "reference": "fb57e927df45ef33573fe7f429ef1e088b0a6df0", + "shasum": "" + }, + "require": { + "wp-forge/helpers": "^2.0" + }, + "require-dev": { + "newfold-labs/wp-php-standards": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "NewfoldLabs\\WP\\Context\\": "includes" + }, + "files": [ + "includes/functions.php", + "bootstrap.php" + ] + }, + "scripts": { + "fix": [ + "vendor/bin/phpcbf . --standard=phpcs.xml" + ], + "lint": [ + "vendor/bin/phpcs . --standard=phpcs.xml -s" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Evan Mullins", + "homepage": "https://evanmullins.com" + } + ], + "description": "Newfold module to determine context for various brands and platforms.", + "support": { + "source": "https://github.com/newfold-labs/wp-module-context/tree/1.0.1", + "issues": "https://github.com/newfold-labs/wp-module-context/issues" + }, + "time": "2024-05-23T20:34:28+00:00" + }, { "name": "newfold-labs/wp-module-loader", "version": "1.0.10", @@ -622,7 +676,7 @@ }, { "name": "bluehost/bluehost-wordpress-plugin", - "version": "3.1.0", + "version": "3.14.8", "dist": { "type": "zip", "url": "https://github.com/bluehost/bluehost-wordpress-plugin/releases/latest/download/bluehost-wordpress-plugin.zip" diff --git a/includes/Helpers/Transient.php b/includes/Helpers/Transient.php index 73cf395..1124140 100644 --- a/includes/Helpers/Transient.php +++ b/includes/Helpers/Transient.php @@ -11,31 +11,37 @@ class Transient { * * If the site has an object-cache.php drop-in, then we can't reliably * use the transients API. We'll try to fall back to the options API. - * - * @return boolean */ - public static function should_use_transients() { + protected static function should_use_transients(): bool { require_once constant( 'ABSPATH' ) . '/wp-admin/includes/plugin.php'; - return ! array_key_exists( 'object-cache.php', get_dropins() ); + return ! array_key_exists( 'object-cache.php', get_dropins() ) + || 'atomic' === \NewfoldLabs\WP\Context\getContext( 'platform' ); // Bluehost Cloud. } /** * Wrapper for get_transient() with Options API fallback * + * @see \get_transient() + * @see \get_option() + * @see \delete_option() + * * @param string $key The key of the transient to retrieve * @return mixed The value of the transient */ - public static function get( $key ) { + public static function get( string $key ) { if ( self::should_use_transients() ) { - return get_transient( $key ); + return \get_transient( $key ); } - $data = get_option( $key ); - if ( ! empty( $data ) && isset( $data['expires'] ) ) { - if ( $data['expires'] > time() ) { + /** + * @var array{value:mixed, expires_at:int} $data The saved value and the Unix time it expires at. + */ + $data = \get_option( $key ); + if ( is_array( $data ) && isset( $data['expires_at'], $data['value'] ) ) { + if ( $data['expires_at'] > time() ) { return $data['value']; } else { - delete_option( $key ); + \delete_option( $key ); } } @@ -45,35 +51,57 @@ public static function get( $key ) { /** * Wrapper for set_transient() with Options API fallback * - * @param string $key Key to use for storing the transient - * @param mixed $value Value to be saved - * @param integer $expires Optional expiration time in seconds from now. Default is 1 hour - * @return boolean Whether the value was saved + * @see \set_transient() + * @see \update_option() + * + * @param string $key Key to use for storing the transient + * @param mixed $value Value to be saved + * @param integer $expires_in Optional expiration time in seconds from now. Default is 1 hour + * + * @return bool Whether the value was saved */ - public static function set( $key, $value, $expires = null ) { - $expiration = ( $expires ) ? $expires : 60 * MINUTE_IN_SECONDS; + public static function set( string $key, $value, int $expires_in = 3600 ): bool { if ( self::should_use_transients() ) { - return set_transient( $key, $value, $expiration ); + return \set_transient( $key, $value, $expires_in ); } $data = array( - 'value' => $value, - 'expires' => $expiration + time(), + 'value' => $value, + 'expires_at' => $expires_in + time(), ); - return update_option( $key, $data, false ); + return \update_option( $key, $data, false ); } /** * Wrapper for delete_transient() with Options API fallback * + * @see \delete_transient() + * @see \delete_option() + * * @param string $key The key of the transient/option to delete - * @return boolean Whether the value was deleted + * @return bool Whether the value was deleted */ - public static function delete( $key ) { + public static function delete( $key ): bool { if ( self::should_use_transients() ) { - return delete_transient( $key ); + return \delete_transient( $key ); } - return delete_option( $key ); + return \delete_option( $key ); + } + + /** + * Make the static functions callable as instance methods. + * + * @param string $name The function name being called. + * @param array $arguments The arguments passed to that function. + * + * @return mixed + * @throws \BadMethodCallException If the method does not exist. + */ + public function __call( $name, $arguments ) { + if ( ! method_exists( __CLASS__, $name ) ) { + throw new \BadMethodCallException( "Method $name does not exist" ); + } + return self::$name( ...$arguments ); } } diff --git a/includes/SiteCapabilities.php b/includes/SiteCapabilities.php index b303064..9f01a1a 100644 --- a/includes/SiteCapabilities.php +++ b/includes/SiteCapabilities.php @@ -2,6 +2,8 @@ namespace NewfoldLabs\WP\Module\Data; +use NewfoldLabs\WP\Module\Data\Helpers\Transient; + /** * Class SiteCapabilities * @@ -12,52 +14,71 @@ class SiteCapabilities { /** - * Get all capabilities. + * Implementation of transient functionality which uses the WordPress options table when an object cache is present. * - * @return array + * @var Transient */ - public function all() { - $capabilities = get_transient( 'nfd_site_capabilities' ); - if ( false === $capabilities ) { - $capabilities = $this->fetch(); - set_transient( 'nfd_site_capabilities', $capabilities, 4 * HOUR_IN_SECONDS ); - } + protected $transient; - return $capabilities; + /** + * Constructor. + * + * @param ?Transient $transient Inject instance of Transient class. + */ + public function __construct( ?Transient $transient = null ) { + $this->transient = $transient ?? new Transient(); } /** - * Check if a capability exists. + * Get the value of a capability. + * + * @used-by \NewfoldLabs\WP\Module\AI\SiteGen\SiteGen::check_capabilities() + * @used-by \NewfoldLabs\WP\Module\AI\Utils\AISearchUtil::check_capabilities() + * @used-by \NewfoldLabs\WP\Module\AI\Utils\AISearchUtil::check_help_capability() + * @used-by \NewfoldLabs\WP\Module\ECommerce\ECommerce::__construct() + * @used-by \NewfoldLabs\WP\Module\HelpCenter\CapabilityController::get_capability() + * @used-by \NewfoldLabs\WP\Module\Onboarding\Data\Config::get_site_capability() * * @param string $capability Capability name. + */ + public function get( string $capability ): bool { + return $this->exists( $capability ) && $this->all()[ $capability ]; + } + + /** + * Get all capabilities. * - * @return bool + * @used-by \NewfoldLabs\WP\Module\Runtime\Runtime::prepareRuntime() */ - public function exists( $capability ) { - return array_key_exists( $capability, $this->all() ); + public function all(): array { + $capabilities = $this->transient->get( 'nfd_site_capabilities' ); + if ( false === $capabilities ) { + $capabilities = $this->fetch(); + $this->transient->set( 'nfd_site_capabilities', $capabilities, 4 * constant( 'HOUR_IN_SECONDS' ) ); + } + + return $capabilities; } /** - * Get the value of a capability. + * Check if a capability exists. * * @param string $capability Capability name. - * - * @return bool */ - public function get( $capability ) { - return $this->exists( $capability ) && $this->all()[ $capability ]; + protected function exists( string $capability ): bool { + return array_key_exists( $capability, $this->all() ); } /** * Fetch all capabilities from Hiive. * - * @return array + * @return array */ - public function fetch() { + protected function fetch(): array { $capabilities = array(); $response = wp_remote_get( - NFD_HIIVE_URL . '/sites/v1/capabilities', + constant( 'NFD_HIIVE_URL' ) . '/sites/v1/capabilities', array( 'headers' => array( 'Content-Type' => 'application/json', diff --git a/tests/phpunit/includes/Helpers/TransientTest.php b/tests/phpunit/includes/Helpers/TransientTest.php new file mode 100644 index 0000000..06e7bb0 --- /dev/null +++ b/tests/phpunit/includes/Helpers/TransientTest.php @@ -0,0 +1,226 @@ +once() + ->andReturn( array( 'not-object-cache.php' => array() ) ); + + \WP_Mock::userFunction( 'set_transient' ) + ->once() + ->with( $test_transient_name, 'value', 999 ) + ->andReturnTrue(); + + \WP_Mock::userFunction( 'update_option' ) + ->never(); + + Transient::set( $test_transient_name, 'value', 999 ); + + $this->assertConditionsMet(); + } + + /** + * @covers ::set + */ + public function test_set_transient_use_options(): void { + + $test_transient_name = uniqid( __FUNCTION__ ); + + \WP_Mock::userFunction( 'get_dropins' ) + ->once() + ->andReturn( array( 'object-cache.php' => array() ) ); + + \WP_Mock::userFunction( 'set_transient' ) + ->never(); + + \WP_Mock::userFunction( 'update_option' ) + ->once() + ->with( + $test_transient_name, + \WP_Mock\Functions::type( 'array' ), + false + ) + ->andReturnTrue(); + + Transient::set( $test_transient_name, 'value', 999 ); + + $this->assertConditionsMet(); + } + + /** + * @covers ::get + */ + public function test_get_transient_use_default(): void { + + $test_transient_name = uniqid( __FUNCTION__ ); + + \WP_Mock::userFunction( 'get_dropins' ) + ->once() + ->andReturn( array( 'not-object-cache.php' => array() ) ); + + \WP_Mock::userFunction( 'get_transient' ) + ->once() + ->with( $test_transient_name ) + ->andReturn( 'value' ); + + \WP_Mock::userFunction( 'get_option' ) + ->never(); + + $result = Transient::get( $test_transient_name ); + + $this->assertEquals( 'value', $result ); + } + + /** + * @covers ::get + */ + public function test_get_transient_use_options(): void { + + $test_transient_name = uniqid( __FUNCTION__ ); + + \WP_Mock::userFunction( 'get_dropins' ) + ->once() + ->andReturn( array( 'object-cache.php' => array() ) ); + + \WP_Mock::userFunction( 'get_transient' ) + ->never(); + + \WP_Mock::userFunction( 'get_option' ) + ->once() + ->with( $test_transient_name, ) + ->andReturn( + array( + 'value' => 'value', + 'expires_at' => time() + 999, + ) + ); + + $result = Transient::get( $test_transient_name ); + + $this->assertEquals( 'value', $result ); + } + + /** + * @covers ::get + */ + public function test_get_transient_use_options_expired(): void { + + $test_transient_name = uniqid( __FUNCTION__ ); + + \WP_Mock::userFunction( 'get_dropins' ) + ->once() + ->andReturn( array( 'object-cache.php' => array() ) ); + + \WP_Mock::userFunction( 'get_transient' ) + ->never(); + + \WP_Mock::userFunction( 'get_option' ) + ->once() + ->with( $test_transient_name, ) + ->andReturn( + array( + 'value' => 'value', + 'expires_at' => time() - 999, + ) + ); + + \WP_Mock::userFunction( 'delete_option' ) + ->once() + ->with( $test_transient_name ) + ->andReturnTrue(); + + $result = Transient::get( $test_transient_name ); + + $this->assertFalse( $result ); + } + + /** + * Test does calling `get` on the instance of Transient class call the static method. + * + * @covers ::__call + */ + public function test_instance_call_method(): void { + + $sut = new Transient(); + + \WP_Mock::userFunction( 'get_dropins' ) + ->once() + ->andReturn( array( 'not-object-cache.php' => array() ) ); + + $return_value = uniqid( __FUNCTION__ ); + + \WP_Mock::userFunction( 'get_transient' ) + ->once() + ->andReturn( $return_value ); + + $result = $sut->get( 'test' ); + + $this->assertEquals( $return_value, $result ); + } + + /** + * @covers ::should_use_transients + */ + public function test_should_use_transients_bluehost_cloud(): void { + + $test_transient_name = uniqid( __FUNCTION__ ); + + \WP_Mock::userFunction( 'get_dropins' ) + ->once() + ->andReturn( array( 'object-cache.php' => array() ) ); + + \WP_Mock::userFunction( 'set_transient' ) + ->once() + ->with( $test_transient_name, 'value', 999 ) + ->andReturnTrue(); + + \WP_Mock::userFunction( 'update_option' ) + ->never(); + + \NewfoldLabs\WP\Context\setContext( 'platform', 'atomic' ); + + Transient::set( $test_transient_name, 'value', 999 ); + + $this->assertConditionsMet(); + + } +} diff --git a/tests/wpunit/includes/SiteCapabilitiesWPUnitTest.php b/tests/wpunit/includes/SiteCapabilitiesWPUnitTest.php new file mode 100644 index 0000000..7f43de1 --- /dev/null +++ b/tests/wpunit/includes/SiteCapabilitiesWPUnitTest.php @@ -0,0 +1,182 @@ +shouldReceive( 'get' ) + ->once() + ->with( 'nfd_site_capabilities' ) + ->andReturn( array( 'test_capability' => true ) ); + + $sut = new SiteCapabilities( $transient ); + + $result = $sut->get( 'test_capability' ); + + $this->assertTrue( $result ); + } + + /** + * @covers ::all + * @covers ::fetch + */ + public function test_fetch_capabilities(): void { + + $transient = Mockery::mock( Transient::class ); + + $transient->shouldReceive( 'get' ) + ->once() + ->with( 'nfd_site_capabilities' ) + ->andReturnFalse(); + + /** + * @see \WP_Http::request() + */ + add_filter( + 'pre_http_request', + function () { + return array( + 'body' => json_encode( array( 'test_capability' => true ) ), + 'response' => array( 'code' => 200 ), + ); + } + ); + + $transient->shouldReceive( 'set' ) + ->once() + ->with( + 'nfd_site_capabilities', + array( 'test_capability' => true ), + 14400, + ) + ->andReturnTrue(); + + $sut = new SiteCapabilities( $transient ); + + $result = $sut->get( 'test_capability' ); + + $this->assertTrue( $result ); + } + + /** + * @covers ::all + * @covers ::fetch + */ + public function test_fetch_capabilities_wp_error(): void { + + $transient = Mockery::mock( Transient::class ); + + $transient->shouldReceive( 'get' ) + ->once() + ->with( 'nfd_site_capabilities' ) + ->andReturnFalse(); + + /** + * @see \WP_Http::request() + */ + add_filter( + 'pre_http_request', + function () { + return new WP_Error( 'could_not_connect', 'Could not connect to Hiive' ); + } + ); + + $transient->shouldReceive( 'set' ) + ->once() + ->with( + 'nfd_site_capabilities', + array(), + 14400, + ) + ->andReturnTrue(); + + $sut = new SiteCapabilities( $transient ); + + $result = $sut->get( 'test_capability' ); + + $this->assertFalse( $result ); + } + + /** + * @covers ::all + * @covers ::fetch + */ + public function test_fetch_capabilities_401(): void { + + $transient = Mockery::mock( Transient::class ); + + $transient->shouldReceive( 'get' ) + ->once() + ->with( 'nfd_site_capabilities' ) + ->andReturnFalse(); + + /** + * @see \WP_Http::request() + */ + add_filter( + 'pre_http_request', + function () { + return array( + 'body' => '', + 'response' => array( 'code' => 401 ), + ); + } + ); + + $transient->shouldReceive( 'set' ) + ->once() + ->with( + 'nfd_site_capabilities', + array(), + 14400, + ) + ->andReturnTrue(); + + $sut = new SiteCapabilities( $transient ); + + $result = $sut->get( 'test_capability' ); + + $this->assertFalse( $result ); + } +}