initial
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\Schema\Parser;
|
||||
use Automattic\Jetpack\Schema\Schema_Error;
|
||||
use Automattic\Jetpack\Schema\Schema_Parser;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Entry;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Delete;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Merge;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
|
||||
|
||||
/**
|
||||
* Data Sync Entry Adapter:
|
||||
* ========================
|
||||
* This class takes in any instance that subscribes to one or more "Entry_Can_*" interfaces
|
||||
* and adapts it to give it a predictable interface.
|
||||
*
|
||||
* This makes it possible to have an Entry class that only subscribes to "Entry_Can_Get"
|
||||
* yet still have all the other methods (set/merge/delete) available.
|
||||
*
|
||||
* Entry Adapter will infer whether an object is able to perform actions (get,set,merge,delete)
|
||||
* based on whether the object is an instance of the corresponding interface (Entry_Can_*).
|
||||
*/
|
||||
final class Data_Sync_Entry_Adapter implements Data_Sync_Entry {
|
||||
|
||||
/**
|
||||
* @var Entry_Can_Get&Entry_Can_Set|Entry_Can_Get&Entry_Can_Merge|Entry_Can_Get&Entry_Can_Delete - The data sync entry.
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* @var Schema_Parser $parser - The schema for the data sync entry.
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* For more explanation, see the class docblock.
|
||||
*
|
||||
* @see Data_Sync_Entry_Adapter
|
||||
* The constructor accepts any entry that subscribes to at least "Entry_Can_Get", but can also
|
||||
* subscribe to any of the other Entry_Can_* interfaces.
|
||||
*
|
||||
* @param Entry_Can_Get $entry - The data sync entry.
|
||||
* @param Parser $schema - The schema for the data sync entry.
|
||||
*/
|
||||
public function __construct( $entry, $schema ) {
|
||||
$this->entry = $entry;
|
||||
$this->parser = $schema;
|
||||
}
|
||||
|
||||
public function is( $interface_reference ) {
|
||||
return $this->entry instanceof $interface_reference;
|
||||
}
|
||||
|
||||
public function get() {
|
||||
|
||||
if ( $this->parser->has_fallback() ) {
|
||||
$default = $this->parser->get_fallback();
|
||||
$value = $this->entry->get( $default );
|
||||
return $this->parser->parse( $value );
|
||||
}
|
||||
|
||||
// If WordPress debug is enabled, don't hide exceptions.
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
return $this->parser->parse( $this->entry->get() );
|
||||
}
|
||||
|
||||
// If WordPress debug is disabled, attempt to recover by just returning the value
|
||||
try {
|
||||
return $this->parser->parse( $this->entry->get() );
|
||||
} catch ( Schema_Error $error ) {
|
||||
return $this->entry->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function set( $value ) {
|
||||
if ( $this->is( Entry_Can_Set::class ) ) {
|
||||
$parsed_value = $this->parser->parse( $value );
|
||||
$this->entry->set( $parsed_value );
|
||||
}
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
public function merge( $partial_value ) {
|
||||
if ( $this->is( Entry_Can_Merge::class ) ) {
|
||||
if ( $this->parser->has_fallback() ) {
|
||||
$default = $this->parser->get_fallback();
|
||||
$existing_value = $this->entry->get( $default );
|
||||
} else {
|
||||
$existing_value = $this->entry->get();
|
||||
}
|
||||
$updated_value = $this->entry->merge( $existing_value, $partial_value );
|
||||
$this->set( $updated_value );
|
||||
}
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
if ( $this->is( Entry_Can_Delete::class ) ) {
|
||||
$this->entry->delete();
|
||||
}
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Parser
|
||||
*/
|
||||
public function get_parser() {
|
||||
return $this->parser;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Delete;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
|
||||
|
||||
final class Data_Sync_Option implements Entry_Can_Get, Entry_Can_Set, Entry_Can_Delete {
|
||||
|
||||
private $option_key;
|
||||
|
||||
public function __construct( $option_key ) {
|
||||
$this->option_key = $option_key;
|
||||
}
|
||||
|
||||
public function get( $fallback_value = false ) {
|
||||
// WordPress looks at argument count to figure out if a fallback value was used.
|
||||
// Only provide the fallback value if it's not the default ( false ).
|
||||
if ( $fallback_value !== false ) {
|
||||
return get_option( $this->option_key, $fallback_value );
|
||||
}
|
||||
return get_option( $this->option_key );
|
||||
}
|
||||
|
||||
public function set( $value ) {
|
||||
update_option( $this->option_key, $value, false );
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
delete_option( $this->option_key );
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
|
||||
final class Data_Sync_Readonly implements Entry_Can_Get {
|
||||
private $callback;
|
||||
public function __construct( $callback ) {
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function get( $fallback_value = false ) {
|
||||
$result = call_user_func( $this->callback );
|
||||
if ( false !== $fallback_value && empty( $result ) ) {
|
||||
return $fallback_value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
/**
|
||||
* This is the main file for the Data_Sync package.
|
||||
*
|
||||
* It's responsible for setting up the registry and the endpoints.
|
||||
*
|
||||
* Setting up with something like this:
|
||||
*
|
||||
* ```
|
||||
* class Widget_Status extends Data_Sync_Handler {}
|
||||
* class Widget_Data extends Data_Sync_Handler {}
|
||||
*
|
||||
* $instance = Data_Sync::setup( 'jetpack_boost', 'jetpack-boost' );
|
||||
* $instance->register( 'widget_status', new Widget_Status() );
|
||||
* $instance->register( 'widget_data', new Widget_Data() );
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* This will to create two endpoints: `/wp-json/jetpack-boost/widget-status` and `/wp-json/jetpack-boost/widget-data`
|
||||
* and pass the following variables to the `jetpack-boost` script handle.
|
||||
*
|
||||
* Note that keys for URLs are always automatically transformed to kebab-case, so `widget_status` becomes `widget-status`,
|
||||
* and it's expected that keys are always in snake_case when referencing options.
|
||||
* They're only transformed to kebab-case when used in URLs.
|
||||
*
|
||||
* ```
|
||||
* jetpack_boost = {
|
||||
* rest_api: {
|
||||
* value: 'https://example.com/wp-json/jetpack-boost',
|
||||
* nonce: '1234567890'
|
||||
* },
|
||||
* widget_status: {
|
||||
* value: 'active',
|
||||
* nonce: '1234567890'
|
||||
* },
|
||||
* widget_data: {
|
||||
* value: { ... },
|
||||
* nonce: '1234567890'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To access the data from WordPress, you can ask the registry for the entry:*
|
||||
* ```
|
||||
* $registry = Registry::get_instance( 'jetpack_boost' );
|
||||
* $entry = $registry->get( 'widget_status' );
|
||||
* $entry->get(); // 'active'
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To make it easier to access the data, you should probably create a dedicated helper function:
|
||||
* ```
|
||||
* function jetpack_boost_get_data( $key ) {
|
||||
* $registry = Registry::get_instance( 'jetpack_boost' );
|
||||
* $entry = $registry->get( $key );
|
||||
* return $entry->get();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package automattic/jetpack-wp-js-data-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\Schema\Parser;
|
||||
use Automattic\Jetpack\Schema\Schema_Context;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Lazy_Entry;
|
||||
|
||||
final class Data_Sync {
|
||||
|
||||
const PACKAGE_VERSION = '0.6.9';
|
||||
|
||||
/**
|
||||
* @var Registry
|
||||
*/
|
||||
private $registry;
|
||||
|
||||
/**
|
||||
* @var string Script Handle name to pass the variables to.
|
||||
*/
|
||||
private $script_handle;
|
||||
|
||||
/**
|
||||
* The Registry class is a singleton.
|
||||
*
|
||||
* @var Data_Sync[]
|
||||
*/
|
||||
private static $instance = array();
|
||||
/**
|
||||
* @var string The namespace to use for the registry.
|
||||
*/
|
||||
private $namespace;
|
||||
|
||||
public function __construct( $namespace ) {
|
||||
$this->namespace = $namespace;
|
||||
$this->registry = new Registry( $namespace );
|
||||
}
|
||||
|
||||
public static function get_instance( $namespace ) {
|
||||
if ( ! isset( self::$instance[ $namespace ] ) ) {
|
||||
self::$instance[ $namespace ] = new self( $namespace );
|
||||
}
|
||||
|
||||
return self::$instance[ $namespace ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve nonces for all action endpoints associated with a given entry.
|
||||
*
|
||||
* @param string $entry_key The key for the entry.
|
||||
*
|
||||
* @return array An associative array of action nonces.
|
||||
*/
|
||||
private function get_action_nonces_for_entry( $entry_key ) {
|
||||
// Assuming a method in Registry class to retrieve all action names for an entry
|
||||
$action_names = $this->registry->get_action_names_for_entry( $entry_key );
|
||||
$nonces = array();
|
||||
|
||||
foreach ( $action_names as $action_name ) {
|
||||
$nonce = $this->registry->get_action_nonce( $entry_key, $action_name );
|
||||
if ( $nonce ) {
|
||||
$nonces[ $action_name ] = $nonce;
|
||||
}
|
||||
}
|
||||
|
||||
return $nonces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't call this method directly.
|
||||
* It's only public so that it can be called as a hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
|
||||
public function _print_options_script_tag() {
|
||||
$data = array(
|
||||
'rest_api' => array(
|
||||
'value' => rest_url( $this->registry->get_namespace_http() ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
),
|
||||
);
|
||||
foreach ( $this->registry->all() as $key => $entry ) {
|
||||
|
||||
$data[ $key ] = array(
|
||||
'value' => $entry->is( Lazy_Entry::class ) ? null : $entry->get(),
|
||||
'nonce' => $this->registry->get_endpoint( $key )->create_nonce(),
|
||||
);
|
||||
|
||||
if ( DS_Utils::is_debug() ) {
|
||||
$data[ $key ]['log'] = $entry->get_parser()->get_log();
|
||||
}
|
||||
|
||||
if ( DS_Utils::debug_disable( $key ) ) {
|
||||
unset( $data[ $key ]['value'] );
|
||||
}
|
||||
|
||||
if ( $entry->is( Lazy_Entry::class ) ) {
|
||||
$data[ $key ]['lazy'] = true;
|
||||
}
|
||||
|
||||
// Include nonces for action endpoints associated with this entry
|
||||
$action_nonces = $this->get_action_nonces_for_entry( $key );
|
||||
if ( ! empty( $action_nonces ) ) {
|
||||
$data[ $key ]['actions'] = $action_nonces;
|
||||
}
|
||||
}
|
||||
|
||||
wp_localize_script( $this->script_handle, $this->namespace, $data );
|
||||
}
|
||||
|
||||
public function attach_to_plugin( $script_handle, $plugin_page_hook ) {
|
||||
$this->script_handle = $script_handle;
|
||||
add_action( $plugin_page_hook, array( $this, '_print_options_script_tag' ) );
|
||||
}
|
||||
|
||||
public function get_registry() {
|
||||
return $this->registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataSync entries have to be registered before they can be used.
|
||||
*
|
||||
* Typically, entries are stored in WP Options, so this method
|
||||
* is will default to registering entries as Data_Sync_Option.
|
||||
*
|
||||
* However, you can provide an `$entry` instance that subscribes Entry_Can_* methods.
|
||||
* If you do, `Entry_Can_Get` interface is required, and all other Entry_Can_* interfaces are optional.
|
||||
*
|
||||
* @param string $key - The key to register the entry under.
|
||||
* @param Parser $parser - The parser to use for the entry.
|
||||
* @param Entry_Can_Get $custom_entry_instance - The entry to register. If null, a new Data_Sync_Option will be created.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register( $key, $parser, $custom_entry_instance = null ) {
|
||||
$option_key = $this->namespace . '_' . $key;
|
||||
|
||||
// If a custom entry instance is provided, and it implements Entry_Can_Get, use that.
|
||||
// Otherwise, this Entry will store data using Data_Sync_Option (wp_options).
|
||||
$entry = ( $custom_entry_instance instanceof Entry_Can_Get )
|
||||
? $custom_entry_instance
|
||||
: new Data_Sync_Option( $option_key );
|
||||
|
||||
/*
|
||||
* ## Adapter
|
||||
* This `register` method is inteded to be a shorthand for the most common use case.
|
||||
*
|
||||
* Custom entries can implement various interfaces depending on whether they can set, merge, delete, etc.
|
||||
* However, the Registry expects an object that implements Data_Sync_Entry.
|
||||
* That's why we wrap the Entry in an Adapter - giving it a guaranteed interface.
|
||||
*
|
||||
* ## Customization
|
||||
* Entries can be flexible because they're wrapped in an Adapter.
|
||||
* But you can also create a class that implements `Data_Sync_Entry` directly if you need to.
|
||||
* In that case, you'd need to use:
|
||||
* ```php
|
||||
* $Data_Sync->get_registry()->register(...)` instead of `$Data_Sync->register(...)
|
||||
* ```
|
||||
*/
|
||||
if ( method_exists( $parser, 'set_context' ) ) {
|
||||
// @phan-suppress-next-line PhanUndeclaredMethod -- Phan misses the method_exists(). See https://github.com/phan/phan/issues/1204.
|
||||
$parser->set_context( new Schema_Context( $key ) );
|
||||
}
|
||||
$entry_adapter = new Data_Sync_Entry_Adapter( $entry, $parser );
|
||||
$this->registry->register( $key, $entry_adapter );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a readonly entry.
|
||||
*
|
||||
* @param string $key The key to register the entry under.
|
||||
* @param Parser $parser The parser to use for the entry.
|
||||
* @param callable $callback The callback to use for the entry.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_readonly(
|
||||
$key,
|
||||
$parser,
|
||||
$callback
|
||||
) {
|
||||
$this->register( $key, $parser, new Data_Sync_Readonly( $callback ) );
|
||||
}
|
||||
|
||||
public function register_action(
|
||||
$key,
|
||||
$action_name,
|
||||
$request_schema,
|
||||
$instance
|
||||
) {
|
||||
$this->registry->register_action( $key, $action_name, $request_schema, $instance );
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\Schema\Utils as Schema_Utils;
|
||||
|
||||
class DS_Utils {
|
||||
/**
|
||||
* Is the current environment a development environment?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_debug(): bool {
|
||||
return ( defined( 'DATASYNC_DEBUG' ) && \DATASYNC_DEBUG ) || Schema_Utils::is_debug();
|
||||
}
|
||||
|
||||
public static function debug_disable( $name ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( isset( $_GET['ds-debug-disable'] ) && ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$values = explode( ',', sanitize_key( $_GET['ds-debug-disable'] ) );
|
||||
if ( $values === array( 'all' ) ) {
|
||||
return true;
|
||||
}
|
||||
return in_array( $name, $values, true );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* The Registry class is a singleton that stores references to all Data_Sync_Entry instances.
|
||||
* It also stores references to all Endpoint instances.
|
||||
* It is namespaced to allow for multiple registries.
|
||||
*
|
||||
* @package automattic/jetpack-wp-js-data-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema_Parser;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Entry;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Endpoints\Action_Endpoint;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Endpoints\Endpoint;
|
||||
|
||||
class Registry {
|
||||
|
||||
/**
|
||||
* Registry instances are namespaced to allow for multiple registries.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $namespace;
|
||||
|
||||
/**
|
||||
* Store a references for every Data_Sync_Entry instance.
|
||||
*
|
||||
* @var Data_Sync_Entry[]
|
||||
*/
|
||||
private $entries = array();
|
||||
|
||||
private $action_endpoints = array();
|
||||
|
||||
/**
|
||||
* Store references for every Endpoint instance.
|
||||
*
|
||||
* @var Endpoint[]
|
||||
*/
|
||||
private $endpoints = array();
|
||||
|
||||
/**
|
||||
* There can be multiple registries, reference them by namepsace.
|
||||
* For example "jetpack_boost".
|
||||
*
|
||||
* @param string $namespace The namespace for this registry instance.
|
||||
*/
|
||||
public function __construct( $namespace ) {
|
||||
$this->namespace = $namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a key.
|
||||
*
|
||||
* @param string $key - Keys should only include alphanumeric characters and underscores.
|
||||
*
|
||||
* @return string
|
||||
* @throws \Exception In debug mode, if the key is invalid.
|
||||
*/
|
||||
private function sanitize_key( $key ) {
|
||||
$sanitized_key = sanitize_key( $key );
|
||||
$sanitized_key = str_replace( '-', '_', $sanitized_key );
|
||||
if ( DS_Utils::is_debug() && $sanitized_key !== $key ) {
|
||||
// If the key is invalid,
|
||||
// Log an error during development
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "Invalid key '$key'. Keys should only include alphanumeric characters and underscores." );
|
||||
}
|
||||
return $sanitized_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a key for use in a URL.
|
||||
*
|
||||
* @param string $key - Keys should only include alphanumeric characters and underscores.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function sanitize_url_key( $key ) {
|
||||
return str_replace( '_', '-', sanitize_key( $key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new entry and add it to the registry.
|
||||
*
|
||||
* @param string $key - The name of the entry. For example `widget_status`.
|
||||
* @param Data_Sync_Entry $entry
|
||||
*
|
||||
* @return Data_Sync_Entry
|
||||
*/
|
||||
public function register( $key, $entry ) {
|
||||
|
||||
$key = $this->sanitize_key( $key );
|
||||
|
||||
$this->entries[ $key ] = $entry;
|
||||
$endpoint = new Endpoint( $this->get_namespace_http(), $this->sanitize_url_key( $key ), $entry );
|
||||
$this->endpoints[ $key ] = $endpoint;
|
||||
|
||||
add_action( 'rest_api_init', array( $endpoint, 'register_rest_routes' ) );
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an action endpoint.
|
||||
*
|
||||
* @param string $key The base key for the endpoint.
|
||||
* @param string $action_name The name of the action.
|
||||
* @param Schema_Parser $request_schema The schema for the action's request body.
|
||||
* @param mixed $action_class The class handling the action logic.
|
||||
*/
|
||||
public function register_action( $key, $action_name, $request_schema, $action_class ) {
|
||||
// Create and store the action endpoint instance
|
||||
$action_endpoint = new Action_Endpoint(
|
||||
$this->get_namespace_http(),
|
||||
$this->sanitize_url_key( $key ),
|
||||
$this->sanitize_url_key( $action_name ),
|
||||
$request_schema,
|
||||
$action_class
|
||||
);
|
||||
|
||||
// Store the action endpoint instance for nonce retrieval
|
||||
$this->action_endpoints[ $key ][ $action_name ] = $action_endpoint;
|
||||
|
||||
// Register the REST route for the action endpoint
|
||||
add_action( 'rest_api_init', array( $action_endpoint, 'register_rest_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all action names for a given entry.
|
||||
*
|
||||
* @param string $entry_key The key for the entry.
|
||||
*
|
||||
* @return array An array of action names.
|
||||
*/
|
||||
public function get_action_names_for_entry( $entry_key ) {
|
||||
if ( isset( $this->action_endpoints[ $entry_key ] ) ) {
|
||||
return array_keys( $this->action_endpoints[ $entry_key ] );
|
||||
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nonce for a specific action endpoint.
|
||||
*
|
||||
* @param string $key The base key for the endpoint.
|
||||
* @param string $action_name The name of the action.
|
||||
*
|
||||
* @return false|string The nonce or false if not found.
|
||||
*/
|
||||
public function get_action_nonce( $key, $action_name ) {
|
||||
if ( isset( $this->action_endpoints[ $key ][ $action_name ] ) ) {
|
||||
return $this->action_endpoints[ $key ][ $action_name ]->create_nonce();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered entries.
|
||||
*
|
||||
* @return Data_Sync_Entry[]
|
||||
*/
|
||||
public function all() {
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for a given key.
|
||||
*
|
||||
* @param string $key - The key for the endpoint.
|
||||
*
|
||||
* @return Endpoint|false
|
||||
*/
|
||||
public function get_endpoint( $key ) {
|
||||
if ( ! isset( $this->endpoints[ $key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
return $this->endpoints[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry for a given key.
|
||||
*
|
||||
* @param string $key - The key for the entry.
|
||||
*
|
||||
* @return Data_Sync_Entry|false
|
||||
*/
|
||||
public function get_entry( $key ) {
|
||||
if ( ! isset( $this->entries[ $key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
return $this->entries[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace for use in a filter.
|
||||
*
|
||||
* @return string The namespace key of this registry instance.
|
||||
*/
|
||||
public function get_namespace() {
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace for use in a URL.
|
||||
*
|
||||
* @return string The namespace key of this registry instance.
|
||||
*/
|
||||
public function get_namespace_http() {
|
||||
return $this->sanitize_url_key( $this->namespace );
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface for action classes in the data sync system.
|
||||
*
|
||||
* @package automattic/jetpack-wp-js-data-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
/**
|
||||
* Interface Action_Interface
|
||||
*/
|
||||
interface Data_Sync_Action {
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle( $data, $request );
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
/**
|
||||
* Data Sync Entry Interface:
|
||||
* ==========================
|
||||
* This interface defines what is a Data Sync Entry.
|
||||
*/
|
||||
interface Data_Sync_Entry {
|
||||
|
||||
/**
|
||||
* Checks if this Data Sync Entry implements a specific interface.
|
||||
*
|
||||
* @param string $interface_reference The name of the method to check (get, set, merge, delete).
|
||||
* @return bool True if the method is supported, false otherwise.
|
||||
*/
|
||||
public function is( $interface_reference );
|
||||
|
||||
/**
|
||||
* Retrieves the current value of the data sync entry.
|
||||
*
|
||||
* @return mixed The current value of the data sync entry.
|
||||
*/
|
||||
public function get();
|
||||
|
||||
/**
|
||||
* Sets the value of the data sync entry.
|
||||
*
|
||||
* @param mixed $value The new value to set for the data sync entry.
|
||||
* @return mixed The updated value of the data sync entry.
|
||||
*/
|
||||
public function set( $value );
|
||||
|
||||
/**
|
||||
* Merges a partial value with the current value of the data sync entry.
|
||||
*
|
||||
* @param mixed $partial_value The partial value to merge with the current value.
|
||||
* @return mixed The updated value of the data sync entry after merging.
|
||||
*/
|
||||
public function merge( $partial_value );
|
||||
|
||||
/**
|
||||
* Deletes the data sync entry.
|
||||
*
|
||||
* @return mixed The value of the data sync entry after deletion.
|
||||
*/
|
||||
public function delete();
|
||||
|
||||
/**
|
||||
* @return mixed The schema of the data sync entry.
|
||||
*/
|
||||
public function get_parser();
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
interface Entry_Can_Delete {
|
||||
|
||||
public function delete();
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
interface Entry_Can_Get {
|
||||
|
||||
public function get( $fallback_value );
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
interface Entry_Can_Merge extends Entry_Can_Set {
|
||||
|
||||
public function merge( $previous_value, $partial_value );
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
interface Entry_Can_Set {
|
||||
|
||||
public function set( $value );
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
interface Entry_Has_Custom_Endpoints {
|
||||
|
||||
public function set( $value );
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Contracts;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync;
|
||||
|
||||
interface Lazy_Entry {
|
||||
/**
|
||||
* "Look pal, I'm so lazy I can't even finish this sente.." - Lazy Entry
|
||||
*
|
||||
* Entries can tag themselves as "lazy" by implementing this interface.
|
||||
* By tagging an entry as "lazy" it won't be loaded with `wp_localize_script`.
|
||||
*
|
||||
* This is useful when you want DataSync, but getting the data is going
|
||||
* to slow down the admin dashboard page load.
|
||||
* For example - uncached network requests.
|
||||
*
|
||||
* @see Data_Sync::_print_options_script_tag()
|
||||
*/
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Register and handle custom action REST API Endpoints for data sync entries.
|
||||
*
|
||||
* @package automattic/jetpack-wp-js-data-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Endpoints;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema_Parser;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\DS_Utils;
|
||||
|
||||
class Action_Endpoint {
|
||||
|
||||
/**
|
||||
* @var Data_Sync_Action - The class that handles the action.
|
||||
*/
|
||||
private $action_class;
|
||||
|
||||
/**
|
||||
* @var Schema_Parser $request_schema - The schema for requests to this action.
|
||||
*/
|
||||
private $request_schema;
|
||||
|
||||
/**
|
||||
* @var string $rest_namespace - The namespace for the REST API endpoint.
|
||||
*/
|
||||
private $rest_namespace;
|
||||
|
||||
/**
|
||||
* @var string $route - The route for the REST API endpoint.
|
||||
*/
|
||||
private $route;
|
||||
|
||||
/**
|
||||
* @var Authenticated_Nonce $nonce - The nonce for the REST API endpoint.
|
||||
*/
|
||||
private $nonce;
|
||||
|
||||
/**
|
||||
* This class handles endpoints for DataSync actions.
|
||||
*
|
||||
* @param string $namespace
|
||||
* @param string $key
|
||||
* @param string $action_name
|
||||
* @param Schema_Parser $request_schema
|
||||
* @param Data_Sync_Action $action_class
|
||||
*/
|
||||
public function __construct( $namespace, $key, $action_name, $request_schema, $action_class ) {
|
||||
$this->action_class = $action_class;
|
||||
$this->request_schema = $request_schema;
|
||||
$this->rest_namespace = $namespace;
|
||||
$this->route = $key . '/action/' . $action_name;
|
||||
$this->nonce = new Authenticated_Nonce( "{$namespace}_{$this->route}_action" );
|
||||
}
|
||||
|
||||
public function register_rest_routes() {
|
||||
register_rest_route(
|
||||
$this->rest_namespace,
|
||||
$this->route,
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'handle_action' ),
|
||||
'permission_callback' => array( $this, 'permissions' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function response_error( $message, $code ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
'code' => $code,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function response_success( $data ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'status' => 'success',
|
||||
'JSON' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function handle_action( $request ) {
|
||||
try {
|
||||
$params = $request->get_json_params();
|
||||
$data = $params['JSON'] ?? null;
|
||||
$parsed_data = $this->request_schema->parse( $data );
|
||||
|
||||
// Delegate to the action handler
|
||||
$result = $this->action_class->handle( $parsed_data, $request );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->response_error( $result->get_error_message(), $result->get_error_code() );
|
||||
}
|
||||
|
||||
if ( true === DS_Utils::debug_disable( $this->route ) ) {
|
||||
// This is a debug request - it's ok to return a different shape from the rest.
|
||||
// Return 418 I'm a teapot if this is a debug request to the endpoint.
|
||||
return rest_ensure_response( new \WP_Error( 'teapot', "I'm a teapot.", array( 'status' => 418 ) ) );
|
||||
}
|
||||
|
||||
return $this->response_success( $result );
|
||||
} catch ( \RuntimeException $e ) {
|
||||
return $this->response_error( $e->getMessage(), 'runtime_error' );
|
||||
}
|
||||
}
|
||||
|
||||
public function create_nonce() {
|
||||
return $this->nonce->create();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \WP_REST_Request $request
|
||||
*/
|
||||
public function permissions( $request ) {
|
||||
$nonce = $request->get_header( 'X-Jetpack-WP-JS-Sync-Nonce' );
|
||||
return $this->nonce->verify( $nonce ) && current_user_can( 'manage_options' );
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* Every Data Sync endpoint should be protected by nonce that belongs to an authenticated user.
|
||||
* and is generated and verified for that specific endpoint.
|
||||
*
|
||||
* @package automattic/jetpack-wp-js-data-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Endpoints;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\DS_Utils;
|
||||
|
||||
class Authenticated_Nonce {
|
||||
|
||||
/**
|
||||
* @var string Which nonce action to verify?
|
||||
*/
|
||||
private $action;
|
||||
|
||||
public function __construct( $name ) {
|
||||
$this->action = $name;
|
||||
}
|
||||
|
||||
public function create() {
|
||||
|
||||
if ( DS_Utils::is_debug() && ! did_action( 'set_current_user' ) ) {
|
||||
throw new \RuntimeException( "Debug: Attempting to create {$this->action} nonce before the user is set." );
|
||||
}
|
||||
return wp_create_nonce( $this->action );
|
||||
}
|
||||
|
||||
public function verify( $nonce ) {
|
||||
if ( DS_Utils::is_debug() && ! did_action( 'set_current_user' ) ) {
|
||||
throw new \RuntimeException( "Debug: Attempting to validate {$this->action} nonce before the user is set." );
|
||||
}
|
||||
|
||||
return wp_verify_nonce( $nonce, $this->action );
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* Register and handle REST API Endpoints for each data sync entry.
|
||||
*
|
||||
* @package automattic/jetpack-wp-js-data-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\WP_JS_Data_Sync\Endpoints;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Entry;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Delete;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Merge;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\DS_Utils;
|
||||
|
||||
class Endpoint {
|
||||
|
||||
/**
|
||||
* @var Data_Sync_Entry $entry - The data sync entry to register the endpoint for.
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/**
|
||||
* @var string $rest_namespace - The namespace for the REST API endpoint.
|
||||
*/
|
||||
private $rest_namespace;
|
||||
|
||||
/**
|
||||
* @var string $route_base - The route for the REST API endpoint.
|
||||
*/
|
||||
private $route_base;
|
||||
|
||||
/**
|
||||
* @var Authenticated_Nonce $nonce - The nonce for the REST API endpoint.
|
||||
*/
|
||||
private $nonce;
|
||||
|
||||
/**
|
||||
* @param string $namespace - The namespace for the REST API endpoint.
|
||||
* @param string $route - The route for the REST API endpoint.
|
||||
* @param Data_Sync_Entry $entry The data sync entry to register the endpoint for.
|
||||
*/
|
||||
public function __construct( $namespace, $route, $entry ) {
|
||||
$this->entry = $entry;
|
||||
$this->rest_namespace = $namespace;
|
||||
$this->route_base = $route;
|
||||
$this->nonce = new Authenticated_Nonce( "{$namespace}_{$route}" );
|
||||
}
|
||||
|
||||
public function register_rest_routes() {
|
||||
|
||||
register_rest_route(
|
||||
$this->rest_namespace,
|
||||
$this->route_base,
|
||||
array(
|
||||
'methods' => 'GET, POST',
|
||||
'callback' => array( $this, 'handle_get' ),
|
||||
'permission_callback' => array( $this, 'permissions' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $this->entry->is( Entry_Can_Set::class ) ) {
|
||||
register_rest_route(
|
||||
$this->rest_namespace,
|
||||
$this->route_base . '/set',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'handle_set' ),
|
||||
'permission_callback' => array( $this, 'permissions' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->entry->is( Entry_Can_Merge::class ) ) {
|
||||
register_rest_route(
|
||||
$this->rest_namespace,
|
||||
$this->route_base . '/merge',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'handle_merge' ),
|
||||
'permission_callback' => array( $this, 'permissions' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->entry->is( Entry_Can_Delete::class ) ) {
|
||||
register_rest_route(
|
||||
$this->rest_namespace,
|
||||
$this->route_base . '/delete',
|
||||
array(
|
||||
'methods' => 'POST, DELETE',
|
||||
'callback' => array( $this, 'handle_delete' ),
|
||||
'permission_callback' => array( $this, 'permissions' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET Requests on /wp-json/<namespace>/<route>
|
||||
*
|
||||
* @param \WP_REST_Request $request - The request object.
|
||||
*/
|
||||
public function handle_get( $request ) {
|
||||
return $this->handler( $request, 'get' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST, PUT, PATCH Requests on /wp-json/<namespace>/<route>/set
|
||||
*
|
||||
* @param \WP_REST_Request $request - The request object.
|
||||
*/
|
||||
public function handle_set( $request ) {
|
||||
return $this->handler( $request, 'set' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST, PUT, PATCH Requests on /wp-json/<namespace>/<route>/merge
|
||||
*
|
||||
* @param \WP_REST_Request $request - The request object.
|
||||
*/
|
||||
public function handle_merge( $request ) {
|
||||
return $this->handler( $request, 'merge' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST, DELETE Requests on /wp-json/<namespace>/<route>/delete
|
||||
*
|
||||
* @param \WP_REST_Request $request - The request object.
|
||||
*/
|
||||
public function handle_delete( $request ) {
|
||||
return $this->handler( $request, 'delete' );
|
||||
}
|
||||
|
||||
private function response_error( $message, $code ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
'code' => $code,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function response_success( $data ) {
|
||||
$response = array(
|
||||
'status' => 'success',
|
||||
'JSON' => $data,
|
||||
);
|
||||
if ( DS_Utils::is_debug() ) {
|
||||
$response['log'] = $this->entry->get_parser()->get_log();
|
||||
}
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Route the request to the apropriate handler.
|
||||
*
|
||||
* @param \WP_REST_Request $request - The request object.
|
||||
*/
|
||||
private function handler( $request, $entry_method = 'get' ) {
|
||||
|
||||
$available_methods = array(
|
||||
'get' => Entry_Can_Get::class,
|
||||
'set' => Entry_Can_Set::class,
|
||||
'merge' => Entry_Can_Merge::class,
|
||||
'delete' => Entry_Can_Delete::class,
|
||||
);
|
||||
if ( ! isset( $available_methods[ $entry_method ] ) ) {
|
||||
// Set status 400 because an unsupported method was used.
|
||||
return rest_ensure_response( new \WP_Error( 'invalid_method', 'Invalid method.', array( 'status' => 400 ) ) );
|
||||
}
|
||||
|
||||
if ( ! $this->entry->is( $available_methods[ $entry_method ] ) ) {
|
||||
// Set Status 500 because the method is valid but is missing in Data_Sync_Entry.
|
||||
return rest_ensure_response( new \WP_Error( 'invalid_method', 'Invalid method. "' . $entry_method . '" ' ) );
|
||||
}
|
||||
|
||||
try {
|
||||
$params = $request->get_json_params();
|
||||
$data = $params['JSON'] ?? null;
|
||||
$result = $this->entry->$entry_method( $data );
|
||||
|
||||
if ( true === DS_Utils::debug_disable( $this->route_base ) ) {
|
||||
// Return 418 I'm a teapot if this is a debug request to the endpoint.
|
||||
return rest_ensure_response( new \WP_Error( 'teapot', "I'm a teapot.", array( 'status' => 418 ) ) );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->response_error( $result->get_error_message(), $result->get_error_code() );
|
||||
}
|
||||
|
||||
return $this->response_success( $result );
|
||||
} catch ( \RuntimeException $e ) {
|
||||
return $this->response_error( $e->getMessage(), 500 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a nonce for this endpoint
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function create_nonce() {
|
||||
return $this->nonce->create();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \WP_REST_Request $request
|
||||
*/
|
||||
public function permissions( $request ) {
|
||||
$nonce = $request->get_header( 'X-Jetpack-WP-JS-Sync-Nonce' );
|
||||
return $this->nonce->verify( $nonce ) && current_user_can( 'manage_options' );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user