, exclude: list}|null */ private static ?array $lists = null; /** * Loads and returns the alert / ignored hostname lists from the plugin option. * * Result is cached in self::$lists for the duration of the request. * * @return array{include: list, exclude: list} */ private static function get_lists(): array { if ( null !== self::$lists ) { return self::$lists; } $raw = get_option( 'robotstxt_mediaaudit_settings' ); $opts = is_array( $raw ) ? $raw : array(); $raw_include = $opts['filter_include'] ?? array(); $raw_exclude = $opts['filter_exclude'] ?? array(); $include = array(); if ( is_array( $raw_include ) ) { foreach ( $raw_include as $item ) { if ( is_string( $item ) && '' !== $item ) { $include[] = $item; } } } $exclude = array(); if ( is_array( $raw_exclude ) ) { foreach ( $raw_exclude as $item ) { if ( is_string( $item ) && '' !== $item ) { $exclude[] = $item; } } } self::$lists = array( 'include' => $include, 'exclude' => $exclude, ); return self::$lists; } /** * Returns true when $domain matches a filter list entry. * * The $entry may carry a `*.` wildcard prefix; its base is derived by * stripping that prefix. A match occurs when: * - $domain equals the base exactly, OR * - $domain ends with a dot followed by the base (subdomain match). * * @param string $domain Lowercase hostname to test. * @param string $entry A list entry, possibly prefixed with `*.`. * * @return bool */ private static function matches( string $domain, string $entry ): bool { $base = str_starts_with( $entry, '*.' ) ? substr( $entry, 2 ) : $entry; if ( $domain === $base ) { return true; } if ( str_ends_with( $domain, '.' . $base ) ) { return true; } return false; } /** * Returns true when the given domain matches any entry in the alert list. * * @param string $domain Hostname to classify (will be lower-cased internally). * * @return bool */ public static function is_alert( string $domain ): bool { $domain = strtolower( $domain ); $lists = self::get_lists(); foreach ( $lists['include'] as $entry ) { if ( self::matches( $domain, $entry ) ) { return true; } } return false; } /** * Returns true when the given domain matches any entry in the ignored list. * * @param string $domain Hostname to classify (will be lower-cased internally). * * @return bool */ public static function is_ignored( string $domain ): bool { $domain = strtolower( $domain ); $lists = self::get_lists(); foreach ( $lists['exclude'] as $entry ) { if ( self::matches( $domain, $entry ) ) { return true; } } return false; } /** * Classifies a domain and returns 'alert', 'ignored', or 'other'. * * Alert takes precedence: a domain that is in both lists is classified as * 'alert'. * * @param string $domain Hostname to classify. * * @return string 'alert'|'ignored'|'other' */ public static function classify( string $domain ): string { if ( self::is_alert( $domain ) ) { return 'alert'; } if ( self::is_ignored( $domain ) ) { return 'ignored'; } return 'other'; } /** * Returns true when at least one key in $domains is classified as alert. * * @param array $domains Map of domain => occurrence count. * * @return bool */ public static function has_alert_domains( array $domains ): bool { foreach ( array_keys( $domains ) as $domain ) { if ( self::is_alert( $domain ) ) { return true; } } return false; } /** * Resets the internal cache so the option is re-read on the next call. * * Useful in tests or after the settings option has been updated. * * @return void */ public static function reset_cache(): void { self::$lists = null; } }