From 3016da4d173bf5ae21eb4011a985145ee41c7a9a Mon Sep 17 00:00:00 2001 From: hxdimpf Date: Tue, 27 Oct 2026 23:00:23 +0100 Subject: [PATCH] logs/images/add: a locked or unlimited PHP memory_limit no longer refuses every image as 'too large (0 megapixels)' OKAPI raises memory_limit to 512M in init_internals() and estimates the processable image size from what is left. When the server doesn't allow the raise (php_admin_value), memory_limit stays at the server's value. At -1 (unlimited) the estimate became negative, and every image, however small, was refused as 'too large (0 megapixels)'. - An unlimited memory_limit counts as the 512M OKAPI asks for (huge images are still refused before GD runs out of memory). - An image that would fit into 512M but not into the server's limit is a server problem: the admins get a mail naming the limit and the fix, the client gets 'can't process images at the moment because of a server problem', not 'your image is too large'. - 'too large' shows the real size (one decimal, megapixels of 10^6). --- okapi/services/logs/images/add/WebService.php | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/okapi/services/logs/images/add/WebService.php b/okapi/services/logs/images/add/WebService.php index ea3ff433..43fde9df 100644 --- a/okapi/services/logs/images/add/WebService.php +++ b/okapi/services/logs/images/add/WebService.php @@ -130,15 +130,35 @@ private static function _call(OkapiRequest $request) } unset($image_properties); - if ($width * $height > self::max_pixels($base64_image)) { + if ($width * $height > self::max_pixels($base64_image, self::memory_limit())) { # This large image may crash the image processing functions. + if ($width * $height <= self::max_pixels($base64_image, self::MEMORY_BUDGET)) { + # The image is fine; the server doesn't give OKAPI the memory it asks + # for in Okapi::init_internals() (e.g. memory_limit locked by + # php_admin_value). Tell the admins, and don't blame the user. + + Okapi::mail_admins( + "OKAPI can't process uploaded images: PHP memory_limit too low", + "services/logs/images/add refused an image of ".$width."x".$height." pixels,\n". + "because PHP gives OKAPI only memory_limit = ".ini_get('memory_limit')." (". + round(memory_get_usage() / 1024 / 1024, 1)." MB already in use).\n\n". + "OKAPI raises memory_limit to ".self::MEMORY_BUDGET." itself, but the server doesn't allow it\n". + "(e.g. php_admin_value in the PHP-FPM pool or the Apache config). Allow at least\n". + self::MEMORY_BUDGET." for OKAPI requests, e.g. with php_value instead of php_admin_value.\n\n". + "Until then every image upload through OKAPI fails." + ); + throw new CannotPublishException(sprintf( + _("%s can't process images at the moment because of a server problem. Please try again later or upload the image on the website."), + Okapi::get_normalized_site_name() + )); + } throw new CannotPublishException(sprintf(_( "The image you have uploaded is too large (%s megapixels). ". "Please use an external application to downscale it first. ". "(You can also contact the developers of '%s' and ask them to ". "downscale uploaded images automatically.)" - ), round($width * $height / 1024 / 1024), $request->consumer->name)); + ), round($width * $height / 1000000, 1), $request->consumer->name)); } try { $image = imagecreatefromstring($image); # can throw @@ -212,16 +232,32 @@ private static function _call(OkapiRequest $request) return array($image_uuid, $position, $image_url); } + /** The memory_limit OKAPI asks for in Okapi::init_internals(). */ + const MEMORY_BUDGET = '512M'; + + /** + * The current memory_limit in bytes. "Unlimited" (-1, possible when the + * server doesn't let OKAPI lower it to MEMORY_BUDGET) counts as + * MEMORY_BUDGET, so huge images are still refused before GD runs out of + * the machine's memory. + */ + private static function memory_limit() + { + $limit = Okapi::from_human_to_bytes(ini_get('memory_limit')); + return ($limit < 0) ? Okapi::from_human_to_bytes(self::MEMORY_BUDGET) : $limit; + } + /** - * Estimate an upper limit of the processable image dimensions. + * Estimate an upper limit of the processable image dimensions for a given + * memory_limit (in bytes or with a K/M/G suffix). * This will be in the scale of ~40 MP for 256 MB memory_limit and * ~16 MP for 128 MB. See Okapi::init_internals() for current memory_limit. */ - private static function max_pixels($base64_image) + private static function max_pixels($base64_image, $memory_limit) { $bytes_per_pixel = 5; # GD needs 5 bytes per pixel for "true color" - $available_memory = Okapi::from_human_to_bytes(ini_get('memory_limit')) - memory_get_usage(); + $available_memory = Okapi::from_human_to_bytes($memory_limit) - memory_get_usage(); $available_memory -= 16 * 1024 * 1024; # reserve $available_memory -= strlen($base64_image); # will be copied for EXIF processing $available_memory -= 3 * $bytes_per_pixel * Settings::get('IMAGE_MAX_PIXEL_COUNT'); # processing buffers