<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Plugin;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Event\AbstractImmutableEvent;
use Joomla\CMS\Event\Result\ResultAwareInterface;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Language\LanguageAwareInterface;
use Joomla\CMS\Language\LanguageAwareTrait;
use Joomla\Event\AbstractEvent;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\EventInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Plugin Class
*
* @since 1.5
*
* @TODO Starting from 7.0 the class will no longer implement DispatcherAwareInterface and LanguageAwareInterface
*/
abstract class CMSPlugin implements DispatcherAwareInterface, PluginInterface, LanguageAwareInterface
{
use DispatcherAwareTrait {
setDispatcher as traitSetDispatcher;
getDispatcher as traitGetDispatcher;
}
use LanguageAwareTrait {
setLanguage as traitSetLanguage;
getLanguage as traitGetLanguage;
}
/**
* A Registry object holding the parameters for the plugin
*
* @var Registry
* @since 1.5
*/
public $params = null;
/**
* The name of the plugin
*
* @var string
* @since 1.5
*/
protected $_name = null;
/**
* The plugin type
*
* @var string
* @since 1.5
*/
protected $_type = null;
/**
* Affects constructor behavior. If true, language files will be loaded automatically.
*
* @var boolean
* @since 3.1
*/
protected $autoloadLanguage = false;
/**
* Should I try to detect and register legacy event listeners, i.e. methods which accept unwrapped arguments? While
* this maintains a great degree of backwards compatibility to Joomla! 3.x-style plugins it is much slower. You are
* advised to implement your plugins using proper Listeners, methods accepting an AbstractEvent as their sole
* parameter, for best performance. Also bear in mind that Joomla! 5.x onwards will only allow proper listeners,
* removing support for legacy Listeners.
*
* @var boolean
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Implement your plugin methods accepting an AbstractEvent object
* Example:
* onEventTriggerName(AbstractEvent $event) {
* $context = $event->getArgument(...);
* }
*/
protected $allowLegacyListeners = true;
/**
* The application object
*
* @var CMSApplicationInterface
*
* @since 4.2.0
*/
private $application;
/**
* Constructor
*
* @param array $config An optional associative array of configuration settings.
* Recognized key values include 'name', 'group', 'params', 'language'
* (this list is not meant to be comprehensive).
*
* @since 1.5
*/
public function __construct($config = [])
{
if ($config instanceof DispatcherInterface) {
@trigger_error(
\sprintf(
'Passing an instance of %1$s to %2$s() will not be supported in 7.0. '
. 'Starting from 7.0 CMSPlugin class will no longer implement DispatcherAwareInterface.',
DispatcherInterface::class,
__METHOD__
),
\E_USER_DEPRECATED
);
// Set the dispatcher we are to register our listeners with
$this->setDispatcher($config);
$config = \func_num_args() > 1 ? func_get_arg(1) : [];
}
// Get the parameters.
if (isset($config['params'])) {
if ($config['params'] instanceof Registry) {
$this->params = $config['params'];
} else {
$this->params = new Registry($config['params']);
}
}
// Get the plugin name.
if (isset($config['name'])) {
$this->_name = $config['name'];
}
// Get the plugin type.
if (isset($config['type'])) {
$this->_type = $config['type'];
}
// Load the language files if needed.
if ($this->autoloadLanguage) {
$this->loadLanguage();
}
if (property_exists($this, 'app')) {
@trigger_error('The application should be injected through setApplication() and requested through getApplication().', E_USER_DEPRECATED);
$reflection = new \ReflectionClass($this);
$appProperty = $reflection->getProperty('app');
if ($appProperty->isPrivate() === false && \is_null($this->app)) {
$this->app = Factory::getApplication();
}
}
if (property_exists($this, 'db')) {
@trigger_error('The database should be injected through the DatabaseAwareInterface and trait.', E_USER_DEPRECATED);
$reflection = new \ReflectionClass($this);
$dbProperty = $reflection->getProperty('db');
if ($dbProperty->isPrivate() === false && \is_null($this->db)) {
$this->db = Factory::getDbo();
}
}
}
/**
* Loads the plugin language file
*
* @param string $extension The extension for which a language file should be loaded
* @param string $basePath The basepath to use
*
* @return boolean True, if the file has successfully loaded.
*
* @since 1.5
*/
public function loadLanguage($extension = '', $basePath = JPATH_ADMINISTRATOR)
{
if (empty($extension)) {
$extension = 'Plg_' . $this->_type . '_' . $this->_name;
}
$extension = strtolower($extension);
$lang = $this->getApplication() ? $this->getApplication()->getLanguage() : Factory::getLanguage();
// If language already loaded, don't load it again.
if ($lang->getPaths($extension)) {
return true;
}
return $lang->load($extension, $basePath)
|| $lang->load($extension, JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name);
}
/**
* Registers legacy Listeners to the Dispatcher, emulating how plugins worked under Joomla! 3.x and below.
*
* By default, this method will look for all public methods whose name starts with "on". It will register
* lambda functions (closures) which try to unwrap the arguments of the dispatched Event into method call
* arguments and call your on<Something> method. The result will be passed back to the Event into its 'result'
* argument.
*
* This method additionally supports Joomla\Event\SubscriberInterface and plugins implementing this will be
* registered to the dispatcher as a subscriber.
*
* @return void
*
* @since 4.0.0
*
* @deprecated 5.4.0 will be removed in 7.0
* Plugin should implement SubscriberInterface.
* These plugins will be added to dispatcher in PluginHelper::import().
*/
public function registerListeners()
{
// Plugins which are SubscriberInterface implementations are handled without legacy layer support
if ($this instanceof SubscriberInterface) {
$this->getDispatcher()->addSubscriber($this);
return;
}
@trigger_error('The plugin should implement SubscriberInterface.', \E_USER_DEPRECATED);
$reflectedObject = new \ReflectionObject($this);
$methods = $reflectedObject->getMethods(\ReflectionMethod::IS_PUBLIC);
/** @var \ReflectionMethod $method */
foreach ($methods as $method) {
if (!str_starts_with($method->name, 'on')) {
continue;
}
// Save time if I'm not to detect legacy listeners
if (!$this->allowLegacyListeners) {
$this->registerListener($method->name);
continue;
}
/** @var \ReflectionParameter[] $parameters */
$parameters = $method->getParameters();
// If the parameter count is not 1 it is by definition a legacy listener
if (\count($parameters) !== 1) {
$this->registerLegacyListener($method->name);
continue;
}
/** @var \ReflectionParameter $param */
$param = array_shift($parameters);
$paramName = $param->getName();
// No type hint / type hint class not an event or parameter name is not "event"? It's a legacy listener.
if ($paramName !== 'event' || !$this->parameterImplementsEventInterface($param)) {
$this->registerLegacyListener($method->name);
continue;
}
// Everything checks out, this is a proper listener.
$this->registerListener($method->name);
}
}
/**
* Registers a legacy event listener, i.e. a method which accepts individual arguments instead of an AbstractEvent
* in its arguments. This provides backwards compatibility to Joomla! 3.x-style plugins.
*
* This method will register lambda functions (closures) which try to unwrap the arguments of the dispatched Event
* into old style method arguments and call your on<Something> method with them. The result will be passed back to
* the Event, as an element into an array argument called 'result'.
*
* @param string $methodName The method name to register
*
* @return void
*
* @since 4.0.0
*
* @deprecated 5.4.0 will be removed in 7.0
* Plugin should implement SubscriberInterface.
*/
final protected function registerLegacyListener(string $methodName)
{
$this->getDispatcher()->addListener(
$methodName,
function (AbstractEvent $event) use ($methodName) {
// Get the event arguments
$arguments = $event->getArguments();
// Extract any old results; they must not be part of the method call.
$allResults = [];
if (isset($arguments['result'])) {
$allResults = $arguments['result'];
unset($arguments['result']);
}
// Convert to indexed array for unpacking.
$arguments = array_values($arguments);
$result = $this->{$methodName}(...$arguments);
// Ignore null results
if ($result === null) {
return;
}
if ($event instanceof ResultAwareInterface) {
$event->addResult($result);
} elseif (!$event instanceof AbstractImmutableEvent) {
// Restore the old results and add the new result from our method call
$allResults[] = $result;
$event['result'] = $allResults;
}
}
);
}
/**
* Registers a proper event listener, i.e. a method which accepts an AbstractEvent as its sole argument. This is the
* preferred way to implement plugins in Joomla! 4.x and will be the only possible method with Joomla! 5.x onwards.
*
* @param string $methodName The method name to register
*
* @return void
*
* @since 4.0.0
*
* @deprecated 5.4.0 will be removed in 7.0
* Plugin should implement SubscriberInterface.
*/
final protected function registerListener(string $methodName)
{
$this->getDispatcher()->addListener($methodName, [$this, $methodName]);
}
/**
* Checks if parameter is typehinted to accept \Joomla\Event\EventInterface.
*
* @param \ReflectionParameter $parameter
*
* @return boolean
*
* @since 4.0.0
*
* @deprecated 5.4.0 will be removed in 7.0
* Plugin should implement SubscriberInterface.
*/
private function parameterImplementsEventInterface(\ReflectionParameter $parameter): bool
{
$reflectionType = $parameter->getType();
// Parameter is not typehinted.
if ($reflectionType === null) {
return false;
}
// Parameter is nullable.
if ($reflectionType->allowsNull()) {
return false;
}
// Handle standard typehints.
if ($reflectionType instanceof \ReflectionNamedType) {
return is_a($reflectionType->getName(), EventInterface::class, true);
}
// Handle PHP 8 union types.
if ($reflectionType instanceof \ReflectionUnionType) {
foreach ($reflectionType->getTypes() as $type) {
if (!is_a($type->getName(), EventInterface::class, true)) {
return false;
}
}
return true;
}
return false;
}
/**
* Returns the internal application or null when not set.
*
* @return CMSApplicationInterface|null
*
* @since 4.2.0
*/
protected function getApplication(): ?CMSApplicationInterface
{
return $this->application;
}
/**
* Sets the application to use.
*
* @param CMSApplicationInterface $application The application
*
* @return void
*
* @since 4.2.0
*/
public function setApplication(CMSApplicationInterface $application): void
{
$this->application = $application;
if ($application->getLanguage()) {
$this->setLanguage($application->getLanguage());
}
}
/**
* Set the language to use.
*
* @param Language $language The language to use
*
* @return void
*
* @since 5.3.0
*
* @deprecated 5.2 will be removed in 7.0
* Plugin should use the language from Application, and only after the app is initialised
*/
public function setLanguage(Language $language): void
{
$this->traitSetLanguage($language);
}
/**
* Get the Language.
*
* @return Language
*
* @throws \UnexpectedValueException May be thrown if the language has not been set.
*
* @since 5.3.0
*
* @deprecated 5.2 will be removed in 7.0
* Plugin should use the language from Application, and only after the app is initialised.
*/
protected function getLanguage(): Language
{
@trigger_error(
__CLASS__ . ': Use of LanguageAwareInterface over CMSPlugin will be removed in 7.0.',
\E_USER_DEPRECATED
);
return $this->traitGetLanguage();
}
/**
* Set the dispatcher to use.
*
* @param DispatcherInterface $dispatcher The dispatcher to use.
*
* @return $this
*
* @since 5.3.0
*
* @deprecated 5.2 will be removed in 7.0
* Plugin should implement DispatcherAwareInterface on its own, when it is needed.
*/
public function setDispatcher(DispatcherInterface $dispatcher)
{
@trigger_error(
__CLASS__ . ': Use of DispatcherAwareInterface over CMSPlugin will be removed in 7.0.'
. ' Plugin should implement DispatcherAwareInterface on its own, when it is needed.',
\E_USER_DEPRECATED
);
return $this->traitSetDispatcher($dispatcher);
}
/**
* Get the event dispatcher.
*
* @return DispatcherInterface
*
* @throws \UnexpectedValueException May be thrown if the dispatcher has not been set.
*
* @since 5.3.0
*
* @deprecated 5.2 will be removed in 7.0
* Plugin should implement DispatcherAwareInterface on its own, when it is needed.
*/
public function getDispatcher()
{
return $this->traitGetDispatcher();
}
}