*/ private static array $pages = array(); /** * The one instance that answers the public output hooks (Hooks::register()), made when WordPress first calls one. * * @return self The instance. */ public static function shared(): self { self::$shared ??= new self(); return self::$shared; } /** * Add the inline style .afsd-im-picture{display:contents}, so a picture element leaves the img's own layout (flex and * grid sizing) as it was, where delivery is allowed. */ public function styles(): void { if ( ! self::allowed() ) { return; } // Keep native flex/grid image sizing, including Nancy's 4:3 homepage card media. // Inline through the native queue: no extra request, and cached HTML retains this rule. wp_register_style( 'afsd-im-frontend', false, array(), VERSION ); wp_enqueue_style( 'afsd-im-frontend' ); wp_add_inline_style( 'afsd-im-frontend', '.afsd-im-picture{display:contents}' ); } /** * The public pages Image Manager may change: optimized delivery ('delivery'), measured sizes ('fit'), the CDN * ('cdn') or the missing width and height ('dimensions'). * * @param string $switch delivery, fit, cdn or dimensions. * @return bool True when this request is a GET for a public page (not the admin, AJAX, an email, the cart, checkout or * account pages, or a non-public post) and the switch is on with the public-pictures confirmation. */ public static function allowed( string $switch = 'delivery' ): bool { $s = Settings::get(); if ( 'GET' !== ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET' ) ) { return false; } if ( ! in_array( $switch, array( 'delivery', 'fit', 'cdn', 'dimensions' ), true ) || empty( $s[ $switch ] ) || ! $s['public_policy'] || is_multisite() || is_admin() || wp_doing_ajax() || wp_doing_cron() || is_feed() || is_preview() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ! did_action( 'wp' ) ) { return false; } if ( did_action( 'woocommerce_email_header' ) > did_action( 'woocommerce_email_footer' ) ) { return false; } if ( function_exists( 'is_cart' ) && ( is_cart() || is_checkout() || is_account_page() ) ) { return false; } if ( is_singular() && ! Sources::public_post( (int) get_queried_object_id() ) ) { return false; } // Refuse action/query routes at render time as well as in the crawler. $uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated by strict URL policy. $path = wp_parse_url( home_url( '/' ) ); $request = ( $path['scheme'] ?? 'https' ) . '://' . ( $path['host'] ?? '' ) . $uri; // Urls::page() resolves the address to its post (url_to_postid(), a full WP_Query), and kses() asks for every // wp_kses_post() of the page once a picture is on it: about 1,400 times on a shop page (1.8.4, F-PERF-06). The served // address does not change during the request; a post's status can, so a cleaned post cache forgets the verdicts. if ( ! array_key_exists( $request, self::$pages ) ) { self::$pages[ $request ] = ! is_wp_error( Urls::page( $request ) ); } return self::$pages[ $request ]; } /** Forgets the remembered page verdicts of allowed() (a post was saved, trashed or deleted). */ public static function forget_pages(): void { self::$pages = array(); } /** * Deliver an attachment's img printed by wp_get_attachment_image (not inside the_content, which has its own pass). * * @param mixed $html The img. * @param mixed $id The attachment. * @param mixed $size The size printed. * @param mixed $icon Whether it is an icon (icons stay). * @param mixed $attrs Its attributes. * @return mixed The img, or a picture element with the copies. */ public function attachment( $html, $id, $size, $icon, $attrs ) { if ( ! is_string( $html ) || $icon || ! self::allowed() || doing_filter( 'the_content' ) ) { return $html; } $product_page = function_exists( 'is_product' ) && is_product(); if ( $product_page && ! Integrations::allow_product_attachment() ) { return $html; } if ( $product_page && 'variable_coordinated' === Integrations::woo_product_image_mode() ) { if ( ! wp_script_is( 'afsd-im-woo-delivery', 'enqueued' ) || ! str_contains( $html, 'data-large_image=' ) ) { return $html; } return Woo_Delivery::decorate( $this->image( $html, (int) $id, true ), true ); } if ( $product_page && str_contains( $html, 'data-large_image=' ) ) { return Woo_Delivery::decorate( $this->image( $html, (int) $id, true ) ); } return $this->image( $html, (int) $id, $product_page ); } /** * The delivery plan of a variation's picture, for WooCommerce's variation data. * * @param string $html The picture's img. * @param int $attachment Its attachment. * @param int $product The product. * @return array The plan, or an empty array. */ public static function variation_plan( string $html, int $attachment, int $product ): array { if ( ! Foundation::available() || ! Woo_Delivery::data_allowed( $product ) ) { return array(); } return Woo_Delivery::plan( self::shared()->image( $html, $attachment, true ) ); } /** * Supported theme/plugin entry point, once Image Manager has booted. It never publishes files or changes settings. * * @param mixed $html One img fragment. * @param int $attachment_id Its attachment, or 0 to read it from the img. * @return string The fragment as delivery prints it. */ public static function render_fragment( $html, int $attachment_id = 0 ): string { if ( ! is_string( $html ) || ! Foundation::available() || ! self::allowed() ) { return is_string( $html ) ? $html : ''; } if ( 'variable_coordinated' === Integrations::woo_product_image_mode() ) { return $html; } if ( function_exists( 'is_product' ) && is_product() && ! Integrations::allow_product_attachment() ) { return $html; } return self::shared()->image( $html, $attachment_id, false ); } /** * Open a guard for the_content (first of the three passes). * * @param mixed $html The content. * @return mixed The content, unchanged. */ public function begin_content( $html ) { $this->content_guards[] = true; return $html; } /** * Note whether the content already holds picture or source elements. * * @param mixed $html The content. * @return mixed The content, unchanged. */ public function check_content( $html ) { if ( $this->content_guards ) { $this->content_guards[ count( $this->content_guards ) - 1 ] = ! is_string( $html ) || stripos( $html, 'content_guards ); return $html; } /** * Deliver an img of the_content or of an approved comment (wp_content_img_tag). * * @param mixed $html The img. * @param mixed $context Where WordPress filters it. * @param mixed $id Its attachment. * @return mixed The img, or a picture element with the copies. */ public function content_image( $html, $context, $id ) { $enabled_context = $this->comment_context || ( 'the_content' === $context && $this->content_guards && false === end( $this->content_guards ) ); return $enabled_context && is_string( $html ) && self::allowed() ? $this->image( $html, (int) $id ) : $html; } /** * Deliver the images of an approved comment on a public post (comment_text). * * @param mixed $html The comment's text. * @param mixed $comment The comment. * @param mixed $args The comment text arguments. * @return mixed The text. */ public function comment( $html, $comment, $args ) { if ( ! is_string( $html ) || ! $comment || '1' !== (string) $comment->comment_approved || ! Sources::public_post( (int) $comment->comment_post_ID ) || ! self::allowed() || stripos( $html, 'comment_context; $this->comment_context = true; try { return wp_filter_content_tags( $html, 'afsd_im_comment' ); } finally { $this->comment_context = $before; } } /** * Only a single supported img fragment enters this method, never an entire response. * * @param string $html The img fragment. * @param int $id Its attachment, or 0 to read it from the img. * @param bool $allow_zoom_metadata Whether WooCommerce's zoom attributes may name copies. * @return string The img, or a picture element with the copies. */ public function image( string $html, int $id = 0, bool $allow_zoom_metadata = false ): string { if ( strlen( $html ) > 32768 || stripos( $html, 'next_tag( 'IMG' ) ) { return $html; } $blocked_attrs = array( 'data-src', 'data-lazy-src', 'data-original', 'data-lazy-original', 'data-srcset', 'data-lazy-srcset', 'data-afsd-im' ); $woo_zoom = $allow_zoom_metadata && is_string( $p->get_attribute( 'data-src' ) ) && $p->get_attribute( 'data-src' ) === $p->get_attribute( 'data-large_image' ) && (int) $p->get_attribute( 'data-large_image_width' ) > 0 && (int) $p->get_attribute( 'data-large_image_height' ) > 0; if ( $woo_zoom ) { $blocked_attrs = array_values( array_diff( $blocked_attrs, array( 'data-src' ) ) ); } if ( ! $allow_zoom_metadata ) { $blocked_attrs = array_merge( $blocked_attrs, array( 'data-large_image', 'data-large-image' ) ); } foreach ( $blocked_attrs as $a ) { if ( null !== $p->get_attribute( $a ) ) { return $html; } } $src = $p->get_attribute( 'src' ); if ( ! is_string( $src ) || '' === $src ) { return $html; } $m = Sources::map( $src, $id ?: Sources::hint( $p ), false ); if ( ! $m ) { return $html; } $s = Settings::get(); if ( $m['cdn'] && ! $s['local_cdn'] ) { return $html; } $id = $m['id']; if ( ! array_key_exists( $id, $this->rows ) ) { $this->rows[ $id ] = Repository::attachment( $id ); } $row = $this->rows[ $id ]; if ( ! $row || $row['excluded'] || ! $row['delivery'] || ( empty( $row['manifest']['variants'] ) && empty( $row['manifest']['responsive'] ) && empty( $row['manifest']['watermarked'] ) ) || ! Sources::eligible( $row, false ) ) { return $html; } $files = Sources::files( $id ); if ( $woo_zoom ) { $zoom_file = Sources::variant_for_url( $p->get_attribute( 'data-src' ), $files ); if ( ! $zoom_file || (int) $zoom_file['width'] !== (int) $p->get_attribute( 'data-large_image_width' ) || (int) $zoom_file['height'] !== (int) $p->get_attribute( 'data-large_image_height' ) ) { return $html; } } $base = Sources::variant_for_url( $src, $files ); if ( ! $base ) { return $html; } // 1.9.0 (D-500 gap 6): with the watermark on, a picture with watermarked copies of every file the img names gets // them, its own img included; one without them (narrower than chosen, or held) keeps its ordinary copies. if ( Watermark::active() && Watermark::wanted( $files, $s ) ) { $marked = $this->watermarked( $p, $row, $files, $base, $woo_zoom, $id ); if ( null !== $marked ) { return $marked; } } $variant = $row['manifest']['variants'][ $base['relative'] ] ?? null; $fill = ! empty( $row['manifest']['responsive'] ); if ( ! $fill && ( ! $variant || ! self::usable( $variant, $base ) ) ) { return $html; } $entries = array(); $srcset = $p->get_attribute( 'srcset' ); if ( is_string( $srcset ) && '' !== $srcset ) { $candidates = Urls::srcset( $srcset ); if ( ! $candidates ) { return $html; } foreach ( $candidates as $candidate ) { $media = Urls::media( $candidate['url'] ); if ( ! $media || ( $media['cdn'] && ! $s['local_cdn'] ) ) { return $html; } $file = Sources::variant_for_url( $candidate['url'], $files ); if ( ! $file || $file['width'] !== $candidate['width'] ) { return $html; } $v = $row['manifest']['variants'][ $file['relative'] ] ?? null; // No loss of responsive candidates or high-density resolution to force a saving. if ( ! $fill && ( ! $v || ! self::usable( $v, $file ) ) ) { return $html; } $entries[] = array( 'variant' => $v ?? array(), 'file' => $file, 'width' => $candidate['width'], 'native_url' => $candidate['url'], ); } } else { $entries[] = array( 'variant' => $variant ?? array(), 'file' => $base, 'width' => 0, 'native_url' => $src, ); } $sets = self::complete_sets( $entries, $fill ); if ( ! $sets ) { return $html; } $sizes = $p->get_attribute( 'sizes' ); // "auto" is only eligible on a lazy img. The source must use its ordinary sizes fallback. if ( is_string( $sizes ) && preg_match( '/^auto(?:\s*,\s*|\s*$)/i', $sizes ) ) { $sizes = preg_replace( '/^auto(?:\s*,\s*|\s*$)/i', '', $sizes ); if ( '' === $sizes && count( $entries ) > 1 ) { return $html; } } $sets = Responsive::extend( $sets, $entries, $row, is_string( $sizes ) ? $sizes : '' ); if ( ! $sets ) { return $html; } // Some themes call wp_kses_post() AFTER the native image hook. Register only exact // generated values, so that sanitization can preserve these wrappers without an // unrestricted frontend allowlist or any changes to saved-content sanitization. if ( count( $this->emitted_sets ) >= 256 ) { return $html; } foreach ( $sets as $set ) { $this->emitted_sets[ $set['srcset'] ] = true; } $this->emitted_ids[ (string) $id ] = true; foreach ( array( 'srcset', 'sizes', 'decoding', 'fetchpriority' ) as $attr ) { $value = $p->get_attribute( $attr ); if ( is_string( $value ) ) { $this->emitted_attrs[ $attr ][ html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ] = true; } } if ( is_string( $sizes ) ) { $this->emitted_attrs['source_sizes'][ $sizes ] = true; } $size_attr = is_string( $sizes ) && '' !== $sizes ? ' sizes="' . esc_attr( $sizes ) . '"' : ''; if ( $p->next_tag( 'IMG' ) ) { return $html; } return '' . self::sources_html( $sets, is_string( $sizes ) ? $sizes : '' ) . $html . ''; } /** * 1.9.0 (D-500 gap 6): the picture with its watermarked copies: the img's src, srcset and WooCommerce's zoom picture * point to the copies in the picture's own format, the sources to the WebP or AVIF ones. * * @param \WP_HTML_Tag_Processor $p The img, its attributes read. * @param array $row The picture's index row. * @param array $files Its files. * @param array $base The file the img's src names. * @param bool $woo_zoom Whether the img carries WooCommerce's zoom picture. * @param int $id The attachment. * @return string|null The markup, or null when a file the img names has no usable watermarked copy. */ private function watermarked( \WP_HTML_Tag_Processor $p, array $row, array $files, array $base, bool $woo_zoom, int $id ): ?string { $native = static function ( array $file ) use ( $row ): ?array { $copy = Watermark::copy( $row, $file ); foreach ( $copy ? Variants::choices( $copy ) : array() as $c ) { if ( ( $c['output_mime'] ?? '' ) === ( $c['source_mime'] ?? '' ) && self::usable( $c, $file ) ) { return $c; } } return null; }; $entries = array(); $srcset = $p->get_attribute( 'srcset' ); if ( is_string( $srcset ) && '' !== $srcset ) { $candidates = Urls::srcset( $srcset ); foreach ( $candidates ? $candidates : array( null ) as $candidate ) { $file = $candidate ? Sources::variant_for_url( $candidate['url'], $files ) : null; if ( ! $file || $file['width'] !== $candidate['width'] || ! $native( $file ) ) { return null; } $entries[] = array( 'variant' => Watermark::copy( $row, $file ), 'file' => $file, 'width' => $candidate['width'], ); } } elseif ( $native( $base ) ) { $entries[] = array( 'variant' => Watermark::copy( $row, $base ), 'file' => $base, 'width' => 0, ); } else { return null; } $own = $native( $base ); if ( ! $own ) { return null; } $zoom = null; if ( $woo_zoom ) { $zoom_file = Sources::variant_for_url( $p->get_attribute( 'data-src' ), $files ); $zoom = $zoom_file ? $native( $zoom_file ) : null; if ( ! $zoom ) { return null; } } $sets = array(); $img = null; foreach ( self::complete_sets( $entries ) as $set ) { if ( $set['mime'] === $own['output_mime'] ) { $img = $set; } else { $sets[] = $set; } } if ( ! $img || count( $this->emitted_sets ) >= 256 ) { return null; } $sizes = $p->get_attribute( 'sizes' ); if ( is_string( $sizes ) && preg_match( '/^auto(?:\s*,\s*|\s*$)/i', $sizes ) ) { $sizes = preg_replace( '/^auto(?:\s*,\s*|\s*$)/i', '', $sizes ); if ( '' === $sizes && count( $entries ) > 1 ) { $sets = array(); } } $p->set_attribute( 'src', self::url( $own ) ); if ( is_string( $srcset ) && '' !== $srcset ) { $p->set_attribute( 'srcset', $img['srcset'] ); } if ( $zoom ) { $p->set_attribute( 'data-src', self::url( $zoom ) ); $p->set_attribute( 'data-large_image', self::url( $zoom ) ); } foreach ( $sets as $set ) { $this->emitted_sets[ $set['srcset'] ] = true; } $this->emitted_ids[ (string) $id ] = true; foreach ( array( 'srcset', 'sizes', 'decoding', 'fetchpriority' ) as $attr ) { $value = $p->get_attribute( $attr ); if ( is_string( $value ) ) { $this->emitted_attrs[ $attr ][ html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ] = true; } } if ( is_string( $sizes ) ) { $this->emitted_attrs['source_sizes'][ $sizes ] = true; } if ( $p->next_tag( 'IMG' ) ) { return null; } $html = $p->get_updated_html(); return $sets ? '' . self::sources_html( $sets, is_string( $sizes ) ? $sizes : '' ) . $html . '' : $html; } /** * Only complete, same-format responsive families are offered. The native img is always retained. * * @param array $entries The img's native files with their copies. * @param bool $native_fallback Whether a native-format family may fill a gap. * @return array Source sets by type. */ public static function complete_sets( array $entries, bool $native_fallback = false ): array { $sets = array(); $first = true; foreach ( $entries as $entry ) { $choices = array(); foreach ( Variants::choices( $entry['variant'] ) as $v ) { if ( ! self::usable( $v, $entry['file'] ) ) { continue; } $mime = Variants::mime( $v ); if ( ! isset( $choices[ $mime ] ) || $v['output_bytes'] < $choices[ $mime ]['output_bytes'] ) { $choices[ $mime ] = $v; } } if ( $native_fallback && ! empty( $entry['native_url'] ) ) { $dim = wp_getimagesize( $entry['file']['path'] ); $mime = $dim['mime'] ?? ''; if ( in_array( $mime, array( 'image/jpeg', 'image/png', 'image/webp', 'image/avif' ), true ) && ! isset( $choices[ $mime ] ) && (int) $dim[0] === $entry['file']['width'] && (int) $dim[1] === $entry['file']['height'] ) { $choices[ $mime ] = array( '_native_url' => $entry['native_url'], 'output_bytes' => filesize( $entry['file']['path'] ), ); } } if ( $first ) { foreach ( $choices as $mime => $v ) { $sets[ $mime ] = array( 'mime' => $mime, 'parts' => array(), 'bytes' => 0, 'managed' => 0, ); } $first = false; } foreach ( array_keys( $sets ) as $mime ) { if ( ! isset( $choices[ $mime ] ) ) { unset( $sets[ $mime ] ); continue; } $v = $choices[ $mime ]; $sets[ $mime ]['managed'] += isset( $v['_native_url'] ) ? 0 : 1; $sets[ $mime ]['parts'][] = ( $v['_native_url'] ?? self::url( $v ) ) . ( $entry['width'] ? ' ' . $entry['width'] . 'w' : '' ); $sets[ $mime ]['bytes'] += (int) $v['output_bytes']; } if ( ! $sets ) { return array(); } } uasort( $sets, static fn( $a, $b ) => $a['bytes'] <=> $b['bytes'] ); foreach ( $sets as &$set ) { $set['srcset'] = implode( ', ', $set['parts'] ); unset( $set['parts'] ); if ( ! $native_fallback ) { unset( $set['managed'] ); } }unset( $set ); return array_values( $sets ); } /** * The source elements of the sets. * * @param array $sets Source sets by type. * @param string $sizes The img's sizes attribute. * @return string The source elements' HTML. */ public static function sources_html( array $sets, string $sizes ): string { $html = ''; foreach ( $sets as $set ) { $html .= ''; } return $html; } /** * Preserve exact emitted responsive fragments through frontend wp_kses_post only. * * @param mixed $allowed The allowed tags and attributes. * @param mixed $context The kses context. * @return mixed The allowed tags and attributes. */ public function kses( $allowed, $context ) { if ( 'post' !== $context || ! is_array( $allowed ) || ! $this->emitted_sets || ! self::allowed() ) { return $allowed; } if ( ! isset( $allowed['picture'] ) ) { $allowed['picture'] = array( 'class' => array( 'required' => true, 'values' => array( 'afsd-im-picture' ), ), 'data-afsd-im' => array( 'required' => true, 'value_callback' => fn( $value ) => isset( $this->emitted_ids[ (string) $value ] ), ), ); } $source = array( 'type' => array( 'required' => true, 'values' => array( 'image/webp', 'image/avif', 'image/jpeg', 'image/png' ), ), 'srcset' => array( 'required' => true, 'value_callback' => fn( $value ) => isset( $this->emitted_sets[ html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ] ), ), 'sizes' => array( 'value_callback' => fn( $value ) => isset( $this->emitted_attrs['source_sizes'][ html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ] ) ), ); if ( ! isset( $allowed['source'] ) ) { $allowed['source'] = $source; } else { foreach ( array( 'srcset', 'sizes' ) as $attr ) { if ( ! isset( $allowed['source'][ $attr ] ) ) { $allowed['source'][ $attr ] = $source[ $attr ]; } } } foreach ( array( 'srcset', 'sizes', 'decoding', 'fetchpriority' ) as $attr ) { if ( isset( $allowed['img'] ) && ! isset( $allowed['img'][ $attr ] ) && ! empty( $this->emitted_attrs[ $attr ] ) ) { $allowed['img'][ $attr ] = array( 'value_callback' => fn( $value ) => isset( $this->emitted_attrs[ $attr ][ html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ] ) ); } } return $allowed; } /** * Whether a copy can be delivered: its file is there under a copy name, readable and not a link, made under a policy * still served, with the right extension and the recorded size, and its native file still there. * * @param array $v The copy. * @param array $file Its native file. * @return bool True when it can. */ public static function usable( array $v, array $file ): bool { try { $path = Settings::root() . '/' . ( $v['file'] ?? '' ); if ( ! Variants::filename( $v['file'] ?? '' ) || is_link( $path ) || ! is_file( $path ) || ! is_readable( $path ) ) { return false; } clearstatcache( true, $file['path'] ); clearstatcache( true, $path ); return Policy::valid_variant( $v ) && pathinfo( $v['file'], PATHINFO_EXTENSION ) === Variants::extension( Variants::mime( $v ) ) && filesize( $path ) === (int) $v['output_bytes'] && is_file( $file['path'] ) && filesize( $file['path'] ) === (int) $v['bytes'] && filemtime( $file['path'] ) === (int) $v['source_mtime'] && ( empty( $v['resized'] ) ? ( $file['width'] === (int) $v['width'] && $file['height'] === (int) $v['height'] ) : ( ( $v['resampler'] ?? '' ) === Responsive::VERSION && $file['width'] === (int) ( $v['source_width'] ?? 0 ) && $file['height'] === (int) ( $v['source_height'] ?? 0 ) && (int) $v['width'] > 0 && (int) $v['width'] < $file['width'] && Responsive::ratio( $v, $file ) ) ); } catch ( \Throwable $e ) { return false; } } /** * A copy's address in Image Manager's folder. * * @param array $v The copy. * @return string The address. */ public static function url( array $v ): string { $u = wp_get_upload_dir(); return trailingslashit( $u['baseurl'] ) . 'affstackdev-image-manager/' . rawurlencode( $v['file'] ); } /** * Whether an address is a copy file in Image Manager's folder. * * @param string $url The address. * @return bool True when it is. */ public static function is_derivative( string $url ): bool { $u = wp_get_upload_dir(); $base = trailingslashit( $u['baseurl'] ) . 'affstackdev-image-manager/'; if ( ! str_starts_with( $url, $base ) ) { return false; } $name = substr( $url, strlen( $base ) ); if ( ! preg_match( '/^([tdw]?)([1-9][0-9]*)-[a-f0-9]{64}\.(?:webp|png|jpg|avif)$/D', $name, $m ) ) { return false; } $row = in_array( $m[1], array( 't', 'd' ), true ) ? Repository::row( (int) $m[2] ) : Repository::attachment( (int) $m[2] ); foreach ( Variants::flatten( $row['manifest'] ?? array() ) as $v ) { if ( $v['file'] === $name ) { try { return is_file( Settings::root() . '/' . $name ); } catch ( \Throwable $e ) { return false; } } } return false; } /** * Stored derivative coverage only; not proof that a particular page offers or downloads a file. * * @param array $row The source row. * @return array Native sizes, those with copies, partial ones, display sizes and the complete type families. */ public static function coverage( array $row ): array { $files = Sources::row_files( $row ); $total = count( $files ); $optimized = 0; $mime_counts = array(); foreach ( $files as $file ) { $seen = array(); $variant = $row['manifest']['variants'][ $file['relative'] ] ?? array(); foreach ( Variants::choices( $variant ) as $v ) { if ( ! self::usable( $v, $file ) ) { continue; } $mime = Variants::mime( $v ); $seen[ $mime ] = true; } if ( $seen ) { ++$optimized; } foreach ( array_keys( $seen ) as $mime ) { $mime_counts[ $mime ] = (int) ( $mime_counts[ $mime ] ?? 0 ) + 1; } } $complete = array(); if ( $total ) { foreach ( $mime_counts as $mime => $count ) { if ( $count === $total ) { $complete[] = $mime; } } } sort( $complete ); return array( 'native_sizes' => $total, 'optimized_native_sizes' => $optimized, 'partial_native_sizes' => max( 0, $total - $optimized ), 'responsive_sizes' => count( $row['manifest']['responsive'] ?? array() ), 'complete_managed_mime_families' => $complete, ); } /** * The words for a source's stored delivery coverage. * * @param array $row The source row. * @return string The translated words. */ public static function coverage_label( array $row ): string { $c = self::coverage( $row ); if ( ! $c['native_sizes'] ) { return __( 'Stored delivery coverage is unavailable for this source.', 'affstackdev-image-manager' ); } /* translators: 1: native sizes with validated optimized copies; 2: native sizes of the image. */ $text = sprintf( _n( 'Stored coverage: %1$d of %2$d native size(s) have validated optimized copies', 'Stored coverage: %1$d of %2$d native size(s) have validated optimized copies', $c['native_sizes'], 'affstackdev-image-manager' ), $c['optimized_native_sizes'], $c['native_sizes'] ); if ( $c['complete_managed_mime_families'] ) { /* translators: %s: comma-separated image formats, such as webp, avif. */ $text .= sprintf( __( '; complete managed family: %s', 'affstackdev-image-manager' ), implode( ', ', array_map( static fn( $v ) => str_replace( 'image/', '', $v ), $c['complete_managed_mime_families'] ) ) ); } elseif ( $c['optimized_native_sizes'] ) { $text .= __( '; no complete all-native-size alternative family is stored', 'affstackdev-image-manager' ); } if ( $c['responsive_sizes'] ) { /* translators: %d: number of display sizes Image Manager added. */ $text .= sprintf( _n( '; %d added display size(s)', '; %d added display size(s)', $c['responsive_sizes'], 'affstackdev-image-manager' ), $c['responsive_sizes'] ); } return $text . __( '. This is stored-file coverage, not browser-download proof.', 'affstackdev-image-manager' ); } /** * Tell Jetpack's image CDN to skip Image Manager's copies, when local copies for Jetpack sizes are allowed. * * @param mixed $skip Whether Jetpack already skips it. * @param mixed $url The image's address. * @param mixed $unused Jetpack's next argument (not used). * @param mixed $scheme Jetpack's scheme argument (not used). * @return mixed True to skip it. */ public function skip_cdn( $skip, $url, $unused = null, $scheme = null ) { return $skip || ( Settings::get()['local_cdn'] && is_string( $url ) && self::is_derivative( $url ) ); } }