mark_attachment_client_compressed( $attach_id, $original_size ); } /** * AJAX: mark an attachment as squeezed after client-side precompress when * after-upload thumb squeeze was skipped (e.g. huge images). * Optionally writes the full-size sidecar WebP (auto_webp / sidecar mode only). * * @return void */ public function ajax_mark_client_compressed() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !current_user_can( 'upload_files' ) ) { wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) ); } $attach_id = ( isset( $_POST['attachmentID'] ) ? (int) $_POST['attachmentID'] : 0 ); if ( $attach_id <= 0 ) { wp_send_json_error( '❌ ' . esc_html__( 'Attachment not found', 'squeeze' ) ); } $mime = get_post_mime_type( $attach_id ); if ( !$mime || strpos( $mime, 'image/' ) !== 0 ) { wp_send_json_error( '❌ ' . esc_html__( 'Invalid image format', 'squeeze' ) ); } $original_size = ( isset( $_POST['originalSize'] ) ? (int) $_POST['originalSize'] : 0 ); $this->mark_attachment_client_compressed( $attach_id, $original_size ); $webp_written = false; $base64_webp = ( isset( $_POST['base64Webp'] ) ? sanitize_text_field( wp_unslash( $_POST['base64Webp'] ) ) : '' ); if ( $base64_webp !== '' ) { $webp_written = $this->maybe_write_full_sidecar_webp( $attach_id, $base64_webp ); } wp_send_json_success( array( 'message' => '✅ ' . esc_html__( 'Squeezed on upload', 'squeeze' ), 'webp_written' => $webp_written, ) ); } /** * Write only the full-size squeeze-webp sidecar (no thumb WebPs, no metadata regen). * Used when after-upload thumb squeeze is skipped for huge images in sidecar mode. * * @param int $attach_id Attachment ID. * @param string $base64_webp Base64 (or data-URL) WebP payload from the browser. * @return bool True when a sidecar file was written. */ public function maybe_write_full_sidecar_webp( $attach_id, $base64_webp ) { $attach_id = (int) $attach_id; if ( $attach_id <= 0 || $base64_webp === '' ) { return false; } // Sidecar / auto_webp only — Direct WebP already uploaded as .webp. if ( !self::$SqueezeHelpers->get_option( 'auto_webp' ) || self::$SqueezeHelpers->get_option( 'direct_webp' ) ) { return false; } $file = get_attached_file( $attach_id ); if ( !$file || !file_exists( $file ) ) { return false; } $mime = get_post_mime_type( $attach_id ); // Only JPEG/PNG get squeeze-webp sidecars; WebP/AVIF originals do not. if ( !$mime || !in_array( $mime, array('image/jpeg', 'image/png'), true ) ) { return false; } $upload_path = trailingslashit( dirname( $file ) ); $filename = basename( $file ); $result = self::$SqueezeHelpers->upload_webp( $upload_path, $base64_webp, $filename ); if ( is_wp_error( $result ) ) { return false; } return true; } /** * Persist squeeze_is_compressed (+ optional size savings) for a client-precompressed image. * * @param int $attach_id Attachment ID. * @param int $original_size Pre-compress byte size (0 to skip savings meta). * @return void */ public function mark_attachment_client_compressed( $attach_id, $original_size = 0 ) { $attach_id = (int) $attach_id; if ( $attach_id <= 0 ) { return; } $already = (bool) get_post_meta( $attach_id, 'squeeze_is_compressed', true ); if ( !$already ) { update_post_meta( $attach_id, 'squeeze_is_compressed', true ); $uncompressed_images = self::$SqueezeHelpers->get_stats_option( 'uncompressed_images' ); if ( $uncompressed_images > 0 ) { $uncompressed_images--; } update_option( 'squeeze_stats', array( 'uncompressed_images' => $uncompressed_images, ) ); do_action( 'squeeze_successful_squeeze' ); } $original_size = (int) $original_size; $attached_file = get_attached_file( $attach_id ); $after_size = ( $attached_file && file_exists( $attached_file ) ? (int) wp_filesize( $attached_file ) : 0 ); if ( $original_size > 0 && $after_size > 0 ) { self::$SqueezeHelpers->save_attachment_size_meta( $attach_id, $original_size, $after_size ); } } public function update_attachment() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !current_user_can( 'upload_files' ) ) { wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) ); } if ( !isset( $_POST["base64"] ) || empty( $_POST["base64"] ) ) { wp_send_json_error( '❌ ' . esc_html__( 'No image data found', 'squeeze' ) ); } $base64 = sanitize_text_field( wp_unslash( $_POST["base64"] ) ); $sizes = ( isset( $_POST["base64Sizes"] ) ? (array) $_POST["base64Sizes"] : array() ); // DO NOT SANITIZE because it's an array $base64_webp = ( isset( $_POST["base64Webp"] ) ? sanitize_text_field( wp_unslash( $_POST["base64Webp"] ) ) : '' ); $sizes_webp = ( isset( $_POST["base64SizesWebp"] ) ? (array) $_POST["base64SizesWebp"] : array() ); $file_format = ( isset( $_POST["format"] ) ? sanitize_text_field( wp_unslash( $_POST["format"] ) ) : '' ); $filename = ( isset( $_POST["filename"] ) ? sanitize_file_name( wp_unslash( $_POST["filename"] ) ) : '' ); $extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); // handle jpg/jpeg extension if ( $extension === 'jpeg' ) { $extension = 'jpg'; } $file_format = strtolower( (string) $file_format ); // JS sends MIME subtype "jpeg"; allowlist key is "jpg". if ( $file_format === 'jpeg' ) { $file_format = 'jpg'; } $original_file = ( isset( $_FILES['originalFile'] ) ? $_FILES['originalFile'] : null ); $original_size_posted = ( isset( $_POST['originalSize'] ) ? (int) $_POST['originalSize'] : 0 ); // Validate against the hardcoded constant — never trust user-writable options for security decisions. $allowed_extensions = array_keys( self::ALLOWED_IMAGE_FORMATS ); if ( !in_array( $extension, $allowed_extensions, true ) || !in_array( $file_format, $allowed_extensions, true ) || empty( $file_format ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Invalid image format', 'squeeze' ) ); } $attach_id = ( isset( $_POST["attachmentID"] ) ? (int) $_POST["attachmentID"] : 0 ); $meta_data = wp_get_attachment_metadata( $attach_id ); $url = ( isset( $_POST["url"] ) ? sanitize_text_field( $_POST["url"] ) : '' ); // sanitize_url() replaces spaces with %20, so we use sanitize_text_field() instead $process = ( isset( $_POST["process"] ) ? sanitize_text_field( $_POST["process"] ) : '' ); // process: all, uncompressed, path // Single Page Squeeze posts process=path for the whole queue, including // Media Library attachments. Those need the library pipeline. if ( $process === 'path' && $attach_id > 0 ) { $process = 'all'; } if ( empty( $attach_id ) && $process !== 'path' || empty( $url ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Attachment not found', 'squeeze' ) ); } $excluded_images = self::$SqueezeHelpers->get_excluded_images(); if ( $is_excluded = self::$SqueezeHelpers->is_excluded_image( $url, $excluded_images ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Attachment is excluded from compression', 'squeeze' ) . ' (' . esc_html__( 'found substring: ', 'squeeze' ) . $is_excluded['exclude_reason'] . ')' ); } $is_backup_original = self::$SqueezeHelpers->get_option( 'backup_original' ); $is_direct_webp = self::$SqueezeHelpers->get_option( 'direct_webp' ); // Upload path. $upload_path = self::$SqueezeHelpers->get_upload_path( $attach_id, $filename, $url ); $decoded = self::$SqueezeHelpers->decode_base64_image( $base64, $file_format ); $webp_file_path = ''; $old_metadata = wp_get_attachment_metadata( $attach_id ); $old_filename = $filename; $source_abspath = self::$SqueezeHelpers->resolve_media_url_to_abspath( $url ); $did_direct_rename = false; // Direct WebP: write sibling .webp (library or path/bulk-from-page) and reclaim orphans. if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) { $desired_webp = sanitize_file_name( preg_replace( '/\\.[^.]+$/', '.webp', $filename ) ); if ( $process !== 'path' && $attach_id > 0 ) { $file = get_attached_file( $attach_id ); $dirname = ( $file ? dirname( $file ) : untrailingslashit( $upload_path ) ); } else { $dirname = ( $source_abspath !== '' ? dirname( $source_abspath ) : untrailingslashit( $upload_path ) ); } $unique_filename = self::$SqueezeHelpers->get_direct_webp_filename( $dirname, $desired_webp, $attach_id ); $webp_file_path = trailingslashit( $dirname ) . $unique_filename; $filename = basename( $unique_filename ); $url = str_replace( ABSPATH, home_url( '/' ), $webp_file_path ); $upload_path = trailingslashit( $dirname ); $did_direct_rename = true; } if ( $original_file ) { $sizes['original']['original_size'] = $original_file['size']; } elseif ( $original_size_posted > 0 ) { // Pre-upload compression: client sends the pre-compress byte size so savings stay accurate // when the file on disk is already the compressed upload. $sizes['original']['original_size'] = $original_size_posted; } else { $path_for_size = ( $source_abspath !== '' && file_exists( $source_abspath ) ? $source_abspath : $upload_path . $old_filename ); $sizes['original']['original_size'] = ( file_exists( $path_for_size ) ? wp_filesize( $path_for_size ) : 0 ); } $sizes['original']['compressed_size'] = strlen( $decoded ); // check if compressed_size is greater than original_size if ( $sizes['original']['compressed_size'] > $sizes['original']['original_size'] ) { if ( $attach_id && $process !== 'path' ) { update_post_meta( $attach_id, 'squeeze_compression_failed', 'larger_than_original' ); } wp_send_json_error( '❌ ' . esc_html__( 'Compressed image size is greater than original size.', 'squeeze' ) . ' ' . sprintf( __( 'Please try to change your compression settings by decreasing the quality or compression level.', 'squeeze' ), self::$SETTINGS_URL . '#squeeze_' . $file_format ) ); } if ( $is_backup_original ) { // backup original (Media Library and Directory Squeeze / path) if ( $original_file ) { // Client sent pre-squeeze original (often WebP for Direct WebP) → bak beside new live name. $backup_original_image = self::$SqueezeHelpers->backup_original_image( $upload_path, $filename, $original_file['tmp_name'] ); } elseif ( $did_direct_rename && $old_filename !== $filename ) { // Fallback when the client did not send originalFile: copy the on-disk JPG/PNG twin. // Prefer .bak.webp via client convertFileToWebp (branch above) — same as Media Library. $backup_source = ( is_string( $source_abspath ) && $source_abspath !== '' && file_exists( $source_abspath ) ? $source_abspath : $upload_path . $old_filename ); $backup_dir = trailingslashit( dirname( $backup_source ) ); $backup_name = wp_basename( $backup_source ); $backup_original_image = self::$SqueezeHelpers->backup_original_image( $backup_dir, $backup_name ); } else { $backup_original_image = self::$SqueezeHelpers->backup_original_image( $upload_path, $filename ); } if ( is_wp_error( $backup_original_image ) ) { wp_send_json_error( $backup_original_image->get_error_message() ); } } // Save the image in the uploads directory. $upload_image = self::$SqueezeHelpers->upload_image( $upload_path, $filename, $decoded ); if ( is_wp_error( $upload_image ) ) { wp_send_json_error( $upload_image->get_error_message() ); } if ( $base64_webp ) { $upload_webp = self::$SqueezeHelpers->upload_webp( $upload_path, $base64_webp, $filename ); if ( is_wp_error( $upload_webp ) ) { wp_send_json_error( $upload_webp->get_error_message() ); } $upload_webp_thumbs = self::$SqueezeHelpers->upload_webp_thumbs( $upload_path, $sizes_webp ); // skip handling errors for webp thumbs, because they are not always required } // upload thumbnails if ( $process !== 'path' ) { if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) { update_attached_file( $attach_id, $webp_file_path ); // update the _wp_attached_file meta value to the new webp file path wp_update_post( [ 'ID' => $attach_id, 'post_mime_type' => 'image/webp', ] ); $metadata = wp_generate_attachment_metadata( $attach_id, $webp_file_path ); wp_update_attachment_metadata( $attach_id, $metadata ); } $sizes = self::$SqueezeHelpers->upload_image_thumbs( $upload_path, $sizes, $file_format, $filename, $attach_id ); //wp_send_json_error( print_r($sizes, true) ); if ( is_wp_error( $sizes ) ) { if ( $attach_id ) { update_post_meta( $attach_id, 'squeeze_compression_failed', 'thumb_upload_failed' ); } wp_send_json_error( $sizes->get_error_message() ); } if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) { // remove webp images from the squeeze-webp directory $this->delete_webp_images( $attach_id, $old_metadata ); // remove original JPG/PNG file if it exists if ( is_array( $old_metadata ) ) { if ( !empty( $old_metadata['sizes'] ) && is_array( $old_metadata['sizes'] ) ) { foreach ( $old_metadata['sizes'] as $size_name => $size_data ) { $old_size_filename = $size_data['file']; wp_delete_file( $upload_path . $old_size_filename ); } } if ( !empty( $old_metadata['file'] ) ) { $old_scaled_filename = basename( $old_metadata['file'] ); wp_delete_file( $upload_path . $old_scaled_filename ); } } $old_original_path = $upload_path . $old_filename; wp_delete_file( $old_original_path ); if ( $is_backup_original ) { // delete old backup file $backup_filename = self::$SqueezeHelpers->create_backup_filename( $old_filename ); $old_backup_path = $upload_path . $backup_filename; wp_delete_file( $old_backup_path ); } } try { $response_msg = self::$SqueezeHelpers->get_comparison_table( $sizes ); } catch ( \Throwable $e ) { if ( $attach_id ) { update_post_meta( $attach_id, 'squeeze_compression_failed', 'comparison_table_error' ); } wp_send_json_error( '❌ ' . esc_html__( 'Failed to build compression comparison.', 'squeeze' ) ); } update_post_meta( $attach_id, "squeeze_is_compressed", true ); delete_post_meta( $attach_id, 'squeeze_compression_failed' ); $had_prior_savings = metadata_exists( 'post', $attach_id, SqueezeHelpers::META_SIZE_BEFORE ); $old_before = ( $had_prior_savings ? (int) get_post_meta( $attach_id, SqueezeHelpers::META_SIZE_BEFORE, true ) : 0 ); $old_after = ( $had_prior_savings ? (int) get_post_meta( $attach_id, SqueezeHelpers::META_SIZE_AFTER, true ) : 0 ); $old_saved = max( 0, $old_before - $old_after ); $size_totals = self::$SqueezeHelpers->sum_size_totals( $sizes ); self::$SqueezeHelpers->save_attachment_size_meta( $attach_id, $size_totals['before'], $size_totals['after'] ); $new_saved = max( 0, $size_totals['before'] - $size_totals['after'] ); $response_msg = '✅ ' . esc_html__( 'Squeezed successfully', 'squeeze' ) . '! ' . $response_msg; $uncompressed_images = self::$SqueezeHelpers->get_stats_option( 'uncompressed_images' ); $uncompressed_images--; update_option( 'squeeze_stats', array( 'uncompressed_images' => $uncompressed_images, ) ); /** * Fires after Squeeze has written all compressed/WebP files for an attachment. * Used by integrations such as WP Offload Media to push the new files to an * external storage provider (S3, GCS, DigitalOcean Spaces, etc.). * * @since 1.7.16 * @param int $attach_id Attachment post ID. */ do_action( 'squeeze_after_update_attachment', $attach_id ); do_action( 'squeeze_successful_squeeze' ); $response_data = array( 'message' => $response_msg, 'sizes' => $sizes, 'filename' => $filename, 'url' => $url, 'persisted_savings' => true, 'savings_is_new' => !$had_prior_savings, 'savings_delta' => $new_saved - $old_saved, ); wp_send_json_success( $response_data ); } else { // Path/bulk: after Direct WebP write, remove the JPG/PNG twin and any leftover sidecar. if ( $did_direct_rename && $old_filename !== $filename ) { // Prefer the resolved source path (same file the client fetched). Reconstructed // upload_path + basename can miss when URL→path mapping differs by separators // or when get_upload_path() disagreed with resolve_media_url_to_abspath(). $old_candidates = array(); if ( is_string( $source_abspath ) && $source_abspath !== '' ) { $old_candidates[] = wp_normalize_path( $source_abspath ); } $old_candidates[] = wp_normalize_path( $upload_path . $old_filename ); $old_candidates = array_unique( array_filter( $old_candidates ) ); foreach ( $old_candidates as $old_path ) { if ( !file_exists( $old_path ) ) { continue; } // Never delete the new .webp we just wrote. if ( wp_basename( $old_path ) === wp_basename( $filename ) ) { continue; } wp_delete_file( $old_path ); } $sidecar_seed = ( isset( $old_candidates[0] ) ? $old_candidates[0] : $upload_path . $old_filename ); $sidecar_path = self::$SqueezeHelpers->convert_image_path_to_webp_path( $sidecar_seed ) . '.webp'; if ( file_exists( $sidecar_path ) ) { wp_delete_file( $sidecar_path ); } // Drop a pre-conversion .bak.jpg/.bak.png only when a .bak.webp already exists. $old_backup_path = $upload_path . self::$SqueezeHelpers->create_backup_filename( $old_filename ); $new_backup_path = $upload_path . self::$SqueezeHelpers->create_backup_filename( $filename ); if ( file_exists( $old_backup_path ) && file_exists( $new_backup_path ) ) { wp_delete_file( $old_backup_path ); } } do_action( 'squeeze_successful_squeeze' ); $size_totals = self::$SqueezeHelpers->sum_size_totals( $sizes ); wp_send_json_success( array( 'message' => '✅ ' . esc_html__( 'Squeezed successfully', 'squeeze' ), 'sizes' => array( 'original' => array( 'original_size' => ( isset( $sizes['original']['original_size'] ) ? (int) $sizes['original']['original_size'] : 0 ), 'compressed_size' => ( isset( $sizes['original']['compressed_size'] ) ? (int) $sizes['original']['compressed_size'] : 0 ), ), ), 'saved' => max( 0, $size_totals['before'] - $size_totals['after'] ), ) ); } wp_die(); } public function restore_attachment() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !isset( $_POST["attachmentID"] ) || empty( $_POST["attachmentID"] ) || !wp_get_attachment_url( $_POST["attachmentID"] ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Attachment not found', 'squeeze' ) ); } $attach_id = (int) $_POST["attachmentID"]; $can_restore = self::$SqueezeHelpers->can_restore( $attach_id ); if ( $can_restore ) { $is_restore_attachment = self::$SqueezeHelpers->restore_attachment( $attach_id ); if ( !is_wp_error( $is_restore_attachment ) ) { $attached_file = get_attached_file( $attach_id ); $filesize = ( $attached_file && file_exists( $attached_file ) ? wp_filesize( $attached_file ) : 0 ); wp_send_json_success( array( 'message' => '✅ ' . esc_html__( 'Restored successfully', 'squeeze' ), 'filesize' => $filesize, 'filesizeHumanReadable' => size_format( $filesize ), ) ); } else { wp_send_json_error( '❌ ' . esc_html__( 'Attachment not restored', 'squeeze' ) ); } } wp_die(); } public function get_attachment() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !current_user_can( 'upload_files' ) ) { wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) ); } if ( !isset( $_POST["attachmentID"] ) || empty( $_POST["attachmentID"] ) || !wp_get_attachment_url( $_POST["attachmentID"] ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Attachment not found', 'squeeze' ) ); } $attach_id = (int) $_POST["attachmentID"]; // Load excluded patterns once per request (cached in SqueezeHelpers::get_excluded_images()). $excluded_images = self::$SqueezeHelpers->get_excluded_images(); $full_image = wp_get_attachment_image_src( $attach_id, 'full' ); if ( !empty( $excluded_images ) && !empty( $full_image[0] ) ) { if ( $is_excluded = self::$SqueezeHelpers->is_excluded_image( $full_image[0], $excluded_images ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Attachment is excluded from compression', 'squeeze' ) . ' (' . esc_html__( 'found substring: ', 'squeeze' ) . $is_excluded['exclude_reason'] . ')' ); } } // Get attachment metadata once (contains sizes data) $metadata = wp_get_attachment_metadata( $attach_id ); $sizes = ( isset( $metadata['sizes'] ) ? $metadata['sizes'] : array() ); // Cache file paths to avoid repeated function calls $attached_file = get_attached_file( $attach_id ); $original_image_path = wp_get_original_image_path( $attach_id ); $is_squeezed = get_post_meta( $attach_id, 'squeeze_is_compressed', true ); // -scaled image $sizes['full'] = array( 'url' => $full_image[0], 'width' => $full_image[1], 'height' => $full_image[2], 'filesize' => wp_filesize( $attached_file ), ); // Build size URLs (WordPress caches these internally) foreach ( $sizes as $size_name => $size_data ) { $sizes[$size_name]['url'] = wp_get_attachment_image_url( $attach_id, $size_name ); } $attachment_data = array( 'id' => $attach_id, 'url' => wp_get_original_image_url( $attach_id ), 'mime' => get_post_mime_type( $attach_id ), 'name' => get_the_title( $attach_id ), 'filename' => basename( $original_image_path ), 'sizes' => $sizes, 'is_squeezed' => $is_squeezed, ); wp_send_json_success( $attachment_data ); wp_die(); } public function get_attachment_by_path() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !current_user_can( 'upload_files' ) ) { wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) ); } if ( !isset( $_POST["path"] ) || empty( $_POST["path"] ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Path not found', 'squeeze' ) ); } $pathes = sanitize_text_field( $_POST["path"] ); $pathes = json_decode( stripslashes( $pathes ), true ); if ( !is_array( $pathes ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Invalid path data', 'squeeze' ) ); } $pathes = array_map( array(self::$SqueezeHelpers, 'normalize_bulk_directory_storage_path'), $pathes ); $blocked_media_paths = array(); $pathes = array_values( array_filter( $pathes, function ( $path ) use(&$blocked_media_paths) { if ( self::$SqueezeHelpers->is_media_uploads_year_month_path( $path ) ) { $blocked_media_paths[] = $path; return false; } return true; } ) ); if ( empty( $pathes ) && !empty( $blocked_media_paths ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Media Library year/month folders cannot be squeezed here. Use Bulk Media Library Squeeze instead.', 'squeeze' ) ); } $attachment_data = array(); $image_formats = self::$SqueezeHelpers->get_image_formats(); $image_formats = implode( ',', $image_formats ); // MIME type mapping based on file extension (much faster than exif_imagetype) $mime_type_map = array( 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'avif' => 'image/avif', 'gif' => 'image/gif', ); // Load excluded patterns once per request (cached in SqueezeHelpers::get_excluded_images()). $excluded_images = self::$SqueezeHelpers->get_excluded_images(); // Cache home URL and normalize ABSPATH to avoid repeated function calls and string operations $home_url = trailingslashit( home_url() ); $abspath_normalized = str_replace( '\\', '/', ABSPATH ); foreach ( $pathes as $path ) { // Remove dangerous patterns related to directory traversal $path = preg_replace( [ '/\\.\\.+/', // Remove multiple dots (.., ...) '/\\/\\*/', ], '', $path ); // replace multiple backslashes with slashes $path = preg_replace( ['/\\/+/'], '/', $path ); // Add trailing slash if it's not there if ( substr( $path, -1 ) !== '/' ) { $path .= '/'; } // Add leading slash if it's not there if ( substr( $path, 0, 1 ) !== '/' ) { $path = '/' . $path; } $images = glob( ABSPATH . $path . '*.{' . $image_formats . '}', GLOB_BRACE ); if ( empty( $images ) ) { continue; } foreach ( $images as $image ) { // Get file extension for MIME type detection (much faster than exif_imagetype) $extension = strtolower( pathinfo( $image, PATHINFO_EXTENSION ) ); // Skip if extension not in our map (safety check) if ( !isset( $mime_type_map[$extension] ) ) { continue; } $filename = basename( $image ); // Skip Squeeze .bak sidecars (photo.bak.jpg) so Directory Squeeze does not re-compress them. if ( self::$SqueezeHelpers->is_squeeze_backup_filename( $filename ) ) { continue; } $attach_mime = $mime_type_map[$extension]; // Convert file path to URL efficiently // Normalize path separators and replace ABSPATH with home URL $image_normalized = str_replace( '\\', '/', $image ); $attach_url = str_replace( $abspath_normalized, $home_url, $image_normalized ); if ( !empty( $excluded_images ) ) { if ( $is_excluded = self::$SqueezeHelpers->is_excluded_image( $attach_url, $excluded_images ) ) { $attachment_data[] = array( 'excluded' => true, 'exclude_reason' => $is_excluded['exclude_reason'], 'filename' => $filename, ); continue; } } // Skip attachment_url_to_postid() to avoid expensive database queries // Path-based compression works with ID = 0 (files not in media library) $attach_id = 0; $attach_name = pathinfo( $image, PATHINFO_FILENAME ); $attachment_data[] = array( 'id' => $attach_id, 'url' => $attach_url, 'mime' => $attach_mime, 'name' => $attach_name, 'filename' => $filename, ); } } // Save pathes to cache set_transient( 'squeeze_bulk_path', $pathes, MONTH_IN_SECONDS ); if ( empty( $attachment_data ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Images were not found in the selected directories', 'squeeze' ) ); } wp_send_json_success( $attachment_data ); wp_die(); } /** * AJAX: restore Squeeze .bak sidecars in selected Directory Squeeze folders (in-place). */ public function restore_path_backups() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !current_user_can( 'upload_files' ) ) { wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) ); } if ( !isset( $_POST['path'] ) || $_POST['path'] === '' ) { wp_send_json_error( '❌ ' . esc_html__( 'Path not found', 'squeeze' ) ); } $pathes = json_decode( sanitize_text_field( wp_unslash( $_POST['path'] ) ), true ); if ( !is_array( $pathes ) ) { $pathes = json_decode( stripslashes( (string) wp_unslash( $_POST['path'] ) ), true ); } if ( !is_array( $pathes ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Invalid path data', 'squeeze' ) ); } $pathes = array_map( array(self::$SqueezeHelpers, 'normalize_bulk_directory_storage_path'), $pathes ); $blocked_media_paths = array(); $pathes = array_values( array_filter( $pathes, function ( $path ) use(&$blocked_media_paths) { if ( self::$SqueezeHelpers->is_media_uploads_year_month_path( $path ) ) { $blocked_media_paths[] = $path; return false; } return true; } ) ); if ( empty( $pathes ) && !empty( $blocked_media_paths ) ) { wp_send_json_error( '❌ ' . esc_html__( 'Media Library year/month folders cannot be restored here. Use Media Library restore instead.', 'squeeze' ) ); } $image_formats = self::$SqueezeHelpers->get_image_formats(); $results = array(); $abspath_norm = wp_normalize_path( ABSPATH ); foreach ( $pathes as $path ) { $path = preg_replace( array('/\\.\\.+/', '/\\/\\*/'), '', $path ); $path = preg_replace( '/\\/+/', '/', $path ); if ( substr( $path, -1 ) !== '/' ) { $path .= '/'; } if ( substr( $path, 0, 1 ) !== '/' ) { $path = '/' . $path; } $dir_fs = wp_normalize_path( ABSPATH . ltrim( $path, '/' ) ); if ( 0 !== strpos( $dir_fs, $abspath_norm ) || !is_dir( $dir_fs ) ) { $results[] = array( 'status' => 'failed', 'bak' => '', 'live' => '', 'message' => sprintf( /* translators: %s: directory path */ __( 'Invalid directory: %s', 'squeeze' ), $path ), ); continue; } $bak_files = glob( trailingslashit( $dir_fs ) . '*.bak.{' . implode( ',', $image_formats ) . '}', GLOB_BRACE ); if ( empty( $bak_files ) ) { continue; } foreach ( $bak_files as $bak_file ) { $bak_file = wp_normalize_path( $bak_file ); $bak_name = wp_basename( $bak_file ); if ( !self::$SqueezeHelpers->is_squeeze_backup_filename( $bak_name ) ) { continue; } $live_name = self::$SqueezeHelpers->get_live_filename_from_backup( $bak_name ); $restored = self::$SqueezeHelpers->restore_directory_backup_file( $bak_file ); if ( is_wp_error( $restored ) ) { $results[] = array( 'status' => 'failed', 'bak' => $bak_name, 'live' => $live_name, 'message' => $restored->get_error_message(), ); continue; } $results[] = array( 'status' => 'restored', 'bak' => $restored['bak'], 'live' => $restored['live'], 'message' => sprintf( /* translators: 1: backup filename, 2: live filename */ __( 'Restored %1$s → %2$s', 'squeeze' ), $restored['bak'], $restored['live'] ), ); } } if ( empty( $results ) ) { wp_send_json_error( '❌ ' . esc_html__( 'No backup (.bak) files were found in the selected directories', 'squeeze' ) ); } wp_send_json_success( array( 'results' => $results, 'summary' => array( 'restored' => count( array_filter( $results, static function ( $r ) { return ($r['status'] ?? '') === 'restored'; } ) ), 'failed' => count( array_filter( $results, static function ( $r ) { return ($r['status'] ?? '') === 'failed'; } ) ), 'skipped' => count( array_filter( $results, static function ( $r ) { return ($r['status'] ?? '') === 'skipped'; } ) ), ), ) ); } /** * AJAX: whether a filename matches the exclusion list (used before plupload compression). */ public function check_file_excluded() { check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' ); if ( !isset( $_POST['fileName'] ) || $_POST['fileName'] === '' ) { wp_send_json_error( '❌ ' . esc_html__( 'File name not found', 'squeeze' ) ); } $file_name = sanitize_text_field( wp_unslash( $_POST['fileName'] ) ); $excluded_images = self::$SqueezeHelpers->get_excluded_images(); $result = self::$SqueezeHelpers->is_excluded_image( $file_name, $excluded_images ); if ( $result ) { wp_send_json_success( array( 'is_excluded' => true, 'exclude_reason' => $result['exclude_reason'], ) ); } wp_send_json_success( array( 'is_excluded' => false, ) ); } /** * Proxy an attachment image through WordPress to avoid CORS errors in the Web Worker. * * When WP Offload Media (or any CDN plugin) serves images from an external provider * (e.g. Google Cloud Storage, Amazon S3), the image URLs are on a different origin than * the WordPress admin. The Web Worker uses fetch() to load images before compressing them. * Browsers block cross-origin fetch() calls that lack proper CORS headers (which GCS/S3 * buckets typically do not emit for arbitrary origins). * * This endpoint fetches the image server-side — where there is no CORS restriction — and * streams it back from the same origin as the admin, so the worker can fetch it without errors. * * Accepts GET parameters: * attachment_id int Attachment post ID. * size string WP image size name: 'original', 'full', or any registered size * (e.g. 'thumbnail', 'medium', 'large'). * _ajax_nonce string squeeze-nonce value. * * @since 1.7.16 */ public function fetch_image() { // Nonce verification (accepts GET or POST). $nonce = ( isset( $_GET['_ajax_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_ajax_nonce'] ) ) : '' ); if ( !$nonce || !wp_verify_nonce( $nonce, 'squeeze-nonce' ) ) { http_response_code( 403 ); wp_die( 'Invalid nonce.' ); } if ( !current_user_can( 'upload_files' ) ) { http_response_code( 403 ); wp_die( 'Unauthorized.' ); } $attach_id = ( isset( $_GET['attachment_id'] ) ? (int) $_GET['attachment_id'] : 0 ); $size = ( isset( $_GET['size'] ) ? sanitize_text_field( wp_unslash( $_GET['size'] ) ) : 'original' ); if ( !$attach_id ) { http_response_code( 404 ); wp_die( 'Attachment not found.' ); } $mime = get_post_mime_type( $attach_id ); if ( !$mime || strpos( $mime, 'image/' ) !== 0 ) { http_response_code( 400 ); wp_die( 'Not an image attachment.' ); } // Determine the local filesystem path for the requested size. $file_path = ''; if ( $size === 'original' ) { $file_path = wp_get_original_image_path( $attach_id ); } elseif ( $size === 'full' ) { $file_path = get_attached_file( $attach_id ); } else { $meta = wp_get_attachment_metadata( $attach_id ); $base_dir = dirname( (string) get_attached_file( $attach_id ) ); if ( !empty( $meta['sizes'][$size]['file'] ) ) { $file_path = trailingslashit( $base_dir ) . $meta['sizes'][$size]['file']; } } // Serve directly from local filesystem when the file is present. if ( $file_path && file_exists( $file_path ) ) { $file_size = filesize( $file_path ); header( 'Content-Type: ' . $mime ); header( 'Content-Length: ' . $file_size ); header( 'Cache-Control: private, no-store' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile readfile( $file_path ); exit; } // Local file not found — the file may have been removed by WP Offload Media's // "Remove Local Files" option. Fetch it from the provider URL server-side // (no CORS restriction on the server) and stream it back to the browser. if ( $size === 'original' ) { $provider_url = wp_get_original_image_url( $attach_id ); } elseif ( $size === 'full' ) { $provider_url = wp_get_attachment_url( $attach_id ); } else { $src = wp_get_attachment_image_src( $attach_id, $size ); $provider_url = ( $src ? $src[0] : '' ); } if ( !$provider_url ) { http_response_code( 404 ); wp_die( 'Image not found.' ); } $response = wp_remote_get( $provider_url, array( 'timeout' => 60, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { http_response_code( 502 ); wp_die( 'Failed to fetch image from provider: ' . esc_html( $response->get_error_message() ) ); } $http_status = wp_remote_retrieve_response_code( $response ); if ( $http_status !== 200 ) { http_response_code( (int) $http_status ); wp_die( 'Provider returned HTTP ' . (int) $http_status ); } $body = wp_remote_retrieve_body( $response ); header( 'Content-Type: ' . $mime ); header( 'Content-Length: ' . strlen( $body ) ); header( 'Cache-Control: private, no-store' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $body; exit; } public function delete_backup_attachment( $attach_id ) { $original_img_path = wp_get_original_image_path( (int) $attach_id ); $backup_img_path = preg_replace( "/(\\.(?!.*\\.))/", '.bak.', $original_img_path ); if ( file_exists( $backup_img_path ) ) { return wp_delete_file( $backup_img_path ); } return false; } public function delete_webp_images( $attach_id, $old_metadata = null ) { $original_img_path = wp_get_original_image_path( (int) $attach_id ); $attachment_data = ( $old_metadata ? $old_metadata : wp_get_attachment_metadata( $attach_id ) ); $delete_webp_images = self::$SqueezeHelpers->delete_webp_images( $original_img_path, $attachment_data ); return $delete_webp_images; } public function bulk_actions( $actions ) { if ( !is_array( $actions ) ) { $actions = array(); } $actions['squeeze_bulk_restore'] = esc_html__( 'Restore Original Image', 'squeeze' ); $actions['squeeze_bulk_compress'] = esc_html__( 'Squeeze Image', 'squeeze' ); $actions['squeeze_bulk_delete_backup'] = esc_html__( 'Delete Backup Image', 'squeeze' ); $actions['squeeze_bulk_delete_webp'] = esc_html__( 'Delete WEBP Image', 'squeeze' ); return $actions; } public function handle_bulk_actions( $redirect_to, $doaction, $post_ids ) { if ( $doaction === 'squeeze_bulk_restore' ) { $restored_ids_count = 0; foreach ( $post_ids as $post_id ) { $can_restore = self::$SqueezeHelpers->can_restore( $post_id ); if ( $can_restore ) { $is_restore_attachment = self::$SqueezeHelpers->restore_attachment( $post_id, true ); if ( $is_restore_attachment ) { $restored_ids_count += 1; } } } $redirect_to = add_query_arg( 'squeeze_bulk_restored', $restored_ids_count, $redirect_to ); } if ( $doaction === 'squeeze_bulk_compress' ) { foreach ( $post_ids as $post_id ) { $redirect_to = add_query_arg( 'squeeze_bulk_compressed', count( $post_ids ), $redirect_to ); } } if ( $doaction === 'squeeze_bulk_delete_backup' ) { $deleted_ids_count = 0; foreach ( $post_ids as $post_id ) { $is_delete_backup = $this->delete_backup_attachment( $post_id ); if ( $is_delete_backup ) { $deleted_ids_count += 1; } } $redirect_to = add_query_arg( 'squeeze_bulk_deleted', $deleted_ids_count, $redirect_to ); } if ( $doaction === 'squeeze_bulk_delete_webp' ) { $deleted_ids_count = 0; foreach ( $post_ids as $post_id ) { $is_delete_webp = $this->delete_webp_images( $post_id ); if ( $is_delete_webp ) { $deleted_ids_count += 1; } } $redirect_to = add_query_arg( 'squeeze_bulk_webp_deleted', $deleted_ids_count, $redirect_to ); } return $redirect_to; } public function bulk_action_admin_notice() { if ( !empty( $_REQUEST['squeeze_bulk_restored'] ) ) { $message = sprintf( /* translators: %d: number of attachments restored */ _n( '%d attachment restored.', '%d attachments restored.', $_REQUEST['squeeze_bulk_restored'], 'squeeze' ), number_format_i18n( $_REQUEST['squeeze_bulk_restored'] ) ); printf( '
%s
%s
%s
%s