diff --git a/class.jetpack-options.php b/class.jetpack-options.php
index 1e51ec27a008..fcb7fbc0ab38 100644
--- a/class.jetpack-options.php
+++ b/class.jetpack-options.php
@@ -1,5 +1,40 @@
connection_manager = new Connection_Manager( );
+
/**
* Prepare Gutenberg Editor functionality
*/
@@ -4536,6 +4555,7 @@ function build_connect_url( $raw = false, $redirect = false, $from = false, $reg
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+
// Generating a register URL instead to refresh the existing token
return $this->build_connect_url( $raw, $redirect, $from, true );
}
@@ -4945,55 +4965,29 @@ public static function xmlrpc_api_url() {
* @return array
*/
public static function generate_secrets( $action, $user_id = false, $exp = 600 ) {
- if ( ! $user_id ) {
+ if ( false === $user_id ) {
$user_id = get_current_user_id();
}
- $secret_name = 'jetpack_' . $action . '_' . $user_id;
- $secrets = Jetpack_Options::get_raw_option( 'jetpack_secrets', array() );
-
- if (
- isset( $secrets[ $secret_name ] ) &&
- $secrets[ $secret_name ]['exp'] > time()
- ) {
- return $secrets[ $secret_name ];
- }
-
- $secret_value = array(
- 'secret_1' => wp_generate_password( 32, false ),
- 'secret_2' => wp_generate_password( 32, false ),
- 'exp' => time() + $exp,
- );
-
- $secrets[ $secret_name ] = $secret_value;
-
- Jetpack_Options::update_raw_option( 'jetpack_secrets', $secrets );
- return $secrets[ $secret_name ];
+ return self::init()->connection_manager->generate_secrets( $action, $user_id, $exp );
}
public static function get_secrets( $action, $user_id ) {
- $secret_name = 'jetpack_' . $action . '_' . $user_id;
- $secrets = Jetpack_Options::get_raw_option( 'jetpack_secrets', array() );
+ $secrets = self::init()->connection_manager->get_secrets( $action, $user_id );
- if ( ! isset( $secrets[ $secret_name ] ) ) {
+ if ( Connection_Manager::SECRETS_MISSING === $secrets ) {
return new WP_Error( 'verify_secrets_missing', 'Verification secrets not found' );
}
- if ( $secrets[ $secret_name ]['exp'] < time() ) {
- self::delete_secrets( $action, $user_id );
+ if ( Connection_Manager::SECRETS_EXPIRED === $secrets ) {
return new WP_Error( 'verify_secrets_expired', 'Verification took too long' );
}
- return $secrets[ $secret_name ];
+ return $secrets;
}
public static function delete_secrets( $action, $user_id ) {
- $secret_name = 'jetpack_' . $action . '_' . $user_id;
- $secrets = Jetpack_Options::get_raw_option( 'jetpack_secrets', array() );
- if ( isset( $secrets[ $secret_name ] ) ) {
- unset( $secrets[ $secret_name ] );
- Jetpack_Options::update_raw_option( 'jetpack_secrets', $secrets );
- }
+ return self::init()->connection_manager->delete_secrets( $action, $user_id );
}
/**
diff --git a/composer.json b/composer.json
index 2bbe94c9cfe1..b53ae66334e9 100644
--- a/composer.json
+++ b/composer.json
@@ -8,8 +8,12 @@
"issues": "https://github.com/Automattic/jetpack/issues"
},
"require": {
+ "composer/installers": "1.6.0",
"ext-openssl": "*",
- "automattic/jetpack-logo": "@dev"
+ "automattic/jetpack-connection": "^1.0",
+ "automattic/jetpack-options": "^1.0",
+ "automattic/jetpack-logo": "@dev",
+ "automattic/jetpack-constants": "@dev"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "0.5.0",
@@ -18,6 +22,22 @@
"phpcompatibility/phpcompatibility-wp": "2.0.0",
"automattic/jetpack-autoloader": "@dev"
},
+ "repositories": [
+ {
+ "type": "path",
+ "url": "./packages/jetpack-connection",
+ "options": {
+ "symlink": true
+ }
+ },
+ {
+ "type": "path",
+ "url": "./packages/jetpack-options",
+ "options": {
+ "symlink": true
+ }
+ }
+ ],
"scripts": {
"php:compatibility": "composer install && vendor/bin/phpcs -p -s --runtime-set testVersion '5.3-' --standard=PHPCompatibilityWP --ignore=docker,tools,tests,node_modules,vendor --extensions=php",
"php:lint": "composer install && vendor/bin/phpcs -p -s",
diff --git a/composer.lock b/composer.lock
index 0faa212bae45..82f9ed388be9 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,11 +4,58 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "254aebf32be452ab169d10927a20164f",
+ "content-hash": "2a6b3658e33a86548e8cb2a6a88a1b1c",
"packages": [
+ {
+ "name": "automattic/jetpack-connection",
+ "version": "1.0.0",
+ "dist": {
+ "type": "path",
+ "url": "./packages/connection",
+ "reference": "9dbcf84ab69da7d648e99580533624e2953c9874"
+ },
+ "require-dev": {
+ "10up/wp_mock": "0.4.2",
+ "phpunit/phpunit": "7.*.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Automattic\\Jetpack\\Connection\\": "src"
+ }
+ },
+ "scripts": {
+ "test": [
+ "phpunit"
+ ]
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Everything needed to connect to the Jetpack infrastructure"
+ },
+ {
+ "name": "automattic/jetpack-constants",
+ "version": "dev-master",
+ "dist": {
+ "type": "path",
+ "url": "./packages/constants",
+ "reference": "b2cebb6eb5d4da90374e17c232168088f76c4c49"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "/legacy"
+ ]
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "A wrapper for defining constants in a more testable way."
+ },
{
"name": "automattic/jetpack-logo",
- "version": "dev-try/custom-autoloader",
+ "version": "dev-master",
"dist": {
"type": "path",
"url": "./packages/logo",
@@ -24,12 +71,162 @@
"GPL-2.0-or-later"
],
"description": "A logo for Jetpack"
+ },
+ {
+ "name": "automattic/jetpack-options",
+ "version": "1.0.0",
+ "dist": {
+ "type": "path",
+ "url": "./packages/options",
+ "reference": "0ff5d88cb668dcb7f8f54dfa4a19c87819e1dc78"
+ },
+ "require": {
+ "automattic/jetpack-constants": "@dev"
+ },
+ "require-dev": {
+ "10up/wp_mock": "0.4.2",
+ "phpunit/phpunit": "7.*.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Automattic\\Jetpack\\Options\\": "src"
+ }
+ },
+ "scripts": {
+ "test": [
+ "phpunit"
+ ]
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ]
+ },
+ {
+ "name": "composer/installers",
+ "version": "v1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/installers.git",
+ "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/installers/zipball/cfcca6b1b60bc4974324efb5783c13dca6932b5b",
+ "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0"
+ },
+ "replace": {
+ "roundcube/plugin-installer": "*",
+ "shama/baton": "*"
+ },
+ "require-dev": {
+ "composer/composer": "1.0.*@dev",
+ "phpunit/phpunit": "^4.8.36"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Composer\\Installers\\Plugin",
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Installers\\": "src/Composer/Installers"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kyle Robinson Young",
+ "email": "kyle@dontkry.com",
+ "homepage": "https://github.com/shama"
+ }
+ ],
+ "description": "A multi-framework Composer library installer",
+ "homepage": "https://composer.github.io/installers/",
+ "keywords": [
+ "Craft",
+ "Dolibarr",
+ "Eliasis",
+ "Hurad",
+ "ImageCMS",
+ "Kanboard",
+ "Lan Management System",
+ "MODX Evo",
+ "Mautic",
+ "Maya",
+ "OXID",
+ "Plentymarkets",
+ "Porto",
+ "RadPHP",
+ "SMF",
+ "Thelia",
+ "WolfCMS",
+ "agl",
+ "aimeos",
+ "annotatecms",
+ "attogram",
+ "bitrix",
+ "cakephp",
+ "chef",
+ "cockpit",
+ "codeigniter",
+ "concrete5",
+ "croogo",
+ "dokuwiki",
+ "drupal",
+ "eZ Platform",
+ "elgg",
+ "expressionengine",
+ "fuelphp",
+ "grav",
+ "installer",
+ "itop",
+ "joomla",
+ "kohana",
+ "laravel",
+ "lavalite",
+ "lithium",
+ "magento",
+ "majima",
+ "mako",
+ "mediawiki",
+ "modulework",
+ "modx",
+ "moodle",
+ "osclass",
+ "phpbb",
+ "piwik",
+ "ppi",
+ "puppet",
+ "pxcms",
+ "reindex",
+ "roundcube",
+ "shopware",
+ "silverstripe",
+ "sydes",
+ "symfony",
+ "typo3",
+ "wordpress",
+ "yawik",
+ "zend",
+ "zikula"
+ ],
+ "time": "2018-08-27T06:10:37+00:00"
}
],
"packages-dev": [
{
"name": "automattic/jetpack-autoloader",
- "version": "dev-try/custom-autoloader",
+ "version": "dev-master",
"dist": {
"type": "path",
"url": "./packages/autoloader",
@@ -424,6 +621,7 @@
"minimum-stability": "stable",
"stability-flags": {
"automattic/jetpack-logo": 20,
+ "automattic/jetpack-constants": 20,
"automattic/jetpack-autoloader": 20
},
"prefer-stable": false,
diff --git a/jetpack.php b/jetpack.php
index 7059426e686c..a03183257e9c 100644
--- a/jetpack.php
+++ b/jetpack.php
@@ -12,9 +12,11 @@
* Domain Path: /languages/
*/
+require plugin_dir_path( __FILE__ ) . '/vendor/autoload.php';
define( 'JETPACK__MINIMUM_WP_VERSION', '5.1' );
define( 'JETPACK__MINIMUM_PHP_VERSION', '5.3.2' );
+
define( 'JETPACK__VERSION', '7.4-alpha' );
define( 'JETPACK_MASTER_USER', true );
define( 'JETPACK__API_VERSION', 1 );
@@ -224,7 +226,6 @@ function jetpack_admin_missing_autoloader() { ?>
require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-tracks.php' );
require_once( JETPACK__PLUGIN_DIR . 'class.frame-nonce-preview.php' );
require_once( JETPACK__PLUGIN_DIR . 'modules/module-headings.php');
-require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-constants.php');
require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-idc.php' );
require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-connection-banner.php' );
require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-plan.php' );
diff --git a/packages/.gitignore b/packages/.gitignore
index f4bdfcb54f7d..7ecda0efc77b 100644
--- a/packages/.gitignore
+++ b/packages/.gitignore
@@ -1,2 +1,3 @@
-## Ignore composer.lock for our library packages
-/**/composer.lock
\ No newline at end of file
+## Ignore composer.lock and vendor folders for our library packages
+/**/composer.lock
+/**/vendor/
\ No newline at end of file
diff --git a/packages/connection/composer.json b/packages/connection/composer.json
new file mode 100644
index 000000000000..fe6c91d3e3d0
--- /dev/null
+++ b/packages/connection/composer.json
@@ -0,0 +1,20 @@
+{
+ "name": "automattic/jetpack-connection",
+ "description": "Everything needed to connect to the Jetpack infrastructure",
+ "type": "library",
+ "license": "GPL-2.0-or-later",
+ "version": "1.0.0",
+ "require": {},
+ "require-dev": {
+ "phpunit/phpunit": "7.*.*",
+ "10up/wp_mock": "0.4.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "Automattic\\Jetpack\\Connection\\": "src"
+ }
+ },
+ "scripts": {
+ "test": "phpunit"
+ }
+}
diff --git a/packages/connection/phpunit.xml b/packages/connection/phpunit.xml
new file mode 100644
index 000000000000..e4f3004e20a1
--- /dev/null
+++ b/packages/connection/phpunit.xml
@@ -0,0 +1,7 @@
+
+
+
+ tests/Manager.php
+
+
+
diff --git a/packages/connection/src/Manager.php b/packages/connection/src/Manager.php
new file mode 100644
index 000000000000..0c093579cf6a
--- /dev/null
+++ b/packages/connection/src/Manager.php
@@ -0,0 +1,281 @@
+secret_callable ) ) {
+ /**
+ * Allows modification of the callable that is used to generate connection secrets.
+ *
+ * @param Callable a function or method that returns a secret string.
+ */
+ $this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' );
+ }
+
+ return $this->secret_callable;
+ }
+
+ /**
+ * Returns the object that is to be used for all option manipulation.
+ *
+ * @return Object $manager an option manager object.
+ */
+ protected function get_option_manager() {
+ if ( ! isset( $this->option_manager ) ) {
+ /**
+ * Allows modification of the object that is used to manipulate stored data.
+ *
+ * @param Jetpack_Options an option manager object.
+ */
+ $this->option_manager = apply_filters( 'jetpack_connection_option_manager', false );
+ }
+
+ return $this->option_manager;
+ }
+
+ /**
+ * Generates two secret tokens and the end of life timestamp for them.
+ *
+ * @param String $action The action name.
+ * @param Integer $user_id The user identifier.
+ * @param Integer $exp Expiration time in seconds.
+ */
+ public function generate_secrets( $action, $user_id, $exp ) {
+ $callable = $this->get_secret_callable();
+
+ $secrets = $this->get_option_manager()->get_raw_option( 'jetpack_secrets', array() );
+
+ $secret_name = 'jetpack_' . $action . '_' . $user_id;
+
+ if (
+ isset( $secrets[ $secret_name ] ) &&
+ $secrets[ $secret_name ]['exp'] > time()
+ ) {
+ return $secrets[ $secret_name ];
+ }
+
+ $secret_value = array(
+ 'secret_1' => call_user_func( $callable ),
+ 'secret_2' => call_user_func( $callable ),
+ 'exp' => time() + $exp,
+ );
+
+ $secrets[ $secret_name ] = $secret_value;
+
+ $this->get_option_manager()->update_option( self::SECRETS_OPTION_NAME, $secrets );
+ return $secrets[ $secret_name ];
+ }
+
+ /**
+ * Returns two secret tokens and the end of life timestamp for them.
+ *
+ * @param String $action The action name.
+ * @param Integer $user_id The user identifier.
+ * @return string|array an array of secrets or an error string.
+ */
+ public function get_secrets( $action, $user_id ) {
+ $secret_name = 'jetpack_' . $action . '_' . $user_id;
+ $secrets = $this->get_option_manager()->get_option( self::SECRETS_OPTION_NAME, array() );
+
+ if ( ! isset( $secrets[ $secret_name ] ) ) {
+ return self::SECRETS_MISSING;
+ }
+
+ if ( $secrets[ $secret_name ]['exp'] < time() ) {
+ $this->delete_secrets( $action, $user_id );
+ return self::SECRETS_EXPIRED;
+ }
+
+ return $secrets[ $secret_name ];
+ }
+
+ /**
+ * Deletes secret tokens in case they, for example, have expired.
+ *
+ * @param String $action The action name.
+ * @param Integer $user_id The user identifier.
+ */
+ public function delete_secrets( $action, $user_id ) {
+ $secret_name = 'jetpack_' . $action . '_' . $user_id;
+ $secrets = $this->get_option_manager()->get_option( self::SECRETS_OPTION_NAME, array() );
+ if ( isset( $secrets[ $secret_name ] ) ) {
+ unset( $secrets[ $secret_name ] );
+ $this->get_option_manager()->update_option( self::SECRETS_OPTION_NAME, $secrets );
+ }
+ }
+
+ /**
+ * Responds to a WordPress.com call to register the current site.
+ * Should be changed to protected.
+ */
+ public function handle_registration() {
+
+ }
+
+ /**
+ * Responds to a WordPress.com call to authorize the current user.
+ * Should be changed to protected.
+ */
+ public function handle_authorization() {
+
+ }
+
+ /**
+ * Builds a URL to the Jetpack connection auth page.
+ * This needs rethinking.
+ *
+ * @param bool $raw If true, URL will not be escaped.
+ * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection.
+ * If string, will be a custom redirect.
+ * @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
+ * @param bool $register If true, will generate a register URL regardless of the existing token, since 4.9.0.
+ *
+ * @return string Connect URL
+ */
+ public function build_connect_url( $raw, $redirect, $from, $register ) {
+ return array( $raw, $redirect, $from, $register );
+ }
+
+ /**
+ * Disconnects from the Jetpack servers.
+ * Forgets all connection details and tells the Jetpack servers to do the same.
+ */
+ public function disconnect_site() {
+
+ }
+}
diff --git a/packages/connection/src/Manager_Interface.php b/packages/connection/src/Manager_Interface.php
new file mode 100644
index 000000000000..b7fae840202d
--- /dev/null
+++ b/packages/connection/src/Manager_Interface.php
@@ -0,0 +1,144 @@
+mock = $this->getMockBuilder( 'stdClass' )
+ ->setMethods( [ 'get_option', 'update_option' ] )
+ ->getMock();
+
+ $this->generator = $this->getMockBuilder( 'stdClass' )
+ ->setMethods( [ 'generate' ] )
+ ->getMock();
+
+ $this->manager = new Manager();
+
+ \WP_Mock::onFilter( 'jetpack_connection_option_manager' )
+ ->with( false )
+ ->reply( $this->mock );
+
+ \WP_Mock::onFilter( 'jetpack_connection_secret_generator' )
+ ->with( 'wp_generate_password' )
+ ->reply( array( $this->generator, 'generate' ) );
+ }
+
+ public function tearDown() {
+ \WP_Mock::tearDown();
+ }
+
+ function test_class_implements_interface() {
+ $manager = new Manager();
+ $this->assertInstanceOf( 'Automattic\Jetpack\Connection\Manager_Interface', $manager );
+ }
+
+ function test_generate_secrets() {
+ $this->generator->expects( $this->exactly( 2 ) )
+ ->method( 'generate' )
+ ->will( $this->returnValue( 'topsecretstring' ) );
+
+ $this->mock->expects( $this->once() )
+ ->method( 'update_option' )
+ ->with(
+ $this->equalTo( Manager::SECRETS_OPTION_NAME ),
+ $this->equalTo( array(
+ 'jetpack_name_1' => array(
+ 'secret_1' => 'topsecretstring',
+ 'secret_2' => 'topsecretstring',
+ 'exp' => time() + 600
+ )
+ ) )
+ );
+
+ $secrets = $this->manager->generate_secrets( 'name', 1, 600 );
+
+ $this->assertEquals( 'topsecretstring', $secrets['secret_1'] );
+ $this->assertEquals( 'topsecretstring', $secrets['secret_2'] );
+ }
+
+ function test_get_secrets_not_found() {
+ $this->mock->expects( $this->once() )
+ ->method( 'get_option' )
+ ->with(
+ $this->equalTo( Manager::SECRETS_OPTION_NAME ),
+ $this->anything()
+ );
+
+ $this->assertEquals(
+ Manager::SECRETS_MISSING,
+ $this->manager->get_secrets( 'name', 1, 600 )
+ );
+ }
+
+ /**
+ * @dataProvider secrets_value_provider
+ */
+ function test_get_secrets_expired( $name, $user_id, $expires, $values ) {
+ $this->mock->expects( $this->exactly( 2 ) )
+ ->method( 'get_option' )
+ ->with(
+ $this->equalTo( Manager::SECRETS_OPTION_NAME ),
+ $this->anything()
+ )
+ ->will(
+ $this->returnValue( array(
+ 'jetpack_' . $name . '_' . $user_id => array_merge(
+ $values,
+
+ // Expired secret, should be removed on access.
+ array( 'exp' => 0 )
+ )
+ ) )
+ );
+
+ $this->mock->expects( $this->once() )
+ ->method( 'update_option' )
+ ->with(
+ $this->equalTo( Manager::SECRETS_OPTION_NAME ),
+ $this->equalTo( array() )
+ );
+
+ $this->assertEquals(
+ Manager::SECRETS_EXPIRED,
+ $this->manager->get_secrets( $name, $user_id, $expires )
+ );
+ }
+
+ /**
+ * @dataProvider secrets_value_provider
+ */
+ function test_get_secrets( $name, $user_id, $expires, $values ) {
+ $this->mock->expects( $this->once() )
+ ->method( 'get_option' )
+ ->with(
+ $this->equalTo( Manager::SECRETS_OPTION_NAME ),
+ $this->anything()
+ )
+ ->will(
+ $this->returnValue( array(
+ 'jetpack_' . $name . '_' . $user_id => array_merge(
+ $values,
+
+ // Making sure the secret is still active.
+ array( 'exp' => $values['exp'] + time() )
+ )
+ ) )
+ );
+
+ $this->assertEquals(
+ $values['secret_1'],
+ $this->manager->get_secrets( $name, $user_id, $expires )['secret_1']
+ );
+ }
+
+ /**
+ * Provides values for secrets to test.
+ */
+ function secrets_value_provider() {
+ return [
+ [
+ 'action_name',
+ 123,
+ 3600,
+ [
+ 'secret_1' => 'secret1',
+ 'secret_2' => 'secret2',
+ 'exp' => 600
+ ]
+ ],
+ [
+ 'action_name_2',
+ 1234,
+ 36000,
+ [
+ 'secret_1' => 'secret1withsomewords',
+ 'secret_2' => 'secret2mithosemthingelse',
+ 'exp' => 36000
+ ]
+ ]
+ ];
+ }
+}
diff --git a/packages/connection/tests/bootstrap.php b/packages/connection/tests/bootstrap.php
new file mode 100644
index 000000000000..d914bd03446f
--- /dev/null
+++ b/packages/connection/tests/bootstrap.php
@@ -0,0 +1,5 @@
+
+
+
+ tests/Manager.php
+
+
+
diff --git a/packages/options/src/Manager.php b/packages/options/src/Manager.php
new file mode 100644
index 000000000000..8f031fe83c5f
--- /dev/null
+++ b/packages/options/src/Manager.php
@@ -0,0 +1,293 @@
+ 'jetpack_options',
+ 'private' => 'jetpack_private_options',
+ );
+
+ /**
+ * Returns an array of option names for a given type.
+ *
+ * @param string $type The type of option to return. Defaults to 'compact'.
+ *
+ * @return array
+ */
+ abstract public function get_option_names( $type );
+
+ /**
+ * Returns the requested option. Looks in jetpack_options or jetpack_$name as appropriate.
+ *
+ * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
+ * @param mixed $default (optional) the default value.
+ *
+ * @return mixed
+ */
+ public function get_option( $name, $default = false ) {
+ if ( $this->is_valid( $name, 'non_compact' ) ) {
+ if ( $this->is_network_option( $name ) ) {
+ return get_site_option( "jetpack_$name", $default );
+ }
+
+ return get_option( "jetpack_$name", $default );
+ }
+
+ foreach ( array_keys( $this->grouped_options ) as $group ) {
+ if ( $this->is_valid( $name, $group ) ) {
+ return $this->get_grouped_option( $group, $name, $default );
+ }
+ }
+
+ // TODO: throw an exception here?
+
+ return $default;
+ }
+
+ /**
+ * Returns a single value from a grouped option.
+ *
+ * @param String $group name of the group, i.e., 'private'.
+ * @param String $name the name of the option to return.
+ * @param Mixed $default a default value in case the option is not found.
+ * @return Mixed the option value or default if not found.
+ */
+ protected function get_grouped_option( $group, $name, $default ) {
+ $options = get_option( $this->grouped_options[ $group ] );
+ if ( is_array( $options ) && isset( $options[ $name ] ) ) {
+ return $options[ $name ];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Updates the single given option. Updates jetpack_options or jetpack_$name as appropriate.
+ *
+ * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
+ * @param mixed $value Option value.
+ * @param string $autoload If not compact option, allows specifying whether to autoload or not.
+ *
+ * @return bool Was the option successfully updated?
+ */
+ public function update_option( $name, $value, $autoload = null ) {
+ /**
+ * Fires before Jetpack updates a specific option.
+ *
+ * @since 3.0.0
+ *
+ * @param str $name The name of the option being updated.
+ * @param mixed $value The new value of the option.
+ */
+ do_action( 'pre_update_jetpack_option_' . $name, $name, $value );
+ if ( $this->is_valid( $name, 'non_compact' ) ) {
+ if ( $this->is_network_option( $name ) ) {
+ return update_site_option( "jetpack_$name", $value );
+ }
+
+ return update_option( "jetpack_$name", $value, $autoload );
+
+ }
+
+ foreach ( array_keys( $this->grouped_options ) as $group ) {
+ if ( $this->is_valid( $name, $group ) ) {
+ return $this->update_grouped_option( $group, $name, $value );
+ }
+ }
+
+ // TODO: throw an exception here?
+
+ return false;
+ }
+
+ /**
+ * Updates a single value from a grouped option.
+ *
+ * @param String $group name of the group, i.e., 'private'.
+ * @param String $name the name of the option to update.
+ * @param Mixed $value the to update the option with.
+ * @return Boolean was the update successful?
+ */
+ protected function update_grouped_option( $group, $name, $value ) {
+ $options = get_option( $this->grouped_options[ $group ] );
+ if ( ! is_array( $options ) ) {
+ $options = array();
+ }
+ $options[ $name ] = $value;
+
+ return update_option( $this->grouped_options[ $group ], $options );
+ }
+
+ /**
+ * Deletes the given option. May be passed multiple option names as an array.
+ * Updates jetpack_options and/or deletes jetpack_$name as appropriate.
+ *
+ * @param string|array $names Option names. They must come _without_ `jetpack_%` prefix. The method will prefix the option names.
+ *
+ * @return bool Was the option successfully deleted?
+ */
+ public function delete_option( $names ) {
+ $result = true;
+ $names = (array) $names;
+
+ if ( ! $this->is_valid( $names ) ) {
+ // TODO: issue a warning here?
+ return false;
+ }
+
+ foreach ( array_intersect( $names, $this->get_option_names( 'non_compact' ) ) as $name ) {
+ if ( $this->is_network_option( $name ) ) {
+ $result = delete_site_option( "jetpack_$name" );
+ } else {
+ $result = delete_option( "jetpack_$name" );
+ }
+ }
+
+ foreach ( array_keys( $this->grouped_options ) as $group ) {
+ if ( ! $this->delete_grouped_option( $group, $names ) ) {
+ $result = false;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Deletes a single value from a grouped option.
+ *
+ * @param String $group name of the group, i.e., 'private'.
+ * @param Array $names the names of the option to delete.
+ * @return Mixed the option value or default if not found.
+ */
+ protected function delete_grouped_option( $group, $names ) {
+ $options = get_option( $this->grouped_options[ $group ], array() );
+
+ $to_delete = array_intersect( $names, $this->get_option_names( $group ), array_keys( $options ) );
+ if ( $to_delete ) {
+ foreach ( $to_delete as $name ) {
+ unset( $options[ $name ] );
+ }
+
+ return update_option( $this->grouped_options[ $group ], $options );
+ }
+
+ return true;
+ }
+
+ /**
+ * Is the option name valid?
+ *
+ * @param string $name The name of the option.
+ * @param string|null $group The name of the group that the option is in. Default to null, which will search non_compact.
+ *
+ * @return bool Is the option name valid?
+ */
+ public function is_valid( $name, $group = null ) {
+ if ( is_array( $name ) ) {
+ $compact_names = array();
+ foreach ( array_keys( $this->grouped_options ) as $_group ) {
+ $compact_names = array_merge( $compact_names, $this->get_option_names( $_group ) );
+ }
+
+ $result = array_diff( $name, $this->get_option_names( 'non_compact' ), $compact_names );
+
+ return empty( $result );
+ }
+
+ if ( is_null( $group ) || 'non_compact' === $group ) {
+ if ( in_array( $name, $this->get_option_names( $group ), true ) ) {
+ return true;
+ }
+ }
+
+ foreach ( array_keys( $this->grouped_options ) as $_group ) {
+ if ( is_null( $group ) || $group === $_group ) {
+ if ( in_array( $name, $this->get_option_names( $_group ), true ) ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if an option must be saved for the whole network in WP Multisite
+ *
+ * @param string $option_name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
+ *
+ * @return bool
+ */
+ public function is_network_option( $option_name ) {
+ if ( ! is_multisite() ) {
+ return false;
+ }
+ return in_array( $option_name, $this->get_option_names( 'network' ), true );
+ }
+
+ /**
+ * Gets an option via $wpdb query.
+ *
+ * @since 5.4.0
+ *
+ * @param string $name Option name.
+ * @param mixed $default Default option value if option is not found.
+ *
+ * @return mixed Option value, or null if option is not found and default is not specified.
+ */
+ function get_raw_option( $name, $default = null ) {
+ if ( $this->bypass_raw_option( $name ) ) {
+ return get_option( $name, $default );
+ }
+ global $wpdb;
+ $value = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
+ $name
+ )
+ );
+ $value = maybe_unserialize( $value );
+ if ( $value === null && $default !== null ) {
+ return $default;
+ }
+ return $value;
+ }
+ /**
+ * This function checks for a constant that, if present, will disable direct DB queries Jetpack uses to manage certain options and force Jetpack to always use Options API instead.
+ * Options can be selectively managed via a blacklist by filtering option names via the jetpack_disabled_raw_option filter.
+ *
+ * @param $name Option name
+ *
+ * @return bool
+ */
+ function bypass_raw_option( $name ) {
+ if ( \Jetpack_Constants::get_constant( 'JETPACK_DISABLE_RAW_OPTIONS' ) ) {
+ return true;
+ }
+ /**
+ * Allows to disable particular raw options.
+ *
+ * @since 5.5.0
+ *
+ * @param array $disabled_raw_options An array of option names that you can selectively blacklist from being managed via direct database queries.
+ */
+ $disabled_raw_options = apply_filters( 'jetpack_disabled_raw_options', array() );
+ return isset( $disabled_raw_options[ $name ] );
+ }
+}
diff --git a/packages/options/tests/Manager.php b/packages/options/tests/Manager.php
new file mode 100644
index 000000000000..a78f50d5cd40
--- /dev/null
+++ b/packages/options/tests/Manager.php
@@ -0,0 +1,246 @@
+manager = new Manager_Test();
+ }
+
+ function test_get_private_option_returns_value() {
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_private_options' ),
+ 'return' => array( 'private_name' => true ),
+ ) );
+
+ $value = $this->manager->get_option( 'private_name' );
+
+ // Did Jetpack_Options::get_option() properly return true?
+ $this->assertTrue( $value );
+ }
+
+ function test_get_network_option_returns_value() {
+ \WP_Mock::userFunction( 'get_site_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_network_name', false ),
+ 'return' => true,
+ ) );
+ \WP_Mock::userFunction( 'is_multisite', array(
+ 'times' => 1,
+ 'args' => array(),
+ 'return' => true,
+ ) );
+
+ $value = $this->manager->get_option( 'network_name' );
+
+ // Did Jetpack_Options::get_option() properly return true?
+ $this->assertTrue( $value );
+ }
+
+ function test_get_non_compact_option_returns_value() {
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_uncompact_option_name', false ),
+ 'return' => true,
+ ) );
+ \WP_Mock::userFunction( 'is_multisite', array(
+ 'times' => 1,
+ 'args' => array(),
+ 'return' => false,
+ ) );
+
+ $value = $this->manager->get_option( 'uncompact_option_name' );
+
+ // Did Jetpack_Options::get_option() properly return true?
+ $this->assertTrue( $value );
+ }
+
+ function test_delete_non_compact_option_returns_true_when_successfully_deleted() {
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_options', array() ),
+ 'return' => array(),
+ ) );
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_private_options', array() ),
+ 'return' => array(),
+ ) );
+ \WP_Mock::userFunction( 'delete_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_uncompact_option_name' ),
+ 'return' => true,
+ ) );
+ \WP_Mock::userFunction( 'is_multisite', array(
+ 'times' => 1,
+ 'args' => array(),
+ 'return' => false,
+ ) );
+
+ $deleted = $this->manager->delete_option( 'uncompact_option_name' );
+
+ // Did Jetpack_Options::delete_option() properly return true?
+ $this->assertTrue( $deleted );
+ }
+
+ function test_delete_network_option_returns_true_when_successfully_deleted() {
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_options', array() ),
+ 'return' => array(),
+ ) );
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_private_options', array() ),
+ 'return' => array(),
+ ) );
+ \WP_Mock::userFunction( 'is_multisite', array(
+ 'times' => 1,
+ 'args' => array(),
+ 'return' => true,
+ ) );
+ \WP_Mock::userFunction( 'delete_site_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_network_name' ),
+ 'return' => true,
+ ) );
+
+ $deleted = $this->manager->delete_option( 'network_name' );
+
+ // Did Jetpack_Options::delete_option() properly return true?
+ $this->assertTrue( $deleted );
+ }
+
+ function test_delete_private_option_returns_true_when_successfully_deleted() {
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_options', array() ),
+ 'return' => array(),
+ ) );
+ \WP_Mock::userFunction( 'get_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_private_options', array() ),
+ 'return' => array( 'private_name' => false ),
+ ) );
+ \WP_Mock::userFunction( 'update_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_private_options', array() ),
+ 'return' => true,
+ ) );
+
+ $deleted = $this->manager->delete_option( 'private_name' );
+
+ // Did Jetpack_Options::delete_option() properly return true?
+ $this->assertTrue( $deleted );
+ }
+
+ function test_update_non_compact_option_returns_true_when_successfully_updated() {
+ \WP_Mock::expectAction(
+ 'pre_update_jetpack_option_uncompact_option_name',
+ 'uncompact_option_name',
+ true
+ );
+
+ \WP_Mock::userFunction( 'update_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_uncompact_option_name', true, NULL ),
+ 'return' => true,
+ ) );
+ \WP_Mock::userFunction( 'is_multisite', array(
+ 'times' => 1,
+ 'args' => array(),
+ 'return' => false,
+ ) );
+
+ $updated = $this->manager->update_option( 'uncompact_option_name', true );
+
+ // Did Jetpack_Options::delete_option() properly return true?
+ $this->assertTrue( $updated );
+ }
+
+ function test_update_network_option_returns_true_when_successfully_updated() {
+ \WP_Mock::expectAction(
+ 'pre_update_jetpack_option_network_name',
+ 'network_name',
+ true
+ );
+
+ \WP_Mock::userFunction( 'update_site_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_network_name', true ),
+ 'return' => true,
+ ) );
+ \WP_Mock::userFunction( 'is_multisite', array(
+ 'times' => 1,
+ 'args' => array(),
+ 'return' => true,
+ ) );
+
+ $updated = $this->manager->update_option( 'network_name', true );
+
+ // Did Jetpack_Options::update_option() properly return true?
+ $this->assertTrue( $updated );
+ }
+
+ function test_update_private_option_returns_true_when_successfully_updated() {
+ \WP_Mock::expectAction(
+ 'pre_update_jetpack_option_private_name',
+ 'private_name',
+ true
+ );
+
+ \WP_Mock::userFunction( 'update_option', array(
+ 'times' => 1,
+ 'args' => array( 'jetpack_private_options', array( 'private_name' => true ) ),
+ 'return' => true,
+ ) );
+
+ $updated = $this->manager->update_option( 'private_name', true );
+
+ // Did Jetpack_Options::update_option() properly return true?
+ $this->assertTrue( $updated );
+ }
+
+ public function tearDown() {
+ \WP_Mock::tearDown();
+ }
+}
+
+class Manager_Test extends Manager {
+
+ /**
+ * Returns an array of option names for a given type.
+ *
+ * @param string $type The type of option to return. Defaults to 'compact'.
+ *
+ * @return array
+ */
+ public function get_option_names( $type = 'compact' ) {
+ switch ( $type ) {
+ case 'non-compact' :
+ case 'non_compact' :
+ return array(
+ 'network_name',
+ 'uncompact_option_name',
+ );
+
+ case 'private' :
+ return array(
+ 'private_name'
+ );
+
+ case 'network' :
+ return array(
+ 'network_name' // Network options must be listed a second time
+ );
+ }
+
+ return array(
+ 'id'
+ );
+ }
+}
diff --git a/packages/options/tests/bootstrap.php b/packages/options/tests/bootstrap.php
new file mode 100644
index 000000000000..d914bd03446f
--- /dev/null
+++ b/packages/options/tests/bootstrap.php
@@ -0,0 +1,5 @@
+