feat: initial ACRIB WordPress deployment

- WordPress 6.9.4 (es_ES) with Kadence theme
- Homepage: Hero, La Asociación, Pilares, Beneficios, Eventos, Miembros, Hazte Miembro, Contacto
- Brand identity: #13294b navy, #a12932 burgundy, #c69c48 gold
- Fonts: Raleway (headings) + Source Sans 3 (body) + Lato (UI)
- Plugins: Kadence Blocks, Polylang, Contact Form 7
- Custom CSS with full brand styling and responsive layout
- HTTPS enforced via wp-config.php proxy detection
This commit is contained in:
Malin
2026-05-19 19:25:59 +02:00
commit f3ff7b7186
6119 changed files with 1984255 additions and 0 deletions

View File

@@ -0,0 +1,338 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options;
use WP_Error;
use WP_Term;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining a single option.
*
* @since 3.7
*
* @phpstan-type SchemaType 'string'|'null'|'number'|'integer'|'boolean'|'array'|'object'
* @phpstan-type Schema array{
* type: SchemaType,
* description: string,
* default: mixed
* }
*/
abstract class Abstract_Option {
/**
* Option value.
*
* @var mixed
*/
protected $value;
/**
* Cached option JSON schema.
*
* @var array|null
*
* @phpstan-var Schema|null
*/
private $schema;
/**
* Validation and sanitization errors.
*
* @var WP_Error
*/
protected $errors;
/**
* Constructor.
*
* @since 3.7
*
* @param mixed $value Optional. Option value.
*/
public function __construct( $value = null ) {
$this->errors = new WP_Error();
if ( ! isset( $value ) ) {
$this->value = $this->get_default();
return;
}
$value = rest_sanitize_value_from_schema( $this->prepare( $value ), $this->get_data_structure(), static::key() );
if ( ! is_wp_error( $value ) ) {
$this->value = $value;
} else {
$this->value = $this->get_default();
}
}
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return non-falsy-string
*/
abstract public static function key(): string;
/**
* Sets option's value if valid, does nothing otherwise.
*
* @since 3.7
*
* @param mixed $value Value to set.
* @param Options $options All options.
* @return bool True if the value has been assigned. False in case of errors.
*/
public function set( $value, Options $options ): bool {
$this->errors = new WP_Error(); // Reset errors.
$value = $this->prepare( $value );
$is_valid = rest_validate_value_from_schema( $value, $this->get_data_structure(), static::key() );
if ( is_wp_error( $is_valid ) ) {
// Blocking validation error.
$this->errors->merge_from( $is_valid );
return false;
}
$value = $this->sanitize( $value, $options );
if ( is_wp_error( $value ) ) {
// Blocking sanitization error.
$this->errors->merge_from( $value );
return false;
}
$this->value = $value;
return true;
}
/**
* Returns option's value.
*
* @since 3.7
*
* @return mixed
*/
public function get() {
return $this->value;
}
/**
* Sets default option value.
*
* @since 3.7
*
* @return mixed The new value.
*/
public function reset() {
$this->value = $this->get_default();
return $this->value;
}
/**
* Returns JSON schema of the option.
*
* @since 3.7
*
* @return array The schema.
*
* @phpstan-return Schema
*/
public function get_schema(): array {
if ( is_array( $this->schema ) ) {
return $this->schema;
}
$this->schema = array_merge(
array(
'description' => $this->get_description(),
'default' => $this->get_default(),
),
$this->get_data_structure()
);
return $this->schema;
}
/**
* Returns non-blocking sanitization errors.
*
* @since 3.7
*
* @return WP_Error
*/
public function get_errors(): WP_Error {
return $this->errors;
}
/**
* Prepares a value before validation.
*
* @since 3.7
*
* @param mixed $value Value to format.
* @return mixed
*/
protected function prepare( $value ) {
return $value;
}
/**
* Sanitizes option's value, can be overridden for specific cases not handled by `rest_sanitize_value_from_schema()`.
* Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
* the value is sanitized and can be stored.
*
* @since 3.7
*
* @param mixed $value Value to sanitize.
* @param Options $options All options.
* @return mixed The sanitized value. An instance of `WP_Error` in case of blocking error.
*/
protected function sanitize( $value, Options $options ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return rest_sanitize_value_from_schema( $value, $this->get_data_structure(), static::key() );
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return mixed
*/
abstract protected function get_default();
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: SchemaType}
*/
abstract protected function get_data_structure(): array;
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
abstract protected function get_description(): string;
/**
* Returns a list of language terms.
*
* @since 3.7
*
* @return array
*
* @phpstan-return list<WP_Term>
*/
protected function get_language_terms(): array {
$language_terms = get_terms(
array(
'taxonomy' => 'language',
'hide_empty' => false,
)
);
return is_array( $language_terms ) ? $language_terms : array();
}
/**
* Adds a non-blocking error warning about unknown language slugs.
*
* @since 3.7
*
* @param array $language_slugs List of language slugs.
* @return void
*/
protected function add_unknown_languages_warning( array $language_slugs ): void {
if ( 1 === count( $language_slugs ) ) {
/* translators: %s is a language slug. */
$message = __( 'The language %s is unknown and has been discarded.', 'polylang' );
} else {
/* translators: %s is a list of language slugs. */
$message = __( 'The languages %s are unknown and have been discarded.', 'polylang' );
}
$this->errors->add(
sprintf( 'pll_unknown_%s_languages', static::key() ),
sprintf(
$message,
wp_sprintf_l(
'%l',
array_map(
function ( $slug ) {
return "<code>{$slug}</code>";
},
$language_slugs
)
)
),
'warning'
);
}
/**
* Adds information to the site health info array.
* Does nothing by default.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return array();
}
/**
* Renders site health information by appending additional fields.
*
* @since 3.8
*
* @param mixed $value The value to be added to the site health fields.
*
* @return array Updated array of site health information including the new fields.
*/
protected function format_single_value_for_site_health_info( $value ): array {
if ( empty( $value ) ) {
return array();
}
return array(
'label' => ucfirst( static::key() ),
'value' => $value,
);
}
/**
* Formats an array to display in options information.
*
* @since 3.8
*
* @param array<scalar> $array An array of formatted data.
* @return string
*/
protected function format_array_for_site_health_info( array $array ): string {
array_walk(
$array,
function ( &$value, $key ) {
$value = "$key => $value";
}
);
return '[' . implode( ', ', $array ) . ']';
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Primitive\Abstract_List;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining object types list option.
*
* @since 3.7
*/
abstract class Abstract_Object_Types extends Abstract_List {
/**
* Sanitizes option's value.
* Can return a `WP_Error` object in case of blocking sanitization error: the value must be rejected then.
*
* @since 3.7
*
* @param array $value Value to filter.
* @param Options $options All options.
* @return array|WP_Error The sanitized value. An instance of `WP_Error` in case of blocking error.
*
* @phpstan-return list<non-falsy-string>|WP_Error
*/
protected function sanitize( $value, Options $options ) {
$value = parent::sanitize( $value, $options );
if ( is_wp_error( $value ) ) {
// Blocking sanitization error.
return $value;
}
/** @var array $value */
return array_values( array_intersect( $value, $this->get_object_types() ) );
}
/**
* Returns non-core object types.
*
* @since 3.7
*
* @return string[] Object type names list.
*
* @phpstan-return array<non-falsy-string>
*/
abstract protected function get_object_types(): array;
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Boolean;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "Detect browser language" boolean option.
* /!\ Sanitization depends on `force_lang`: this option must be set AFTER `force_lang`.
*
* @since 3.7
*/
class Browser extends Abstract_Boolean {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'browser'
*/
public static function key(): string {
return 'browser';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! $this->get() ) {
$value = '0: ' . __( 'Detect browser language deactivated', 'polylang' );
} else {
$value = '1: ' . __( 'Detect browser language activated', 'polylang' );
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Sanitizes option's value.
* Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
* the value is sanitized and can be stored.
*
* @since 3.7
*
* @param bool $value Value to sanitize.
* @param Options $options All options.
* @return bool|WP_Error The sanitized value. An instance of `WP_Error` in case of blocking error.
*/
protected function sanitize( $value, Options $options ) {
if ( 3 === $options->get( 'force_lang' ) && ! class_exists( 'PLL_Xdata_Domain', true ) ) {
// Cannot share cookies between domains without Polylang Pro.
return false;
}
/** @var bool|WP_Error */
$value = parent::sanitize( $value, $options );
return $value;
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return sprintf(
/* translators: %1$s and %2$s are "true/false" values. */
__( 'Detect preferred browser language on front page: %1$s to detect, %2$s to not detect.', 'polylang' ),
'`true`',
'`false`'
);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Model\Languages;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Options\Primitive\Abstract_String;
defined( 'ABSPATH' ) || exit;
/**
* Class defining language slug string option.
*
* @since 3.7
*/
class Default_Lang extends Abstract_String {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'default_lang'
*/
public static function key(): string {
return 'default_lang';
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'string', pattern: Languages::SLUG_PATTERN}
*/
protected function get_data_structure(): array {
$string_schema = parent::get_data_structure();
$string_schema['pattern'] = Languages::SLUG_PATTERN;
return $string_schema;
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'Slug of the default language.', 'polylang' );
}
/**
* Sanitizes option's value.
* Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
* the value is sanitized and can be stored.
*
* @since 3.7
*
* @param string $value Value to sanitize.
* @param Options $options All options.
* @return string|WP_Error The sanitized value. An instance of `WP_Error` in case of error.
*/
protected function sanitize( $value, Options $options ) {
$value = parent::sanitize( $value, $options );
if ( is_wp_error( $value ) ) {
return $value;
}
/** @var string $value */
if ( ! get_term_by( 'slug', $value, 'language' ) ) {
return new WP_Error( 'pll_invalid_language', sprintf( 'The language slug \'%s\' is not a valid language.', $value ) );
}
return $value;
}
}

View File

@@ -0,0 +1,210 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Model\Languages;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Map;
defined( 'ABSPATH' ) || exit;
/**
* Class defining single associative array of domain as value and language slug as key option.
* /!\ Sanitization depends on `force_lang`: this option must be set AFTER `force_lang`.
*
* @since 3.7
*
* @phpstan-type DomainsValue array<non-falsy-string, string>
*/
class Domains extends Abstract_Map {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'domains'
*/
public static function key(): string {
return 'domains';
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return array
*/
protected function get_default() {
return array();
}
/**
* Returns the JSON schema part specific to the inner structure of this option.
*
* @since 3.8
*
* @return array Inner structure.
*/
protected function get_inner_structure(): array {
return array(
'patternProperties' => array(
Languages::SLUG_PATTERN => array( // Language slug as key.
'type' => 'string',
'format' => 'uri',
),
),
'additionalProperties' => false,
);
}
/**
* Sanitizes option's value.
* Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
* the value is sanitized and can be stored.
*
* @since 3.7
*
* @param array $value Value to sanitize.
* @param Options $options All options.
* @return array|WP_Error The sanitized value. An instance of `WP_Error` in case of blocking error.
*
* @phpstan-return DomainsValue|WP_Error
*/
protected function sanitize( $value, Options $options ) {
// Sanitize new URLs.
$value = parent::sanitize( $value, $options );
if ( is_wp_error( $value ) ) {
// Blocking error.
return $value;
}
/** @phpstan-var DomainsValue */
$current_value = $this->get();
/** @phpstan-var DomainsValue $value */
$all_values = array(); // Previous and new values.
$missing_langs = array(); // Lang names corresponding to the empty values.
$language_terms = $this->get_language_terms();
// Detect empty values, fill missing keys with previous values.
foreach ( $language_terms as $lang ) {
if ( array_key_exists( $lang->slug, $value ) ) {
// Use the new value.
$all_values[ $lang->slug ] = $value[ $lang->slug ];
unset( $value[ $lang->slug ] );
} else {
// Use previous value.
$all_values[ $lang->slug ] = $current_value[ $lang->slug ] ?? '';
}
if ( empty( $all_values[ $lang->slug ] ) ) {
// The value is empty.
$missing_langs[] = $lang->name;
}
}
// Detect invalid language slugs.
if ( ! empty( $value ) ) {
// Non-blocking error.
$this->add_unknown_languages_warning( array_keys( $value ) );
}
if ( 3 === $options->get( 'force_lang' ) && ! empty( $missing_langs ) ) {
// Non-blocking error.
if ( 1 === count( $missing_langs ) ) {
/* translators: %s is a native language name. */
$message = __( 'Please enter a valid URL for %s.', 'polylang' );
} else {
/* translators: %s is a list of native language names. */
$message = __( 'Please enter valid URLs for %s.', 'polylang' );
}
$this->errors->add(
'pll_empty_domains',
sprintf( $message, wp_sprintf_l( '%l', $missing_langs ) ),
'warning'
);
}
// Ping all URLs to make sure they are valid.
if ( $options->get( 'force_lang' ) > 1 ) {
$failed_urls = array();
foreach ( array_filter( $all_values ) as $url ) {
$url = add_query_arg( 'deactivate-polylang', 1, $url );
// Don't redefine vip_safe_wp_remote_get() as it has not the same signature as wp_remote_get().
$response = function_exists( 'vip_safe_wp_remote_get' ) ? vip_safe_wp_remote_get( $url ) : wp_remote_get( $url );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$failed_urls[] = $url;
}
}
if ( ! empty( $failed_urls ) ) {
// Non-blocking error.
if ( 1 === count( $failed_urls ) ) {
/* translators: %s is a URL. */
$message = __( 'Polylang was unable to access the %s URL. Please check that the URL is valid.', 'polylang' );
} else {
/* translators: %s is a list of URLs. */
$message = __( 'Polylang was unable to access the %s URLs. Please check that the URLs are valid.', 'polylang' );
}
$this->errors->add(
'pll_invalid_domains',
sprintf( $message, wp_sprintf_l( '%l', $failed_urls ) ),
'warning'
);
}
}
/** @phpstan-var DomainsValue */
return $all_values;
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'Domains used when the language is set from different domains.', 'polylang' );
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( 3 === $options->get( 'force_lang' ) ) {
return $this->format_single_value_for_site_health_info( $this->get() );
}
return parent::get_site_health_info( $options );
}
/**
* Returns the reset value for a key.
*
* @since 3.8
*
* @param string $key The key to reset. Unused.
* @return mixed The reset value.
*/
protected function reset_value( string $key ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return '';
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Syntex\Polylang\Options\Abstract_Option;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the first activation option.
*
* @since 3.7
*/
class First_Activation extends Abstract_Option {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'first_activation'
*/
public static function key(): string {
return 'first_activation';
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return int
*
* @phpstan-return int<0, max>
*/
protected function get_default() {
return time();
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'integer', minimum: 0, maximum: int<0, max>, readonly: true}
*/
protected function get_data_structure(): array {
return array(
'type' => 'integer',
'minimum' => 0,
'maximum' => PHP_INT_MAX,
'readonly' => true,
);
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'Time of first activation of Polylang.', 'polylang' );
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return $this->format_single_value_for_site_health_info( wp_date( get_option( 'date_format' ), $this->get() ) );
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Syntex\Polylang\Options\Abstract_Option;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "Determine how the current language is defined" option.
*
* @since 3.7
*/
class Force_Lang extends Abstract_Option {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'force_lang'
*/
public static function key(): string {
return 'force_lang';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
switch ( $this->get() ) {
case '0':
$value = '0: ' . __( 'The language is set from content', 'polylang' );
break;
case '1':
$value = '1: ' . __( 'The language is set from the directory name in pretty permalinks', 'polylang' );
break;
case '2':
$value = '2: ' . __( 'The language is set from the subdomain name in pretty permalinks', 'polylang' );
break;
case '3':
$value = '3: ' . __( 'The language is set from different domains', 'polylang' );
break;
default:
$value = '';
break;
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return int
*/
protected function get_default() {
return 1;
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'integer', enum: list<0|1|2|3>|list<1|2|3>}
*/
protected function get_data_structure(): array {
return array(
'type' => 'integer',
'enum' => 'yes' === get_option( 'pll_language_from_content_available' ) ? array( 0, 1, 2, 3 ) : array( 1, 2, 3 ),
);
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'Determine how the current language is defined.', 'polylang' );
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Boolean;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "Display/Hide URL language information for default language" boolean option.
* /!\ Sanitization depends on `force_lang`: this option must be set AFTER `force_lang`.
*
* @since 3.7
*/
class Hide_Default extends Abstract_Boolean {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'hide_default'
*/
public static function key(): string {
return 'hide_default';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( $this->get() ) {
$value = '1: ' . __( 'Hide URL language information for default language', 'polylang' );
} else {
$value = '0: ' . __( 'Display URL language information for default language', 'polylang' );
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return bool
*/
protected function get_default() {
return true;
}
/**
* Sanitizes option's value.
* Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
* the value is sanitized and can be stored.
*
* @since 3.7
*
* @param bool $value Value to sanitize.
* @param Options $options All options.
* @return bool|WP_Error The sanitized value. An instance of `WP_Error` in case of blocking error.
*/
protected function sanitize( $value, Options $options ) {
if ( 3 === $options->get( 'force_lang' ) ) {
return false;
}
/** @var bool|WP_Error */
return parent::sanitize( $value, $options );
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return sprintf(
/* translators: %1$s and %2$s are "true/false" values. */
__( 'Remove the language code in URL for the default language: %1$s to hide, %2$s to display.', 'polylang' ),
'`true`',
'`false`'
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Boolean;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "Translate media" boolean option.
*
* @since 3.7
*/
class Media_Support extends Abstract_Boolean {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'media_support'
*/
public static function key(): string {
return 'media_support';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( $this->get() ) {
$value = '1: ' . __( 'The media are translated', 'polylang' );
} else {
$value = '0: ' . __( 'The media are not translated', 'polylang' );
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return sprintf(
/* translators: %1$s and %2$s are "true/false" values. */
__( 'Translate media: %1$s to translate, %2$s otherwise.', 'polylang' ),
'`true`',
'`false`'
);
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Abstract_Option;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Model\Languages;
defined( 'ABSPATH' ) || exit;
/**
* Class defining navigation menus array option.
*
* @since 3.7
*
* @phpstan-type NavMenusValue array<
* non-falsy-string,
* array<
* non-falsy-string,
* array<non-falsy-string, int<0, max>>
* >
* >
*/
class Nav_Menus extends Abstract_Option {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'nav_menus'
*/
public static function key(): string {
return 'nav_menus';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$current_theme = get_stylesheet();
/** @phpstan-var NavMenusValue $nav_menus */
$nav_menus = $this->get();
if ( empty( $nav_menus[ $current_theme ] ) ) {
return array();
}
$parts = array();
foreach ( $nav_menus[ $current_theme ] as $location => $lang ) {
if ( empty( $lang ) ) {
$parts[] = sprintf( '%1$s: %2$s', $location, __( 'Not used', 'polylang' ) );
} else {
$parts[] = sprintf(
'%1$s: %2$s',
$location,
$this->format_array_for_site_health_info( $lang )
);
}
}
return $this->format_single_value_for_site_health_info( implode( ' | ', $parts ) );
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return array
*/
protected function get_default() {
return array();
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*/
protected function get_data_structure(): array {
return array(
'type' => 'object', // Correspond to associative array in PHP, @see{https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types}.
'patternProperties' => array(
'[^\/:<>\*\?"\|]+' => array( // Excludes invalid directory name characters @see https://developer.wordpress.org/reference/classes/wp_rest_themes_controller/register_routes/
'type' => 'object',
'patternProperties' => array(
'[\w-]+' => array( // Accepted characters for menu locations @see https://developer.wordpress.org/reference/classes/wp_rest_menu_locations_controller/register_routes/
'type' => 'object',
'patternProperties' => array(
Languages::SLUG_PATTERN => array( // Language slug as key.
'type' => 'integer',
'minimum' => 0, // A post ID.
),
),
'additionalProperties' => false,
),
),
'additionalProperties' => false,
),
),
'additionalProperties' => false,
);
}
/**
* Sanitizes option's value.
* Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
* the value is sanitized and can be stored.
*
* @since 3.7
*
* @param array $value Value to sanitize.
* @param Options $options All options.
* @return array|WP_Error The sanitized value. An instance of `WP_Error` in case of blocking error.
*
* @phpstan-return NavMenusValue|WP_Error
*/
protected function sanitize( $value, Options $options ) {
// Sanitize new value.
$value = parent::sanitize( $value, $options );
if ( is_wp_error( $value ) ) {
// Blocking error.
return $value;
}
/** @phpstan-var NavMenusValue $value */
if ( empty( $value ) ) {
// Nothing to validate.
return $value;
}
$all_langs = array();
$language_terms = wp_list_pluck( $this->get_language_terms(), 'slug' );
foreach ( $value as $theme_slug => $menu_ids_by_location ) {
foreach ( $menu_ids_by_location as $location => $menu_ids ) {
// Make sure the language slugs correspond to an existing language.
$value[ $theme_slug ][ $location ] = array();
foreach ( $language_terms as $lang_slug ) {
if ( ! empty( $menu_ids[ $lang_slug ] ) ) {
$value[ $theme_slug ][ $location ][ $lang_slug ] = $menu_ids[ $lang_slug ];
}
}
// Detect unknown languages.
$all_langs = array_merge( $all_langs, $menu_ids );
}
}
/** @phpstan-var NavMenusValue $value */
$unknown_langs = array_diff_key( $all_langs, array_flip( $language_terms ) );
// Detect invalid language slugs.
if ( ! empty( $unknown_langs ) ) {
// Non-blocking error.
$this->add_unknown_languages_warning( array_keys( $unknown_langs ) );
}
return $value;
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'Translated navigation menus for each theme.', 'polylang' );
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
defined( 'ABSPATH' ) || exit;
/**
* Class defining post types list option.
*
* @since 3.7
*/
class Post_Types extends Abstract_Object_Types {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'post_types'
*/
public static function key(): string {
return 'post_types';
}
/**
* Returns non-core post types.
*
* @since 3.7
*
* @return string[] Object type names list.
*
* @phpstan-return array<non-falsy-string>
*/
protected function get_object_types(): array {
/** @phpstan-var array<non-falsy-string> */
return get_post_types( array( '_builtin' => false ) );
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'List of post types to translate.', 'polylang' );
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "previous version" option.
*
* @since 3.7
*/
class Previous_Version extends Version {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'previous_version'
*/
public static function key(): string {
return 'previous_version';
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( "Polylang's previous version.", 'polylang' );
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! $this->get() ) {
return $this->format_single_value_for_site_health_info( __( 'This is the first activation', 'polylang' ) );
}
return parent::get_site_health_info( $options );
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Boolean;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "Remove the page name or page id from the URL of the front page" boolean option.
*
* @since 3.7
*/
class Redirect_Lang extends Abstract_Boolean {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'redirect_lang'
*/
public static function key(): string {
return 'redirect_lang';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( $this->get() ) {
$value = '1: ' . __( 'The front page URL contains the language code instead of the page name or page id', 'polylang' );
} else {
$value = '0: ' . __( 'The front page URL contains the page name or page id instead of the language code', 'polylang' );
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return sprintf(
/* translators: %1$s and %2$s are "true/false" values. */
__( 'Remove the page name or page ID from the URL of the front page: %1$s to remove, %2$s to keep.', 'polylang' ),
'`true`',
'`false`'
);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Error;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Boolean;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "Remove /language/ in pretty permalinks" boolean option.
*
* @since 3.7
*/
class Rewrite extends Abstract_Boolean {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'rewrite'
*/
public static function key(): string {
return 'rewrite';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( $this->get() ) {
$value = '1: ' . sprintf(
/* translators: %s is a URL slug: `/language/`. */
__( 'Remove %s in pretty permalinks', 'polylang' ),
'`/language/`'
);
} else {
$value = '0: ' . sprintf(
/* translators: %s is a URL slug: `/language/`. */
__( 'Keep %s in pretty permalinks', 'polylang' ),
'`/language/`'
);
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return bool
*/
protected function get_default() {
return true;
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return sprintf(
/* translators: %1$s is a URL slug: `/language/`. %2$s and %3$s are "true/false" values. */
__( 'Remove %1$s in pretty permalinks: %2$s to remove, %3$s to keep.', 'polylang' ),
'`/language/`',
'`true`',
'`false`'
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use NOOP_Translations;
use PLL_Settings_Sync;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Options\Primitive\Abstract_List;
defined( 'ABSPATH' ) || exit;
/**
* Class defining synchronization settings list option.
*
* @since 3.7
*
* @phpstan-import-type SchemaType from \WP_Syntex\Polylang\Options\Abstract_Option
*/
class Sync extends Abstract_List {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'sync'
*/
public static function key(): string {
return 'sync';
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( empty( $this->get() ) ) {
$value = '0: ' . __( 'Synchronization disabled', 'polylang' );
} else {
$value = implode( ', ', $this->get() );
}
return $this->format_single_value_for_site_health_info( $value );
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'array', items: array{type: SchemaType, enum: non-empty-list<non-falsy-string>}}
*/
protected function get_data_structure(): array {
$GLOBALS['l10n']['polylang'] = new NOOP_Translations(); // Prevents loading the translations too early.
$enum = array_keys( PLL_Settings_Sync::list_metas_to_sync() );
unset( $GLOBALS['l10n']['polylang'] );
return array(
'type' => 'array',
'items' => array(
'type' => $this->get_type(),
'enum' => $enum,
),
);
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'List of data to synchronize.', 'polylang' );
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
defined( 'ABSPATH' ) || exit;
/**
* Class defining taxonomies list option.
*
* @since 3.7
*/
class Taxonomies extends Abstract_Object_Types {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'taxonomies'
*/
public static function key(): string {
return 'taxonomies';
}
/**
* Returns non-core taxonomies.
*
* @since 3.7
*
* @return string[] Object type names list.
*
* @phpstan-return array<non-falsy-string>
*/
protected function get_object_types(): array {
$public_taxonomies = get_taxonomies( array( '_builtin' => false ) );
/** @phpstan-var array<non-falsy-string> */
return array_diff( $public_taxonomies, get_taxonomies( array( '_pll' => true ) ) );
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( 'List of taxonomies to translate.', 'polylang' );
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Business;
use WP_Syntex\Polylang\Options\Primitive\Abstract_String;
defined( 'ABSPATH' ) || exit;
/**
* Class defining the "version" option.
*
* @since 3.7
*/
class Version extends Abstract_String {
/**
* Returns option key.
*
* @since 3.7
*
* @return string
*
* @phpstan-return 'version'
*/
public static function key(): string {
return 'version';
}
/**
* Returns the description used in the JSON schema.
*
* @since 3.7
*
* @return string
*/
protected function get_description(): string {
return __( "Polylang's version.", 'polylang' );
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'string', readonly: true, readonly: true}
*/
protected function get_data_structure(): array {
return array_merge( parent::get_data_structure(), array( 'readonly' => true ) );
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options;
use WP_Error;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining a decorator for options when Polylang is not active on the current site.
*
* @since 3.8
*/
class Inactive_Option extends Abstract_Option {
public const ERROR_CODE = 'pll_not_active';
/**
* The option to decorate.
*
* @var Abstract_Option
*/
private $option;
/**
* The key of the option to decorate.
*
* @var string
*
* @phpstan-var non-falsy-string
*/
private static $key = 'not-an-option';
/**
* Constructor.
*
* @since 3.8
*
* @param Abstract_Option $option The option to wrap.
*/
public function __construct( Abstract_Option $option ) {
$this->option = $option;
$this->errors = new WP_Error();
// Make sure the option doesn't contain any value.
$this->option->reset();
}
/**
* Returns option key.
*
* @since 3.8
*
* @return string
*
* @phpstan-return non-falsy-string
*/
public static function key(): string {
return self::$key;
}
/**
* Does nothing except adding an error.
*
* @since 3.8
*
* @param mixed $value Value to set.
* @param Options $options All options.
* @return bool True if the value has been assigned. False in case of errors.
*/
public function set( $value, Options $options ): bool { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! in_array( self::ERROR_CODE, $this->errors->get_error_codes(), true ) ) {
$this->errors->add(
self::ERROR_CODE,
/* translators: %s is a blog ID. */
sprintf( __( 'Polylang is not active on site %s.', 'polylang' ), (int) get_current_blog_id() )
);
}
return false;
}
/**
* Returns the value of the option, usually the default value for inactive options.
*
* @since 3.8
*
* @return mixed
*/
public function get() {
return $this->option->get();
}
/**
* Returns an empty schema.
*
* @since 3.8
*
* @return array The schema.
*/
public function get_schema(): array {
return array();
}
/**
* Returns the default value.
*
* @since 3.8
*
* @return mixed
*/
protected function get_default() {
return $this->option->get();
}
/**
* Not used but required by `Abstract_Option`.
*
* @since 3.8
*
* @return array Partial schema.
*/
protected function get_data_structure(): array {
return array();
}
/**
* Not used but required by `Abstract_Option`.
*
* @since 3.8
*
* @return string
*/
protected function get_description(): string {
return '';
}
}

View File

@@ -0,0 +1,740 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options;
use WP_Error;
use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use WP_Syntex\Polylang\Options\Abstract_Option;
use WP_Syntex\Polylang\Options\Primitive\Abstract_Map;
use WP_Syntex\Polylang\Options\Primitive\Abstract_List;
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>
*
* @phpstan-import-type Schema from Abstract_Option as OptionSchema
* @phpstan-type Schema array{
* '$schema': non-falsy-string,
* title: non-falsy-string,
* description: string,
* type: 'object',
* properties: array<non-falsy-string, OptionSchema>,
* additionalProperties: false
* }
*/
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;
/**
* Map of memoized values of blog IDs to tell if Polylang is active.
*
* @var bool[]
* @phpstan-var array<int, bool>
*/
private $is_plugin_active = array();
/**
* Cached options JSON schema by blog ID.
*
* @var array[]|null
* @phpstan-var array<int, Schema>|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;
$this->is_plugin_active = array( $this->blog_id => true );
// 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 {
$key = $class_name::key();
if ( ! array_key_exists( $key, $this->options[ $this->current_blog_id ] ) ) {
// Option raw value doesn't exist in database, use default instead.
$this->options[ $this->current_blog_id ][ $key ] = $this->maybe_make_option_inactive(
new $class_name()
);
return $this;
}
// If option exists in database, use this value.
if ( $this->options[ $this->current_blog_id ][ $key ] instanceof Abstract_Option ) {
// Already registered, do nothing.
$this->options[ $this->current_blog_id ][ $key ] = $this->maybe_make_option_inactive(
$this->options[ $this->current_blog_id ][ $key ]
);
return $this;
}
// Option raw value exists in database, use it.
$this->options[ $this->current_blog_id ][ $key ] = $this->maybe_make_option_inactive(
new $class_name( $this->options[ $this->current_blog_id ][ $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;
}
$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 the name of an option. */
$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.
*
* @phpstan-return Schema
*/
public function get_schema(): array {
if ( isset( $this->schema[ $this->current_blog_id ] ) ) {
return $this->schema[ $this->current_blog_id ];
}
$properties = array();
if ( $this->is_plugin_active() ) {
foreach ( $this->options[ $this->current_blog_id ] as $option ) {
if ( ! $option instanceof Abstract_Option || empty( $option->get_schema() ) ) {
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();
}
/**
* Removes an option sub value from its array.
*
* @since 3.8
*
* @param string $key The name of the option to splice.
* @param mixed $value The value to remove.
* @return WP_Error An error object, empty if the value was removed successfully.
*/
public function remove( string $key, $value ): WP_Error {
if ( ! $this->has( $key ) ) {
return new WP_Error(
'pll_unknown_option_key',
/* translators: %s is the name of an option. */
sprintf( __( 'Unknown option key %s.', 'polylang' ), "'$key'" )
);
}
$option = $this->options[ $this->current_blog_id ][ $key ];
if ( ! $option instanceof Abstract_List && ! $option instanceof Abstract_Map ) {
return new WP_Error(
'pll_invalid_option_type',
/* translators: %s is the name of an option. */
sprintf( __( 'Option %s is not a list or map.', 'polylang' ), "'$key'" )
);
}
if ( $option->remove( $value ) ) {
$this->modified[ $this->current_blog_id ] = true;
return new WP_Error();
}
return new WP_Error(
'pll_remove_failed',
/* translators: %1$s is the value to remove. %2$s is the name of an option. */
sprintf( __( 'Failed to remove %1$s from %2$s.', 'polylang' ), print_r( $value, true ), "'$key'" ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
);
}
/**
* Adds a value to an option.
*
* @since 3.8
*
* @param string $key The name of the option to add the value to.
* @param mixed $value The value to add.
* @return WP_Error An error object, empty if the value was added successfully.
*/
public function add( string $key, $value ): WP_Error {
if ( ! $this->has( $key ) ) {
return new WP_Error(
'pll_unknown_option_key',
/* translators: %s is the name of an option. */
sprintf( __( 'Unknown option key %s.', 'polylang' ), "'$key'" )
);
}
$option = $this->options[ $this->current_blog_id ][ $key ];
if ( ! $option instanceof Abstract_List && ! $option instanceof Abstract_Map ) {
return new WP_Error(
'pll_invalid_option_type',
/* translators: %s is the name of an option. */
sprintf( __( 'Option %s is not a list or map.', 'polylang' ), "'$key'" )
);
}
if ( $option->add( $value, $this ) ) {
$this->modified[ $this->current_blog_id ] = true;
return new WP_Error();
}
return new WP_Error(
'pll_add_failed',
/* translators: %1$s is the value to add. %2$s is the name of an option. */
sprintf( __( 'Failed to add %1$s to %2$s.', 'polylang' ), print_r( $value, true ), "'$key'" ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
);
}
/**
* 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() );
}
/**
* Retrieves site health information based on the current blog's options.
*
* @since 3.8
*
* @return array The site health information array.
*/
public function get_site_health_info(): array {
$infos = array();
foreach ( $this->options[ $this->current_blog_id ] as $option ) {
if ( ! $option instanceof Abstract_Option ) {
continue;
}
$info = $option->get_site_health_info( $this );
if ( ! empty( $info ) ) {
$infos[ $option::key() ] = $info;
}
}
return $infos;
}
/**
* 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 {
if ( ! $this->is_plugin_active() ) {
// Don't try to get the options from the DB.
$this->options[ $this->current_blog_id ] = array();
} else {
$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
* @since 3.8 New parameter `$is_plugin_active`.
*
* @param Options $options Instance of the options.
* @param int $current_blog_id Current blog ID.
* @param bool $is_plugin_active True if Polylang is active on the current site, false otherwise.
* This can be false after calling `switch_to_blog()`.
*/
do_action( 'pll_init_options_for_blog', $this, $this->current_blog_id, $this->is_plugin_active() );
}
/**
* Tells if Polylang is active on the current blog.
*
* @since 3.8
*
* @return bool
*/
private function is_plugin_active(): bool {
if ( isset( $this->is_plugin_active[ $this->current_blog_id ] ) ) {
return $this->is_plugin_active[ $this->current_blog_id ];
}
$this->is_plugin_active[ $this->current_blog_id ] = pll_is_plugin_active( POLYLANG_BASENAME ) || doing_action( 'activate_' . POLYLANG_BASENAME );
return $this->is_plugin_active[ $this->current_blog_id ];
}
/**
* Decorates options if we are on a site where Polylang is not active.
*
* @since 3.8
*
* @param Abstract_Option $option The option to decorate.
* @return Abstract_Option
*/
private function maybe_make_option_inactive( Abstract_Option $option ): Abstract_Option {
if ( $this->is_plugin_active() || $option instanceof Inactive_Option ) {
return $option;
}
return new Inactive_Option( $option );
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Primitive;
use WP_Syntex\Polylang\Options\Abstract_Option;
defined( 'ABSPATH' ) || exit;
/**
* Class defining single boolean option.
* Note that for historic reason, boolean are stored as 0 or 1.
*
* @since 3.7
*/
abstract class Abstract_Boolean extends Abstract_Option {
/**
* Returns the default value.
*
* @since 3.7
*
* @return bool
*/
protected function get_default() {
return false;
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'boolean'}
*/
protected function get_data_structure(): array {
return array(
'type' => 'boolean',
);
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Primitive;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Options\Abstract_Option;
defined( 'ABSPATH' ) || exit;
/**
* Class defining single list option, default value type to mixed.
*
* @since 3.7
*
* @phpstan-import-type SchemaType from Abstract_Option
*/
abstract class Abstract_List extends Abstract_Option {
/**
* Option value.
*
* @var array
*/
protected $value;
/**
* Prepares a value before validation.
* Allows to receive a string-keyed array but returns an integer-keyed array.
*
* @since 3.7
*
* @param mixed $value Value to format.
* @return mixed
*/
protected function prepare( $value ) {
if ( is_array( $value ) ) {
return array_values( array_unique( $value ) );
}
return $value;
}
/**
* Returns the JSON schema value type for the list items.
* Possible values are `'string'`, `'null'`, `'number'` (float), `'integer'`, `'boolean'`,
* `'array'` (array with integer keys), and `'object'` (array with string keys).
*
* @since 3.7
* @see https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types
*
* @return string
*
* @phpstan-return SchemaType
*/
protected function get_type(): string {
return 'string';
}
/**
* Returns the default value.
*
* @since 3.7
*
* @return array
*/
protected function get_default() {
return array();
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'array', items: array{type: SchemaType}}
*/
protected function get_data_structure(): array {
return array(
'type' => 'array',
'items' => array(
'type' => $this->get_type(),
),
);
}
/**
* Removes an item from the list.
*
* @since 3.8
*
* @param mixed $item The item to remove.
* @return bool True if the value has been removed. False otherwise.
*/
public function remove( $item ): bool {
if ( ! in_array( $item, $this->value, true ) ) {
return false;
}
$this->value = array_diff(
$this->value,
array( $item )
);
return true;
}
/**
* Adds an item to the list.
*
* @since 3.8
*
* @param mixed $item The item to add.
* @param Options $options The options instance.
* @return bool True if the value was added successfully. False otherwise.
*/
public function add( $item, Options $options ): bool {
/** @var array $updated_value */
$updated_value = $this->get();
$updated_value[] = $item;
return $this->set(
$updated_value,
$options
);
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Primitive;
use WP_Syntex\Polylang\Options\Options;
use WP_Syntex\Polylang\Options\Abstract_Option;
defined( 'ABSPATH' ) || exit;
/**
* Class defining a map option.
*
* @since 3.8
*/
abstract class Abstract_Map extends Abstract_Option {
/**
* Option value.
*
* @var array
*/
protected $value;
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.8
*
* @return array Partial schema.
*/
protected function get_data_structure(): array {
return array_merge(
$this->get_inner_structure(),
array(
'type' => 'object', // Correspond to associative array in PHP, @see{https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types}.
)
);
}
/**
* Removes a key from the map.
*
* @since 3.8
*
* @param string $key The key to remove.
* @return bool True if the key has been removed. False otherwise.
*/
public function remove( string $key ): bool {
if ( ! array_key_exists( $key, $this->value ) ) {
return false;
}
$this->value[ $key ] = $this->reset_value( $key );
return true;
}
/**
* Adds an item to the map.
*
* @since 3.8
*
* @param array<string, mixed> $item The item(s) to add. Must be a key-value pair.
* @param Options $options The options instance.
* @return bool True if the value was added successfully. False otherwise.
*/
public function add( $item, Options $options ): bool {
if ( ! is_array( $item ) ) {
return false;
}
/** @var array<string, mixed> $old_value */
$old_value = $this->get();
$updated_value = array_merge(
$old_value,
$item
);
return $this->set(
$updated_value,
$options
);
}
/**
* Returns the JSON schema part specific to the inner structure of this option.
*
* @since 3.8
*
* @return array Partial schema.
*/
abstract protected function get_inner_structure(): array;
/**
* Returns the reset value for a key.
*
* @since 3.8
*
* @param string $key The key to reset.
* @return mixed The reset value.
*/
abstract protected function reset_value( string $key );
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options\Primitive;
use WP_Syntex\Polylang\Options\Abstract_Option;
use WP_Syntex\Polylang\Options\Options;
defined( 'ABSPATH' ) || exit;
/**
* Class defining single string option.
*
* @since 3.7
*/
abstract class Abstract_String extends Abstract_Option {
/**
* Returns the default value.
*
* @since 3.7
*
* @return string
*/
protected function get_default() {
return '';
}
/**
* Returns the JSON schema part specific to this option.
*
* @since 3.7
*
* @return array Partial schema.
*
* @phpstan-return array{type: 'string'}
*/
protected function get_data_structure(): array {
return array(
'type' => 'string',
);
}
/**
* Adds information to the site health info array.
*
* @since 3.8
*
* @param Options $options An instance of the Options class providing additional configuration.
*
* @return array The updated site health information.
*/
public function get_site_health_info( Options $options ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return $this->format_single_value_for_site_health_info( $this->get() );
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @package Polylang
*/
namespace WP_Syntex\Polylang\Options;
defined( 'ABSPATH' ) || exit;
/**
* Polylang's options registry.
*
* @since 3.7
*/
class Registry {
protected const OPTIONS = array(
// URL modifications.
Business\Force_Lang::class,
Business\Domains::class,
Business\Hide_Default::class,
Business\Rewrite::class,
Business\Redirect_Lang::class,
// Detect browser language.
Business\Browser::class,
// Media.
Business\Media_Support::class,
// Custom post types and taxonomies.
Business\Post_Types::class,
Business\Taxonomies::class,
// Synchronization.
Business\Sync::class,
// Internal.
Business\Default_Lang::class,
Business\Nav_Menus::class,
// Read only.
Business\First_Activation::class,
Business\Previous_Version::class,
Business\Version::class,
);
/**
* Registers Polylang's options.
*
* @since 3.7
*
* @param Options $options Instance of the options.
* @return void
*/
public static function register( Options $options ): void {
array_map( array( $options, 'register' ), static::OPTIONS );
}
}