File: /var/www/vabsp.kaunokolegija.lt/wp-content/plugins/polylang/include/Options/Options.php
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options;
use WP_Error;
use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use WP_Syntex\Polylang\Options\Abstract_Option;
defined( 'ABSPATH' ) || exit;
/**
* Class that manages Polylang's options:
* - Automatically stores the options into the database on `shutdown` if they have been modified.
* - Behaves almost like an array, meaning only values can be get/set (implements `ArrayAccess`).
* - Handles `switch_to_blog()`.
* - Options are always defined: it is not possible to unset them from the list, they are set to their default value instead.
* - If an option is not registered but exists in database, its raw value will be kept and remain untouched.
*
* @since 3.7
*
* @implements ArrayAccess<non-falsy-string, mixed>
* @implements IteratorAggregate<non-empty-string, mixed>
*/
class Options implements ArrayAccess, IteratorAggregate {
public const OPTION_NAME = 'polylang';
/**
* Polylang's options, by blog ID.
* Raw value if option is not registered yet, `Abstract_Option` instance otherwise.
*
* @var Abstract_Option[][]|mixed[][]
* @phpstan-var array<int, array<non-falsy-string, mixed>>
*/
private $options = array();
/**
* Tells if the options have been modified, by blog ID.
*
* @var bool[]
* @phpstan-var array<int, true>
*/
private $modified = array();
/**
* The original blog ID.
*
* @var int
*/
private $blog_id;
/**
* The current blog ID.
*
* @var int
*/
private $current_blog_id;
/**
* Cached options JSON schema by blog ID.
*
* @var array[]|null
*/
private $schema;
/**
* Constructor.
*
* @since 3.7
*/
public function __construct() {
// Keep track of the blog ID.
$this->blog_id = (int) get_current_blog_id();
$this->current_blog_id = $this->blog_id;
// Handle options.
$this->init_options_for_current_blog();
add_filter( 'pre_update_option_polylang', array( $this, 'protect_wp_option_storage' ), 1 );
add_action( 'switch_blog', array( $this, 'on_blog_switch' ), -1000 ); // Options must be ready early.
add_action( 'shutdown', array( $this, 'save_all' ), 1000 ); // Make sure to save options after everything.
}
/**
* Registers an option.
* Options must be registered in the right order: some options depend on other options' value.
*
* @since 3.7
*
* @param string $class_name Option class to register.
* @return self
*
* @phpstan-param class-string<Abstract_Option> $class_name
*/
public function register( string $class_name ): self {
foreach ( $this->options as &$options ) {
$key = $class_name::key();
if ( ! array_key_exists( $key, $options ) ) {
// Option raw value doesn't exist in database, use default instead.
$options[ $key ] = new $class_name();
continue;
}
// If option exists in database, use this value.
if ( $options[ $key ] instanceof Abstract_Option ) {
// Already registered, do nothing.
continue;
}
// Option raw value exists in database, use it.
$options[ $key ] = new $class_name( $options[ $key ] );
}
return $this;
}
/**
* Prevents storing an instance of `Options` into the database.
*
* @since 3.7
*
* @param array|Options $value The options to store.
* @return array
*/
public function protect_wp_option_storage( $value ) {
if ( $value instanceof self ) {
return $value->get_all();
}
return $value;
}
/**
* Initializes options for the newly switched blog if applicable.
*
* @since 3.7
*
* @param int $blog_id The blog ID.
* @return void
*/
public function on_blog_switch( $blog_id ): void {
$this->current_blog_id = (int) $blog_id;
if ( isset( $this->options[ $blog_id ] ) ) {
return;
}
if ( ! pll_is_plugin_active( POLYLANG_BASENAME ) && ! doing_action( 'activate_' . POLYLANG_BASENAME ) ) {
return;
}
$this->init_options_for_current_blog();
}
/**
* Stores the options into the database for all blogs.
* Hooked to `shutdown`.
*
* @since 3.7
*
* @return void
*/
public function save_all(): void {
// Find blog with modified options.
$modified = $this->get_modified();
if ( empty( $modified ) ) {
// Not modified.
return;
}
remove_action( 'switch_blog', array( $this, 'on_blog_switch' ), -1000 );
// Handle the original blog first, maybe this will prevent the use of `switch_to_blog()`.
if ( isset( $modified[ $this->blog_id ] ) && $this->current_blog_id === $this->blog_id ) {
$this->save();
unset( $modified[ $this->blog_id ] );
if ( empty( $modified ) ) {
// All done, no need of `switch_to_blog()`.
return;
}
}
foreach ( $modified as $blog_id => $_yup ) {
switch_to_blog( $blog_id );
$this->save();
restore_current_blog();
}
}
/**
* Stores the options into the database.
*
* @since 3.7
*
* @return bool True if the options were updated, false otherwise.
*/
public function save(): bool {
if ( empty( $this->modified[ $this->current_blog_id ] ) ) {
return false;
}
unset( $this->modified[ $this->current_blog_id ] );
if ( is_multisite() && ! get_site( $this->current_blog_id ) ) { // Cached by `$this->get_modified()` if called from `$this->save_all()`.
// Deleted. Should not happen if called from `$this->save_all()`.
return false;
}
$options = get_option( self::OPTION_NAME, array() );
if ( is_array( $options ) ) {
// Preserve options that are not from Polylang.
$options = array_merge( $options, $this->get_all() );
} else {
$options = $this->get_all();
}
return update_option( self::OPTION_NAME, $options );
}
/**
* Returns all options.
*
* @since 3.7
*
* @return mixed[] All options values.
*/
public function get_all(): array {
if ( empty( $this->options[ $this->current_blog_id ] ) ) {
// No options.
return array();
}
return array_map(
function ( $value ) {
return $value->get();
},
array_filter(
$this->options[ $this->current_blog_id ],
function ( $value ) {
return $value instanceof Abstract_Option;
}
)
);
}
/**
* Merges a subset of options into the current blog ones.
*
* @since 3.7
*
* @param array $values Array of raw options.
* @return WP_Error
*/
public function merge( array $values ): WP_Error {
$errors = new WP_Error();
foreach ( $this->options[ $this->current_blog_id ] as $key => $option ) {
if ( ! isset( $values[ $key ] ) || ! $this->has( $key ) ) {
continue;
}
$option_errors = $this->set( $key, $values[ $key ] );
if ( $option_errors->has_errors() ) {
// Blocking and non-blocking errors.
$errors->merge_from( $option_errors );
}
unset( $values[ $key ] );
}
if ( empty( $values ) ) {
return $errors;
}
// Merge all "unknown option" errors into a single error message.
if ( 1 === count( $values ) ) {
/* translators: %s is an option name. */
$message = __( 'Unknown option key %s.', 'polylang' );
} else {
/* translators: %s is a list of option names. */
$message = __( 'Unknown option keys %s.', 'polylang' );
}
$errors->add(
'pll_unknown_option_keys',
sprintf(
$message,
wp_sprintf_l(
'%l',
array_map(
function ( $value ) {
return "'$value'";
},
array_keys( $values )
)
)
)
);
return $errors;
}
/**
* Returns JSON schema for all options of the current blog.
*
* @since 3.7
*
* @return array The schema.
*/
public function get_schema(): array {
if ( isset( $this->schema[ $this->current_blog_id ] ) ) {
return $this->schema[ $this->current_blog_id ];
}
$properties = array();
if ( ! empty( $this->options[ $this->current_blog_id ] ) ) {
foreach ( $this->options[ $this->current_blog_id ] as $option ) {
if ( ! $option instanceof Abstract_Option ) {
continue;
}
$properties[ $option->key() ] = $option->get_schema();
}
}
$this->schema[ $this->current_blog_id ] = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => static::OPTION_NAME,
'description' => __( 'Polylang options', 'polylang' ),
'type' => 'object',
'properties' => $properties,
'additionalProperties' => false,
);
return $this->schema[ $this->current_blog_id ];
}
/**
* Tells if an option exists.
*
* @since 3.7
*
* @param string $key The name of the option to check for.
* @return bool
*/
public function has( string $key ): bool {
return isset( $this->options[ $this->current_blog_id ][ $key ] ) && $this->options[ $this->current_blog_id ][ $key ] instanceof Abstract_Option;
}
/**
* Returns the value of the specified option.
*
* @since 3.7
*
* @param string $key The name of the option to retrieve.
* @return mixed
*/
public function get( string $key ) {
if ( ! $this->has( $key ) ) {
$v = null;
return $v;
}
/** @var Abstract_Option */
$option = $this->options[ $this->current_blog_id ][ $key ];
return $option->get();
}
/**
* Assigns a value to the specified option.
*
* This doesn't allow to set an unknown option.
* When doing multiple `set()`, options must be set in the right order: some options depend on other options' value.
*
* @since 3.7
*
* @param string $key The name of the option to assign the value to.
* @param mixed $value The value to set.
* @return WP_Error
*/
public function set( string $key, $value ): WP_Error {
if ( ! $this->has( $key ) ) {
/* translators: %s is the name of an option. */
return new WP_Error( 'pll_unknown_option_key', sprintf( __( 'Unknown option key %s.', 'polylang' ), "'$key'" ) );
}
/** @var Abstract_Option */
$option = $this->options[ $this->current_blog_id ][ $key ];
$old_value = $option->get();
if ( $option->set( $value, $this ) && $option->get() !== $old_value ) {
// No blocking errors: the value can be stored.
$this->modified[ $this->current_blog_id ] = true;
}
// Return errors.
return $option->get_errors();
}
/**
* Resets an option to its default value.
*
* @since 3.7
*
* @param string $key The name of the option to reset.
* @return mixed The new value.
*/
public function reset( string $key ) {
if ( ! $this->has( $key ) ) {
return null;
}
/** @var Abstract_Option */
$option = $this->options[ $this->current_blog_id ][ $key ];
if ( $option->get() !== $option->reset() ) {
$this->modified[ $this->current_blog_id ] = true;
}
return $option->get();
}
/**
* Tells if an option exists.
* Required by interface `ArrayAccess`.
*
* @since 3.7
*
* @param string $offset The name of the option to check for.
* @return bool
*/
public function offsetExists( $offset ): bool {
return $this->has( (string) $offset );
}
/**
* Returns the value of the specified option.
* Required by interface `ArrayAccess`.
*
* @since 3.7
*
* @param string $offset The name of the option to retrieve.
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet( $offset ) {
return $this->get( (string) $offset );
}
/**
* Assigns a value to the specified option.
* This doesn't allow to set an unknown option.
* Required by interface `ArrayAccess`.
*
* @since 3.7
*
* @param string $offset The name of the option to assign the value to.
* @param mixed $value The value to set.
* @return void
*/
public function offsetSet( $offset, $value ): void {
$this->set( (string) $offset, $value );
}
/**
* Resets an option.
* This doesn't allow to unset an option, this resets it to its default value instead.
* Required by interface `ArrayAccess`.
*
* @since 3.7
*
* @param string $offset The name of the option to unset.
* @return void
*/
public function offsetUnset( $offset ): void {
$this->reset( (string) $offset );
}
/**
* Returns all current site's option values.
* Required by interface `IteratorAggregate`.
*
* @since 3.7
*
* @return ArrayIterator
*
* @phpstan-return ArrayIterator<non-empty-string, mixed>
*/
public function getIterator(): ArrayIterator {
return new ArrayIterator( $this->get_all() );
}
/**
* Returns the list of modified sites.
* On multisite, sites are cached.
* /!\ At this point, some sites may have been deleted. They are removed from `$this->modified` here.
*
* @since 3.7
*
* @return bool[]
* @phpstan-return array<int, true>
*/
private function get_modified(): array {
if ( empty( $this->modified ) ) {
// Not modified.
return $this->modified;
}
// Cleanup deleted sites and cache existing ones.
if ( ! is_multisite() ) {
// Not multisite: no need to cache or verify existence.
return $this->modified;
}
// Fetch all the data instead of only the IDs, so it is cached.
$sites = get_sites(
array(
'site__in' => array_keys( $this->modified ),
'number' => count( $this->modified ),
)
);
// Keep only existing blogs.
$this->modified = array();
foreach ( $sites as $site ) {
$this->modified[ $site->id ] = true;
}
return $this->modified;
}
/**
* Initializes options for the current blog.
*
* @since 3.7
*
* @return void
*/
private function init_options_for_current_blog(): void {
$options = get_option( self::OPTION_NAME );
if ( empty( $options ) || ! is_array( $options ) ) {
$this->options[ $this->current_blog_id ] = array();
$this->modified[ $this->current_blog_id ] = true;
} else {
$this->options[ $this->current_blog_id ] = $options;
}
/**
* Fires after the options have been init for the current blog.
* This is the best place to register options.
*
* @since 3.7
*
* @param Options $options Instance of the options.
* @param int $current_blog_id Current blog ID.
*/
do_action( 'pll_init_options_for_blog', $this, $this->current_blog_id );
}
}