From ddda10fe0bbe7ab45668e2f4e41895a6722fbd14 Mon Sep 17 00:00:00 2001
Date: Sat, 13 Feb 2016 17:56:32 +0100
Subject: [PATCH] Prevent WP setting the memory limit to a value lower than it
 currently is.

Also fixes a bug in how the memory limits were tested in the first place.
---
 src/wp-admin/admin.php                            | 16 +----
 src/wp-admin/includes/file.php                    |  3 +-
 src/wp-admin/includes/image-edit.php              |  3 +-
 src/wp-includes/class-wp-image-editor-gd.php      | 10 +--
 src/wp-includes/class-wp-image-editor-imagick.php |  3 +-
 src/wp-includes/default-constants.php             | 31 ++++----
 src/wp-includes/deprecated.php                    |  3 +-
 src/wp-includes/functions.php                     | 88 +++++++++++++++++++++++
 src/wp-includes/load.php                          | 28 ++++++++
 tests/phpunit/tests/functions.php                 | 16 +++++
 10 files changed, 154 insertions(+), 47 deletions(-)

diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php
index d67160d..dd30e20 100644
--- a/src/wp-admin/admin.php
+++ b/src/wp-admin/admin.php
@@ -138,21 +138,7 @@ else
 	require(ABSPATH . 'wp-admin/menu.php');
 
 if ( current_user_can( 'manage_options' ) ) {
-	/**
-	 * Filter the maximum memory limit available for administration screens.
-	 *
-	 * This only applies to administrators, who may require more memory for tasks like updates.
-	 * Memory limits when processing images (uploaded or edited by users of any role) are
-	 * handled separately.
-	 *
-	 * The WP_MAX_MEMORY_LIMIT constant specifically defines the maximum memory limit available
-	 * when in the administration back-end. The default is 256M, or 256 megabytes of memory.
-	 *
-	 * @since 3.0.0
-	 *
-	 * @param string 'WP_MAX_MEMORY_LIMIT' The maximum WordPress memory limit. Default 256M.
-	 */
-	@ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
+	wp_raise_memory_limit( 'admin' );
 }
 
 /**
diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php
index cb85c05..68a38cf 100644
--- a/src/wp-admin/includes/file.php
+++ b/src/wp-admin/includes/file.php
@@ -558,8 +558,7 @@ function unzip_file($file, $to) {
 		return new WP_Error('fs_unavailable', __('Could not access filesystem.'));
 
 	// Unzip can use a lot of memory, but not this much hopefully
-	/** This filter is documented in wp-admin/admin.php */
-	@ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
+	wp_raise_memory_limit( 'admin' );
 
 	$needed_dirs = array();
 	$to = trailingslashit($to);
diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php
index 8947f53..a954d24 100644
--- a/src/wp-admin/includes/image-edit.php
+++ b/src/wp-admin/includes/image-edit.php
@@ -586,8 +586,7 @@ function image_edit_apply_changes( $image, $changes ) {
 function stream_preview_image( $post_id ) {
 	$post = get_post( $post_id );
 
-	/** This filter is documented in wp-admin/admin.php */
-	@ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
+	wp_raise_memory_limit( 'admin' );
 
 	$img = wp_get_image_editor( _load_image_to_edit_path( $post_id ) );
 
diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php
index 2093c6b..9cb756d 100644
--- a/src/wp-includes/class-wp-image-editor-gd.php
+++ b/src/wp-includes/class-wp-image-editor-gd.php
@@ -96,16 +96,8 @@ class WP_Image_Editor_GD extends WP_Image_Editor {
 		if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) )
 			return new WP_Error( 'error_loading_image', __('File doesn&#8217;t exist?'), $this->file );
 
-		/**
-		 * Filter the memory limit allocated for image manipulation.
-		 *
-		 * @since 3.5.0
-		 *
-		 * @param int|string $limit Maximum memory limit to allocate for images. Default WP_MAX_MEMORY_LIMIT.
-		 *                          Accepts an integer (bytes), or a shorthand string notation, such as '256M'.
-		 */
 		// Set artificially high because GD uses uncompressed images in memory
-		@ini_set( 'memory_limit', apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT ) );
+		wp_raise_memory_limit( 'image' );
 
 		$this->image = @imagecreatefromstring( file_get_contents( $this->file ) );
 
diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php
index a14fa40..1888316 100644
--- a/src/wp-includes/class-wp-image-editor-imagick.php
+++ b/src/wp-includes/class-wp-image-editor-imagick.php
@@ -129,9 +129,8 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
 		if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) )
 			return new WP_Error( 'error_loading_image', __('File doesn&#8217;t exist?'), $this->file );
 
