How to Fix Joomla Content Plugins

Long-time users of Joomla are quite familiar with content plugins. When you want to take some user-entered text and reformat it into something else, there’s nothing quite as handy as having a content plugin ready to do your bidding.

However, Joomla 1.5 brought some subtle behavior changes. In Joomla 1.0, content plugins act on both articles entered through the Article Manager, as well as HTML entered in user created modules. When a Joomla 1.0 content plugin is recoded for 1.5, the original effect still takes place on content items, but module output is left unchanged.

What happened? Content plugins in Joomla 1.5 are designed to only act on articles managed through the Article Manager. This will seem inconvenient to people used to the old behavior, but there is a good reason for this change. While many content plugins reformat user-entered HTML, others add markup near the article title or just after an article is output. Also, some content plugins are designed to do additional tasks when articles are saved. These actions do not make sense within the context of a module, so content plugins no longer run on them.

While this good in that it enforces consistency, it also poses a problem. Now that content plugins only work on articles, how do you reformat markup coming from modules? Fortunately, there are a couple of workarounds in Joomla 1.5 achieving similar (if not better) results.

Consider the following content plugin. It’s designed to find strings prefixed with @ and turn them into links to Twitter user pages. This is similar to other search-and-replace content plugins likely running on your Joomla site right now:

<?php
defined( '_JEXEC' ) or die( 'Restricted access' );

jimport( 'joomla.plugin.plugin' );

class plgContentTwitterusername extends JPlugin
{
function onPrepareContent( &$article, &$params, $limitstart )
{
$pattern = '/(\W)@([a-zA-Z0-9_]+)(\W)/';
$replacement = '$1<a href="http://twitter.com/$2">@$2</a>$3';
$article->text = preg_replace($pattern, $replacement, $article->text);

return true;
}
}

Aside from the regular expression defined in $pattern, this is a straightforward content plugin: find all valid Twitter usernames and turn them into links. Since this is a content plugin, it will only do the replacement on articles managed by com_content. To get this code to run on the output of other components as well as modules, there are three viable options.

Option 1: Recode Content plugins as System plugins

The most reliable way of replicating Joomla 1.0 content plugin behavior in 1.5 is to rewrite content plugins as a system plugins. Fortunately, Joomla 1.5 provides you with much more control over page output than 1.0 does. Most importantly, Joomla 1.5 has the onAfterRender system event. This allows you to add code that runs once all output is generated, but right before Joomla sends it back to the browser.

Let’s rewrite the Twitter Username content plugin as a system plugin that responds to onAfterRender. Taking the original regular expression code, we can do a search and replace on the output buffer rather than an article object:

<?php
defined( '_JEXEC' ) or die( 'Restricted access' );

jimport( 'joomla.plugin.plugin' );

class plgSystemTwitterusername extends JPlugin
{
function onAfterRender()
{
$output = JResponse::getBody();

$pattern = '/(\W)@([a-zA-Z0-9_]+)(\W)/';
$replacement = '$1<a href="http://twitter.com/$2">@$2</a>$3';
$output = preg_replace($pattern, $replacement, $output);

JResponse::setBody($output);

return true;
}
}

NOTE: if you’re running this code in a Joomla installation alongside the content plugin, be sure to turn the content plugin off first. This will prevent the search and replace from running twice.

First notice that onAfterRender does not accept any parameters; we now need to find a way of getting the output we want to filter. The getBody() method in JResponse allows us to get all of the HTML output generated by Joomla. Next, we run the same search and replace technique used in the content plugin. Finally, we replace the current output buffer with our filtered version. We do this by passing it into JResponse::setBody().

If you publish this plugin and go to the administrator backend, you’ll notice that inside of your text editor, any @twitterusernames will also be changed into links. This is because system plugins run in both the frontend and backend. The onAfterRender event gets triggered in both places. When editing content the backend, you usually want to edit the raw code rather than a filtered version, so the current behavior is not desirable.

To make sure this system plugin only executes when we’re in the frontend, add the following code just before the first line in the function:

$app = JFactory::getApplication();

if($app->isAdmin()) {
return;
}

The function JFactory::getApplication() returns a JApplication object, which has the isAdmin() method. This method returns a boolean value we can use to determine whether we’re in the frontend or backend portion of the website. If we’re in the backend of the website, the return statement allows us to bail from the function early.

In addition to checking for the backend, you may also want to check when option is set to com_content and task is set to edit. This will allow you to determine when people are editing content items in the frontend; another scenario where filtering is not desirable.

The main advantage with recoding content plugins as system plugins is that you regain the functionality of Joomla 1.0 content plugins. Also, you only incur one call to the onAfterRender event, as opposed to the potential of several onPrepareContent event calls on the same content plugin. The biggest caveat is that you are searching and replacing all output, including the markup in your template.

If you use this method, make sure that the output you’re trying to search and replace is well defined and does not replace output that should be left alone. Also, onAfterRender events apply to the output of all formats; this includes RSS feeds, XML files, JSON calls, and other raw formats. A call to JRequest::getVar(‘format’) will return the format being requested; you can then use this to determine whether or not you wish to do the replacement.

