10, 'redirection' => 2, 'user-agent' => 'AIM-Transparency delivery check (self)', ) ); if ( is_wp_error( $resp ) ) { return self::store( self::record( 'unreachable', $url, $size, $resp->get_error_message() ) ); } $code = (int) wp_remote_retrieve_response_code( $resp ); if ( 200 !== $code ) { /* translators: %d: HTTP status code. */ return self::store( self::record( 'unreachable', $url, $size, sprintf( __( 'The image returned HTTP %d.', 'aim-transparency' ), $code ) ) ); } $body = (string) wp_remote_retrieve_body( $resp ); if ( '' === $body ) { return self::store( self::record( 'unreachable', $url, $size, __( 'The image came back empty.', 'aim-transparency' ) ) ); } if ( false !== strpos( $body, self::NEEDLE ) ) { return self::store( self::record( 'verified', $url, $size, '' ) ); } return self::store( self::record( 'stripped', $url, $size, self::culprit( $resp ) ) ); } /* --- internals ----------------------------------------------------------- */ /** * Picks a marked image whose mark we believe was written. * * @since 2.2.0 * * @return int Attachment ID, or 0 when the library holds nothing to check. */ protected static function candidate() { $ids = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', /* * Every image, not a list of formats. What matters is whether a mark was * successfully written, and the embed status below already says so: a format the * writer cannot handle never records "embedded". Naming formats here would mean * revisiting this line every time the writer learns a new one, which is exactly * how WebP came to be excluded the day after it started working. */ 'post_mime_type' => 'image', 'posts_per_page' => 5, 'fields' => 'ids', 'no_found_rows' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one admin-triggered check. 'meta_key' => AICL_Plugin::META_EMBED, ) ); foreach ( (array) $ids as $id ) { $embed = (string) get_post_meta( $id, AICL_Plugin::META_EMBED, true ); if ( 0 === strpos( $embed, 'embedded' ) && false === strpos( $embed, 'embedded 0' ) ) { return (int) $id; } } return 0; } /** * Returns the URL a visitor is actually served, preferring a generated size over the original. * * @since 2.2.0 * * @param int $id Attachment ID. * @return array{0:string,1:string} The URL and the size name used. */ protected static function served_url( $id ) { $meta = wp_get_attachment_metadata( $id ); $sizes = ( isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) ? $meta['sizes'] : array(); foreach ( array( 'large', 'medium_large', 'medium' ) as $size ) { if ( isset( $sizes[ $size ] ) ) { $url = wp_get_attachment_image_url( $id, $size ); if ( $url ) { return array( (string) $url, $size ); } } } return array( (string) wp_get_attachment_url( $id ), 'full' ); } /** * Names a likely cause from the response headers, when one is recognisable. * * Recognising nothing is an ordinary outcome, not a failure: the finding rests on the missing * bytes, and an unknown re-encoder is still a re-encoder. * * @since 2.2.0 * * @param array|WP_Error $resp The HTTP response. * @return string A human-readable cause, or an empty string. */ protected static function culprit( $resp ) { $headers = wp_remote_retrieve_headers( $resp ); $flat = array(); foreach ( (array) $headers as $key => $value ) { $flat[ strtolower( (string) $key ) ] = strtolower( is_array( $value ) ? implode( ' ', $value ) : (string) $value ); } if ( isset( $flat['cf-polished'] ) ) { return __( 'Cloudflare Polish is re-encoding this image.', 'aim-transparency' ); } if ( isset( $flat['x-hosted-by'] ) && false !== strpos( $flat['x-hosted-by'], 'photon' ) ) { return __( 'Jetpack Photon is serving this image.', 'aim-transparency' ); } if ( isset( $flat['server'] ) && false !== strpos( $flat['server'], 'bunnycdn' ) ) { return __( 'BunnyCDN is serving this image.', 'aim-transparency' ); } return ''; } /** * Shapes one delivery record. * * @since 2.2.0 * * @param string $state One of verified, stripped, unreachable, not_applicable, never. * @param string $url The URL checked. * @param string $size The size name checked. * @param string $detail A human-readable note. * @return array The record. */ protected static function record( $state, $url, $size, $detail ) { return array( 'state' => (string) $state, 'url' => (string) $url, 'size' => (string) $size, 'detail' => (string) $detail, 'checked_at' => time(), ); } /** * Persists a record and returns it. * * @since 2.2.0 * * @param array $record The record to store. * @return array The same record. */ protected static function store( $record ) { update_option( self::OPTION, $record, false ); return $record; } }