Last active
February 16, 2025 23:05
-
-
Save ccbayer/4f049262311d44a4265f2e45c62255e3 to your computer and use it in GitHub Desktop.
WordPress Alt Image Importer / Exporter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| document.addEventListener('DOMContentLoaded', () => { | |
| const exportButton = document.querySelector('#export-media-csv'); | |
| if (exportButton) { | |
| exportButton.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const url = `${ajaxurl}?action=export_media_csv`; | |
| // if URL is returned, generate a hidden button that JS clicks to download the CSV | |
| if (url) { | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.style.display = 'none'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } else { | |
| console.error('CSV not generated'); | |
| } | |
| }); | |
| } | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Alt Text tools for the theme. | |
| * | |
| * This file is for processing / batch uploading alt text for images | |
| * | |
| * @link https://developer.wordpress.org/themes/basics/template-tags/ | |
| * @package YourTheme | |
| */ | |
| namespace YourThemne\AltTools; | |
| /** | |
| * Handle a CSV file upload and update alt text for media items in WordPress using WP_Filesystem. | |
| * | |
| * The CSV file must have the following headings: | |
| * ID, Title, _wp_attachment_image_alt, _wp_attached_file, URL | |
| */ | |
| add_action( | |
| 'admin_menu', | |
| function () { | |
| add_submenu_page( | |
| 'tools.php', // Parent menu slug | |
| 'Update Media Alt Text', // Page title | |
| 'Update Media Alt Text', // Menu title | |
| 'manage_options', // Capability | |
| 'update-media-alt-text', // Menu slug | |
| __NAMESPACE__ . '\render_update_media_alt_text_page' // Callback function | |
| ); | |
| } | |
| ); | |
| /** | |
| * Renders the markup for the Admin page. Includes a form and buttons to process the | |
| * upload, and export existing media library. | |
| */ | |
| function render_update_media_alt_text_page() { | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| return; | |
| } | |
| if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated | |
| if ( ! isset( $_POST['update_alt_nonce'] ) || ! wp_verify_nonce( $_POST['update_alt_nonce'], 'update_media_alt_text' ) ) { | |
| wp_die( 'Unauthorized request' ); | |
| } | |
| if ( isset( $_FILES['csv_file']['error'] ) && UPLOAD_ERR_OK === $_FILES['csv_file']['error'] ) { | |
| if ( isset( $_FILES['csv_file']['tmp_name'] ) ) { | |
| $file_path = $_FILES['csv_file']['tmp_name']; | |
| $updated_items = process_csv_and_update_alt_text( $file_path ); | |
| } else { | |
| echo '<div class="notice notice-warning"><p>No file was found / uploaded.</p></div>'; | |
| return; | |
| } | |
| if ( ! empty( $updated_items ) ) { | |
| echo '<div class="notice notice-success"><p>The following media items were updated:</p>'; | |
| echo '<table class="widefat fixed"> | |
| <thead> | |
| <tr> | |
| <th>ID</th> | |
| <th>Title</th> | |
| <th>New Alt Text</th> | |
| <th>Media Link</th> | |
| </tr> | |
| </thead> | |
| <tbody>'; | |
| foreach ( $updated_items as $item ) { | |
| echo '<tr> | |
| <td>' . esc_html( $item['ID'] ) . '</td> | |
| <td>' . esc_html( $item['Title'] ) . '</td> | |
| <td>' . esc_html( $item['AltText'] ) . '</td> | |
| <td><a href="' . esc_url( $item['MediaLink'] ) . '" target="_blank">View Media</a></td> | |
| </tr>'; | |
| } | |
| echo '</tbody></table></div>'; | |
| } else { | |
| echo '<div class="notice notice-warning"><p>No media items were updated.</p></div>'; | |
| } | |
| } else { | |
| echo '<div class="notice notice-error"><p>There was an error uploading the file.</p></div>'; | |
| } | |
| } | |
| ?> | |
| <div class="wrap"> | |
| <h1>Update Media Alt Text</h1> | |
| <p> | |
| Upload a CSV file to update the alt text for media items in your library. | |
| The file must include the following columns: <code>ID</code>, <code>Title</code>, <code>_wp_attachment_image_alt</code>, <code>_wp_attached_file</code> and <code>URL</code>. | |
| <p>Alt text will only be updated if the media ID and file name match and if the current alt text is empty. Ensure your file is correctly formatted before uploading.</p> | |
| <form method="post" enctype="multipart/form-data"> | |
| <?php wp_nonce_field( 'update_media_alt_text', 'update_alt_nonce' ); ?> | |
| <label for="csv_file">Upload CSV File:</label> | |
| <input type="file" id="csv_file" name="csv_file" accept=".csv"> | |
| <button type="submit" class="button button-primary">Upload and Update</button> | |
| </form> | |
| <hr/> | |
| <h2>Export Existing Media</h2> | |
| <p>Click the button below to download a CSV file containing all media items in your library.<p> | |
| <p>This file will include columns for <code>ID</code>, <code>Title</code>, <code>_wp_attachment_image_alt</code>, <code>_wp_attached_file</code>, and <code>URL</code>. You can populate the _wp_attachment_image_alt column with the desired alt text and re-upload the file to update media items. | |
| <form method="post"> | |
| <input type="hidden" name="export_csv" value="1"> | |
| <button id="export-media-csv" class="button">Export Media as CSV</button> | |
| </form> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * Reads the uploaded CSV, and processes each row | |
| * if the row matches a media item (ID / Name) **and** it has no alt, | |
| * the script will copy the alt field from the CSV into the media items alt text field. | |
| * | |
| * @param string $file_path - path to the uploaded file | |
| */ | |
| function process_csv_and_update_alt_text( $file_path ) { | |
| global $wp_filesystem; | |
| if ( ! function_exists( 'WP_Filesystem' ) ) { | |
| require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| } | |
| WP_Filesystem(); | |
| // Read the file contents | |
| $file_contents = $wp_filesystem->get_contents( $file_path ); | |
| if ( false === $file_contents ) { | |
| return []; // If file can't be read, return empty array | |
| } | |
| // Split file contents into rows; if rows are empty stop processing | |
| $rows = explode( "\n", $file_contents ); | |
| if ( empty( $rows ) ) { | |
| return []; | |
| } | |
| // Extract the header row; If header is invalid, stop processing | |
| $header = str_getcsv( array_shift( $rows ) ); | |
| if ( ! validate_csv_header( $header ) ) { | |
| return []; | |
| } | |
| $updated_items = []; | |
| // Process each row while skipping empty rows | |
| foreach ( $rows as $row ) { | |
| if ( empty( trim( $row ) ) ) { | |
| continue; | |
| } | |
| $data = str_getcsv( $row ); | |
| // Ensure the row has the same number of elements as the header | |
| if ( count( $data ) !== count( $header ) ) { | |
| continue; | |
| } | |
| $row_data = array_combine( $header, $data ); | |
| // Check if the media item exists | |
| $attachment_id = intval( $row_data['ID'] ); | |
| $new_alt_text = sanitize_text_field( $row_data['_wp_attachment_image_alt'] ); | |
| $attached_file = sanitize_text_field( $row_data['_wp_attached_file'] ); | |
| // If it does, skip if alt text is empty | |
| if ( empty( $new_alt_text ) ) { | |
| continue; | |
| } | |
| $current_file = get_post_meta( $attachment_id, '_wp_attached_file', true ); | |
| $current_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); | |
| // Only update alt text if the ID and file match and the alt text is empty | |
| if ( $attached_file === $current_file && empty( $current_alt ) ) { | |
| update_post_meta( $attachment_id, '_wp_attachment_image_alt', $new_alt_text ); | |
| $updated_items[] = [ | |
| 'ID' => $attachment_id, | |
| 'Title' => get_the_title( $attachment_id ), | |
| 'AltText' => $new_alt_text, | |
| 'MediaLink' => get_edit_post_link( $attachment_id ), | |
| ]; | |
| } | |
| } | |
| return $updated_items; | |
| } | |
| /** | |
| * Validates the format of the CSV, expecting these headers: | |
| * 'ID', 'Title', '_wp_attachment_image_alt', '_wp_attached_file', 'URL' | |
| * | |
| * @param array $header - the header of the CSV | |
| * @return bool true if the header matches, false otherwise | |
| */ | |
| function validate_csv_header( $header ) { | |
| $required_columns = [ 'ID', 'Title', '_wp_attachment_image_alt', '_wp_attached_file', 'URL' ]; | |
| return ! array_diff( $required_columns, $header ); | |
| } | |
| /** | |
| * AJAX handler to export media as CSV | |
| */ | |
| function export_media_csv() { | |
| // Check user permissions | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| wp_die( 'Unauthorized', 403 ); | |
| } | |
| // Generate CSV content | |
| $args = [ | |
| 'post_type' => 'attachment', | |
| 'post_status' => 'inherit', | |
| 'posts_per_page' => -1, // phpcs:ignore WordPress.WP.PostsPerPageNoUnlimited.posts_per_page_posts_per_page | |
| ]; | |
| $attachments = get_posts( $args ); | |
| if ( empty( $attachments ) ) { | |
| wp_die( 'No media items found.', 404 ); | |
| } | |
| // Start output buffering | |
| ob_start(); | |
| // Output CSV headers | |
| echo "ID,Title,_wp_attachment_image_alt,_wp_attached_file,URL\n"; | |
| foreach ( $attachments as $attachment ) { | |
| $id = $attachment->ID; | |
| $title = str_replace( ',', ' ', $attachment->post_title ); | |
| $alt_text = str_replace( ',', ' ', get_post_meta( $id, '_wp_attachment_image_alt', true ) ); | |
| $file = str_replace( ',', ' ', get_post_meta( $id, '_wp_attached_file', true ) ); | |
| $url = wp_get_attachment_url( $id ); | |
| echo "{$id},{$title},{$alt_text},{$file},{$url}\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| } | |
| // Send the CSV to the browser | |
| header( 'Content-Type: text/csv' ); | |
| header( 'Content-Disposition: attachment; filename="media-export.csv"' ); | |
| header( 'Content-Length: ' . ob_get_length() ); | |
| // Flush output buffer | |
| ob_end_flush(); | |
| exit; | |
| } | |
| add_action( 'wp_ajax_export_media_csv', __NAMESPACE__ . '\export_media_csv' ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment