absolute path. * The untouched upload kept by WordPress for very large images * ("original_image") is never sent or changed. * * @param int $attachment_id Attachment ID. * @return array */ public static function files_for( $attachment_id ) { $main = get_attached_file( $attachment_id ); if ( ! $main || ! is_file( $main ) || ! self::inside_uploads( $main ) ) { return array(); } $files = array( wp_basename( $main ) => $main ); $dir = trailingslashit( dirname( $main ) ); $meta = wp_get_attachment_metadata( $attachment_id ); if ( ! empty( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) { foreach ( $meta['sizes'] as $size ) { if ( empty( $size['file'] ) ) { continue; } $name = wp_basename( $size['file'] ); if ( ! isset( $files[ $name ] ) && is_file( $dir . $name ) ) { $files[ $name ] = $dir . $name; } } } return $files; } /** * Optimize every file of an attachment. Work already done is skipped, so * the call can be repeated until it returns true. * * @param int $attachment_id Attachment ID. * @param float $deadline Unix time to stop at (0 = no limit). At least one file is always processed. * @return true|string|WP_Error true when finished, 'partial' when out of time, 'locked' when another request has it, * WP_Error when the run should stop (quota, bad key, service down). */ public static function optimize_attachment( $attachment_id, $deadline = 0 ) { $attachment_id = (int) $attachment_id; if ( ! self::is_supported( $attachment_id ) ) { return self::fail( $attachment_id, __( 'This file type is not supported.', 'lean-pixel' ) ); } if ( self::is_excluded( $attachment_id ) ) { update_post_meta( $attachment_id, self::STATUS, 'excluded' ); return true; } $files = self::files_for( $attachment_id ); if ( ! $files ) { return self::fail( $attachment_id, __( 'The image file is missing from the server.', 'lean-pixel' ) ); } if ( ! LeanPixel_Queue::lock( 'att_' . $attachment_id, 5 * MINUTE_IN_SECONDS ) ) { return 'locked'; // Another request is working on this image right now. } $result = self::optimize_locked( $attachment_id, $files, $deadline ); LeanPixel_Queue::unlock( 'att_' . $attachment_id ); return $result; } private static function optimize_locked( $attachment_id, array $files, $deadline ) { $settings = LeanPixel_Settings::all(); $data = self::data( $attachment_id ); $worked = false; foreach ( $files as $name => $path ) { if ( isset( $data['files'][ $name ] ) ) { continue; } if ( $worked && $deadline && microtime( true ) > $deadline ) { update_post_meta( $attachment_id, self::DATA, $data ); return 'partial'; } $record = self::process_file( $path, $settings, wp_basename( get_attached_file( $attachment_id ) ) === $name, self::image_id( $attachment_id ) ); if ( is_wp_error( $record ) ) { if ( in_array( $record->get_error_code(), self::STOP_CODES, true ) ) { update_post_meta( $attachment_id, self::DATA, $data ); return $record; } $record = array( 'before' => (int) filesize( $path ), 'after' => (int) filesize( $path ), 'skip' => $record->get_error_message(), ); } else { LeanPixel_Settings::record_stats( $record, 1 ); } $data['files'][ $name ] = $record; $worked = true; // Saved after every file so an interrupted request loses nothing. update_post_meta( $attachment_id, self::DATA, $data ); } $data['level'] = $settings['level']; $data['time'] = time(); unset( $data['error'] ); $skipped = array(); foreach ( $data['files'] as $record ) { if ( ! empty( $record['skip'] ) ) { $skipped[] = $record['skip']; } } if ( count( $skipped ) === count( $data['files'] ) ) { $data['error'] = $skipped[0]; update_post_meta( $attachment_id, self::DATA, $data ); update_post_meta( $attachment_id, self::STATUS, 'error' ); return true; } update_post_meta( $attachment_id, self::DATA, $data ); update_post_meta( $attachment_id, self::STATUS, 'optimized' ); self::refresh_filesizes( $attachment_id ); return true; } /** * Resize (if asked), compress one file (through the service, or on this * server in local mode) and write back the result. * * @param string $path Absolute path. * @param array $settings Plugin settings. * @param bool $is_main Whether this is the attachment's full-size file (only that one is ever resized). * @param string $image Id shared by every file of the image, so the service counts it once. * @return array|WP_Error Record for the file. */ private static function process_file( $path, array $settings, $is_main = false, $image = '' ) { $type = wp_check_filetype( $path ); $mime = $type['type']; if ( ! in_array( $mime, self::mimes(), true ) ) { return new WP_Error( 'unsupported_type', __( 'This file type is not supported.', 'lean-pixel' ) ); } if ( ! wp_is_writable( $path ) || ! wp_is_writable( dirname( $path ) ) ) { return new WP_Error( 'not_writable', __( 'The file is not writable.', 'lean-pixel' ) ); } // A kept original that is already there (from an interrupted earlier // attempt, or from before a resize was requested) is the true "before". $backup = self::backup_path( $path ); $has_backup = $backup && is_file( $backup ); $before = $has_backup ? (int) filesize( $backup ) : (int) filesize( $path ); $record = array( 'before' => $before, 'after' => (int) filesize( $path ), 'webp' => 0, 'avif' => 0, 'backup' => $has_backup ? 1 : 0, ); // Step 1: scale an oversized full-size image down. $max = $is_main ? (int) $settings['max_dimension'] : 0; if ( $max > 0 && 'image/gif' !== $mime ) { $source = $has_backup ? $backup : $path; $size = wp_getimagesize( $source ); if ( $size && max( $size[0], $size[1] ) > $max && self::can_load( $size[0], $size[1] ) ) { if ( $settings['keep_originals'] && ! $has_backup ) { $kept = self::keep_original( $path, $backup ); if ( is_wp_error( $kept ) ) { return $kept; } $has_backup = true; $record['backup'] = 1; } if ( true === self::resize( $source, $path, $max, $mime ) ) { $record['resized'] = array( (int) $size[0], (int) $size[1] ); } clearstatcache( true, $path ); $record['after'] = (int) filesize( $path ); } } // Step 2: compress. $current = (int) filesize( $path ); $result = empty( $settings['local'] ) ? LeanPixel_API::optimize( $path, $mime, $image ) : LeanPixel_Local::optimize( $path, $mime, $settings ); if ( is_wp_error( $result ) ) { return $result; } $replaced = false; $optimized = self::decode( isset( $result['optimized'] ) ? $result['optimized'] : null ); if ( null !== $optimized && strlen( $optimized ) < $current && self::looks_like( $optimized, $mime ) && self::same_picture( $optimized, $path, $mime ) ) { if ( $settings['keep_originals'] && ! $has_backup ) { $kept = self::keep_original( $path, $backup ); if ( is_wp_error( $kept ) ) { return $kept; } $record['backup'] = 1; } if ( ! self::write( $path, $optimized ) ) { return new WP_Error( 'write_failed', __( 'The optimized file could not be saved.', 'lean-pixel' ) ); } $record['after'] = strlen( $optimized ); $current = $record['after']; $replaced = true; } unset( $optimized ); // The service leaves a file alone when compressing it would gain very // little. Its camera and location details still come out, unless they // are to be kept. (Local mode does this itself.) if ( ! $replaced && 'image/jpeg' === $mime && empty( $settings['keep_metadata'] ) && empty( $settings['local'] ) ) { $stripped = LeanPixel_Local::without_details( $path ); if ( null !== $stripped && strlen( $stripped ) < $current && self::looks_like( $stripped, $mime ) && self::same_picture( $stripped, $path, $mime ) ) { if ( $settings['keep_originals'] && ! $has_backup ) { $kept = self::keep_original( $path, $backup ); if ( is_wp_error( $kept ) ) { return $kept; } $record['backup'] = 1; } if ( self::write( $path, $stripped ) ) { $record['after'] = strlen( $stripped ); } } unset( $stripped ); } foreach ( array( 'webp', 'avif' ) as $format ) { if ( ! $settings[ $format ] ) { continue; } $bytes = self::decode( isset( $result[ $format ] ) ? $result[ $format ] : null ); if ( null !== $bytes && strlen( $bytes ) < $before && self::looks_like( $bytes, 'image/' . $format ) && self::same_picture( $bytes, $path, 'image/' . $format ) && self::write( $path . '.' . $format, $bytes ) ) { $record[ $format ] = strlen( $bytes ); } } return $record; } /** * Copy a file into the kept-originals folder before it is changed. * * @param string $path File about to be changed. * @param string|false $backup Where its copy goes. * @return true|WP_Error */ private static function keep_original( $path, $backup ) { $error = new WP_Error( 'backup_failed', __( 'The original could not be backed up, so the file was left unchanged.', 'lean-pixel' ) ); if ( ! $backup ) { return $error; } if ( is_file( $backup ) ) { return true; } self::protect_backup_dir(); if ( ! wp_mkdir_p( dirname( $backup ) ) || ! copy( $path, $backup ) ) { return $error; } return true; } /** * Whether PHP has room to open an image of this size. Running out of * memory cannot be caught, so an image that will not fit is left at its size. */ public static function can_load( $width, $height ) { wp_raise_memory_limit( 'image' ); $limit = wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) ); if ( $limit <= 0 ) { return true; // No limit set. } // Source and scaled copy in memory at 4 bytes a pixel, plus working room. $needed = $width * $height * 4 * 1.8; return $needed < ( $limit - memory_get_usage( true ) ); } /** * Scale an image so its longest side is $max pixels, using WordPress's own * image editor, and save it over $path. The file name does not change, so * existing links keep working. * * @param string $source File to read (the kept original when there is one). * @param string $path File to replace. * @param int $max Longest side in pixels. * @param string $mime Mime type to save as. * @return true|WP_Error */ private static function resize( $source, $path, $max, $mime ) { $editor = wp_get_image_editor( $source ); if ( is_wp_error( $editor ) ) { return $editor; } if ( method_exists( $editor, 'maybe_exif_rotate' ) ) { $editor->maybe_exif_rotate(); // The scaled copy loses its camera orientation flag, so apply it now. } $editor->set_quality( 90 ); // High, because the service compresses the result next. $scaled = $editor->resize( $max, $max, false ); if ( is_wp_error( $scaled ) ) { return $scaled; } $ext = pathinfo( $path, PATHINFO_EXTENSION ); $tmp = dirname( $path ) . '/' . wp_basename( $path, '.' . $ext ) . '-leanpixel-' . strtolower( wp_generate_password( 8, false, false ) ) . '.' . $ext; $perms = fileperms( $path ) & 0777; $saved = $editor->save( $tmp, $mime ); if ( is_wp_error( $saved ) || empty( $saved['path'] ) || ! is_file( $saved['path'] ) ) { return is_wp_error( $saved ) ? $saved : new WP_Error( 'resize_failed', __( 'The image could not be resized.', 'lean-pixel' ) ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- atomic replace in the uploads folder. if ( ! rename( $saved['path'], $path ) ) { wp_delete_file( $saved['path'] ); return new WP_Error( 'resize_failed', __( 'The image could not be resized.', 'lean-pixel' ) ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- keep the original permissions. chmod( $path, $perms ); return true; } /** * Put the originals back and remove the WebP/AVIF copies. * * @param int $attachment_id Attachment ID. * @return true|WP_Error */ public static function restore_attachment( $attachment_id ) { $attachment_id = (int) $attachment_id; $data = self::data( $attachment_id ); $files = self::files_for( $attachment_id ); // Checked before anything is touched, so a restore is all or nothing. if ( ! self::can_restore( $attachment_id, $data ) ) { return new WP_Error( 'no_backup', __( 'The originals are no longer kept for this image, so it cannot be restored.', 'lean-pixel' ) ); } if ( ! LeanPixel_Queue::lock( 'att_' . $attachment_id, 5 * MINUTE_IN_SECONDS ) ) { return new WP_Error( 'locked', __( 'This image is being processed right now. Please try again in a moment.', 'lean-pixel' ) ); } $result = self::restore_locked( $attachment_id, $data, $files ); LeanPixel_Queue::unlock( 'att_' . $attachment_id ); return $result; } private static function restore_locked( $attachment_id, array $data, array $files ) { foreach ( $data['files'] as $name => $record ) { if ( ! isset( $files[ $name ] ) ) { continue; } $path = $files[ $name ]; $backup = self::backup_path( $path ); if ( ! empty( $record['backup'] ) && $backup && is_file( $backup ) ) { if ( ! copy( $backup, $path ) ) { return new WP_Error( 'restore_failed', __( 'A file could not be restored. Check the folder permissions.', 'lean-pixel' ) ); } wp_delete_file( $backup ); } self::delete_siblings( $path ); LeanPixel_Settings::record_stats( $record, -1 ); } delete_post_meta( $attachment_id, self::DATA ); update_post_meta( $attachment_id, self::STATUS, 'restored' ); self::refresh_filesizes( $attachment_id ); return true; } /** * Remove WebP/AVIF copies and backups when an attachment is deleted. * * @param int $attachment_id Attachment ID. */ public static function on_delete( $attachment_id ) { if ( ! self::is_supported( $attachment_id ) ) { return; } foreach ( self::files_for( $attachment_id ) as $path ) { self::delete_siblings( $path ); $backup = self::backup_path( $path ); if ( $backup && is_file( $backup ) ) { wp_delete_file( $backup ); } } } /** * Whether every file this plugin changed still has its original on disk. * * @param int $attachment_id Attachment ID. * @param array $data The attachment's record. * @return bool */ public static function can_restore( $attachment_id, array $data ) { $main = get_attached_file( $attachment_id ); if ( ! $main || ! $data['files'] ) { return false; } $dir = trailingslashit( dirname( $main ) ); $changed = 0; foreach ( $data['files'] as $name => $record ) { if ( ! isset( $record['before'], $record['after'] ) || ( $record['after'] >= $record['before'] && empty( $record['resized'] ) ) ) { continue; // The file itself was never replaced. } $backup = self::backup_path( $dir . wp_basename( $name ) ); if ( empty( $record['backup'] ) || ! $backup || ! is_file( $backup ) ) { return false; } ++$changed; } return $changed > 0; } /** * Name of the folder (inside uploads) that holds the kept originals. It * carries a random suffix so the copies cannot be found by guessing the * address, which matters on servers that ignore .htaccess files. */ public static function backup_folder() { $name = get_option( self::BACKUP_OPTION ); if ( ! is_string( $name ) || ! preg_match( '/^leanpixel-backup(-[a-z0-9]{12})?$/', $name ) ) { $name = self::OLD_BACKUP_DIR . '-' . strtolower( wp_generate_password( 12, false, false ) ); update_option( self::BACKUP_OPTION, $name, false ); } return $name; } public static function backup_dir() { $uploads = wp_get_upload_dir(); return trailingslashit( $uploads['basedir'] ) . self::backup_folder(); } /** * Block web access to the kept originals. They can hold camera and * location details that the public, optimized copies no longer have. */ public static function protect_backup_dir() { $dir = self::backup_dir(); if ( ! wp_mkdir_p( $dir ) ) { return; } $files = array( '.htaccess' => "\nRequire all denied\n\n\nOrder allow,deny\nDeny from all\n\n", 'index.php' => " $contents ) { if ( ! is_file( $dir . '/' . $name ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- small guard file in uploads. file_put_contents( $dir . '/' . $name, $contents ); } } } /** * Move kept originals from the old, guessable folder name to the new one * and protect them. Runs once after an update. */ public static function migrate_backup_dir() { $uploads = wp_get_upload_dir(); $old = trailingslashit( $uploads['basedir'] ) . self::OLD_BACKUP_DIR; if ( is_dir( $old ) && ! get_option( self::BACKUP_OPTION ) ) { $name = self::backup_folder(); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged -- same filesystem, inside uploads. if ( ! @rename( $old, trailingslashit( $uploads['basedir'] ) . $name ) ) { update_option( self::BACKUP_OPTION, self::OLD_BACKUP_DIR, false ); // Could not move: keep the old name, still protected below. } } if ( is_dir( self::backup_dir() ) ) { self::protect_backup_dir(); } } /** Whether a path really is inside the uploads folder (no "..", symlinks resolved). */ private static function inside_uploads( $path ) { if ( false !== strpos( wp_normalize_path( $path ), '/../' ) ) { return false; } $uploads = wp_get_upload_dir(); $base = realpath( $uploads['basedir'] ); $real = realpath( $path ); return $base && $real && 0 === strpos( wp_normalize_path( $real ), trailingslashit( wp_normalize_path( $base ) ) ); } /** * Disk space used by the kept originals. * * @param float $seconds Stop counting after this long on very large folders. * @return array { bytes, files, complete } */ public static function backup_usage( $seconds = 1.5 ) { $usage = array( 'bytes' => 0, 'files' => 0, 'complete' => true, ); $dir = self::backup_dir(); if ( ! is_dir( $dir ) ) { return $usage; } $stop = microtime( true ) + $seconds; $walk = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ) ); foreach ( $walk as $file ) { if ( $file->isFile() && ! in_array( $file->getFilename(), array( '.htaccess', 'index.php' ), true ) ) { $usage['bytes'] += $file->getSize(); ++$usage['files']; } if ( 0 === $usage['files'] % 200 && microtime( true ) > $stop ) { $usage['complete'] = false; break; } } return $usage; } /** * Delete the kept originals. Works in slices so a very large folder * cannot time out; call again until 'done' is true. * * @param float $deadline Unix time to stop at. * @return array { done, deleted } */ public static function delete_backups( $deadline ) { $dir = self::backup_dir(); $deleted = 0; if ( ! is_dir( $dir ) ) { return array( 'done' => true, 'deleted' => 0, ); } $walk = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $walk as $item ) { if ( $item->isDir() && ! $item->isLink() ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged -- empty folder inside uploads. @rmdir( $item->getPathname() ); } else { wp_delete_file( $item->getPathname() ); ++$deleted; } if ( 0 === $deleted % 100 && microtime( true ) > $deadline ) { return array( 'done' => false, 'deleted' => $deleted, ); } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged -- empty folder inside uploads. @rmdir( $dir ); return array( 'done' => true, 'deleted' => $deleted, ); } /** * Find optimized images whose full-size file is larger than the resize * setting and queue them again, so the resize is applied. Only the * full-size file is redone; thumbnails are left as they are. * * @param int $after_id Continue after this attachment ID (0 to start). * @param float $deadline Unix time to stop at. * @return array { done, cursor, queued } */ public static function requeue_oversized( $after_id, $deadline ) { global $wpdb; $max = (int) LeanPixel_Settings::get( 'max_dimension' ); $queued = 0; if ( $max <= 0 ) { return array( 'done' => true, 'cursor' => 0, 'queued' => 0, ); } do { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- paging through this plugin's own records. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s AND post_id > %d ORDER BY post_id LIMIT 200", self::STATUS, 'optimized', $after_id ) ); foreach ( $ids as $id ) { $id = (int) $id; $after_id = $id; $meta = wp_get_attachment_metadata( $id ); $main = get_attached_file( $id ); if ( ! $main || ! is_array( $meta ) || empty( $meta['width'] ) || empty( $meta['height'] ) || 'image/gif' === get_post_mime_type( $id ) ) { continue; } if ( max( (int) $meta['width'], (int) $meta['height'] ) <= $max ) { continue; } $data = self::data( $id ); $name = wp_basename( $main ); if ( isset( $data['files'][ $name ] ) ) { LeanPixel_Settings::record_stats( $data['files'][ $name ], -1 ); unset( $data['files'][ $name ] ); update_post_meta( $id, self::DATA, $data ); } update_post_meta( $id, self::STATUS, 'queued' ); ++$queued; } } while ( count( $ids ) === 200 && microtime( true ) < $deadline ); return array( 'done' => count( $ids ) < 200, 'cursor' => $after_id, 'queued' => $queued, ); } /** Totals for one attachment: [before, after, files, webp count, avif count]. */ public static function totals( array $data ) { $t = array( 'before' => 0, 'after' => 0, 'files' => 0, 'webp' => 0, 'avif' => 0, ); foreach ( $data['files'] as $record ) { if ( ! empty( $record['skip'] ) ) { continue; } $t['before'] += (int) $record['before']; $t['after'] += (int) $record['after']; $t['files']++; $t['webp'] += empty( $record['webp'] ) ? 0 : 1; $t['avif'] += empty( $record['avif'] ) ? 0 : 1; } return $t; } /* ---------------------------------------------------------------- */ private static function fail( $attachment_id, $message ) { $data = self::data( $attachment_id ); $data['error'] = $message; update_post_meta( $attachment_id, self::DATA, $data ); update_post_meta( $attachment_id, self::STATUS, 'error' ); return true; } /** * Where the untouched copy of a file is kept: * uploads//. * * @param string $path Absolute path inside the uploads folder. * @return string|false */ public static function backup_path( $path ) { $uploads = wp_get_upload_dir(); $base = trailingslashit( wp_normalize_path( $uploads['basedir'] ) ); $path = wp_normalize_path( $path ); if ( 0 !== strpos( $path, $base ) || false !== strpos( $path, '/../' ) ) { return false; } return $base . self::backup_folder() . '/' . substr( $path, strlen( $base ) ); } private static function delete_siblings( $path ) { foreach ( array( '.webp', '.avif' ) as $ext ) { if ( is_file( $path . $ext ) ) { wp_delete_file( $path . $ext ); } } } private static function decode( $part ) { if ( ! is_array( $part ) ) { return null; } if ( isset( $part['bytes'] ) && is_string( $part['bytes'] ) ) { $bytes = $part['bytes']; // Bundle reply: already raw. } elseif ( ! empty( $part['data'] ) && is_string( $part['data'] ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- image bytes from the service. $bytes = base64_decode( $part['data'], true ); } else { return null; } if ( false === $bytes || '' === $bytes ) { return null; } if ( isset( $part['size'] ) && (int) $part['size'] !== strlen( $bytes ) ) { return null; } return $bytes; } /** Check the first bytes so only real image data is ever written. */ private static function looks_like( $bytes, $mime ) { switch ( $mime ) { case 'image/jpeg': return 0 === strpos( $bytes, "\xFF\xD8\xFF" ); case 'image/png': return 0 === strpos( $bytes, "\x89PNG\r\n\x1A\n" ); case 'image/gif': return 0 === strpos( $bytes, 'GIF8' ); case 'image/webp': return 'RIFF' === substr( $bytes, 0, 4 ) && 'WEBP' === substr( $bytes, 8, 4 ); case 'image/avif': return 'ftyp' === substr( $bytes, 4, 4 ) && in_array( substr( $bytes, 8, 4 ), array( 'avif', 'avis' ), true ); } return false; } /** * The service is trusted to compress, not to decide what the picture is: * what comes back must decode as the expected format and have exactly the * dimensions of the file it replaces or accompanies. */ private static function same_picture( $bytes, $path, $mime ) { $original = wp_getimagesize( $path ); if ( ! $original ) { return false; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- returns false on undecodable data. $returned = @getimagesizefromstring( $bytes ); if ( ! $returned ) { // PHP before 8.1 cannot read AVIF dimensions; the header check has already passed. return 'image/avif' === $mime && version_compare( PHP_VERSION, '8.1', '<' ); } return $returned['mime'] === $mime && (int) $returned[0] === (int) $original[0] && (int) $returned[1] === (int) $original[1]; } /** Write through a temporary file so a failed write never leaves a broken image. */ private static function write( $path, $bytes ) { $tmp = $path . '.' . strtolower( wp_generate_password( 8, false, false ) ) . '.leanpixel-tmp'; $perms = is_file( $path ) ? ( fileperms( $path ) & 0777 ) : ( fileperms( dirname( $path ) ) & 0666 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- uploads folder. if ( strlen( $bytes ) !== file_put_contents( $tmp, $bytes ) ) { wp_delete_file( $tmp ); return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- atomic replace in the uploads folder. if ( ! rename( $tmp, $path ) ) { wp_delete_file( $tmp ); return false; } if ( $perms ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- keep the original permissions. chmod( $path, $perms ); } clearstatcache( true, $path ); return true; } /** Keep the file sizes WordPress stores in the attachment metadata accurate. */ private static function refresh_filesizes( $attachment_id ) { $meta = wp_get_attachment_metadata( $attachment_id ); if ( ! is_array( $meta ) ) { return; } $files = self::files_for( $attachment_id ); $main = get_attached_file( $attachment_id ); $changed = false; clearstatcache(); if ( isset( $meta['filesize'] ) && $main && is_file( $main ) ) { $meta['filesize'] = (int) filesize( $main ); $changed = true; } // A resize, or restoring from one, changes the full-size dimensions. $size = ( $main && is_file( $main ) ) ? wp_getimagesize( $main ) : false; if ( $size && isset( $meta['width'], $meta['height'] ) && ( (int) $meta['width'] !== (int) $size[0] || (int) $meta['height'] !== (int) $size[1] ) ) { $meta['width'] = (int) $size[0]; $meta['height'] = (int) $size[1]; $changed = true; } if ( ! empty( $meta['sizes'] ) ) { foreach ( $meta['sizes'] as $key => $size ) { $name = isset( $size['file'] ) ? wp_basename( $size['file'] ) : ''; if ( isset( $size['filesize'], $files[ $name ] ) ) { $meta['sizes'][ $key ]['filesize'] = (int) filesize( $files[ $name ] ); $changed = true; } } } if ( $changed ) { wp_update_attachment_metadata( $attachment_id, $meta ); } } }