PluginProbe
Squeeze – Image Optimization & Compression, WEBP Conversion / trunk
Squeeze – Image Optimization & Compression, WEBP Conversion vtrunk
1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 trunk 1.0 1.1 1.2 1.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5 1.5.1 1.5.2 1.6 All 42 releases
squeeze / inc / handlers.php

handlers.php in Squeeze – Image Optimization & Compression, WEBP Conversion trunk, at inc/handlers.php

1,068 lines 53.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SqueezeFree;
4
5 // Exit if accessed directly.
6 if ( !defined( 'ABSPATH' ) ) {
7 exit;
8 }
9 class SqueezeHandlers extends SqueezeInit {
10 public function __construct() {
11 add_action( 'wp_ajax_squeeze_update_attachment', [$this, 'update_attachment'] );
12 add_action( 'wp_ajax_squeeze_restore_attachment', [$this, 'restore_attachment'] );
13 add_action( 'wp_ajax_squeeze_get_attachment', [$this, 'get_attachment'] );
14 add_action( 'wp_ajax_squeeze_fetch_image', [$this, 'fetch_image'] );
15 add_action( 'wp_ajax_squeeze_get_attachment_by_path', [$this, 'get_attachment_by_path'] );
16 add_action( 'wp_ajax_squeeze_get_next_attachments', [$this, 'get_next_attachments'] );
17 add_action( 'wp_ajax_squeeze_get_directories', [$this, 'get_directories'] );
18 add_action( 'wp_ajax_squeeze_set_options', [$this, 'set_options'] );
19 add_action( 'wp_ajax_squeeze_check_file_excluded', [$this, 'check_file_excluded'] );
20 add_action( 'delete_attachment', [$this, 'delete_backup_attachment'] );
21 add_action( 'delete_attachment', [$this, 'delete_webp_images'] );
22 add_action(
23 'add_attachment',
24 [$this, 'maybe_mark_voxel_client_compressed'],
25 10,
26 1
27 );
28 add_filter( 'bulk_actions-upload', [$this, 'bulk_actions'] );
29 add_filter(
30 'handle_bulk_actions-upload',
31 [$this, 'handle_bulk_actions'],
32 10,
33 3
34 );
35 add_filter( 'image_size_names_choose', [$this, 'custom_image_sizes'] );
36 add_filter( 'mod_rewrite_rules', [$this, 'add_webp_rewrite_rules'] );
37 add_action( 'pre-html-upload-ui', [$this, 'single_file_upload_notice'], 10 );
38 add_action( 'admin_notices', [$this, 'bulk_action_admin_notice'] );
39 add_action( 'init', [$this, 'output_buffer_start'], 1 );
40 add_action( 'shutdown', [$this, 'output_buffer_end'], 0 );
41 // test this
42 add_filter(
43 'wp_prepare_attachment_for_js',
44 [$this, 'update_attachment_metadata_for_js'],
45 10,
46 3
47 );
48 add_filter(
49 'wp_get_attachment_metadata',
50 [$this, 'update_attachment_metadata'],
51 10,
52 2
53 );
54 }
55
56 /**
57 * Voxel (and similar) multipart uploads are compressed in the browser before send, so
58 * squeeze_update_attachment never runs. When the client adds signed fields to the same
59 * request, mark new image attachments so the Media Library and stats stay consistent.
60 *
61 * @param int $attach_id Attachment ID.
62 * @return void
63 */
64 public function maybe_mark_voxel_client_compressed( $attach_id ) {
65 $attach_id = (int) $attach_id;
66 if ( $attach_id <= 0 ) {
67 return;
68 }
69 if ( empty( $_POST['_squeeze_voxel_client'] ) || '1' !== $_POST['_squeeze_voxel_client'] ) {
70 return;
71 }
72 if ( empty( $_POST['_squeeze_voxel_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_squeeze_voxel_nonce'] ) ), 'squeeze-nonce' ) ) {
73 return;
74 }
75 if ( !current_user_can( 'upload_files' ) ) {
76 return;
77 }
78 if ( get_post_meta( $attach_id, 'squeeze_is_compressed', true ) ) {
79 return;
80 }
81 $mime = get_post_mime_type( $attach_id );
82 if ( !$mime || strpos( $mime, 'image/' ) !== 0 ) {
83 return;
84 }
85 update_post_meta( $attach_id, 'squeeze_is_compressed', true );
86 $uncompressed_images = self::$SqueezeHelpers->get_stats_option( 'uncompressed_images' );
87 if ( $uncompressed_images > 0 ) {
88 $uncompressed_images--;
89 }
90 update_option( 'squeeze_stats', array(
91 'uncompressed_images' => $uncompressed_images,
92 ) );
93 do_action( 'squeeze_successful_squeeze' );
94 }
95
96 public function update_attachment() {
97 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
98 if ( !current_user_can( 'upload_files' ) ) {
99 wp_send_json_error( '' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
100 }
101 if ( !isset( $_POST["base64"] ) || empty( $_POST["base64"] ) ) {
102 wp_send_json_error( '' . esc_html__( 'No image data found', 'squeeze' ) );
103 }
104 $base64 = sanitize_text_field( wp_unslash( $_POST["base64"] ) );
105 $sizes = ( isset( $_POST["base64Sizes"] ) ? (array) $_POST["base64Sizes"] : array() );
106 // DO NOT SANITIZE because it's an array
107 $base64_webp = ( isset( $_POST["base64Webp"] ) ? sanitize_text_field( wp_unslash( $_POST["base64Webp"] ) ) : '' );
108 $sizes_webp = ( isset( $_POST["base64SizesWebp"] ) ? (array) $_POST["base64SizesWebp"] : array() );
109 $file_format = ( isset( $_POST["format"] ) ? sanitize_text_field( wp_unslash( $_POST["format"] ) ) : '' );
110 $filename = ( isset( $_POST["filename"] ) ? sanitize_file_name( wp_unslash( $_POST["filename"] ) ) : '' );
111 $extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
112 // handle jpg/jpeg extension
113 if ( $extension === 'jpeg' ) {
114 $extension = 'jpg';
115 }
116 $file_format = strtolower( (string) $file_format );
117 // JS sends MIME subtype "jpeg"; allowlist key is "jpg".
118 if ( $file_format === 'jpeg' ) {
119 $file_format = 'jpg';
120 }
121 $original_file = ( isset( $_FILES['originalFile'] ) ? $_FILES['originalFile'] : null );
122 // Validate against the hardcoded constant — never trust user-writable options for security decisions.
123 $allowed_extensions = array_keys( self::ALLOWED_IMAGE_FORMATS );
124 if ( !in_array( $extension, $allowed_extensions, true ) || !in_array( $file_format, $allowed_extensions, true ) || empty( $file_format ) ) {
125 wp_send_json_error( '' . esc_html__( 'Invalid image format', 'squeeze' ) );
126 }
127 $attach_id = ( isset( $_POST["attachmentID"] ) ? (int) $_POST["attachmentID"] : 0 );
128 $meta_data = wp_get_attachment_metadata( $attach_id );
129 $url = ( isset( $_POST["url"] ) ? sanitize_text_field( $_POST["url"] ) : '' );
130 // sanitize_url() replaces spaces with %20, so we use sanitize_text_field() instead
131 $process = ( isset( $_POST["process"] ) ? sanitize_text_field( $_POST["process"] ) : '' );
132 // process: all, uncompressed, path
133 // Single Page Squeeze posts process=path for the whole queue, including
134 // Media Library attachments. Those need the library pipeline.
135 if ( $process === 'path' && $attach_id > 0 ) {
136 $process = 'all';
137 }
138 if ( empty( $attach_id ) && $process !== 'path' || empty( $url ) ) {
139 wp_send_json_error( '' . esc_html__( 'Attachment not found', 'squeeze' ) );
140 }
141 $excluded_images = self::$SqueezeHelpers->get_excluded_images();
142 if ( $is_excluded = self::$SqueezeHelpers->is_excluded_image( $url, $excluded_images ) ) {
143 wp_send_json_error( '' . esc_html__( 'Attachment is excluded from compression', 'squeeze' ) . ' (' . esc_html__( 'found substring: ', 'squeeze' ) . $is_excluded['exclude_reason'] . ')' );
144 }
145 $is_backup_original = self::$SqueezeHelpers->get_option( 'backup_original' );
146 $is_direct_webp = self::$SqueezeHelpers->get_option( 'direct_webp' );
147 // Upload path.
148 $upload_path = self::$SqueezeHelpers->get_upload_path( $attach_id, $filename, $url );
149 $decoded = self::$SqueezeHelpers->decode_base64_image( $base64, $file_format );
150 $webp_file_path = '';
151 $old_metadata = wp_get_attachment_metadata( $attach_id );
152 $old_filename = $filename;
153 $source_abspath = self::$SqueezeHelpers->resolve_media_url_to_abspath( $url );
154 $did_direct_rename = false;
155 // Direct WebP: write sibling .webp (library or path/bulk-from-page) and reclaim orphans.
156 if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) {
157 $desired_webp = sanitize_file_name( preg_replace( '/\\.[^.]+$/', '.webp', $filename ) );
158 if ( $process !== 'path' && $attach_id > 0 ) {
159 $file = get_attached_file( $attach_id );
160 $dirname = ( $file ? dirname( $file ) : untrailingslashit( $upload_path ) );
161 } else {
162 $dirname = ( $source_abspath !== '' ? dirname( $source_abspath ) : untrailingslashit( $upload_path ) );
163 }
164 $unique_filename = self::$SqueezeHelpers->get_direct_webp_filename( $dirname, $desired_webp, $attach_id );
165 $webp_file_path = trailingslashit( $dirname ) . $unique_filename;
166 $filename = basename( $unique_filename );
167 $url = str_replace( ABSPATH, home_url( '/' ), $webp_file_path );
168 $upload_path = trailingslashit( $dirname );
169 $did_direct_rename = true;
170 }
171 if ( $original_file ) {
172 $sizes['original']['original_size'] = $original_file['size'];
173 } else {
174 $path_for_size = ( $source_abspath !== '' && file_exists( $source_abspath ) ? $source_abspath : $upload_path . $old_filename );
175 $sizes['original']['original_size'] = ( file_exists( $path_for_size ) ? wp_filesize( $path_for_size ) : 0 );
176 }
177 $sizes['original']['compressed_size'] = strlen( $decoded );
178 // check if compressed_size is greater than original_size
179 if ( $sizes['original']['compressed_size'] > $sizes['original']['original_size'] ) {
180 if ( $attach_id && $process !== 'path' ) {
181 update_post_meta( $attach_id, 'squeeze_compression_failed', 'larger_than_original' );
182 }
183 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 ) );
184 }
185 if ( $is_backup_original && $process !== 'path' ) {
186 // do not backup for non library images
187 // backup original
188 if ( $original_file ) {
189 $backup_original_image = self::$SqueezeHelpers->backup_original_image( $upload_path, $filename, $original_file['tmp_name'] );
190 } else {
191 $backup_original_image = self::$SqueezeHelpers->backup_original_image( $upload_path, $filename );
192 }
193 if ( is_wp_error( $backup_original_image ) ) {
194 wp_send_json_error( $backup_original_image->get_error_message() );
195 }
196 }
197 // Save the image in the uploads directory.
198 $upload_image = self::$SqueezeHelpers->upload_image( $upload_path, $filename, $decoded );
199 if ( is_wp_error( $upload_image ) ) {
200 wp_send_json_error( $upload_image->get_error_message() );
201 }
202 if ( $base64_webp ) {
203 $upload_webp = self::$SqueezeHelpers->upload_webp( $upload_path, $base64_webp, $filename );
204 if ( is_wp_error( $upload_webp ) ) {
205 wp_send_json_error( $upload_webp->get_error_message() );
206 }
207 $upload_webp_thumbs = self::$SqueezeHelpers->upload_webp_thumbs( $upload_path, $sizes_webp );
208 // skip handling errors for webp thumbs, because they are not always required
209 }
210 // upload thumbnails
211 if ( $process !== 'path' ) {
212 if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) {
213 update_attached_file( $attach_id, $webp_file_path );
214 // update the _wp_attached_file meta value to the new webp file path
215 wp_update_post( [
216 'ID' => $attach_id,
217 'post_mime_type' => 'image/webp',
218 ] );
219 $metadata = wp_generate_attachment_metadata( $attach_id, $webp_file_path );
220 wp_update_attachment_metadata( $attach_id, $metadata );
221 }
222 $sizes = self::$SqueezeHelpers->upload_image_thumbs(
223 $upload_path,
224 $sizes,
225 $file_format,
226 $filename,
227 $attach_id
228 );
229 //wp_send_json_error( print_r($sizes, true) );
230 if ( is_wp_error( $sizes ) ) {
231 if ( $attach_id ) {
232 update_post_meta( $attach_id, 'squeeze_compression_failed', 'thumb_upload_failed' );
233 }
234 wp_send_json_error( $sizes->get_error_message() );
235 }
236 if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) {
237 // remove webp images from the squeeze-webp directory
238 $this->delete_webp_images( $attach_id, $old_metadata );
239 // remove original JPG/PNG file if it exists
240 foreach ( $old_metadata['sizes'] as $size_name => $size_data ) {
241 $old_size_filename = $size_data['file'];
242 wp_delete_file( $upload_path . $old_size_filename );
243 }
244 $old_scaled_filename = basename( $old_metadata['file'] );
245 wp_delete_file( $upload_path . $old_scaled_filename );
246 $old_original_path = $upload_path . $old_filename;
247 wp_delete_file( $old_original_path );
248 if ( $is_backup_original ) {
249 // delete old backup file
250 $backup_filename = self::$SqueezeHelpers->create_backup_filename( $old_filename );
251 $old_backup_path = $upload_path . $backup_filename;
252 wp_delete_file( $old_backup_path );
253 }
254 }
255 try {
256 $response_msg = self::$SqueezeHelpers->get_comparison_table( $sizes );
257 } catch ( \Throwable $e ) {
258 if ( $attach_id ) {
259 update_post_meta( $attach_id, 'squeeze_compression_failed', 'comparison_table_error' );
260 }
261 wp_send_json_error( '' . esc_html__( 'Failed to build compression comparison.', 'squeeze' ) );
262 }
263 update_post_meta( $attach_id, "squeeze_is_compressed", true );
264 delete_post_meta( $attach_id, 'squeeze_compression_failed' );
265 $response_msg = '<strong>�
266 ' . esc_html__( 'Squeezed successfully', 'squeeze' ) . '!</strong> ' . $response_msg;
267 $uncompressed_images = self::$SqueezeHelpers->get_stats_option( 'uncompressed_images' );
268 $uncompressed_images--;
269 update_option( 'squeeze_stats', array(
270 'uncompressed_images' => $uncompressed_images,
271 ) );
272 /**
273 * Fires after Squeeze has written all compressed/WebP files for an attachment.
274 * Used by integrations such as WP Offload Media to push the new files to an
275 * external storage provider (S3, GCS, DigitalOcean Spaces, etc.).
276 *
277 * @since 1.8.0
278 * @param int $attach_id Attachment post ID.
279 */
280 do_action( 'squeeze_after_update_attachment', $attach_id );
281 do_action( 'squeeze_successful_squeeze' );
282 $response_data = array(
283 'message' => $response_msg,
284 'sizes' => $sizes,
285 'filename' => $filename,
286 'url' => $url,
287 );
288 wp_send_json_success( $response_data );
289 } else {
290 // Path/bulk: after Direct WebP write, remove the JPG/PNG twin and any leftover sidecar.
291 if ( $did_direct_rename && $old_filename !== $filename ) {
292 $old_path = $upload_path . $old_filename;
293 if ( file_exists( $old_path ) ) {
294 wp_delete_file( $old_path );
295 }
296 $sidecar_path = self::$SqueezeHelpers->convert_image_path_to_webp_path( $old_path ) . '.webp';
297 if ( file_exists( $sidecar_path ) ) {
298 wp_delete_file( $sidecar_path );
299 }
300 }
301 do_action( 'squeeze_successful_squeeze' );
302 wp_send_json_success( '�
303 ' . esc_html__( 'Squeezed successfully', 'squeeze' ) );
304 }
305 wp_die();
306 }
307
308 public function restore_attachment() {
309 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
310 if ( !isset( $_POST["attachmentID"] ) || empty( $_POST["attachmentID"] ) || !wp_get_attachment_url( $_POST["attachmentID"] ) ) {
311 wp_send_json_error( '' . esc_html__( 'Attachment not found', 'squeeze' ) );
312 }
313 $attach_id = (int) $_POST["attachmentID"];
314 $can_restore = self::$SqueezeHelpers->can_restore( $attach_id );
315 if ( $can_restore ) {
316 $is_restore_attachment = self::$SqueezeHelpers->restore_attachment( $attach_id );
317 if ( !is_wp_error( $is_restore_attachment ) ) {
318 wp_send_json_success( '�
319 ' . esc_html__( 'Restored successfully', 'squeeze' ) );
320 } else {
321 wp_send_json_error( ' ' . esc_html__( 'Attachment not restored', 'squeeze' ) );
322 }
323 }
324 wp_die();
325 }
326
327 public function get_attachment() {
328 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
329 if ( !current_user_can( 'upload_files' ) ) {
330 wp_send_json_error( ' ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
331 }
332 if ( !isset( $_POST["attachmentID"] ) || empty( $_POST["attachmentID"] ) || !wp_get_attachment_url( $_POST["attachmentID"] ) ) {
333 wp_send_json_error( ' ' . esc_html__( 'Attachment not found', 'squeeze' ) );
334 }
335 $attach_id = (int) $_POST["attachmentID"];
336 // Load excluded patterns once per request (cached in SqueezeHelpers::get_excluded_images()).
337 $excluded_images = self::$SqueezeHelpers->get_excluded_images();
338 $full_image = wp_get_attachment_image_src( $attach_id, 'full' );
339 if ( !empty( $excluded_images ) && !empty( $full_image[0] ) ) {
340 if ( $is_excluded = self::$SqueezeHelpers->is_excluded_image( $full_image[0], $excluded_images ) ) {
341 wp_send_json_error( ' ' . esc_html__( 'Attachment is excluded from compression', 'squeeze' ) . ' (' . esc_html__( 'found substring: ', 'squeeze' ) . $is_excluded['exclude_reason'] . ')' );
342 }
343 }
344 // Get attachment metadata once (contains sizes data)
345 $metadata = wp_get_attachment_metadata( $attach_id );
346 $sizes = ( isset( $metadata['sizes'] ) ? $metadata['sizes'] : array() );
347 // Cache file paths to avoid repeated function calls
348 $attached_file = get_attached_file( $attach_id );
349 $original_image_path = wp_get_original_image_path( $attach_id );
350 $is_squeezed = get_post_meta( $attach_id, 'squeeze_is_compressed', true );
351 // -scaled image
352 $sizes['full'] = array(
353 'url' => $full_image[0],
354 'width' => $full_image[1],
355 'height' => $full_image[2],
356 'filesize' => wp_filesize( $attached_file ),
357 );
358 // Build size URLs (WordPress caches these internally)
359 foreach ( $sizes as $size_name => $size_data ) {
360 $sizes[$size_name]['url'] = wp_get_attachment_image_url( $attach_id, $size_name );
361 }
362 $attachment_data = array(
363 'id' => $attach_id,
364 'url' => wp_get_original_image_url( $attach_id ),
365 'mime' => get_post_mime_type( $attach_id ),
366 'name' => get_the_title( $attach_id ),
367 'filename' => basename( $original_image_path ),
368 'sizes' => $sizes,
369 'is_squeezed' => $is_squeezed,
370 );
371 wp_send_json_success( $attachment_data );
372 wp_die();
373 }
374
375 public function get_attachment_by_path() {
376 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
377 if ( !current_user_can( 'upload_files' ) ) {
378 wp_send_json_error( ' ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
379 }
380 if ( !isset( $_POST["path"] ) || empty( $_POST["path"] ) ) {
381 wp_send_json_error( ' ' . esc_html__( 'Path not found', 'squeeze' ) );
382 }
383 $pathes = sanitize_text_field( $_POST["path"] );
384 $pathes = json_decode( stripslashes( $pathes ), true );
385 if ( !is_array( $pathes ) ) {
386 wp_send_json_error( ' ' . esc_html__( 'Invalid path data', 'squeeze' ) );
387 }
388 $pathes = array_map( array(self::$SqueezeHelpers, 'normalize_bulk_directory_storage_path'), $pathes );
389 $attachment_data = array();
390 $image_formats = self::$SqueezeHelpers->get_image_formats();
391 $image_formats = implode( ',', $image_formats );
392 // MIME type mapping based on file extension (much faster than exif_imagetype)
393 $mime_type_map = array(
394 'jpg' => 'image/jpeg',
395 'jpeg' => 'image/jpeg',
396 'png' => 'image/png',
397 'webp' => 'image/webp',
398 'avif' => 'image/avif',
399 'gif' => 'image/gif',
400 );
401 // Load excluded patterns once per request (cached in SqueezeHelpers::get_excluded_images()).
402 $excluded_images = self::$SqueezeHelpers->get_excluded_images();
403 // Cache home URL and normalize ABSPATH to avoid repeated function calls and string operations
404 $home_url = trailingslashit( home_url() );
405 $abspath_normalized = str_replace( '\\', '/', ABSPATH );
406 foreach ( $pathes as $path ) {
407 // Remove dangerous patterns related to directory traversal
408 $path = preg_replace( [
409 '/\\.\\.+/',
410 // Remove multiple dots (.., ...)
411 '/\\/\\*/',
412 ], '', $path );
413 // replace multiple backslashes with slashes
414 $path = preg_replace( ['/\\/+/'], '/', $path );
415 // Add trailing slash if it's not there
416 if ( substr( $path, -1 ) !== '/' ) {
417 $path .= '/';
418 }
419 // Add leading slash if it's not there
420 if ( substr( $path, 0, 1 ) !== '/' ) {
421 $path = '/' . $path;
422 }
423 $images = glob( ABSPATH . $path . '*.{' . $image_formats . '}', GLOB_BRACE );
424 if ( empty( $images ) ) {
425 continue;
426 }
427 foreach ( $images as $image ) {
428 // Get file extension for MIME type detection (much faster than exif_imagetype)
429 $extension = strtolower( pathinfo( $image, PATHINFO_EXTENSION ) );
430 // Skip if extension not in our map (safety check)
431 if ( !isset( $mime_type_map[$extension] ) ) {
432 continue;
433 }
434 $attach_mime = $mime_type_map[$extension];
435 $filename = basename( $image );
436 // Convert file path to URL efficiently
437 // Normalize path separators and replace ABSPATH with home URL
438 $image_normalized = str_replace( '\\', '/', $image );
439 $attach_url = str_replace( $abspath_normalized, $home_url, $image_normalized );
440 if ( !empty( $excluded_images ) ) {
441 if ( $is_excluded = self::$SqueezeHelpers->is_excluded_image( $attach_url, $excluded_images ) ) {
442 $attachment_data[] = array(
443 'excluded' => true,
444 'exclude_reason' => $is_excluded['exclude_reason'],
445 'filename' => $filename,
446 );
447 continue;
448 }
449 }
450 // Skip attachment_url_to_postid() to avoid expensive database queries
451 // Path-based compression works with ID = 0 (files not in media library)
452 $attach_id = 0;
453 $attach_name = pathinfo( $image, PATHINFO_FILENAME );
454 $attachment_data[] = array(
455 'id' => $attach_id,
456 'url' => $attach_url,
457 'mime' => $attach_mime,
458 'name' => $attach_name,
459 'filename' => $filename,
460 );
461 }
462 }
463 // Save pathes to cache
464 set_transient( 'squeeze_bulk_path', $pathes, MONTH_IN_SECONDS );
465 if ( empty( $attachment_data ) ) {
466 wp_send_json_error( '' . esc_html__( 'Images were not found in the selected directories', 'squeeze' ) );
467 }
468 wp_send_json_success( $attachment_data );
469 wp_die();
470 }
471
472 /**
473 * AJAX: whether a filename matches the exclusion list (used before plupload compression).
474 */
475 public function check_file_excluded() {
476 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
477 if ( !isset( $_POST['fileName'] ) || $_POST['fileName'] === '' ) {
478 wp_send_json_error( '' . esc_html__( 'File name not found', 'squeeze' ) );
479 }
480 $file_name = sanitize_text_field( wp_unslash( $_POST['fileName'] ) );
481 $excluded_images = self::$SqueezeHelpers->get_excluded_images();
482 $result = self::$SqueezeHelpers->is_excluded_image( $file_name, $excluded_images );
483 if ( $result ) {
484 wp_send_json_success( array(
485 'is_excluded' => true,
486 'exclude_reason' => $result['exclude_reason'],
487 ) );
488 }
489 wp_send_json_success( array(
490 'is_excluded' => false,
491 ) );
492 }
493
494 /**
495 * Proxy an attachment image through WordPress to avoid CORS errors in the Web Worker.
496 *
497 * When WP Offload Media (or any CDN plugin) serves images from an external provider
498 * (e.g. Google Cloud Storage, Amazon S3), the image URLs are on a different origin than
499 * the WordPress admin. The Web Worker uses fetch() to load images before compressing them.
500 * Browsers block cross-origin fetch() calls that lack proper CORS headers (which GCS/S3
501 * buckets typically do not emit for arbitrary origins).
502 *
503 * This endpoint fetches the image server-side — where there is no CORS restriction — and
504 * streams it back from the same origin as the admin, so the worker can fetch it without errors.
505 *
506 * Accepts GET parameters:
507 * attachment_id int Attachment post ID.
508 * size string WP image size name: 'original', 'full', or any registered size
509 * (e.g. 'thumbnail', 'medium', 'large').
510 * _ajax_nonce string squeeze-nonce value.
511 *
512 * @since 1.8.0
513 */
514 public function fetch_image() {
515 // Nonce verification (accepts GET or POST).
516 $nonce = ( isset( $_GET['_ajax_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_ajax_nonce'] ) ) : '' );
517 if ( !$nonce || !wp_verify_nonce( $nonce, 'squeeze-nonce' ) ) {
518 http_response_code( 403 );
519 wp_die( 'Invalid nonce.' );
520 }
521 if ( !current_user_can( 'upload_files' ) ) {
522 http_response_code( 403 );
523 wp_die( 'Unauthorized.' );
524 }
525 $attach_id = ( isset( $_GET['attachment_id'] ) ? (int) $_GET['attachment_id'] : 0 );
526 $size = ( isset( $_GET['size'] ) ? sanitize_text_field( wp_unslash( $_GET['size'] ) ) : 'original' );
527 if ( !$attach_id ) {
528 http_response_code( 404 );
529 wp_die( 'Attachment not found.' );
530 }
531 $mime = get_post_mime_type( $attach_id );
532 if ( !$mime || strpos( $mime, 'image/' ) !== 0 ) {
533 http_response_code( 400 );
534 wp_die( 'Not an image attachment.' );
535 }
536 // Determine the local filesystem path for the requested size.
537 $file_path = '';
538 if ( $size === 'original' ) {
539 $file_path = wp_get_original_image_path( $attach_id );
540 } elseif ( $size === 'full' ) {
541 $file_path = get_attached_file( $attach_id );
542 } else {
543 $meta = wp_get_attachment_metadata( $attach_id );
544 $base_dir = dirname( (string) get_attached_file( $attach_id ) );
545 if ( !empty( $meta['sizes'][$size]['file'] ) ) {
546 $file_path = trailingslashit( $base_dir ) . $meta['sizes'][$size]['file'];
547 }
548 }
549 // Serve directly from local filesystem when the file is present.
550 if ( $file_path && file_exists( $file_path ) ) {
551 $file_size = filesize( $file_path );
552 header( 'Content-Type: ' . $mime );
553 header( 'Content-Length: ' . $file_size );
554 header( 'Cache-Control: private, no-store' );
555 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
556 readfile( $file_path );
557 exit;
558 }
559 // Local file not found — the file may have been removed by WP Offload Media's
560 // "Remove Local Files" option. Fetch it from the provider URL server-side
561 // (no CORS restriction on the server) and stream it back to the browser.
562 if ( $size === 'original' ) {
563 $provider_url = wp_get_original_image_url( $attach_id );
564 } elseif ( $size === 'full' ) {
565 $provider_url = wp_get_attachment_url( $attach_id );
566 } else {
567 $src = wp_get_attachment_image_src( $attach_id, $size );
568 $provider_url = ( $src ? $src[0] : '' );
569 }
570 if ( !$provider_url ) {
571 http_response_code( 404 );
572 wp_die( 'Image not found.' );
573 }
574 $response = wp_remote_get( $provider_url, array(
575 'timeout' => 60,
576 'sslverify' => true,
577 ) );
578 if ( is_wp_error( $response ) ) {
579 http_response_code( 502 );
580 wp_die( 'Failed to fetch image from provider: ' . esc_html( $response->get_error_message() ) );
581 }
582 $http_status = wp_remote_retrieve_response_code( $response );
583 if ( $http_status !== 200 ) {
584 http_response_code( (int) $http_status );
585 wp_die( 'Provider returned HTTP ' . (int) $http_status );
586 }
587 $body = wp_remote_retrieve_body( $response );
588 header( 'Content-Type: ' . $mime );
589 header( 'Content-Length: ' . strlen( $body ) );
590 header( 'Cache-Control: private, no-store' );
591 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
592 echo $body;
593 exit;
594 }
595
596 public function delete_backup_attachment( $attach_id ) {
597 $original_img_path = wp_get_original_image_path( (int) $attach_id );
598 $backup_img_path = preg_replace( "/(\\.(?!.*\\.))/", '.bak.', $original_img_path );
599 if ( file_exists( $backup_img_path ) ) {
600 return wp_delete_file( $backup_img_path );
601 }
602 return false;
603 }
604
605 public function delete_webp_images( $attach_id, $old_metadata = null ) {
606 $original_img_path = wp_get_original_image_path( (int) $attach_id );
607 $attachment_data = ( $old_metadata ? $old_metadata : wp_get_attachment_metadata( $attach_id ) );
608 $delete_webp_images = self::$SqueezeHelpers->delete_webp_images( $original_img_path, $attachment_data );
609 return $delete_webp_images;
610 }
611
612 public function bulk_actions( $actions ) {
613 if ( !is_array( $actions ) ) {
614 $actions = array();
615 }
616 $actions['squeeze_bulk_restore'] = esc_html__( 'Restore Original Image', 'squeeze' );
617 $actions['squeeze_bulk_compress'] = esc_html__( 'Squeeze Image', 'squeeze' );
618 $actions['squeeze_bulk_delete_backup'] = esc_html__( 'Delete Backup Image', 'squeeze' );
619 $actions['squeeze_bulk_delete_webp'] = esc_html__( 'Delete WEBP Image', 'squeeze' );
620 return $actions;
621 }
622
623 public function handle_bulk_actions( $redirect_to, $doaction, $post_ids ) {
624 if ( $doaction === 'squeeze_bulk_restore' ) {
625 $restored_ids_count = 0;
626 foreach ( $post_ids as $post_id ) {
627 $can_restore = self::$SqueezeHelpers->can_restore( $post_id );
628 if ( $can_restore ) {
629 $is_restore_attachment = self::$SqueezeHelpers->restore_attachment( $post_id, true );
630 if ( $is_restore_attachment ) {
631 $restored_ids_count += 1;
632 }
633 }
634 }
635 $redirect_to = add_query_arg( 'squeeze_bulk_restored', $restored_ids_count, $redirect_to );
636 }
637 if ( $doaction === 'squeeze_bulk_compress' ) {
638 foreach ( $post_ids as $post_id ) {
639 $redirect_to = add_query_arg( 'squeeze_bulk_compressed', count( $post_ids ), $redirect_to );
640 }
641 }
642 if ( $doaction === 'squeeze_bulk_delete_backup' ) {
643 $deleted_ids_count = 0;
644 foreach ( $post_ids as $post_id ) {
645 $is_delete_backup = $this->delete_backup_attachment( $post_id );
646 if ( $is_delete_backup ) {
647 $deleted_ids_count += 1;
648 }
649 }
650 $redirect_to = add_query_arg( 'squeeze_bulk_deleted', $deleted_ids_count, $redirect_to );
651 }
652 if ( $doaction === 'squeeze_bulk_delete_webp' ) {
653 $deleted_ids_count = 0;
654 foreach ( $post_ids as $post_id ) {
655 $is_delete_webp = $this->delete_webp_images( $post_id );
656 if ( $is_delete_webp ) {
657 $deleted_ids_count += 1;
658 }
659 }
660 $redirect_to = add_query_arg( 'squeeze_bulk_webp_deleted', $deleted_ids_count, $redirect_to );
661 }
662 return $redirect_to;
663 }
664
665 public function bulk_action_admin_notice() {
666 if ( !empty( $_REQUEST['squeeze_bulk_restored'] ) ) {
667 $message = sprintf(
668 /* translators: %d: number of attachments restored */
669 _n(
670 '%d attachment restored.',
671 '%d attachments restored.',
672 $_REQUEST['squeeze_bulk_restored'],
673 'squeeze'
674 ),
675 number_format_i18n( $_REQUEST['squeeze_bulk_restored'] )
676 );
677 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
678 }
679 if ( !empty( $_REQUEST['squeeze_bulk_compressed'] ) ) {
680 $message = sprintf(
681 /* translators: %d: number of attachments squeezed */
682 _n(
683 '%d attachment squeezed.',
684 '%d attachments squeezed.',
685 $_REQUEST['squeeze_bulk_compressed'],
686 'squeeze'
687 ),
688 number_format_i18n( $_REQUEST['squeeze_bulk_compressed'] )
689 );
690 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
691 }
692 if ( !empty( $_REQUEST['squeeze_bulk_deleted'] ) ) {
693 $message = sprintf(
694 /* translators: %d: number of backup images deleted */
695 _n(
696 '%d backup image deleted.',
697 '%d backup images deleted.',
698 $_REQUEST['squeeze_bulk_deleted'],
699 'squeeze'
700 ),
701 number_format_i18n( $_REQUEST['squeeze_bulk_deleted'] )
702 );
703 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
704 }
705 if ( !empty( $_REQUEST['squeeze_bulk_webp_deleted'] ) ) {
706 $message = sprintf(
707 /* translators: %d: number of webp images deleted */
708 _n(
709 '%d WEBP image deleted.',
710 '%d WEBP images deleted.',
711 $_REQUEST['squeeze_bulk_webp_deleted'],
712 'squeeze'
713 ),
714 number_format_i18n( $_REQUEST['squeeze_bulk_webp_deleted'] )
715 );
716 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
717 }
718 }
719
720 public function custom_image_sizes( $sizes ) {
721 $available_sizes = wp_get_registered_image_subsizes();
722 if ( empty( $available_sizes ) ) {
723 return $sizes;
724 }
725 foreach ( $available_sizes as $size_name => $size_data ) {
726 $sizes[$size_name] = $size_data['width'] . 'x' . $size_data['height'];
727 }
728 return $sizes;
729 }
730
731 public function get_next_attachments() {
732 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
733 $per_page = self::$MEDIA_PER_PAGE;
734 $page = ( isset( $_POST['page'] ) ? (int) $_POST['page'] : 1 );
735 $type = ( isset( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : 'uncompressed' );
736 $last_id = ( isset( $_POST['lastId'] ) ? (int) $_POST['lastId'] : 0 );
737 if ( $type === 'uncompressed' ) {
738 $next_images = self::$SqueezeHelpers->get_uncompressed_images( $last_id );
739 } else {
740 $next_images = self::$SqueezeHelpers->get_total_images( $page );
741 }
742 wp_send_json_success( $next_images );
743 }
744
745 public function single_file_upload_notice() {
746 global $current_screen;
747 if ( $current_screen->id === 'media' ) {
748 ?>
749 <div class="notice notice-warning hide-if-js squeeze-single-file-upload-notice">
750 <p><?php
751 esc_html_e( 'Single file upload is not supported for the image compression by Squeeze. Please use multi-file uploader or bulk squeeze.', 'squeeze' );
752 ?></p>
753 </div>
754 <?php
755 }
756 }
757
758 public function get_directories() {
759 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
760 if ( !current_user_can( 'manage_options' ) ) {
761 wp_send_json_error( '' . esc_html__( 'You do not have permission to browse directories.', 'squeeze' ) );
762 }
763 $parent_directory = ( isset( $_POST['parentDir'] ) ? sanitize_text_field( wp_unslash( $_POST['parentDir'] ) ) : '' );
764 $allowed_base = realpath( WP_CONTENT_DIR );
765 if ( $allowed_base === false ) {
766 wp_send_json_error( '' . esc_html__( 'Content directory is not accessible.', 'squeeze' ) );
767 }
768 if ( $parent_directory === '' ) {
769 $base_dir = $allowed_base;
770 } else {
771 // Parent from UI is site-relative (/wp-content/foo/); resolve under WP_CONTENT_DIR only.
772 $rel = self::$SqueezeHelpers->bulk_parent_relative_to_content_dir( $parent_directory );
773 if ( '' !== $rel && preg_match( '#(^|/)\\.\\.(/|$)#', $rel ) ) {
774 wp_send_json_error( '' . esc_html__( 'Invalid directory path.', 'squeeze' ) );
775 }
776 $candidate = $allowed_base . (( '' !== $rel ? '/' . $rel : '' ));
777 $base_dir = realpath( $candidate );
778 if ( $base_dir === false || strpos( $base_dir, $allowed_base ) !== 0 ) {
779 wp_send_json_error( '' . esc_html__( 'Invalid directory path.', 'squeeze' ) );
780 }
781 }
782 $directories = scandir( $base_dir );
783 if ( !$parent_directory ) {
784 $directories[] = $base_dir;
785 }
786 $result = array_filter( $directories, function ( $dir ) use($base_dir) {
787 if ( $dir === $base_dir ) {
788 return true;
789 }
790 return is_dir( $base_dir . '/' . $dir ) && !in_array( $dir, ['.', '..'] );
791 } );
792 $output = array_map( function ( $dir ) use($base_dir) {
793 if ( $dir === 'squeeze-webp' ) {
794 return [
795 'name' => '',
796 'path' => '',
797 'is_writeable' => false,
798 'parent' => '',
799 ];
800 }
801 if ( $dir === $base_dir ) {
802 $path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( $dir . '/' );
803 $parent_path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( dirname( $base_dir ) . '/' );
804 return [
805 'name' => 'wp-content',
806 'path' => $path,
807 'is_writeable' => false,
808 'parent' => $parent_path,
809 ];
810 }
811 $path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( $base_dir . '/' . $dir . '/' );
812 $parent_path = self::$SqueezeHelpers->bulk_directory_uri_from_filesystem( dirname( $base_dir . '/' . $dir ) . '/' );
813 return [
814 'name' => $dir,
815 'path' => $path,
816 'is_writeable' => wp_is_writable( $base_dir . '/' . $dir ),
817 'parent' => $parent_path,
818 ];
819 }, $result );
820 usort( $output, function ( $a, $b ) {
821 if ( $a['name'] === 'wp-content' ) {
822 return -1;
823 } elseif ( $b['name'] === 'wp-content' ) {
824 return 1;
825 }
826 return strcmp( $a['name'], $b['name'] );
827 } );
828 wp_send_json( $output );
829 }
830
831 public function add_webp_rewrite_rules( $rules ) {
832 $is_auto_webp = self::$SqueezeHelpers->get_option( 'auto_webp' );
833 $modules = self::$SqueezeHelpers->apache_get_modules();
834 // Get the WordPress installation subdirectory, if applicable
835 $wordpress_subdirectory = wp_parse_url( home_url(), PHP_URL_PATH );
836 // Check if WordPress is installed in a subdirectory (not just the root)
837 if ( strlen( $wordpress_subdirectory ) > 1 ) {
838 // Ensure the subdirectory is used correctly in the rules
839 $rewrite_base = $wordpress_subdirectory . '/';
840 } else {
841 // If WordPress is installed in the root, no subdirectory path is needed
842 $rewrite_base = '/';
843 }
844 $webp_rules = "\n# Serve WebP images from the wp-content/squeeze-webp folder if available\n";
845 $webp_rules .= "RewriteCond %{HTTP_ACCEPT} image/webp\n";
846 // Check if browser supports WebP
847 $webp_rules .= "RewriteCond %{REQUEST_URI} \\.(jpg|jpeg|png)\$ [NC]\n";
848 // Check if request is for JPG, JPEG, or PNG
849 $webp_rules .= "RewriteCond %{DOCUMENT_ROOT}" . $rewrite_base . "wp-content/squeeze-webp/\$1.\$2.webp -f\n";
850 // Check if WebP file exists
851 $webp_rules .= "RewriteRule ^wp-content/(.+)\\.(jpg|jpeg|png)\$ wp-content/squeeze-webp/\$1.\$2.webp [T=image/webp,E=webp_request,L]\n";
852 // Serve WebP file
853 $webp_rules .= "# END Serve WebP images from the wp-content/squeeze-webp folder if available\n";
854 $webp_rules .= "\n";
855 if ( !$is_auto_webp ) {
856 // If auto WebP conversion is disabled, return the original rules and replace the WebP rules if they exist
857 $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 );
858 return $rules;
859 }
860 // Skip only when we can positively detect that mod_rewrite is absent.
861 // Under CGI/FPM, apache_get_modules() is unavailable even when Apache has
862 // mod_rewrite — same situation WordPress still writes permalink rules for.
863 // Rules sit inside WP's <IfModule mod_rewrite.c> block.
864 if ( is_array( $modules ) && !in_array( 'mod_rewrite', $modules, true ) ) {
865 return $rules;
866 }
867 return $webp_rules . $rules;
868 }
869
870 public function output_buffer_start() {
871 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 ) {
872 ob_start( [$this, 'replace_image_urls_with_webp'] );
873 }
874 }
875
876 /**
877 * While WordPress flushes automatically, you can add an explicit handler on shutdown
878 * (with higher priority than 1, e.g., 0) to ensure clean flushing if there are conflicts
879 * (e.g., with zlib compression or other plugins).
880 * This suppresses potential PHP notices like "failed to send buffer of zlib output compression"
881 * without discarding content:
882 */
883 public function output_buffer_end() {
884 while ( ob_get_level() > 0 ) {
885 @ob_end_flush();
886 // Suppress notices; flushes modified content
887 }
888 }
889
890 // Other helper functions
891 public function replace_image_urls_with_webp( $content ) {
892 $is_replace_urls = self::$SqueezeHelpers->is_webp_replace_urls();
893 $is_direct_webp = self::$SqueezeHelpers->get_option( 'direct_webp' );
894 if ( !$is_replace_urls && !$is_direct_webp ) {
895 return $content;
896 }
897 $content_folder = basename( WP_CONTENT_DIR );
898 // Regular expression to find JPG and PNG images in src and srcset attributes.
899 //$pattern = '/(\/\/.*?\/' . preg_quote($content_folder, '/') . '\/)([^"\s]+\.(jpg|jpeg|png))(\?[^"\s]*)?/i';
900 // Supports query params, size suffixes, and encoded filenames.
901 $pattern = '/(\\/\\/[^"\\s]*?' . preg_quote( $content_folder, '/' ) . '\\/[^"\\s]+\\.(jpg|jpeg|png))(\\?[^"\\s]*)?/i';
902 // Callback function to replace the URLs.
903 $callback = function ( $matches ) use($is_direct_webp, $is_replace_urls) {
904 // $matches[0] is the full matched URL
905 $full_match = $matches[0];
906 $url_no_query = $matches[1];
907 $query = ( isset( $matches[3] ) ? $matches[3] : '' );
908 $file_extension = strtolower( pathinfo( $url_no_query, PATHINFO_EXTENSION ) );
909 if ( $file_extension === 'webp' ) {
910 return $full_match;
911 // Already a WebP image, no need to replace.
912 }
913 $protocol = ( is_ssl() ? 'https:' : 'http:' );
914 $abs_url = $protocol . $url_no_query;
915 $file_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $abs_url );
916 if ( $is_replace_urls ) {
917 $webp_url = self::$SqueezeHelpers->convert_image_path_to_webp_path( $url_no_query ) . '.webp';
918 // WebP URL like 'example.com/wp-content/squeeze-webp/uploads/2024/12/test.jpg.webp'
919 // Check if the exact sized WEBP sidecar exists on the server (local disk check).
920 $webp_file_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $protocol . $webp_url );
921 $webp_exists = $webp_file_path !== '' && file_exists( $webp_file_path );
922 if ( $webp_exists ) {
923 return $webp_url;
924 // Use WEBP version if it exists.
925 }
926 // Fallback: sized/scaled/rotated URL → base (unsuffixed) .webp sidecar.
927 $base_webp_candidates = array();
928 if ( preg_match( '/-\\d+x\\d+\\.[^.]+\\.webp$/i', $webp_url ) ) {
929 $base_webp_candidates[] = preg_replace( '/-\\d+x\\d+(\\.[^.]+)\\.webp$/i', '$1.webp', $webp_url );
930 }
931 if ( preg_match( '/-scaled\\.[^.]+\\.webp$/i', $webp_url ) ) {
932 $base_webp_candidates[] = preg_replace( '/-scaled(\\.[^.]+)\\.webp$/i', '$1.webp', $webp_url );
933 }
934 if ( preg_match( '/-rotated\\.[^.]+\\.webp$/i', $webp_url ) ) {
935 $base_webp_candidates[] = preg_replace( '/-rotated(\\.[^.]+)\\.webp$/i', '$1.webp', $webp_url );
936 }
937 foreach ( $base_webp_candidates as $candidate_webp_url ) {
938 if ( $candidate_webp_url === $webp_url ) {
939 continue;
940 }
941 $candidate_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $protocol . $candidate_webp_url );
942 if ( $candidate_path !== '' && file_exists( $candidate_path ) ) {
943 return $candidate_webp_url;
944 }
945 }
946 }
947 // Direct WebP: prefer a sibling .webp when present (e.g. hardcoded CSS bg still points at .jpg
948 // after conversion, or path/bulk left both files on disk).
949 if ( $is_direct_webp ) {
950 // try to find possible WEBP variants
951 $webp_candidates = [];
952 // Base WebP
953 $webp_url = preg_replace( '/\\.[^.]+$/i', '.webp', $url_no_query );
954 $webp_candidates[] = $webp_url;
955 // Unscaled version (remove -scaled)
956 if ( preg_match( '/-scaled\\.[^.]+$/i', $webp_url ) ) {
957 $webp_candidates[] = preg_replace( '/-scaled\\.[^.]+$/i', '.webp', $webp_url );
958 }
959 // Dimensioned variants (e.g. test-300x200.jpg)
960 if ( preg_match( '/-\\d+x\\d+\\.[^.]+$/i', $webp_url ) ) {
961 $webp_candidates[] = preg_replace( '/-\\d+x\\d+\\.[^.]+$/i', '.webp', $webp_url );
962 }
963 // Numbered suffixes: test-1.webp, test-2.webp...
964 for ($i = 1; $i <= 99; $i++) {
965 $webp_candidates[] = preg_replace( '/(-scaled)?\\.[^.]+$/i', '-' . $i . '.webp', $webp_url );
966 }
967 foreach ( $webp_candidates as $candidate ) {
968 $candidate_full = ( strpos( $candidate, '//' ) === 0 ? $protocol . $candidate : $candidate );
969 $candidate_path = self::$SqueezeHelpers->resolve_media_url_to_abspath( $candidate_full );
970 if ( $candidate_path !== '' && file_exists( $candidate_path ) ) {
971 return $candidate . $query;
972 }
973 }
974 /*
975 // try to find webp without -scaled suffix
976 $webp_url_no_scaled = preg_replace('/-scaled\.webp$/', '.webp', $webp_url);
977 $webp_file_path_no_scaled = str_replace(home_url(), ABSPATH, $protocol.$webp_url_no_scaled); // Convert URL to file path.
978 if (file_exists($webp_file_path_no_scaled)) {
979 return $webp_url_no_scaled; // Use WEBP version if it exists.
980 } else {
981 // loop through the files with number suffixes
982 for ($i = 1; $i <= 99; $i++) {
983 $webp_url_numbered = preg_replace('/(-scaled)?\.webp$/', '-' . $i . '.webp', $webp_url);
984 $webp_file_path_numbered = str_replace(home_url(), ABSPATH, $protocol.$webp_url_numbered); // Convert URL to file path.
985 if (file_exists($webp_file_path_numbered)) {
986 return $webp_url_numbered; // Use WEBP version if it exists.
987 }
988 }
989 }
990 //*/
991 }
992 return $full_match;
993 // Fallback to the original URL if WEBP file doesn't exist.
994 };
995 // Replace URLs in the content, including src and srcset attributes.
996 $content = preg_replace_callback( $pattern, $callback, $content );
997 return $content;
998 }
999
1000 public function set_options() {
1001 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
1002 if ( !current_user_can( 'manage_options' ) ) {
1003 wp_send_json_error( '' . esc_html__( 'You do not have permission to change plugin settings', 'squeeze' ) );
1004 }
1005 $options = ( isset( $_POST['options'] ) ? $_POST['options'] : array() );
1006 if ( empty( $options ) ) {
1007 wp_send_json_error( '' . esc_html__( 'Options not found', 'squeeze' ) );
1008 }
1009 $is_set_options = self::$SqueezeHelpers->set_options( $options );
1010 if ( !$is_set_options ) {
1011 wp_send_json_error( '' . esc_html__( 'Options not saved', 'squeeze' ) );
1012 }
1013 wp_send_json_success( '�
1014 ' . esc_html__( 'Options saved successfully', 'squeeze' ) );
1015 }
1016
1017 public function update_attachment_metadata_for_js( $response, $attachment, $meta ) {
1018 if ( isset( $response['filesizeInBytes'] ) && isset( $response['filesizeHumanReadable'] ) ) {
1019 // check if the attachment is compressed
1020 $is_squeezed = get_post_meta( $attachment->ID, 'squeeze_is_compressed', true );
1021 if ( !$is_squeezed ) {
1022 return $response;
1023 }
1024 // get updated filesize from the actual file
1025 $attachment_path = get_attached_file( $attachment->ID );
1026 if ( !$attachment_path || !file_exists( $attachment_path ) ) {
1027 return $response;
1028 }
1029 $filesize = wp_filesize( $attachment_path );
1030 $filesize_human = size_format( $filesize );
1031 $image_info = wp_getimagesize( $attachment_path );
1032 $response['filesizeInBytes'] = $filesize;
1033 $response['filesizeHumanReadable'] = $filesize_human;
1034 if ( $image_info ) {
1035 $response['width'] = $image_info[0];
1036 $response['height'] = $image_info[1];
1037 }
1038 }
1039 return $response;
1040 }
1041
1042 public function update_attachment_metadata( $data, $attachment_id ) {
1043 if ( isset( $data['filesize'] ) ) {
1044 // check if the attachment is compressed
1045 $is_squeezed = get_post_meta( $attachment_id, 'squeeze_is_compressed', true );
1046 if ( !$is_squeezed ) {
1047 return $data;
1048 }
1049 // get updated filesize from the actual file
1050 $attachment_path = get_attached_file( $attachment_id );
1051 if ( !$attachment_path || !file_exists( $attachment_path ) ) {
1052 return $data;
1053 }
1054 $filesize = wp_filesize( $attachment_path );
1055 $image_info = wp_getimagesize( $attachment_path );
1056 $data['filesize'] = $filesize;
1057 if ( $image_info ) {
1058 $image_width = $image_info[0];
1059 $image_height = $image_info[1];
1060 $data['width'] = $image_width;
1061 $data['height'] = $image_height;
1062 }
1063 }
1064 return $data;
1065 }
1066
1067 }
1068