# squeeze/trunk/inc/handlers.php

Squeeze – Browser-Based Image Compression &amp; WebP Converter (No API, No Limits), version trunk. 1,340 lines.

- Page: https://pluginprobe.com/plugins/squeeze/trunk/code/inc/handlers.php
- Raw: https://pluginprobe.com/plugins/squeeze/trunk/raw/inc/handlers.php
- Modified: 2026-10-08T12:14:44+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/squeeze/trunk/code/inc/handlers.php#L10-L20`.

```php
<?php

namespace SqueezeFree;

// Exit if accessed directly.
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class SqueezeHandlers extends SqueezeInit {
    public function __construct() {
        add_action( 'wp_ajax_squeeze_update_attachment', [$this, 'update_attachment'] );
        add_action( 'wp_ajax_squeeze_restore_attachment', [$this, 'restore_attachment'] );
        add_action( 'wp_ajax_squeeze_get_attachment', [$this, 'get_attachment'] );
        add_action( 'wp_ajax_squeeze_fetch_image', [$this, 'fetch_image'] );
        add_action( 'wp_ajax_squeeze_get_attachment_by_path', [$this, 'get_attachment_by_path'] );
        add_action( 'wp_ajax_squeeze_restore_path_backups', [$this, 'restore_path_backups'] );
        add_action( 'wp_ajax_squeeze_get_next_attachments', [$this, 'get_next_attachments'] );
        add_action( 'wp_ajax_squeeze_get_directories', [$this, 'get_directories'] );
        add_action( 'wp_ajax_squeeze_set_options', [$this, 'set_options'] );
        add_action( 'wp_ajax_squeeze_check_file_excluded', [$this, 'check_file_excluded'] );
        add_action( 'wp_ajax_squeeze_mark_client_compressed', [$this, 'ajax_mark_client_compressed'] );
        add_action( 'delete_attachment', [$this, 'delete_backup_attachment'] );
        add_action( 'delete_attachment', [$this, 'delete_webp_images'] );
        add_action(
            'add_attachment',
            [$this, 'maybe_mark_voxel_client_compressed'],
            10,
            1
        );
        add_filter( 'bulk_actions-upload', [$this, 'bulk_actions'] );
        add_filter(
            'handle_bulk_actions-upload',
            [$this, 'handle_bulk_actions'],
            10,
            3
        );
        add_filter( 'image_size_names_choose', [$this, 'custom_image_sizes'] );
        add_filter( 'mod_rewrite_rules', [$this, 'add_webp_rewrite_rules'] );
        add_action( 'pre-html-upload-ui', [$this, 'single_file_upload_notice'], 10 );
        add_action( 'admin_notices', [$this, 'bulk_action_admin_notice'] );
        add_action( 'init', [$this, 'output_buffer_start'], 1 );
        add_action( 'shutdown', [$this, 'output_buffer_end'], 0 );
        // test this
        add_filter(
            'wp_prepare_attachment_for_js',
            [$this, 'update_attachment_metadata_for_js'],
            10,
            3
        );
        add_filter(
            'wp_get_attachment_metadata',
            [$this, 'update_attachment_metadata'],
            10,
            2
        );
    }

    /**
     * Client-precompressed multipart uploads (Media Library Plupload, Voxel, etc.) never
     * hit squeeze_update_attachment when after-upload thumb squeeze is skipped. When the
     * client adds signed fields to the same async-upload request, mark new image
     * attachments so the Media Library and stats stay consistent.
     *
     * Accepts either:
     * - `_squeeze_client` + `_squeeze_client_nonce` (Media Library / general)
     * - `_squeeze_voxel_client` + `_squeeze_voxel_nonce` (Voxel compat)
     *
     * @param int $attach_id Attachment ID.
     * @return void
     */
    public function maybe_mark_voxel_client_compressed( $attach_id ) {
        $attach_id = (int) $attach_id;
        if ( $attach_id <= 0 ) {
            return;
        }
        $is_client = !empty( $_POST['_squeeze_client'] ) && '1' === $_POST['_squeeze_client'];
        $is_voxel = !empty( $_POST['_squeeze_voxel_client'] ) && '1' === $_POST['_squeeze_voxel_client'];
        if ( !$is_client && !$is_voxel ) {
            return;
        }
        $nonce = '';
        if ( $is_client && !empty( $_POST['_squeeze_client_nonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_POST['_squeeze_client_nonce'] ) );
        } elseif ( $is_voxel && !empty( $_POST['_squeeze_voxel_nonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_POST['_squeeze_voxel_nonce'] ) );
        }
        if ( '' === $nonce || !wp_verify_nonce( $nonce, 'squeeze-nonce' ) ) {
            return;
        }
        if ( !current_user_can( 'upload_files' ) ) {
            return;
        }
        $mime = get_post_mime_type( $attach_id );
        if ( !$mime || strpos( $mime, 'image/' ) !== 0 ) {
            return;
        }
        $original_size = ( isset( $_POST['_squeeze_original_size'] ) ? (int) $_POST['_squeeze_original_size'] : 0 );
        $this->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 <a href="%s" target="_blank">compression settings</a> 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 = '<strong>✅ ' . esc_html__( 'Squeezed successfully', 'squeeze' ) . '!</strong> ' . $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( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
        }
        if ( !empty( $_REQUEST['squeeze_bulk_compressed'] ) ) {
            $message = sprintf( 
                /* translators: %d: number of attachments squeezed */
                _n(
                    '%d attachment squeezed.',
                    '%d attachments squeezed.',
                    $_REQUEST['squeeze_bulk_compressed'],
                    'squeeze'
                ),
                number_format_i18n( $_REQUEST['squeeze_bulk_compressed'] )
             );
            printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
        }
        if ( !empty( $_REQUEST['squeeze_bulk_deleted'] ) ) {
            $message = sprintf( 
                /* translators: %d: number of backup images deleted */
                _n(
                    '%d backup image deleted.',
                    '%d backup images deleted.',
                    $_REQUEST['squeeze_bulk_deleted'],
                    'squeeze'
                ),
                number_format_i18n( $_REQUEST['squeeze_bulk_deleted'] )
             );
            printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
        }
        if ( !empty( $_REQUEST['squeeze_bulk_webp_deleted'] ) ) {
            $message = sprintf( 
                /* translators: %d: number of webp images deleted */
                _n(
                    '%d WEBP image deleted.',
                    '%d WEBP images deleted.',
                    $_REQUEST['squeeze_bulk_webp_deleted'],
                    'squeeze'
                ),
                number_format_i18n( $_REQUEST['squeeze_bulk_webp_deleted'] )
             );
            printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
        }
    }

    public function custom_image_sizes( $sizes ) {
        $available_sizes = wp_get_registered_image_subsizes();
        if ( empty( $available_sizes ) ) {
            return $sizes;
        }
        foreach ( $available_sizes as $size_name => $size_data ) {
            $sizes[$size_name] = $size_data['width'] . 'x' . $size_data['height'];
        }
        return $sizes;
    }

    public function get_next_attachments() {
        check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
        $per_page = self::$MEDIA_PER_PAGE;
        $page = ( isset( $_POST['page'] ) ? (int) $_POST['page'] : 1 );
        $type = ( isset( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : 'uncompressed' );
        $last_id = ( isset( $_POST['lastId'] ) ? (int) $_POST['lastId'] : 0 );
        if ( $type === 'uncompressed' ) {
            $next_images = self::$SqueezeHelpers->get_uncompressed_images( $last_id );
        } else {
            $next_images = self::$SqueezeHelpers->get_total_images( $page );
        }
        wp_send_json_success( $next_images );
    }

    public function single_file_upload_notice() {
        global $current_screen;
        if ( $current_screen->id === 'media' ) {
            ?>
			<div class="notice notice-warning hide-if-js squeeze-single-file-upload-notice">
				<p><?php 
            esc_html_e( 'Single file upload is not supported for the image compression by Squeeze. Please use multi-file uploader or bulk squeeze.', 'squeeze' );
            ?></p>
			</div>
			<?php 
        }
    }

    public function get_directories() {
        check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to browse directories.', 'squeeze' ) );
        }
        $parent_directory = ( isset( $_POST['parentDir'] ) ? sanitize_text_field( wp_unslash( $_POST['parentDir'] ) ) : '' );
        $allowed_base = realpath( WP_CONTENT_DIR );
        if ( $allowed_base === false ) {
            wp_send_json_error( '❌ ' . esc_html__( 'Content directory is not accessible.', 'squeeze' ) );
        }
        if ( $parent_directory === '' ) {
            $base_dir = $allowed_base;
        } else {
            // Parent from UI is site-relative (/wp-content/foo/); resolve under WP_CONTENT_DIR only.
            $rel = self::$SqueezeHelpers->bulk_parent_relative_to_content_dir( $parent_directory );
            if ( '' !== $rel && preg_match( '#(^|/)\\.\\.(/|$)#', $rel ) ) {
                wp_send_json_error( '❌ ' . esc_html__( 'Invalid directory path.', 'squeeze' ) );
            }
            $candidate = $allowed_base . (( '' !== $rel ? '/' . $rel : '' ));
            $base_dir = realpath( $candidate );
            if ( $base_dir === false || strpos( $base_dir, $allowed_base ) !== 0 ) {
                wp_send_json_error( '❌ ' . esc_html__( 'Invalid directory path.', 'squeeze' ) );
            }
        }
        $directories = scandir( $base_dir );
        if ( !$parent_directory ) {
            $directories[] = $base_dir;
        }
        $result = array_filter( $directories, function ( $dir ) use($base_dir) {
            if ( $dir === $base_dir ) {
                return true;
            }
            return is_dir( $base_dir . '/' . $dir ) && !in_array( $dir, ['.', '..'] );
        } );
        $output = array_map( function ( $dir ) use($base_dir) {
            if ( $dir === 'squeeze-webp' ) {
                return [
                    'name'         => '',
                    'path'         => '',
                    'is_writeable' => false,
                    'parent'       => '',
                ];
            }
            if ( $dir === $base_dir ) {
                $path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( $dir . '/' );
                $parent_path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( dirname( $base_dir ) . '/' );
                return [
                    'name'         => 'wp-content',
                    'path'         => $path,
                    'is_writeable' => false,
                    'parent'       => $parent_path,
                ];
            }
            $path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( $base_dir . '/' . $dir . '/' );
            $parent_path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( dirname( $base_dir . '/' . $dir ) . '/' );
            $is_ym = self::$SqueezeHelpers->is_media_uploads_year_month_path( $path );
            return [
                'name'         => $dir,
                'path'         => $path,
                'is_writeable' => !$is_ym && wp_is_writable( $base_dir . '/' . $dir ),
                'is_blocked'   => $is_ym,
                'parent'       => $parent_path,
            ];
        }, $result );
        usort( $output, function ( $a, $b ) {
            if ( $a['name'] === 'wp-content' ) {
                return -1;
            } elseif ( $b['name'] === 'wp-content' ) {
                return 1;
            }
            return strcmp( $a['name'], $b['name'] );
        } );
        wp_send_json( $output );
    }

    public function add_webp_rewrite_rules( $rules ) {
        $is_auto_webp = self::$SqueezeHelpers->get_option( 'auto_webp' );
        $modules = self::$SqueezeHelpers->apache_get_modules();
        // Get the WordPress installation subdirectory, if applicable
        $wordpress_subdirectory = wp_parse_url( home_url(), PHP_URL_PATH );
        // Check if WordPress is installed in a subdirectory (not just the root)
        if ( strlen( $wordpress_subdirectory ) > 1 ) {
            // Ensure the subdirectory is used correctly in the rules
            $rewrite_base = $wordpress_subdirectory . '/';
        } else {
            // If WordPress is installed in the root, no subdirectory path is needed
            $rewrite_base = '/';
        }
        $webp_rules = "\n# Serve WebP images from the wp-content/squeeze-webp folder if available\n";
        $webp_rules .= "RewriteCond %{HTTP_ACCEPT} image/webp\n";
        // Check if browser supports WebP
        $webp_rules .= "RewriteCond %{REQUEST_URI} \\.(jpg|jpeg|png)\$ [NC]\n";
        // Check if request is for JPG, JPEG, or PNG
        $webp_rules .= "RewriteCond %{DOCUMENT_ROOT}" . $rewrite_base . "wp-content/squeeze-webp/\$1.\$2.webp -f\n";
        // Check if WebP file exists
        $webp_rules .= "RewriteRule ^wp-content/(.+)\\.(jpg|jpeg|png)\$ wp-content/squeeze-webp/\$1.\$2.webp [T=image/webp,E=webp_request,L]\n";
        // Serve WebP file
        $webp_rules .= "# END Serve WebP images from the wp-content/squeeze-webp folder if available\n";
        $webp_rules .= "\n";
        if ( !$is_auto_webp ) {
            // If auto WebP conversion is disabled, return the original rules and replace the WebP rules if they exist
            $rules = preg_replace( '/# Serve WebP images from the wp-content\\/squeeze-webp folder if available.*?# END Serve WebP images from the wp-content\\/squeeze-webp folder if available\\n/s', '', $rules );
            return $rules;
        }
        // Skip only when we can positively detect that mod_rewrite is absent.
        // Under CGI/FPM, apache_get_modules() is unavailable even when Apache has
        // mod_rewrite — same situation WordPress still writes permalink rules for.
        // Rules sit inside WP's <IfModule mod_rewrite.c> block.
        if ( is_array( $modules ) && !in_array( 'mod_rewrite', $modules, true ) ) {
            return $rules;
        }
        return $webp_rules . $rules;
    }

    public function output_buffer_start() {
        if ( !is_admin() && (!isset( $_SERVER['HTTP_X_WP_REMOTE_REQUEST'] ) || $_SERVER['HTTP_X_WP_REMOTE_REQUEST'] !== 'true') || function_exists( "wp_doing_ajax" ) && wp_doing_ajax() || defined( 'DOING_AJAX' ) && DOING_AJAX ) {
            ob_start( [$this, 'replace_image_urls_with_webp'] );
        }
    }

    /**
     * While WordPress flushes automatically, you can add an explicit handler on shutdown 
     * (with higher priority than 1, e.g., 0) to ensure clean flushing if there are conflicts 
     * (e.g., with zlib compression or other plugins). 
     * This suppresses potential PHP notices like "failed to send buffer of zlib output compression" 
     * without discarding content:
     */
    public function output_buffer_end() {
        while ( ob_get_level() > 0 ) {
            @ob_end_flush();
            // Suppress notices; flushes modified content
        }
    }

    // Other helper functions
    public function replace_image_urls_with_webp( $content ) {
        $is_replace_urls = self::$SqueezeHelpers->is_webp_replace_urls();
        $is_direct_webp = self::$SqueezeHelpers->get_option( 'direct_webp' );
        if ( !$is_replace_urls && !$is_direct_webp ) {
            return $content;
        }
        $content_folder = basename( WP_CONTENT_DIR );
        // Regular expression to find JPG and PNG images in src and srcset attributes.
        //$pattern = '/(\/\/.*?\/' . preg_quote($content_folder, '/') . '\/)([^"\s]+\.(jpg|jpeg|png))(\?[^"\s]*)?/i';
        // Supports query params, size suffixes, and encoded filenames.
        $pattern = '/(\\/\\/[^"\\s]*?' . preg_quote( $content_folder, '/' ) . '\\/[^"\\s]+\\.(jpg|jpeg|png))(\\?[^"\\s]*)?/i';
        // Callback function to replace the URLs.
        $callback = function ( $matches ) use($is_direct_webp, $is_replace_urls) {
            // $matches[0] is the full matched URL
            $full_match = $matches[0];
            $url_no_query = $matches[1];
            $query = ( isset( $matches[3] ) ? $matches[3] : '' );
            $file_extension = strtolower( pathinfo( $url_no_query, PATHINFO_EXTENSION ) );
            if ( $file_extension === 'webp' ) {
                return $full_match;
                // Already a WebP image, no need to replace.
            }
            $protocol = ( is_ssl() ? 'https:' : 'http:' );
            $abs_url = $protocol . $url_no_query;
            $file_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $abs_url );
            if ( $is_replace_urls ) {
                $webp_url = self::$SqueezeHelpers->convert_image_path_to_webp_path( $url_no_query ) . '.webp';
                // WebP URL like 'example.com/wp-content/squeeze-webp/uploads/2024/12/test.jpg.webp'
                // Check if the exact sized WEBP sidecar exists on the server (local disk check).
                $webp_file_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $protocol . $webp_url );
                $webp_exists = $webp_file_path !== '' && file_exists( $webp_file_path );
                if ( $webp_exists ) {
                    return $webp_url;
                    // Use WEBP version if it exists.
                }
                // Fallback: sized/scaled/rotated URL → base (unsuffixed) .webp sidecar.
                $base_webp_candidates = array();
                if ( preg_match( '/-\\d+x\\d+\\.[^.]+\\.webp$/i', $webp_url ) ) {
                    $base_webp_candidates[] = preg_replace( '/-\\d+x\\d+(\\.[^.]+)\\.webp$/i', '$1.webp', $webp_url );
                }
                if ( preg_match( '/-scaled\\.[^.]+\\.webp$/i', $webp_url ) ) {
                    $base_webp_candidates[] = preg_replace( '/-scaled(\\.[^.]+)\\.webp$/i', '$1.webp', $webp_url );
                }
                if ( preg_match( '/-rotated\\.[^.]+\\.webp$/i', $webp_url ) ) {
                    $base_webp_candidates[] = preg_replace( '/-rotated(\\.[^.]+)\\.webp$/i', '$1.webp', $webp_url );
                }
                foreach ( $base_webp_candidates as $candidate_webp_url ) {
                    if ( $candidate_webp_url === $webp_url ) {
                        continue;
                    }
                    $candidate_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $protocol . $candidate_webp_url );
                    if ( $candidate_path !== '' && file_exists( $candidate_path ) ) {
                        return $candidate_webp_url;
                    }
                }
            }
            // Direct WebP: prefer a sibling .webp when present (e.g. hardcoded CSS bg still points at .jpg
            // after conversion, or path/bulk left both files on disk).
            if ( $is_direct_webp ) {
                // try to find possible WEBP variants
                $webp_candidates = [];
                // Base WebP
                $webp_url = preg_replace( '/\\.[^.]+$/i', '.webp', $url_no_query );
                $webp_candidates[] = $webp_url;
                // Unscaled version (remove -scaled)
                if ( preg_match( '/-scaled\\.[^.]+$/i', $webp_url ) ) {
                    $webp_candidates[] = preg_replace( '/-scaled\\.[^.]+$/i', '.webp', $webp_url );
                }
                // Dimensioned variants (e.g. test-300x200.jpg)
                if ( preg_match( '/-\\d+x\\d+\\.[^.]+$/i', $webp_url ) ) {
                    $webp_candidates[] = preg_replace( '/-\\d+x\\d+\\.[^.]+$/i', '.webp', $webp_url );
                }
                // Numbered suffixes: test-1.webp, test-2.webp...
                for ($i = 1; $i <= 99; $i++) {
                    $webp_candidates[] = preg_replace( '/(-scaled)?\\.[^.]+$/i', '-' . $i . '.webp', $webp_url );
                }
                foreach ( $webp_candidates as $candidate ) {
                    $candidate_full = ( strpos( $candidate, '//' ) === 0 ? $protocol . $candidate : $candidate );
                    $candidate_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $candidate_full );
                    if ( $candidate_path !== '' && file_exists( $candidate_path ) ) {
                        return $candidate . $query;
                    }
                }
                /*
                				// try to find webp without -scaled suffix
                				$webp_url_no_scaled = preg_replace('/-scaled\.webp$/', '.webp', $webp_url);
                				$webp_file_path_no_scaled = str_replace(home_url(), ABSPATH, $protocol.$webp_url_no_scaled); // Convert URL to file path.
                				if (file_exists($webp_file_path_no_scaled)) {
                					return $webp_url_no_scaled; // Use WEBP version if it exists.
                				} else {
                					// loop through the files with number suffixes
                					for ($i = 1; $i <= 99; $i++) {
                						$webp_url_numbered = preg_replace('/(-scaled)?\.webp$/', '-' . $i . '.webp', $webp_url);
                						$webp_file_path_numbered = str_replace(home_url(), ABSPATH, $protocol.$webp_url_numbered); // Convert URL to file path.
                						if (file_exists($webp_file_path_numbered)) {
                							return $webp_url_numbered; // Use WEBP version if it exists.
                						}
                					}
                				}
                				//*/
            }
            return $full_match;
            // Fallback to the original URL if WEBP file doesn't exist.
        };
        // Replace URLs in the content, including src and srcset attributes.
        $content = preg_replace_callback( $pattern, $callback, $content );
        return $content;
    }

    public function set_options() {
        check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( '❌ ' . esc_html__( 'You do not have permission to change plugin settings', 'squeeze' ) );
        }
        $options = ( isset( $_POST['options'] ) ? $_POST['options'] : array() );
        if ( empty( $options ) ) {
            wp_send_json_error( '❌ ' . esc_html__( 'Options not found', 'squeeze' ) );
        }
        $is_set_options = self::$SqueezeHelpers->set_options( $options );
        if ( !$is_set_options ) {
            wp_send_json_error( '❌ ' . esc_html__( 'Options not saved', 'squeeze' ) );
        }
        wp_send_json_success( '✅ ' . esc_html__( 'Options saved successfully', 'squeeze' ) );
    }

    public function update_attachment_metadata_for_js( $response, $attachment, $meta ) {
        if ( isset( $response['filesizeInBytes'] ) && isset( $response['filesizeHumanReadable'] ) ) {
            // Refresh filesize from disk only (cheap). After restore the cached
            // `_wp_attachment_metadata['filesize']` is often stale; we must not
            // gate this on squeeze_is_compressed. Do not call getimagesize here —
            // that re-decodes the full image during async-upload thumb generation
            // and can 502 low-memory hosts.
            $attachment_path = get_attached_file( $attachment->ID );
            if ( !$attachment_path || !file_exists( $attachment_path ) ) {
                return $response;
            }
            $filesize = wp_filesize( $attachment_path );
            $response['filesizeInBytes'] = $filesize;
            $response['filesizeHumanReadable'] = size_format( $filesize );
        }
        return $response;
    }

    public function update_attachment_metadata( $data, $attachment_id ) {
        if ( isset( $data['filesize'] ) ) {
            $attachment_path = get_attached_file( $attachment_id );
            if ( !$attachment_path || !file_exists( $attachment_path ) ) {
                return $data;
            }
            $data['filesize'] = wp_filesize( $attachment_path );
        }
        return $data;
    }

}

```
