types = $types ? $types : new TypeRegistry(); } public static function is_reserved_key( $key ) { return 0 === strpos( sanitize_key( $key ), self::KEY_PREFIX ); } public static function date_formats() { return array( 'full' => __( 'Full', 'glovario' ), 'long' => __( 'Long', 'glovario' ), 'medium' => __( 'Medium', 'glovario' ), 'short' => __( 'Short', 'glovario' ), 'wordpress' => __( 'WordPress site format', 'glovario' ), ); } public static function sanitize_date_format( $format ) { $format = sanitize_key( $format ); return array_key_exists( $format, self::date_formats() ) ? $format : self::DEFAULT_DATE_FORMAT; } public function date_format_options( $context = array() ) { $options = array(); foreach ( self::date_formats() as $format => $label ) { $preview_context = is_array( $context ) ? $context : array(); $preview_context['date_format'] = $format; $options[] = array( 'value' => $format, 'label' => $label, 'example' => self::current_date( $preview_context ), ); } return $options; } /** * One built-in, computing only that one. Every shortcode, token, and block * passes here first, so a site variable returns at once. * * @param string $key Variable key. * @param array $context Resolution context. * @return array|null */ public function find_by_key( $key, $context = array() ) { $key = sanitize_key( $key ); if ( ! self::is_reserved_key( $key ) ) { return null; } foreach ( $this->definitions() as $definition_key => $definition ) { if ( sanitize_key( $definition_key ) === $key ) { return $this->normalize( $definition_key, $definition, is_array( $context ) ? $context : array() ); } } return null; } public function all( $context = array() ) { $context = is_array( $context ) ? $context : array(); $variables = array(); foreach ( $this->definitions() as $key => $definition ) { $variable = $this->normalize( $key, $definition, $context ); if ( $variable ) { $variables[] = $variable; } } return $variables; } /** * The built-in definitions, values not yet computed. * * @return array Definitions keyed by variable key. */ private function definitions() { $definitions = array( 'glovario_builtin_site_name' => array( 'label' => __( 'Site Name', 'glovario' ), 'type' => 'text', 'description' => __( 'The title configured in WordPress General Settings.', 'glovario' ), 'value' => static function () { return wp_specialchars_decode( get_option( 'blogname', '' ), ENT_QUOTES ); }, ), 'glovario_builtin_site_url' => array( 'label' => __( 'Site URL', 'glovario' ), 'type' => 'url', 'description' => __( 'The public home URL for this WordPress site.', 'glovario' ), 'value' => static function () { return home_url( '/' ); }, ), 'glovario_builtin_admin_email' => array( 'label' => __( 'Administration Email', 'glovario' ), 'type' => 'email', 'description' => __( 'The administration email configured in WordPress.', 'glovario' ), 'value' => static function () { return get_option( 'admin_email', '' ); }, ), 'glovario_builtin_current_year' => array( 'label' => __( 'Current Year', 'glovario' ), 'type' => 'number', 'description' => __( 'The current year in the site timezone.', 'glovario' ), 'value' => static function ( $key, $definition, $resolution_context ) { $timestamp = isset( $resolution_context['timestamp'] ) ? (int) $resolution_context['timestamp'] : time(); return wp_date( 'Y', $timestamp, wp_timezone() ); }, ), self::CURRENT_DATE_KEY => array( 'label' => __( 'Current Date', 'glovario' ), 'type' => 'text', 'description' => __( 'The current date in the request locale and site timezone. References default to Long and can select another supported format.', 'glovario' ), 'value' => static function ( $key, $definition, $resolution_context ) { return self::current_date( $resolution_context ); }, ), 'glovario_builtin_site_description' => array( 'label' => __( 'Site Tagline', 'glovario' ), 'type' => 'text', 'description' => __( 'The tagline configured in WordPress General Settings.', 'glovario' ), 'value' => static function () { return wp_specialchars_decode( get_option( 'blogdescription', '' ), ENT_QUOTES ); }, ), ); /** * Filters the available read-only built-in variables. * * Keys must use the reserved glovario_builtin_ prefix. Each definition requires * a label, registered variable type, and a scalar value or callable. * * @param array $definitions Built-in definitions keyed by variable key. */ $definitions = apply_filters( 'glovario_builtin_variables', $definitions ); return is_array( $definitions ) ? $definitions : array(); } private function normalize( $key, $definition, $context ) { $key = sanitize_key( $key ); if ( ! self::is_reserved_key( $key ) || ! is_array( $definition ) || empty( $definition['label'] ) || ! $this->types->exists( $definition['type'] ?? '' ) || ! array_key_exists( 'value', $definition ) ) { return null; } try { $value = is_callable( $definition['value'] ) ? call_user_func( $definition['value'], $key, $definition, $context ) : $definition['value']; } catch ( \Throwable $error ) { do_action( 'glovario_builtin_resolution_error', $error, $key, $definition ); return null; } if ( ! is_scalar( $value ) && null !== $value ) { return null; } $value = apply_filters( 'glovario_builtin_value', null === $value ? '' : (string) $value, $key, $definition ); return array( 'id' => 0, 'variable_key' => $key, 'label' => sanitize_text_field( $definition['label'] ), 'type' => sanitize_key( $definition['type'] ), 'value' => is_scalar( $value ) || null === $value ? (string) $value : '', 'description' => sanitize_textarea_field( $definition['description'] ?? '' ), 'category_id' => null, 'category_name' => __( 'Built-in', 'glovario' ), 'builtin' => true, 'created_at' => '', 'updated_at' => '', ); } private static function current_date( $context ) { $locale = isset( $context['locale'] ) ? sanitize_locale_name( $context['locale'] ) : determine_locale(); $locale = $locale ? $locale : 'en_US'; $timestamp = isset( $context['timestamp'] ) ? (int) $context['timestamp'] : time(); $timezone = wp_timezone(); $date_format = self::sanitize_date_format( $context['date_format'] ?? self::DEFAULT_DATE_FORMAT ); /** * Filters the PHP date format used by the current-date built-in. * * Return a non-empty format to override automatic locale formatting. * * @param string $format Empty by default. * @param string $locale Resolution locale. * @param array $context Resolution context. * @param string $date_format Selected locale date format. */ $format = apply_filters( 'glovario_builtin_current_date_format', '', $locale, $context, $date_format ); if ( is_string( $format ) && '' !== $format ) { return wp_date( $format, $timestamp, $timezone ); } if ( 'wordpress' === $date_format ) { return wp_date( get_option( 'date_format' ), $timestamp, $timezone ); } if ( class_exists( '\\IntlDateFormatter' ) ) { try { $intl_formats = array( 'full' => \IntlDateFormatter::FULL, 'long' => \IntlDateFormatter::LONG, 'medium' => \IntlDateFormatter::MEDIUM, 'short' => \IntlDateFormatter::SHORT, ); $formatter = new \IntlDateFormatter( str_replace( '_', '-', $locale ), $intl_formats[ $date_format ], \IntlDateFormatter::NONE, $timezone, \IntlDateFormatter::GREGORIAN ); $formatted = $formatter->format( $timestamp ); if ( false !== $formatted ) { return $formatted; } } catch ( \Throwable $error ) { do_action( 'glovario_builtin_date_format_error', $error, $locale, $context ); } } return wp_date( self::fallback_date_format( $date_format, $locale ), $timestamp, $timezone ); } private static function fallback_date_format( $date_format, $locale ) { // The language's usual date order, used when the Intl extension is unavailable. /* translators: Long date format for PHP's date(): https://www.php.net/manual/datetime.format.php. Used only when the server lacks the Intl extension. Keep your language's usual order, for example "j F Y". */ $long = _x( 'F j, Y', 'long date format', 'glovario' ); if ( 'long' === $date_format ) { return $long; } if ( 'medium' === $date_format ) { return str_replace( 'F', 'M', $long ); } if ( 'full' === $date_format ) { return 'l, ' . $long; } $locale_parts = preg_split( '/[_-]/', strtoupper( $locale ) ); $region = $locale_parts[1] ?? ''; if ( in_array( $region, array( 'US', 'PH' ), true ) ) { return 'n/j/y'; } if ( 'CA' === $region ) { return 'Y-m-d'; } if ( in_array( $region, array( 'CN', 'JP', 'KR', 'TW' ), true ) ) { return 'Y/n/j'; } return 'j/n/y'; } }