array( 'label' => string, 'resolve' => callable ). */ public static function get_registry() { $registry = array( 'SiteTitle' => array( 'label' => __( 'Site title', 'acknowledgify' ), 'resolve' => static function () { return (string) get_bloginfo( 'name', 'display' ); }, ), 'SiteTagline' => array( 'label' => __( 'Site tagline', 'acknowledgify' ), 'resolve' => static function () { return (string) get_bloginfo( 'description', 'display' ); }, ), 'SiteURL' => array( 'label' => __( 'Site address', 'acknowledgify' ), 'resolve' => static function () { return (string) home_url( '/' ); }, ), 'SiteHost' => array( 'label' => __( 'Site domain', 'acknowledgify' ), 'resolve' => static function () { $host = wp_parse_url( home_url(), PHP_URL_HOST ); return is_string( $host ) ? $host : ''; }, ), 'CurrentYear' => array( 'label' => __( 'Current year', 'acknowledgify' ), 'resolve' => static function () { return (string) wp_date( 'Y' ); }, ), 'CurrentDate' => array( 'label' => __( 'Current date', 'acknowledgify' ), 'resolve' => static function () { return (string) wp_date( (string) get_option( 'date_format', 'Y-m-d' ) ); }, ), 'ThemeName' => array( 'label' => __( 'Active theme', 'acknowledgify' ), 'resolve' => static function () { return (string) wp_get_theme()->get( 'Name' ); }, ), 'WPVersion' => array( 'label' => __( 'WordPress version', 'acknowledgify' ), 'resolve' => static function () { return (string) get_bloginfo( 'version' ); }, ), ); /** * Filters the placeholder token registry. * * Add a token by adding a key whose value is an array with a 'label' * string and a 'resolve' callable returning a string. The callable * runs on the front end for every request that outputs a credit, so * keep it cheap and avoid remote calls. * * @param array $registry Token name => array( 'label', 'resolve' ). */ return (array) apply_filters( 'acknowledgify_tokens', $registry ); } /** * Every token resolved to its value for this site. * * @return array Lower-cased token name => resolved string. */ public static function get_values() { if ( null !== self::$cache ) { return self::$cache; } $values = array(); foreach ( self::get_registry() as $token => $definition ) { if ( ! isset( $definition['resolve'] ) || ! is_callable( $definition['resolve'] ) ) { continue; } $values[ strtolower( $token ) ] = (string) call_user_func( $definition['resolve'] ); } self::$cache = $values; return self::$cache; } /** * Token name => resolved value, using the registry's own casing. * * Used by the settings screen to show a reference table and to resolve * tokens in the live preview without a round trip. * * @return array */ public static function get_reference() { $reference = array(); foreach ( self::get_registry() as $token => $definition ) { $reference[ $token ] = array( 'label' => isset( $definition['label'] ) ? (string) $definition['label'] : $token, 'value' => ( isset( $definition['resolve'] ) && is_callable( $definition['resolve'] ) ) ? (string) call_user_func( $definition['resolve'] ) : '', ); } return $reference; } /** * Replace every known token in a string with its value. * * Called before output escaping, never after. Each of the three outputs * escapes differently (plain text, attribute, filtered HTML) and a * resolved value has to pass through the escaping that suits its * destination, exactly like text the user typed. * * Unknown tokens are left alone rather than stripped, so a typo shows up * as %%SiteTitel%% in the preview instead of silently vanishing. * * @param string $text Text possibly containing tokens. * @return string */ public static function resolve( $text ) { $text = (string) $text; if ( ! str_contains( $text, '%%' ) ) { return $text; } $values = self::get_values(); return (string) preg_replace_callback( self::PATTERN, static function ( $matches ) use ( $values ) { $key = strtolower( $matches[1] ); return array_key_exists( $key, $values ) ? $values[ $key ] : $matches[0]; }, $text ); } /** * Sanitize a stored URL without resolving portable placeholder tokens. * * Mask tokens with URL-safe markers before WordPress normalization. A * leading token may represent a complete URL; give its marker a scheme * so esc_url_raw() cannot prepend http:// to the stored token. Final HTML * output must still use esc_url() AFTER resolving tokens. * * @param mixed $value Submitted URL or token expression. * @return string Sanitized, unresolved URL. */ public static function sanitize_url( $value ) { $value = is_scalar( $value ) ? trim( (string) $value ) : ''; if ( ! preg_match( self::PATTERN, $value ) ) { return esc_url_raw( $value ); } $map = array(); $prefix = 'acktoken' . hash( 'sha256', $value ); $masked = preg_replace_callback( self::PATTERN, static function ( $matches ) use ( &$map, $prefix, $value ) { $marker = $prefix . count( $map ); if ( empty( $map ) && str_starts_with( $value, $matches[0] ) ) { $marker = 'https://' . $marker . '.invalid'; } $map[ $marker ] = $matches[0]; return $marker; }, $value ); return strtr( esc_url_raw( $masked ), $map ); } /** * Drop the resolved-value cache. * * @return void */ public static function flush_cache() { self::$cache = null; } }