Home > Articles > Web Development

This chapter is from the book

This chapter is from the book

Tour of Selected Core Plugins

One confusing thing about plugins is that they vary so much. The only thing they have in common is how they are called. Given this, we will start our discussion with a quick look at a few core plugins. These examples will give you an idea of the variety of tasks you can accomplish with plugins.

System: SEF

Our first example is the SEF plugin. This is a class called plgSystemSef in the file plugins/system/sef/sef.php and enables Joomla to use search-engine-friendly (SEF) URLs.

Where Is It Triggered?

Let’s start with how this plugin gets executed—in other words, the code that includes the plugin class and triggers this plugin’s event.

The SEF plugin is a system plugin and it is triggered with the onAfterRender event. Before we trigger the event, we need to include the plugin file.

If we are loading a page in the front end of our site, we invoke the render() method of the JSite class (in the file includes/application.php). Near the end of this method, we see the following line of code:

JPluginHelper::importPlugin('system');

This command loads all the enabled system plugins into working memory. (If a plugin is disabled in the Plugin Manager, it doesn’t get loaded.) We only have to do this command once during a given method. After the system plugin classes are loaded into working memory, we can trigger one or more system events.

Because these plugins are class declarations, we haven’t executed any code yet. Later in the render() method of the JSite class we actually trigger the onAfterRender() method:

// Trigger the onAfterRender event.
$this->triggerEvent('onAfterRender');

This triggers the onAfterRender event. Let’s follow the code to see how it works. The variable $this is an object of type JSite, so $this->triggerEvent calls the triggerEvent() method of the JSite class with one argument, the string 'OnAfterRender'.

JSite extends the JApplication class (libraries/joomla/application/application.php). Because JSite doesn’t have its own triggerEvent() method (in other words, it does not override that method inherited from its parent class), it calls the method from JApplication.

So the triggerEvent() method from JApplication gets executed. This code is as follows:

function triggerEvent($event, $args=null)
{
       $dispatcher = JDispatcher::getInstance();
       return $dispatcher->trigger($event, $args);
}

This code creates an object of type JDispatcher and then calls the trigger() method for that object. The $event argument is set to “onAfterRender” and, because we didn’t pass a second argument, the $args argument is set to its default value of null.

The result is that it executes the onAfterRender() method of every enabled plugin that is available in working memory. In this case, the search is limited to system plugins because we specified the type as “system” when we called importPlugin('system'), so only system plugins are loaded into our working memory.

Normally, our plugin method names should be consistent with the plugin type. For example, we should only use system event method names in system plugins. If we follow this convention, it doesn’t matter if we have other plugin types loaded into memory, since only methods that match the event type will be executed.

What Does It Do?

Now let’s look at the SEF plugin code. The code for the first part of the sef.php file is as follows:

// no direct access
defined ('_JEXEC') or die;

