settings = $settings; $this->repository = $repository; } /** * Registers the hook. * * @since 1.0.0 * * @return void */ public function hooks(): void { add_action( 'template_redirect', array( $this, 'maybe_log' ), 9999 ); } /** * Logs the current request when it is a 404 worth keeping. * * @since 1.0.0 * * @return void */ public function maybe_log(): void { if ( ! is_404() ) { return; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reduced to a path below, never output as HTML. $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only tested for bot names, never stored. $source = isset( $_SERVER['HTTP_REFERER'] ) ? (string) wp_unslash( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reduced to host and path below. $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; if ( ! in_array( $method, array( 'GET', 'HEAD' ), true ) || ! $this->should_log( $uri, $agent ) ) { return; } $path = $this->path_of( $uri ); if ( '' === $path ) { return; } $this->repository->record( $path, $this->clean_referrer( $source ), (int) $this->settings->value( 'monitor404', 'max_rows' ) ); } /** * Whether a 404 for this request is recorded. * * @since 1.0.0 * * @param string $uri Request URI. * @param string $agent User agent, used only to recognise bots. * @return bool */ public function should_log( string $uri, string $agent ): bool { if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) { return false; } if ( $this->settings->value( 'monitor404', 'ignore_bots' ) && $this->looks_like_bot( $agent ) ) { return false; } $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); $extension = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ); $ignored = array_map( 'strtolower', array_map( 'strval', (array) $this->settings->value( 'monitor404', 'ignore_extensions' ) ) ); return '' === $extension || ! in_array( $extension, $ignored, true ); } /** * The stored form of a request address: the path on this site, without the query string. * * @since 1.0.0 * * @param string $uri Request URI. * @return string */ public function path_of( string $uri ): string { $normalizer = new Path_Normalizer( true, false, 'ignore' ); $relative = $normalizer->relative( $uri ); $pos = strpos( $relative, '?' ); $path = false === $pos ? $relative : substr( $relative, 0, $pos ); return sanitize_text_field( rawurldecode( $path ) ); } /** * The linking page without query string or fragment. * * @since 1.0.0 * * @param string $referrer Raw Referer header. * @return string */ public function clean_referrer( string $referrer ): string { $parts = wp_parse_url( $referrer ); if ( ! is_array( $parts ) || empty( $parts['host'] ) || ! isset( $parts['scheme'] ) || ! in_array( $parts['scheme'], array( 'http', 'https' ), true ) ) { return ''; } return esc_url_raw( $parts['scheme'] . '://' . $parts['host'] . ( isset( $parts['path'] ) ? $parts['path'] : '' ) ); } /** * Whether a user agent belongs to a crawler or an automated tool. * * @since 1.0.0 * * @param string $agent User agent. * @return bool */ private function looks_like_bot( string $agent ): bool { if ( '' === $agent ) { return true; } return 1 === preg_match( '/bot|crawl|spider|slurp|mediapartners|facebookexternalhit|bingpreview|headless|lighthouse|python-requests|curl\/|wget|monitor|uptime|scrapy|httpclient|java\//i', $agent ); } }