Plugins are fundamental part of our sites. They can do a lot of useful things and extend Joomla in ways the core may have not been designed to. For this reason plugins have a lot of power over the entire Joomla application. A mistake in a plugin can bring down a site or cause unexpected and unresolvable issues in third party components, plugins and modules.
The root cause of these problems is typically that the plugin author did test their plugin, but have only done so in the very narrow use case they expect their plugins to be used. This is called “happy path testing” and is almost as bad as no testing at all. When the plugin is used in any other context — the CLI application, non-HTML output, in cases where the output format may not be determined until after the component for the page has finished executing — it might cause unintended consequences, i.e. the site will break. Even worse, clients will start blaming the innocent parties: Joomla itself and third party developers whose software is written the right way and works perfectly fine.
To best demonstrate how easy it is to make grave mistakes with too little code let's see a plugin class which makes four major mistakes in six lines of code:
class PlgSystemFoobar extends \Joomla\CMS\Plugin\CMSPlugin { public function __construct(&$subject, $config) { parent::__construct($subject, $config); if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) return; $document = \Joomla\CMS\Factory::getDocument(); $document->addScript(\Joomla\CMS\Uri\Uri::root(true) . 'plugins/system/foobar/js/foobar.js'); } }
This looks like a deceptively simple system plugin. It adds a JavaScript file to every page load in the frontend. Right?
Well, that is its intention but not what it actually does. It also breaks Joomla 4's CLI and API applications, it breaks pages with non-HTML output, it forbids components from using non-HTML output and tries to load the JavaScript file from the wrong place.
Do not execute plugin logic in the plugin constructor
Let's think about Joomla's lifetime. In broad terms, the request ends up getting handled by Joomla's
index.php
file. This spins up the Joomla! application, e.g.
\Joomla\CMS\Application\SiteApplication
for the frontend. The main entry point for the application
object is the doExecute
method. This method does a lot of initialisation before routing and dispatching
the application, meaning that this initialisation takes place before Joomla has parsed SEF URLs or created a
document object. In fact, Joomla will load all enabled system plugins before Joomla
has figured out any basic facts about itself.
The developer of this plugin put their business logic in the constructor of the plugin which is executed at
this early stage of the Joomla application initialisation. While isClient()
will work, the
rest of the code which tries to get the document object will break the site.
The plugin erroneously goes through the \Joomla\CMS\Factory::getDocument()
method to
get the document. This is deprecated in Joomla 4. You are supposed to use the getDocument()
method of the application object. Had the developer done that they'd have seen that they are getting null because
the document has not been created yet.
In fact, all this code should be moved from the plugin object constructor to the onAfterDispatch
method to work correctly.
Do not go through the \Joomla\CMS\Factory
The second problem with this plugin is the exact reason why the Factory's
getDocument()
method is deprecated.
Calling the factory's getDocument()
method will forcibly create a document object
which will then be used by the application object. The document object is created based on the information in the
request. However, as you might recall, at this point Joomla has not yet parsed the SEF route! This would also be
true if this code was moved in the onAfterInitialise
event, the earliest system plugin event triggered
by Joomla.
Since the SEF URL has not been parsed Joomla cannot reliably know the type of document to use. Think for
example about a URL like https://www.example.com/foobar.json
which when gone through the SEF URL router
will, among other things, set format=json
in the request. This means that this request expects Joomla to
create a \Joomla\CMS\Document\JsonDocument
document object.
However, since format=json
has not been set yet, Joomla will assume format=html
when you
call getDocument()
. Therefore, it will create a
\Joomla\CMS\Document\HtmlDocument
document object which will be used by the application
object as well. This will of course break the component which is handling the request as it (correctly) expect a
JSONDocument and it instead gets an HTMLDocument. I think we can all agree that the plugin's author is at fault for
this mess.
You should only ever call two methods of the
\Joomla\CMS\Factory
in Joomla 4:
-
getContainer()
. This returns Joomla's Dependency Injection container (DI Container, sometimes abbreviated as DIC). If possible, you should avoid using it directly, instead pushing dependencies through the service provider of your extension. -
getApplication()
. This returns the current Joomla application object handling the request. If possible, your extension should be passed the application object from its service provider instead of getting the object through the factory in your extension's code. The service provider will still have to go through the factory as there is not yet a single container resource which returns the currently active application (but this might change in Joomla 5.0 and will almost definitely change by Joomla 6.0).
To get the application's document you should do
\Joomla\CMS\Factory::getApplication()->getDocument();
.
There is more to Joomla than HTML output
This is an absurdly common mistake. Developers seem to assume that Joomla will only ever generate HTML output. This is an unreasonable assumption since Joomla 1.0 was released in 2005 — not to mention that it was not a reasonable assumption in Mambo, Joomla's predecessor, either. Joomla is perfectly capable of generating non-HTML output such as XML, JSON, RSS feeds, Atom feeds, raw binary output (e.g. images) and so on and so forth. If there was ever a question as to whether this is possible, changes made early in the Joomla 3.x releases introducing new core document and view classes should have driven that point home.
The developer of this plugin made the unreasonable assumption that their
$document
will always contain an HTMLDocument
.
A URL like https://www.example.com/index.php?option=com_whatever&format=json
— despite the two
problems already mentioned above — would still populate $document
with a
JSONDocument
object. However, JSONDocument
does not have an
addScript
method. Therefore this plugin causes a PHP fatal error right away. Whoops!
The correct way to do that is called “feature detection”:
$document = \Joomla\CMS\Factory::getApplication()->getDocument(); if (!($document instanceof \Joomla\CMS\Document\HtmlDocument)) { return; }
If the document returned by our application is not an HTMLDocument
we do not try to do
anything else. Simple, isn't it?
There is more to Joomla than the frontend and backend
Now let's get to the biggest bug of them all: assuming that Joomla consists entirely of the frontend (site) and backend (administrator) application. This has not been true since Joomla 1.6, released in 2010. When I first wrote this section in 2022 it was already 12 years this assumption has been wrong and I would still see plugins making it and breaking Joomla sites.
Not to put too fine a point on this, this line is completely wrong:
if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) return;
Clearly, the developer wanted their code to mean “if this is not the frontend of the site don't do anything”.
Instead, what they actually wrote is “if this is the backend of the site — therefore this is the frontend of the
site or the api application, or the console application, or any custom application which extends from
Joomla's WebApplication
class — don't do anything”. Whoops!
As we have already mentioned, Joomla 4 has a number of applications shipped with it:
-
installation. This is the web installer when you build a new site. Third party code, like our counterexample system plugin, does not load in it and does not concern third party developers. It lives in the
installation
folder of your site. It's been around since Joomla 1.0.Note If an
installation
folder is present Joomla will redirect to it if you try to access any of its other web-based applications. This happens very early in theincludes/framework.php
file which is one of the first things loaded byincludes/app.php
, after loadingincludes/defines.php
and making sure thelibraries/vendor
folder exists.This automatic redirection does not happen when
\Joomla\CMS\Version::DEV_STATUS
is anything other thanstable
, i.e. inalpha
,beta
,rc
(Release candidate) releases anddev
(development) builds from the Joomla Git repository sources. -
site. The frontend of the site, accessed through the
index.php
file in your site's root. It's been around since Joomla 1.0. -
administrator. The backend of the site, accessed through
administrator/index.php
. It lives in theadministrator
folder of your site. It's been around since Joomla 1.0. -
api. The JSON API application, accessed through
api/index.php
. It lives in theapi
folder of your Joomla site. Introduced in Joomla 4.0. -
cli. The
cli/joomla.php
command line application. Introduced in Joomla 4.0.
Further to that, applications other than site and administrator have existing since Joomla 1.5.
From Joomla 1.5 onwards it's been possible to create your own custom application by extending
JApplicationWeb
. These custom application do load system plugins by default. They were used
to create custom entry points for callbacks, e.g. in payment plugins for e-commerce components. They are no longer
used as the reason for their existence has been made a moot point since the advent of com_ajax
in
Joomla 2.5.
From Joomla 1.6 and up until Joomla 5.4 it's possible for developers to create custom CLI applications by
extending JApplicationCli
. These applications do not load system plugins by default so they
are unlikely to have broken. Unlikely does not mean impossible, though; it's possible that a CLI application may
have a need to load plugins such as system
, content
,
privacy
etc.
This is why despite the fact that this has been an issue since the dawn of Joomla 1.5 in 2007 plugin developers may have not bumped into this until Joomla 4 was released.
The correct way to do this is, of course, to check explicitly for the application you want to run under:
if (!\Joomla\CMS\Factory::getApplication()->isClient('site')) return;
Do not load static resources directly and/or from your plugin's folder
This is a bonus round and not a bug which will break sites today, but it will break sites come Joomla 6.0 and a mild security concern. It also reminds us that plugins, like all other Joomla extension types, should be using the WebAssetManager.
The developer of the extension chose to load their static JavaScript file using the deprecated
addScript()
method of the document and located the file in the plugin's folder structure.
This is a two in one issue.
First of all, ever since Joomla 1.5 (released in 2007 — 15 years before I first wrote
this section in 2022) Joomla introduced the media
folder where extensions are expected to place
all publicly available static files, be they static files shipped with the extension or user-generated content
managed outside Joomla's Media Manager.
The developer of the plugin should have placed their JavaScript file in the
media/plg_system_foobar/js
folder using the following section in their plugin's XML
manifest:
<media folder="media" destination="plg_system_foobar"> <folder>js</folder> <file>joomla.asset.json</file> </media>
(We'll see what the joomla.asset.json file is in a moment)
It is a bad security practice mixing executable backend code (.php
files) with frontend
executable code (.js
, .es6
etc) files and static media files (CSS, images,
videos, ...).
Joomla is moving towards placing all frontend (web browser) accessible stuff into the media folder — even for templates, as of Joomla 4.1 — and will most likely start applying security controls to prevent web access to the plugin, component, module etc folders. You have been warned. In fact, you were being warned since 2007 but nobody was listening, so let's see how many developers will come screaming bloody murder when these access controls are enforced about 20 years after the preferred alternative was introduced in Joomla 1.5.0. Ahem. forgive me, I digressed.
The next issue is that HTMLDocument's addScript()
method has been deprecated. Joomla
4 has moved into using asset dependencies and a Web Asset Manager
to, well, manage the asset dependencies.
Assets and their dependencies are declared in the joomla.asset.json
file located in the
extension's subdirectory under the media
directory. So, the developer should have shipped a
file media/plg_system_foobar/joomla.asset.json
with the following contents:
{ "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", "name": "plg_system_foobar", "version": "1.0.0", "description": "Foobar plugin", "license": "GPL-3.0-or-later", "assets": [ { "name": "plg_system_foobar.foobar", "description": "Foobar JavaScript", "type": "script", "uri": "plg_system_foobar/foobar.js", "dependencies": [ "core" ], "attributes": { "defer": true } } ] }
This allows the developer to tell Joomla to add their script using this very simple piece of code:
$document->getWebAssetManager()->useScript('plg_system_foobar.foobar');
Here's the kicker. This is safe even if you don't check the Document object type. Yeah,
if $document
is a JSONDocument
which does not have a concept of web assets
this code would still work. Joomla defines the Web Asset Manager for all document types, even those which can't
possibly use it. It's the page renderer which will make use of the dependencies, if they are supported. I know,
right?! It's actually an amazingly good feature! That said, it's still a good idea to check the document type the
way I've told you to avoid doing pointless work, or introducing future bugs.
Putting it all together
Let's put everything we learned together. The tiny plugin barely grew by a couple of lines and it no longer breaks the sites it is installed in. The changes are in bold type:
class PlgSystemFoobar extends \Joomla\CMS\Plugin\CMSPlugin { public function onAfterDispatch() { if (!\Joomla\CMS\Factory::getApplication()->isClient('site')) return; $document = \Joomla\CMS\Factory::getApplication()->getDocument(); if (!($document instanceof \Joomla\CMS\Document\HtmlDocument)) { return; } $document->getWebAssetManager()->useScript('plg_system_foobar.foobar'); } }
It's still short. It's still readable. It's (mostly) future-proof — well, it's still using the legacy plugin structure but I was trying to draw your attention to the problems, not divert your attention to the plumbing covered in previous sections of this book.