Magento's Not Sane AdminNotification Module

Published: July 5, 2018

Tags:

For my past 4 and a half years working with Magento, I’ve gotten very used to seeing messages like this when I log into the Magento admin panel.

Example admin notification

I typically close them out and proceed about my business. I had never quite understood how these notifications work until recently. Here I’ll document the not quite sane mechanics behind Magento’s admin notification system.

NOTE: This post is based on Magento_AdminNotification as of Magento version 2.2.5

Pulling from the Magento Notification RSS Feed

Notifications, it turns out, are pulled from an RSS feed that Magento publishes at notifications.magentocommerce.com/magento2/community/notifications.rss. This URL can be found in vendor/magento/module-admin-notification/etc/config.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <system>
            <adminnotification>
                <feed_url>notifications.magentocommerce.com/magento2/community/notifications.rss</feed_url>
                <!--
                    More configurations...
                -->
            </adminnotification>
        </system>
    </default>
</config>

In vendor/magento/module-admin-notification/etc/adminhtml/events.xml we can see that the adminnotification observer fires on each controller_action_predispatch event…

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="controller_action_predispatch">
        <observer name="adminnotification" instance="Magento\AdminNotification\Observer\PredispatchAdminActionControllerObserver" />
    </event>
</config>

This observer calls checkUpdate on Magento\AdminNotification\Model\Feed

public function execute(\Magento\Framework\Event\Observer $observer)
{
    if ($this->_backendAuthSession->isLoggedIn()) {
        $feedModel = $this->_feedFactory->create();
        /* @var $feedModel \Magento\AdminNotification\Model\Feed */
        $feedModel->checkUpdate();
    }
}

checkUpdate consults the update frequency setting and the last updated time to determine if it needs to check the feed…

if ($this->getFrequency() + $this->getLastUpdate() > time()) {
    return $this;
}

If it’s time to re-fetch the feed it…

Showing The Notifications to Admin Users

There are a couple ways that Magento presents notifications to admin users. The one that you’re probably most familiar with it the popup after login. This popup is controller by Magento\AdminNotification\Block\Window::canShow()

public function canShow()
{
    return $this->_authSession->isFirstPageAfterLogin() && $this->_getLatestItem();
}

In English, the popup is shown to the user if…

  1. This is the first page the user is viewing after logging in AND
  2. The notification’s severity was set to “critical” AND
  3. The notification hasn’t been marked as read or removed.

Numbers 2 and 3 are specifically driven by Magento\AdminNotification\Model\ResourceModel\Inbox\Collection\Critical::_initSelect

protected function _initSelect()
{
    parent::_initSelect();
    $this->addOrder(
        'notification_id',
        self::SORT_ORDER_DESC
    )->addFieldToFilter(
        'is_read',
        ['neq' => 1]
    )->addFieldToFilter(
        'is_remove',
        ['neq' => 1]
    )->addFieldToFilter(
        'severity',
        \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL
    )->setPageSize(
        1
    );
    return $this;
}

Where It Breaks Down

Astute readers may have noticed something interesting about Magento\AdminNotification\Model\ResourceModel\Inbox\Collection\Critical::_initSelect…nowhere is the collection filtered for the user id of the currently logged in users. This is because the adminnotification_inbox table has no user id column! This can be seen by reviewing vendor/module-admin-notification/Setup/InstallSchema.php which defines the schema for that table…

$table = $installer->getConnection()->newTable(
    $installer->getTable('adminnotification_inbox')
)->addColumn(
    'notification_id',
    \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
    null,
    ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
    'Notification id'
)->addColumn(
    'severity',
    \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
    null,
    ['unsigned' => true, 'nullable' => false, 'default' => '0'],
    'Problem type'
)->addColumn(
    'date_added',
    \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
    null,
    ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT],
    'Create date'
)->addColumn(
    'title',
    \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
    255,
    ['nullable' => false],
    'Title'
)->addColumn(
    'description',
    \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
    '64k',
    [],
    'Description'
)->addColumn(
    'url',
    \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
    255,
    [],
    'Url'
)->addColumn(
    'is_read',
    \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
    null,
    ['unsigned' => true, 'nullable' => false, 'default' => '0'],
    'Flag if notification read'
)->addColumn(
    'is_remove',
    \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
    null,
    ['unsigned' => true, 'nullable' => false, 'default' => '0'],
    'Flag if notification might be removed'
)->addIndex(
    $installer->getIdxName('adminnotification_inbox', ['severity']),
    ['severity']
)->addIndex(
    $installer->getIdxName('adminnotification_inbox', ['is_read']),
    ['is_read']
)->addIndex(
    $installer->getIdxName('adminnotification_inbox', ['is_remove']),
    ['is_remove']
)->setComment(
    'Adminnotification Inbox'
);
$installer->getConnection()->createTable($table);

To summarize this means that admin notifications work something like this…

Why This Is Not Quite Sane

There are a couple underlying issues with the mechanics behind this module…

  1. The idea of a shared notification inbox for all system users in-and-of-itself reeks. If Magento is pushing out critical messages, it’s probably a good idea to not allow one user to dismiss them and prevent any other users from seeing them. It is worth noting that there are ACLs to limit user’s ability to remove or mark notification as read. While this adds some sanity to the implementation, the underlying architecture of a shared inbox is fundamentally broken.
  2. The fact that the pop-up shows indefinitely until dismissed has likely ticked off a number of Magento merchant users. Most users would probably expect that after seeing the notification the first time it would not show again.

Suffice it to say, there is immense room for improvement with this notification system.

How Could This Be Done Better

First and foremost, there should be a user_id column on the admin_notification_inbox table and notifications shouldn’t be system-wide, they should be to specific users.

Next, the notifications shouldn’t show perpetually until you mark them as read. As mentioned previously, users most likely expect to only see these messages once.

Finally, I’d suggest that the notifications Magento publishes be given different categories so that specific roles can be configured to receive specific categories of notifications. For example, store administrators should receive notifications about security updates, but those don’t need to the marketing team. However the marketing team might be interested to learn about feature enhancements.

Conclusion

Have thoughts about this? Leave them below.

Would love to gather feedback here so that this can ultimately be fixed in core.

Max Chadwick Hi, I'm Max!

I'm a software developer who mainly works in PHP, but also dabbles in Ruby and Go. Technical topics that interest me are monitoring, security and performance.

During the day I solve challenging technical problems at Something Digital where I mainly work with the Magento platform. I also blog about tech, work on open source and hunt for bugs.

If you'd like to get in touch with me the best way is on Twitter.