esc_url_raw( rest_url( AccessVane_REST::NAMESPACE_V1 . '/alt-text' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'strings' => array( 'saved' => __( 'Saved', 'accessvane' ), 'failed' => __( 'Could not save', 'accessvane' ), ), ) ); } /** * Count images with no alt text. * * @return int */ private function missing_count() { $query = new WP_Query( $this->query_args( 1, 1 ) ); return (int) $query->found_posts; } /** * Query arguments for attachments missing alt text. * * Uses a NOT EXISTS / empty-string pair because WordPress writes an empty * string for images that were saved with the field left blank, and no row * at all for images that were never touched. * * @param int $page Page number. * @param int $per_page Results per page. * @return array */ private function query_args( $page, $per_page ) { return array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' ), 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => 'date', 'order' => 'DESC', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- the whole purpose of this screen is to find images by a missing meta value. 'meta_query' => array( 'relation' => 'OR', array( 'key' => '_wp_attachment_image_alt', 'compare' => 'NOT EXISTS', ), array( 'key' => '_wp_attachment_image_alt', 'value' => '', 'compare' => '=', ), ), ); } /** * A suggested description, offered but never saved automatically. * * Caption first, then title — and never the file name, which describes * nothing. Returning an empty string means "we have nothing useful", which * is a more honest answer than inventing something. * * @param WP_Post $attachment Attachment. * @return string */ private function suggestion( $attachment ) { $caption = trim( (string) $attachment->post_excerpt ); if ( '' !== $caption ) { return $caption; } $title = trim( (string) $attachment->post_title ); if ( '' === $title ) { return ''; } if ( preg_match( '/\.(jpe?g|png|gif|webp|avif|svg)$/i', $title ) ) { return ''; } if ( preg_match( '/^(img|dsc|dscn|pxl|photo|image|screenshot|screen[ _-]?shot|untitled)[ _-]?\d*/i', $title ) ) { return ''; } if ( preg_match( '/^\d{6,}$/', $title ) ) { return ''; } return $title; } /** * Render the screen. * * @return void */ public function render_page() { if ( ! current_user_can( 'upload_files' ) ) { wp_die( esc_html__( 'You are not allowed to edit media.', 'accessvane' ), 403 ); } $paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination. $query = new WP_Query( $this->query_args( $paged, self::PER_PAGE ) ); $total = (int) $query->found_posts; echo '
'; echo '

' . esc_html__( 'Image alt text', 'accessvane' ) . '

'; echo '

' . esc_html__( 'Every image that carries meaning needs a short description of what it shows. Type it and it saves as you go — there is no Save button to remember.', 'accessvane' ) . '

'; ?>

' . esc_html__( 'Every image has a description', 'accessvane' ) . '

'; echo '

' . esc_html__( 'Nothing in your media library is missing alt text. Worth re-checking after your next batch of uploads.', 'accessvane' ) . '

'; return; } printf( '

%s

', esc_html( sprintf( /* translators: %d: number of images. */ _n( '%d image still needs a description.', '%d images still need a description.', $total, 'accessvane' ), $total ) ) ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $query->posts as $attachment ) { $thumb = wp_get_attachment_image_url( $attachment->ID, 'thumbnail' ); $suggestion = $this->suggestion( $attachment ); $edit_link = get_edit_post_link( $attachment->ID ); echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Image', 'accessvane' ) . '' . esc_html__( 'Description (alt text)', 'accessvane' ) . '' . esc_html__( 'Decorative', 'accessvane' ) . '
'; if ( $thumb ) { printf( '%2$s', esc_url( $thumb ), /* translators: %s: image file name. */ esc_attr( sprintf( __( 'Preview of %s', 'accessvane' ), $attachment->post_title ) ) ); } echo ''; printf( '', esc_attr( $attachment->ID ), esc_html__( 'Alt text for this image', 'accessvane' ) ); printf( '', esc_attr( $attachment->ID ), esc_attr__( 'Describe what this image shows', 'accessvane' ) ); echo ''; if ( '' !== $suggestion ) { printf( '

%3$s

', esc_attr( $suggestion ), esc_html__( 'Use the caption', 'accessvane' ), esc_html( '"' . $suggestion . '"' ) ); } if ( $edit_link ) { printf( '

%2$s

', esc_url( $edit_link ), esc_html__( 'Open in Media Library', 'accessvane' ) ); } echo '
'; printf( '', esc_html__( 'Skip it', 'accessvane' ) ); echo '
'; $pages = (int) ceil( $total / self::PER_PAGE ); if ( $pages > 1 ) { echo '
'; echo wp_kses_post( paginate_links( array( 'base' => add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $paged, 'total' => $pages, 'prev_text' => __( '« Previous', 'accessvane' ), 'next_text' => __( 'Next »', 'accessvane' ), ) ) ); echo '
'; } echo ''; wp_reset_postdata(); } }