Option 2: Trigger content plugins at onAfterRender

Sometimes you won’t want to go to the extent of completely rewriting a content plugin just to use it elsewhere. An alternative is to manually trigger the onPrepareContent event during the onAfterRender event in a system plugin. The big advantage with this is that you don’t have to disturb your existing plugins: you simply execute them again.

Here’s an example of an onAfterRender event handler for system plugin that runs the twitterusername content plugin:

function onAfterRender()
{
$app = JFactory::getApplication();

if($app->isAdmin()) {
return;
}

$dispatcher = JDispatcher::getInstance();

$article = new stdClass();

$body = JResponse::getBody();
$article->text = $body;

$params = array();

$results = $dispatcher->trigger(
'onPrepareContent', array (&$article, &$params, 0)
);

JResponse::setBody($article->text);
}

Like the original system plugin, we first check to make sure we aren’t in the backend. Next, we get an instance of JDispatcher, which is used to trigger plugin events.

Before triggering the onPrepareContent event, we need to assemble the three arguments that the event will be expecting. First, there’s the $article object. Since our twitterusername content plugin only looks at the text property of the article object, we can use PHP’s stdClass to get an empty object and simulate it. We get the output generated by Joomla through JResponse::getBody(), then set the text property of our $article object with the results.

Next, the onPrepareContent event will expect the article parameters. Again, since our twitterusername plugin doesn’t check any parameters, we can pass in the empty array we set in $params. The final parameter is used to designate which page we’re on in multi-part articles. Since this is of no use to the twitterusername plugin, we can pass in 0.

With our data ready, we call the trigger() method of the JDispatcher object we stored in $dispatcher. This method takes two arguments: the name of the event to trigger and an array of arguments to pass to the event handler. The array we pass in has the $article and $params variables, along with the value 0. The variables are expected to be passed in by reference, so we prefix both with &.

If desired, we can check the status of the plugin execution in the $results variable set with the return value of JDispatcher::trigger(). In our case, we really just want the plugin to execute and the output to proceed as normal, regardless of the status. Since our content filters through the twitterusername plugin, we can now set the output again using JResponse::setBody(). We passed the $article object to the content plugin by reference, so $article->text has the output we want.

The primary advantage of loading the content plugins through the system plugin is that we don’t have to rewrite the existing plugins. This can be very helpful when using third-party plugins. You can patch them individually without worrying about manually applying the same code to custom system plugins.

This approach has some drawbacks. First, if a specific page is generated by com_content, content plugins will execute on the component output twice. Depending on the nature of your content plugins, this can result in double replacements as well as degraded performance. Also, your plugins may be relying on properties of the $article and $params variables that you may have to further mock up. Finally, you are executing all of the onPrepareContent plugin event handlers at once, which makes this approach impractical if you’re only wanting to run one of the content plugins on the output as a whole.

Option 3: Filter your extension output explicitly

If you are writing a custom Joomla extension, it is possible to filter your output through the onPrepareContent events in specific places. This allows you to avoid creating a new plugin or rewriting an existing one. It’s also extremely easy to implement. Simply pass your data through this JHTML function:

JHTML::_('content.prepare', $yourTextHere);

The JHTML::_(‘content.prepare’) function will return the variable you pass in after filtering it through the onPrepareContent events. The simplicity of this method is very attractive for when you’re writing custom code. However, you should only use it in one or two specific places at a time. Each call to this function will trigger all of your content plugins. The more you do this, the longer it will take for Joomla to render the page.

Alternatively, if you have an entire component view that needs to be filtered, you can use PHP’s output buffering functions to capture all of the output. This way, you can filter several fields at once without incurring the overhead of running content plugins multiple times. Consider this modification to a standard JView::display() function:

ob_start();
parent::display($tmpl);
$output = ob_get_contents();
ob_end_clean();

echo JHTML::_('content.prepare', $output);

This code starts by creating an output buffer. The call to parent::display($tmpl) renders the view’s layout into the buffer. Calling ob_get_contents() returns the output from parent::display($tmpl) that was just captured. To finish buffering, ob_end_clean() is called. Since we captured the output of parent::display($tmpl) in $output, we can now filter it through JHTML::_(‘content.prepare’) and echo it out.

This approach gives you the most control over when and where content plugins are executed. When possible, this option is preferable over the others. It is simple and doesn’t require to to build any new plugins. However, this option only works when you can modify the PHP code generating the original output. This will not work on Custom HTML modules as those are generated from Joomla’s core. Alternatively, you can clone the Custom HTML module type and modify it to run all of the output through JHTML::_(‘content.prepare’) before sending it to the browser.

Summary

Joomla 1.5 gives you several options for filtering your output through the onPrepareContent event of content plugins. You have control over when and where it is applied. If you want to universally apply filtering to all output from Joomla, build a system plugin that responds to the onAfterRender event. To run all of your content plugins on all output, a system plugin can trigger them again. When you’re writing your own extensions, JHTML::_(‘content.prepare’) is available for filtering specific pieces of output through the content plugins.