$settings */ public function update( array $settings ): bool { // Merge with CURRENT stored values (not defaults) — same merge-safe // pattern as the other repositories. $merged = wp_parse_args( $settings, $this->get() ); return update_option( self::OPTION_NAME, array( 'excluded_post_types' => self::sanitize_keys( $merged['excluded_post_types'] ), 'excluded_taxonomies' => self::sanitize_keys( $merged['excluded_taxonomies'] ), 'include_users' => (bool) $merged['include_users'], // 0 = WP core's own default (2000 per page). 'max_urls' => min( self::MAX_URLS_CEILING, max( 0, (int) $merged['max_urls'] ) ), 'custom_urls' => self::sanitize_custom_urls( $merged['custom_urls'] ), 'excluded_urls' => self::sanitize_excluded_urls( $merged['excluded_urls'] ), ) ); } /** * Exclusion rules are stored as normalized PATHS (always starting with * "/", optional trailing "*" wildcard kept) — a full same-host URL is * reduced to its path, a foreign-host URL is rejected, since the sitemap * only ever contains this site's own URLs. * * @param mixed $values * * @return string[] */ private static function sanitize_excluded_urls( $values ): array { $site_host = strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) ); $clean = array(); foreach ( (array) $values as $value ) { $rule = trim( (string) $value ); if ( '' === $rule ) { continue; } $wildcard = str_ends_with( $rule, '*' ); if ( $wildcard ) { $rule = rtrim( substr( $rule, 0, -1 ) ); } if ( preg_match( '#^https?://#i', $rule ) ) { if ( strtolower( (string) wp_parse_url( $rule, PHP_URL_HOST ) ) !== $site_host ) { continue; } $rule = (string) wp_parse_url( $rule, PHP_URL_PATH ); } if ( '' === $rule || '/' !== $rule[0] ) { $rule = '/' . ltrim( $rule, '/' ); } // NOT sanitize_text_field(): it strips anything shaped like a // %[hex][hex] octet, which reduces a percent-encoded non-Latin // slug ("/%d0%ba%d0%b8-x/") to "/--x/" and silently breaks the // rule (the M49 trap). TemplateSanitizer keeps the octets. $rule = \AcezSeo\Support\TemplateSanitizer::sanitize_line( $rule ); if ( '/' === $rule && ! $wildcard ) { // Excluding the bare homepage is legitimate; excluding "/*" // would empty the whole sitemap — allowed anyway (explicit // user intent), so no special case needed here. $clean[] = $rule; continue; } $clean[] = $rule . ( $wildcard ? '*' : '' ); } return array_values( array_unique( $clean ) ); } /** * @param mixed $values * * @return string[] */ private static function sanitize_keys( $values ): array { $clean = array(); foreach ( (array) $values as $value ) { $key = sanitize_key( (string) $value ); if ( '' !== $key ) { $clean[] = $key; } } return array_values( array_unique( $clean ) ); } /** * Google's sitemap spec requires every listed URL to live on the same * host as the sitemap itself — foreign-host entries are silently ignored * by crawlers anyway, so they are rejected here instead of stored. * * @param mixed $values * * @return string[] */ private static function sanitize_custom_urls( $values ): array { $site_host = (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ); $clean = array(); foreach ( (array) $values as $value ) { $url = esc_url_raw( trim( (string) $value ) ); if ( '' === $url ) { continue; } if ( ! in_array( wp_parse_url( $url, PHP_URL_SCHEME ), array( 'http', 'https' ), true ) ) { continue; } if ( strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ) !== strtolower( $site_host ) ) { continue; } $clean[] = $url; } return array_values( array_unique( $clean ) ); } /** * @return array{excluded_post_types: string[], excluded_taxonomies: string[], include_users: bool, max_urls: int, custom_urls: string[], excluded_urls: string[]} */ private static function defaults(): array { return array( 'excluded_post_types' => array(), 'excluded_taxonomies' => array(), 'include_users' => true, 'max_urls' => 0, 'custom_urls' => array(), 'excluded_urls' => array(), ); } }