matcher = $matcher; } /** * Registers the hook. * * @return void */ public function register(): void { add_action( 'init', array( $this, 'maybe_block' ), 1 ); } /** * Blocks the current request when it comes from a blocked crawler. * * @return void */ public function maybe_block(): void { if ( ! $this->should_run() ) { return; } if ( $this->is_declaration_request() ) { return; } $token = $this->matcher->match_agent( $this->get_user_agent() ); if ( null === $token ) { return; } $this->send_block_response( $token ); } /** * Whether the request is for one of the declaration endpoints. * * @return bool True when the request must not be blocked. */ private function is_declaration_request(): bool { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $path = (string) wp_parse_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH ); if ( '' === $path ) { return false; } $robots = (string) wp_parse_url( home_url( '/robots.txt' ), PHP_URL_PATH ); $well_known = (string) wp_parse_url( home_url( '/.well-known/' ), PHP_URL_PATH ); if ( '' !== $robots && untrailingslashit( $path ) === untrailingslashit( $robots ) ) { return true; } return '' !== $well_known && 0 === strpos( $path, $well_known ); } /** * Whether this layer should evaluate the current request. * * @return bool True when the request is a candidate for blocking. */ private function should_run(): bool { if ( aiblackout_is_disabled() ) { return false; } // Never interfere with the admin, cron, CLI or a logged-in user. if ( is_admin() || wp_doing_cron() || ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) ) { return false; } if ( defined( 'WP_CLI' ) && WP_CLI ) { return false; } if ( is_user_logged_in() ) { return false; } return true; } /** * Returns the request's User-Agent header. * * @return string User agent, or an empty string. */ private function get_user_agent(): string { if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) { return ''; } return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); } /** * Sends the 403 response and stops. * * @param string $token The crawler token that matched. * @return void */ private function send_block_response( string $token ): void { if ( ! headers_sent() ) { status_header( 403 ); header( 'X-Robots-Tag: noai, noimageai', true ); header( 'Content-Type: text/plain; charset=utf-8' ); nocache_headers(); } /** * Fires when the PHP layer refuses a request from an AI crawler. * * @since 1.0.0 * * @param string $token The crawler token that matched. */ do_action( 'aiblackout_crawler_blocked', $token ); echo esc_html__( 'This site does not permit AI crawling. Your content is not training data.', 'ai-blackout' ); exit; } }