__( 'Add alt text before using this image.', 'ablocks' ), 'hint' => __( 'Describe what the image shows. Leave it empty only if the image is purely decorative.', 'ablocks' ), ] ); } /** * Refuse to publish a post that uses images with no alt text. * * This is the part that actually enforces the rule. The media modal gate * covers the normal path, but it is client side and only guards the modal — * a block pasted in, an imported page or a REST call would sail past it. * Checking at save time is the backstop that cannot be walked around. * * The post is demoted to draft rather than rejected, so the writing is never * lost; a notice explains why. * * @param array $data Sanitised post data. * @param array $postarr Raw post data. * @return array */ public static function block_publish_without_alt( $data, $postarr ) { if ( 'publish' !== $data['post_status'] ) { return $data; } if ( ! empty( $data['post_type'] ) && ! is_post_type_viewable( $data['post_type'] ) ) { return $data; } if ( wp_is_post_revision( $postarr['ID'] ?? 0 ) || wp_is_post_autosave( $postarr['ID'] ?? 0 ) ) { return $data; } $missing = self::images_without_alt( (string) $data['post_content'] ); if ( empty( $missing ) ) { return $data; } $data['post_status'] = 'draft'; set_transient( 'ablocks_alt_block_' . get_current_user_id(), $missing, 60 ); return $data; } /** * Attachment ids referenced in content that have no alt text. * * @param string $content Post content. * @return int[] */ public static function images_without_alt( $content ) { if ( false === strpos( $content, 'wp-image-' ) ) { return []; } if ( ! preg_match_all( '/wp-image-(\d+)/', $content, $matches ) ) { return []; } $missing = []; foreach ( array_unique( $matches[1] ) as $id ) { $id = (int) $id; if ( ! $id ) { continue; } $alt = get_post_meta( $id, '_wp_attachment_image_alt', true ); if ( '' === trim( (string) $alt ) ) { $missing[] = $id; } } return $missing; } /** * Explain a blocked publish. */ public static function publish_blocked_notice() { $key = 'ablocks_alt_block_' . get_current_user_id(); $missing = get_transient( $key ); if ( empty( $missing ) || ! is_array( $missing ) ) { return; } delete_transient( $key ); $links = []; foreach ( array_slice( $missing, 0, 10 ) as $id ) { $title = get_the_title( $id ); $edit = get_edit_post_link( $id ); $label = $title ? $title : '#' . $id; $links[] = $edit ? '' . esc_html( $label ) . '' : esc_html( $label ); } echo '
'; esc_html_e( 'Saved as a draft: some images have no alt text.', 'ablocks' ); echo '
'; esc_html_e( 'Your site requires every image to describe itself before a post goes live. Add alt text to the images below, then publish again.', 'ablocks' ); echo '
' . wp_kses_post( implode( ', ', $links ) ) . '