Looking for something?

In this article, we're going to create a custom notification system that integrates into the existing notification inbox system. This is the continuation of Magento: Administrator Notifications article.

All associated code and documentation is freely provided under the Boost Software License. Although not legally binding, this author humbly requests that derivative works are not licensed under the GPL or other such licenses. Such licenses are blatantly anti-business and are generally not safe for use in commercial settings.

10,000 Foot Overview

While the design of the Magento Core notifications system works well with the upgrade / patching strategy laid out by the Magento Core Team, it ultimately lacks the flexibility required for third party module development use. Say your company is actively supporting 50 different community modules. Not all of your customers will have installed all 50 modules, so it would not make sense to send all module notifications to everyone. It's clear that a higher level of granularity is required for such a system to be effective. While implementing our own notification system, we will be addressing this shortcoming and adding some additional features.

Enhanced Granularity - Notification Filtering

Before diving in, let's take a look at the RSS feed format that the core notification system reads from.

<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <atom:link href="https://www.magentocommerce.com/products/notifications/community/notifications.rss" rel="self" type="application/rss+xml" />
        <title>MagentoCommerce</title>
        <link>http://www.magentocommerce.com/</link>
        <description>MagentoCommerce</description>
        <copyright>Copyright (c) 2016 Magento</copyright>
        <webMaster>This email address is being protected from spambots. You need JavaScript enabled to view it. (Magento support)</webMaster>
        <language>en</language>
        <lastBuildDate>Thu, 31 Mar 2016 00:25:07 UTC</lastBuildDate>
        <ttl>300</ttl>
        <item>
            <title><![CDATA[Protect Your Business from Brute-Force Password Guessing Attacks]]></title>
            <link><![CDATA[https://magento.com/security/best-practices/protect-your-magento-installation-password-guessing ]]></link>
            <severity>1</severity>
            <description><![CDATA[We just posted an article on the Magento Security Center that shares best practices... ]]></description>
            <pubDate>Thu, 31 Mar 2016 00:25:07 UTC</pubDate>
        </item>
    </channel>
</rss>

According to the RSS2.0 specification, this feed fails validation on the grounds of non-namespaced custom elements. We're going to be fixing that in our format by leveraging namespaces. Specifically, we'll be adding an <extras:vendor> tag, an <extras:severity> tag, and an <extras:module> tag to bring the format up to spec. So without further ado; our altered format:

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Stabilus Extension Notifications</title>
    <link>https://www.thinklikeamage.com</link>
    <description>Notification channel for Stabilus Extensions</description>
    <extras:vendor>Stabilus</extras:vendor>
    <item>
      <title>Stabilus AdminNotification Module Updates Available</title>
      <link>https://www.thinklikeamage.com/</link>
      <description><![CDATA[Are you up to date?  Be sure to check out our release archive.]]></description>
      <pubDate>Mon, 20 June 2016 04:46:42 GMT</pubDate>
      <extras:severity>1</extras:severity>
      <extras:module>AdminNotification</extras:module>
    </item>
  </channel>
</rss>

By utilizing the <extras:vendor> and <extras:module> elements, we are able to categorize notifications on a per-vendor, per-module basis. The RSS2.0 specification only allows for a single <channel> per RSS feed, but in normal circumstances each software vendor would have their own website to host an RSS feed from, so this should not be a problem.

Remember that if your RSS feed contains markup, be sure to wrap the relative content in a <![CDATA[]]> tag. Now let's continue to the boring part of the module creation.

Boilerplate

Global Module Configuration File

First, we'll create the global configuration document. The only notable features of this file are that we are using the community codepool and that we have a dependency on Mage_AdminNotification. This file should be located at 'app/etc/modules/Stabilus_AdminNotification.xml'.

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <modules>
        <Stabilus_AdminNotification>
            <active>true</active>
            <codePool>community</codePool>
            <depends>
                <Mage_AdminNotification />
            </depends>
        </Stabilus_AdminNotification>
    </modules>
</config>

Internal Module Configuration File

Next, we'll create the internal configuration document. This document is fairly straight-forward -- the only notable anomalies being that there is no resource_model specified and the default values set for the 'stabilus/adminnotification/*' variables. There is no resource model specified because there is no database interaction required. This serves two purposes: first and foremost to simplify the overall solution and the runner-up being a negligible performance gain. Less notable is the observer that is referenced -- nothing out of the ordinary. This file is located at 'app/code/community/Stabilus/etc/AdminNotification/config.xml'.

<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<Stabilus_AdminNotification>
<version>1.0.0</version>
</Stabilus_AdminNotification>
</modules>
<global>
<blocks>
<stabilus_adminnotification>
<class>Stabilus_AdminNotification_Block</class>
</stabilus_adminnotification>
</blocks>
<helpers>
<stabilus_adminnotification>
<class>Stabilus_AdminNotification_Helper</class>
</stabilus_adminnotification>
</helpers>
<models>
<stabilus_adminnotification>
<class>Stabilus_AdminNotification_Model</class>
<!-- Note - no resource model specified intentionally -->
</stabilus_adminnotification>
</models>
</global>
<adminhtml>
<events>
<controller_action_predispatch>
<observers>
<stabilus_adminnotification>
<class>stabilus_adminnotification/observer</class>
<method>preDispatch</method>
</stabilus_adminnotification>
</observers>
</controller_action_predispatch>
</events>
</adminhtml>
<default>
<stabilus>
<adminnotification>
<feed_url>notifications.thinklikeamage.com/community/notifications.rss</feed_url>
<check_frequency_hours>24</check_frequency_hours>
<last_check>0</last_check>
<use_ssl>1</use_ssl>
<curlopt_timeout>2</curlopt_timeout>
<curlopt_sslversion>0</curlopt_sslversion>
<curlopt_verifypeer>1</curlopt_verifypeer>
<curlopt_verifyhost>2</curlopt_verifyhost>
</adminnotification>
</stabilus>
</default>
</config>

Internal System Configuration File

Next stop, we create the system configuration document to govern the user-configurable parts of the module. There are a couple of notable things in this file. First of all, we have defined our own source model for the check_frequency_hours field. This allows us to add custom options that aren't available in the default adminhtml source model without modifying the way that the core operates. Our code exists as its own separate entity which is guaranteed not to conflict with other third party modules. Likewise, a custom frontend model is defined for the last_update field. This is required because the Magento core has hard-coded values in the default adminhtml frontend model. More details will be included in the appropriate section. This file is located at 'app/code/community/Stabilus/AdminNotification/etc/system.xml'.

<?xml version="1.0" encoding="UTF-8"?>
<config>
<sections>
<system>
<groups>
<stabilus_adminnotification translate="label" module="stabilus_adminnotification">
<label>Stabilus Notifications</label>
<frontend_type>text</frontend_type>
<sort_order>275</sort_order>
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
<fields>
<check_frequency_hours translate="label comment">
<label>Update Check Frequency</label>
<comment>the minimum number of hours between update checks</comment>
<frontend_type>select</frontend_type>
<source_model>stabilus_adminnotification/system_config_source_notification_frequency</source_model>
<sort_order>50</sort_order>
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
</check_frequency_hours>
<last_update translate="label comment">
<label>Last Update</label>
<frontend_type>label</frontend_type>
<frontend_model>stabilus_adminnotification/system_config_form_field_notification</frontend_model>
<sort_order>100</sort_order>
<show_in_default>1</show_in_default>
<show_in_website>0</show_in_website>
<show_in_store>0</show_in_store>
</last_update>
</fields>
</stabilus_adminnotification>
</groups>
</system>
</sections>
</config>

Internal Admin Configuration File

Since we've nested the system configuration fields under the system section, we can simply use the existing ACL entries. Likewise, the AdminNotification module handles the ACL entries for the notification inbox routes as well. As such, we can safely exclude an 'adminhtml.xml' file for this module.

Interesting Code

If you've attempted to visit your administration panel at this point, you'll notice that everything's broken. None of the administrator routes work anymore and now give a warning such as this:

Warning: get_class() expects parameter 1 to be object, boolean given  in /usr/share/nginx/html/magento/app/code/core/Mage/Core/Model/App.php on line 1360

#0 [internal function]: mageCoreErrorHandler(2, 'get_class() exp...', '/usr/share/ngin...', 1360, Array)
#1 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Model/App.php(1360): get_class(false)
#2 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Model/App.php(1337): Mage_Core_Model_App->_callObserverMethod(false, 'preDispatch', Object(Varien_Event_Observer))
#3 /usr/share/nginx/html/magento/app/Mage.php(448): Mage_Core_Model_App->dispatchEvent('controller_acti...', Array)
#4 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Controller/Varien/Action.php(527): Mage::dispatchEvent('controller_acti...', Array)
#5 /usr/share/nginx/html/magento/app/code/core/Mage/Adminhtml/Controller/Action.php(160): Mage_Core_Controller_Varien_Action->preDispatch()
#6 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Controller/Varien/Action.php(407): Mage_Adminhtml_Controller_Action->preDispatch()
#7 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Controller/Varien/Router/Standard.php(254): Mage_Core_Controller_Varien_Action->dispatch('index')
#8 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Controller/Varien/Front.php(172): Mage_Core_Controller_Varien_Router_Standard->match(Object(Mage_Core_Controller_Request_Http))
#9 /usr/share/nginx/html/magento/app/code/core/Mage/Core/Model/App.php(365): Mage_Core_Controller_Varien_Front->dispatch()
#10 /usr/share/nginx/html/magento/app/Mage.php(684): Mage_Core_Model_App->run(Array)
#11 /usr/share/nginx/html/magento/index.php(83): Mage::run('default', 'store')
#12 {main}

If you're not seeing a warning like the one above, do yourself an early favor and apply a development configuration to your PHP installation.

Anyhow, recall that earlier we specified a pre-dispatch observer in the internal module configuration file? Magento is trying to load it but can't find it. In fact, there are a lot of holes that we're going to have to fill in now. Normally now I start from the top of the configuration file and work my way down filling in holes as I go.

Helper Classes

Remember that we've utilized the Magento translation system within our module configuration documents. Therefore we must create a Data helper that extends from the 'Mage_Core_Helper_Abstract' class. My approach for implementing Magento helpers is a little different than most. I always declare my own abstract helper class that extends from 'Mage_Core_Helper_Abstract' and then subsequently all of my helpers extend the new internal abstract helper. This allows for a clean, safe hierarchy.

Abstract Helper

The following file can be found at 'app/code/community/Stabilus/AdminNotification/Helper/Abstract.php'

<?php
abstract class Stabilus_AdminNotification_Helper_Abstract extends Mage_Core_Helper_Abstract {
    
    const XML_PATH_CHECK_FREQUENCY_HOURS = 'system/stabilus_adminnotification/check_frequency_hours';
    
    public function getCheckFrequencyHours() {
        return Mage::getStoreConfig(self::XML_PATH_CHECK_FREQUENCY_HOURS);
    }
}

The only thing that the abstract helper class does is provide user-friendly (read: IDE Friendly) wrappers around the otherwise undocumented system configuration fields. All helpers contained within the module will extend from this class.

Data Helper

Now in typical Magento fashion, the Data helper is formed. Note that I superstitiously leave this file devoid of implementation. The following file can be found at 'app/code/community/Stabilus/AdminNotificaiton/Helper/Data.php'.

<?php

class Stabilus_AdminNotification_Helper_Data extends Stabilus_AdminNotificaiton_Helper_Abstract {
    
}

Block Classes

The only block class that's used for this module is to render the 'system/stabilus_adminnotification/last_update' system configuration field. A custom renderer class is required because the core block has a hard-coded value defining the cache key to retrieve. Our new class externalizes the value retrieval mechanism via a helper to allow overrides in the normal Magento fashion. Additionally, this class gracefully handles situations when an update check has never occurred. The following file can be found at 'app/code/community/Stabilus/AdminNotification/Block/System/Config/Form/Field/Notification.php'.

<?php
class Stabilus_AdminNotification_Block_System_Config_Form_Field_Notification 
    extends Mage_Adminhtml_Block_System_Config_Form_Field_Notification {

    protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
    {
        $helper = Mage::helper('stabilus_adminnotification');
        
        $time = $helper->getLastUpdateCheck();
        
        /// Intuitively handle situations where a check has never occurred
        if(!$time) {
            return $helper->__('Never');
        }
        
        /// Convert the time to the format defined by the current locale
        $locale = Mage::app()->getLocale();
        $format = $locale->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM);
        return $locale->date(intval($time))->toString($format);
    }    
}

Model Classes

Frequency Source Model

In the system configuration file, you will notice that we're using a customized frequency source model. This source model adds some additional functionality to the existing core model. The following file can be found at 'app/code/community/Stabilus/AdminNotification/Model/System/Config/Source/Notification/Frequency.php'. Notice that the path to this file mimics that which is found in the mage/adminhtml core module.

<?php
class Stabilus_AdminNotification_Model_System_Config_Source_Notification_Frequency 
    extends Mage_Adminhtml_Model_System_Config_Source_Notification_Frequency {
    
    protected static $_options;
    
    public function toOptionArray() {

        /// Ensure this class is effectively a singleton (internally) even if 
        /// the user / system calls it improperly
        if(!self::$_options) {
            $helper = Mage::helper('stabilus_adminnotification');
            
            /// Retain the 'core' options...
            self::$_options = parent::toOptionArray();
            
            /// And add in some new ones
            self::$_options += array(0  => $helper->__('No Notification Checking'),
                                     3  => $helper->__('3 Hours'),
                                     4  => $helper->__('4 Hours'),
                                     9  => $helper->__('9 Hours'),
                                     15 => $helper->__('15 Hours'),
                                     18 => $helper->__('18 Hours'));
            
            /// Intuitively reorder the array by key
            if(!ksort(self::$_options)) {
                /// If this happens, the Magento Core Team has some explaining to do...
                Mage::log($helper->__('Sort failed at line %s in file %s', 
                        __LINE__, __FILE__), Zend_Log::WARN);
            }
        }
        return self::$_options;
    }
}

Observer

There really isn't anything notable about this observer. It simply forwards the execution to the business-logic model, 'Stabilus_AdminNotification_Model_Feed' if notification checking is enabled. The following file can be found at 'app/code/community/Stabilus/AdminNotification/Model/Observer.php'.

<?php

class Stabilus_AdminNotification_Model_Observer {

    public function preDispatch(Varien_Event_Observer $observer)
    {
        if (Mage::getSingleton('admin/session')->isLoggedIn() && 
                Mage::helper('stabilus_adminnotification')->areNotificationsEnabled()) {
            Mage::getSingleton('stabilus_adminnotification/feed')->checkUpdate();
        }
    }
}

Feed

The 'Stabilus_AdminNotification_Model_Feed' class is where the real magic happens. It can be found at 'app/code/community/Stabilus/AdminNotification/Model/Feed.php'.

Stepping out of the 'preDispatch' method of the 'Stabilus_AdminNotification_Model_Observer' class, we step directly into the 'checkUpdate' method of the 'Stabilus_AdminNotification_Model_Feed' class. Essentially, this method operates as follows:

  1. Turn on internal error handling for the libxml library while preserving the original state.
  2. Read, parse, and validate the remote XML feed
  3. Clear any libxml errors that might have been cased by the method (see the user-notes on http://php.net/manual/en/function.libxml-use-internal-errors.php)
  4. Restore the original libxml error handling state
  5. Pass the data parsed from the remote XML feed to the 'Mage_Adminhtml_Notification_Model_Inbox' class.
  6. Update the "last checked" system configuration field.
<?php

class Stabilus_AdminNotification_Model_Feed extends Mage_Core_Model_Abstract {
public function checkUpdate() { $helper = Mage::helper('stabilus_adminnotification'); try {
/// Utilize internal error handling to prevent any uncontrollable PHP warnings from being emitted /// Clear the error buffer libxml_clear_errors();
/// Preserve the existing status of the internal error flag $preserved_status = libxml_use_internal_errors(true);
/// Attempt to read and parse, and process the remote feed $data = $this->_processFeed($this->_parseXmlFeed($this->_readXmlFeed()));
/// Clear any errors that we caused libxml_clear_errors(); /// Restore the original state libxml_use_internal_errors($preserved_status); if($data) { /// Pass the custom feed off to the core inbox Mage::getModel('adminnotification/inbox')->parse(array_reverse($data)); } } catch (Exception $e) { Mage::logException($e); } /// Regardless of success or failure, reset the last update check timer $helper->setLastUpdateCheck(); }

The next part of this article is on hold until further notice due to a matter of security. Sorry for the inconvenience.