-		/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
 		// Even though Imagick uses less PHP memory than GD, set higher limit for users that have low PHP.ini limits
-		@ini_set( 'memory_limit', apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT ) );
+		wp_raise_memory_limit( 'image' );
 
 		try {
 			$this->image = new Imagick( $this->file );
diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php
index c9092bd..9b45fb7 100644
--- a/src/wp-includes/default-constants.php
+++ b/src/wp-includes/default-constants.php
@@ -17,7 +17,7 @@
 function wp_initial_constants() {
 	global $blog_id;
 
-	// set memory limits
+	// Define memory limits.
 	if ( !defined('WP_MEMORY_LIMIT') ) {
 		if ( is_multisite() ) {
 			define('WP_MEMORY_LIMIT', '64M');
@@ -26,27 +26,26 @@ function wp_initial_constants() {
 		}
 	}
 
+	$current_limit     = @ini_get( 'memory_limit' );
+	$current_limit_int = wp_php_ini_bytes_to_int( $current_limit );
+
 	if ( ! defined( 'WP_MAX_MEMORY_LIMIT' ) ) {
-		define( 'WP_MAX_MEMORY_LIMIT', '256M' );
+		if ( -1 === $current_limit_int || $current_limit_int > 268435456 ) {
+			define( 'WP_MAX_MEMORY_LIMIT', $current_limit );
+		} else {
+			define( 'WP_MAX_MEMORY_LIMIT', '256M' );
+		}
+	}
+
+	// Set memory limits.
+	$wp_limit_int = wp_php_ini_bytes_to_int( WP_MEMORY_LIMIT );
+	if ( -1 !== $current_limit_int && ( -1 === $wp_limit_int || $wp_limit_int > $current_limit_int ) ) {
+		@ini_set( 'memory_limit', WP_MEMORY_LIMIT );
 	}
 
 	if ( ! isset($blog_id) )
 		$blog_id = 1;
 
-	// set memory limits.
-	if ( function_exists( 'memory_get_usage' ) ) {
-		$current_limit = @ini_get( 'memory_limit' );
-		$current_limit_int = intval( $current_limit );
-		if ( false !== strpos( $current_limit, 'G' ) )
-			$current_limit_int *= 1024;
-		$wp_limit_int = intval( WP_MEMORY_LIMIT );
-		if ( false !== strpos( WP_MEMORY_LIMIT, 'G' ) )
-			$wp_limit_int *= 1024;
-
-		if ( -1 != $current_limit && ( -1 == WP_MEMORY_LIMIT || $current_limit_int < $wp_limit_int ) )
-			@ini_set( 'memory_limit', WP_MEMORY_LIMIT );
-	}
-
 	if ( !defined('WP_CONTENT_DIR') )
 		define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); // no trailing slash, full paths only - WP_CONTENT_URL is defined further down
 
diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php
index 202ad84..e2d8692 100644
--- a/src/wp-includes/deprecated.php
+++ b/src/wp-includes/deprecated.php
@@ -3162,7 +3162,8 @@ function wp_load_image( $file ) {
 		return __('The GD image library is not installed.');
 
 	// Set artificially high because GD uses uncompressed images in memory
-	@ini_set( 'memory_limit', apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT ) );
+	wp_raise_memory_limit( 'image' );
+
 	$image = imagecreatefromstring( file_get_contents( $file ) );
 
 	if ( !is_resource( $image ) )
diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php
index 3e9b792..d63e9e5 100644
--- a/src/wp-includes/functions.php
+++ b/src/wp-includes/functions.php
@@ -5206,3 +5206,91 @@ function mysql_to_rfc3339( $date_string ) {
 	// Strip timezone information
 	return preg_replace( '/(?:Z|[+-]\d{2}(?::\d{2})?)$/', '', $formatted );
 }
+
+/**
+ * Raises the PHP memory limit for memory intensive processes.
+ *
+ * Allows only to raise the exciting limit and prevents lowering it.
+ *
+ * @since 4.5.0
+ *
+ * @param string $context Context in which the function is called.
+ *                        Either 'admin' or 'image'. Defaults to 'admin'.
+ */
+function wp_raise_memory_limit( $context = 'admin' ) {
+	$current_limit     = @ini_get( 'memory_limit' );
+	$current_limit_int = wp_php_ini_bytes_to_int( $current_limit );
+
+	if ( -1 === $current_limit_int ) {
+		return;
+	}
+
+	$wp_max_limit     = WP_MAX_MEMORY_LIMIT;
+	$wp_max_limit_int = wp_php_ini_bytes_to_int( $wp_max_limit );
+	$filtered_limit   = $wp_max_limit;
+
+	switch ( $context ) {
+		case 'admin':
+			/**
+			 * Filter the memory limit available for administration screens.
+			 *
+			 * This only applies to administrators, who may require more memory for tasks like updates.
+			 * Memory limits when processing images (uploaded or edited by users of any role) are
+			 * handled separately.
+			 *
+			 * The WP_MAX_MEMORY_LIMIT constant specifically defines the maximum memory limit available
+			 * when in the administration back-end. The default is 256M (256 megabytes
+			 * of memory) or the original `memory_limit` php.ini value if this is higher.
+			 *
+			 * @since 3.0.0
+			 *
+			 * @param int|string $filtered_limit The maximum WordPress memory limit.
+			 *                                   Accepts an integer (bytes), or a shorthand string
+			 *                                   notation, such as '256M'.
+			 */
+			$filtered_limit = apply_filters( 'admin_memory_limit', $filtered_limit );
+			break;
+
+		case 'image':
+			/**
+			 * Filter the memory limit allocated for image manipulation.
+			 *
+			 * @since 3.5.0
+			 *
+			 * @param int|string $filtered_limit Maximum memory limit to allocate for images.
+			 *                                   Default 256M or the original php.ini memory_limit,
+			 *                                   whichever is higher.
+			 *                                   Accepts an integer (bytes), or a shorthand string
+			 *                                   notation, such as '256M'.
+			 */
+			$filtered_limit = apply_filters( 'image_memory_limit', $filtered_limit );
+			break;
+
+		default:
+			/**
+			 * Filter the memory limit allocated for arbitrary contexts.
+			 *
+			 * The dynamic portion of the hook name, `$context`, refers to an arbitrary
+			 * context passed on calling the function. This allows for plugins to define
+			 * their own contexts for raising the memory limit.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @param int|string $filtered_limit Maximum memory limit to allocate for images.
+			 *                                   Default 256M or the original php.ini memory_limit,
+			 *                                   whichever is higher.
+			 *                                   Accepts an integer (bytes), or a shorthand string
+			 *                                   notation, such as '256M'.
+			 */
+			$filtered_limit = apply_filters( "{$context}_memory_limit", $filtered_limit );
+			break;
+	}
+
+	$filtered_limit_int = wp_php_ini_bytes_to_int( $filtered_limit );
+
+	if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) {
+		@ini_set( 'memory_limit', $filtered_limit );
+	} elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) {
+		@ini_set( 'memory_limit', $wp_max_limit );
+	}
+}
diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php
index ec59459..1ca73df 100644
--- a/src/wp-includes/load.php
+++ b/src/wp-includes/load.php
@@ -893,3 +893,31 @@ function wp_installing( $is_installing = null ) {
 
 	return (bool) $installing;
 }
