Magento OE 1.9.4.2, Magento CE 1.14.4.2, and Their Compatibility with Different Versions of PHP

Magento OE 1.9.4.2, Magento CE 1.14.4.2, and Their Compatibility with Different Versions of PHP

6 min read

Send to you:

Magento 1 was developed back before PHP 5 was used. In all versions, including the latest Magento Open Source 1.9.4.2 and Magento Commerce 1.13.4.2, a check is done for PHP 5.3 at the entry point of index.php. This means that all of the code of the core, as well as the code of custom extensions, must be compatible with PHP 5.3. This official resource declares PHP compatibility: https://docs.magento.com/m1/ce/user_guide/magento/system-requirements.html

However, incompatibility with PHP 5 was discovered when analyzing the latest versions of Magento Open Source 1.9.2.4 and Magento Commerce 1.14.4.2. Below we will take a look at the reason for these incompatibilities.

Analysis of Incompatibility Issues with PHP 5

Magento has the core class Mage_Core_Helper_Data, in which a method for getting a random string is implemented, getRandomString(). Its implementation in OE and CE before 1.9.4.2 and 1.14.4.2 looked as follows:

public function getRandomString($len, $chars = null)
    {
        if (is_null($chars)) {
            $chars = self::CHARS_LOWERS . self::CHARS_UPPERS . self::CHARS_DIGITS;
        }
        for ($i = 0, $str = '', $lc = strlen($chars)-1; $i < $len; $i  ) {
            $str .= $chars[mt_rand(0, $lc)];
        }
        return $str;
    }

In the latest Magento versions that we mentioned earlier, this method was released differently:

public function getRandomString($len, $chars = null)
    {
        if (is_null($chars)) {
            $chars = self::CHARS_LOWERS . self::CHARS_UPPERS . self::CHARS_DIGITS;
        }
        for ($i = 0, $str = '', $lc = strlen($chars)-1; $i < $len; $i  ) {
            $str .= $chars[random_int(0, $lc)];
        }
        return $str;
    }

As you can see, the only difference is one function: they replaced mt_rand with random_int. Therein lies the problem: the random_int function was added in PHP 7, but PHP 5 doesn’t support it. Consequently, if this method is called in PHP 5 when generating a page or executing any other code, it will lead to a fatal unknown function call error. Magento still declares compatibility starting with version 5.3 in the index.php file, which is incorrect based on what we’ve stated above.

Why did Magento replace the function in this method? It’s most likely because the behavior of the mt_rand function was changed in PHP 7.2. Developers wanted to keep the behavior of the getRandomString method as close as possible to its behavior in previous versions. In so doing, PHP 5 support was virtually discontinued for the sake of supporting PHP 7.2. Naturally, the logical solution to this problem is upgrading the version of PHP on your server to version 7. But in some cases, this can cause problems in custom modules that are incompatible with PHP 7. Many custom modules were written before PHP 7 existed and are no longer updated. It’s also not possible to fix encoded modules that are bound to certain PHP versions. So how do we solve this problem?

Solution

In these cases, the solution could be to copy the file app/code/core/Mage/Core/functions.php to app/code/local/Mage/Core/functions.php and add the following code:

