$country, 'city' => $city, ); } /** * Whether the database says the address belongs to a network that hides * where its user is: an anonymous proxy, VPN, Tor exit, public or * residential proxy, hosting provider, or satellite link. Its country is * the exit's, not the person's -- so it can be stored as the address's * country, but it is never one end of a journey (Context\Travel). * * GeoLite2 and GeoIP2 Country/City carry the older `is_anonymous_proxy` * and `is_satellite_provider` traits; MaxMind's Anonymous IP and * Enterprise databases carry the rest. A database with none of them * answers false for everything, which is the plugin's behaviour before * this existed. * * @param string $ip A single IPv4 or IPv6 address, before truncation. */ public static function is_anonymous( string $ip ): bool { $record = self::record( $ip ); $traits = is_array( $record ) && isset( $record['traits'] ) && is_array( $record['traits'] ) ? $record['traits'] : array(); foreach ( self::ANONYMISING_TRAITS as $trait ) { if ( true === ( $traits[ $trait ] ?? null ) ) { return true; } } return false; } /** * The database record for an address, or null. * * @param string $ip A single IPv4 or IPv6 address. * @return array|null */ private static function record( string $ip ): ?array { if ( false === filter_var( $ip, FILTER_VALIDATE_IP ) ) { return null; } $reader = self::reader(); if ( null === $reader ) { return null; } try { $record = $reader->get( $ip ); } catch ( \Throwable $e ) { // An IPv6 address against an IPv4-only database, a corrupt tree, // or a decode a resource limit refused: no location for this // one, not a failed event. return null; } return is_array( $record ) ? $record : null; } /** * Forget the open reader, so the next lookup opens the database the * option now names. Called whenever that option changes. */ public static function reset(): void { if ( self::$reader instanceof Reader ) { try { self::$reader->close(); } catch ( \Throwable $e ) { // Already closed or never fully opened; nothing to do. unset( $e ); } } self::$reader = null; self::$reader_path = ''; } /** * The reader for the configured database, opened once per request. */ private static function reader(): ?Reader { $path = GeoDatabase::path(); if ( '' === $path ) { return null; } if ( $path !== self::$reader_path ) { self::reset(); self::$reader_path = $path; try { self::$reader = is_readable( $path ) ? new Reader( $path ) : false; } catch ( \Throwable $e ) { self::$reader = false; } } return self::$reader instanceof Reader ? self::$reader : null; } /** * The ISO 3166-1 alpha-2 country code: where the address is, or failing * that where its network is registered (MaxMind's own fallback order). * * @param array $record A database record. */ private static function country( array $record ): ?string { foreach ( array( 'country', 'registered_country' ) as $key ) { $code = $record[ $key ]['iso_code'] ?? null; if ( is_string( $code ) && 1 === preg_match( '/^[A-Z]{2}$/', $code ) ) { return $code; } } return null; } /** * The English city name, plain text, bounded -- the database is a file * someone uploaded, so its strings are treated as untrusted. * * @param array $record A database record. */ private static function city( array $record ): ?string { $name = $record['city']['names']['en'] ?? null; if ( ! is_string( $name ) ) { return null; } $name = trim( wp_strip_all_tags( $name ) ); if ( '' === $name ) { return null; } return function_exists( 'mb_substr' ) ? mb_substr( $name, 0, self::CITY_LENGTH ) : substr( $name, 0, self::CITY_LENGTH ); } }