robotstxt-mediaaudit/includes/External/HostnameFilter.php
2026-06-03 06:29:56 +00:00

186 lines
4.4 KiB
PHP

<?php
/**
* Utility for classifying hostnames against the alert/ignored filter lists.
*
* @package MediaRightsAudit\External
*/
namespace MediaRightsAudit\External;
/**
* Classifies hostnames against the site's configured alert (include) and
* ignored (exclude) lists stored in the robotstxt_mediaaudit_settings option.
*
* All methods are static; the loaded lists are cached in a class property so
* that the option is read at most once per request.
*/
class HostnameFilter {
/**
* Cached filter lists, populated lazily on first use.
*
* @var array{include: list<string>, exclude: list<string>}|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<string>, exclude: list<string>}
*/
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<string, int> $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;
}
}