+
+/**
+ * Converts a PHP ini shorthand byte value to an integer byte value.
+ *
+ * @since 4.5.0
+ *
+ * @see http://php.net/manual/en/function.ini-get.php
+ * @see http://php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ *
+ * @param string $value An PHP ini byte value, either shorthand or ordinary.
+ * @return int Value in bytes.
+ */
+function wp_php_ini_bytes_to_int( $value ) {
+	$value = trim( $value );
+	$last  = strtolower( $value[ strlen( $value ) - 1 ] );
+	
+	switch( $last ) {
+		// Note: the `break` statement is left out on purpose!
+		case 'g':
+			$value *= 1024;
+		case 'm':
+			$value *= 1024;
+		case 'k':
+			$value *= 1024;
+	}
+
+	return (int) $value;
+}
diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php
index 631b303..f0643fb 100644
--- a/tests/phpunit/tests/functions.php
+++ b/tests/phpunit/tests/functions.php
@@ -717,4 +717,20 @@ class Tests_Functions extends WP_UnitTestCase {
 		the_date( 'Y', 'before ', ' after', false );
 		$this->assertEquals( '', ob_get_clean() );
 	}
+
+	/**
+	 * Test raising the memory limit.
+	 *
+	 * {@internal Unfortunately as the default for 'WP_MAX_MEMORY_LIMIT' in the
+	 * test suite is -1, we can not test the memory limit negotiations.}}
+	 *
+	 * @ticket 32075
+	 */
+	function test_wp_raise_memory_limit() {
+		$original = ini_get( 'memory_limit' );
+
+		ini_set( 'memory_limit', '40M' );
+		wp_raise_memory_limit();
+		$this->assertEquals( '-1', ini_get( 'memory_limit' ) );
+	}
 }
-- 
1.9.4.msysgit.2

