get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_mime_type LIKE %s AND post_mime_type NOT IN ('image/svg+xml','image/x-icon','image/vnd.microsoft.icon')", $wpdb->esc_like( 'image/' ) . '%' ) ); $missing = self::query_missing( 0, 0, true ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $decorative = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = '1'", self::DECORATIVE_META ) ); return array( 'total' => $total, 'missing' => $missing, 'decorative' => $decorative, 'described' => max( 0, $total - $missing - $decorative ), ); } /** * Images with no alt text that are not marked decorative. * * @param int $offset Offset. * @param int $limit Page size. * @param bool $count Return the count instead of rows. * @return int|int[] Count or attachment ids. */ public static function query_missing( $offset = 0, $limit = 20, $count = false ) { global $wpdb; if ( $count ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} alt ON alt.post_id = p.ID AND alt.meta_key = '_wp_attachment_image_alt' LEFT JOIN {$wpdb->postmeta} deco ON deco.post_id = p.ID AND deco.meta_key = %s WHERE p.post_type = 'attachment' AND p.post_mime_type LIKE %s AND p.post_mime_type NOT IN ('image/svg+xml','image/x-icon','image/vnd.microsoft.icon') AND ( alt.meta_value IS NULL OR TRIM(alt.meta_value) = '' ) AND ( deco.meta_value IS NULL OR deco.meta_value <> '1' )", self::DECORATIVE_META, $wpdb->esc_like( 'image/' ) . '%' ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $ids = $wpdb->get_col( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} alt ON alt.post_id = p.ID AND alt.meta_key = '_wp_attachment_image_alt' LEFT JOIN {$wpdb->postmeta} deco ON deco.post_id = p.ID AND deco.meta_key = %s WHERE p.post_type = 'attachment' AND p.post_mime_type LIKE %s AND p.post_mime_type NOT IN ('image/svg+xml','image/x-icon','image/vnd.microsoft.icon') AND ( alt.meta_value IS NULL OR TRIM(alt.meta_value) = '' ) AND ( deco.meta_value IS NULL OR deco.meta_value <> '1' ) ORDER BY p.post_date DESC LIMIT %d OFFSET %d", self::DECORATIVE_META, $wpdb->esc_like( 'image/' ) . '%', $limit, $offset ) ); return array_map( 'intval', $ids ); } /** * Data the Images screen shows for one attachment. * * @param int $id Attachment id. * @return array */ public static function describe( $id ) { $parent = (int) wp_get_post_parent_id( $id ); $file = get_attached_file( $id ); return array( 'id' => $id, 'thumb' => wp_get_attachment_image_url( $id, 'thumbnail' ), 'medium' => wp_get_attachment_image_url( $id, 'medium_large' ), 'title' => get_the_title( $id ), 'file' => $file ? wp_basename( $file ) : '', 'alt' => (string) get_post_meta( $id, '_wp_attachment_image_alt', true ), 'decorative' => (bool) get_post_meta( $id, self::DECORATIVE_META, true ), 'parent_title' => $parent ? get_the_title( $parent ) : '', 'parent_link' => $parent ? get_edit_post_link( $parent, 'raw' ) : '', 'edit_link' => get_edit_post_link( $id, 'raw' ), ); } /** * Saves alt text or marks an image decorative. * * @param int $id Attachment id. * @param string $alt Alt text. * @param bool $decorative Whether the image is decorative. * @return array|\WP_Error Updated image data. */ public static function save( $id, $alt, $decorative ) { $post = get_post( $id ); if ( ! $post || 'attachment' !== $post->post_type || ! wp_attachment_is_image( $id ) ) { return new \WP_Error( 'fixlight_not_image', __( 'That is not an image in the media library.', 'fixlight' ), array( 'status' => 404 ) ); } if ( ! current_user_can( 'edit_post', $id ) ) { return new \WP_Error( 'fixlight_forbidden', __( 'You cannot edit this image.', 'fixlight' ), array( 'status' => 403 ) ); } $alt = trim( sanitize_text_field( $alt ) ); // The ledger counts images, so saving the same image again does not add to it. $had_alt = '' !== trim( (string) get_post_meta( $id, '_wp_attachment_image_alt', true ) ); $was_decorative = (bool) get_post_meta( $id, self::DECORATIVE_META, true ); if ( $decorative ) { update_post_meta( $id, self::DECORATIVE_META, '1' ); delete_post_meta( $id, '_wp_attachment_image_alt' ); if ( ! $was_decorative ) { self::count( 'decorative' ); } } else { delete_post_meta( $id, self::DECORATIVE_META ); if ( '' === $alt ) { delete_post_meta( $id, '_wp_attachment_image_alt' ); } else { update_post_meta( $id, '_wp_attachment_image_alt', $alt ); if ( ! $had_alt ) { self::count( 'alt_written' ); } } } Cache::purge_soon(); return self::describe( $id ); } /** * Adds to the fix ledger. * * @param string $key Counter. */ private static function count( $key ) { $ledger = get_option( self::LEDGER, array() ); $ledger = is_array( $ledger ) ? $ledger : array(); $ledger[ $key ] = isset( $ledger[ $key ] ) ? (int) $ledger[ $key ] + 1 : 1; update_option( self::LEDGER, $ledger, false ); } /** * Ledger counters. * * @return array{alt_written:int,decorative:int} */ public static function ledger() { $ledger = get_option( self::LEDGER, array() ); $ledger = is_array( $ledger ) ? $ledger : array(); return array( 'alt_written' => isset( $ledger['alt_written'] ) ? (int) $ledger['alt_written'] : 0, 'decorative' => isset( $ledger['decorative'] ) ? (int) $ledger['decorative'] : 0, ); } }