/**
* Joomla! SEF Plugin
*
* @package     Joomla
* @subpackage  System
*/
class plgSystemSef extends JPlugin
{
        /**
         * Converting the site URL to fit to the HTTP request
         */
        public function onAfterRender()
        {

The first line of code (after the documentation block which is not shown) is our standard defined command, which ensures that we are running this code inside Joomla. Before version 2.5, we needed a jimport statement to import the library file libraries/joomla/plugin/plugin.php. Starting with version 2.5, this file is loaded for us automatically by the Joomla platform’s autoloader. It contains the class JPlugin, which is the parent class for all our plugins. We use it when we declare this class name, plgSystemSef, as a subclass of the JPlugin class.

In this case, the type is system and the file name is sef, hence the full name plgSystemSef. Finally, we declare the public function onAfterRender().

This plugin scans the HTML document for links and converts those links to search-engine-friendly links. It also replaces relative URLs with full-path URLs for a few other types of links. We aren’t going to discuss the onAfterRender() method in detail, but let’s look at two of aspects of it.

First, let’s look at this code near the beginning of the method:

if ($app->getName() != 'site' || $app->getCfg('sef')=='0') {
        return true;
}

This is checking two conditions. The first one, $app->getName() != 'site', checks to see if we are not in the front end of our website. The second condition, $app->getCfg('sef')=='0', checks whether we have the search-engine-friendly URL’s parameter set to zero (off) in our Global Configuration. If either of these conditions is true, then we exit the method immediately with a return value of boolean true.

Why do we do this? We only want to change the URLs when (a) we are in the front end of the site and (b) when the SEF setting is set to yes. However, we need to understand that this plugin is executed every time we encounter the onAfterRender event, whether we are in the front end or the administrative back end, and regardless of the SEF setting. That is why we have to put the check inside our plugin to make sure that the conditions for running this apply. We check that the conditions are met and, if not, we just exit the method before we have made any changes to the document object.

The second important point is that the onAfterRender() method does not take any arguments and it returns a boolean value to indicate whether or not it executed successfully. Different plugin types and methods have different method signatures (sets of arguments passed to the method) and return different values, so you have to be aware of these when you create a plugin.

Authentication: joomla Folder

This plugin is run when a user logs in to the site. It checks that the user name and password are valid. It is one of three authentication plugins included in the core Joomla distribution and is the default method for checking Joomla users.

This plugin is in the file plugins/authentication/joomla/joomla.php and its class name is plgAuthenticationJoomla.

How Does It Get Executed?

When a user attempts to log in to a Joomla site, the authenticate() method of the JAuthentication class (libraries/joomla/user/authentication.php) is executed. In that method, we see the expected line of code

$plugins = JPluginHelper::getPlugin('authentication');

that loads all the enabled authentication plugins into working memory.

Later in that method, we see a foreach loop as follows:

foreach ($plugins as $plugin)
{
   $className = 'plg'.$plugin->type.$plugin->name;
   if (class_exists($className)) {
      $plugin = new $className($this, (array)$plugin);
   }
   else {
      // bail here if the plugin can't be created
      JError::raiseWarning(50, JText::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $className));
      continue;
   }

   // Try to authenticate
   $plugin->onUserAuthenticate($credentials, $options, $response);

This loops through any enabled authentication plugins and checks that the class name exists. If any enabled authentication plugin does not exist, it fails with an error. If all the classes exist, then it executes the last line, which triggers the onUserAuthenticate method for each plugin. Note that three arguments are passed to the plugin: $credentials, $options, and $response. We discuss them in the next section.

What Does It Do?

The code for the onUserAuthenticate method is shown in Listing 5.1.

Listing 5.1. onUserAuthenciate Method for Joomla Authentication

function onUserAuthenticate($credentials, $options, &$response)
{
    $response->type = 'Joomla';
    // Joomla! does not like blank passwords
    if (empty($credentials['password'])) {
        $response->status = JAUTHENTICATE_STATUS_FAILURE;
        $response->error_message = Text::_('JGLOBAL_AUTH_EMPTY_PASS_NOT_ALLOWED');
        return false;
    }
    // Initialise variables.
    $conditions = '';

    // Get a database object
    $db     = JFactory::getDbo();
    $query  = $db->getQuery(true);

    $query->select('id, password');
    $query->from('#__users');
    $query->where('username=' . $db->Quote($credentials['username']));

    $db->setQuery($query);
    $result = $db->loadObject();

    if ($result) {
        $parts   = explode(':', $result->password);
        $crypt   = $parts[0];
        $salt    = @$parts[1];
        $testcrypt = JUserHelper::getCryptedPassword(
            $credentials['password'], $salt);

        if ($crypt == $testcrypt) {
            // Bring this in line with the rest of the system
            $user = JUser::getInstance($result->id);
            $response->email = $user->email;
            $response->fullname = $user->name;
            if (JFactory::getApplication()->isAdmin()) {
                $response->language = $user->getParam('admin_language');
            }
            else {
                 $response->language = $user->getParam('language');
            }
            $response->status = JAUTHENTICATE_STATUS_SUCCESS;
            $response->error_message = '';
         } else {
            $response->status = JAUTHENTICATE_STATUS_FAILURE;
            $response->error_message = JText::_('JGLOBAL_AUTH_INVALID_PASS');
         }
    } else {
        $response->status = JAUTHENTICATE_STATUS_FAILURE;
        $response->error_message = JText::_('JGLOBAL_AUTH_NO_USER');
    }
}

Let’s discuss the code for this plugin. The first lines are as follows:

function onUserAuthenticate($credentials, $options, &$response)
  {

The method takes three arguments. The variable $credentials is an associative array with two elements: “password” and “username.” This is the password and username the user has typed in to the form. The second argument, $options, is not used in this method.

The third argument, $response, is very important. Notice that there is an ampersand (“&”) in front of $response. This tells us that this variable is passed by reference. This means that when we make changes to this object during our method, the calling method will see the changed object. See the sidebar entitled “Assign by Reference and Pass by Reference” for more information on this.

This method returns a boolean false if the login does not succeed. If the login does succeed, no value is returned. We do, however, pass data back to the calling method, because we change the $response object and those changes are available to the calling method after this method finishes.

Before version 2.5, we needed to use jimport to import the user helper. We need this class later on to encrypt the test password. In version 2.5 and later this class is loaded by the autoloader.

The next line

$response->type = 'Joomla';

sets the type field of the $response object to 'Joomla'. This field indicates what authentication plugin was used to validate the user.

The next code block is as follows:

// Joomla! does not like blank passwords
if (empty($credentials['password'])) {
       $response->status = JAUTHENTICATE_STATUS_FAILURE;
       $response->error_message = JText::_('JGLOBAL_AUTH_EMPTY_PASS_NOT_ALLOWED');
       return false;
}

This is an if statement that checks that there was a password entered. If not, the authentication fails. To indicate this, we set the status and error_message fields of the $response object and we return a boolean false.

The next block of code does a simple database query to get the user ID and password from the Joomla database. This is our first example of a database query, and it uses the JDatabaseQuery class that was added in version 1.6. The line

$db = JFactory::getDbo();

creates a JDatabase object. This is normally the first step for any database query. The next line

$query = $db->getQuery(true);

creates the JDatabaseQuery object. The next line

$query->select('id, password');

adds the database columns id and password to the SELECT part of the query. The next line

$query->from('#__users');

adds the #__users table to the query. Note that we access the table with the prefix "#__" (pound sign and two underscore characters). Before the query is actually run, this prefix will be replaced with the table prefix selected when Joomla was installed (for example, "jos_").

The next line

$query->where('username=' .
$db->Quote($credentials['username']));

adds a WHERE clause that restricts the query to rows where the username column is equal to the username element in the $credentials array. Because the username column must be unique within the database, we will only get one row from this query.

psn-dexter-padlock.jpg

The method $db->quote() is very important for security. It puts quotes around the username value and “escapes” any characters that have special meaning in SQL queries—for example, if single or double quotes could potentially be used to end one SQL statement and start a new statement. To protect against this, they are converted to \\' or \\". This causes the database to ignore the special meaning of these characters and prevents someone from entering in a SQL command in the username field.

At this point, we have built our query and are ready to run it against the database. This is done in the following code:

$db->setQuery($query);
$result = $db->loadObject();

The first line passes the query to the database object, and the second line runs the query against the database and returns the query results to the $result variable. If for some reason the query was not successful, $result will be empty or the boolean false.

The remainder of the method is an if/then/else block that starts as follows:

if ($result) {
  $parts     = explode(':', $result->password);
  $crypt     = $parts[0];
  $salt     = @$parts[1];
  $testcrypt = JUserHelper::getCryptedPassword($credentials['password'], $salt);

The first line checks that the $result variable evaluates to a boolean true. If it doesn’t, we skip down to the outside else code block as follows:

} else {
    $response->status = JAUTHENTICATE_STATUS_FAILURE;
    $response->error_message = JText::_('JGLOBAL_AUTH_NO_USER');
}

This gives the user an error message saying the login was not successful.

If the database query returned a valid result (in $result), then we execute the if code block. The first part is as follows:

$parts   = explode(':', $result->password);
$crypt  = $parts[0];
$salt   = @$parts[1];
$testcrypt = JUserHelper::getCryptedPassword($credentials['password'], $salt);

In the Joomla database, the password is stored as two fields separated by a colon. The first line in the previous code block uses the PHP explode function to put the two parts of the password column into an array called $parts. Then we put the first part of that into a variable called $crypt and the second part into a variable called $salt.

By default, Joomla uses a one-way hash command called md5 to encrypt passwords. By one way, we mean that you can only encrypt a password. You cannot decrypt it. To check that the user has entered the right password, we encrypt the value entered by the user and store in the $testcrypt variable.

Then we do another “if/then/else” code block, based on whether or not the encrypted value of the entered password equals the encrypted value stored in the database. This code block is as follows:

if ($crypt == $testcrypt) {
   // Bring this in line with the rest of the system
   $user = JUser::getInstance($result->id);
   $response->email = $user->email;
   $response->fullname = $user->name;
   if (JFactory::getApplication()->isAdmin()) {
      $response->language = $user->getParam('admin_language');
   }
   else {
      $response->language = $user->getParam('language');
   }
   $response->status = JAUTHENTICATE_STATUS_SUCCESS;
   $response->error_message = '';
 } else {
   $response->status = JAUTHENTICATE_STATUS_FAILURE;
   $response->error_message = JText::_('JGLOBAL_AUTH_INVALID_PASS');
 }

In the first part of the code block, our passwords match. So we get the user object and set the email and fullname fields of the $response object based on the user object values. Then we get the correct language object, depending on whether we are in the front or back end of the site. Finally, we set the status field of the $response to a success message.

If the passwords don’t equal, we set the status field to indicate a failure and set the error_message field.

Notice that we don’t issue a return command when the login is successful. Instead, this method uses a trick to communicate back to the calling method. The trick is that the $response variable is changed inside this method to show field values from the valid user object.

Recall that the plugin’s authenticate() method was called in our example from the authenticate() method of the JAuthentication class. If we look at the JAuthentication code after the plugin is called, we see the following:

    // If authentication is successful break out of the loop
    if ($response->status === JAUTHENTICATE_STATUS_SUCCESS)
    {
       if (empty($response->type)) {
           $response->type = isset($plugin->_name) ? $plugin->_name :
             $plugin->name;
       }
       if (empty($response->username)) {
           $response->username = $credentials['username'];
       }

       if (empty($response->fullname)) {
           $response->fullname = $credentials['username'];
       }

       if (empty($response->password)) {
           $response->password = $credentials['password'];
       }
    }
}
return $response;

This altered version of the $response object is available to this method and, in fact, is returned by this method. Even though the plugin method doesn’t return the $response object, it still passes its results back to the calling method via the updated $response object.

Content: joomla Folder

This plugin is in the file plugins/content/joomla/joomla.php and its class name is plgContentJoomla.

It has two methods. The onContentAfterSave() method is used to send a notification e-mail to users when a new article has been saved. The onContentBefore Delete() method is used to check whether a category has any items assigned to it before deleting it. Let’s look at the onContentBeforeDelete() method.

How Does It Get Executed?

When a user deletes categories, articles, contacts, or other items in the administrative back end of Joomla, the onContentBeforeDelete event is triggered. One place this is done is in the JModelAdmin class (libraries/joomla/application/component/modeladmin.php). If we examine the delete() method, we see the following code:

// Trigger the onContentBeforeDelete event.
$result = $dispatcher->trigger($this->event_before_delete, array($context, $table));

In this class, the field event_before_delete has been set to the string onContentBeforeDelete in the class’s constructor method.

There are two things to note about this code. First, we are expecting a return value, which is stored in the $result variable. Second, we pass two arguments to the trigger() method: the event name and an array with two elements. The trigger() method unpacks this array and passes each of its elements as arguments to the onContentBeforeDelete() method. In this case, the two arguments are $context and $table. The variable $context is designed to tell us something about the context in which this event has been triggered (for example, “com_categories.category”). The variable $table is an array of the data that is about to be deleted.

What Does It Do?

The first part of the method is as follows:

public function onContentBeforeDelete($context, $data)
{
    // Skip plugin if we are deleting something other than categories
    if ($context != 'com_categories.category') {
       return true;
    }

As discussed earlier, this plugin will be executed any time a user is deleting any type of content. Because this plugin checks whether a category has any items assigned to it, it only makes sense in the context of deleting a category. So the first if statement checks to make sure we are trying to delete a category. If not, we exit the plugin, returning a boolean true.

As mentioned earlier, every plugin has a PHP file and an XML file. A plugin’s XML file does three things. First, it provides descriptive information about the plugin, such as its name, version, date, author, and license. Second, it lists all the files that need to be installed or uninstalled. Finally, it defines any parameters or options that can be set when using the plugin. These options show in the Plugin Manager screen when the plugin is opened for editing.

Parameters in Joomla allow the website administrator to control details about how the site will work without needing to write programming code. In this example, the Content → Joomla! plugin allows the administrator to control whether or not to check that categories are empty before deleting them. This is accomplished with a parameter by the name of check_categories. We will discuss parameters in more detail later in this chapter.

The check_categories parameter allows the administrator to disable the category check. This is accomplished in the next code block of the method:

// Check if this function is enabled.
if (!$this->params->def('check_categories', 1)) {
    return true;
}

The object $this->params is a JRegistry object that contains the parameters saved in the #__extensions database table for this plugin. The def() method reads the parameter value, using a default value of 1 if the parameter is not defined. Recall that in PHP, a zero evaluates to a boolean false. Here we take advantage of this. The parameter will be zero if we don’t want to check categories and 1 if we do. If the parameter is zero, the condition (using the PHP “!” not operator) will be true, so we will halt the method and return true. If the parameter is not set or 1, we skip the return statement and continue with the method.

The next part of the method follows:

$extension = JRequest::getString('extension');
psn-dexter-padlock.jpg

Here, we get the $extension based on the value in the PHP $_REQUEST variable. Notice that we use the Joomla library method JRequest::getString(). We could just read the $_REQUEST array directly. However, it is strongly recommended always to use the JRequest methods to do this, since they provide built-in filtering. In this case, the getString() method filters out hex and URL-encoded characters. JRequest provides a number of methods for reading request variables (getString(), getInt(), getWord(), and so on), and we always want to use the most restrictive method that we can. In other words, if we know the request value should always be an integer, we should use getInt. See Appendix B for all the filter types available.

The next code block is shown here:

// Default to true if not a core extension
$result = true;

$tableInfo = array (
    'com_banners' => array('table_name' => '#__banners'),
    'com_contact' => array('table_name' => '#__contact_details'),
    'com_content' => array('table_name' => '#__content'),
    'com_newsfeeds' => array('table_name' => '#__newsfeeds'),
    'com_weblinks' => array('table_name' => '#__weblinks')
);

Here, we set our result variable to true as a default value. Then, we build an array of the different table names for the different extension types. This plugin will only work for these five extensions. This array tells us the table name for each extension.

The next section of code is as follows:

// Now check to see if this is a known core extension
if (isset($tableInfo[$extension]))
{
    // Get table name for known core extensions
    $table = $tableInfo[$extension]['table_name'];
    // See if this category has any content items
    $count = $this->_countItemsInCategory($table, $data->get('id'));

This checks whether our current extension is in the array of the five core extensions. If it is, we execute the code inside the if statement. If the current extension is not one of the five core extensions, we skip to the bottom of the method and just return the $result variable, which we set to true earlier.

Inside the if code block, we set the $table variable to the table name we defined earlier. Then we set the $count variable, using the private method _countItemsInCategory(). This method runs the database query to see how many items (articles, contacts, and so on) are in this category. Note that we pass as arguments the name of the table ($table) and the value data->get('id'), which gives us the id field for the category from the $data object that was passed in as the second argument.

The next section of code follows:

// Return false if db error
if ($count === false)
{
       $result = false;
}

This checks whether we got a valid result from our _countItemsInCategory() method. This method returns a number or a boolean false. Note that we use the triple === comparison operator to check that $count is a boolean and is false. We have to do that because zero is a valid return value from our countItemsInCategory() method. If the method did return false, then for some reason the database query returned an error. In this case, we set the return value to false. If the method did return a valid result, we enter the else block of code that follows.

else
  {
  // Show error if items are found in the category
  if ($count > 0) {
     $msg = JText::sprintf('COM_CATEGORIES_DELETE_NOT_ALLOWED', $data->get('title')) .
     JText::plural('COM_CATEGORIES_N_ITEMS_ASSIGNED', $count);
     JError::raiseWarning(403, $msg);
     $result = false;
  }
  // Check for items in any child categories (if it is a leaf, there are no child categories)
  if (!$data->isLeaf()) {
     $count = $this->_countItemsInChildren(
     $table, $data->get('id'), $data);
     if ($count === false)
     {
     $result = false;
     }
     elseif ($count > 0)
     {
       $msg = JText::sprintf('COM_CATEGORIES_DELETE_NOT_ALLOWED', $data->get('title')) .
       JText::plural('COM_CATEGORIES_HAS_SUBCATEGORY_ITEMS', $count);
       JError::raiseWarning(403, $msg);
       $result = false;
    }
  }
}

The first if statement checks if the count is greater than zero. If so, we produce a warning message to the user and set the $result variable to false.

An important point here is that, by returning false, this plugin will prevent the user from deleting the category. Another point here is that we don’t actually do the return statement until the end of the method. So we continue to execute the code.

The next section of code checks whether there are any items contained in any child categories, using the _CountItemsInChildren() method. Note that we use a shortcut to save a little processing time. There is a method in the $data object called isLeaf(). This method returns a true if the current category is a “leaf” in the category “tree,” meaning that it doesn’t have any child categories. If so, we don’t have to check for items in child categories. In this case, we skip the whole code block.

If there are child categories, and if there are any items in these categories, we create another warning message and we set the $result variable to false. Note that if both warning conditions are present—meaning we have items in the current category and in child categories—then we issue both warnings. We use the JError::raisewarning() to display the warning to the user, and we include the count of items in the warning message.

A cool new method called plural() was added to the JText class in Joomla version 1.6. This allows Joomla to automatically select the right language tag based on whether the number being shown is 1 or more than 1. We use that to show both of our warning messages. For example, we want it to say “item” if there is one (“1 item”) but “items” if there are more than one (“5 items”). The JText::plural() method does this for us without requiring an if statement. Also, it handles languages where there are different forms of a word for one, two, or three items.

The end of the method is the code "return $result;", which just returns true if no items were found or false otherwise. As noted previously, this method only does any real work when we are deleting in the #__categories table. We could have added this check into the category table class instead of using a plugin. Why use a plugin?

The answer is flexibility. Performing this check in a plugin provides the administrator a number of options. First of all, plugins can be disabled, which allows an administrator to remove the category checking. Second, the parameters in the plugin allow the individual checks to be turned on and off. Third, you can provide your own plugin that either replaces or supplements the functionality of this or any core plugin. Finally, this plugin provides an easy-to-follow model for third-party extension developers to use to provide category checking for their extensions.

This is a great demonstration of the real power of plugins to enhance the flexibility of the system. They can be disabled or replaced without hacking any core files, allowing you to control lots of behind-the-scenes processing in Joomla.

onBeforeCompileHead

Now we are going to have some fun. We’re going to write a simple plugin that uses an event called onBeforeCompileHead. This event allows us to modify the HTML head element in the page just before it is rendered. So we can use this event to modify any HTML element that goes in the head, including meta, title, link, or script elements.

How Does It Get Executed?

The onBeforeCompileHead event is triggered in the fetchHead() method of JDocumentRendererHtml (libraries/joomla/document/html/renderer/head.php). This method reads the information for the head HTML element from the document object and prints it out to the buffer in HTML text format. The following code triggers the event:

// Trigger the onBeforeCompileHead event
$app = JFactory::getApplication();
$app->triggerEvent('onBeforeCompileHead');

What Does It Do?

If we look at the HTML page source code for the home page of a site with the sample data installed, we see a series of meta elements inside the head element:

  <meta name="robots" content="index, follow" />
  <meta name="keywords" content="My keywords." />
  <meta name="rights" content="My rights." />
  <meta name="language" content="en-GB" />

Our plugin will be simple. It will add a “revised” attribute to the HTML meta element, based on a parameter that the user enters for the plugin. For example, the output of our plugin might be as follows:

  <meta name="revised" content="Mark Dexter, 17 March 2012" />

where the content attribute is the text typed into the plugin parameter.

To do this, we will need to understand how the JDocumentHTML object stores the data for the HTML head element. Let’s do a bit of investigating. In the fetchHead() method of JDocumentRendererHead class where the onBeforeCompileHead is triggered, we see that we have a variable $document in the method signature. This is a JDocumentHTML object, which has a method called getHeadData() that returns the header data for the document or page. If we put the command

var_dump($document->getHeadData());

in the fetchHead() method of that class (for example, just before the code that triggers the event) and then display the home page on the site, we will see a long dump of the output of the getHeadData(), part of which is shown in the following.

array
  'title' => string 'Home' (length=4)
  'description' => string 'My description.' (length=15)
  'link' => string '' (length=0)
  'metaTags' =>
    array
      'http-equiv' =>
        array
          'content-type' => string 'text/html' (length=9)
      'standard' =>
        array
          'robots' => string 'index, follow' (length=13)
          'keywords' => string 'My keywords.' (length=12)
          'rights' => string 'My rights.' (length=10)
            'language' => string 'en-GB' (length=5)

If we compare this to the HTML source code shown earlier, we see that the meta elements with name attributes are stored in the object as an associative array stored in the standard element inside the metaTags element. The value of the name attribute is the key to the associative array (for example, “robots”), and the value of the content attribute is the value of the associative array (for example, “index, follow”).

We want our plugin to add a new meta element with the name attribute of “revised” and the value to be the option entered in the Plugin Manager form by the user. We want to keep any existing meta elements and just add this as a new one.

Our code is going to work as follows:

  1. Read the existing header data from the document. This will be an array like the one shown earlier.
  2. Add an element to the associative array that is stored inside the standard element of the array inside the metaTags element. The key for this array will be “revised” and the data will be the value entered by the user for the parameter.
  3. Write back the modified array to the document object using the setHeaderData() (which is the mirror image of the getHeaderData() method).
  4. Finally, we only want to do this if there is some data in the plugin parameter. If it is empty, don’t do anything.

Now we are going to create the plugin. Here are the steps:

  1. Create the folder for the plugin. We’ll call the plugin “mymeta,” so we need to create a folder called plugins/system/mymeta.
  2. To save typing, we can copy some existing files and just edit them. Copy the files index.html, p3p.php, and p3p.xml from the plugins/system/p3p folder to the new plugins/system/mymeta folder. Then rename the p3p.php and p3p.xml to mymeta.php and mymeta.xml.
  3. Edit the mymeta.xml file so it appears as shown in Listing 5.2. Here we changed the name, author, creationDate, copyright, description, and filename XML tags.

    Listing 5.2. mymeta.xml File

    <?xml version="1.0" encoding="utf-8"?>
    <install version="1.6" type="plugin" group="system">
     <name>My Meta Plugin</name>
     <author>Mark Dexter and Louis Landry</author>
     <creationDate>January 2012</creationDate>
     <copyright>Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.</copyright>
     <license>GNU General Public License version 2 or later; see LICENSE.txt
    </license>
     <authorEmail>admin@joomla.org</authorEmail>
     <authorUrl>www.joomla.org</authorUrl>
     <version>2.5.0</version>
     <description>My Meta Plugin</description>
     <files>
        <filename plugin="mymeta">mymeta.php</filename>
        <filename>index.html</filename>
     </files>
     <config>
        <fields name="params">
         <fieldset name="basic">
            <field name="revised" type="text"
             description="Meta revised text for content attribute"
             label="Revised Content"
             default=""
             size="50"
             />
         </fieldset>
        </fields>
     </config>
    </install>

    We also changed the entire field element to add our new parameter. We set the name to “revised”; set the type to “text”; and set the description, label, and size.

  4. At this point, we have the code for entering the parameter for our plugin. Next we need to actually write the plugin. Listing 5.3 shows the listing for the mymeta.php file, with the plugin code.

    Listing 5.3. mymeta.php File

    <?php
    /**
     * @copyright Copyright (C) 2012 Mark Dexter and Louis Landry.
     * @license   GNU General Public License version 2 or later; see LICENSE.txt
     */
    // no direct access
    defined('_JEXEC') or die;
    jimport('joomla.plugin.plugin');
    /**
     * Example System Plugin
     */
    class plgSystemMyMeta extends JPlugin
    {
         function onBeforeCompileHead()
         {
            if ($this->params->get('revised')) {
               $document = JFactory::getDocument();
               $headData = $document->getHeadData();
               $headData['metaTags']['standard']['revised'] =
                  $this->params->get('revised');
               $document->setHeadData($headData);          }
         }
    }

    We have renamed the class to plugSystemMyMeta and named the function onBeforeCompileHead, the same as the event we are using for the plugin. The code is simple, once you understand the array structure of the getHeadData() method.

    First we check whether there is anything in the “revised” parameter from our plugin. If not, we skip all the processing.

    If there is something in this parameter, we proceed. We get the document object and then save the results of getHeadData() in $headData.

    We create a new associative array element called “revised” and set its value to the parameter value. Note that this is an array that is nested inside two other arrays, as we saw when we dumped this value earlier.

  5. At this point, our plugin is complete and ready to go. However, our Joomla installation doesn’t know about it yet. The files are in the correct folders, but there is no row in the #__extensions table for the plugin.

    Recall from Chapter 4 when we copied the beez20 template that we had to use the Discover feature to install the new template. The same thing holds true here.

    So, in the administrative back end, navigate to the Extensions → Extension Manager and select the Discover tab. Then click the Discover icon in the toolbar. You should see something similar to Figure 5.1.

    Figure 5.1

    Figure 5.1. Discover screen showing new plugin

  6. Now click the check box at the left and then click the Install icon in the toolbar. A message should display that indicates the plugin was successfully installed.

    Installing an extension creates a row in the #__extensions table that stores information about the plugin. Joomla only “knows about” extensions that are in this table. The Discover process looks for extensions that are in the file system and not in the #__extensions table.

    The normal way to install an extension is from an archive file created for that purpose. In the next section, we create a plugin and create a zip archive to allow it to be installed.

Now that our plugin is installed, let’s test it. Navigate to the Extensions → Plugin Manager and filter on system plugins. You should see the My Meta Plugin listed. Clicking on it should show a screen as shown in Figure 5.2.

Figure 5.2

Figure 5.2. Edit screen for My Meta Plugin

Change the plugin to Enabled, enter in something for the Revised Content, and then navigate to the home page of the site. In your browser, select the option to show the HTML source code for the page (for example, in Firefox, select View → Page source). You should see something like the following. The line added by the plugin is highlighted:

  <meta name="robots" content="index, follow" />
  <meta name="keywords" content="My keywords." />
  <meta name="rights" content="My rights." />
  <meta name="language" content="en-GB" />
  <meta name="revised" content="Mark Dexter, 17 March 2011" />
  <meta name="description" content="My description." />

As a final test, go back to the Plugin Editor and blank out the Revised Content value. Then redisplay the home page and check the source code. Now there should be no meta tag with the name="revised", since there was no content for this tag.

If we step back for a minute, we can appreciate how easy it was for us to make this change. We simply added two new files to the system and edited a few lines of code. With this small amount of work, we were able to change the content of the head element on every page in our site.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020