PluginProbe
Squeeze – Image Optimization & Compression, WEBP Conversion / 1.7.8
Squeeze – Image Optimization & Compression, WEBP Conversion v1.7.8
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 1.7.8, at inc/handlers.php

800 lines 40.4 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_get_attachment_by_path', [$this, 'get_attachment_by_path'] );
15 add_action( 'wp_ajax_squeeze_get_next_attachments', [$this, 'get_next_attachments'] );
16 add_action( 'wp_ajax_squeeze_get_directories', [$this, 'get_directories'] );
17 add_action( 'wp_ajax_squeeze_set_options', [$this, 'set_options'] );
18 add_action( 'delete_attachment', [$this, 'delete_backup_attachment'] );
19 add_action( 'delete_attachment', [$this, 'delete_webp_images'] );
20 add_filter( 'bulk_actions-upload', [$this, 'bulk_actions'] );
21 add_filter(
22 'handle_bulk_actions-upload',
23 [$this, 'handle_bulk_actions'],
24 10,
25 3
26 );
27 add_filter( 'image_size_names_choose', [$this, 'custom_image_sizes'] );
28 add_filter( 'mod_rewrite_rules', [$this, 'add_webp_rewrite_rules'] );
29 add_action( 'pre-html-upload-ui', [$this, 'single_file_upload_notice'], 10 );
30 add_action( 'admin_notices', [$this, 'bulk_action_admin_notice'] );
31 add_action( 'init', [$this, 'output_buffer_start'], 1 );
32 add_action( 'shutdown', [$this, 'output_buffer_end'], 0 );
33 // test this
34 add_filter(
35 'wp_prepare_attachment_for_js',
36 [$this, 'update_attachment_metadata_for_js'],
37 10,
38 3
39 );
40 add_filter(
41 'wp_get_attachment_metadata',
42 [$this, 'update_attachment_metadata'],
43 10,
44 2
45 );
46 }
47
48 public function update_attachment() {
49 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
50 if ( !current_user_can( 'upload_files' ) ) {
51 wp_send_json_error( '' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
52 }
53 if ( !isset( $_POST["base64"] ) || empty( $_POST["base64"] ) ) {
54 wp_send_json_error( '' . esc_html__( 'No image data found', 'squeeze' ) );
55 }
56 $base64 = sanitize_text_field( wp_unslash( $_POST["base64"] ) );
57 $sizes = ( isset( $_POST["base64Sizes"] ) ? (array) $_POST["base64Sizes"] : array() );
58 // DO NOT SANITIZE because it's an array
59 $base64_webp = ( isset( $_POST["base64Webp"] ) ? sanitize_text_field( wp_unslash( $_POST["base64Webp"] ) ) : '' );
60 $sizes_webp = ( isset( $_POST["base64SizesWebp"] ) ? (array) $_POST["base64SizesWebp"] : array() );
61 $file_format = ( isset( $_POST["format"] ) ? sanitize_text_field( wp_unslash( $_POST["format"] ) ) : '' );
62 $filename = ( isset( $_POST["filename"] ) ? sanitize_text_field( wp_unslash( $_POST["filename"] ) ) : '' );
63 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
64 // handle jpg/jpeg extension
65 if ( $extension === 'jpeg' ) {
66 $extension = 'jpg';
67 }
68 $image_formats = self::$SqueezeHelpers->get_image_formats();
69 $original_file = ( isset( $_FILES['originalFile'] ) ? $_FILES['originalFile'] : null );
70 if ( !in_array( $extension, $image_formats ) || empty( $file_format ) ) {
71 wp_send_json_error( '' . esc_html__( 'Invalid image format', 'squeeze' ) );
72 }
73 $attach_id = ( isset( $_POST["attachmentID"] ) ? (int) $_POST["attachmentID"] : 0 );
74 $meta_data = wp_get_attachment_metadata( $attach_id );
75 $url = ( isset( $_POST["url"] ) ? sanitize_text_field( $_POST["url"] ) : '' );
76 // sanitize_url() replaces spaces with %20, so we use sanitize_text_field() instead
77 $process = ( isset( $_POST["process"] ) ? sanitize_text_field( $_POST["process"] ) : '' );
78 // process: all, uncompressed, path
79 if ( empty( $attach_id ) && $process !== 'path' || empty( $url ) ) {
80 wp_send_json_error( '' . esc_html__( 'Attachment not found', 'squeeze' ) );
81 }
82 $is_backup_original = self::$SqueezeHelpers->get_option( 'backup_original' );
83 $is_direct_webp = self::$SqueezeHelpers->get_option( 'direct_webp' );
84 // Upload path.
85 $upload_path = self::$SqueezeHelpers->get_upload_path( $attach_id, $filename, $url );
86 $decoded = self::$SqueezeHelpers->decode_base64_image( $base64, $file_format );
87 $webp_file_path = '';
88 $old_metadata = wp_get_attachment_metadata( $attach_id );
89 $old_filename = $filename;
90 // in case the original file is not webp and we should convert it to webp
91 // we need to generate a new filename for the webp file
92 // and update its url
93 if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp && $process !== 'path' ) {
94 $file = get_attached_file( $attach_id );
95 // get the _wp_attached_file meta value which looks like "2023/10/image.jpg"
96 $path_info = pathinfo( $file );
97 $dirname = $path_info['dirname'];
98 //Get a unique filename in that folder
99 $unique_filename = wp_unique_filename( $dirname, sanitize_file_name( preg_replace( '/\\.[^.]+$/', '.webp', $filename ) ) );
100 $webp_file_path = trailingslashit( $dirname ) . $unique_filename;
101 // looks like "E:\Extra-Time\test/wp-content/uploads/2023/10/image.webp"
102 $filename = basename( $unique_filename );
103 $url = str_replace( ABSPATH, home_url( '/' ), $webp_file_path );
104 // convert file path to URL
105 //wp_send_json_error($filename . ' ' . $url . ' ' . $webp_file_path);
106 }
107 if ( $original_file ) {
108 $sizes['original']['original_size'] = $original_file['size'];
109 } else {
110 $sizes['original']['original_size'] = wp_filesize( $upload_path . $filename );
111 }
112 $sizes['original']['compressed_size'] = strlen( $decoded );
113 // check if compressed_size is greater than original_size
114 if ( $sizes['original']['compressed_size'] > $sizes['original']['original_size'] ) {
115 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 ) );
116 }
117 if ( $is_backup_original && $process !== 'path' ) {
118 // do not backup for non library images
119 // backup original
120 if ( $original_file ) {
121 $backup_original_image = self::$SqueezeHelpers->backup_original_image( $upload_path, $filename, $original_file['tmp_name'] );
122 } else {
123 $backup_original_image = self::$SqueezeHelpers->backup_original_image( $upload_path, $filename );
124 }
125 if ( is_wp_error( $backup_original_image ) ) {
126 wp_send_json_error( $backup_original_image->get_error_message() );
127 }
128 }
129 // Save the image in the uploads directory.
130 $upload_image = self::$SqueezeHelpers->upload_image( $upload_path, $filename, $decoded );
131 if ( is_wp_error( $upload_image ) ) {
132 wp_send_json_error( $upload_image->get_error_message() );
133 }
134 if ( $base64_webp ) {
135 $upload_webp = self::$SqueezeHelpers->upload_webp( $upload_path, $base64_webp, $filename );
136 if ( is_wp_error( $upload_webp ) ) {
137 wp_send_json_error( $upload_webp->get_error_message() );
138 }
139 $upload_webp_thumbs = self::$SqueezeHelpers->upload_webp_thumbs( $upload_path, $sizes_webp );
140 // skip handling errors for webp thumbs, because they are not always required
141 }
142 // upload thumbnails
143 if ( $process !== 'path' ) {
144 if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) {
145 update_attached_file( $attach_id, $webp_file_path );
146 // update the _wp_attached_file meta value to the new webp file path
147 wp_update_post( [
148 'ID' => $attach_id,
149 'post_mime_type' => 'image/webp',
150 ] );
151 $metadata = wp_generate_attachment_metadata( $attach_id, $webp_file_path );
152 wp_update_attachment_metadata( $attach_id, $metadata );
153 }
154 $sizes = self::$SqueezeHelpers->upload_image_thumbs(
155 $upload_path,
156 $sizes,
157 $file_format,
158 $filename
159 );
160 //wp_send_json_error( print_r($sizes, true) );
161 if ( is_wp_error( $sizes ) ) {
162 wp_send_json_error( $sizes->get_error_message() );
163 }
164 if ( $file_format === 'webp' && $extension !== 'webp' && $is_direct_webp ) {
165 // remove webp images from the squeeze-webp directory
166 $this->delete_webp_images( $attach_id, $old_metadata );
167 // remove original JPG/PNG file if it exists
168 foreach ( $old_metadata['sizes'] as $size_name => $size_data ) {
169 $old_size_filename = $size_data['file'];
170 wp_delete_file( $upload_path . $old_size_filename );
171 }
172 $old_scaled_filename = basename( $old_metadata['file'] );
173 wp_delete_file( $upload_path . $old_scaled_filename );
174 $old_original_path = $upload_path . $old_filename;
175 wp_delete_file( $old_original_path );
176 if ( $is_backup_original ) {
177 // delete old backup file
178 $backup_filename = self::$SqueezeHelpers->create_backup_filename( $old_filename );
179 $old_backup_path = $upload_path . $backup_filename;
180 wp_delete_file( $old_backup_path );
181 }
182 }
183 update_post_meta( $attach_id, "squeeze_is_compressed", true );
184 $response_msg = self::$SqueezeHelpers->get_comparison_table( $sizes );
185 $response_msg = '<strong>�
186 ' . esc_html__( 'Squeezed successfully', 'squeeze' ) . '!</strong> ' . $response_msg;
187 $uncompressed_images = self::$SqueezeHelpers->get_stats_option( 'uncompressed_images' );
188 $uncompressed_images--;
189 update_option( 'squeeze_stats', array(
190 'uncompressed_images' => $uncompressed_images,
191 ) );
192 //wp_send_json_error( print_r($sizes, true) );
193 //wp_send_json_success($response_msg . print_r($sizes['scaled']['url'], true) . ' | ' . $filename . ' | ' . pathinfo($filename, PATHINFO_FILENAME) );
194 //wp_send_json_success($response_msg);
195 wp_send_json_success( array(
196 'message' => $response_msg,
197 'sizes' => $sizes,
198 'filename' => $filename,
199 'url' => $url,
200 ) );
201 } else {
202 wp_send_json_success( '�
203 ' . esc_html__( 'Squeezed successfully', 'squeeze' ) );
204 }
205 wp_die();
206 }
207
208 public function restore_attachment() {
209 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
210 if ( !isset( $_POST["attachmentID"] ) || empty( $_POST["attachmentID"] ) || !wp_get_attachment_url( $_POST["attachmentID"] ) ) {
211 wp_send_json_error( '' . esc_html__( 'Attachment not found', 'squeeze' ) );
212 }
213 $attach_id = (int) $_POST["attachmentID"];
214 $can_restore = self::$SqueezeHelpers->can_restore( $attach_id );
215 if ( $can_restore ) {
216 $is_restore_attachment = self::$SqueezeHelpers->restore_attachment( $attach_id );
217 if ( !is_wp_error( $is_restore_attachment ) ) {
218 wp_send_json_success( '�
219 ' . esc_html__( 'Restored successfully', 'squeeze' ) );
220 } else {
221 wp_send_json_error( ' ' . esc_html__( 'Attachment not restored', 'squeeze' ) );
222 }
223 }
224 wp_die();
225 }
226
227 public function get_attachment() {
228 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
229 if ( !current_user_can( 'upload_files' ) ) {
230 wp_send_json_error( ' ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
231 }
232 if ( !isset( $_POST["attachmentID"] ) || empty( $_POST["attachmentID"] ) || !wp_get_attachment_url( $_POST["attachmentID"] ) ) {
233 wp_send_json_error( ' ' . esc_html__( 'Attachment not found', 'squeeze' ) );
234 }
235 $attach_id = (int) $_POST["attachmentID"];
236 // Load excluded images once per request (Premium only) - cached in get_excluded_images()
237 $excluded_images = array();
238 // Get attachment metadata once (contains sizes data)
239 $metadata = wp_get_attachment_metadata( $attach_id );
240 $sizes = ( isset( $metadata['sizes'] ) ? $metadata['sizes'] : array() );
241 $full_image = wp_get_attachment_image_src( $attach_id, 'full' );
242 // Cache file paths to avoid repeated function calls
243 $attached_file = get_attached_file( $attach_id );
244 $original_image_path = wp_get_original_image_path( $attach_id );
245 $is_squeezed = get_post_meta( $attach_id, 'squeeze_is_compressed', true );
246 // -scaled image
247 $sizes['full'] = array(
248 'url' => $full_image[0],
249 'width' => $full_image[1],
250 'height' => $full_image[2],
251 'filesize' => wp_filesize( $attached_file ),
252 );
253 // Build size URLs (WordPress caches these internally)
254 foreach ( $sizes as $size_name => $size_data ) {
255 $sizes[$size_name]['url'] = wp_get_attachment_image_url( $attach_id, $size_name );
256 }
257 $attachment_data = array(
258 'id' => $attach_id,
259 'url' => wp_get_original_image_url( $attach_id ),
260 'mime' => get_post_mime_type( $attach_id ),
261 'name' => get_the_title( $attach_id ),
262 'filename' => basename( $original_image_path ),
263 'sizes' => $sizes,
264 'is_squeezed' => $is_squeezed,
265 );
266 wp_send_json_success( $attachment_data );
267 wp_die();
268 }
269
270 public function get_attachment_by_path() {
271 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
272 if ( !current_user_can( 'upload_files' ) ) {
273 wp_send_json_error( ' ' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
274 }
275 if ( !isset( $_POST["path"] ) || empty( $_POST["path"] ) ) {
276 wp_send_json_error( ' ' . esc_html__( 'Path not found', 'squeeze' ) );
277 }
278 $pathes = sanitize_text_field( $_POST["path"] );
279 $pathes = json_decode( stripslashes( $pathes ), true );
280 $attachment_data = array();
281 $image_formats = self::$SqueezeHelpers->get_image_formats();
282 $image_formats = implode( ',', $image_formats );
283 // MIME type mapping based on file extension (much faster than exif_imagetype)
284 $mime_type_map = array(
285 'jpg' => 'image/jpeg',
286 'jpeg' => 'image/jpeg',
287 'png' => 'image/png',
288 'webp' => 'image/webp',
289 'avif' => 'image/avif',
290 'gif' => 'image/gif',
291 );
292 // Load excluded images once per request (Premium only) - cached in get_excluded_images()
293 $excluded_images = array();
294 // Cache home URL and normalize ABSPATH to avoid repeated function calls and string operations
295 $home_url = trailingslashit( home_url() );
296 $abspath_normalized = str_replace( '\\', '/', ABSPATH );
297 foreach ( $pathes as $path ) {
298 // Remove dangerous patterns related to directory traversal
299 $path = preg_replace( [
300 '/\\.\\.+/',
301 // Remove multiple dots (.., ...)
302 '/\\/\\*/',
303 ], '', $path );
304 // replace multiple backslashes with slashes
305 $path = preg_replace( ['/\\/+/'], '/', $path );
306 // Add trailing slash if it's not there
307 if ( substr( $path, -1 ) !== '/' ) {
308 $path .= '/';
309 }
310 // Add leading slash if it's not there
311 if ( substr( $path, 0, 1 ) !== '/' ) {
312 $path = '/' . $path;
313 }
314 $images = glob( ABSPATH . $path . '*.{' . $image_formats . '}', GLOB_BRACE );
315 if ( empty( $images ) ) {
316 continue;
317 }
318 foreach ( $images as $image ) {
319 // Get file extension for MIME type detection (much faster than exif_imagetype)
320 $extension = strtolower( pathinfo( $image, PATHINFO_EXTENSION ) );
321 // Skip if extension not in our map (safety check)
322 if ( !isset( $mime_type_map[$extension] ) ) {
323 continue;
324 }
325 $attach_mime = $mime_type_map[$extension];
326 $filename = basename( $image );
327 // Convert file path to URL efficiently
328 // Normalize path separators and replace ABSPATH with home URL
329 $image_normalized = str_replace( '\\', '/', $image );
330 $attach_url = str_replace( $abspath_normalized, $home_url, $image_normalized );
331 // Skip attachment_url_to_postid() to avoid expensive database queries
332 // Path-based compression works with ID = 0 (files not in media library)
333 $attach_id = 0;
334 $attach_name = pathinfo( $image, PATHINFO_FILENAME );
335 $attachment_data[] = array(
336 'id' => $attach_id,
337 'url' => $attach_url,
338 'mime' => $attach_mime,
339 'name' => $attach_name,
340 'filename' => $filename,
341 );
342 }
343 }
344 // Save pathes to cache
345 set_transient( 'squeeze_bulk_path', $pathes, MONTH_IN_SECONDS );
346 if ( empty( $attachment_data ) ) {
347 wp_send_json_error( '' . esc_html__( 'Images were not found in the selected directories', 'squeeze' ) );
348 }
349 wp_send_json_success( $attachment_data );
350 wp_die();
351 }
352
353 public function delete_backup_attachment( $attach_id ) {
354 $original_img_path = wp_get_original_image_path( (int) $attach_id );
355 $backup_img_path = preg_replace( "/(\\.(?!.*\\.))/", '.bak.', $original_img_path );
356 if ( file_exists( $backup_img_path ) ) {
357 return wp_delete_file( $backup_img_path );
358 }
359 return false;
360 }
361
362 public function delete_webp_images( $attach_id, $old_metadata = null ) {
363 $original_img_path = wp_get_original_image_path( (int) $attach_id );
364 $attachment_data = ( $old_metadata ? $old_metadata : wp_get_attachment_metadata( $attach_id ) );
365 $delete_webp_images = self::$SqueezeHelpers->delete_webp_images( $original_img_path, $attachment_data );
366 return $delete_webp_images;
367 }
368
369 public function bulk_actions( $actions ) {
370 if ( !is_array( $actions ) ) {
371 $actions = array();
372 }
373 $actions['squeeze_bulk_restore'] = esc_html__( 'Restore Original Image', 'squeeze' );
374 $actions['squeeze_bulk_compress'] = esc_html__( 'Squeeze Image', 'squeeze' );
375 $actions['squeeze_bulk_delete_backup'] = esc_html__( 'Delete Backup Image', 'squeeze' );
376 $actions['squeeze_bulk_delete_webp'] = esc_html__( 'Delete WEBP Image', 'squeeze' );
377 return $actions;
378 }
379
380 public function handle_bulk_actions( $redirect_to, $doaction, $post_ids ) {
381 if ( $doaction === 'squeeze_bulk_restore' ) {
382 $restored_ids_count = 0;
383 foreach ( $post_ids as $post_id ) {
384 $can_restore = self::$SqueezeHelpers->can_restore( $post_id );
385 if ( $can_restore ) {
386 $is_restore_attachment = self::$SqueezeHelpers->restore_attachment( $post_id, true );
387 if ( $is_restore_attachment ) {
388 $restored_ids_count += 1;
389 }
390 }
391 }
392 $redirect_to = add_query_arg( 'squeeze_bulk_restored', $restored_ids_count, $redirect_to );
393 }
394 if ( $doaction === 'squeeze_bulk_compress' ) {
395 foreach ( $post_ids as $post_id ) {
396 $redirect_to = add_query_arg( 'squeeze_bulk_compressed', count( $post_ids ), $redirect_to );
397 }
398 }
399 if ( $doaction === 'squeeze_bulk_delete_backup' ) {
400 $deleted_ids_count = 0;
401 foreach ( $post_ids as $post_id ) {
402 $is_delete_backup = $this->delete_backup_attachment( $post_id );
403 if ( $is_delete_backup ) {
404 $deleted_ids_count += 1;
405 }
406 }
407 $redirect_to = add_query_arg( 'squeeze_bulk_deleted', $deleted_ids_count, $redirect_to );
408 }
409 if ( $doaction === 'squeeze_bulk_delete_webp' ) {
410 $deleted_ids_count = 0;
411 foreach ( $post_ids as $post_id ) {
412 $is_delete_webp = $this->delete_webp_images( $post_id );
413 if ( $is_delete_webp ) {
414 $deleted_ids_count += 1;
415 }
416 }
417 $redirect_to = add_query_arg( 'squeeze_bulk_webp_deleted', $deleted_ids_count, $redirect_to );
418 }
419 return $redirect_to;
420 }
421
422 public function bulk_action_admin_notice() {
423 if ( !empty( $_REQUEST['squeeze_bulk_restored'] ) ) {
424 $message = sprintf(
425 /* translators: %d: number of attachments restored */
426 _n(
427 '%d attachment restored.',
428 '%d attachments restored.',
429 $_REQUEST['squeeze_bulk_restored'],
430 'squeeze'
431 ),
432 number_format_i18n( $_REQUEST['squeeze_bulk_restored'] )
433 );
434 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
435 }
436 if ( !empty( $_REQUEST['squeeze_bulk_compressed'] ) ) {
437 $message = sprintf(
438 /* translators: %d: number of attachments squeezed */
439 _n(
440 '%d attachment squeezed.',
441 '%d attachments squeezed.',
442 $_REQUEST['squeeze_bulk_compressed'],
443 'squeeze'
444 ),
445 number_format_i18n( $_REQUEST['squeeze_bulk_compressed'] )
446 );
447 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
448 }
449 if ( !empty( $_REQUEST['squeeze_bulk_deleted'] ) ) {
450 $message = sprintf(
451 /* translators: %d: number of backup images deleted */
452 _n(
453 '%d backup image deleted.',
454 '%d backup images deleted.',
455 $_REQUEST['squeeze_bulk_deleted'],
456 'squeeze'
457 ),
458 number_format_i18n( $_REQUEST['squeeze_bulk_deleted'] )
459 );
460 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
461 }
462 if ( !empty( $_REQUEST['squeeze_bulk_webp_deleted'] ) ) {
463 $message = sprintf(
464 /* translators: %d: number of webp images deleted */
465 _n(
466 '%d WEBP image deleted.',
467 '%d WEBP images deleted.',
468 $_REQUEST['squeeze_bulk_webp_deleted'],
469 'squeeze'
470 ),
471 number_format_i18n( $_REQUEST['squeeze_bulk_webp_deleted'] )
472 );
473 printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html( $message ) );
474 }
475 }
476
477 public function custom_image_sizes( $sizes ) {
478 $available_sizes = wp_get_registered_image_subsizes();
479 if ( empty( $available_sizes ) ) {
480 return $sizes;
481 }
482 foreach ( $available_sizes as $size_name => $size_data ) {
483 $sizes[$size_name] = $size_data['width'] . 'x' . $size_data['height'];
484 }
485 return $sizes;
486 }
487
488 public function get_next_attachments() {
489 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
490 $per_page = self::$MEDIA_PER_PAGE;
491 $page = ( isset( $_POST['page'] ) ? (int) $_POST['page'] : 1 );
492 $type = ( isset( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : 'uncompressed' );
493 $last_id = ( isset( $_POST['lastId'] ) ? (int) $_POST['lastId'] : 0 );
494 if ( $type === 'uncompressed' ) {
495 $next_images = self::$SqueezeHelpers->get_uncompressed_images( $last_id );
496 } else {
497 $next_images = self::$SqueezeHelpers->get_total_images( $page );
498 }
499 wp_send_json_success( $next_images );
500 }
501
502 public function single_file_upload_notice() {
503 global $current_screen;
504 if ( $current_screen->id === 'media' ) {
505 ?>
506 <div class="notice notice-warning hide-if-js squeeze-single-file-upload-notice">
507 <p><?php
508 esc_html_e( 'Single file upload is not supported for the image compression by Squeeze. Please use multi-file uploader or bulk squeeze.', 'squeeze' );
509 ?></p>
510 </div>
511 <?php
512 }
513 }
514
515 public function get_directories() {
516 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
517 if ( !current_user_can( 'manage_options' ) ) {
518 wp_send_json_error( '' . esc_html__( 'You do not have permission to browse directories.', 'squeeze' ) );
519 }
520 $parent_directory = ( isset( $_POST['parentDir'] ) ? sanitize_text_field( wp_unslash( $_POST['parentDir'] ) ) : '' );
521 $allowed_base = realpath( WP_CONTENT_DIR );
522 if ( $allowed_base === false ) {
523 wp_send_json_error( '' . esc_html__( 'Content directory is not accessible.', 'squeeze' ) );
524 }
525 if ( $parent_directory === '' ) {
526 $base_dir = $allowed_base;
527 } else {
528 // Prevent path traversal: resolve path and ensure it stays under WP_CONTENT_DIR.
529 $parent_directory = str_replace( '\\', '/', $parent_directory );
530 $parent_directory = preg_replace( '#/+#', '/', trim( $parent_directory, '/' ) );
531 if ( $parent_directory === '' || preg_match( '#(^|/)\\.\\.(/|$)#', $parent_directory ) ) {
532 wp_send_json_error( '' . esc_html__( 'Invalid directory path.', 'squeeze' ) );
533 }
534 $base_dir = realpath( $allowed_base . '/' . $parent_directory );
535 if ( $base_dir === false || strpos( $base_dir, $allowed_base ) !== 0 ) {
536 wp_send_json_error( '' . esc_html__( 'Invalid directory path.', 'squeeze' ) );
537 }
538 }
539 $directories = scandir( $base_dir );
540 if ( !$parent_directory ) {
541 $directories[] = $base_dir;
542 }
543 $result = array_filter( $directories, function ( $dir ) use($base_dir) {
544 if ( $dir === $base_dir ) {
545 return true;
546 }
547 return is_dir( $base_dir . '/' . $dir ) && !in_array( $dir, ['.', '..'] );
548 } );
549 $output = array_map( function ( $dir ) use($base_dir) {
550 if ( $dir === 'squeeze-webp' ) {
551 return [
552 'name' => '',
553 'path' => '',
554 'is_writeable' => false,
555 'parent' => '',
556 ];
557 }
558 if ( $dir === $base_dir ) {
559 $path = str_replace( ABSPATH, '/', $dir . '/' );
560 $parent_path = dirname( $base_dir );
561 $parent_path = str_replace( ABSPATH, '/', $parent_path . '/' );
562 return [
563 'name' => 'wp-content',
564 'path' => $path,
565 'is_writeable' => false,
566 'parent' => $parent_path,
567 ];
568 }
569 $path = str_replace( ABSPATH, '/', $base_dir . '/' . $dir . '/' );
570 $parent_path = dirname( $base_dir . '/' . $dir );
571 $parent_path = str_replace( ABSPATH, '/', $parent_path . '/' );
572 // Remove double slashes from path
573 $path = preg_replace( '/\\/+/', '/', $path );
574 $parent_path = preg_replace( '/\\/+/', '/', $parent_path );
575 return [
576 'name' => $dir,
577 'path' => $path,
578 'is_writeable' => wp_is_writable( $base_dir . '/' . $dir ),
579 'parent' => $parent_path,
580 ];
581 }, $result );
582 usort( $output, function ( $a, $b ) {
583 if ( $a['name'] === 'wp-content' ) {
584 return -1;
585 } elseif ( $b['name'] === 'wp-content' ) {
586 return 1;
587 }
588 return strcmp( $a['name'], $b['name'] );
589 } );
590 wp_send_json( $output );
591 }
592
593 public function add_webp_rewrite_rules( $rules ) {
594 $is_auto_webp = self::$SqueezeHelpers->get_option( 'auto_webp' );
595 $modules = self::$SqueezeHelpers->apache_get_modules();
596 // Get the WordPress installation subdirectory, if applicable
597 $wordpress_subdirectory = wp_parse_url( home_url(), PHP_URL_PATH );
598 // Check if WordPress is installed in a subdirectory (not just the root)
599 if ( strlen( $wordpress_subdirectory ) > 1 ) {
600 // Ensure the subdirectory is used correctly in the rules
601 $rewrite_base = $wordpress_subdirectory . '/';
602 } else {
603 // If WordPress is installed in the root, no subdirectory path is needed
604 $rewrite_base = '/';
605 }
606 $webp_rules = "\n# Serve WebP images from the wp-content/squeeze-webp folder if available\n";
607 $webp_rules .= "RewriteCond %{HTTP_ACCEPT} image/webp\n";
608 // Check if browser supports WebP
609 $webp_rules .= "RewriteCond %{REQUEST_URI} \\.(jpg|jpeg|png)\$ [NC]\n";
610 // Check if request is for JPG, JPEG, or PNG
611 $webp_rules .= "RewriteCond %{DOCUMENT_ROOT}" . $rewrite_base . "wp-content/squeeze-webp/\$1.\$2.webp -f\n";
612 // Check if WebP file exists
613 $webp_rules .= "RewriteRule ^wp-content/(.+)\\.(jpg|jpeg|png)\$ wp-content/squeeze-webp/\$1.\$2.webp [T=image/webp,E=webp_request,L]\n";
614 // Serve WebP file
615 $webp_rules .= "# END Serve WebP images from the wp-content/squeeze-webp folder if available\n";
616 $webp_rules .= "\n";
617 if ( !$is_auto_webp ) {
618 // If auto WebP conversion is disabled, return the original rules and replace the WebP rules if they exist
619 $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 );
620 return $rules;
621 }
622 // Check if the server is Apache and htaccess is writable
623 if ( !is_array( $modules ) || !in_array( 'mod_rewrite', $modules ) ) {
624 return $rules;
625 }
626 return $webp_rules . $rules;
627 }
628
629 public function output_buffer_start() {
630 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 ) {
631 ob_start( [$this, 'replace_image_urls_with_webp'] );
632 }
633 }
634
635 /**
636 * While WordPress flushes automatically, you can add an explicit handler on shutdown
637 * (with higher priority than 1, e.g., 0) to ensure clean flushing if there are conflicts
638 * (e.g., with zlib compression or other plugins).
639 * This suppresses potential PHP notices like "failed to send buffer of zlib output compression"
640 * without discarding content:
641 */
642 public function output_buffer_end() {
643 while ( ob_get_level() > 0 ) {
644 @ob_end_flush();
645 // Suppress notices; flushes modified content
646 }
647 }
648
649 // Other helper functions
650 public function replace_image_urls_with_webp( $content ) {
651 $is_replace_urls = self::$SqueezeHelpers->is_webp_replace_urls();
652 $is_direct_webp = self::$SqueezeHelpers->get_option( 'direct_webp' );
653 if ( !$is_replace_urls && !$is_direct_webp ) {
654 return $content;
655 }
656 $content_folder = basename( WP_CONTENT_DIR );
657 // Regular expression to find JPG and PNG images in src and srcset attributes.
658 //$pattern = '/(\/\/.*?\/' . preg_quote($content_folder, '/') . '\/)([^"\s]+\.(jpg|jpeg|png))(\?[^"\s]*)?/i';
659 // Supports query params, size suffixes, and encoded filenames.
660 $pattern = '/(\\/\\/[^"\\s]*?' . preg_quote( $content_folder, '/' ) . '\\/[^"\\s]+\\.(jpg|jpeg|png))(\\?[^"\\s]*)?/i';
661 // Callback function to replace the URLs.
662 $callback = function ( $matches ) use($is_direct_webp, $is_replace_urls) {
663 // $matches[0] is the full matched URL
664 $full_match = $matches[0];
665 $url_no_query = $matches[1];
666 $query = ( isset( $matches[3] ) ? $matches[3] : '' );
667 $file_extension = strtolower( pathinfo( $url_no_query, PATHINFO_EXTENSION ) );
668 if ( $file_extension === 'webp' ) {
669 return $full_match;
670 // Already a WebP image, no need to replace.
671 }
672 $protocol = ( is_ssl() ? 'https:' : 'http:' );
673 $file_path = str_replace( home_url(), ABSPATH, $protocol . $url_no_query );
674 // Convert URL to file path.
675 if ( $is_replace_urls ) {
676 $webp_url = self::$SqueezeHelpers->convert_image_path_to_webp_path( $url_no_query ) . '.webp';
677 // WebP URL like 'example.com/wp-content/squeeze-webp/uploads/2024/12/test.jpg.webp'
678 // Check if the WEBP file exists on the server.
679 $webp_file_path = str_replace( home_url(), ABSPATH, $protocol . $webp_url );
680 // Convert URL to file path.
681 //return $webp_file_path.'::'.$webp_url;
682 if ( file_exists( $webp_file_path ) ) {
683 return $webp_url;
684 // Use WEBP version if it exists.
685 }
686 }
687 if ( $is_direct_webp && !file_exists( $file_path ) ) {
688 // try to find possible WEBP variants
689 $webp_candidates = [];
690 // Base WebP
691 $webp_url = preg_replace( '/\\.[^.]+$/i', '.webp', $url_no_query );
692 $webp_candidates[] = $webp_url;
693 // Unscaled version (remove -scaled)
694 if ( preg_match( '/-scaled\\.[^.]+$/i', $webp_url ) ) {
695 $webp_candidates[] = preg_replace( '/-scaled\\.[^.]+$/i', '.webp', $webp_url );
696 }
697 // Dimensioned variants (e.g. test-300x200.jpg)
698 if ( preg_match( '/-\\d+x\\d+\\.[^.]+$/i', $webp_url ) ) {
699 $webp_candidates[] = preg_replace( '/-\\d+x\\d+\\.[^.]+$/i', '.webp', $webp_url );
700 }
701 // Numbered suffixes: test-1.webp, test-2.webp...
702 for ($i = 1; $i <= 99; $i++) {
703 $webp_candidates[] = preg_replace( '/(-scaled)?\\.[^.]+$/i', '-' . $i . '.webp', $webp_url );
704 }
705 foreach ( $webp_candidates as $candidate ) {
706 $candidate_full = ( strpos( $candidate, '//' ) === 0 ? $protocol . $candidate : $candidate );
707 $candidate_path = str_replace( home_url(), ABSPATH, $candidate_full );
708 if ( file_exists( $candidate_path ) ) {
709 return $candidate . $query;
710 }
711 }
712 /*
713 // try to find webp without -scaled suffix
714 $webp_url_no_scaled = preg_replace('/-scaled\.webp$/', '.webp', $webp_url);
715 $webp_file_path_no_scaled = str_replace(home_url(), ABSPATH, $protocol.$webp_url_no_scaled); // Convert URL to file path.
716 if (file_exists($webp_file_path_no_scaled)) {
717 return $webp_url_no_scaled; // Use WEBP version if it exists.
718 } else {
719 // loop through the files with number suffixes
720 for ($i = 1; $i <= 99; $i++) {
721 $webp_url_numbered = preg_replace('/(-scaled)?\.webp$/', '-' . $i . '.webp', $webp_url);
722 $webp_file_path_numbered = str_replace(home_url(), ABSPATH, $protocol.$webp_url_numbered); // Convert URL to file path.
723 if (file_exists($webp_file_path_numbered)) {
724 return $webp_url_numbered; // Use WEBP version if it exists.
725 }
726 }
727 }
728 //*/
729 }
730 return $full_match;
731 // Fallback to the original URL if WEBP file doesn't exist.
732 };
733 // Replace URLs in the content, including src and srcset attributes.
734 $content = preg_replace_callback( $pattern, $callback, $content );
735 return $content;
736 }
737
738 public function set_options() {
739 check_ajax_referer( 'squeeze-nonce', '_ajax_nonce' );
740 if ( !current_user_can( 'upload_files' ) ) {
741 wp_send_json_error( '' . esc_html__( 'You do not have permission to upload files', 'squeeze' ) );
742 }
743 $options = ( isset( $_POST['options'] ) ? $_POST['options'] : array() );
744 if ( empty( $options ) ) {
745 wp_send_json_error( '' . esc_html__( 'Options not found', 'squeeze' ) );
746 }
747 $is_set_options = self::$SqueezeHelpers->set_options( $options );
748 if ( !$is_set_options ) {
749 wp_send_json_error( '' . esc_html__( 'Options not saved', 'squeeze' ) );
750 }
751 wp_send_json_success( '�
752 ' . esc_html__( 'Options saved successfully', 'squeeze' ) );
753 }
754
755 public function update_attachment_metadata_for_js( $response, $attachment, $meta ) {
756 if ( isset( $response['filesizeInBytes'] ) && isset( $response['filesizeHumanReadable'] ) ) {
757 // check if the attachment is compressed
758 $is_squeezed = get_post_meta( $attachment->ID, 'squeeze_is_compressed', true );
759 if ( !$is_squeezed ) {
760 return $response;
761 }
762 // get updated filesize from the actual file
763 $attachment_path = get_attached_file( $attachment->ID );
764 $filesize = wp_filesize( $attachment_path );
765 $filesize_human = size_format( $filesize );
766 $image_info = getimagesize( $attachment_path );
767 $image_width = $image_info[0];
768 $image_height = $image_info[1];
769 $response['filesizeInBytes'] = $filesize;
770 $response['filesizeHumanReadable'] = $filesize_human;
771 $response['width'] = $image_width;
772 $response['height'] = $image_height;
773 }
774 return $response;
775 }
776
777 public function update_attachment_metadata( $data, $attachment_id ) {
778 if ( isset( $data['filesize'] ) ) {
779 // check if the attachment is compressed
780 $is_squeezed = get_post_meta( $attachment_id, 'squeeze_is_compressed', true );
781 if ( !$is_squeezed ) {
782 return $data;
783 }
784 // get updated filesize from the actual file
785 $attachment_path = get_attached_file( $attachment_id );
786 $filesize = wp_filesize( $attachment_path );
787 $image_info = getimagesize( $attachment_path );
788 $data['filesize'] = $filesize;
789 if ( $image_info ) {
790 $image_width = $image_info[0];
791 $image_height = $image_info[1];
792 $data['width'] = $image_width;
793 $data['height'] = $image_height;
794 }
795 }
796 return $data;
797 }
798
799 }
800