Magento 2.3 Logout Bug

Magento 2.3 Logout Bug

5 min read

Send to you:

When analyzing server logs from Magento 2.3.3, an error message of this kind was found:

2019/11/26 16:03:05 [error] 19565#0: *114206 FastCGI sent in stderr: "PHP message: PHP Fatal error:  Uncaught TypeError: strpos() expects parameter 1 to be string, null given in /home/magento/www/vendor/magento/module-theme/Controller/Result/JsFooterPlugin.php:44
Stack trace:
#0 /home/magento/www/vendor/magento/module-theme/Controller/Result/JsFooterPlugin.php(44): strpos(NULL, '</body') #1 /home/magento/www/vendor/magento/framework/Interception/Interceptor.php(121): Magento\Theme\Controller\Result\JsFooterPlugin->beforeSendResponse(Object(Magento\Framework\App\Response\Http\Interceptor))
#2 /home/magento/www/vendor/magento/framework/Interception/Interceptor.php(153): Magento\Framework\App\Response\Http\Interceptor->Magento\Framework\Interception\{closure}()
#3 /home/magento/www/generated/code/Magento/Framework/App/Response/Http/Interceptor.php(26): Magento\Framework\App\Response\Http\Interceptor->___callPlugins('sendResponse', Array, Array)
#4 /home/magento/www/vendor/magento/framework/App/Bootstrap.php(262): Magento\Framework\App\Response\Http\Interceptor->sendResponse()" while reading response header from upstream, client: 127.0.0.1,

We needed to identify the reason for the error and fix it. To do this, we had to analyze the code of the Magento core.

Analyzing the Code of the Magento Core

Starting in version 2.3.2, the following plugin was added in the Theme module in etc/fronted/di.xml.

<type name="Magento\Framework\App\Response\Http">
    <plugin name="result-js-footer" type="Magento\Theme\Controller\Result\JsFooterPlugin"/>

</type>

The code of this plugin looks as follows:

declare(strict_types=1);
namespace Magento\Theme\Controller\Result;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Framework\App\Response\Http;
/**
 * Plugin for putting all js to footer.
 */
class JsFooterPlugin
{
    private const XML_PATH_DEV_MOVE_JS_TO_BOTTOM = 'dev/js/move_script_to_bottom';
    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;
    /**
     * @param ScopeConfigInterface $scopeConfig
     */
    public function __construct(ScopeConfigInterface $scopeConfig)
    {
        $this->scopeConfig = $scopeConfig;
    }
    /**
     * Put all javascript to footer before sending the response.
     *
     * @param Http $subject
     * @return void
     */
    public function beforeSendResponse(Http $subject)
    {
        $content = $subject->getContent();
        $script = [];
        if (strpos($content, '</body') !== false) { if ($this->scopeConfig->isSetFlag(
                self::XML_PATH_DEV_MOVE_JS_TO_BOTTOM,
                ScopeInterface::SCOPE_STORE
            )
            ) {
                $pattern = '#<script[^>]* (?<!text/x-magento-template.)>.*?</script>#is';
                $content = preg_replace_callback(
                    $pattern,
                    function ($matchPart) use (&$script) {
                        $script[] = $matchPart[0];
                        return '';
                    },
                    $content
                );
                $subject->setContent(
                    str_replace('</body', implode("\n", $script) . "\n</body", $content)
                );
            }
        }
    }
}

As you can see, its purpose is to transfer JavaScript to the footer, if the shop administrator has enabled this. The error arises in the strpos function, since the $content variable is null. Let’s determine when this happens, and why.

There is also the Amazon Payment module in the Magento core, which requires the Amazon Login module to function, which is responsible for logging in using Amazon. If a user logs in using Amazon, this is noted in the session. Starting in Magento 2.3.1, the module unchecks this box if the user logs out of Magento. This was implemented using an asynchronous request, which is made when the user gets through the logout page. The controller Amazon\Login\Controller\Logout\Index is responsible for this, which looks as follows.
class Index extends \Magento\Framework\App\Action\Action

{
    /**
     * @var JsonFactory
     */
    private $jsonFactory;
    /**
     * @var Session
     */
    private $sessionHelper;
    /**
     * @param Context     $context
     * @param JsonFactory $jsonFactory
     * @param Session     $sessionHelper
     */
    public function __construct(Context $context, JsonFactory $jsonFactory, Session $sessionHelper)
    {
        parent::__construct($context);
        $this->jsonFactory   = $jsonFactory;
        $this->sessionHelper = $sessionHelper;
    }
    public function execute()
    {
        $this->sessionHelper->setIsAmazonLoggedIn(false);
        return $this->jsonFactory->create();
    }
}

As you can see, the execute method of the controller returns an object of the type \Magento\Framework\Controller\Result\Json. In theory, we should have passed JSON to the front end, which should have been generated by the method.

/**
 * Render content
 *
 * @param HttpResponseInterface|ResponseInterface $response
 * @return $this
 */
public function renderResult(ResponseInterface $response)
{
    $this->applyHttpHeaders($response);
    return $this->render($response);
}

This method, in turn, calls the protected method render, which looks as follows:

/**
 * {@inheritdoc}
 */
protected function render(HttpResponseInterface $response)
{
    $this->translateInline->processResponseBody($this->json, true);
    $response->setHeader('Content-Type', 'application/json', true);
    $response->setBody($this->json);
    return $this;
}

The problem is that $this->json is NULL. One of two methods could have initialized it:

/**
 * Set json data
 *
 * @param mixed $data
 * @param boolean $cycleCheck Optional; whether or not to check for object recursion; off by default
 * @param array $options Additional options used during encoding
 * @return $this
 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
 */
public function setData($data, $cycleCheck = false, $options = [])
{
    if ($data instanceof \Magento\Framework\DataObject) {
        $data = $data->toArray();
    }
    $this->json = $this->serializer->serialize($data);
    return $this;
}
/**
 * @param string $jsonData
 * @return $this
 */
public function setJsonData($jsonData)
{
    $this->json = (string)$jsonData;
    return $this;
}

However, this was not done in the controller Amazon\Login\Controller\Logout\Index, since this controller does not need to pass any data to the front end. There’s something else we should mention. PHP has historically been a language with weak typing data. This means that in versions prior to PHP 7, if the type of the variables entered didn’t match what was declared, the most that would happen would be the function would throw a warning and return an empty or false value. This means that the code of the beforeSendResponse method could process without fatal errors. It’s the same in versions starting with PHP 7, except for the statement declare(strict_types = 1). This statement includes more strict data type control and throws a TypeError instead of automatically converting variables and throwing a warning. The conversion could have been processed in this method using try{}catch{}, but there is no such processing in this version. Using strict type control is the current Magento policy, and most core classes in Magento 2.3 contain this statement. However, developers of third-party modules, including Amazon, do not always abide by this policy.

Either way, this error should not exist. It can be fixed using a plugin:

<type name="Magento\Framework\App\Response\Http">
    <plugin name="fix-amazon-logout" type="Web4pro\All\Model\Plugin" sortOrder="20"/>
</type>

public function afterGetContent($subject,$content){
    if(!$content){
        $content = "";
    }
    return $content;
}

SUM MARY

We have successfully cast a null value into an empty string, and the error is no longer thrown. And, well, we can only wish the Magento team to test the compatibility of third-party modules included with new changes in the core.

Posted on: February 25, 2020

4.6/5.0

Article rating (11 Reviews)

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

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