, * on_trash: string, * log_404: bool, * log_retention: int, * log_ignore_files: bool, * log_ignore_bots: bool, * log_ignore: string, * pass_query: bool, * case_sensitive: bool, * cache_hours: int, * fallback_404: string, * editors: bool, * delete_data: bool * } */ public static function defaults(): array { return [ 'auto_slug' => true, 'auto_terms' => true, 'auto_post_types' => [], 'on_trash' => 'none', 'log_404' => true, 'log_retention' => 30, 'log_ignore_files' => true, 'log_ignore_bots' => false, 'log_ignore' => self::DEFAULT_IGNORE, 'pass_query' => true, 'case_sensitive' => false, 'cache_hours' => 1, 'fallback_404' => 'none', 'editors' => false, 'delete_data' => false, ]; } /** * Read once per request. Settings are consulted on every front-end page view, and * get_option() is already cached, but merging the defaults is not free either. * * @var array|null */ private static ?array $cache = null; /** @return array */ public static function all(): array { if ( null === self::$cache ) { $saved = get_option( self::OPTION, [] ); self::$cache = array_merge( self::defaults(), is_array( $saved ) ? $saved : [] ); } return self::$cache; } public static function get( string $key ): mixed { return self::all()[ $key ] ?? null; } /** * Save a partial set. Anything not named keeps its current value, and anything not * known is dropped rather than stored, so the option never collects junk. * * @param array $values * @return array The settings as saved. */ public static function update( array $values ): array { $current = self::all(); $defaults = self::defaults(); foreach ( $values as $key => $value ) { if ( ! array_key_exists( $key, $defaults ) ) { continue; } $current[ $key ] = self::sanitize( $key, $value ); } update_option( self::OPTION, $current, true ); self::$cache = null; return self::all(); } public static function flush(): void { self::$cache = null; } private static function sanitize( string $key, mixed $value ): mixed { switch ( $key ) { case 'auto_post_types': return array_values( array_filter( array_map( 'sanitize_key', (array) $value ) ) ); case 'on_trash': return in_array( $value, [ 'none', 'gone', 'parent' ], true ) ? $value : 'none'; case 'fallback_404': return in_array( $value, [ 'none', 'guess', 'home' ], true ) ? $value : 'none'; case 'log_retention': // A day at least, or the log is emptied before anybody could read it; a // year at most, because past that the addresses are history, not problems. return max( 1, min( 365, (int) $value ) ); case 'cache_hours': return max( 0, min( 8760, (int) $value ) ); case 'log_ignore': $lines = preg_split( '/\R/', (string) $value ) ?: []; $lines = array_filter( array_map( static fn( $l ) => sanitize_text_field( $l ), $lines ) ); return implode( "\n", array_slice( $lines, 0, 200 ) ); default: return (bool) $value; } } }