if (!is_callable('RandomCompat_intval')) {
    /**
     * Cast to an integer if we can, safely.
     *
     * If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX)
     * (non-inclusive), it will sanely cast it to an int. If you it's equal to
     * ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats
     * lose precision, so the <= and => operators might accidentally let a float
     * through.
     *
     * @param int|float $number    The number we want to convert to an int
     * @param bool      $fail_open Set to true to not throw an exception
     *
     * @return float|int
     * @psalm-suppress InvalidReturnType
     *
     * @throws TypeError
     */
    function RandomCompat_intval($number, $fail_open = false)
    {
        if (is_int($number) || is_float($number)) {
            $number  = 0;
        } elseif (is_numeric($number)) {
            /** @psalm-suppress InvalidOperand */
            $number  = 0;
        }
        /** @var int|float $number */
        if (
            is_float($number)
            &&
            $number > ~PHP_INT_MAX
            &&
            $number < PHP_INT_MAX
        ) {
            $number = (int) $number;
        }
        if (is_int($number)) {
            return (int) $number;
        } elseif (!$fail_open) {
            throw new TypeError(
                'Expected an integer.'
            );
        }
        return $number;
    }
}
if (!is_callable('random_int')) {

    /**
     * Fetch a random integer between $min and $max inclusive
     *
     * @param int $min
     * @param int $max
     *
     * @throws Exception
     *
     * @return int
     */
    function random_int($min, $max)
    {
        /**
         * Type and input logic checks
         *
         * If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX)
         * (non-inclusive), it will sanely cast it to an int. If you it's equal to
         * ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats
         * lose precision, so the <= and => operators might accidentally let a float
         * through.
         */
        try {
            /** @var int $min */
            $min = RandomCompat_intval($min);
        } catch (TypeError $ex) {
            throw new TypeError(
                'random_int(): $min must be an integer'
            );
        }
        try {
            /** @var int $max */
            $max = RandomCompat_intval($max);
        } catch (TypeError $ex) {
            throw new TypeError(
                'random_int(): $max must be an integer'
            );
        }
        /**
         * Now that we've verified our weak typing system has given us an integer,
         * let's validate the logic then we can move forward with generating random
         * integers along a given range.
         */
        if ($min > $max) {
            throw new Error(
                'Minimum value must be less than or equal to the maximum value'
            );
        }
        if ($max === $min) {
            return (int) $min;
        }
        /**
         * Initialize variables to 0
         *
         * We want to store:
         * $bytes => the number of random bytes we need
         * $mask => an integer bitmask (for use with the &) operator
         *          so we can minimize the number of discards
         */
        $attempts = $bits = $bytes = $mask = $valueShift = 0;
        /** @var int $attempts */
        /** @var int $bits */
        /** @var int $bytes */
        /** @var int $mask */
        /** @var int $valueShift */
        /**
         * At this point, $range is a positive number greater than 0. It might
         * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to
         * a float and we will lose some precision.
         *
         * @var int|float $range
         */
        $range = $max - $min;
        /**
         * Test for integer overflow:
         */
        if (!is_int($range)) {
            /**
             * Still safely calculate wider ranges.
             * Provided by @CodesInChaos, @oittaa
             *
             * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435
             *
             * We use ~0 as a mask in this case because it generates all 1s
             *
             * @ref https://eval.in/400356 (32-bit)
             * @ref http://3v4l.org/XX9r5  (64-bit)
             */
            $bytes = PHP_INT_SIZE;
            /** @var int $mask */
            $mask = ~0;
        } else {
            /**
             * $bits is effectively ceil(log($range, 2)) without dealing with
             * type juggling
             */
            while ($range > 0) {
                if ($bits % 8 === 0) {
                      $bytes;
                }
                  $bits;
                $range >>= 1;
                /** @var int $mask */
                $mask = $mask << 1 | 1; } $valueShift = $min; } /** @var int $val */ $val = 0; /** * Now that we have our parameters set up, let's begin generating * random integers until one falls between $min and $max */ /** @psalm-suppress RedundantCondition */ do { /** * The rejection probability is at most 0.5, so this corresponds * to a failure probability of 2^-128 for a working RNG */ if ($attempts > 128) {
                throw new Exception(
                    'random_int: RNG is broken - too many rejections'
                );
            }
            /**
             * Let's grab the necessary number of random bytes
             */
            $randomByteString = random_bytes($bytes);
            /**
             * Let's turn $randomByteString into an integer
             *
             * This uses bitwise operators (<< and |) to build an integer * out of the values extracted from ord() * * Example: [9F] | [6D] | [32] | [0C] =>
             *   159   27904   3276800   201326592 =>
             *   204631455
             */
            $val &= 0;
            for ($i = 0; $i < $bytes;   $i) {
                $val |= ord($randomByteString[$i]) << ($i * 8); } /** @var int $val */ /** * Apply mask */ $val &= $mask; $val  = $valueShift;   $attempts; /** * If $val overflows to a floating point number, * ... or is larger than $max, * ... or smaller than $min, * then try again. */ } while (!is_int($val) || $val > $max || $val < $min);
        return (int) $val;
    }
}
if (!is_callable('random_bytes')) {
    /**
     * Powered by ext/mcrypt (and thankfully NOT libmcrypt)
     *
     * @ref https://bugs.php.net/bug.php?id=55169
     * @ref https://github.com/php/php-src/blob/c568ffe5171d942161fc8dda066bce844bdef676/ext/mcrypt/mcrypt.c#L1321-L1386
     *
     * @param int $bytes
     *
     * @throws Exception
     *
     * @return string
     */
    function random_bytes($bytes)
    {
        try {
            /** @var int $bytes */
            $bytes = RandomCompat_intval($bytes);
        } catch (TypeError $ex) {
            throw new TypeError(
                'random_bytes(): $bytes must be an integer'
            );
        }
        if ($bytes < 1) {
            throw new Error(
                'Length must be greater than 0'
            );
        }
        /** @var string|bool $buf */
        $buf = @mcrypt_create_iv((int) $bytes, (int) MCRYPT_DEV_URANDOM);
        if (
            is_string($buf)
            &&
            RandomCompat_strlen($buf) === $bytes
        ) {
            /**
             * Return our random entropy buffer here:
             */
            return $buf;
        }
        /**
         * If we reach here, PHP has failed us.
         */
        throw new Exception(
            'Could not gather sufficient random data'
        );
    }
}
if (!is_callable('RandomCompat_strlen')) {
    if (
        defined('MB_OVERLOAD_STRING')
        &&
        ((int) ini_get('mbstring.func_overload')) & MB_OVERLOAD_STRING
    ) {
        /**
         * strlen() implementation that isn't brittle to mbstring.func_overload
         *
         * This version uses mb_strlen() in '8bit' mode to treat strings as raw
         * binary rather than UTF-8, ISO-8859-1, etc
         *
         * @param string $binary_string
         *
         * @throws TypeError
         *
         * @return int
         */
        function RandomCompat_strlen($binary_string)
        {
            if (!is_string($binary_string)) {
                throw new TypeError(
                    'RandomCompat_strlen() expects a string'
                );
            }
            return (int) mb_strlen($binary_string, '8bit');
        }
    } else {
        /**
         * strlen() implementation that isn't brittle to mbstring.func_overload
         *
         * This version just used the default strlen()
         *
         * @param string $binary_string
         *
         * @throws TypeError
         *
         * @return int
         */
        function RandomCompat_strlen($binary_string)
        {
            if (!is_string($binary_string)) {
                throw new TypeError(
                    'RandomCompat_strlen() expects a string'
                );
            }
            return (int) strlen($binary_string);
        }
    }
}

SUM MARY

After adding the code, the function will be available and the site will work on servers with PHP 5 just like before the upgrade. Nevertheless, we only recommend using this method if for some reason you can't update your PHP version.

Posted on: October 03, 2019

5.0/5.0

Article rating (9 Reviews)

Do you find this article useful? Please, let us know your opinion and rate the post!

  • Not bad
  • Good
  • Very Good
  • Great
  • Awesome