PluginProbe
Imsanity / 2.9.3
Imsanity v2.9.3
2.9.4 2.9.3 2.9.2 trunk 2.4.3 2.5.0 2.6.0 2.7.0 2.7.2 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.9.0 2.9.1
imsanity / imsanity.php

imsanity.php in Imsanity 2.9.3, at imsanity.php

442 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main file for Imsanity plugin.
4 *
5 * This file includes the core of Imsanity and the top-level image handler.
6 *
7 * @link https://wordpress.org/plugins/imsanity/
8 * @package Imsanity
9 */
10
11 /*
12 Plugin Name: Imsanity
13 Plugin URI: https://wordpress.org/plugins/imsanity/
14 Description: Imsanity stops insanely huge image uploads
15 Author: Exactly WWW
16 Domain Path: /languages
17 Version: 2.9.3
18 Requires at least: 6.7
19 Requires PHP: 7.4
20 Author URI: https://ewww.io/about/
21 License: GPLv3
22 */
23
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 define( 'IMSANITY_VERSION', '2.9.3' );
29 define( 'IMSANITY_SCHEMA_VERSION', '1.1' );
30
31 define( 'IMSANITY_SOURCE_POST', 1 );
32 define( 'IMSANITY_SOURCE_LIBRARY', 2 );
33 define( 'IMSANITY_SOURCE_OTHER', 4 );
34
35 /**
36 * The full path of the main plugin file.
37 *
38 * @var string IMSANITY_PLUGIN_FILE
39 */
40 define( 'IMSANITY_PLUGIN_FILE', __FILE__ );
41
42 /**
43 * The directory path of the main plugin file.
44 *
45 * @var string IMSANITY_PLUGIN_DIR
46 */
47 define( 'IMSANITY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
48
49 /**
50 * The path of the main plugin file, relative to the plugins/ folder.
51 *
52 * @var string IMSANITY_PLUGIN_FILE_REL
53 */
54 define( 'IMSANITY_PLUGIN_FILE_REL', plugin_basename( __FILE__ ) );
55
56 /**
57 * Initialize the plugin, setup constants.
58 */
59 function imsanity_init() {
60 if ( ! defined( 'IMSANITY_DEFAULT_MAX_WIDTH' ) ) {
61 define( 'IMSANITY_DEFAULT_MAX_WIDTH', (int) apply_filters( 'imsanity_default_max_width', 1920 ) );
62 }
63 if ( ! defined( 'IMSANITY_DEFAULT_MAX_HEIGHT' ) ) {
64 define( 'IMSANITY_DEFAULT_MAX_HEIGHT', (int) apply_filters( 'imsanity_default_max_height', 1920 ) );
65 }
66 if ( ! defined( 'IMSANITY_DEFAULT_BMP_TO_JPG' ) ) {
67 define( 'IMSANITY_DEFAULT_BMP_TO_JPG', (bool) apply_filters( 'imsanity_default_bmp_to_jpg', true ) );
68 }
69 if ( ! defined( 'IMSANITY_DEFAULT_PNG_TO_JPG' ) ) {
70 define( 'IMSANITY_DEFAULT_PNG_TO_JPG', (bool) apply_filters( 'imsanity_default_png_to_jpg', false ) );
71 }
72 if ( ! defined( 'IMSANITY_DEFAULT_QUALITY' ) ) {
73 define( 'IMSANITY_DEFAULT_QUALITY', (int) apply_filters( 'imsanity_default_quality', 82 ) );
74 }
75 if ( ! defined( 'IMSANITY_DEFAULT_AVIF_QUALITY' ) ) {
76 define( 'IMSANITY_DEFAULT_AVIF_QUALITY', (int) apply_filters( 'imsanity_default_avif_quality', 86 ) );
77 }
78 if ( ! defined( 'IMSANITY_DEFAULT_WEBP_QUALITY' ) ) {
79 define( 'IMSANITY_DEFAULT_WEBP_QUALITY', (int) apply_filters( 'imsanity_default_webp_quality', 86 ) );
80 }
81 }
82
83 /**
84 * Load translations for Imsanity.
85 */
86 function imsanity_load_textdomain() {
87 load_plugin_textdomain( 'imsanity', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
88 }
89
90 /**
91 * Import supporting libraries.
92 */
93 require_once plugin_dir_path( __FILE__ ) . 'libs/debug.php';
94 require_once plugin_dir_path( __FILE__ ) . 'libs/utils.php';
95 require_once plugin_dir_path( __FILE__ ) . 'settings.php';
96 require_once plugin_dir_path( __FILE__ ) . 'ajax.php';
97 require_once plugin_dir_path( __FILE__ ) . 'media.php';
98 if ( defined( 'WP_CLI' ) && WP_CLI ) {
99 require_once plugin_dir_path( __FILE__ ) . 'class-imsanity-cli.php';
100 }
101
102 /**
103 * Inspects the request and determines where the upload came from.
104 *
105 * @return IMSANITY_SOURCE_POST | IMSANITY_SOURCE_LIBRARY | IMSANITY_SOURCE_OTHER
106 */
107 function imsanity_get_source() {
108 imsanity_debug( __FUNCTION__ );
109 $id = array_key_exists( 'post_id', $_REQUEST ) ? (int) $_REQUEST['post_id'] : ''; // phpcs:ignore WordPress.Security.NonceVerification
110 $action = ! empty( $_REQUEST['action'] ) ? sanitize_key( $_REQUEST['action'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
111 imsanity_debug( "getting source for id=$id and action=$action" );
112
113 // Uncomment this (and remove the trailing .) to temporarily check the full $_SERVER vars.
114 // imsanity_debug( $_SERVER );.
115 $referer = '';
116 if ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {
117 $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) );
118 imsanity_debug( "http_referer: $referer" );
119 }
120
121 $request_uri = wp_referer_field( false );
122 imsanity_debug( "request URI: $request_uri" );
123
124 // A post_id indicates image is attached to a post.
125 if ( $id > 0 ) {
126 imsanity_debug( 'from a post (id)' );
127 return IMSANITY_SOURCE_POST;
128 }
129
130 // If the referrer is the post editor, that's a good indication the image is attached to a post.
131 if ( false !== strpos( $referer, '/post.php' ) ) {
132 imsanity_debug( 'from a post.php' );
133 return IMSANITY_SOURCE_POST;
134 }
135 // If the referrer is the (new) post editor, that's a good indication the image is attached to a post.
136 if ( false !== strpos( $referer, '/post-new.php' ) ) {
137 imsanity_debug( 'from a new post' );
138 return IMSANITY_SOURCE_POST;
139 }
140
141 // Post_id of 0 is 3.x otherwise use the action parameter.
142 if ( 0 === $id || 'upload-attachment' === $action ) {
143 imsanity_debug( 'from the library' );
144 return IMSANITY_SOURCE_LIBRARY;
145 }
146
147 // We don't know where this one came from but $_REQUEST['_wp_http_referer'] may contain info.
148 imsanity_debug( 'unknown source' );
149 return IMSANITY_SOURCE_OTHER;
150 }
151
152 /**
153 * Given the source, returns the max width/height.
154 *
155 * @example: list( $w, $h ) = imsanity_get_max_width_height( IMSANITY_SOURCE_LIBRARY );
156 * @param int $source One of IMSANITY_SOURCE_POST | IMSANITY_SOURCE_LIBRARY | IMSANITY_SOURCE_OTHER.
157 */
158 function imsanity_get_max_width_height( $source ) {
159 if ( ! defined( 'IMSANITY_DEFAULT_MAX_WIDTH' ) ) {
160 imsanity_init();
161 }
162 $w = (int) imsanity_get_option( 'imsanity_max_width', IMSANITY_DEFAULT_MAX_WIDTH );
163 $h = (int) imsanity_get_option( 'imsanity_max_height', IMSANITY_DEFAULT_MAX_HEIGHT );
164
165 switch ( $source ) {
166 case IMSANITY_SOURCE_POST:
167 break;
168 case IMSANITY_SOURCE_LIBRARY:
169 $w = (int) imsanity_get_option( 'imsanity_max_width_library', $w );
170 $h = (int) imsanity_get_option( 'imsanity_max_height_library', $h );
171 break;
172 default:
173 $w = (int) imsanity_get_option( 'imsanity_max_width_other', $w );
174 $h = (int) imsanity_get_option( 'imsanity_max_height_other', $h );
175 break;
176 }
177
178 // NOTE: filters MUST return an array of 2 items, or the defaults will be used.
179 return apply_filters( 'imsanity_get_max_width_height', array( $w, $h ), $source );
180 }
181
182 /**
183 * Handler after a file has been uploaded. If the file is an image, check the size
184 * to see if it is too big and, if so, resize and overwrite the original.
185 *
186 * @param array $params The parameters submitted with the upload.
187 */
188 function imsanity_handle_upload( $params ) {
189 imsanity_debug( __FUNCTION__ );
190
191 if ( empty( $params['file'] ) || empty( $params['type'] ) ) {
192 imsanity_debug( 'missing file or type parameter, skipping' );
193 return $params;
194 }
195
196 // If "noresize" is included in the filename then we will bypass imsanity scaling.
197 if ( strpos( $params['file'], 'noresize' ) !== false ) {
198 imsanity_debug( "skipping {$params['file']}" );
199 return $params;
200 }
201
202 // Make sure default constants are defined, just in case this triggers before the init action.
203 if ( ! defined( 'IMSANITY_DEFAULT_MAX_WIDTH' ) ) {
204 imsanity_init();
205 }
206
207 if ( apply_filters( 'imsanity_skip_image', false, $params['file'] ) ) {
208 imsanity_debug( "skipping {$params['file']} per filter" );
209 return $params;
210 }
211
212 // If preferences specify so then we can convert an original bmp or png file into jpg.
213 if ( ( 'image/bmp' === $params['type'] || 'image/x-ms-bmp' === $params['type'] ) && imsanity_get_option( 'imsanity_bmp_to_jpg', IMSANITY_DEFAULT_BMP_TO_JPG ) ) {
214 $params = imsanity_convert_to_jpg( 'bmp', $params );
215 }
216
217 if ( 'image/png' === $params['type'] && imsanity_get_option( 'imsanity_png_to_jpg', IMSANITY_DEFAULT_PNG_TO_JPG ) ) {
218 $params = imsanity_convert_to_jpg( 'png', $params );
219 }
220
221 // Store the path for reference in case $params is modified.
222 $oldpath = $params['file'];
223
224 // Let folks filter the allowed mime-types for resizing.
225 // Also allows conditional support for WebP and AVIF if the server supports it.
226 $allowed_types = apply_filters( 'imsanity_allowed_mimes', array( 'image/png', 'image/gif', 'image/jpeg' ), $oldpath );
227 if ( is_string( $allowed_types ) ) {
228 $allowed_types = array( $allowed_types );
229 } elseif ( ! is_array( $allowed_types ) ) {
230 $allowed_types = array();
231 }
232
233 if (
234 ( ! is_wp_error( $params ) ) &&
235 is_file( $oldpath ) &&
236 is_readable( $oldpath ) &&
237 is_writable( $oldpath ) &&
238 filesize( $oldpath ) > 0 &&
239 in_array( $params['type'], $allowed_types, true )
240 ) {
241 // If the Modern Image Formats plugin is active but fallback mode is disabled, permit conversion to AVIF/WebP during upload by defining IMSANITY_ALLOW_CONVERSION.
242 // Otherwise, no conversion should be allowed at all. The upload handler will still check for conversion and work with it if it happens somehow.
243 if ( ! defined( 'IMSANITY_ALLOW_CONVERSION' ) && function_exists( 'webp_uploads_is_fallback_enabled' ) && ! webp_uploads_is_fallback_enabled() ) {
244 define( 'IMSANITY_ALLOW_CONVERSION', true );
245 }
246
247 // figure out where the upload is coming from.
248 $source = imsanity_get_source();
249
250 $maxw = IMSANITY_DEFAULT_MAX_WIDTH;
251 $maxh = IMSANITY_DEFAULT_MAX_HEIGHT;
252 $max_width_height = imsanity_get_max_width_height( $source );
253 if ( is_array( $max_width_height ) && 2 === count( $max_width_height ) ) {
254 list( $maxw, $maxh ) = $max_width_height;
255 }
256 $maxw = (int) $maxw;
257 $maxh = (int) $maxh;
258
259 $dimensions = getimagesize( $oldpath );
260 if ( is_array( $dimensions ) && count( $dimensions ) >= 2 ) {
261 $oldw = $dimensions[0];
262 $oldh = $dimensions[1];
263 } else {
264 imsanity_debug( "could not get dimensions for $oldpath, skipping" );
265 return $params;
266 }
267
268 if ( ( $oldw > $maxw + 1 && $maxw > 0 ) || ( $oldh > $maxh + 1 && $maxh > 0 ) ) {
269
270 $ftype = imsanity_quick_mimetype( $oldpath );
271 $orientation = imsanity_get_orientation( $oldpath, $ftype );
272 // If we are going to rotate the image 90 degrees during the resize, swap the existing image dimensions.
273 if ( 6 === (int) $orientation || 8 === (int) $orientation ) {
274 $old_oldw = $oldw;
275 $oldw = $oldh;
276 $oldh = $old_oldw;
277 }
278
279 if ( $maxw > 0 && $maxh > 0 && $oldw >= $maxw && $oldh >= $maxh && ( $oldh > $maxh || $oldw > $maxw ) && apply_filters( 'imsanity_crop_image', false ) ) {
280 $neww = $maxw;
281 $newh = $maxh;
282 } else {
283 list( $neww, $newh ) = wp_constrain_dimensions( $oldw, $oldh, $maxw, $maxh );
284 }
285
286 global $ewww_preempt_editor;
287 if ( ! isset( $ewww_preempt_editor ) ) {
288 $ewww_preempt_editor = false;
289 }
290 $original_preempt = $ewww_preempt_editor;
291 $ewww_preempt_editor = true;
292 $resizeresult = imsanity_image_resize( $oldpath, $neww, $newh, apply_filters( 'imsanity_crop_image', false ) );
293 $ewww_preempt_editor = $original_preempt;
294
295 if ( $resizeresult && ! is_wp_error( $resizeresult ) ) {
296 $newpath = $resizeresult;
297 $new_type = $params['type'];
298
299 imsanity_debug( "checking $newpath to see if resize was successful" );
300 if ( is_file( $newpath ) && filesize( $newpath ) < filesize( $oldpath ) ) {
301 imsanity_debug( 'resized image is smaller, replacing original' );
302 // We saved some file space. remove original and replace with resized image.
303 $new_type = imsanity_mimetype( $newpath );
304 unlink( $oldpath );
305 rename( $newpath, $oldpath );
306 if ( $new_type && $new_type !== $params['type'] ) {
307 imsanity_debug( "mimetype changed from {$params['type']} to $new_type" );
308 $params['type'] = $new_type;
309 $params['file'] = imsanity_update_extension( $oldpath, $new_type );
310 if ( $params['file'] !== $oldpath ) {
311 rename( $oldpath, $params['file'] );
312 }
313 $params['url'] = imsanity_update_extension( $params['url'], $new_type );
314 imsanity_debug( "renamed file to match new extension: {$params['file']} / {$params['url']}" );
315 }
316 } elseif ( is_file( $newpath ) ) {
317 imsanity_debug( 'resized image is bigger, discarding' );
318 // The resized image is actually bigger in filesize (most likely due to jpg quality).
319 // Keep the old one and just get rid of the resized image.
320 unlink( $newpath );
321 }
322 } elseif ( false === $resizeresult ) {
323 imsanity_debug( 'resize returned false, unknown error' );
324 return $params;
325 } elseif ( is_wp_error( $resizeresult ) ) {
326 // resize didn't work, likely because the image processing libraries are missing.
327 // remove the old image so we don't leave orphan files hanging around.
328 unlink( $oldpath );
329
330 $params = wp_handle_upload_error(
331 $oldpath,
332 sprintf(
333 /* translators: 1: error message 2: link to support forums */
334 esc_html__( 'Imsanity was unable to resize this image for the following reason: %1$s. If you continue to see this error message, you may need to install missing server components. If you think you have discovered a bug, please report it on the Imsanity support forum: %2$s', 'imsanity' ),
335 $resizeresult->get_error_message(),
336 'https://wordpress.org/support/plugin/imsanity'
337 )
338 );
339 imsanity_debug( 'resize result is wp_error, should have already output error to log' );
340 } else {
341 imsanity_debug( 'unknown resize result, inconceivable!' );
342 return $params;
343 }
344 }
345 }
346 clearstatcache();
347 return $params;
348 }
349
350
351 /**
352 * Read in the image file from the params and then save as a new jpg file.
353 * if successful, remove the original image and alter the return
354 * parameters to return the new jpg instead of the original
355 *
356 * @param string $type Type of the image to be converted: 'bmp' or 'png'.
357 * @param array $params The upload parameters.
358 * @return array altered params
359 */
360 function imsanity_convert_to_jpg( $type, $params ) {
361 imsanity_debug( __FUNCTION__ );
362
363 if ( ! defined( 'IMSANITY_DEFAULT_QUALITY' ) ) {
364 imsanity_init();
365 }
366
367 if ( apply_filters( 'imsanity_disable_convert', false, $type, $params ) ) {
368 imsanity_debug( "skipping conversion for {$params['file']}" );
369 return $params;
370 }
371
372 $img = null;
373
374 if ( 'bmp' === $type ) {
375 if ( ! function_exists( 'imagecreatefrombmp' ) ) {
376 imsanity_debug( 'imagecreatefrombmp does not exist' );
377 return $params;
378 }
379 $img = imagecreatefrombmp( $params['file'] );
380 } elseif ( 'png' === $type ) {
381 // Prevent converting PNG images with alpha/transparency, unless overridden by the user.
382 if ( apply_filters( 'imsanity_skip_alpha', imsanity_has_alpha( $params['file'] ), $params['file'] ) ) {
383 return $params;
384 }
385 if ( ! function_exists( 'imagecreatefrompng' ) ) {
386 return wp_handle_upload_error( $params['file'], esc_html__( 'Imsanity requires the GD library to convert PNG images to JPG', 'imsanity' ) );
387 }
388
389 $input = imagecreatefrompng( $params['file'] );
390 // convert png transparency to white.
391 $img = imagecreatetruecolor( imagesx( $input ), imagesy( $input ) );
392 imagefill( $img, 0, 0, imagecolorallocate( $img, 255, 255, 255 ) );
393 imagealphablending( $img, true );
394 imagecopy( $img, $input, 0, 0, 0, 0, imagesx( $input ), imagesy( $input ) );
395 } else {
396 return wp_handle_upload_error( $params['file'], esc_html__( 'Unknown image type specified in imsanity_convert_to_jpg', 'imsanity' ) );
397 }
398
399 // We need to change the extension from the original to .jpg so we have to ensure it will be a unique filename.
400 $uploads = wp_upload_dir();
401 $oldfilename = wp_basename( $params['file'] );
402 $newfilename = wp_basename( str_ireplace( '.' . $type, '.jpg', $oldfilename ) );
403 $newfilename = wp_unique_filename( $uploads['path'], $newfilename );
404
405 $quality = imsanity_get_option( 'imsanity_quality', IMSANITY_DEFAULT_QUALITY );
406
407 if ( imagejpeg( $img, $uploads['path'] . '/' . $newfilename, $quality ) ) {
408 // Conversion succeeded: remove the original bmp & remap the params.
409 unlink( $params['file'] );
410
411 $params['file'] = $uploads['path'] . '/' . $newfilename;
412 $params['url'] = $uploads['url'] . '/' . $newfilename;
413 $params['type'] = 'image/jpeg';
414 } else {
415 unlink( $params['file'] );
416
417 return wp_handle_upload_error(
418 $oldfilename,
419 /* translators: %s: the image mime type */
420 sprintf( esc_html__( 'Imsanity was unable to process the %s file. If you continue to see this error you may need to disable the conversion option in the Imsanity settings.', 'imsanity' ), $type )
421 );
422 }
423
424 return $params;
425 }
426
427 // Run plugin init actions.
428 add_action( 'init', 'imsanity_init' );
429 // Add filter to hook into uploads.
430 add_filter( 'wp_handle_upload', 'imsanity_handle_upload' );
431 // Load legacy language files on plugins_loaded.
432 add_action( 'plugins_loaded', 'imsanity_load_textdomain' );
433
434 // Adds a column to the media library list view to display optimization results.
435 add_filter( 'manage_media_columns', 'imsanity_media_columns' );
436 // Outputs the actual column information for each attachment.
437 add_action( 'manage_media_custom_column', 'imsanity_custom_column', 10, 2 );
438 // Checks for AVIF support and adds it to the allowed mime types.
439 add_filter( 'imsanity_allowed_mimes', 'imsanity_add_avif_support' );
440 // Checks for WebP support and adds it to the allowed mime types.
441 add_filter( 'imsanity_allowed_mimes', 'imsanity_add_webp_support' );
442