*/ public static function abilities(): array { $idSchema = ['minimum' => 1, 'type' => 'integer']; return [ new Ability( 'sarcio/media-search', __('Look for images', 'sarcio'), Ability::READ, 'Search the media library for images by words in their title, caption or file name. Returns id, title, alt text, url and size. Use it to find the image id for a featured image or an alt text change.', [ 'properties' => [ 'limit' => ['maximum' => 20, 'minimum' => 1, 'type' => 'integer'], 'query' => ['maxLength' => 200, 'type' => 'string'], ], 'required' => [], 'type' => 'object', ], static fn (array $args): bool => current_user_can('upload_files') || current_user_can('edit_posts'), static fn (array $args): array => self::search($args), ), new Ability( 'sarcio/featured-image-set', __('Set a page’s featured image', 'sarcio'), Ability::WRITE, 'Set the featured image of a page, post or product to an image from the media library (imageId from media-search), or remove it with imageId 0.', ['properties' => ['id' => $idSchema, 'imageId' => ['minimum' => 0, 'type' => 'integer']], 'required' => ['id', 'imageId'], 'type' => 'object'], static fn (array $args): bool => isset($args['id']) ? current_user_can('edit_post', (int) $args['id']) : current_user_can('edit_posts'), static fn (array $args): array => self::setFeatured($args), null, static fn (array $before, array $after): array => self::writeFeatured((int) $before['id'], (int) $before['imageId']) + ['restored' => true], false, static fn (array $args, Overlay $overlay): array => self::stageFeatured($args, $overlay), ), new Ability( 'sarcio/image-alt-update', __('Change an image’s alt text', 'sarcio'), Ability::WRITE, 'Set the alt text of an image in the media library (imageId from media-search): what screen readers and search engines read instead of the picture.', ['properties' => ['alt' => ['maxLength' => 500, 'type' => 'string'], 'imageId' => $idSchema], 'required' => ['imageId', 'alt'], 'type' => 'object'], static fn (array $args): bool => isset($args['imageId']) ? current_user_can('edit_post', (int) $args['imageId']) : current_user_can('upload_files'), static fn (array $args): array => self::setAlt($args), null, static fn (array $before, array $after): array => self::writeAlt((int) $before['imageId'], (string) $before['alt']) + ['restored' => true], false, static fn (array $args, Overlay $overlay): array => self::stageAlt($args, $overlay), ), new Ability( 'sarcio/images-without-alt', __('Find images without alt text', 'sarcio'), Ability::READ, 'Images in the media library with no alt text (up to 100): id, title and URL, so alt text can be written for each with image-alt-bulk.', ['properties' => [], 'required' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('upload_files'), static fn (array $args): array => ['images' => HealthAbilities::imagesWithoutAlt()], ), new Ability( 'sarcio/image-alt-bulk', __('Set alt text on several images', 'sarcio'), Ability::WRITE, 'Set the alt text of up to 50 images at once: items of {imageId, alt}. Previewed first; undo puts every previous alt text back.', ['properties' => ['items' => ['items' => ['properties' => ['alt' => ['maxLength' => 500, 'type' => 'string'], 'imageId' => $idSchema], 'required' => ['imageId', 'alt'], 'type' => 'object'], 'maxItems' => 50, 'minItems' => 1, 'type' => 'array']], 'required' => ['items'], 'type' => 'object'], static fn (array $args): bool => current_user_can('upload_files'), static fn (array $args): array => self::setAltBulk($args), null, static fn (array $before, array $after): array => self::restoreAltBulk($before), false, static fn (array $args, Overlay $overlay): array => self::stageAltBulk($args, $overlay), ), ]; } /** * @param array $args * @return array{0:array,1:array}|array{error:string} before and after by image id */ private static function altChanges(array $args, ?Overlay $overlay): array { $before = []; $after = []; foreach ((array) $args['items'] as $item) { $id = (int) ($item['imageId'] ?? 0); if (!current_user_can('edit_post', $id) || self::image($id) === null) { continue; } $staged = $overlay?->meta[$id][self::ALT_KEY] ?? null; $was = $staged !== null ? (string) $staged : (string) get_post_meta($id, self::ALT_KEY, true); $now = sanitize_text_field((string) ($item['alt'] ?? '')); if ($was === $now) { continue; } $before[$id] = $was; $after[$id] = $now; } if ($after === []) { return ['error' => __('None of those images would change (missing, not yours to edit, or already so).', 'sarcio')]; } return [$before, $after]; } /** @param array $args */ private static function stageAltBulk(array $args, Overlay $overlay): array { $changes = self::altChanges($args, $overlay); if (isset($changes['error'])) { return $changes; } foreach ($changes[1] as $id => $alt) { $overlay->setMeta($id, self::ALT_KEY, $alt); } return [ 'after' => implode("\n", array_map(static fn (int $id, string $alt): string => $id . ': ' . $alt, array_keys($changes[1]), $changes[1])), 'before' => implode("\n", array_map(static fn (int $id, string $alt): string => $id . ': ' . ($alt === '' ? '(none)' : $alt), array_keys($changes[0]), $changes[0])), 'summary' => sprintf( /* translators: %d: how many images. */ _n('Set alt text on %d image', 'Set alt text on %d images', count($changes[1]), 'sarcio'), count($changes[1]), ), ]; } /** @param array $args */ private static function setAltBulk(array $args): array { $changes = self::altChanges($args, null); if (isset($changes['error'])) { return $changes; } foreach ($changes[1] as $id => $alt) { self::writeAlt($id, $alt); } return [ 'changed' => count($changes[1]), 'journal' => [ 'after' => ['alts' => $changes[1]], 'before' => ['alts' => $changes[0]], 'summary' => sprintf( /* translators: %d: how many images. */ _n('Set alt text on %d image', 'Set alt text on %d images', count($changes[1]), 'sarcio'), count($changes[1]), ), 'target' => 'images:' . implode(',', array_keys($changes[1])), ], ]; } /** @param array $before */ private static function restoreAltBulk(array $before): array { $restored = 0; foreach ((array) ($before['alts'] ?? []) as $id => $alt) { if (!isset(self::writeAlt((int) $id, (string) $alt)['error'])) { $restored++; } } return ['restored' => $restored]; } /** @return array */ public static function describe(object $image): array { $meta = function_exists('wp_get_attachment_metadata') ? wp_get_attachment_metadata((int) $image->ID) : null; return [ 'alt' => (string) get_post_meta((int) $image->ID, self::ALT_KEY, true), 'caption' => (string) $image->post_excerpt, 'height' => is_array($meta) ? (int) ($meta['height'] ?? 0) : 0, 'id' => (int) $image->ID, 'mime' => (string) $image->post_mime_type, 'title' => (string) $image->post_title, 'url' => (string) wp_get_attachment_url((int) $image->ID), 'width' => is_array($meta) ? (int) ($meta['width'] ?? 0) : 0, ]; } /** @param array $args */ private static function search(array $args): array { $images = get_posts([ 'numberposts' => (int) ($args['limit'] ?? 10), 'post_mime_type' => 'image', 'post_status' => 'inherit', 'post_type' => 'attachment', 's' => (string) ($args['query'] ?? ''), ]); return ['results' => array_map([self::class, 'describe'], is_array($images) ? array_values(array_filter($images, 'is_object')) : [])]; } private static function image(int $imageId): ?object { $image = $imageId > 0 ? get_post($imageId) : null; return $image !== null && (string) $image->post_type === 'attachment' ? $image : null; } /** * @param array $args * @return array{post:object,image:?object,before:int}|array{error:string} */ private static function featuredChange(array $args): array { $post = get_post((int) $args['id']); if ($post === null) { return ['error' => __('There is no page or post with that id.', 'sarcio')]; } $imageId = (int) $args['imageId']; $image = $imageId > 0 ? self::image($imageId) : null; if ($imageId > 0 && $image === null) { return ['error' => __('There is no image with that id in the media library.', 'sarcio')]; } $before = (int) get_post_meta((int) $post->ID, self::THUMBNAIL_KEY, true); if ($before === $imageId) { return ['error' => __('Nothing would change.', 'sarcio')]; } return ['before' => $before, 'image' => $image, 'post' => $post]; } /** @param array $args */ private static function setFeatured(array $args): array { $change = self::featuredChange($args); if (isset($change['error'])) { return $change; } $id = (int) $change['post']->ID; $written = self::writeFeatured($id, (int) $args['imageId']); if (isset($written['error'])) { return $written; } return [ 'id' => $id, 'imageId' => (int) $args['imageId'], 'journal' => [ 'after' => ['id' => $id, 'imageId' => (int) $args['imageId'], 'imageTitle' => $change['image'] !== null ? (string) $change['image']->post_title : ''], 'before' => ['id' => $id, 'imageId' => $change['before'], 'imageTitle' => $change['before'] > 0 ? (string) get_the_title($change['before']) : ''], 'summary' => (int) $args['imageId'] > 0 ? sprintf( /* translators: 1: the image title, 2: the page title. */ __('Set “%1$s” as the featured image of “%2$s”', 'sarcio'), (string) $change['image']->post_title, (string) $change['post']->post_title, ) : sprintf( /* translators: %s: the page title. */ __('Removed the featured image of “%s”', 'sarcio'), (string) $change['post']->post_title, ), 'target' => 'post:' . $id, ], ]; } /** @param array $args */ private static function stageFeatured(array $args, Overlay $overlay): array { $change = self::featuredChange($args); if (isset($change['error'])) { return $change; } $id = (int) $change['post']->ID; $staged = $overlay->meta[$id][self::THUMBNAIL_KEY] ?? null; $before = $staged !== null ? (int) $staged : $change['before']; $overlay->setMeta($id, self::THUMBNAIL_KEY, (int) $args['imageId'] > 0 ? (string) $args['imageId'] : ''); return [ 'after' => $change['image'] !== null ? (string) $change['image']->post_title . ' · ' . wp_get_attachment_url((int) $args['imageId']) : __('(no featured image)', 'sarcio'), 'before' => $before > 0 ? (string) get_the_title($before) . ' · ' . wp_get_attachment_url($before) : __('(no featured image)', 'sarcio'), 'summary' => sprintf( /* translators: %s: the page title. */ __('Change the featured image of “%s”', 'sarcio'), (string) $change['post']->post_title, ), ]; } private static function writeFeatured(int $id, int $imageId): array { if (get_post($id) === null) { return ['error' => __('That page no longer exists.', 'sarcio')]; } if ($imageId > 0) { update_post_meta($id, self::THUMBNAIL_KEY, (string) $imageId); } else { delete_post_meta($id, self::THUMBNAIL_KEY); } return ['id' => $id, 'imageId' => $imageId]; } /** @param array $args */ private static function setAlt(array $args): array { $image = self::image((int) $args['imageId']); if ($image === null) { return ['error' => __('There is no image with that id in the media library.', 'sarcio')]; } $before = (string) get_post_meta((int) $image->ID, self::ALT_KEY, true); $after = sanitize_text_field((string) $args['alt']); if ($before === $after) { return ['error' => __('Nothing would change.', 'sarcio')]; } self::writeAlt((int) $image->ID, $after); return [ 'alt' => $after, 'imageId' => (int) $image->ID, 'journal' => [ 'after' => ['alt' => $after, 'imageId' => (int) $image->ID], 'before' => ['alt' => $before, 'imageId' => (int) $image->ID], 'summary' => sprintf( /* translators: %s: the image title. */ __('Changed the alt text of “%s”', 'sarcio'), (string) $image->post_title, ), 'target' => 'image:' . $image->ID, ], ]; } /** @param array $args */ private static function stageAlt(array $args, Overlay $overlay): array { $image = self::image((int) $args['imageId']); if ($image === null) { return ['error' => __('There is no image with that id in the media library.', 'sarcio')]; } $staged = $overlay->meta[(int) $image->ID][self::ALT_KEY] ?? null; $before = $staged !== null ? (string) $staged : (string) get_post_meta((int) $image->ID, self::ALT_KEY, true); $after = sanitize_text_field((string) $args['alt']); if ($before === $after) { return ['error' => __('Nothing would change.', 'sarcio')]; } $overlay->setMeta((int) $image->ID, self::ALT_KEY, $after); return [ 'after' => $after, 'before' => $before, 'summary' => sprintf( /* translators: %s: the image title. */ __('Change the alt text of “%s”', 'sarcio'), (string) $image->post_title, ), ]; } private static function writeAlt(int $imageId, string $alt): array { if (self::image($imageId) === null) { return ['error' => __('That image no longer exists.', 'sarcio')]; } if ($alt === '') { delete_post_meta($imageId, self::ALT_KEY); } else { update_post_meta($imageId, self::ALT_KEY, $alt); } return ['alt' => $alt, 'imageId' => $imageId]; } }