From 1cbc918895300a551b49d371d9fde6c7bdbbead0 Mon Sep 17 00:00:00 2001
From: jrfnl <github_nospam@adviesenzo.nl>
Date: Fri, 8 Jul 2016 08:43:26 +0200
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.

Includes unit tests for the newly added functions in load.php.
---
 src/wp-admin/admin.php                            |  16 +---
 src/wp-admin/includes/file.php                    |   5 +-
 src/wp-admin/includes/image-edit.php              |   3 +-
 src/wp-includes/class-wp-image-editor-gd.php      |  12 +--
 src/wp-includes/class-wp-image-editor-imagick.php |   5 +-
 src/wp-includes/default-constants.php             |  65 +++++++------
 src/wp-includes/deprecated.php                    |   5 +-
 src/wp-includes/functions.php                     | 110 ++++++++++++++++++++++
 src/wp-includes/load.php                          |  57 +++++++++++
 src/wp-includes/media.php                         |  20 ----
 tests/phpunit/tests/functions.php                 |  14 +++
 tests/phpunit/tests/load.php                      |  72 ++++++++++++++
 12 files changed, 294 insertions(+), 90 deletions(-)
 create mode 100644 tests/phpunit/tests/load.php

diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php
index 3fabd87..ad341c8 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' ) ) {
-	/**
-	 * Filters 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 3b754df..ad66a8d 100644
--- a/src/wp-admin/includes/file.php
+++ b/src/wp-admin/includes/file.php
@@ -569,9 +569,8 @@ function unzip_file($file, $to) {
 	if ( ! $wp_filesystem || !is_object($wp_filesystem) )
 		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 ) );
+	// Unzip can use a lot of memory, but not this much hopefully.
+	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 b34d2ac..c614a77 100644
--- a/src/wp-admin/includes/image-edit.php
+++ b/src/wp-admin/includes/image-edit.php
@@ -585,8 +585,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 96e7bf1..d7823eb 100644
--- a/src/wp-includes/class-wp-image-editor-gd.php
+++ b/src/wp-includes/class-wp-image-editor-gd.php
@@ -96,18 +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 );
 
-		/**
-		 * Filters 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'.
-		 */
-		$image_memory_limit = apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT );
-
 		// Set artificially high because GD uses uncompressed images in memory.
-		@ini_set( 'memory_limit', $image_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 3d92d39..82b872d 100644
--- a/src/wp-includes/class-wp-image-editor-imagick.php
+++ b/src/wp-includes/class-wp-image-editor-imagick.php
@@ -137,14 +137,11 @@ 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-gd.php */
-		$image_memory_limit = apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT );
-
 		/*
 		 * Even though Imagick uses less PHP memory than GD, set higher limit
 		 * for users that have low PHP.ini limits.
 		 */
