definition. * @return mixed */ public static function chosen( $sizes ) { $skip = (array) ( Settings::get()['skip_sizes'] ?? array() ); return is_array( $sizes ) && $skip ? array_diff_key( $sizes, array_flip( $skip ) ) : $sizes; } /** * The registered sizes: name => width, height, crop. * * @return array */ public static function registered(): array { return function_exists( 'wp_get_registered_image_subsizes' ) ? (array) wp_get_registered_image_subsizes() : array(); } /** * The saved check, or an empty array. * * @return array */ public static function state(): array { $s = get_option( self::OPTION, array() ); return is_array( $s ) ? $s : array(); } /** * The sizes one picture should have now that differ from what it has: missing ones are left to WordPress, out-of-date * ones are named. * * @param array $meta The attachment metadata. * @param array $sizes The registered sizes WordPress makes. * @return array Out-of-date size names. */ private static function outdated( array $meta, array $sizes ): array { $out = array(); $w = (int) ( $meta['width'] ?? 0 ); $h = (int) ( $meta['height'] ?? 0 ); foreach ( (array) ( $meta['sizes'] ?? array() ) as $name => $have ) { if ( ! isset( $sizes[ $name ] ) || ! is_array( $have ) || ! $w || ! $h ) { continue; } $d = $sizes[ $name ]; $want = image_resize_dimensions( $w, $h, (int) $d['width'], (int) $d['height'], $d['crop'] ); if ( is_array( $want ) && ( (int) ( $have['width'] ?? 0 ) !== (int) $want[4] || (int) ( $have['height'] ?? 0 ) !== (int) $want[5] ) ) { $out[] = (string) $name; } } return $out; } /** * Start a check, or continue the saved one, for about eight seconds: each picture gets its missing sizes, and its * out-of-date sizes again (the old files kept as backup sizes). * * @param bool $start Start a new check. * @return array The saved check. * @throws \RuntimeException When the check may not run now. */ public static function step( bool $start ): array { $lock = Reviewed_Store::enter(); try { if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'upload_files' ) ) { throw new \RuntimeException( 'sizes_access' ); } $token = Repository::acquire( 'image_sizes', 300 ); if ( ! $token ) { throw new \RuntimeException( 'sizes_busy' ); } try { require_once ABSPATH . 'wp-admin/includes/image.php'; global $wpdb; $s = self::state(); if ( $start ) { $s = array( 'v' => 1, 'started' => time(), 'updated' => time(), 'cursor' => 0, 'checked' => 0, 'pictures' => 0, 'made' => 0, 'remade' => 0, 'failed' => array(), 'unregistered' => array(), 'status' => 'more', ); } elseif ( ! $s || 'more' !== ( $s['status'] ?? '' ) ) { throw new \RuntimeException( 'sizes_stale' ); } $sizes = self::chosen( self::registered() ); $all = self::registered(); $until = microtime( true ) + 8; while ( 'more' === $s['status'] && microtime( true ) < $until ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one bounded batch of a check the administrator starts; it must read the current rows. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM %i WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_mime_type LIKE %s AND ID > %d ORDER BY ID ASC LIMIT 20", $wpdb->posts, 'image/%', (int) $s['cursor'] ) ); if ( ! $ids ) { $s['status'] = 'done'; break; } foreach ( array_map( 'intval', $ids ) as $id ) { $s['cursor'] = $id; ++$s['checked']; $meta = wp_get_attachment_metadata( $id ); if ( ! is_array( $meta ) || ( empty( $meta['sizes'] ) && empty( $meta['width'] ) ) ) { continue; } foreach ( array_keys( (array) ( $meta['sizes'] ?? array() ) ) as $name ) { if ( ! isset( $all[ $name ] ) ) { $entry = $s['unregistered'][ $name ] ?? array( 0, 0 ); $s['unregistered'][ $name ] = array( $entry[0] + 1, $entry[1] + (int) ( $meta['sizes'][ $name ]['filesize'] ?? 0 ) ); } } $old = self::outdated( $meta, $sizes ); $missing = function_exists( 'wp_get_missing_image_subsizes' ) ? array_keys( (array) wp_get_missing_image_subsizes( $id ) ) : array(); if ( ! $old && ! $missing ) { continue; } if ( ! current_user_can( 'edit_post', $id ) || ! is_file( (string) wp_get_original_image_path( $id ) ) ) { $s['failed'][] = $id; continue; } if ( $old ) { $backup = get_post_meta( $id, '_wp_attachment_backup_sizes', true ); $backup = is_array( $backup ) ? $backup : array(); foreach ( $old as $name ) { $backup[ $name . '-afsd-' . time() ] = $meta['sizes'][ $name ]; unset( $meta['sizes'][ $name ] ); } Reviewed_Store::update_post_meta( $id, '_wp_attachment_backup_sizes', $backup ); Reviewed_Store::wp_update_attachment_metadata( $id, $meta ); } $done = wp_update_image_subsizes( $id ); if ( is_wp_error( $done ) ) { $s['failed'][] = $id; continue; } ++$s['pictures']; $s['made'] += count( $missing ); $s['remade'] += count( $old ); } if ( ! Repository::owns( $token, 'image_sizes' ) ) { throw new \RuntimeException( 'sizes_busy' ); } } $s['failed'] = array_slice( array_values( array_unique( array_map( 'intval', $s['failed'] ) ) ), 0, 500 ); $s['updated'] = time(); if ( ! Reviewed_Store::update_option( self::OPTION, $s, false ) && self::state() !== $s ) { throw new \RuntimeException( 'sizes_storage' ); } return $s; } finally { Repository::release( $token, 'image_sizes' ); } } finally { Reviewed_Store::leave( $lock ); } } /** * Words for the check's error codes. * * @param string $code The code. * @return string */ public static function human_error( string $code ): string { $messages = array( 'sizes_access' => __( 'Only an administrator who may upload files can make sizes.', 'affstackdev-image-manager' ), 'sizes_busy' => __( 'Sizes are being made already. Try again in a few minutes.', 'affstackdev-image-manager' ), 'sizes_storage' => __( 'The progress could not be saved.', 'affstackdev-image-manager' ), 'sizes_stale' => __( 'There is nothing to continue. Start again.', 'affstackdev-image-manager' ), ); return $messages[ $code ] ?? ''; } }