-		@ini_set( 'memory_limit', $image_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 fe96e34..2aa3430 100644
--- a/src/wp-includes/default-constants.php
+++ b/src/wp-includes/default-constants.php
@@ -17,36 +17,50 @@
 function wp_initial_constants() {
 	global $blog_id;
 
-	// set memory limits
-	if ( !defined('WP_MEMORY_LIMIT') ) {
-		if ( is_multisite() ) {
-			define('WP_MEMORY_LIMIT', '64M');
+	/**#@+
+	 * Constants for expressing human-readable data sizes in their respective number of bytes.
+	 *
+	 * @since 4.4.0
+	 */
+	define( 'KB_IN_BYTES', 1024 );
+	define( 'MB_IN_BYTES', 1024 * KB_IN_BYTES );
+	define( 'GB_IN_BYTES', 1024 * MB_IN_BYTES );
+	define( 'TB_IN_BYTES', 1024 * GB_IN_BYTES );
+	/**#@-*/
+
+	$current_limit     = @ini_get( 'memory_limit' );
+	$current_limit_int = wp_convert_hr_to_bytes( $current_limit );
+
+	// Define memory limits.
+	if ( ! defined( 'WP_MEMORY_LIMIT' ) ) {
+		if ( false === wp_is_ini_value_changeable( 'memory_limit' ) ) {
+			define( 'WP_MEMORY_LIMIT', $current_limit );
+		} elseif ( is_multisite() ) {
+			define( 'WP_MEMORY_LIMIT', '64M' );
 		} else {
-			define('WP_MEMORY_LIMIT', '40M');
+			define( 'WP_MEMORY_LIMIT', '40M' );
 		}
 	}
 
 	if ( ! defined( 'WP_MAX_MEMORY_LIMIT' ) ) {
-		define( 'WP_MAX_MEMORY_LIMIT', '256M' );
+		if ( false === wp_is_ini_value_changeable( 'memory_limit' ) ) {
+			define( 'WP_MAX_MEMORY_LIMIT', $current_limit );
+		} elseif ( -1 === $current_limit_int || $current_limit_int > 268435456 /* = 256M */ ) {
+			define( 'WP_MAX_MEMORY_LIMIT', $current_limit );
+		} else {
+			define( 'WP_MAX_MEMORY_LIMIT', '256M' );
+		}
+	}
+
+	// Set memory limits.
+	$wp_limit_int = wp_convert_hr_to_bytes( 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
 
@@ -110,17 +124,6 @@ function wp_initial_constants() {
 	define( 'MONTH_IN_SECONDS',  30 * DAY_IN_SECONDS    );
 	define( 'YEAR_IN_SECONDS',  365 * DAY_IN_SECONDS    );
 	/**#@-*/
-
-	/**#@+
-	 * Constants for expressing human-readable data sizes in their respective number of bytes.
-	 *
-	 * @since 4.4.0
-	 */
-	define( 'KB_IN_BYTES', 1024 );
-	define( 'MB_IN_BYTES', 1024 * KB_IN_BYTES );
-	define( 'GB_IN_BYTES', 1024 * MB_IN_BYTES );
-	define( 'TB_IN_BYTES', 1024 * GB_IN_BYTES );
-	/**#@-*/
 }
 
 /**
diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php
index c61e5d3..0d30113 100644
--- a/src/wp-includes/deprecated.php
+++ b/src/wp-includes/deprecated.php
@@ -3175,11 +3175,8 @@ function wp_load_image( $file ) {
 	if ( ! function_exists('imagecreatefromstring') )
 		return __('The GD image library is not installed.');
 
-	/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
-	$image_memory_limit = apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT );
-
 	// Set artificially high because GD uses uncompressed images in memory.
-	@ini_set( 'memory_limit', $image_memory_limit );
+	wp_raise_memory_limit( 'image' );
 
 	$image = imagecreatefromstring( file_get_contents( $file ) );
 
diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php
index df0dbce..1920e39 100644
--- a/src/wp-includes/functions.php
+++ b/src/wp-includes/functions.php
@@ -5379,3 +5379,113 @@ function wp_object_type_exists( $object_type ) {
 
 	return false;
 }
+
+/**
+ * Attempts to raise the PHP memory limit for memory intensive processes.
+ *
+ * Only allows raising the existing limit and prevents lowering it.
+ *
+ * @since 4.6.0
+ *
+ * @param string $context  Context in which the function is called.
+ *                         Either 'admin', 'image' or an arbitrary other context.
+ *                         Defaults to 'admin'.
+ *                         If an arbitrary context is passed, the similarly arbitrary
+ *                         "{$context}_memory_limit" filter will be invoked.
+ * @return bool|int|string The limit that was set or false on failure.
+ */
+function wp_raise_memory_limit( $context = 'admin' ) {
+	// Exit early if the limit cannot be changed.
+	if ( false === wp_is_ini_value_changeable( 'memory_limit' ) ) {
+		return false;
+	}
+
+	$current_limit     = @ini_get( 'memory_limit' );
+	$current_limit_int = wp_convert_hr_to_bytes( $current_limit );
+
+	if ( -1 === $current_limit_int ) {
+		return false;
+	}
+
+	$wp_max_limit     = WP_MAX_MEMORY_LIMIT;
+	$wp_max_limit_int = wp_convert_hr_to_bytes( $wp_max_limit );
+	$filtered_limit   = $wp_max_limit;
+
+	switch ( $context ) {
+		case 'admin':
+			/**
+			 * Filters 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 (256 megabytes
+			 * of memory) or the original `memory_limit` php.ini value if this is higher.
+			 *
+			 * @since 3.0.0
+			 * @since 4.6.0 The default takes the original `memory_limit` into account.
+			 *
+			 * @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':
+			/**
+			 * Filters the memory limit allocated for image manipulation.
+			 *
+			 * @since 3.5.0
+			 * @since 4.6.0 The default takes the original `memory_limit` into account.
+			 *
+			 * @param int|string $filtered_limit Maximum memory limit to allocate for images.
+			 *                                   Default WP_MAX_MEMORY_LIMIT 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:
+			/**
+			 * Filters 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.6.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_convert_hr_to_bytes( $filtered_limit );
+
+	if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) {
+		if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) {
+			return $filtered_limit;
+		} else {
+			return false;
+		}
+	} elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) {
+		if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) {
+			return $wp_max_limit;
+		} else {
+			return false;
+		}
+	}
+	else {
+		return false;
+	}
+}
diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php
index c6ff6cb..1d8e52d 100644
--- a/src/wp-includes/load.php
+++ b/src/wp-includes/load.php
@@ -974,3 +974,60 @@ function is_ssl() {
 	}
 	return false;
 }
+
+/**
+ * Converts a shorthand byte value to an integer byte value.
+ *
+ * @since 2.3.0
+ *
+ * @link http://php.net/manual/en/function.ini-get.php
+ * @link 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 An integer byte value.
+ */
+function wp_convert_hr_to_bytes( $value ) {
+	$value = trim( $value );
+	$last  = strtolower( substr( $value, -1 ) );
+
+	switch ( $last ) {
+		case 'g':
+			$value *= GB_IN_BYTES;
+			break;
+		case 'm':
+			$value *= MB_IN_BYTES;
+			break;
+		case 'k':
+			$value *= KB_IN_BYTES;
+			break;
+		default:
+			// Left empty on purpose.
+			break;
+	}
+
+    // Deal with large (float) values which run into the maximum integer size.
+	return (int) min( PHP_INT_MAX, $value );
+}
+
+/**
+ * Determines whether a PHP ini value is changeable at runtime.
+ *
+ * @since 4.6.0
+ *
+ * @link http://php.net/manual/en/function.ini-get-all.php
+ *
+ * @param string $setting The name of the ini setting to check.
+ * @return bool True if the value is changeable at runtime. False otherwise.
+ */
+function wp_is_ini_value_changeable( $setting ) {
+	static $ini_all;
+
+	if ( ! isset( $ini_all ) ) {
+		$ini_all = ini_get_all();
+	}
+
+	if ( isset( $ini_all[ $setting ]['access'] ) && ( INI_ALL === $ini_all[ $setting ]['access'] || INI_USER === $ini_all[ $setting ]['access'] ) ) {
+		return true;
+	}
+	return false;
+}
diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php
index 6a060b1..df96a17 100644
--- a/src/wp-includes/media.php
+++ b/src/wp-includes/media.php
@@ -2777,26 +2777,6 @@ function wp_expand_dimensions( $example_width, $example_height, $max_width, $max
 }
 
 /**
- * Converts a shorthand byte value to an integer byte value.
- *
- * @since 2.3.0
- *
- * @param string $size A shorthand byte value.
- * @return int An integer byte value.
- */
-function wp_convert_hr_to_bytes( $size ) {
-	$size  = strtolower( $size );
-	$bytes = (int) $size;
-	if ( strpos( $size, 'k' ) !== false )
-		$bytes = intval( $size ) * KB_IN_BYTES;
-	elseif ( strpos( $size, 'm' ) !== false )
-		$bytes = intval($size) * MB_IN_BYTES;
-	elseif ( strpos( $size, 'g' ) !== false )
-		$bytes = intval( $size ) * GB_IN_BYTES;
-	return $bytes;
-}
-
-/**
  * Determines the maximum upload size allowed in php.ini.
  *
  * @since 2.5.0
diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php
index ce5f657..ff49f28 100644
--- a/tests/phpunit/tests/functions.php
+++ b/tests/phpunit/tests/functions.php
@@ -865,4 +865,18 @@ class Tests_Functions extends WP_UnitTestCase {
 
 		$this->assertNull( wp_ext2type( 'unknown_format' ) );
 	}
+
+	/**
+	 * Test raising the memory limit.
+	 *
+	 * 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() {
+		ini_set( 'memory_limit', '40M' );
+		$this->assertSame( -1, wp_raise_memory_limit() );
+		$this->assertEquals( '-1', ini_get( 'memory_limit' ) );
+	}
 }
diff --git a/tests/phpunit/tests/load.php b/tests/phpunit/tests/load.php
new file mode 100644
index 0000000..a27de49
--- /dev/null
+++ b/tests/phpunit/tests/load.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @group load.php
+ */
+class Tests_Load extends WP_UnitTestCase {
+
+	/**
+	 * Test converting (PHP ini) byte values to integer byte values.
+	 *
+	 * @dataProvider data_wp_convert_hr_to_bytes
+	 */
+	function test_wp_convert_hr_to_bytes( $value, $expected ) {
+		$this->assertSame( $expected, wp_convert_hr_to_bytes( $value ) );
+	}
+
+	function data_wp_convert_hr_to_bytes() {
+		$array = array(
+			// Integer input.
+			array( -1, -1 ), // = no memory limit.
+			array( 8388608, 8388608 ), // 8M.
+
+			// String input (memory limit shorthand values).
+			array( '32k', 32768 ),
+			array( '64K', 65536 ),
+			array( '128m', 134217728 ),
+			array( '256M', 268435456 ),
+			array( '1g', 1073741824 ),
+			array( '1024', 1024 ), // No letter will be interpreted as integer value.
+
+			// Edge cases.
+			array( 'g', 0 ),
+			array( 'null', 0 ),
+			array( 'off', 0 ),
+		);
+
+		// Test for running into maximum integer size limit on 32bit systems.
+		if ( 2147483647 === PHP_INT_MAX ) {
+			$array[] = array( '2G', 2147483647 );
+			$array[] = array( '4G', 2147483647 );
+		} else {
+			$array[] = array( '2G', 2147483648 );
+			$array[] = array( '4G', 4294967296 );
+		}
+
+		return $array;
+	}
+
+	/**
+	 * Test the determining of the changeability of a PHP ini value.
+	 *
+	 * @dataProvider data_wp_is_ini_value_changeable
+	 */
+	function test_wp_is_ini_value_changeable( $setting, $expected ) {
+		$this->assertSame( $expected, wp_is_ini_value_changeable( $setting ) );
+	}
+
+	function data_wp_is_ini_value_changeable() {
+		$array = array(
+			array( 'memory_limit', true ), // PHP_INI_ALL.
+			array( 'log_errors', true ), // PHP_INI_ALL.
+			array( 'upload_max_filesize', false ), // PHP_INI_PERDIR.
+			array( 'upload_tmp_dir', false ), // PHP_INI_SYSTEM.
+		);
+
+		if ( extension_loaded( 'Tidy' ) && version_compare( PHP_VERSION, '7.0.0', '>' ) ) {
+			$array[] = array( 'tidy.clean_output', true ); // PHP_INI_USER.
+		}
+
+		return $array;
+	}
+}
-- 
1.9.4.msysgit.2

