first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

9
libraries/.htaccess Normal file
View File

@ -0,0 +1,9 @@
# Apache 2.4+
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Apache 2.0-2.2
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>

88
libraries/bootstrap.php Normal file
View File

@ -0,0 +1,88 @@
<?php
/**
* Bootstrap file for the Joomla! CMS [with legacy libraries].
* Including this file into your application will make Joomla libraries available for use.
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
/**
* Set the platform root path as a constant if necessary.
*
* @deprecated 4.4.0 will be removed in 6.0
* Use defined('_JEXEC') or die; to detect if the CMS is loaded correctly
**/
defined('JPATH_PLATFORM') or define('JPATH_PLATFORM', __DIR__);
// Detect the native operating system type.
$os = strtoupper(substr(PHP_OS, 0, 3));
defined('IS_WIN') or define('IS_WIN', ($os === 'WIN'));
defined('IS_UNIX') or define('IS_UNIX', (($os !== 'MAC') && ($os !== 'WIN')));
// Import the library loader if necessary.
if (!class_exists('JLoader')) {
require_once JPATH_LIBRARIES . '/loader.php';
// If JLoader still does not exist panic.
if (!class_exists('JLoader')) {
throw new RuntimeException('Joomla Platform not loaded.');
}
}
// Setup the autoloaders.
JLoader::setup();
// Create the Composer autoloader
/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require JPATH_LIBRARIES . '/vendor/autoload.php';
// We need to pull our decorated class loader into memory before unregistering Composer's loader
class_exists('\\Joomla\\CMS\\Autoload\\ClassLoader');
$loader->unregister();
// Decorate Composer autoloader
spl_autoload_register([new \Joomla\CMS\Autoload\ClassLoader($loader), 'loadClass'], true, true);
/**
* Register the global exception handler. And set error level to server default error level.
* The error level may be changed later in boot up process, after application config will be loaded.
* Do not remove the variable, to allow to use it further, after including this file.
*/
$errorHandler = \Symfony\Component\ErrorHandler\ErrorHandler::register();
\Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer::setTemplate(__DIR__ . '/../templates/system/fatal.php');
// Register the error handler which processes E_USER_DEPRECATED errors
if (error_reporting() & E_USER_DEPRECATED) {
set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED);
}
// Suppress phar stream wrapper for non .phar files
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor())
);
if (in_array('phar', stream_get_wrappers())) {
stream_wrapper_unregister('phar');
stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper');
}
// Define the Joomla version if not already defined.
defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion());
// Set up the message queue logger for web requests
if (array_key_exists('REQUEST_METHOD', $_SERVER)) {
\Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']);
}
// Register the Crypto lib
JLoader::register('Crypto', JPATH_LIBRARIES . '/php-encryption/Crypto.php');
// Register the PasswordHash library.
JLoader::register('PasswordHash', JPATH_LIBRARIES . '/phpass/PasswordHash.php');

77
libraries/cms.php Normal file
View File

@ -0,0 +1,77 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
trigger_error(
sprintf(
'Bootstrapping Joomla using the %1$s file is deprecated. Use %2$s instead.',
__FILE__,
__DIR__ . '/bootstrap.php'
),
E_USER_DEPRECATED
);
/**
* Set the platform root path as a constant if necessary.
*
* @deprecated 4.4.0 will be removed in 6.0
* Use defined('_JEXEC') or die; to detect if the CMS is loaded correctly
**/
if (!defined('JPATH_PLATFORM')) {
define('JPATH_PLATFORM', __DIR__);
}
// Import the library loader if necessary
if (!class_exists('JLoader')) {
require_once JPATH_LIBRARIES . '/loader.php';
}
// Make sure that the Joomla Platform has been successfully loaded
if (!class_exists('JLoader')) {
throw new RuntimeException('Joomla Platform not loaded.');
}
// Create the Composer autoloader
$loader = require JPATH_LIBRARIES . '/vendor/autoload.php';
// We need to pull our decorated class loader into memory before unregistering Composer's loader
class_exists('\\Joomla\\CMS\\Autoload\\ClassLoader');
$loader->unregister();
// Decorate Composer autoloader
spl_autoload_register([new \Joomla\CMS\Autoload\ClassLoader($loader), 'loadClass'], true, true);
// Suppress phar stream wrapper for non .phar files
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor())
);
if (in_array('phar', stream_get_wrappers())) {
stream_wrapper_unregister('phar');
stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper');
}
// Define the Joomla version if not already defined
if (!defined('JVERSION')) {
define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion());
}
// Register a handler for uncaught exceptions that shows a pretty error page when possible
set_exception_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleException']);
// Set up the message queue logger for web requests
if (array_key_exists('REQUEST_METHOD', $_SERVER)) {
\Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']);
}
// Register the Crypto lib
JLoader::register('Crypto', JPATH_LIBRARIES . '/php-encryption/Crypto.php');

View File

@ -0,0 +1,55 @@
<?php
/**
* Bootstrap file for the Joomla Platform [with legacy libraries]. Including this file into your application
* will make Joomla Platform libraries [including legacy libraries] available for use.
*
* @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
trigger_error(
sprintf(
'Bootstrapping Joomla using the %1$s file is deprecated. Use %2$s instead.',
__FILE__,
__DIR__ . '/bootstrap.php'
),
E_USER_DEPRECATED
);
/**
* Set the platform root path as a constant if necessary.
*
* @deprecated 4.4.0 will be removed in 6.0
* Use defined('_JEXEC') or die; to detect if the CMS is loaded correctly
**/
if (!defined('JPATH_PLATFORM')) {
define('JPATH_PLATFORM', __DIR__);
}
// Detect the native operating system type.
$os = strtoupper(substr(PHP_OS, 0, 3));
if (!defined('IS_WIN')) {
define('IS_WIN', $os === 'WIN');
}
if (!defined('IS_UNIX')) {
define('IS_UNIX', $os !== 'MAC' && $os !== 'WIN');
}
// Import the library loader if necessary.
if (!class_exists('JLoader')) {
require_once JPATH_LIBRARIES . '/loader.php';
}
// Make sure that the Joomla Loader has been successfully loaded.
if (!class_exists('JLoader')) {
throw new RuntimeException('Joomla Loader not loaded.');
}
// Setup the autoloaders.
JLoader::setup();
// Register the PasswordHash lib
JLoader::register('PasswordHash', JPATH_LIBRARIES . '/phpass/PasswordHash.php');

55
libraries/import.php Normal file
View File

@ -0,0 +1,55 @@
<?php
/**
* Bootstrap file for the Joomla Platform. Including this file into your application will make Joomla
* Platform libraries available for use.
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
trigger_error(
sprintf(
'Bootstrapping Joomla using the %1$s file is deprecated. Use %2$s instead.',
__FILE__,
__DIR__ . '/bootstrap.php'
),
E_USER_DEPRECATED
);
/**
* Set the platform root path as a constant if necessary.
*
* @deprecated 4.4.0 will be removed in 6.0
* Use defined('_JEXEC') or die; to detect if the CMS is loaded correctly
**/
if (!defined('JPATH_PLATFORM')) {
define('JPATH_PLATFORM', __DIR__);
}
// Detect the native operating system type.
$os = strtoupper(substr(PHP_OS, 0, 3));
if (!defined('IS_WIN')) {
define('IS_WIN', $os === 'WIN');
}
if (!defined('IS_UNIX')) {
define('IS_UNIX', IS_WIN === false);
}
// Import the library loader if necessary.
if (!class_exists('JLoader')) {
require_once JPATH_LIBRARIES . '/loader.php';
}
// Make sure that the Joomla Platform has been successfully loaded.
if (!class_exists('JLoader')) {
throw new RuntimeException('Joomla Platform not loaded.');
}
// Setup the autoloaders.
JLoader::setup();
// Register the PasswordHash lib
JLoader::register('PasswordHash', JPATH_LIBRARIES . '/phpass/PasswordHash.php');

1
libraries/index.html Normal file
View File

@ -0,0 +1 @@
<!DOCTYPE html><title></title>

730
libraries/loader.php Normal file
View File

@ -0,0 +1,730 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
*/
defined('_JEXEC') or die;
/**
* Static class to handle loading of libraries.
*
* @since 1.7.0
*/
abstract class JLoader
{
/**
* Container for already imported library paths.
*
* @var array
* @since 1.7.0
*/
protected static $classes = [];
/**
* Container for already imported library paths.
*
* @var array
* @since 1.7.0
*/
protected static $imported = [];
/**
* Container for registered library class prefixes and path lookups.
*
* @var array
* @since 3.0.0
*/
protected static $prefixes = [];
/**
* Holds proxy classes and the class names the proxy.
*
* @var array
* @since 3.2
*/
protected static $classAliases = [];
/**
* Holds the inverse lookup for proxy classes and the class names the proxy.
*
* @var array
* @since 3.4
*/
protected static $classAliasesInverse = [];
/**
* Container for namespace => path map.
*
* @var array
* @since 3.1.4
*/
protected static $namespaces = [];
/**
* Holds a reference for all deprecated aliases (mainly for use by a logging platform).
*
* @var array
* @since 3.6.3
*/
protected static $deprecatedAliases = [];
/**
* The root folders where extensions can be found.
*
* @var array
* @since 4.0.0
*/
protected static $extensionRootFolders = [];
/**
* Method to discover classes of a given type in a given path.
*
* @param string $classPrefix The class name prefix to use for discovery.
* @param string $parentPath Full path to the parent folder for the classes to discover.
* @param boolean $force True to overwrite the autoload path value for the class if it already exists.
* @param boolean $recurse Recurse through all child directories as well as the parent path.
*
* @return void
*
* @since 1.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to
* register an autoloader for your files.
*/
public static function discover($classPrefix, $parentPath, $force = true, $recurse = false)
{
try {
if ($recurse) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($parentPath),
RecursiveIteratorIterator::SELF_FIRST
);
} else {
$iterator = new DirectoryIterator($parentPath);
}
/** @type $file DirectoryIterator */
foreach ($iterator as $file) {
$fileName = $file->getFilename();
// Only load for php files.
if ($file->isFile() && $file->getExtension() === 'php') {
// Get the class name and full path for each file.
$class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName));
// Register the class with the autoloader if not already registered or the force flag is set.
if ($force || empty(self::$classes[$class])) {
self::register($class, $file->getPath() . '/' . $fileName);
}
}
}
} catch (UnexpectedValueException $e) {
// Exception will be thrown if the path is not a directory. Ignore it.
}
}
/**
* Method to get the list of registered classes and their respective file paths for the autoloader.
*
* @return array The array of class => path values for the autoloader.
*
* @since 1.7.0
*/
public static function getClassList()
{
return self::$classes;
}
/**
* Method to get the list of deprecated class aliases.
*
* @return array An associative array with deprecated class alias data.
*
* @since 3.6.3
*/
public static function getDeprecatedAliases()
{
return self::$deprecatedAliases;
}
/**
* Method to get the list of registered namespaces.
*
* @return array The array of namespace => path values for the autoloader.
*
* @since 3.1.4
*/
public static function getNamespaces()
{
return self::$namespaces;
}
/**
* Loads a class from specified directories.
*
* @param string $key The class name to look for (dot notation).
* @param string $base Search this directory for the class.
*
* @return boolean True on success.
*
* @since 1.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to
* register an autoloader for your files.
*/
public static function import($key, $base = null)
{
// Only import the library if not already attempted.
if (!isset(self::$imported[$key])) {
// Setup some variables.
$success = false;
$parts = explode('.', $key);
$class = array_pop($parts);
$base = (!empty($base)) ? $base : __DIR__;
$path = str_replace('.', DIRECTORY_SEPARATOR, $key);
// Handle special case for helper classes.
if ($class === 'helper') {
$class = ucfirst(array_pop($parts)) . ucfirst($class);
} else {
// Standard class.
$class = ucfirst($class);
}
// If we are importing a library from the Joomla namespace set the class to autoload.
if (strpos($path, 'joomla') === 0) {
// Since we are in the Joomla namespace prepend the classname with J.
$class = 'J' . $class;
// Only register the class for autoloading if the file exists.
if (is_file($base . '/' . $path . '.php')) {
self::$classes[strtolower($class)] = $base . '/' . $path . '.php';
$success = true;
}
} else {
/**
* If we are not importing a library from the Joomla namespace directly include the
* file since we cannot assert the file/folder naming conventions.
*/
// If the file exists attempt to include it.
if (is_file($base . '/' . $path . '.php')) {
$success = (bool) include_once $base . '/' . $path . '.php';
}
}
// Add the import key to the memory cache container.
self::$imported[$key] = $success;
}
return self::$imported[$key];
}
/**
* Load the file for a class.
*
* @param string $class The class to be loaded.
*
* @return boolean True on success
*
* @since 1.7.0
*/
public static function load($class)
{
// Sanitize class name.
$key = strtolower($class);
// If the class already exists do nothing.
if (class_exists($class, false)) {
return true;
}
// If the class is registered include the file.
if (isset(self::$classes[$key])) {
$found = (bool) include_once self::$classes[$key];
if ($found) {
self::loadAliasFor($class);
}
// If the class doesn't exists, we probably have a class alias available
if (!class_exists($class, false)) {
// Search the alias class, first none namespaced and then namespaced
$original = array_search($class, self::$classAliases) ?: array_search('\\' . $class, self::$classAliases);
// When we have an original and the class exists an alias should be created
if ($original && class_exists($original, false)) {
class_alias($original, $class);
}
}
return true;
}
return false;
}
/**
* Directly register a class to the autoload list.
*
* @param string $class The class name to register.
* @param string $path Full path to the file that holds the class to register.
* @param boolean $force True to overwrite the autoload path value for the class if it already exists.
*
* @return void
*
* @since 1.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to
* register an autoloader for your files.
*/
public static function register($class, $path, $force = true)
{
// When an alias exists, register it as well
if (array_key_exists(strtolower($class), self::$classAliases)) {
self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force);
}
// Sanitize class name.
$class = strtolower($class);
// Only attempt to register the class if the name and file exist.
if (!empty($class) && is_file($path)) {
// Register the class with the autoloader if not already registered or the force flag is set.
if ($force || empty(self::$classes[$class])) {
self::$classes[$class] = $path;
}
}
}
/**
* Register a class prefix with lookup path. This will allow developers to register library
* packages with different class prefixes to the system autoloader. More than one lookup path
* may be registered for the same class prefix, but if this method is called with the reset flag
* set to true then any registered lookups for the given prefix will be overwritten with the current
* lookup path. When loaded, prefix paths are searched in a "last in, first out" order.
*
* @param string $prefix The class prefix to register.
* @param string $path Absolute file path to the library root where classes with the given prefix can be found.
* @param boolean $reset True to reset the prefix with only the given lookup path.
* @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array.
*
* @return void
*
* @throws RuntimeException
*
* @since 3.0.0
*/
public static function registerPrefix($prefix, $path, $reset = false, $prepend = false)
{
// Verify the library path exists.
if (!is_dir($path)) {
$path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path);
throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500);
}
// If the prefix is not yet registered or we have an explicit reset flag then set set the path.
if ($reset || !isset(self::$prefixes[$prefix])) {
self::$prefixes[$prefix] = [$path];
} else {
// Otherwise we want to simply add the path to the prefix.
if ($prepend) {
array_unshift(self::$prefixes[$prefix], $path);
} else {
self::$prefixes[$prefix][] = $path;
}
}
}
/**
* Offers the ability for "just in time" usage of `class_alias()`.
* You cannot overwrite an existing alias.
*
* @param string $alias The alias name to register.
* @param string $original The original class to alias.
* @param string|boolean $version The version in which the alias will no longer be present.
*
* @return boolean True if registration was successful. False if the alias already exists.
*
* @since 3.2
*/
public static function registerAlias($alias, $original, $version = false)
{
// PHP is case insensitive so support all kind of alias combination
$lowercasedAlias = strtolower($alias);
if (!isset(self::$classAliases[$lowercasedAlias])) {
self::$classAliases[$lowercasedAlias] = $original;
$original = self::stripFirstBackslash($original);
if (!isset(self::$classAliasesInverse[$original])) {
self::$classAliasesInverse[$original] = [$lowercasedAlias];
} else {
self::$classAliasesInverse[$original][] = $lowercasedAlias;
}
// If given a version, log this alias as deprecated
if ($version) {
self::$deprecatedAliases[] = ['old' => $alias, 'new' => $original, 'version' => $version];
}
return true;
}
return false;
}
/**
* Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order.
*
* @param string $namespace A case sensitive Namespace to register.
* @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found.
* @param boolean $reset True to reset the namespace with only the given lookup path.
* @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array.
*
* @return void
*
* @throws RuntimeException
*
* @since 3.1.4
*/
public static function registerNamespace($namespace, $path, $reset = false, $prepend = false)
{
// Verify the library path exists.
if (!is_dir($path)) {
$path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path);
throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500);
}
// Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way.
$namespace = trim($namespace, '\\');
// If the namespace is not yet registered or we have an explicit reset flag then set the path.
if ($reset || !isset(self::$namespaces[$namespace])) {
self::$namespaces[$namespace] = [$path];
} else {
// Otherwise we want to simply add the path to the namespace.
if ($prepend) {
array_unshift(self::$namespaces[$namespace], $path);
} else {
self::$namespaces[$namespace][] = $path;
}
}
}
/**
* Method to setup the autoloaders for the Joomla Platform.
* Since the SPL autoloaders are called in a queue we will add our explicit
* class-registration based loader first, then fall back on the autoloader based on conventions.
* This will allow people to register a class in a specific location and override platform libraries
* as was previously possible.
*
* @param boolean $enablePsr True to enable autoloading based on PSR-0.
* @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core).
* @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core).
*
* @return void
*
* @since 3.1.4
*/
public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true)
{
if ($enableClasses) {
// Register the class map based autoloader.
spl_autoload_register(['JLoader', 'load']);
}
if ($enablePrefixes) {
// Register the prefix autoloader.
spl_autoload_register(['JLoader', '_autoload']);
}
if ($enablePsr) {
// Register the PSR based autoloader.
spl_autoload_register(['JLoader', 'loadByPsr']);
spl_autoload_register(['JLoader', 'loadByAlias']);
}
}
/**
* Method to autoload classes that are namespaced to the PSR-4 standard.
*
* @param string $class The fully qualified class name to autoload.
*
* @return boolean True on success, false otherwise.
*
* @since 3.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Use JLoader::loadByPsr instead
*/
public static function loadByPsr4($class)
{
return self::loadByPsr($class);
}
/**
* Method to autoload classes that are namespaced to the PSR-4 standard.
*
* @param string $class The fully qualified class name to autoload.
*
* @return boolean True on success, false otherwise.
*
* @since 4.0.0
*/
public static function loadByPsr($class)
{
$class = self::stripFirstBackslash($class);
// Find the location of the last NS separator.
$pos = strrpos($class, '\\');
// If one is found, we're dealing with a NS'd class.
if ($pos !== false) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
$className = substr($class, $pos + 1);
} else {
// If not, no need to parse path.
$classPath = null;
$className = $class;
}
$classPath .= $className . '.php';
// Loop through registered namespaces until we find a match.
foreach (self::$namespaces as $ns => $paths) {
if (strpos($class, "{$ns}\\") === 0) {
$nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR);
// Loop through paths registered to this namespace until we find a match.
foreach ($paths as $path) {
$classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1));
// We do not allow files outside the namespace root to be loaded
if (strpos($classFilePath, realpath($path)) !== 0) {
continue;
}
// We check for class_exists to handle case-sensitive file systems
if (is_file($classFilePath) && !class_exists($class, false)) {
$found = (bool) include_once $classFilePath;
if ($found) {
self::loadAliasFor($class);
}
return $found;
}
}
}
}
return false;
}
/**
* Method to autoload classes that have been aliased using the registerAlias method.
*
* @param string $class The fully qualified class name to autoload.
*
* @return void
*
* @since 3.2
*/
public static function loadByAlias($class)
{
$class = strtolower(self::stripFirstBackslash($class));
if (isset(self::$classAliases[$class])) {
// Force auto-load of the regular class
class_exists(self::$classAliases[$class], true);
// Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is
// auto-loaded above.
if (!class_exists($class, false) && !interface_exists($class, false)) {
class_alias(self::$classAliases[$class], $class);
}
}
}
/**
* Applies a class alias for an already loaded class, if a class alias was created for it.
*
* @param string $class We'll look for and register aliases for this (real) class name
*
* @return void
*
* @since 3.4
*/
public static function applyAliasFor($class)
{
$class = self::stripFirstBackslash($class);
if (isset(self::$classAliasesInverse[$class])) {
foreach (self::$classAliasesInverse[$class] as $alias) {
class_alias($class, $alias);
}
}
}
/**
* Autoload a class based on name.
*
* @param string $class The class to be loaded.
*
* @return boolean True if the class was loaded, false otherwise.
*
* @since 1.7.3
*/
public static function _autoload($class)
{
foreach (self::$prefixes as $prefix => $lookup) {
$chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0;
if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) {
return self::_load(substr($class, strlen($prefix)), $lookup);
}
}
return false;
}
/**
* Load a class based on name and lookup array.
*
* @param string $class The class to be loaded (without prefix).
* @param array $lookup The array of base paths to use for finding the class file.
*
* @return boolean True if the class was loaded, false otherwise.
*
* @since 3.0.0
*/
private static function _load($class, $lookup)
{
// Split the class name into parts separated by camelCase.
$parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class);
$partsCount = count($parts);
foreach ($lookup as $base) {
// Generate the path based on the class name parts.
$path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php');
// Load the file if it exists and is in the lookup path.
if (strpos($path, realpath($base)) === 0 && is_file($path)) {
$found = (bool) include_once $path;
if ($found) {
self::loadAliasFor($class);
}
return $found;
}
// Backwards compatibility patch
// If there is only one part we want to duplicate that part for generating the path.
if ($partsCount === 1) {
// Generate the path based on the class name parts.
$path = realpath($base . '/' . implode('/', array_map('strtolower', [$parts[0], $parts[0]])) . '.php');
// Load the file if it exists and is in the lookup path.
if (strpos($path, realpath($base)) === 0 && is_file($path)) {
$found = (bool) include_once $path;
if ($found) {
self::loadAliasFor($class);
}
return $found;
}
}
}
return false;
}
/**
* Loads the aliases for the given class.
*
* @param string $class The class.
*
* @return void
*
* @since 3.8.0
*/
private static function loadAliasFor($class)
{
if (!array_key_exists($class, self::$classAliasesInverse)) {
return;
}
foreach (self::$classAliasesInverse[$class] as $alias) {
// Force auto-load of the alias class
class_exists($alias, true);
}
}
/**
* Strips the first backslash from the given class if present.
*
* @param string $class The class to strip the first prefix from.
*
* @return string The striped class name.
*
* @since 3.8.0
*/
private static function stripFirstBackslash($class)
{
return $class && $class[0] === '\\' ? substr($class, 1) : $class;
}
}
// Check if jexit is defined first (our unit tests mock this)
if (!function_exists('jexit')) {
/**
* Global application exit.
*
* This function provides a single exit point for the platform.
*
* @param mixed $message Exit code or string. Defaults to zero.
*
* @return never
*
* @codeCoverageIgnore
* @since 1.7.0
*/
function jexit($message = 0): never
{
exit($message);
}
}
/**
* Intelligent file importer.
*
* @param string $path A dot syntax path.
* @param string $base Search this directory for the class.
*
* @return boolean True on success.
*
* @since 1.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to
* register an autoloader for your files.
*/
function jimport($path, $base = null)
{
return JLoader::import($path, $base);
}

325
libraries/namespacemap.php Normal file
View File

@ -0,0 +1,325 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
*/
use Joomla\CMS\Log\Log;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Class JNamespacePsr4Map
*
* @since 4.0.0
*/
class JNamespacePsr4Map
{
/**
* Path to the autoloader
*
* @var string
* @since 4.0.0
*/
protected $file = JPATH_CACHE . '/autoload_psr4.php';
/**
* @var array|null
* @since 4.0.0
*/
private $cachedMap = null;
/**
* Check if the file exists
*
* @return boolean
*
* @since 4.0.0
*/
public function exists()
{
return is_file($this->file);
}
/**
* Check if the namespace mapping file exists, if not create it
*
* @return void
*
* @since 4.0.0
*/
public function ensureMapFileExists()
{
if (!$this->exists()) {
$this->create();
}
}
/**
* Create the namespace file
*
* @return boolean
*
* @since 4.0.0
*/
public function create()
{
$extensions = array_merge(
$this->getNamespaces('component'),
$this->getNamespaces('module'),
$this->getNamespaces('template'),
$this->getNamespaces('plugin'),
$this->getNamespaces('library')
);
ksort($extensions);
$this->writeNamespaceFile($extensions);
return true;
}
/**
* Load the PSR4 file
*
* @return boolean
*
* @since 4.0.0
*/
public function load()
{
if (!$this->exists()) {
$this->create();
}
$map = $this->cachedMap ?: require $this->file;
$loader = include JPATH_LIBRARIES . '/vendor/autoload.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
return true;
}
/**
* Write the Namespace mapping file
*
* @param array $elements Array of elements
*
* @return void
*
* @since 4.0.0
*/
protected function writeNamespaceFile($elements)
{
$content = [];
$content[] = "<?php";
$content[] = 'defined(\'_JEXEC\') or die;';
$content[] = 'return [';
foreach ($elements as $namespace => $path) {
$content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],';
}
$content[] = '];';
/**
* Backup the current error_reporting level and set a new level
*
* We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file
* and this will output to the response BEFORE the session has started, causing the session start to fail
* and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which
* we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later.
*/
$error_reporting = error_reporting(0);
try {
File::write($this->file, implode("\n", $content));
} catch (Exception $e) {
Log::add('Could not save ' . $this->file, Log::WARNING);
$map = [];
$constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS', 'JPATH_LIBRARIES'];
foreach ($elements as $namespace => $path) {
foreach ($constants as $constant) {
$path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path);
}
$namespace = str_replace('\\\\', '\\', $namespace);
$map[$namespace] = [ $path ];
}
$this->cachedMap = $map;
}
// Restore previous value of error_reporting
error_reporting($error_reporting);
}
/**
* Get an array of namespaces with their respective path for the given extension type.
*
* @param string $type The extension type
*
* @return array
*
* @since 4.0.0
*/
private function getNamespaces(string $type): array
{
$extensions = [];
foreach ($this->getExtensions($type) as $extensionPath => $file) {
// Load the manifest file
$xml = simplexml_load_file($file, 'SimpleXMLElement', LIBXML_NOERROR);
// When invalid, ignore
if (!$xml) {
continue;
}
// The namespace node
$namespaceNode = $xml->namespace;
// The namespace string
$namespace = (string) $namespaceNode;
// Ignore when the string is empty
if (!$namespace) {
continue;
}
// Normalize the namespace string
$namespace = str_replace('\\', '\\\\', $namespace) . '\\\\';
$namespacePath = rtrim($extensionPath . '/' . $namespaceNode->attributes()->path, '/');
if ($type === 'plugin' || $type === 'library') {
$baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \'';
$path = substr($namespacePath, strlen($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES));
// Set the namespace
$extensions[$namespace] = $baseDir . $path . '\'';
continue;
}
// Check if we need to use administrator path
$isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0;
$path = substr($namespacePath, strlen($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE));
// Add the site path when a component
if ($type === 'component') {
if (is_dir(JPATH_SITE . $path)) {
$extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\'';
}
if (is_dir(JPATH_API . $path)) {
$extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\'';
}
}
// Add the application specific segment when a component or module
$baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \'';
$realPath = ($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE) . $path;
$namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\';
// Validate if the directory exists
if (!is_dir($realPath)) {
continue;
}
// Set the namespace
$extensions[$namespace] = $baseDir . $path . '\'';
}
// Return the namespaces
return $extensions;
}
/**
* Returns an array of extensions with their respective paths as keys and manifest paths as values.
*
* @param string $type The extension type
*
* @return array
*
* @since 4.3.0
*/
private function getExtensions(string $type): array
{
$manifests = [];
if ($type === 'library') {
try {
// Scan library manifest directories for XML files
foreach (Folder::files(JPATH_MANIFESTS . '/libraries', '\.xml$', true, true) as $file) {
// Match manifest to extension directory
$manifests[JPATH_LIBRARIES . '/' . File::stripExt(substr($file, strlen(JPATH_MANIFESTS . '/libraries') + 1))] = $file;
}
} catch (UnexpectedValueException $e) {
return [];
}
return $manifests;
}
if ($type === 'component') {
$directories = [JPATH_ADMINISTRATOR . '/components'];
} elseif ($type === 'module') {
$directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules'];
} elseif ($type === 'template') {
$directories = [JPATH_SITE . '/templates', JPATH_ADMINISTRATOR . '/templates'];
} else {
try {
$directories = Folder::folders(JPATH_PLUGINS, '.', false, true);
} catch (UnexpectedValueException $e) {
$directories = [];
}
}
foreach ($directories as $directory) {
try {
$extensionDirectories = Folder::folders($directory, '.', false, false);
} catch (UnexpectedValueException $e) {
continue;
}
foreach ($extensionDirectories as $extension) {
// Compile the extension path
$extensionPath = $directory . '/' . $extension;
if ($type === 'component') {
// Strip the com_ from the extension name for components
$file = $extensionPath . '/' . substr($extension, 4) . '.xml';
if (!is_file($file)) {
$file = $extensionPath . '/' . $extension . '.xml';
}
} elseif ($type === 'template') {
// Template manifestfiles have a fix filename
$file = $extensionPath . '/templateDetails.xml';
} else {
$file = $extensionPath . '/' . $extension . '.xml';
}
if (is_file($file)) {
$manifests[$extensionPath] = $file;
}
}
}
return $manifests;
}
}

View File

@ -0,0 +1,676 @@
<?php
/*
* PHP Encryption Library
* Copyright (c) 2014, Taylor Hornby
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Web: https://defuse.ca/secure-php-encryption.htm
* GitHub: https://github.com/defuse/php-encryption
*
* WARNING: This encryption library is not a silver bullet. It only provides
* symmetric encryption given a uniformly random key. This means you MUST NOT
* use an ASCII string like a password as the key parameter, it MUST be
* a uniformly random key generated by CreateNewRandomKey(). If you want to
* encrypt something with a password, apply a password key derivation function
* like PBKDF2 or scrypt with a random salt to generate a key.
*
* WARNING: Error handling is very important, especially for crypto code!
*
* How to use this code:
*
* Generating a Key
* ----------------
* try {
* $key = Crypto::CreateNewRandomKey();
* // WARNING: Do NOT encode $key with bin2hex() or base64_encode(),
* // they may leak the key to the attacker through side channels.
* } catch (CryptoTestFailedException $ex) {
* die('Cannot safely create a key');
* } catch (CannotPerformOperationException $ex) {
* die('Cannot safely create a key');
* }
*
* Encrypting a Message
* --------------------
* $message = "ATTACK AT DAWN";
* try {
* $ciphertext = Crypto::Encrypt($message, $key);
* } catch (CryptoTestFailedException $ex) {
* die('Cannot safely perform encryption');
* } catch (CannotPerformOperationException $ex) {
* die('Cannot safely perform decryption');
* }
*
* Decrypting a Message
* --------------------
* try {
* $decrypted = Crypto::Decrypt($ciphertext, $key);
* } catch (InvalidCiphertextException $ex) { // VERY IMPORTANT
* // Either:
* // 1. The ciphertext was modified by the attacker,
* // 2. The key is wrong, or
* // 3. $ciphertext is not a valid ciphertext or was corrupted.
* // Assume the worst.
* die('DANGER! DANGER! The ciphertext has been tampered with!');
* } catch (CryptoTestFailedException $ex) {
* die('Cannot safely perform encryption');
* } catch (CannotPerformOperationException $ex) {
* die('Cannot safely perform decryption');
* }
*/
/*
* Raised by Decrypt() when one of the following conditions are met:
* - The key is wrong.
* - The ciphertext is invalid or not in the correct format.
* - The attacker modified the ciphertext.
*/
class InvalidCiphertextException extends Exception {}
/* If you see these, it means it is NOT SAFE to do encryption on your system. */
class CannotPerformOperationException extends Exception {}
class CryptoTestFailedException extends Exception {}
class Crypto
{
// Ciphertext format: [____HMAC____][____IV____][____CIPHERTEXT____].
/* Do not change these constants! */
const CIPHER = MCRYPT_RIJNDAEL_128;
const KEY_BYTE_SIZE = 16;
const CIPHER_MODE = 'cbc';
const HASH_FUNCTION = 'sha256';
const MAC_BYTE_SIZE = 32;
const ENCRYPTION_INFO = 'DefusePHP|KeyForEncryption';
const AUTHENTICATION_INFO = 'DefusePHP|KeyForAuthentication';
/*
* Use this to generate a random encryption key.
*/
public static function CreateNewRandomKey()
{
Crypto::RuntimeTest();
return self::SecureRandom(self::KEY_BYTE_SIZE);
}
/*
* Encrypts a message.
* $plaintext is the message to encrypt.
* $key is the encryption key, a value generated by CreateNewRandomKey().
* You MUST catch exceptions thrown by this function. See docs above.
*/
public static function Encrypt($plaintext, $key)
{
Crypto::RuntimeTest();
if (self::our_strlen($key) !== self::KEY_BYTE_SIZE)
{
throw new CannotPerformOperationException("Bad key.");
}
// Generate a sub-key for encryption.
$keysize = self::KEY_BYTE_SIZE;
$ekey = self::HKDF(self::HASH_FUNCTION, $key, $keysize, self::ENCRYPTION_INFO);
// Generate a random initialization vector.
self::EnsureFunctionExists("mcrypt_get_iv_size");
$ivsize = mcrypt_get_iv_size(self::CIPHER, self::CIPHER_MODE);
if ($ivsize === FALSE || $ivsize <= 0) {
throw new CannotPerformOperationException();
}
$iv = self::SecureRandom($ivsize);
$ciphertext = $iv . self::PlainEncrypt($plaintext, $ekey, $iv);
// Generate a sub-key for authentication and apply the HMAC.
$akey = self::HKDF(self::HASH_FUNCTION, $key, self::KEY_BYTE_SIZE, self::AUTHENTICATION_INFO);
$auth = hash_hmac(self::HASH_FUNCTION, $ciphertext, $akey, true);
$ciphertext = $auth . $ciphertext;
return $ciphertext;
}
/*
* Decrypts a ciphertext.
* $ciphertext is the ciphertext to decrypt.
* $key is the key that the ciphertext was encrypted with.
* You MUST catch exceptions thrown by this function. See docs above.
*/
public static function Decrypt($ciphertext, $key)
{
Crypto::RuntimeTest();
// Extract the HMAC from the front of the ciphertext.
if (self::our_strlen($ciphertext) <= self::MAC_BYTE_SIZE) {
throw new InvalidCiphertextException();
}
$hmac = self::our_substr($ciphertext, 0, self::MAC_BYTE_SIZE);
if ($hmac === FALSE) {
throw new CannotPerformOperationException();
}
$ciphertext = self::our_substr($ciphertext, self::MAC_BYTE_SIZE);
if ($ciphertext === FALSE) {
throw new CannotPerformOperationException();
}
// Regenerate the same authentication sub-key.
$akey = self::HKDF(self::HASH_FUNCTION, $key, self::KEY_BYTE_SIZE, self::AUTHENTICATION_INFO);
if (self::VerifyHMAC($hmac, $ciphertext, $akey))
{
// Regenerate the same encryption sub-key.
$keysize = self::KEY_BYTE_SIZE;
$ekey = self::HKDF(self::HASH_FUNCTION, $key, $keysize, self::ENCRYPTION_INFO);
// Extract the initialization vector from the ciphertext.
self::EnsureFunctionExists("mcrypt_get_iv_size");
$ivsize = mcrypt_get_iv_size(self::CIPHER, self::CIPHER_MODE);
if ($ivsize === FALSE || $ivsize <= 0) {
throw new CannotPerformOperationException();
}
if (self::our_strlen($ciphertext) <= $ivsize) {
throw new InvalidCiphertextException();
}
$iv = self::our_substr($ciphertext, 0, $ivsize);
if ($iv === FALSE) {
throw new CannotPerformOperationException();
}
$ciphertext = self::our_substr($ciphertext, $ivsize);
if ($ciphertext === FALSE) {
throw new CannotPerformOperationException();
}
$plaintext = self::PlainDecrypt($ciphertext, $ekey, $iv);
return $plaintext;
}
else
{
/*
* We throw an exception instead of returning FALSE because we want
* a script that doesn't handle this condition to CRASH, instead
* of thinking the ciphertext decrypted to the value FALSE.
*/
throw new InvalidCiphertextException();
}
}
/*
* Runs tests.
* Raises CannotPerformOperationException or CryptoTestFailedException if
* one of the tests fail. If any tests fails, your system is not capable of
* performing encryption, so make sure you fail safe in that case.
*/
public static function RuntimeTest()
{
// 0: Tests haven't been run yet.
// 1: Tests have passed.
// 2: Tests are running right now.
// 3: Tests have failed.
static $test_state = 0;
if ($test_state === 1 || $test_state === 2) {
return;
}
try {
$test_state = 2;
self::AESTestVector();
self::HMACTestVector();
self::HKDFTestVector();
self::TestEncryptDecrypt();
if (self::our_strlen(Crypto::CreateNewRandomKey()) != self::KEY_BYTE_SIZE) {
throw new CryptoTestFailedException();
}
if (self::ENCRYPTION_INFO == self::AUTHENTICATION_INFO) {
throw new CryptoTestFailedException();
}
} catch (CryptoTestFailedException $ex) {
// Do this, otherwise it will stay in the "tests are running" state.
$test_state = 3;
throw $ex;
}
// Change this to '0' make the tests always re-run (for benchmarking).
$test_state = 1;
}
/*
* Never call this method directly!
*/
private static function PlainEncrypt($plaintext, $key, $iv)
{
self::EnsureFunctionExists("mcrypt_module_open");
$crypt = mcrypt_module_open(self::CIPHER, "", self::CIPHER_MODE, "");
if ($crypt === FALSE) {
throw new CannotPerformOperationException();
}
// Pad the plaintext to a multiple of the block size.
self::EnsureFunctionExists("mcrypt_enc_get_block_size");
$block = mcrypt_enc_get_block_size($crypt);
$pad = $block - (self::our_strlen($plaintext) % $block);
$plaintext .= str_repeat(chr($pad), $pad);
self::EnsureFunctionExists("mcrypt_generic_init");
$ret = mcrypt_generic_init($crypt, $key, $iv);
if ($ret !== 0) {
throw new CannotPerformOperationException();
}
self::EnsureFunctionExists("mcrypt_generic");
$ciphertext = mcrypt_generic($crypt, $plaintext);
self::EnsureFunctionExists("mcrypt_generic_deinit");
$ret = mcrypt_generic_deinit($crypt);
if ($ret !== TRUE) {
throw new CannotPerformOperationException();
}
self::EnsureFunctionExists("mcrypt_module_close");
$ret = mcrypt_module_close($crypt);
if ($ret !== TRUE) {
throw new CannotPerformOperationException();
}
return $ciphertext;
}
/*
* Never call this method directly!
*/
private static function PlainDecrypt($ciphertext, $key, $iv)
{
self::EnsureFunctionExists("mcrypt_module_open");
$crypt = mcrypt_module_open(self::CIPHER, "", self::CIPHER_MODE, "");
if ($crypt === FALSE) {
throw new CannotPerformOperationException();
}
self::EnsureFunctionExists("mcrypt_enc_get_block_size");
$block = mcrypt_enc_get_block_size($crypt);
self::EnsureFunctionExists("mcrypt_generic_init");
$ret = mcrypt_generic_init($crypt, $key, $iv);
if ($ret !== 0) {
throw new CannotPerformOperationException();
}
self::EnsureFunctionExists("mdecrypt_generic");
$plaintext = mdecrypt_generic($crypt, $ciphertext);
self::EnsureFunctionExists("mcrypt_generic_deinit");
$ret = mcrypt_generic_deinit($crypt);
if ($ret !== TRUE) {
throw new CannotPerformOperationException();
}
self::EnsureFunctionExists("mcrypt_module_close");
$ret = mcrypt_module_close($crypt);
if ($ret !== TRUE) {
throw new CannotPerformOperationException();
}
// Remove the padding.
$pad = ord($plaintext[self::our_strlen($plaintext) - 1]);
if ($pad <= 0 || $pad > $block) {
throw new CannotPerformOperationException();
}
$plaintext = self::our_substr($plaintext, 0, self::our_strlen($plaintext) - $pad);
if ($plaintext === FALSE) {
throw new CannotPerformOperationException();
}
return $plaintext;
}
/*
* Returns a random binary string of length $octets bytes.
*/
private static function SecureRandom($octets)
{
self::EnsureFunctionExists("mcrypt_create_iv");
$random = mcrypt_create_iv($octets, MCRYPT_DEV_URANDOM);
if ($random === FALSE) {
throw new CannotPerformOperationException();
} else {
return $random;
}
}
/*
* Use HKDF to derive multiple keys from one.
* http://tools.ietf.org/html/rfc5869
*/
private static function HKDF($hash, $ikm, $length, $info = '', $salt = NULL)
{
// Find the correct digest length as quickly as we can.
$digest_length = self::MAC_BYTE_SIZE;
if ($hash != self::HASH_FUNCTION) {
$digest_length = self::our_strlen(hash_hmac($hash, '', '', true));
}
// Sanity-check the desired output length.
if (empty($length) || !is_int($length) ||
$length < 0 || $length > 255 * $digest_length) {
throw new CannotPerformOperationException();
}
// "if [salt] not provided, is set to a string of HashLen zeroes."
if (is_null($salt)) {
$salt = str_repeat("\x00", $digest_length);
}
// HKDF-Extract:
// PRK = HMAC-Hash(salt, IKM)
// The salt is the HMAC key.
$prk = hash_hmac($hash, $ikm, $salt, true);
// HKDF-Expand:
// This check is useless, but it serves as a reminder to the spec.
if (self::our_strlen($prk) < $digest_length) {
throw new CannotPerformOperationException();
}
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; self::our_strlen($t) < $length; $block_index++) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
$last_block = hash_hmac(
$hash,
$last_block . $info . chr($block_index),
$prk,
true
);
// T = T(1) | T(2) | T(3) | ... | T(N)
$t .= $last_block;
}
// ORM = first L octets of T
$orm = self::our_substr($t, 0, $length);
if ($orm === FALSE) {
throw new CannotPerformOperationException();
}
return $orm;
}
private static function VerifyHMAC($correct_hmac, $message, $key)
{
$message_hmac = hash_hmac(self::HASH_FUNCTION, $message, $key, true);
// We can't just compare the strings with '==', since it would make
// timing attacks possible. We could use the XOR-OR constant-time
// comparison algorithm, but I'm not sure if that's good enough way up
// here in an interpreted language. So we use the method of HMACing the
// strings we want to compare with a random key, then comparing those.
// NOTE: This leaks information when the strings are not the same
// length, but they should always be the same length here. Enforce it:
if (self::our_strlen($correct_hmac) !== self::our_strlen($message_hmac)) {
throw new CannotPerformOperationException();
}
$blind = self::CreateNewRandomKey();
$message_compare = hash_hmac(self::HASH_FUNCTION, $message_hmac, $blind);
$correct_compare = hash_hmac(self::HASH_FUNCTION, $correct_hmac, $blind);
return $correct_compare === $message_compare;
}
private static function TestEncryptDecrypt()
{
$key = Crypto::CreateNewRandomKey();
$data = "EnCrYpT EvErYThInG\x00\x00";
// Make sure encrypting then decrypting doesn't change the message.
$ciphertext = Crypto::Encrypt($data, $key);
try {
$decrypted = Crypto::Decrypt($ciphertext, $key);
} catch (InvalidCiphertextException $ex) {
// It's important to catch this and change it into a
// CryptoTestFailedException, otherwise a test failure could trick
// the user into thinking it's just an invalid ciphertext!
throw new CryptoTestFailedException();
}
if($decrypted !== $data)
{
throw new CryptoTestFailedException();
}
// Modifying the ciphertext: Appending a string.
try {
Crypto::Decrypt($ciphertext . "a", $key);
throw new CryptoTestFailedException();
} catch (InvalidCiphertextException $e) { /* expected */ }
// Modifying the ciphertext: Changing an IV byte.
try {
$ciphertext[0] = chr((ord($ciphertext[0]) + 1) % 256);
Crypto::Decrypt($ciphertext, $key);
throw new CryptoTestFailedException();
} catch (InvalidCiphertextException $e) { /* expected */ }
// Decrypting with the wrong key.
$key = Crypto::CreateNewRandomKey();
$data = "abcdef";
$ciphertext = Crypto::Encrypt($data, $key);
$wrong_key = Crypto::CreateNewRandomKey();
try {
Crypto::Decrypt($ciphertext, $wrong_key);
throw new CryptoTestFailedException();
} catch (InvalidCiphertextException $e) { /* expected */ }
// Ciphertext too small (shorter than HMAC).
$key = Crypto::CreateNewRandomKey();
$ciphertext = str_repeat("A", self::MAC_BYTE_SIZE - 1);
try {
Crypto::Decrypt($ciphertext, $key);
throw new CryptoTestFailedException();
} catch (InvalidCiphertextException $e) { /* expected */ }
}
private static function HKDFTestVector()
{
// HKDF test vectors from RFC 5869
// Test Case 1
$ikm = str_repeat("\x0b", 22);
$salt = self::hexToBytes("000102030405060708090a0b0c");
$info = self::hexToBytes("f0f1f2f3f4f5f6f7f8f9");
$length = 42;
$okm = self::hexToBytes(
"3cb25f25faacd57a90434f64d0362f2a" .
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf" .
"34007208d5b887185865"
);
$computed_okm = self::HKDF("sha256", $ikm, $length, $info, $salt);
if ($computed_okm !== $okm) {
throw new CryptoTestFailedException();
}
// Test Case 7
$ikm = str_repeat("\x0c", 22);
$length = 42;
$okm = self::hexToBytes(
"2c91117204d745f3500d636a62f64f0a" .
"b3bae548aa53d423b0d1f27ebba6f5e5" .
"673a081d70cce7acfc48"
);
$computed_okm = self::HKDF("sha1", $ikm, $length);
if ($computed_okm !== $okm) {
throw new CryptoTestFailedException();
}
}
private static function HMACTestVector()
{
// HMAC test vector From RFC 4231 (Test Case 1)
$key = str_repeat("\x0b", 20);
$data = "Hi There";
$correct = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7";
if (hash_hmac(self::HASH_FUNCTION, $data, $key) != $correct) {
throw new CryptoTestFailedException();
}
}
private static function AESTestVector()
{
// AES CBC mode test vector from NIST SP 800-38A
$key = self::hexToBytes("2b7e151628aed2a6abf7158809cf4f3c");
$iv = self::hexToBytes("000102030405060708090a0b0c0d0e0f");
$plaintext = self::hexToBytes(
"6bc1bee22e409f96e93d7e117393172a" .
"ae2d8a571e03ac9c9eb76fac45af8e51" .
"30c81c46a35ce411e5fbc1191a0a52ef" .
"f69f2445df4f9b17ad2b417be66c3710"
);
$ciphertext = self::hexToBytes(
"7649abac8119b246cee98e9b12e9197d" .
"5086cb9b507219ee95db113a917678b2" .
"73bed6b8e3c1743b7116e69e22229516" .
"3ff1caa1681fac09120eca307586e1a7" .
/* Block due to padding. Not from NIST test vector.
Padding Block: 10101010101010101010101010101010
Ciphertext: 3ff1caa1681fac09120eca307586e1a7
(+) 2fe1dab1780fbc19021eda206596f1b7
AES 8cb82807230e1321d3fae00d18cc2012
*/
"8cb82807230e1321d3fae00d18cc2012"
);
$computed_ciphertext = self::PlainEncrypt($plaintext, $key, $iv);
if ($computed_ciphertext !== $ciphertext) {
throw new CryptoTestFailedException();
}
$computed_plaintext = self::PlainDecrypt($ciphertext, $key, $iv);
if ($computed_plaintext !== $plaintext) {
throw new CryptoTestFailedException();
}
}
/* WARNING: Do not call this function on secrets. It creates side channels. */
private static function hexToBytes($hex_string)
{
return pack("H*", $hex_string);
}
private static function EnsureFunctionExists($name)
{
if (!function_exists($name)) {
throw new CannotPerformOperationException();
}
}
/*
* We need these strlen() and substr() functions because when
* 'mbstring.func_overload' is set in php.ini, the standard strlen() and
* substr() are replaced by mb_strlen() and mb_substr().
*/
private static function our_strlen($str)
{
if (function_exists('mb_strlen')) {
$length = mb_strlen($str, '8bit');
if ($length === FALSE) {
throw new CannotPerformOperationException();
}
return $length;
} else {
return strlen($str);
}
}
private static function our_substr($str, $start, $length = NULL)
{
if (function_exists('mb_substr'))
{
// mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP
// 5.3, so we have to find the length ourselves.
if (!isset($length)) {
if ($start >= 0) {
$length = self::our_strlen($str) - $start;
} else {
$length = -$start;
}
}
return mb_substr($str, $start, $length, '8bit');
}
// Unlike mb_substr(), substr() doesn't accept NULL for length
if (isset($length)) {
return substr($str, $start, $length);
} else {
return substr($str, $start);
}
}
}
/*
* We want to catch all uncaught exceptions that come from the Crypto class,
* since by default, PHP will leak the key in the stack trace from an uncaught
* exception. This is a really ugly hack, but I think it's justified.
*
* Everything up to handler() getting called should be reliable, so this should
* reliably suppress the stack traces. The rest is just a bonus so that we don't
* make it impossible to debug other exceptions.
*
* This bit of code was adapted from: http://stackoverflow.com/a/7939492
*/
class CryptoExceptionHandler
{
private $rethrow = NULL;
public function __construct()
{
set_exception_handler([$this, "handler"]);
}
public function handler($ex)
{
if (
$ex instanceof InvalidCiphertextException ||
$ex instanceof CannotPerformOperationException ||
$ex instanceof CryptoTestFailedException
) {
echo "FATAL ERROR: Uncaught crypto exception. Suppresssing output.\n";
} else {
/* Re-throw the exception in the destructor. */
$this->rethrow = $ex;
}
}
public function __destruct() {
if ($this->rethrow) {
throw $this->rethrow;
}
}
}
$crypto_exception_handler_object_dont_touch_me = new CryptoExceptionHandler();

View File

@ -0,0 +1,227 @@
<?php
#
# Portable PHP password hashing framework.
#
# Version 0.5 / genuine.
#
# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
# the public domain. Revised in subsequent years, still public domain.
#
# There's absolutely no warranty.
#
# The homepage URL for this framework is:
#
# http://www.openwall.com/phpass/
#
# Please be sure to update the Version line if you edit this file in any way.
# It is suggested that you leave the main version number intact, but indicate
# your project name (after the slash) and add your own revision information.
#
# Please do not change the "private" password hashing method implemented in
# here, thereby making your hashes incompatible. However, if you must, please
# change the hash type identifier (the "$P$") to something different.
#
# Obviously, since this code is in the public domain, the above are not
# requirements (there can be none), but merely suggestions.
#
class PasswordHash {
var $itoa64;
var $iteration_count_log2;
var $portable_hashes;
var $random_state;
function __construct($iteration_count_log2, $portable_hashes)
{
$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
$iteration_count_log2 = 8;
$this->iteration_count_log2 = $iteration_count_log2;
$this->portable_hashes = $portable_hashes;
$this->random_state = microtime();
if (function_exists('getmypid'))
$this->random_state .= getmypid();
}
function PasswordHash($iteration_count_log2, $portable_hashes)
{
self::__construct($iteration_count_log2, $portable_hashes);
}
function get_random_bytes($count)
{
$output = '';
if (@is_readable('/dev/urandom') &&
($fh = @fopen('/dev/urandom', 'rb'))) {
$output = fread($fh, $count);
fclose($fh);
}
if (strlen($output) < $count) {
$output = '';
for ($i = 0; $i < $count; $i += 16) {
$this->random_state =
md5(microtime() . $this->random_state);
$output .= md5($this->random_state, TRUE);
}
$output = substr($output, 0, $count);
}
return $output;
}
function encode64($input, $count)
{
$output = '';
$i = 0;
do {
$value = ord($input[$i++]);
$output .= $this->itoa64[$value & 0x3f];
if ($i < $count)
$value |= ord($input[$i]) << 8;
$output .= $this->itoa64[($value >> 6) & 0x3f];
if ($i++ >= $count)
break;
if ($i < $count)
$value |= ord($input[$i]) << 16;
$output .= $this->itoa64[($value >> 12) & 0x3f];
if ($i++ >= $count)
break;
$output .= $this->itoa64[($value >> 18) & 0x3f];
} while ($i < $count);
return $output;
}
function gensalt_private($input)
{
$output = '$P$';
$output .= $this->itoa64[min($this->iteration_count_log2 +
((PHP_VERSION >= '5') ? 5 : 3), 30)];
$output .= $this->encode64($input, 6);
return $output;
}
function crypt_private($password, $setting)
{
$output = '*0';
if (substr($setting, 0, 2) === $output)
$output = '*1';
$id = substr($setting, 0, 3);
# We use "$P$", phpBB3 uses "$H$" for the same thing
if ($id !== '$P$' && $id !== '$H$')
return $output;
$count_log2 = strpos($this->itoa64, $setting[3]);
if ($count_log2 < 7 || $count_log2 > 30)
return $output;
$count = 1 << $count_log2;
$salt = substr($setting, 4, 8);
if (strlen($salt) !== 8)
return $output;
# We were kind of forced to use MD5 here since it's the only
# cryptographic primitive that was available in all versions
# of PHP in use. To implement our own low-level crypto in PHP
# would have resulted in much worse performance and
# consequently in lower iteration counts and hashes that are
# quicker to crack (by non-PHP code).
$hash = md5($salt . $password, TRUE);
do {
$hash = md5($hash . $password, TRUE);
} while (--$count);
$output = substr($setting, 0, 12);
$output .= $this->encode64($hash, 16);
return $output;
}
function gensalt_blowfish($input)
{
# This one needs to use a different order of characters and a
# different encoding scheme from the one in encode64() above.
# We care because the last character in our encoded string will
# only represent 2 bits. While two known implementations of
# bcrypt will happily accept and correct a salt string which
# has the 4 unused bits set to non-zero, we do not want to take
# chances and we also do not want to waste an additional byte
# of entropy.
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$output = '$2a$';
$output .= chr(ord('0') + $this->iteration_count_log2 / 10);
$output .= chr(ord('0') + $this->iteration_count_log2 % 10);
$output .= '$';
$i = 0;
do {
$c1 = ord($input[$i++]);
$output .= $itoa64[$c1 >> 2];
$c1 = ($c1 & 0x03) << 4;
if ($i >= 16) {
$output .= $itoa64[$c1];
break;
}
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 4;
$output .= $itoa64[$c1];
$c1 = ($c2 & 0x0f) << 2;
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 6;
$output .= $itoa64[$c1];
$output .= $itoa64[$c2 & 0x3f];
} while (1);
return $output;
}
function HashPassword($password)
{
$random = '';
if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) {
$random = $this->get_random_bytes(16);
$hash =
crypt($password, $this->gensalt_blowfish($random));
if (strlen($hash) === 60)
return $hash;
}
if (strlen($random) < 6)
$random = $this->get_random_bytes(6);
$hash =
$this->crypt_private($password,
$this->gensalt_private($random));
if (strlen($hash) === 34)
return $hash;
# Returning '*' on error is safe here, but would _not_ be safe
# in a crypt(3)-like function used _both_ for generating new
# hashes and for validating passwords against existing hashes.
return '*';
}
function CheckPassword($password, $stored_hash)
{
$hash = $this->crypt_private($password, $stored_hash);
if ($hash[0] === '*')
$hash = crypt($password, $stored_hash);
# This is not constant-time. In order to keep the code simple,
# for timing safety we currently rely on the salts being
# unpredictable, which they are at least in the non-fallback
# cases (that is, when we use /dev/urandom and bcrypt).
return $hash === $stored_hash;
}
}
?>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Access\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining an authentication failed event
*
* @since 4.0.0
*/
class AuthenticationFailed extends \RuntimeException
{
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Access\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining a not allowed access
*
* @since 3.6.3
*/
class NotAllowed extends \RuntimeException
{
}

View File

@ -0,0 +1,164 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Access;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Rule class.
*
* @since 2.5.0
*/
class Rule
{
/**
* A named array
*
* @var array
* @since 1.7.0
*/
protected $data = [];
/**
* Constructor.
*
* The input array must be in the form: array(-42 => true, 3 => true, 4 => false)
* or an equivalent JSON encoded string.
*
* @param mixed $identities A JSON format string (probably from the database) or a named array.
*
* @since 1.7.0
*/
public function __construct($identities)
{
// Convert string input to an array.
if (\is_string($identities)) {
$identities = json_decode($identities, true);
}
$this->mergeIdentities($identities);
}
/**
* Get the data for the action.
*
* @return array A named array
*
* @since 1.7.0
*/
public function getData()
{
return $this->data;
}
/**
* Merges the identities
*
* @param mixed $identities An integer or array of integers representing the identities to check.
*
* @return void
*
* @since 1.7.0
*/
public function mergeIdentities($identities)
{
if ($identities instanceof Rule) {
$identities = $identities->getData();
}
if (\is_array($identities)) {
foreach ($identities as $identity => $allow) {
$this->mergeIdentity($identity, $allow);
}
}
}
/**
* Merges the values for an identity.
*
* @param integer $identity The identity.
* @param boolean $allow The value for the identity (true == allow, false == deny).
*
* @return void
*
* @since 1.7.0
*/
public function mergeIdentity($identity, $allow)
{
$identity = (int) $identity;
$allow = (int) ((bool) $allow);
// Check that the identity exists.
if (isset($this->data[$identity])) {
// Explicit deny always wins a merge.
if ($this->data[$identity] !== 0) {
$this->data[$identity] = $allow;
}
} else {
$this->data[$identity] = $allow;
}
}
/**
* Checks that this action can be performed by an identity.
*
* The identity is an integer where +ve represents a user group,
* and -ve represents a user.
*
* @param mixed $identities An integer or array of integers representing the identities to check.
*
* @return mixed True if allowed, false for an explicit deny, null for an implicit deny.
*
* @since 1.7.0
*/
public function allow($identities)
{
// Implicit deny by default.
$result = null;
// Check that the inputs are valid.
if (!empty($identities)) {
if (!\is_array($identities)) {
$identities = [$identities];
}
foreach ($identities as $identity) {
// Technically the identity just needs to be unique.
$identity = (int) $identity;
// Check if the identity is known.
if (isset($this->data[$identity])) {
$result = (bool) $this->data[$identity];
// An explicit deny wins.
if ($result === false) {
break;
}
}
}
}
return $result;
}
/**
* Convert this object into a JSON encoded string.
*
* @return string JSON encoded string
*
* @since 1.7.0
*/
public function __toString()
{
return json_encode($this->data);
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Access;
use Joomla\CMS\Object\CMSObject;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Access rules class.
*
* @since 2.5.0
*/
class Rules
{
/**
* A named array.
*
* @var array
* @since 1.7.0
*/
protected $data = [];
/**
* Constructor.
*
* The input array must be in the form: array('action' => array(-42 => true, 3 => true, 4 => false))
* or an equivalent JSON encoded string, or an object where properties are arrays.
*
* @param mixed $input A JSON format string (probably from the database) or a nested array.
*
* @since 1.7.0
*/
public function __construct($input = '')
{
// Convert in input to an array.
if (\is_string($input)) {
$input = json_decode($input, true);
} elseif (\is_object($input)) {
$input = (array) $input;
}
if (\is_array($input)) {
// Top level keys represent the actions.
foreach ($input as $action => $identities) {
$this->mergeAction($action, $identities);
}
}
}
/**
* Get the data for the action.
*
* @return array A named array of Rule objects.
*
* @since 1.7.0
*/
public function getData()
{
return $this->data;
}
/**
* Method to merge a collection of Rules.
*
* @param mixed $input Rule or array of Rules
*
* @return void
*
* @since 1.7.0
*/
public function mergeCollection($input)
{
// Check if the input is an array.
if (\is_array($input)) {
foreach ($input as $actions) {
$this->merge($actions);
}
}
}
/**
* Method to merge actions with this object.
*
* @param mixed $actions Rule object, an array of actions or a JSON string array of actions.
*
* @return void
*
* @since 1.7.0
*/
public function merge($actions)
{
if (\is_string($actions)) {
$actions = json_decode($actions, true);
}
if (\is_array($actions)) {
foreach ($actions as $action => $identities) {
$this->mergeAction($action, $identities);
}
} elseif ($actions instanceof Rules) {
$data = $actions->getData();
foreach ($data as $name => $identities) {
$this->mergeAction($name, $identities);
}
}
}
/**
* Merges an array of identities for an action.
*
* @param string $action The name of the action.
* @param array $identities An array of identities
*
* @return void
*
* @since 1.7.0
*/
public function mergeAction($action, $identities)
{
if (isset($this->data[$action])) {
// If exists, merge the action.
$this->data[$action]->mergeIdentities($identities);
} else {
// If new, add the action.
$this->data[$action] = new Rule($identities);
}
}
/**
* Checks that an action can be performed by an identity.
*
* The identity is an integer where +ve represents a user group,
* and -ve represents a user.
*
* @param string $action The name of the action.
* @param mixed $identity An integer representing the identity, or an array of identities
*
* @return mixed Object or null if there is no information about the action.
*
* @since 1.7.0
*/
public function allow($action, $identity)
{
// Check we have information about this action.
if (isset($this->data[$action])) {
return $this->data[$action]->allow($identity);
}
}
/**
* Get the allowed actions for an identity.
*
* @param mixed $identity An integer representing the identity or an array of identities
*
* @return CMSObject Allowed actions for the identity or identities
*
* @since 1.7.0
*/
public function getAllowed($identity)
{
// Sweep for the allowed actions.
$allowed = new CMSObject();
foreach ($this->data as $name => &$action) {
if ($action->allow($identity)) {
$allowed->set($name, true);
}
}
return $allowed;
}
/**
* Magic method to convert the object to JSON string representation.
*
* @return string JSON representation of the actions array
*
* @since 1.7.0
*/
public function __toString()
{
$temp = [];
foreach ($this->data as $name => $rule) {
if ($data = $rule->getData()) {
$temp[$name] = $data;
}
}
return json_encode($temp, JSON_FORCE_OBJECT);
}
}

View File

@ -0,0 +1,226 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Adapter;
use Joomla\CMS\Factory;
use Joomla\CMS\Object\LegacyErrorHandlingTrait;
use Joomla\CMS\Object\LegacyPropertyManagementTrait;
use Joomla\Database\DatabaseAwareInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Adapter Class
* Retains common adapter pattern functions
* Class harvested from joomla.installer.installer
*
* @since 1.6
* @deprecated 4.3 will be removed in 6.0
* Will be removed without replacement
*/
class Adapter
{
use LegacyErrorHandlingTrait;
use LegacyPropertyManagementTrait;
/**
* Associative array of adapters
*
* @var static[]
* @since 1.6
*/
protected $_adapters = [];
/**
* Adapter Folder
*
* @var string
* @since 1.6
*/
protected $_adapterfolder = 'adapters';
/**
* Adapter Class Prefix
*
* @var string
* @since 1.6
*/
protected $_classprefix = 'J';
/**
* Base Path for the adapter instance
*
* @var string
* @since 1.6
*/
protected $_basepath = null;
/**
* Database Connector Object
*
* @var \Joomla\Database\DatabaseDriver
* @since 1.6
*/
protected $_db;
/**
* Constructor
*
* @param string $basepath Base Path of the adapters
* @param string $classprefix Class prefix of adapters
* @param string $adapterfolder Name of folder to append to base path
*
* @since 1.6
*/
public function __construct($basepath, $classprefix = null, $adapterfolder = null)
{
$this->_basepath = $basepath;
$this->_classprefix = $classprefix ?: 'J';
$this->_adapterfolder = $adapterfolder ?: 'adapters';
$this->_db = Factory::getDbo();
// Ensure BC, when removed in 5, then the db must be set with setDatabase explicitly
if ($this instanceof DatabaseAwareInterface) {
$this->setDatabase($this->_db);
}
}
/**
* Get the database connector object
*
* @return \Joomla\Database\DatabaseDriver Database connector object
*
* @since 1.6
*/
public function getDbo()
{
return $this->_db;
}
/**
* Return an adapter.
*
* @param string $name Name of adapter to return
* @param array $options Adapter options
*
* @return static|boolean Adapter of type 'name' or false
*
* @since 1.6
*/
public function getAdapter($name, $options = [])
{
if (\array_key_exists($name, $this->_adapters)) {
return $this->_adapters[$name];
}
if ($this->setAdapter($name, $options)) {
return $this->_adapters[$name];
}
return false;
}
/**
* Set an adapter by name
*
* @param string $name Adapter name
* @param object $adapter Adapter object
* @param array $options Adapter options
*
* @return boolean True if successful
*
* @since 1.6
*/
public function setAdapter($name, &$adapter = null, $options = [])
{
if (\is_object($adapter)) {
$this->_adapters[$name] = &$adapter;
return true;
}
$class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name);
if (class_exists($class)) {
$this->_adapters[$name] = new $class($this, $this->_db, $options);
return true;
}
$class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter';
if (class_exists($class)) {
$this->_adapters[$name] = new $class($this, $this->_db, $options);
return true;
}
$fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php';
if (!is_file($fullpath)) {
return false;
}
// Try to load the adapter object
$class = $this->_classprefix . ucfirst($name);
\JLoader::register($class, $fullpath);
if (!class_exists($class)) {
return false;
}
$this->_adapters[$name] = new $class($this, $this->_db, $options);
return true;
}
/**
* Loads all adapters.
*
* @param array $options Adapter options
*
* @return void
*
* @since 1.6
*/
public function loadAllAdapters($options = [])
{
$files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder);
/** @type $file \DirectoryIterator */
foreach ($files as $file) {
$fileName = $file->getFilename();
// Only load for php files.
if (!$file->isFile() || $file->getExtension() != 'php') {
continue;
}
// Try to load the adapter object
require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName;
// Derive the class name from the filename.
$name = str_ireplace('.php', '', ucfirst(trim($fileName)));
$class = $this->_classprefix . ucfirst($name);
if (!class_exists($class)) {
// Skip to next one
continue;
}
$adapter = new $class($this, $this->_db, $options);
$this->_adapters[$name] = clone $adapter;
}
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Adapter;
use Joomla\CMS\Factory;
use Joomla\CMS\Object\LegacyErrorHandlingTrait;
use Joomla\CMS\Object\LegacyPropertyManagementTrait;
use Joomla\Database\DatabaseDriver;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Adapter Instance Class
*
* @since 1.6
* @deprecated 4.3 will be removed in 6.0
* Will be removed without replacement
*/
class AdapterInstance
{
use LegacyErrorHandlingTrait;
use LegacyPropertyManagementTrait;
/**
* Parent
*
* @var Adapter
* @since 1.6
*/
protected $parent = null;
/**
* Database
*
* @var DatabaseDriver
* @since 1.6
*/
protected $db = null;
/**
* Constructor
*
* @param Adapter $parent Parent object
* @param DatabaseDriver $db Database object
* @param array $options Configuration Options
*
* @since 1.6
*/
public function __construct(Adapter $parent, DatabaseDriver $db, array $options = [])
{
// Set the properties from the options array that is passed in
$this->setProperties($options);
// Set the parent and db in case $options for some reason overrides it.
$this->parent = $parent;
// Pull in the global dbo in case something happened to it.
$this->db = $db ?: Factory::getDbo();
}
/**
* Retrieves the parent object
*
* @return Adapter
*
* @since 1.6
*/
public function getParent()
{
return $this->parent;
}
}

View File

@ -0,0 +1,511 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\Web\WebClient;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Application\AfterDispatchEvent;
use Joomla\CMS\Event\Application\AfterInitialiseDocumentEvent;
use Joomla\CMS\Event\Application\AfterRouteEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Input\Input;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Router;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\DI\Container;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Administrator Application class
*
* @since 3.2
*/
class AdministratorApplication extends CMSApplication
{
use MultiFactorAuthenticationHandler;
/**
* List of allowed components for guests and users which do not have the core.login.admin privilege.
*
* By default we allow two core components:
*
* - com_login Absolutely necessary to let users log into the backend of the site. Do NOT remove!
* - com_ajax Handle AJAX requests or other administrative callbacks without logging in. Required for
* passwordless authentication using WebAuthn.
*
* @var array
*/
protected $allowedUnprivilegedOptions = [
'com_login',
'com_ajax',
];
/**
* Class constructor.
*
* @param ?Input $input An optional argument to provide dependency injection for the application's input
* object. If the argument is a JInput object that object will become the
* application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's config
* object. If the argument is a Registry object that object will become the
* application's config object, otherwise a default config object is created.
* @param ?WebClient $client An optional argument to provide dependency injection for the application's
* client object. If the argument is a WebClient object that object will become the
* application's client object, otherwise a default client object is created.
* @param ?Container $container Dependency injection container.
*
* @since 3.2
*/
public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null)
{
// Register the application name
$this->name = 'administrator';
// Register the client ID
$this->clientId = 1;
// Execute the parent constructor
parent::__construct($input, $config, $client, $container);
// Set the root in the URI based on the application name
Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\'));
}
/**
* Dispatch the application
*
* @param string $component The component which is being rendered.
*
* @return void
*
* @since 3.2
*/
public function dispatch($component = null)
{
if ($component === null) {
$component = $this->findOption();
}
// Load the document to the API
$this->loadDocument();
// Set up the params
$document = Factory::getDocument();
switch ($document->getType()) {
case 'html':
// Get the template
$template = $this->getTemplate(true);
$clientId = $this->getClientId();
// Store the template and its params to the config
$this->set('theme', $template->template);
$this->set('themeParams', $template->params);
// Add Asset registry files
$wr = $document->getWebAssetManager()->getRegistry();
if ($component) {
$wr->addExtensionRegistryFile($component);
}
if (!empty($template->parent)) {
$wr->addTemplateRegistryFile($template->parent, $clientId);
}
$wr->addTemplateRegistryFile($template->template, $clientId);
break;
default:
break;
}
$document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION'));
$document->setDescription($this->get('MetaDesc'));
$document->setGenerator('Joomla! - Open Source Content Management');
// Trigger the onAfterInitialiseDocument event.
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
$this->dispatchEvent(
'onAfterInitialiseDocument',
new AfterInitialiseDocumentEvent('onAfterInitialiseDocument', ['subject' => $this, 'document' => $document])
);
$contents = ComponentHelper::renderComponent($component);
$document->setBuffer($contents, ['type' => 'component']);
// Trigger the onAfterDispatch event.
$this->dispatchEvent(
'onAfterDispatch',
new AfterDispatchEvent('onAfterDispatch', ['subject' => $this])
);
}
/**
* Method to run the Web application routines.
*
* @return void
*
* @since 3.2
*/
protected function doExecute()
{
// Get the language from the (login) form or user state
$login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : '';
$options = ['language' => $login_lang ?: $this->getUserState('application.lang')];
// Initialise the application
$this->initialiseApp($options);
// Mark afterInitialise in the profiler.
JDEBUG ? $this->profiler->mark('afterInitialise') : null;
// Route the application
$this->route();
// Mark afterRoute in the profiler.
JDEBUG ? $this->profiler->mark('afterRoute') : null;
/*
* Check if the user is required to reset their password
*
* Before $this->route(); "option" and "view" can't be safely read using:
* $this->input->getCmd('option'); or $this->input->getCmd('view');
* ex: due of the sef urls
*/
$this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout');
// Dispatch the application
$this->dispatch();
// Mark afterDispatch in the profiler.
JDEBUG ? $this->profiler->mark('afterDispatch') : null;
}
/**
* Return a reference to the Router object.
*
* @param string $name The name of the application.
* @param array $options An optional associative array of configuration settings.
*
* @return Router
*
* @since 3.2
*
* @deprecated 4.3 will be removed in 6.0
* Inject the router or load it from the dependency injection container
* Example:
* Factory::getContainer()->get(AdministratorRouter::class);
*/
public static function getRouter($name = 'administrator', array $options = [])
{
return parent::getRouter($name, $options);
}
/**
* Gets the name of the current template.
*
* @param boolean $params True to return the template parameters
*
* @return string|\stdClass The name of the template if the params argument is false. The template object if the params argument is true.
*
* @since 3.2
* @throws \InvalidArgumentException
*/
public function getTemplate($params = false)
{
if (\is_object($this->template)) {
if ($params) {
return $this->template;
}
return $this->template->template;
}
$adminStyle = $this->getIdentity() ? (int) $this->getIdentity()->getParam('admin_style') : 0;
$template = $this->bootComponent('templates')->getMVCFactory()
->createModel('Style', 'Administrator')->getAdminTemplate($adminStyle);
$template->template = InputFilter::getInstance()->clean($template->template, 'cmd');
$template->params = new Registry($template->params);
// Fallback template
if (
!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')
&& !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')
) {
$this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']);
$template->params = new Registry();
$template->template = 'atum';
// Check, the data were found and if template really exists
if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) {
throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template));
}
}
// Cache the result
$this->template = $template;
// Pass the parent template to the state
$this->set('themeInherits', $template->parent);
if ($params) {
return $template;
}
return $template->template;
}
/**
* Initialise the application.
*
* @param array $options An optional associative array of configuration settings.
*
* @return void
*
* @since 3.2
*/
protected function initialiseApp($options = [])
{
$user = Factory::getUser();
// If the user is a guest we populate it with the guest user group.
if ($user->guest) {
$guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1);
$user->groups = [$guestUsergroup];
}
// If a language was specified it has priority, otherwise use user or default language settings
if (empty($options['language'])) {
$lang = $user->getParam('admin_language');
// Make sure that the user's language exists
if ($lang && LanguageHelper::exists($lang)) {
$options['language'] = $lang;
} else {
$params = ComponentHelper::getParams('com_languages');
$options['language'] = $params->get('administrator', $this->get('language', 'en-GB'));
}
}
// One last check to make sure we have something
if (!LanguageHelper::exists($options['language'])) {
$lang = $this->get('language', 'en-GB');
if (LanguageHelper::exists($lang)) {
$options['language'] = $lang;
} else {
// As a last ditch fail to english
$options['language'] = 'en-GB';
}
}
// Finish initialisation
parent::initialiseApp($options);
}
/**
* Login authentication function
*
* @param array $credentials Array('username' => string, 'password' => string)
* @param array $options Array('remember' => boolean)
*
* @return boolean True on success.
*
* @since 3.2
*/
public function login($credentials, $options = [])
{
// The minimum group
$options['group'] = 'Public Backend';
// Make sure users are not auto-registered
$options['autoregister'] = false;
// Set the application login entry point
if (!\array_key_exists('entry_url', $options)) {
$options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login';
}
// Set the access control action to check.
$options['action'] = 'core.login.admin';
$result = parent::login($credentials, $options);
if (!($result instanceof \Exception)) {
$lang = $this->input->getCmd('lang', '');
$lang = preg_replace('/[^A-Z-]/i', '', $lang);
if ($lang) {
$this->setUserState('application.lang', $lang);
}
$this->bootComponent('messages')->getMVCFactory()
->createModel('Messages', 'Administrator')->purge($this->getIdentity() ? $this->getIdentity()->id : 0);
}
return $result;
}
/**
* Purge the jos_messages table of old messages
*
* @return void
*
* @since 3.2
*
* @deprecated 4.3 will be removed in 6.0
* Purge the messages through the messages model
* Example:
* Factory::getApplication()->bootComponent('messages')->getMVCFactory()
* ->createModel('Messages', 'Administrator')->purge(Factory::getApplication()->getIdentity()->id);
*/
public static function purgeMessages()
{
Factory::getApplication()->bootComponent('messages')->getMVCFactory()
->createModel('Messages', 'Administrator')->purge(Factory::getUser()->id);
}
/**
* Rendering is the process of pushing the document buffers into the template
* placeholders, retrieving data from the document and pushing it into
* the application response buffer.
*
* @return void
*
* @since 3.2
*/
protected function render()
{
// Get the \JInput object
$input = $this->input;
$component = $input->getCmd('option', 'com_login');
$file = $input->getCmd('tmpl', 'index');
if ($component === 'com_login') {
$file = 'login';
}
$this->set('themeFile', $file . '.php');
// Safety check for when configuration.php root_user is in use.
$rootUser = $this->get('root_user');
if (property_exists('\JConfig', 'root_user')) {
if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) {
$this->enqueueMessage(
Text::sprintf(
'JWARNING_REMOVE_ROOT_USER',
'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1'
),
'warning'
);
} elseif (Factory::getUser()->authorise('core.admin')) {
// Show this message to superusers too
$this->enqueueMessage(
Text::sprintf(
'JWARNING_REMOVE_ROOT_USER_ADMIN',
$rootUser,
'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1'
),
'warning'
);
}
}
parent::render();
}
/**
* Route the application.
*
* Routing is the process of examining the request environment to determine which
* component should receive the request. The component optional parameters
* are then set in the request object to be processed when the application is being
* dispatched.
*
* @return void
*
* @since 3.2
*/
protected function route()
{
$uri = Uri::getInstance();
if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') {
// Forward to https
$uri->setScheme('https');
$this->redirect((string) $uri, 301);
}
$this->isHandlingMultiFactorAuthentication();
// Trigger the onAfterRoute event.
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
$this->dispatchEvent(
'onAfterRoute',
new AfterRouteEvent('onAfterRoute', ['subject' => $this])
);
}
/**
* Return the application option string [main component].
*
* @return string The component to access.
*
* @since 4.0.0
*/
public function findOption(): string
{
/** @var self $app */
$app = Factory::getApplication();
$option = strtolower($app->getInput()->get('option', ''));
$user = $app->getIdentity();
/**
* Special handling for guest users and authenticated users without the Backend Login privilege.
*
* If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the
* request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating
* with a user account that has the Backend Login privilege.
*/
if ($user->get('guest') || !$user->authorise('core.login.admin')) {
$option = \in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login';
}
/**
* If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel
* component. This allows the /administrator URL to display something meaningful after logging in instead of an
* error.
*/
if (empty($option)) {
$option = 'com_cpanel';
}
/**
* Force the option to the input object. This is necessary because we might have force-changed the component in
* the two if-blocks above.
*/
$app->getInput()->set('option', $option);
return $option;
}
}

View File

@ -0,0 +1,442 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\Web\WebClient;
use Joomla\CMS\Access\Exception\AuthenticationFailed;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Application\AfterApiRouteEvent;
use Joomla\CMS\Event\Application\AfterDispatchEvent;
use Joomla\CMS\Event\Application\AfterInitialiseDocumentEvent;
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\ApiRouter;
use Joomla\CMS\Router\Exception\RouteNotFoundException;
use Joomla\CMS\Uri\Uri;
use Joomla\DI\Container;
use Joomla\Input\Json as JInputJson;
use Joomla\Registry\Registry;
use Negotiation\Accept;
use Negotiation\Exception\InvalidArgument;
use Negotiation\Negotiator;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! API Application class
*
* @since 4.0.0
*/
final class ApiApplication extends CMSApplication
{
/**
* Maps extension types to their
*
* @var array
* @since 4.0.0
*/
protected $formatMapper = [];
/**
* The authentication plugin type
*
* @var string
* @since 4.0.0
*/
protected $authenticationPluginType = 'api-authentication';
/**
* Class constructor.
*
* @param ?JInputJson $input An optional argument to provide dependency injection for the application's input
* object. If the argument is a JInput object that object will become the
* application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's config
* object. If the argument is a Registry object that object will become the
* application's config object, otherwise a default config object is created.
* @param ?WebClient $client An optional argument to provide dependency injection for the application's client
* object. If the argument is a WebClient object that object will become the
* application's client object, otherwise a default client object is created.
* @param ?Container $container Dependency injection container.
*
* @since 4.0.0
*/
public function __construct(JInputJson $input = null, Registry $config = null, WebClient $client = null, Container $container = null)
{
// Register the application name
$this->name = 'api';
// Register the client ID
$this->clientId = 3;
// Execute the parent constructor
parent::__construct($input, $config, $client, $container);
$this->addFormatMap('application/json', 'json');
$this->addFormatMap('application/vnd.api+json', 'jsonapi');
// Set the root in the URI based on the application name
Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true)));
}
/**
* Method to run the application routines.
*
* Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly.
*
* @return void
*
* @since 4.0.0
*/
protected function doExecute()
{
// Initialise the application
$this->initialiseApp();
// Mark afterInitialise in the profiler.
JDEBUG ? $this->profiler->mark('afterInitialise') : null;
// Route the application
$this->route();
// Mark afterApiRoute in the profiler.
JDEBUG ? $this->profiler->mark('afterApiRoute') : null;
// Dispatch the application
$this->dispatch();
// Mark afterDispatch in the profiler.
JDEBUG ? $this->profiler->mark('afterDispatch') : null;
}
/**
* Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten.
*
* @param string $contentHeader The content header
* @param string $format The content type format
*
* @return void
*
* @since 4.0.0
*/
public function addFormatMap($contentHeader, $format)
{
if (!\array_key_exists($contentHeader, $this->formatMapper)) {
$this->formatMapper[$contentHeader] = $format;
}
}
/**
* Rendering is the process of pushing the document buffers into the template
* placeholders, retrieving data from the document and pushing it into
* the application response buffer.
*
* @return void
*
* @since 4.0.0
*
* @note Rendering should be overridden to get rid of the theme files.
*/
protected function render()
{
// Render the document
$this->setBody($this->document->render($this->allowCache()));
}
/**
* Method to send the application response to the client. All headers will be sent prior to the main application output data.
*
* @param array $options An optional argument to enable CORS. (Temporary)
*
* @return void
*
* @since 4.0.0
*/
protected function respond($options = [])
{
// Set the Joomla! API signature
$this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true);
$forceCORS = (int) $this->get('cors');
if ($forceCORS) {
/**
* Enable CORS (Cross-origin resource sharing)
* Obtain allowed CORS origin from Global Settings.
* Set to * (=all) if not set.
*/
$allowedOrigin = $this->get('cors_allow_origin', '*');
$this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true);
$this->setHeader('Access-Control-Allow-Headers', 'Authorization');
if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) {
$this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true);
$this->setHeader('Access-Control-Allow-Credentials', 'true', true);
}
}
// Parent function can be overridden later on for debugging.
parent::respond();
}
/**
* Gets the name of the current template.
*
* @param boolean $params True to return the template parameters
*
* @return string|\stdClass
*
* @since 4.0.0
*/
public function getTemplate($params = false)
{
// The API application should not need to use a template
if ($params) {
$template = new \stdClass();
$template->template = 'system';
$template->params = new Registry();
$template->inheritable = 0;
$template->parent = '';
return $template;
}
return 'system';
}
/**
* Route the application.
*
* Routing is the process of examining the request environment to determine which
* component should receive the request. The component optional parameters
* are then set in the request object to be processed when the application is being
* dispatched.
*
* @return void
*
* @since 4.0.0
*/
protected function route()
{
$router = $this->getContainer()->get(ApiRouter::class);
// Trigger the onBeforeApiRoute event.
PluginHelper::importPlugin('webservices', null, true, $this->getDispatcher());
$this->dispatchEvent(
'onBeforeApiRoute',
new BeforeApiRouteEvent('onBeforeApiRoute', ['router' => $router, 'subject' => $this])
);
$caught404 = false;
$method = $this->input->getMethod();
try {
$this->handlePreflight($method, $router);
$route = $router->parseApiRoute($method);
} catch (RouteNotFoundException $e) {
$caught404 = true;
}
/**
* Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't
* tell us otherwise it uses the plain JSON API
*/
$priorities = ['application/vnd.api+json'];
if (!$caught404 && \array_key_exists('format', $route['vars'])) {
$priorities = $route['vars']['format'];
}
$negotiator = new Negotiator();
try {
$mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities);
} catch (InvalidArgument $e) {
$mediaType = null;
}
// If we can't find a match bail with a 406 - Not Acceptable
if ($mediaType === null) {
throw new Exception\NotAcceptable('Could not match accept header', 406);
}
/** @var Accept $mediaType */
$format = $mediaType->getValue();
if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) {
$format = $this->formatMapper[$mediaType->getValue()];
}
$this->input->set('format', $format);
if ($caught404) {
throw $e;
}
$this->input->set('controller', $route['controller']);
$this->input->set('task', $route['task']);
foreach ($route['vars'] as $key => $value) {
// We inject the format directly above based on the negotiated format. We do not want the array of possible
// formats provided by the plugin!
if ($key === 'format') {
continue;
}
// We inject the component key into the option parameter in global input for b/c with the other applications
if ($key === 'component') {
$this->input->set('option', $route['vars'][$key]);
continue;
}
if ($this->input->getMethod() === 'POST') {
$this->input->post->set($key, $value);
} else {
$this->input->set($key, $value);
}
}
$this->dispatchEvent(
'onAfterApiRoute',
new AfterApiRouteEvent('onAfterApiRoute', ['subject' => $this])
);
if (!isset($route['vars']['public']) || $route['vars']['public'] === false) {
if (!$this->login(['username' => ''], ['silent' => true, 'action' => 'core.login.api'])) {
throw new AuthenticationFailed();
}
}
}
/**
* Handles preflight requests.
*
* @param String $method The REST verb
*
* @param ApiRouter $router The API Routing object
*
* @return void
*
* @since 4.0.0
*/
protected function handlePreflight($method, $router)
{
/**
* If not an OPTIONS request or CORS is not enabled,
* there's nothing useful to do here.
*/
if ($method !== 'OPTIONS' || !(int) $this->get('cors')) {
return;
}
// Extract routes matching current route from all known routes.
$matchingRoutes = $router->getMatchingRoutes();
// Extract exposed methods from matching routes.
$matchingRoutesMethods = array_unique(
array_reduce(
$matchingRoutes,
function ($carry, $route) {
return array_merge($carry, $route->getMethods());
},
[]
)
);
/**
* Obtain allowed CORS origin from Global Settings.
* Set to * (=all) if not set.
*/
$allowedOrigin = $this->get('cors_allow_origin', '*');
/**
* Obtain allowed CORS headers from Global Settings.
* Set to sensible default if not set.
*/
$allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token');
/**
* Obtain allowed CORS methods from Global Settings.
* Set to methods exposed by current route if not set.
*/
$allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods));
// No use to go through the regular route handling hassle,
// so let's simply output the headers and exit.
$this->setHeader('status', '204');
$this->setHeader('Access-Control-Allow-Origin', $allowedOrigin);
$this->setHeader('Access-Control-Allow-Headers', $allowedHeaders);
$this->setHeader('Access-Control-Allow-Methods', $allowedMethods);
$this->sendHeaders();
$this->close();
}
/**
* Returns the application Router object.
*
* @return ApiRouter
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Inject the router or load it from the dependency injection container
* Example:
* Factory::getContainer()->get(ApiRouter::class);
*
*/
public function getApiRouter()
{
return $this->getContainer()->get(ApiRouter::class);
}
/**
* Dispatch the application
*
* @param string $component The component which is being rendered.
*
* @return void
*
* @since 4.0.0
*/
public function dispatch($component = null)
{
// Get the component if not set.
if (!$component) {
$component = $this->input->get('option', null);
}
// Load the document to the API
$this->loadDocument();
// Set up the params
$document = Factory::getDocument();
// Trigger the onAfterInitialiseDocument event.
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
$this->dispatchEvent(
'onAfterInitialiseDocument',
new AfterInitialiseDocumentEvent('onAfterInitialiseDocument', ['subject' => $this, 'document' => $document])
);
$contents = ComponentHelper::renderComponent($component);
$document->setBuffer($contents, ['type' => 'component']);
// Trigger the onAfterDispatch event.
$this->dispatchEvent(
'onAfterDispatch',
new AfterDispatchEvent('onAfterDispatch', ['subject' => $this])
);
}
}

View File

@ -0,0 +1,207 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\OutputFilter;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Application helper functions
*
* @since 1.5
*/
class ApplicationHelper
{
/**
* Client information array
*
* @var array
* @since 1.6
*/
protected static $_clients = [];
/**
* Return the name of the request component [main component]
*
* @param string $default The default option
*
* @return string Option (e.g. com_something)
*
* @since 1.6
*/
public static function getComponentName($default = null)
{
static $option;
if ($option) {
return $option;
}
$input = Factory::getApplication()->getInput();
$option = strtolower($input->get('option', ''));
if (empty($option)) {
$option = $default;
}
$input->set('option', $option);
return $option;
}
/**
* Provides a secure hash based on a seed
*
* @param string $seed Seed string.
*
* @return string A secure hash
*
* @since 3.2
*/
public static function getHash($seed)
{
return md5(Factory::getApplication()->get('secret') . $seed);
}
/**
* This method transliterates a string into a URL
* safe string or returns a URL safe UTF-8 string
* based on the global configuration
*
* @param string $string String to process
* @param string $language Language to transliterate to if unicode slugs are disabled
*
* @return string Processed string
*
* @since 3.2
*/
public static function stringURLSafe($string, $language = '')
{
if (Factory::getApplication()->get('unicodeslugs') == 1) {
$output = OutputFilter::stringUrlUnicodeSlug($string);
} else {
if ($language === '*' || $language === '') {
$languageParams = ComponentHelper::getParams('com_languages');
$language = $languageParams->get('site');
}
$output = OutputFilter::stringURLSafe($string, $language);
}
return $output;
}
/**
* Gets information on a specific client id. This method will be useful in
* future versions when we start mapping applications in the database.
*
* This method will return a client information array if called
* with no arguments which can be used to add custom application information.
*
* @param integer|string|null $id A client identifier
* @param boolean $byName If true, find the client by its name
*
* @return \stdClass|\stdClass[]|null Object describing the client, array containing all the clients or null if $id not known
*
* @since 1.5
*/
public static function getClientInfo($id = null, $byName = false)
{
// Only create the array if it is empty
if (empty(self::$_clients)) {
$obj = new \stdClass();
// Site Client
$obj->id = 0;
$obj->name = 'site';
$obj->path = JPATH_SITE;
self::$_clients[0] = clone $obj;
// Administrator Client
$obj->id = 1;
$obj->name = 'administrator';
$obj->path = JPATH_ADMINISTRATOR;
self::$_clients[1] = clone $obj;
// Installation Client
$obj->id = 2;
$obj->name = 'installation';
$obj->path = JPATH_INSTALLATION;
self::$_clients[2] = clone $obj;
// API Client
$obj->id = 3;
$obj->name = 'api';
$obj->path = JPATH_API;
self::$_clients[3] = clone $obj;
// CLI Client
$obj->id = 4;
$obj->name = 'cli';
$obj->path = JPATH_CLI;
self::$_clients[4] = clone $obj;
}
// If no client id has been passed return the whole array
if ($id === null) {
return self::$_clients;
}
// Are we looking for client information by id or by name?
if (!$byName) {
if (isset(self::$_clients[$id])) {
return self::$_clients[$id];
}
} else {
foreach (self::$_clients as $client) {
if ($client->name == strtolower($id)) {
return $client;
}
}
}
return null;
}
/**
* Adds information for a client.
*
* @param mixed $client A client identifier either an array or object
*
* @return boolean True if the information is added. False on error
*
* @since 1.6
*/
public static function addClientInfo($client)
{
if (\is_array($client)) {
$client = (object) $client;
}
if (!\is_object($client)) {
return false;
}
$info = self::getClientInfo();
if (!isset($client->id)) {
$client->id = \count($info);
}
self::$_clients[$client->id] = clone $client;
return true;
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\AbstractApplication;
use Joomla\CMS\Input\Input;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla Platform Base Application Class
*
* @property-read Input $input The application input object
*
* @since 3.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Application classes should directly be based on \Joomla\Application\AbstractApplication
* don't use this class anymore
*/
abstract class BaseApplication extends AbstractApplication implements DispatcherAwareInterface
{
use DispatcherAwareTrait;
use EventAware;
use IdentityAware;
/**
* Class constructor.
*
* @param ?Input $input An optional argument to provide dependency injection for the application's
* input object. If the argument is a \JInput object that object will become
* the application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's
* config object. If the argument is a Registry object that object will become
* the application's config object, otherwise a default config object is created.
*
* @since 3.0.0
*/
public function __construct(Input $input = null, Registry $config = null)
{
$this->input = $input instanceof Input ? $input : new Input();
$this->config = $config instanceof Registry ? $config : new Registry();
$this->initialise();
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Class CliInput
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
class CliInput
{
/**
* Get a value from standard input.
*
* @return string The input string from standard input.
*
* @codeCoverageIgnore
* @since 4.0.0
*/
public function in()
{
return rtrim(fread(STDIN, 8192), "\n\r");
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI;
use Joomla\CMS\Application\CLI\Output\Processor\ProcessorInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Base class defining a command line output handler
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
abstract class CliOutput
{
/**
* Output processing object
*
* @var ProcessorInterface
* @since 4.0.0
*/
protected $processor;
/**
* Constructor
*
* @param ?ProcessorInterface $processor The output processor.
*
* @since 4.0.0
*/
public function __construct(ProcessorInterface $processor = null)
{
$this->setProcessor($processor ?: new Output\Processor\ColorProcessor());
}
/**
* Set a processor
*
* @param ProcessorInterface $processor The output processor.
*
* @return $this
*
* @since 4.0.0
*/
public function setProcessor(ProcessorInterface $processor)
{
$this->processor = $processor;
return $this;
}
/**
* Get a processor
*
* @return ProcessorInterface
*
* @since 4.0.0
* @throws \RuntimeException
*/
public function getProcessor()
{
if ($this->processor) {
return $this->processor;
}
throw new \RuntimeException('A ProcessorInterface object has not been set.');
}
/**
* Write a string to an output handler.
*
* @param string $text The text to display.
* @param boolean $nl True (default) to append a new line at the end of the output string.
*
* @return $this
*
* @since 4.0.0
* @codeCoverageIgnore
*/
abstract public function out($text = '', $nl = true);
}

View File

@ -0,0 +1,263 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Class defining ANSI-color styles for command line output
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
final class ColorStyle
{
/**
* Known colors
*
* @var array
* @since 4.0.0
*/
private static $knownColors = [
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
];
/**
* Known styles
*
* @var array
* @since 4.0.0
*/
private static $knownOptions = [
'bold' => 1,
'underscore' => 4,
'blink' => 5,
'reverse' => 7,
];
/**
* Foreground base value
*
* @var integer
* @since 4.0.0
*/
private static $fgBase = 30;
/**
* Background base value
*
* @var integer
* @since 4.0.0
*/
private static $bgBase = 40;
/**
* Foreground color
*
* @var integer
* @since 4.0.0
*/
private $fgColor = 0;
/**
* Background color
*
* @var integer
* @since 4.0.0
*/
private $bgColor = 0;
/**
* Array of style options
*
* @var array
* @since 4.0.0
*/
private $options = [];
/**
* Constructor
*
* @param string $fg Foreground color.
* @param string $bg Background color.
* @param array $options Style options.
*
* @since 4.0.0
* @throws \InvalidArgumentException
*/
public function __construct(string $fg = '', string $bg = '', array $options = [])
{
if ($fg) {
if (\array_key_exists($fg, static::$knownColors) == false) {
throw new \InvalidArgumentException(
sprintf(
'Invalid foreground color "%1$s" [%2$s]',
$fg,
implode(', ', $this->getKnownColors())
)
);
}
$this->fgColor = static::$fgBase + static::$knownColors[$fg];
}
if ($bg) {
if (\array_key_exists($bg, static::$knownColors) == false) {
throw new \InvalidArgumentException(
sprintf(
'Invalid background color "%1$s" [%2$s]',
$bg,
implode(', ', $this->getKnownColors())
)
);
}
$this->bgColor = static::$bgBase + static::$knownColors[$bg];
}
foreach ($options as $option) {
if (\array_key_exists($option, static::$knownOptions) == false) {
throw new \InvalidArgumentException(
sprintf(
'Invalid option "%1$s" [%2$s]',
$option,
implode(', ', $this->getKnownOptions())
)
);
}
$this->options[] = $option;
}
}
/**
* Convert to a string.
*
* @return string
*
* @since 4.0.0
*/
public function __toString()
{
return $this->getStyle();
}
/**
* Create a color style from a parameter string.
*
* Example: fg=red;bg=blue;options=bold,blink
*
* @param string $string The parameter string.
*
* @return $this
*
* @since 4.0.0
* @throws \RuntimeException
*/
public static function fromString(string $string): self
{
$fg = '';
$bg = '';
$options = [];
$parts = explode(';', $string);
foreach ($parts as $part) {
$subParts = explode('=', $part);
if (\count($subParts) < 2) {
continue;
}
switch ($subParts[0]) {
case 'fg':
$fg = $subParts[1];
break;
case 'bg':
$bg = $subParts[1];
break;
case 'options':
$options = explode(',', $subParts[1]);
break;
default:
throw new \RuntimeException('Invalid option: ' . $subParts[0]);
}
}
return new self($fg, $bg, $options);
}
/**
* Get the translated color code.
*
* @return string
*
* @since 4.0.0
*/
public function getStyle(): string
{
$values = [];
if ($this->fgColor) {
$values[] = $this->fgColor;
}
if ($this->bgColor) {
$values[] = $this->bgColor;
}
foreach ($this->options as $option) {
$values[] = static::$knownOptions[$option];
}
return implode(';', $values);
}
/**
* Get the known colors.
*
* @return string[]
*
* @since 4.0.0
*/
public function getKnownColors(): array
{
return array_keys(static::$knownColors);
}
/**
* Get the known options.
*
* @return string[]
*
* @since 4.0.0
*/
public function getKnownOptions(): array
{
return array_keys(static::$knownOptions);
}
}

View File

@ -0,0 +1,194 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI\Output\Processor;
use Joomla\CMS\Application\CLI\ColorStyle;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Command line output processor supporting ANSI-colored output
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
class ColorProcessor implements ProcessorInterface
{
/**
* Flag to remove color codes from the output
*
* @var boolean
* @since 4.0.0
*/
public $noColors = false;
/**
* Regex to match tags
*
* @var string
* @since 4.0.0
*/
protected $tagFilter = '/<([a-z=;]+)>(.*?)<\/\\1>/s';
/**
* Regex used for removing color codes
*
* @var string
* @since 4.0.0
*/
protected static $stripFilter = '/<[\/]?[a-z=;]+>/';
/**
* Array of ColorStyle objects
*
* @var ColorStyle[]
* @since 4.0.0
*/
protected $styles = [];
/**
* Class constructor
*
* @param boolean $noColors Defines non-colored mode on construct
*
* @since 4.0.0
*/
public function __construct($noColors = null)
{
if ($noColors === null) {
/*
* By default windows cmd.exe and PowerShell does not support ANSI-colored output
* if the variable is not set explicitly colors should be disabled on Windows
*/
$noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
}
$this->noColors = $noColors;
$this->addPredefinedStyles();
}
/**
* Add a style.
*
* @param string $name The style name.
* @param ColorStyle $style The color style.
*
* @return $this
*
* @since 4.0.0
*/
public function addStyle($name, ColorStyle $style)
{
$this->styles[$name] = $style;
return $this;
}
/**
* Strip color tags from a string.
*
* @param string $string The string.
*
* @return string
*
* @since 4.0.0
*/
public static function stripColors($string)
{
return preg_replace(static::$stripFilter, '', $string);
}
/**
* Process a string.
*
* @param string $string The string to process.
*
* @return string
*
* @since 4.0.0
*/
public function process($string)
{
preg_match_all($this->tagFilter, $string, $matches);
if (!$matches) {
return $string;
}
foreach ($matches[0] as $i => $m) {
if (\array_key_exists($matches[1][$i], $this->styles)) {
$string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]);
} elseif (strpos($matches[1][$i], '=')) {
// Custom format
$string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i]));
}
}
return $string;
}
/**
* Replace color tags in a string.
*
* @param string $text The original text.
* @param string $tag The matched tag.
* @param string $match The match.
* @param ColorStyle $style The color style to apply.
*
* @return mixed
*
* @since 4.0.0
*/
private function replaceColors($text, $tag, $match, ColorStyle $style)
{
$replace = $this->noColors
? $match
: "\033[" . $style . 'm' . $match . "\033[0m";
return str_replace('<' . $tag . '>' . $match . '</' . $tag . '>', $replace, $text);
}
/**
* Adds predefined color styles to the ColorProcessor object
*
* @return $this
*
* @since 4.0.0
*/
private function addPredefinedStyles()
{
$this->addStyle(
'info',
new ColorStyle('green', '', ['bold'])
);
$this->addStyle(
'comment',
new ColorStyle('yellow', '', ['bold'])
);
$this->addStyle(
'question',
new ColorStyle('black', 'cyan')
);
$this->addStyle(
'error',
new ColorStyle('white', 'red')
);
return $this;
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI\Output\Processor;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface for a command line output processor
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
interface ProcessorInterface
{
/**
* Process the provided output into a string.
*
* @param string $output The string to process.
*
* @return string
*
* @since 4.0.0
*/
public function process($output);
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI\Output;
use Joomla\CMS\Application\CLI\CliOutput;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Output handler for writing command line output to the stdout interface
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
class Stdout extends CliOutput
{
/**
* Write a string to standard output
*
* @param string $text The text to display.
* @param boolean $nl True (default) to append a new line at the end of the output string.
*
* @return $this
*
* @codeCoverageIgnore
* @since 4.0.0
*/
public function out($text = '', $nl = true)
{
fwrite(STDOUT, $this->getProcessor()->process($text) . ($nl ? "\n" : null));
return $this;
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\CLI\Output;
use Joomla\CMS\Application\CLI\CliOutput;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Output handler for writing command line output to the stdout interface
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the `joomla/console` package instead
*/
class Xml extends CliOutput
{
/**
* Write a string to standard output.
*
* @param string $text The text to display.
* @param boolean $nl True (default) to append a new line at the end of the output string.
*
* @return $this
*
* @since 4.0.0
* @throws \RuntimeException
* @codeCoverageIgnore
*/
public function out($text = '', $nl = true)
{
fwrite(STDOUT, $text . ($nl ? "\n" : null));
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\ConfigurationAwareApplicationInterface;
use Joomla\CMS\Extension\ExtensionManagerInterface;
use Joomla\CMS\Language\Language;
use Joomla\CMS\User\User;
use Joomla\Input\Input;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface defining a Joomla! CMS Application class
*
* @since 4.0.0
* @note In Joomla 6 this interface will no longer extend EventAwareInterface
* @property-read Input $input {@deprecated 4.0 will be removed in 6.0} The Joomla Input property. Deprecated in favour of getInput()
*/
interface CMSApplicationInterface extends ExtensionManagerInterface, ConfigurationAwareApplicationInterface, EventAwareInterface
{
/**
* Constant defining an enqueued emergency message
*
* @var string
* @since 4.0.0
*/
public const MSG_EMERGENCY = 'emergency';
/**
* Constant defining an enqueued alert message
*
* @var string
* @since 4.0.0
*/
public const MSG_ALERT = 'alert';
/**
* Constant defining an enqueued critical message
*
* @var string
* @since 4.0.0
*/
public const MSG_CRITICAL = 'critical';
/**
* Constant defining an enqueued error message
*
* @var string
* @since 4.0.0
*/
public const MSG_ERROR = 'error';
/**
* Constant defining an enqueued warning message
*
* @var string
* @since 4.0.0
*/
public const MSG_WARNING = 'warning';
/**
* Constant defining an enqueued notice message
*
* @var string
* @since 4.0.0
*/
public const MSG_NOTICE = 'notice';
/**
* Constant defining an enqueued info message
*
* @var string
* @since 4.0.0
*/
public const MSG_INFO = 'info';
/**
* Constant defining an enqueued debug message
*
* @var string
* @since 4.0.0
*/
public const MSG_DEBUG = 'debug';
/**
* Enqueue a system message.
*
* @param string $msg The message to enqueue.
* @param string $type The message type.
*
* @return void
*
* @since 4.0.0
*/
public function enqueueMessage($msg, $type = self::MSG_INFO);
/**
* Get the system message queue.
*
* @return array The system message queue.
*
* @since 4.0.0
*/
public function getMessageQueue();
/**
* Check the client interface by name.
*
* @param string $identifier String identifier for the application interface
*
* @return boolean True if this application is of the given type client interface.
*
* @since 4.0.0
*/
public function isClient($identifier);
/**
* Flag if the application instance is a CLI or web based application.
*
* Helper function, you should use the native PHP functions to detect if it is a CLI application.
*
* @return boolean
*
* @since 4.0.0
*
* @deprecated 4.0 will be removed in 6.0
* Will be removed without replacement. CLI will be handled by the joomla/console package instead
*/
public function isCli();
/**
* Get the application identity.
*
* @return User|null A User object or null if not set.
*
* @since 4.0.0
*/
public function getIdentity();
/**
* Method to get the application input object.
*
* @return Input
*
* @since 4.0.0
*/
public function getInput(): Input;
/**
* Method to get the application language object.
*
* @return Language The language object
*
* @since 4.0.0
*/
public function getLanguage();
/**
* Gets the name of the current running application.
*
* @return string The name of the application.
*
* @since 4.0.0
*/
public function getName();
/**
* Allows the application to load a custom or default identity.
*
* @param ?User $identity An optional identity object. If omitted, the factory user is created.
*
* @return $this
*
* @since 4.0.0
*/
public function loadIdentity(User $identity = null);
}

View File

@ -0,0 +1,103 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\SessionAwareWebApplicationInterface;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\CMS\Router\Router;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface defining a Joomla! CMS Application class for web applications.
*
* @since 4.0.0
*/
interface CMSWebApplicationInterface extends SessionAwareWebApplicationInterface, CMSApplicationInterface
{
/**
* Method to get the application document object.
*
* @return Document The document object
*
* @since 4.0.0
*/
public function getDocument();
/**
* Get the menu object.
*
* @param string $name The application name for the menu
* @param array $options An array of options to initialise the menu with
*
* @return AbstractMenu|null An AbstractMenu object or null if not set.
*
* @since 4.0.0
*/
public function getMenu($name = null, $options = []);
/**
* Returns the application Router object.
*
* @param string $name The name of the application.
* @param array $options An optional associative array of configuration settings.
*
* @return Router
*
* @since 4.0.0
*
* @deprecated 4.3 will be removed in 6.0
* Inject the router or load it from the dependency injection container
* Example: Factory::getContainer()->get($name);
*/
public static function getRouter($name = null, array $options = []);
/**
* Gets a user state.
*
* @param string $key The path of the state.
* @param mixed $default Optional default value, returned if the internal value is null.
*
* @return mixed The user state or null.
*
* @since 4.0.0
*/
public function getUserState($key, $default = null);
/**
* Gets the value of a user state variable.
*
* @param string $key The key of the user state variable.
* @param string $request The name of the variable passed in a request.
* @param string $default The default value for the variable if not found. Optional.
* @param string $type Filter for the variable. Optional.
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
*
* @return mixed The request user state.
*
* @since 4.0.0
*/
public function getUserStateFromRequest($key, $request, $default = null, $type = 'none');
/**
* Sets the value of a user state variable.
*
* @param string $key The path of the state.
* @param mixed $value The value of the variable.
*
* @return mixed The previous state, if one existed. Null otherwise.
*
* @since 4.0.0
*/
public function setUserState($key, $value);
}

View File

@ -0,0 +1,431 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\AbstractApplication;
use Joomla\CMS\Application\CLI\CliInput;
use Joomla\CMS\Application\CLI\CliOutput;
use Joomla\CMS\Application\CLI\Output\Stdout;
use Joomla\CMS\Event\Application\AfterExecuteEvent;
use Joomla\CMS\Event\Application\BeforeExecuteEvent;
use Joomla\CMS\Extension\ExtensionManagerTrait;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use Joomla\DI\Container;
use Joomla\DI\ContainerAwareTrait;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Input\Input;
use Joomla\Registry\Registry;
use Joomla\Session\SessionInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Base class for a Joomla! command line application.
*
* @since 2.5.0
*
* @deprecated 4.0 will be removed in 6.0
* Use the ConsoleApplication instead
*/
abstract class CliApplication extends AbstractApplication implements DispatcherAwareInterface, CMSApplicationInterface
{
use DispatcherAwareTrait;
use EventAware;
use IdentityAware;
use ContainerAwareTrait;
use ExtensionManagerTrait;
use ExtensionNamespaceMapper;
/**
* Output object
*
* @var CliOutput
* @since 4.0.0
*/
protected $output;
/**
* The input.
*
* @var \Joomla\Input\Input
* @since 4.0.0
*/
protected $input = null;
/**
* CLI Input object
*
* @var CliInput
* @since 4.0.0
*/
protected $cliInput;
/**
* The application language object.
*
* @var Language
* @since 4.0.0
*/
protected $language;
/**
* The application message queue.
*
* @var array
* @since 4.0.0
*/
protected $messages = [];
/**
* The application instance.
*
* @var CliApplication
* @since 1.7.0
*/
protected static $instance;
/**
* Class constructor.
*
* @param ?Input $input An optional argument to provide dependency injection for the application's
* input object. If the argument is a JInputCli object that object will become
* the application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's
* config object. If the argument is a Registry object that object will become
* the application's config object, otherwise a default config object is created.
* @param ?CliOutput $output The output handler.
* @param ?CliInput $cliInput The CLI input handler.
* @param ?DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's
* event dispatcher. If the argument is a DispatcherInterface object that object will become
* the application's event dispatcher, if it is null then the default event dispatcher
* will be created based on the application's loadDispatcher() method.
* @param ?Container $container Dependency injection container.
*
* @since 1.7.0
*/
public function __construct(
Input $input = null,
Registry $config = null,
CliOutput $output = null,
CliInput $cliInput = null,
DispatcherInterface $dispatcher = null,
Container $container = null
) {
// Close the application if we are not executed from the command line.
if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) {
$this->close();
}
$container = $container ?: Factory::getContainer();
$this->setContainer($container);
$this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class));
if (!$container->has('session')) {
$container->alias('session', 'session.cli')
->alias('JSession', 'session.cli')
->alias(\Joomla\CMS\Session\Session::class, 'session.cli')
->alias(\Joomla\Session\Session::class, 'session.cli')
->alias(\Joomla\Session\SessionInterface::class, 'session.cli');
}
$this->input = new \Joomla\CMS\Input\Cli();
$this->language = Factory::getLanguage();
$this->output = $output ?: new Stdout();
$this->cliInput = $cliInput ?: new CliInput();
parent::__construct($config);
// Set the current directory.
$this->set('cwd', getcwd());
// Set up the environment
$this->input->set('format', 'cli');
}
/**
* Magic method to access properties of the application.
*
* @param string $name The name of the property.
*
* @return mixed A value if the property name is valid, null otherwise.
*
* @since 4.0.0
*
* @deprecated 4.0 will be removed in 6.0
* This is a B/C proxy for deprecated read accesses
* Example: Factory::getApplication()->getInput();
*/
public function __get($name)
{
switch ($name) {
case 'input':
@trigger_error(
'Accessing the input property of the application is deprecated, use the getInput() method instead.',
E_USER_DEPRECATED
);
return $this->getInput();
default:
$trace = debug_backtrace();
trigger_error(
sprintf(
'Undefined property via __get(): %1$s in %2$s on line %3$s',
$name,
$trace[0]['file'],
$trace[0]['line']
),
E_USER_NOTICE
);
}
}
/**
* Method to get the application input object.
*
* @return Input
*
* @since 4.0.0
*/
public function getInput(): Input
{
return $this->input;
}
/**
* Method to get the application language object.
*
* @return Language The language object
*
* @since 4.0.0
*/
public function getLanguage()
{
return $this->language;
}
/**
* Returns a reference to the global CliApplication object, only creating it if it doesn't already exist.
*
* This method must be invoked as: $cli = CliApplication::getInstance();
*
* @param string $name The name (optional) of the Application Cli class to instantiate.
*
* @return CliApplication
*
* @since 1.7.0
*
* @deprecated 4.0 will be removed in 6.0
* Load the app through the container or via the Factory
* Example: Factory::getContainer()->get(CliApplication::class)
*
* @throws \RuntimeException
*/
public static function getInstance($name = null)
{
// Only create the object if it doesn't exist.
if (empty(static::$instance)) {
if (!class_exists($name)) {
throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500);
}
static::$instance = new $name();
}
return static::$instance;
}
/**
* Execute the application.
*
* @return void
*
* @since 1.7.0
*/
public function execute()
{
$this->createExtensionNamespaceMap();
// Trigger the onBeforeExecute event
$this->dispatchEvent(
'onBeforeExecute',
new BeforeExecuteEvent('onBeforeExecute', ['subject' => $this, 'container' => $this->getContainer()])
);
// Perform application routines.
$this->doExecute();
// Trigger the onAfterExecute event.
$this->dispatchEvent(
'onAfterExecute',
new AfterExecuteEvent('onAfterExecute', ['subject' => $this])
);
}
/**
* Get an output object.
*
* @return CliOutput
*
* @since 4.0.0
*/
public function getOutput()
{
return $this->output;
}
/**
* Get a CLI input object.
*
* @return CliInput
*
* @since 4.0.0
*/
public function getCliInput()
{
return $this->cliInput;
}
/**
* Write a string to standard output.
*
* @param string $text The text to display.
* @param boolean $nl True (default) to append a new line at the end of the output string.
*
* @return $this
*
* @since 4.0.0
*/
public function out($text = '', $nl = true)
{
$this->getOutput()->out($text, $nl);
return $this;
}
/**
* Get a value from standard input.
*
* @return string The input string from standard input.
*
* @codeCoverageIgnore
* @since 4.0.0
*/
public function in()
{
return $this->getCliInput()->in();
}
/**
* Set an output object.
*
* @param CliOutput $output CliOutput object
*
* @return $this
*
* @since 3.3
*/
public function setOutput(CliOutput $output)
{
$this->output = $output;
return $this;
}
/**
* Enqueue a system message.
*
* @param string $msg The message to enqueue.
* @param string $type The message type.
*
* @return void
*
* @since 4.0.0
*/
public function enqueueMessage($msg, $type = self::MSG_INFO)
{
if (!\array_key_exists($type, $this->messages)) {
$this->messages[$type] = [];
}
$this->messages[$type][] = $msg;
}
/**
* Get the system message queue.
*
* @return array The system message queue.
*
* @since 4.0.0
*/
public function getMessageQueue()
{
return $this->messages;
}
/**
* Check the client interface by name.
*
* @param string $identifier String identifier for the application interface
*
* @return boolean True if this application is of the given type client interface.
*
* @since 4.0.0
*/
public function isClient($identifier)
{
return $identifier === 'cli';
}
/**
* Method to get the application session object.
*
* @return SessionInterface The session object
*
* @since 4.0.0
*/
public function getSession()
{
return $this->container->get(SessionInterface::class);
}
/**
* Retrieve the application configuration object.
*
* @return Registry
*
* @since 4.0.0
*/
public function getConfig()
{
return $this->config;
}
/**
* Flag if the application instance is a CLI or web based application.
*
* Helper function, you should use the native PHP functions to detect if it is a CLI application.
*
* @return boolean
*
* @since 4.0.0
* @deprecated 4.0 will be removed in 6.0
* Will be removed without replacements
*/
public function isCli()
{
return true;
}
}

View File

@ -0,0 +1,635 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\CMS\Console;
use Joomla\CMS\Extension\ExtensionManagerTrait;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Router;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Version;
use Joomla\Console\Application;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\DI\Container;
use Joomla\DI\ContainerAwareTrait;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Input\Input;
use Joomla\Registry\Registry;
use Joomla\Session\SessionInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The Joomla! CMS Console Application
*
* @since 4.0.0
*/
class ConsoleApplication extends Application implements DispatcherAwareInterface, CMSApplicationInterface
{
use DispatcherAwareTrait;
use EventAware;
use IdentityAware;
use ContainerAwareTrait;
use ExtensionManagerTrait;
use ExtensionNamespaceMapper;
use DatabaseAwareTrait;
/**
* The input.
*
* @var Input
* @since 4.0.0
*/
protected $input = null;
/**
* The name of the application.
*
* @var string
* @since 4.0.0
*/
protected $name = null;
/**
* The application language object.
*
* @var Language
* @since 4.0.0
*/
protected $language;
/**
* The application message queue.
*
* @var array
* @since 4.0.0
*/
private $messages = [];
/**
* The application session object.
*
* @var SessionInterface
* @since 4.0.0
*/
private $session;
/**
* Class constructor.
*
* @param Registry $config An optional argument to provide dependency injection for the application's config object. If the
* argument is a Registry object that object will become the application's config object,
* otherwise a default config object is created.
* @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's event dispatcher. If the
* argument is a DispatcherInterface object that object will become the application's event dispatcher,
* if it is null then the default event dispatcher will be created based on the application's
* loadDispatcher() method.
* @param Container $container Dependency injection container.
* @param Language $language The language object provisioned for the application.
* @param InputInterface|null $input An optional argument to provide dependency injection for the application's input object. If the
* argument is an InputInterface object that object will become the application's input object,
* otherwise a default input object is created.
* @param OutputInterface|null $output An optional argument to provide dependency injection for the application's output object. If the
* argument is an OutputInterface object that object will become the application's output object,
* otherwise a default output object is created.
*
* @since 4.0.0
*/
public function __construct(
Registry $config,
DispatcherInterface $dispatcher,
Container $container,
Language $language,
?InputInterface $input = null,
?OutputInterface $output = null
) {
// Close the application if it is not executed from the command line.
if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) {
$this->close();
}
// Set up a Input object for Controllers etc to use
$this->input = new \Joomla\CMS\Input\Cli();
$this->language = $language;
parent::__construct($input, $output, $config);
$this->setVersion(JVERSION);
// Register the client name as cli
$this->name = 'cli';
$this->setContainer($container);
$this->setDispatcher($dispatcher);
// Set the execution datetime and timestamp;
$this->set('execution.datetime', gmdate('Y-m-d H:i:s'));
$this->set('execution.timestamp', time());
$this->set('execution.microtimestamp', microtime(true));
// Set the current directory.
$this->set('cwd', getcwd());
// Set up the environment
$this->input->set('format', 'cli');
}
/**
* Magic method to access properties of the application.
*
* @param string $name The name of the property.
*
* @return mixed A value if the property name is valid, null otherwise.
*
* @since 4.0.0
*
* @deprecated 4.0 will be removed in 6.0
* This is a B/C proxy for deprecated read accesses, use getInput() method instead
* Example:
* $app->getInput();
*/
public function __get($name)
{
switch ($name) {
case 'input':
@trigger_error(
'Accessing the input property of the application is deprecated, use the getInput() method instead.',
E_USER_DEPRECATED
);
return $this->getInput();
default:
$trace = debug_backtrace();
trigger_error(
sprintf(
'Undefined property via __get(): %1$s in %2$s on line %3$s',
$name,
$trace[0]['file'],
$trace[0]['line']
),
E_USER_NOTICE
);
}
}
/**
* Method to run the application routines.
*
* @return integer The exit code for the application
*
* @since 4.0.0
* @throws \Throwable
*/
protected function doExecute(): int
{
$exitCode = parent::doExecute();
$style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput());
$methodMap = [
self::MSG_ALERT => 'error',
self::MSG_CRITICAL => 'caution',
self::MSG_DEBUG => 'comment',
self::MSG_EMERGENCY => 'caution',
self::MSG_ERROR => 'error',
self::MSG_INFO => 'note',
self::MSG_NOTICE => 'note',
self::MSG_WARNING => 'warning',
];
// Output any enqueued messages before the app exits
foreach ($this->getMessageQueue() as $type => $messages) {
$method = $methodMap[$type] ?? 'comment';
$style->$method($messages);
}
return $exitCode;
}
/**
* Execute the application.
*
* @return void
*
* @since 4.0.0
* @throws \Throwable
*/
public function execute()
{
// Load extension namespaces
$this->createExtensionNamespaceMap();
/**
* Address issues with instantiating WebApplication descendants under CLI.
*
* IMPORTANT! This code must be always be executed **before** the first use of
* PluginHelper::importPlugin(). Some plugins will attempt to register an MVCFactory for a
* component in their service provider. This will in turn try to get the SiteRouter service
* for the component which tries to get an instance of SiteApplication which will fail with
* a RuntimeException if the populateHttpHost() method has not already executed.
*/
$this->populateHttpHost();
// Import CMS plugin groups to be able to subscribe to events
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
PluginHelper::importPlugin('console', null, true, $this->getDispatcher());
parent::execute();
}
/**
* Enqueue a system message.
*
* @param string $msg The message to enqueue.
* @param string $type The message type.
*
* @return void
*
* @since 4.0.0
*/
public function enqueueMessage($msg, $type = self::MSG_INFO)
{
if (!\array_key_exists($type, $this->messages)) {
$this->messages[$type] = [];
}
$this->messages[$type][] = $msg;
}
/**
* Gets the name of the current running application.
*
* @return string The name of the application.
*
* @since 4.0.0
*/
public function getName(): string
{
return $this->name;
}
/**
* Get the commands which should be registered by default to the application.
*
* @return \Joomla\Console\Command\AbstractCommand[]
*
* @since 4.0.0
*/
protected function getDefaultCommands(): array
{
return array_merge(
parent::getDefaultCommands(),
[
new Console\CleanCacheCommand(),
new Console\CheckUpdatesCommand(),
new Console\RemoveOldFilesCommand(),
new Console\AddUserCommand($this->getDatabase()),
new Console\AddUserToGroupCommand($this->getDatabase()),
new Console\RemoveUserFromGroupCommand($this->getDatabase()),
new Console\DeleteUserCommand($this->getDatabase()),
new Console\ChangeUserPasswordCommand(),
new Console\ListUserCommand($this->getDatabase()),
new Console\SiteCreatePublicFolderCommand(),
]
);
}
/**
* Retrieve the application configuration object.
*
* @return Registry
*
* @since 4.0.0
*/
public function getConfig()
{
return $this->config;
}
/**
* Method to get the application input object.
*
* @return Input
*
* @since 4.0.0
*/
public function getInput(): Input
{
return $this->input;
}
/**
* Method to get the application language object.
*
* @return Language The language object
*
* @since 4.0.0
*/
public function getLanguage()
{
return $this->language;
}
/**
* Get the system message queue.
*
* @return array The system message queue.
*
* @since 4.0.0
*/
public function getMessageQueue()
{
return $this->messages;
}
/**
* Method to get the application session object.
*
* @return SessionInterface The session object
*
* @since 4.0.0
*/
public function getSession()
{
return $this->session;
}
/**
* Check the client interface by name.
*
* @param string $identifier String identifier for the application interface
*
* @return boolean True if this application is of the given type client interface.
*
* @since 4.0.0
*/
public function isClient($identifier)
{
return $this->getName() === $identifier;
}
/**
* Flag if the application instance is a CLI or web based application.
*
* Helper function, you should use the native PHP functions to detect if it is a CLI application.
*
* @return boolean
*
* @since 4.0.0
*
* @deprecated 4.0 will be removed in 6.0
* Will be removed without replacement. CLI will be handled by the joomla/console package instead
*/
public function isCli()
{
return true;
}
/**
* Sets the session for the application to use, if required.
*
* @param SessionInterface $session A session object.
*
* @return $this
*
* @since 4.0.0
*/
public function setSession(SessionInterface $session): self
{
$this->session = $session;
return $this;
}
/**
* Flush the media version to refresh versionable assets
*
* @return void
*
* @since 4.0.0
*/
public function flushAssets()
{
(new Version())->refreshMediaVersion();
}
/**
* Get the long version string for the application.
*
* Overrides the parent method due to conflicting use of the getName method between the console application and
* the CMS application interface.
*
* @return string
*
* @since 4.0.0
*/
public function getLongVersion(): string
{
return sprintf('Joomla! <info>%s</info> (debug: %s)', (new Version())->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No'));
}
/**
* Set the name of the application.
*
* @param string $name The new application name.
*
* @return void
*
* @since 4.0.0
* @throws \RuntimeException because the application name cannot be changed
*/
public function setName(string $name): void
{
throw new \RuntimeException('The console application name cannot be changed');
}
/**
* Returns the application Router object.
*
* @param string $name The name of the application.
* @param array $options An optional associative array of configuration settings.
*
* @return Router
*
* @since 4.0.6
*
* @throws \InvalidArgumentException
*
* @deprecated 4.3 will be removed in 6.0
* Inject the router or load it from the dependency injection container
* Example: Factory::getContainer()->get(ApiRouter::class);
*/
public static function getRouter($name = null, array $options = [])
{
if (empty($name)) {
throw new \InvalidArgumentException('A router name must be set in console application.');
}
$options['mode'] = Factory::getApplication()->get('sef');
return Router::getInstance($name, $options);
}
/**
* Populates the HTTP_HOST and REQUEST_URI from the URL provided in the --live-site parameter.
*
* If the URL provided is empty or invalid we will use the URL
* https://joomla.invalid/set/by/console/application just so that the CLI application doesn't
* crash when a WebApplication descendant is instantiated in it.
*
* This is a practical workaround for using any service depending on a WebApplication
* descendant under CLI.
*
* Practical example: using a component's MVCFactory which instantiates the SiteRouter
* service for that component which in turn relies on an instance of SiteApplication.
*
* @return void
* @since 4.2.1
* @link https://github.com/joomla/joomla-cms/issues/38518
*/
protected function populateHttpHost()
{
// First check for the --live-site command line option.
$input = $this->getConsoleInput();
$liveSite = '';
if ($input->hasParameterOption(['--live-site', false])) {
$liveSite = $input->getParameterOption(['--live-site'], '');
}
// Fallback to the $live_site global configuration option in configuration.php
$liveSite = $liveSite ?: $this->get('live_site', 'https://joomla.invalid/set/by/console/application');
/**
* Try to use the live site URL we were given. If all else fails, fall back to
* https://joomla.invalid/set/by/console/application.
*/
try {
$uri = Uri::getInstance($liveSite);
} catch (\RuntimeException $e) {
$uri = Uri::getInstance('https://joomla.invalid/set/by/console/application');
}
/**
* Yes, this is icky but it is the only way to trick WebApplication into compliance.
*
* @see \Joomla\Application\AbstractWebApplication::detectRequestUri
*/
$_SERVER['HTTP_HOST'] = $uri->toString(['host', 'port']);
$_SERVER['REQUEST_URI'] = $uri->getPath();
$_SERVER['HTTPS'] = $uri->getScheme() === 'https' ? 'on' : 'off';
}
/**
* Builds the default input definition.
*
* @return InputDefinition
*
* @since 4.2.1
*/
protected function getDefaultInputDefinition(): InputDefinition
{
return new InputDefinition(
[
new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
new InputOption(
'--live-site',
null,
InputOption::VALUE_OPTIONAL,
'The URL to your site, e.g. https://www.example.com'
),
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display the help information'),
new InputOption(
'--quiet',
'-q',
InputOption::VALUE_NONE,
'Flag indicating that all output should be silenced'
),
new InputOption(
'--verbose',
'-v|vv|vvv',
InputOption::VALUE_NONE,
'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'
),
new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Displays the application version'),
new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
new InputOption(
'--no-interaction',
'-n',
InputOption::VALUE_NONE,
'Flag to disable interacting with the user'
),
]
);
}
/**
* Gets a user state.
*
* @param string $key The path of the state.
* @param mixed $default Optional default value, returned if the internal value is null.
*
* @return mixed The user state or null.
*
* @since 4.4.0
*/
public function getUserState($key, $default = null)
{
$registry = $this->getSession()->get('registry');
if ($registry !== null) {
return $registry->get($key, $default);
}
return $default;
}
/**
* Gets the value of a user state variable.
*
* @param string $key The key of the user state variable.
* @param string $request The name of the variable passed in a request.
* @param string $default The default value for the variable if not found. Optional.
* @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional.
*
* @return mixed The request user state.
*
* @since 4.4.0
*/
public function getUserStateFromRequest($key, $request, $default = null, $type = 'none')
{
$cur_state = $this->getUserState($key, $default);
$new_state = $this->input->get($request, null, $type);
if ($new_state === null) {
return $cur_state;
}
// Save the new value only if it was set in this request.
$this->setUserState($key, $new_state);
return $new_state;
}
}

View File

@ -0,0 +1,864 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\CMS\Event\Application\AfterExecuteEvent;
use Joomla\CMS\Event\Application\BeforeExecuteEvent;
use Joomla\CMS\Event\Application\DaemonForkEvent;
use Joomla\CMS\Event\Application\DaemonReceiveSignalEvent;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Input\Cli;
use Joomla\CMS\Log\Log;
use Joomla\Event\DispatcherInterface;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Class to turn CliApplication applications into daemons. It requires CLI and PCNTL support built into PHP.
*
* @link https://www.php.net/manual/en/book.pcntl.php
* @link https://www.php.net/manual/en/features.commandline.php
* @since 1.7.0
*/
abstract class DaemonApplication extends CliApplication
{
/**
* @var array The available POSIX signals to be caught by default.
* @link https://www.php.net/manual/pcntl.constants.php
* @since 1.7.0
*/
protected static $signals = [
'SIGHUP',
'SIGINT',
'SIGQUIT',
'SIGILL',
'SIGTRAP',
'SIGABRT',
'SIGIOT',
'SIGBUS',
'SIGFPE',
'SIGUSR1',
'SIGSEGV',
'SIGUSR2',
'SIGPIPE',
'SIGALRM',
'SIGTERM',
'SIGSTKFLT',
'SIGCLD',
'SIGCHLD',
'SIGCONT',
'SIGTSTP',
'SIGTTIN',
'SIGTTOU',
'SIGURG',
'SIGXCPU',
'SIGXFSZ',
'SIGVTALRM',
'SIGPROF',
'SIGWINCH',
'SIGPOLL',
'SIGIO',
'SIGPWR',
'SIGSYS',
'SIGBABY',
'SIG_BLOCK',
'SIG_UNBLOCK',
'SIG_SETMASK',
];
/**
* @var boolean True if the daemon is in the process of exiting.
* @since 1.7.0
*/
protected $exiting = false;
/**
* @var integer The parent process id.
* @since 3.0.0
*/
protected $parentId = 0;
/**
* @var integer The process id of the daemon.
* @since 1.7.0
*/
protected $processId = 0;
/**
* @var boolean True if the daemon is currently running.
* @since 1.7.0
*/
protected $running = false;
/**
* Class constructor.
*
* @param ?Cli $input An optional argument to provide dependency injection for the application's
* input object. If the argument is a JInputCli object that object will become
* the application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's
* config object. If the argument is a Registry object that object will become
* the application's config object, otherwise a default config object is created.
* @param ?DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's
* event dispatcher. If the argument is a DispatcherInterface object that object will become
* the application's event dispatcher, if it is null then the default event dispatcher
* will be created based on the application's loadDispatcher() method.
*
* @since 1.7.0
*/
public function __construct(Cli $input = null, Registry $config = null, DispatcherInterface $dispatcher = null)
{
// Verify that the process control extension for PHP is available.
if (!\defined('SIGHUP')) {
Log::add('The PCNTL extension for PHP is not available.', Log::ERROR);
throw new \RuntimeException('The PCNTL extension for PHP is not available.');
}
// Verify that POSIX support for PHP is available.
if (!\function_exists('posix_getpid')) {
Log::add('The POSIX extension for PHP is not available.', Log::ERROR);
throw new \RuntimeException('The POSIX extension for PHP is not available.');
}
// Call the parent constructor.
parent::__construct($input, $config, null, null, $dispatcher);
// Set some system limits.
if (\function_exists('set_time_limit')) {
set_time_limit($this->config->get('max_execution_time', 0));
}
if ($this->config->get('max_memory_limit') !== null) {
ini_set('memory_limit', $this->config->get('max_memory_limit', '256M'));
}
// Flush content immediately.
ob_implicit_flush();
}
/**
* Method to handle POSIX signals.
*
* @param integer $signal The received POSIX signal.
*
* @return void
*
* @since 1.7.0
* @see pcntl_signal()
* @throws \RuntimeException
*/
public static function signal($signal)
{
// Log all signals sent to the daemon.
Log::add('Received signal: ' . $signal, Log::DEBUG);
// Let's make sure we have an application instance.
if (!is_subclass_of(static::$instance, CliApplication::class)) {
Log::add('Cannot find the application instance.', Log::EMERGENCY);
throw new \RuntimeException('Cannot find the application instance.');
}
// Fire the onReceiveSignal event.
static::$instance->getDispatcher()->dispatch(
'onReceiveSignal',
new DaemonReceiveSignalEvent('onReceiveSignal', [
'signal' => $signal,
'subject' => static::$instance,
])
);
switch ($signal) {
case SIGINT:
case SIGTERM:
// Handle shutdown tasks
if (static::$instance->running && static::$instance->isActive()) {
static::$instance->shutdown();
} else {
static::$instance->close();
}
break;
case SIGHUP:
// Handle restart tasks
if (static::$instance->running && static::$instance->isActive()) {
static::$instance->shutdown(true);
} else {
static::$instance->close();
}
break;
case SIGCHLD:
// A child process has died
while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) {
usleep(1000);
}
break;
case SIGCLD:
while (static::$instance->pcntlWait($signal, WNOHANG) > 0) {
$signal = static::$instance->pcntlChildExitStatus($signal);
}
break;
default:
break;
}
}
/**
* Check to see if the daemon is active. This does not assume that $this daemon is active, but
* only if an instance of the application is active as a daemon.
*
* @return boolean True if daemon is active.
*
* @since 1.7.0
*/
public function isActive()
{
// Get the process id file location for the application.
$pidFile = $this->config->get('application_pid_file');
// If the process id file doesn't exist then the daemon is obviously not running.
if (!is_file($pidFile)) {
return false;
}
// Read the contents of the process id file as an integer.
$fp = fopen($pidFile, 'r');
$pid = fread($fp, filesize($pidFile));
$pid = (int) $pid;
fclose($fp);
// Check to make sure that the process id exists as a positive integer.
if (!$pid) {
return false;
}
// Check to make sure the process is active by pinging it and ensure it responds.
if (!posix_kill($pid, 0)) {
// No response so remove the process id file and log the situation.
@ unlink($pidFile);
Log::add('The process found based on PID file was unresponsive.', Log::WARNING);
return false;
}
return true;
}
/**
* Load an object or array into the application configuration object.
*
* @param mixed $data Either an array or object to be loaded into the configuration object.
*
* @return DaemonApplication Instance of $this to allow chaining.
*
* @since 1.7.0
*/
public function loadConfiguration($data)
{
/*
* Setup some application metadata options. This is useful if we ever want to write out startup scripts
* or just have some sort of information available to share about things.
*/
// The application author name. This string is used in generating startup scripts and has
// a maximum of 50 characters.
$tmp = (string) $this->config->get('author_name', 'Joomla Platform');
$this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp);
// The application author email. This string is used in generating startup scripts.
$tmp = (string) $this->config->get('author_email', 'admin@joomla.org');
$this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL));
// The application name. This string is used in generating startup scripts.
$tmp = (string) $this->config->get('application_name', 'DaemonApplication');
$this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp));
// The application description. This string is used in generating startup scripts.
$tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.');
$this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING));
/*
* Setup the application path options. This defines the default executable name, executable directory,
* and also the path to the daemon process id file.
*/
// The application executable daemon. This string is used in generating startup scripts.
$tmp = (string) $this->config->get('application_executable', basename($this->input->executable));
$this->config->set('application_executable', $tmp);
// The home directory of the daemon.
$tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable));
$this->config->set('application_directory', $tmp);
// The pid file location. This defaults to a path inside the /tmp directory.
$name = $this->config->get('application_name');
$tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid'));
$this->config->set('application_pid_file', $tmp);
/*
* Setup the application identity options. It is important to remember if the default of 0 is set for
* either UID or GID then changing that setting will not be attempted as there is no real way to "change"
* the identity of a process from some user to root.
*/
// The user id under which to run the daemon.
$tmp = (int) $this->config->get('application_uid', 0);
$options = ['options' => ['min_range' => 0, 'max_range' => 65000]];
$this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options));
// The group id under which to run the daemon.
$tmp = (int) $this->config->get('application_gid', 0);
$options = ['options' => ['min_range' => 0, 'max_range' => 65000]];
$this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options));
// Option to kill the daemon if it cannot switch to the chosen identity.
$tmp = (bool) $this->config->get('application_require_identity', 1);
$this->config->set('application_require_identity', $tmp);
/*
* Setup the application runtime options. By default our execution time limit is infinite obviously
* because a daemon should be constantly running unless told otherwise. The default limit for memory
* usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory
* management leaves a bit to be desired :-)
*/
// The maximum execution time of the application in seconds. Zero is infinite.
$tmp = $this->config->get('max_execution_time');
if ($tmp !== null) {
$this->config->set('max_execution_time', (int) $tmp);
}
// The maximum amount of memory the application can use.
$tmp = $this->config->get('max_memory_limit', '256M');
if ($tmp !== null) {
$this->config->set('max_memory_limit', (string) $tmp);
}
return $this;
}
/**
* Execute the daemon.
*
* @return void
*
* @since 1.7.0
*/
public function execute()
{
// Trigger the onBeforeExecute event
$this->dispatchEvent(
'onBeforeExecute',
new BeforeExecuteEvent('onBeforeExecute', ['subject' => $this, 'container' => $this->getContainer()])
);
// Enable basic garbage collection.
gc_enable();
Log::add('Starting ' . $this->name, Log::INFO);
// Set off the process for becoming a daemon.
if ($this->daemonize()) {
// Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor
// incoming signals after each tick and call the relevant signal handler automatically.
declare(ticks=1);
// Start the main execution loop.
while (true) {
// Perform basic garbage collection.
$this->gc();
// Don't completely overload the CPU.
usleep(1000);
// Execute the main application logic.
$this->doExecute();
}
} else {
// We were not able to daemonize the application so log the failure and die gracefully.
Log::add('Starting ' . $this->name . ' failed', Log::INFO);
}
// Trigger the onAfterExecute event.
$this->dispatchEvent(
'onAfterExecute',
new AfterExecuteEvent('onAfterExecute', ['subject' => $this])
);
}
/**
* Restart daemon process.
*
* @return void
*
* @since 1.7.0
*/
public function restart()
{
Log::add('Stopping ' . $this->name, Log::INFO);
$this->shutdown(true);
}
/**
* Stop daemon process.
*
* @return void
*
* @since 1.7.0
*/
public function stop()
{
Log::add('Stopping ' . $this->name, Log::INFO);
$this->shutdown();
}
/**
* Method to change the identity of the daemon process and resources.
*
* @return boolean True if identity successfully changed
*
* @since 1.7.0
* @see posix_setuid()
*/
protected function changeIdentity()
{
// Get the group and user ids to set for the daemon.
$uid = (int) $this->config->get('application_uid', 0);
$gid = (int) $this->config->get('application_gid', 0);
// Get the application process id file path.
$file = $this->config->get('application_pid_file');
// Change the user id for the process id file if necessary.
if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) {
Log::add('Unable to change user ownership of the process id file.', Log::ERROR);
return false;
}
// Change the group id for the process id file if necessary.
if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) {
Log::add('Unable to change group ownership of the process id file.', Log::ERROR);
return false;
}
// Set the correct home directory for the process.
if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) {
system('export HOME="' . $info['dir'] . '"');
}
// Change the user id for the process necessary.
if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) {
Log::add('Unable to change user ownership of the process.', Log::ERROR);
return false;
}
// Change the group id for the process necessary.
if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) {
Log::add('Unable to change group ownership of the process.', Log::ERROR);
return false;
}
// Get the user and group information based on uid and gid.
$user = posix_getpwuid($uid);
$group = posix_getgrgid($gid);
Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO);
return true;
}
/**
* Method to put the application into the background.
*
* @return boolean
*
* @since 1.7.0
* @throws \RuntimeException
*/
protected function daemonize()
{
// Is there already an active daemon running?
if ($this->isActive()) {
Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY);
return false;
}
// Reset Process Information
$this->processId = 0;
$this->running = false;
// Detach process!
try {
// Check if we should run in the foreground.
if (!$this->input->get('f')) {
// Detach from the terminal.
$this->detach();
} else {
// Setup running values.
$this->exiting = false;
$this->running = true;
// Set the process id.
$this->processId = (int) posix_getpid();
$this->parentId = $this->processId;
}
} catch (\RuntimeException $e) {
Log::add('Unable to fork.', Log::EMERGENCY);
return false;
}
// Verify the process id is valid.
if ($this->processId < 1) {
Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY);
return false;
}
// Clear the umask.
@ umask(0);
// Write out the process id file for concurrency management.
if (!$this->writeProcessIdFile()) {
Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY);
return false;
}
// Attempt to change the identity of user running the process.
if (!$this->changeIdentity()) {
// If the identity change was required then we need to return false.
if ($this->config->get('application_require_identity')) {
Log::add('Unable to change process owner.', Log::CRITICAL);
return false;
}
Log::add('Unable to change process owner.', Log::WARNING);
}
// Setup the signal handlers for the daemon.
if (!$this->setupSignalHandlers()) {
return false;
}
// Change the current working directory to the application working directory.
@ chdir($this->config->get('application_directory'));
return true;
}
/**
* This is truly where the magic happens. This is where we fork the process and kill the parent
* process, which is essentially what turns the application into a daemon.
*
* @return void
*
* @since 3.0.0
* @throws \RuntimeException
*/
protected function detach()
{
Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG);
// Attempt to fork the process.
$pid = $this->fork();
// If the pid is positive then we successfully forked, and can close this application.
if ($pid) {
// Add the log entry for debugging purposes and exit gracefully.
Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG);
$this->close();
} else {
// We are in the forked child process.
// Setup some protected values.
$this->exiting = false;
$this->running = true;
// Set the parent to self.
$this->parentId = $this->processId;
}
}
/**
* Method to fork the process.
*
* @return integer The child process id to the parent process, zero to the child process.
*
* @since 1.7.0
* @throws \RuntimeException
*/
protected function fork()
{
// Attempt to fork the process.
$pid = $this->pcntlFork();
// If the fork failed, throw an exception.
if ($pid === -1) {
throw new \RuntimeException('The process could not be forked.');
}
if ($pid === 0) {
// Update the process id for the child.
$this->processId = (int) posix_getpid();
} else {
// Log the fork in the parent.
// Log the fork.
Log::add('Process forked ' . $pid, Log::DEBUG);
}
// Trigger the onFork event.
$this->postFork();
return $pid;
}
/**
* Method to perform basic garbage collection and memory management in the sense of clearing the
* stat cache. We will probably call this method pretty regularly in our main loop.
*
* @return void
*
* @since 1.7.0
*/
protected function gc()
{
// Perform generic garbage collection.
gc_collect_cycles();
// Clear the stat cache so it doesn't blow up memory.
clearstatcache();
}
/**
* Method to attach the DaemonApplication signal handler to the known signals. Applications
* can override these handlers by using the pcntl_signal() function and attaching a different
* callback method.
*
* @return boolean
*
* @since 1.7.0
* @see pcntl_signal()
*/
protected function setupSignalHandlers()
{
// We add the error suppression for the loop because on some platforms some constants are not defined.
foreach (self::$signals as $signal) {
// Ignore signals that are not defined.
if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) {
// Define the signal to avoid notices.
Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG);
\define($signal, null);
// Don't listen for signal.
continue;
}
// Attach the signal handler for the signal.
if (!$this->pcntlSignal(\constant($signal), ['DaemonApplication', 'signal'])) {
Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY);
return false;
}
}
return true;
}
/**
* Method to shut down the daemon and optionally restart it.
*
* @param boolean $restart True to restart the daemon on exit.
*
* @return void
*
* @since 1.7.0
*/
protected function shutdown($restart = false)
{
// If we are already exiting, chill.
if ($this->exiting) {
return;
}
// If not, now we are.
$this->exiting = true;
// If we aren't already daemonized then just kill the application.
if (!$this->running && !$this->isActive()) {
Log::add('Process was not daemonized yet, just halting current process', Log::INFO);
$this->close();
}
// Only read the pid for the parent file.
if ($this->parentId == $this->processId) {
// Read the contents of the process id file as an integer.
$fp = fopen($this->config->get('application_pid_file'), 'r');
$pid = fread($fp, filesize($this->config->get('application_pid_file')));
$pid = (int) $pid;
fclose($fp);
// Remove the process id file.
@ unlink($this->config->get('application_pid_file'));
// If we are supposed to restart the daemon we need to execute the same command.
if ($restart) {
$this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &'));
} else {
// If we are not supposed to restart the daemon let's just kill -9.
passthru('kill -9 ' . $pid);
$this->close();
}
}
}
/**
* Method to write the process id file out to disk.
*
* @return boolean
*
* @since 1.7.0
*/
protected function writeProcessIdFile()
{
// Verify the process id is valid.
if ($this->processId < 1) {
Log::add('The process id is invalid.', Log::EMERGENCY);
return false;
}
// Get the application process id file path.
$file = $this->config->get('application_pid_file');
if (empty($file)) {
Log::add('The process id file path is empty.', Log::ERROR);
return false;
}
// Make sure that the folder where we are writing the process id file exists.
$folder = \dirname($file);
if (!is_dir($folder) && !Folder::create($folder)) {
Log::add('Unable to create directory: ' . $folder, Log::ERROR);
return false;
}
// Write the process id file out to disk.
if (!file_put_contents($file, $this->processId)) {
Log::add('Unable to write process id file: ' . $file, Log::ERROR);
return false;
}
// Make sure the permissions for the process id file are accurate.
if (!chmod($file, 0644)) {
Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR);
return false;
}
return true;
}
/**
* Method to handle post-fork triggering of the onFork event.
*
* @return void
*
* @since 3.0.0
*/
protected function postFork()
{
// Trigger the onFork event.
$this->dispatchEvent(
'onFork',
new DaemonForkEvent('onFork', ['subject' => $this])
);
}
/**
* Method to return the exit code of a terminated child process.
*
* @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid().
*
* @return integer The child process exit code.
*
* @see pcntl_wexitstatus()
* @since 1.7.3
*/
protected function pcntlChildExitStatus($status)
{
return pcntl_wexitstatus($status);
}
/**
* Method to return the exit code of a terminated child process.
*
* @return integer On success, the PID of the child process is returned in the parent's thread
* of execution, and a 0 is returned in the child's thread of execution. On
* failure, a -1 will be returned in the parent's context, no child process
* will be created, and a PHP error is raised.
*
* @see pcntl_fork()
* @since 1.7.3
*/
protected function pcntlFork()
{
return pcntl_fork();
}
/**
* Method to install a signal handler.
*
* @param integer $signal The signal number.
* @param callable $handler The signal handler which may be the name of a user created function,
* or method, or either of the two global constants SIG_IGN or SIG_DFL.
* @param boolean $restart Specifies whether system call restarting should be used when this
* signal arrives.
*
* @return boolean True on success.
*
* @see pcntl_signal()
* @since 1.7.3
*/
protected function pcntlSignal($signal, $handler, $restart = true)
{
return pcntl_signal($signal, $handler, $restart);
}
/**
* Method to wait on or return the status of a forked child.
*
* @param integer &$status Status information.
* @param integer $options If wait3 is available on your system (mostly BSD-style systems),
* you can provide the optional options parameter.
*
* @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG
* was provided as an option (on wait3-available systems) and no child was available.
*
* @see pcntl_wait()
* @since 1.7.3
*/
protected function pcntlWait(&$status, $options = 0)
{
return pcntl_wait($status, $options);
}
}

View File

@ -0,0 +1,116 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\CMS\Event\CoreEventAware;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Psr\Log\LoggerInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Trait for application classes which dispatch events
*
* @since 4.0.0
*/
trait EventAware
{
use CoreEventAware;
/**
* Get the event dispatcher.
*
* @return DispatcherInterface
*
* @since 4.0.0
* @throws \UnexpectedValueException May be thrown if the dispatcher has not been set.
*/
abstract public function getDispatcher();
/**
* Get the logger.
*
* @return LoggerInterface
*
* @since 4.0.0
*/
abstract public function getLogger();
/**
* Registers a handler to a particular event group.
*
* @param string $event The event name.
* @param callable $handler The handler, a function or an instance of an event object.
*
* @return $this
*
* @since 4.0.0
*/
public function registerEvent($event, callable $handler)
{
try {
$this->getDispatcher()->addListener($event, $handler);
} catch (\UnexpectedValueException $e) {
// No dispatcher is registered, don't throw an error (mimics old behavior)
}
return $this;
}
/**
* Calls all handlers associated with an event group.
*
* This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the
* Dispatcher and handle the returned EventInterface object instead of going through this method. This method is
* deprecated and will be removed in Joomla! 5.x.
*
* This method will only return the 'result' argument of the event
*
* @param string $eventName The event name.
* @param array|Event $args An array of arguments or an Event object (optional).
*
* @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set.
*
* @since 4.0.0
* @throws \InvalidArgumentException
*
* @deprecated 4.0 will be removed in 6.0
* Use the Dispatcher method instead
* Example: Factory::getApplication()->getDispatcher()->dispatch($eventName, $event);
*
*/
public function triggerEvent($eventName, $args = [])
{
try {
$dispatcher = $this->getDispatcher();
} catch (\UnexpectedValueException $exception) {
$this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this)));
return [];
}
if ($args instanceof Event) {
$event = $args;
} elseif (\is_array($args)) {
$className = self::getEventClassByEventName($eventName);
$event = new $className($eventName, $args);
} else {
throw new \InvalidArgumentException('The arguments must either be an event or an array');
}
$result = $dispatcher->dispatch($eventName, $event);
// @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place
return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result'];
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface defining application that can trigger Joomla 3.x style events
*
* @since 4.0.0
* @deprecated 4.3 will be removed in 6.0
* This interface will be removed without replacement as the Joomla 3.x compatibility layer will be removed
* @todo Move to compat plugin
*/
interface EventAwareInterface extends DispatcherAwareInterface
{
/**
* Get the event dispatcher.
*
* @return DispatcherInterface
*
* @since 4.0.0
* @throws \UnexpectedValueException May be thrown if the dispatcher has not been set.
*/
public function getDispatcher();
/**
* Calls all handlers associated with an event group.
*
* This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the
* Dispatcher and handle the returned EventInterface object instead of going through this method. This method is
* deprecated and will be removed in Joomla! 5.x.
*
* This method will only return the 'result' argument of the event
*
* @param string $eventName The event name.
* @param array|Event $args An array of arguments or an Event object (optional).
*
* @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set.
*
* @since 4.0.0
* @throws \InvalidArgumentException
*
* @deprecated 4.0 will be removed in 6.0
* Use the Dispatcher method instead
* Example: Factory::getApplication()->getDispatcher()->dispatch($eventName, $event);
*/
public function triggerEvent($eventName, $args = []);
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining a not acceptable class
*
* @since 4.0.0
*/
class NotAcceptable extends \RuntimeException
{
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Trait for application classes which ensures the namespace mapper exists and includes it.
*
* @since 4.0.0
*/
trait ExtensionNamespaceMapper
{
/**
* Allows the application to load a custom or default identity.
*
* @return void
*
* @since 4.0.0
*/
public function createExtensionNamespaceMap()
{
\JLoader::register('JNamespacePsr4Map', JPATH_LIBRARIES . '/namespacemap.php');
$extensionPsr4Loader = new \JNamespacePsr4Map();
$extensionPsr4Loader->load();
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryAwareTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Trait for application classes which are identity (user) aware
*
* @since 4.0.0
*/
trait IdentityAware
{
use UserFactoryAwareTrait;
/**
* The application identity object.
*
* @var User
* @since 4.0.0
*/
protected $identity;
/**
* Get the application identity.
*
* @return User
*
* @since 4.0.0
*/
public function getIdentity()
{
return $this->identity;
}
/**
* Allows the application to load a custom or default identity.
*
* @param User $identity An optional identity object. If omitted, a null user object is created.
*
* @return $this
*
* @since 4.0.0
*/
public function loadIdentity(User $identity = null)
{
$this->identity = $identity ?: $this->getUserFactory()->loadUserById(0);
return $this;
}
}

View File

@ -0,0 +1,520 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Encrypt\Aes;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Table\User as UserTable;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Table\MfaTable;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Implements the code required for integrating with Joomla's Multi-factor Authentication.
*
* Please keep in mind that Joomla's MFA, like any MFA method, is designed to be user-interactive.
* Moreover, it's meant to be used in an HTML- and JavaScript-aware execution environment i.e. a web
* browser, web view or similar.
*
* If your application is designed to work non-interactively (e.g. a JSON API application) or
* outside and HTML- and JavaScript-aware execution environments (e.g. CLI) you MUST NOT use this
* trait. Authentication should be either implicit (e.g. CLI) or using sufficiently secure non-
* interactive methods (tokens, certificates, ...).
*
* Regarding the Joomla CMS itself, only the SiteApplication (frontend) and AdministratorApplication
* (backend) applications use this trait because of this reason. The CLI application is implicitly
* authorised at the highest level, whereas the ApiApplication encourages the use of tokens for
* authentication.
*
* @since 4.2.0
*/
trait MultiFactorAuthenticationHandler
{
/**
* Handle the redirection to the Multi-factor Authentication captive login or setup page.
*
* @return boolean True if we are currently handling a Multi-factor Authentication captive page.
* @throws \Exception
* @since 4.2.0
*/
protected function isHandlingMultiFactorAuthentication(): bool
{
// Multi-factor Authentication checks take place only for logged in users.
try {
$user = $this->getIdentity();
} catch (\Exception $e) {
return false;
}
if (!($user instanceof User) || $user->guest) {
return false;
}
// If there is no need for a redirection I must not proceed
if (!$this->needsMultiFactorAuthenticationRedirection()) {
return false;
}
/**
* Automatically migrate from legacy MFA, if needed.
*
* We prefer to do a user-by-user migration instead of migrating everybody on Joomla update
* for practical reasons. On a site with hundreds or thousands of users the migration could
* take several minutes, causing Joomla Update to time out.
*
* Instead, every time we are in a captive Multi-factor Authentication page (captive MFA login
* or captive forced MFA setup) we spend a few milliseconds to check if a migration is
* necessary. If it's necessary, we perform it.
*
* The captive pages don't load any content or modules, therefore the few extra milliseconds
* we spend here are not a big deal. A failed all-users migration which would stop Joomla
* Update dead in its tracks would, however, be a big deal (broken sites). Moreover, a
* migration that has to be initiated by the site owner would also be a big deal — if they
* did not know they need to do it none of their users who had previously enabled MFA would
* now have it enabled!
*
* To paraphrase Otto von Bismarck: programming, like politics, is the art of the possible,
* the attainable -- the art of the next best.
*/
$this->migrateFromLegacyMFA();
// We only kick in when the user has actually set up MFA or must definitely enable MFA.
$userOptions = ComponentHelper::getParams('com_users');
$neverMFAUserGroups = $userOptions->get('neverMFAUserGroups', []);
$forceMFAUserGroups = $userOptions->get('forceMFAUserGroups', []);
$isMFADisallowed = \count(
array_intersect(
\is_array($neverMFAUserGroups) ? $neverMFAUserGroups : [],
$user->getAuthorisedGroups()
)
) >= 1;
$isMFAMandatory = \count(
array_intersect(
\is_array($forceMFAUserGroups) ? $forceMFAUserGroups : [],
$user->getAuthorisedGroups()
)
) >= 1;
$isMFADisallowed = $isMFADisallowed && !$isMFAMandatory;
$isMFAPending = $this->isMultiFactorAuthenticationPending();
$session = $this->getSession();
$isNonHtml = $this->input->getCmd('format', 'html') !== 'html';
// Prevent non-interactive (non-HTML) content from being loaded until MFA is validated.
if ($isMFAPending && $isNonHtml) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
if ($isMFAPending && !$isMFADisallowed) {
/**
* Saves the current URL as the return URL if all of the following conditions apply
* - It is not a URL to com_users' MFA feature itself
* - A return URL does not already exist, is imperfect or external to the site
*
* If no return URL has been set up and the current URL is com_users' MFA feature
* we will save the home page as the redirect target.
*/
$returnUrl = $session->get('com_users.return_url', '');
if (empty($returnUrl) || !Uri::isInternal($returnUrl)) {
$returnUrl = $this->isMultiFactorAuthenticationPage()
? Uri::base()
: Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']);
$session->set('com_users.return_url', $returnUrl);
}
// Redirect
$this->redirect(Route::_('index.php?option=com_users&view=captive', false), 307);
}
// If we're here someone just logged in but does not have MFA set up. Just flag him as logged in and continue.
$session->set('com_users.mfa_checked', 1);
// If the user is in a group that requires MFA we will redirect them to the setup page.
if (!$isMFAPending && $isMFAMandatory) {
// First unset the flag to make sure the redirection will apply until they conform to the mandatory MFA
$session->set('com_users.mfa_checked', 0);
// Now set a flag which forces rechecking MFA for this user
$session->set('com_users.mandatory_mfa_setup', 1);
// Then redirect them to the setup page
if (!$this->isMultiFactorAuthenticationPage()) {
$url = Route::_('index.php?option=com_users&view=methods', false);
$this->redirect($url, 307);
}
}
// Do I need to redirect the user to the MFA setup page after they have fully logged in?
$hasRejectedMultiFactorAuthenticationSetup = $this->hasRejectedMultiFactorAuthenticationSetup() && !$isMFAMandatory;
if (
!$isMFAPending && !$isMFADisallowed && ($userOptions->get('mfaredirectonlogin', 0) == 1)
&& !$user->guest && !$hasRejectedMultiFactorAuthenticationSetup && !empty(MfaHelper::getMfaMethods())
) {
$this->redirect(
$userOptions->get('mfaredirecturl', '') ?:
Route::_('index.php?option=com_users&view=methods&layout=firsttime', false)
);
}
return true;
}
/**
* Does the current user need to complete MFA authentication before being allowed to access the site?
*
* @return boolean
* @throws \Exception
* @since 4.2.0
*/
private function isMultiFactorAuthenticationPending(): bool
{
$user = $this->getIdentity();
if (empty($user) || $user->guest) {
return false;
}
// Get the user's MFA records
$records = MfaHelper::getUserMfaRecords($user->id);
// No MFA Methods? Then we obviously don't need to display a Captive login page.
if (\count($records) < 1) {
return false;
}
// Let's get a list of all currently active MFA Methods
$mfaMethods = MfaHelper::getMfaMethods();
// If no MFA Method is active we can't really display a Captive login page.
if (empty($mfaMethods)) {
return false;
}
// Get a list of just the Method names
$methodNames = [];
foreach ($mfaMethods as $mfaMethod) {
$methodNames[] = $mfaMethod['name'];
}
// Filter the records based on currently active MFA Methods
foreach ($records as $record) {
if (\in_array($record->method, $methodNames)) {
// We found an active Method. Show the Captive page.
return true;
}
}
// No viable MFA Method found. We won't show the Captive page.
return false;
}
/**
* Check whether we'll need to do a redirection to the Multi-factor Authentication captive page.
*
* @return boolean
* @since 4.2.0
*/
private function needsMultiFactorAuthenticationRedirection(): bool
{
$isAdmin = $this->isClient('administrator');
/**
* We only kick in if the session flag is not set AND the user is not flagged for monitoring of their MFA status
*
* In case a user belongs to a group which requires MFA to be always enabled and they logged in without having
* MFA enabled we have the recheck flag. This prevents the user from enabling and immediately disabling MFA,
* circumventing the requirement for MFA.
*/
$session = $this->getSession();
$isMFAComplete = $session->get('com_users.mfa_checked', 0) != 0;
$isMFASetupMandatory = $session->get('com_users.mandatory_mfa_setup', 0) != 0;
if ($isMFAComplete && !$isMFASetupMandatory) {
return false;
}
// Make sure we are logged in
try {
$user = $this->getIdentity();
} catch (\Exception $e) {
// This would happen if we are in CLI or under an old Joomla! version. Either case is not supported.
return false;
}
// The plugin only needs to kick in when you have logged in
if (empty($user) || $user->guest) {
return false;
}
// If we are in the administrator section we only kick in when the user has backend access privileges
if ($isAdmin && !$user->authorise('core.login.admin')) {
// @todo How exactly did you end up here if you didn't have the core.login.admin privilege to begin with?!
return false;
}
// Do not redirect if we are already in a MFA management or captive page
if ($this->isMultiFactorAuthenticationPage()) {
return false;
}
$option = strtolower($this->input->getCmd('option', ''));
$task = strtolower($this->input->getCmd('task', ''));
// Allow the frontend user to log out (in case they forgot their MFA code or something)
if (!$isAdmin && ($option == 'com_users') && \in_array($task, ['user.logout', 'user.menulogout'])) {
return false;
}
// Allow the backend user to log out (in case they forgot their MFA code or something)
if ($isAdmin && ($option == 'com_login') && ($task == 'logout')) {
return false;
}
// Allow the Joomla update finalisation to run
if ($isAdmin && $option === 'com_joomlaupdate' && \in_array($task, ['update.finalise', 'update.cleanup', 'update.finaliseconfirm'])) {
return false;
}
return true;
}
/**
* Is this a page concerning the Multi-factor Authentication feature?
*
* @param bool $onlyCaptive Should I only check for the MFA captive page?
*
* @return boolean
* @since 4.2.0
*/
public function isMultiFactorAuthenticationPage(bool $onlyCaptive = false): bool
{
$option = $this->input->get('option');
$task = $this->input->get('task');
$view = $this->input->get('view');
if ($option !== 'com_users') {
return false;
}
$allowedViews = ['captive', 'method', 'methods', 'callback'];
$allowedTasks = [
'captive.display', 'captive.captive', 'captive.validate',
'methods.display',
];
if (!$onlyCaptive) {
$allowedTasks = array_merge(
$allowedTasks,
[
'method.display', 'method.add', 'method.edit', 'method.regenerateBackupCodes',
'method.delete', 'method.save', 'methods.disable', 'methods.doNotShowThisAgain',
]
);
}
return \in_array($view, $allowedViews) || \in_array($task, $allowedTasks);
}
/**
* Does the user have a "don't show this again" flag?
*
* @return boolean
* @since 4.2.0
*/
private function hasRejectedMultiFactorAuthenticationSetup(): bool
{
$user = $this->getIdentity();
$profileKey = 'mfa.dontshow';
/** @var DatabaseInterface $db */
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select($db->quoteName('profile_value'))
->from($db->quoteName('#__user_profiles'))
->where($db->quoteName('user_id') . ' = :userId')
->where($db->quoteName('profile_key') . ' = :profileKey')
->bind(':userId', $user->id, ParameterType::INTEGER)
->bind(':profileKey', $profileKey);
try {
$result = $db->setQuery($query)->loadResult();
} catch (\Exception $e) {
$result = 1;
}
return $result == 1;
}
/**
* Automatically migrates a user's legacy MFA records into the new Captive MFA format.
*
* @return void
* @since 4.2.0
*/
private function migrateFromLegacyMFA(): void
{
$user = $this->getIdentity();
if (!($user instanceof User) || $user->guest || $user->id <= 0) {
return;
}
/** @var DatabaseInterface $db */
$db = Factory::getContainer()->get(DatabaseInterface::class);
$userTable = new UserTable($db);
if (!$userTable->load($user->id) || empty($userTable->otpKey)) {
return;
}
[$otpMethod, $otpKey] = explode(':', $userTable->otpKey, 2);
$secret = $this->get('secret');
$otpKey = $this->decryptLegacyTFAString($secret, $otpKey);
$otep = $this->decryptLegacyTFAString($secret, $userTable->otep);
$config = @json_decode($otpKey, true);
$hasConverted = true;
if (!empty($config)) {
switch ($otpMethod) {
case 'totp':
$this->getLanguage()->load('plg_multifactorauth_totp', JPATH_ADMINISTRATOR);
(new MfaTable($db))->save(
[
'user_id' => $user->id,
'title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'),
'method' => 'totp',
'default' => 0,
'created_on' => Date::getInstance()->toSql(),
'last_used' => null,
'tries' => 0,
'try_count' => null,
'options' => ['key' => $config['code']],
]
);
break;
case 'yubikey':
$this->getLanguage()->load('plg_multifactorauth_yubikey', JPATH_ADMINISTRATOR);
(new MfaTable($db))->save(
[
'user_id' => $user->id,
'title' => sprintf("%s %s", Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), $config['yubikey']),
'method' => 'yubikey',
'default' => 0,
'created_on' => Date::getInstance()->toSql(),
'last_used' => null,
'tries' => 0,
'try_count' => null,
'options' => ['id' => $config['yubikey']],
]
);
break;
default:
$hasConverted = false;
break;
}
}
// Convert the emergency codes
if ($hasConverted && !empty(@json_decode($otep, true))) {
// Delete any other record with the same user_id and Method.
$method = 'emergencycodes';
$userId = $user->id;
$query = $db->getQuery(true)
->delete($db->quoteName('#__user_mfa'))
->where($db->quoteName('user_id') . ' = :user_id')
->where($db->quoteName('method') . ' = :method')
->bind(':user_id', $userId, ParameterType::INTEGER)
->bind(':method', $method);
$db->setQuery($query)->execute();
// Migrate data
(new MfaTable($db))->save(
[
'user_id' => $user->id,
'title' => Text::_('COM_USERS_USER_BACKUPCODES'),
'method' => 'backupcodes',
'default' => 0,
'created_on' => Date::getInstance()->toSql(),
'last_used' => null,
'tries' => 0,
'try_count' => null,
'options' => @json_decode($otep, true),
]
);
}
// Remove the legacy MFA
$update = (object) [
'id' => $user->id,
'otpKey' => '',
'otep' => '',
];
$db->updateObject('#__users', $update, ['id']);
}
/**
* Tries to decrypt the legacy MFA configuration.
*
* @param string $secret Site's secret key
* @param string $stringToDecrypt Base64-encoded and encrypted, JSON-encoded information
*
* @return string Decrypted, but JSON-encoded, information
*
* @link https://github.com/joomla/joomla-cms/pull/12497
* @since 4.2.0
*/
private function decryptLegacyTFAString(string $secret, string $stringToDecrypt): string
{
// Is this already decrypted?
try {
$decrypted = @json_decode($stringToDecrypt, true);
} catch (\Exception $e) {
$decrypted = null;
}
if (!empty($decrypted)) {
return $stringToDecrypt;
}
// No, we need to decrypt the string
$aes = new Aes($secret, 256);
$decrypted = $aes->decryptString($stringToDecrypt);
if (!\is_string($decrypted) || empty($decrypted)) {
$aes->setPassword($secret, true);
$decrypted = $aes->decryptString($stringToDecrypt);
}
if (!\is_string($decrypted) || empty($decrypted)) {
return '';
}
// Remove the null padding added during encryption
return rtrim($decrypted, "\0");
}
}

View File

@ -0,0 +1,891 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\Web\WebClient;
use Joomla\CMS\Cache\CacheControllerFactoryAwareTrait;
use Joomla\CMS\Cache\Controller\OutputController;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Application\AfterDispatchEvent;
use Joomla\CMS\Event\Application\AfterInitialiseDocumentEvent;
use Joomla\CMS\Event\Application\AfterRouteEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Input\Input;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Pathway\Pathway;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Router\SiteRouter;
use Joomla\CMS\Uri\Uri;
use Joomla\DI\Container;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Site Application class
*
* @since 3.2
*/
final class SiteApplication extends CMSApplication
{
use CacheControllerFactoryAwareTrait;
use MultiFactorAuthenticationHandler;
/**
* Option to filter by language
*
* @var boolean
* @since 4.0.0
*/
protected $language_filter = false;
/**
* Option to detect language by the browser
*
* @var boolean
* @since 4.0.0
*/
protected $detect_browser = false;
/**
* The registered URL parameters.
*
* @var object
* @since 4.3.0
*/
public $registeredurlparams;
/**
* Class constructor.
*
* @param ?Input $input An optional argument to provide dependency injection for the application's input
* object. If the argument is a JInput object that object will become the
* application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's config
* object. If the argument is a Registry object that object will become the
* application's config object, otherwise a default config object is created.
* @param ?WebClient $client An optional argument to provide dependency injection for the application's client
* object. If the argument is a WebClient object that object will become the
* application's client object, otherwise a default client object is created.
* @param ?Container $container Dependency injection container.
*
* @since 3.2
*/
public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null)
{
// Register the application name
$this->name = 'site';
// Register the client ID
$this->clientId = 0;
// Execute the parent constructor
parent::__construct($input, $config, $client, $container);
}
/**
* Check if the user can access the application
*
* @param integer $itemid The item ID to check authorisation for
*
* @return void
*
* @since 3.2
*
* @throws \Exception When you are not authorised to view the home page menu item
*/
protected function authorise($itemid)
{
$menus = $this->getMenu();
$user = Factory::getUser();
if (!$menus->authorise($itemid)) {
if ($user->get('id') == 0) {
// Set the data
$this->setUserState('users.login.form.data', ['return' => Uri::getInstance()->toString()]);
$url = Route::_('index.php?option=com_users&view=login', false);
$this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error');
$this->redirect($url);
} else {
// Get the home page menu item
$home_item = $menus->getDefault($this->getLanguage()->getTag());
// If we are already in the homepage raise an exception
if ($menus->getActive()->id == $home_item->id) {
throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
// Otherwise redirect to the homepage and show an error
$this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error');
$this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false));
}
}
}
/**
* Dispatch the application
*
* @param string $component The component which is being rendered.
*
* @return void
*
* @since 3.2
*/
public function dispatch($component = null)
{
// Get the component if not set.
if (!$component) {
$component = $this->input->getCmd('option', null);
}
// Load the document to the API
$this->loadDocument();
// Set up the params
$document = $this->getDocument();
$params = $this->getParams();
// Register the document object with Factory
Factory::$document = $document;
switch ($document->getType()) {
case 'html':
// Set up the language
LanguageHelper::getLanguages('lang_code');
// Set metadata
$document->setMetaData('rights', $this->get('MetaRights'));
// Get the template
$template = $this->getTemplate(true);
// Store the template and its params to the config
$this->set('theme', $template->template);
$this->set('themeParams', $template->params);
// Add Asset registry files
$wr = $document->getWebAssetManager()->getRegistry();
if ($component) {
$wr->addExtensionRegistryFile($component);
}
if ($template->parent) {
$wr->addTemplateRegistryFile($template->parent, $this->getClientId());
}
$wr->addTemplateRegistryFile($template->template, $this->getClientId());
break;
case 'feed':
$document->setBase(htmlspecialchars(Uri::current()));
break;
}
$document->setTitle($params->get('page_title'));
$document->setDescription($params->get('page_description'));
// Add version number or not based on global configuration
if ($this->get('MetaVersion', 0)) {
$document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION);
} else {
$document->setGenerator('Joomla! - Open Source Content Management');
}
// Trigger the onAfterInitialiseDocument event.
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
$this->dispatchEvent(
'onAfterInitialiseDocument',
new AfterInitialiseDocumentEvent('onAfterInitialiseDocument', ['subject' => $this, 'document' => $document])
);
$contents = ComponentHelper::renderComponent($component);
$document->setBuffer($contents, ['type' => 'component']);
// Trigger the onAfterDispatch event.
$this->dispatchEvent(
'onAfterDispatch',
new AfterDispatchEvent('onAfterDispatch', ['subject' => $this])
);
}
/**
* Method to run the Web application routines.
*
* @return void
*
* @since 3.2
*/
protected function doExecute()
{
// Initialise the application
$this->initialiseApp();
// Mark afterInitialise in the profiler.
JDEBUG ? $this->profiler->mark('afterInitialise') : null;
// Route the application
$this->route();
// Mark afterRoute in the profiler.
JDEBUG ? $this->profiler->mark('afterRoute') : null;
if (!$this->isHandlingMultiFactorAuthentication()) {
/*
* Check if the user is required to reset their password
*
* Before $this->route(); "option" and "view" can't be safely read using:
* $this->input->getCmd('option'); or $this->input->getCmd('view');
* ex: due of the sef urls
*/
$this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout');
}
// Dispatch the application
$this->dispatch();
// Mark afterDispatch in the profiler.
JDEBUG ? $this->profiler->mark('afterDispatch') : null;
}
/**
* Return the current state of the detect browser option.
*
* @return boolean
*
* @since 3.2
*/
public function getDetectBrowser()
{
return $this->detect_browser;
}
/**
* Return the current state of the language filter.
*
* @return boolean
*
* @since 3.2
*/
public function getLanguageFilter()
{
return $this->language_filter;
}
/**
* Get the application parameters
*
* @param string $option The component option
*
* @return Registry The parameters object
*
* @since 3.2
*/
public function getParams($option = null)
{
static $params = [];
$hash = '__default';
if (!empty($option)) {
$hash = $option;
}
if (!isset($params[$hash])) {
// Get component parameters
if (!$option) {
$option = $this->input->getCmd('option', null);
}
// Get new instance of component global parameters
$params[$hash] = clone ComponentHelper::getParams($option);
// Get menu parameters
$menus = $this->getMenu();
$menu = $menus->getActive();
// Get language
$lang_code = $this->getLanguage()->getTag();
$languages = LanguageHelper::getLanguages('lang_code');
$title = $this->get('sitename');
if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) {
$description = $languages[$lang_code]->metadesc;
} else {
$description = $this->get('MetaDesc');
}
$rights = $this->get('MetaRights');
$robots = $this->get('robots');
// Retrieve com_menu global settings
$temp = clone ComponentHelper::getParams('com_menus');
// Lets cascade the parameters if we have menu item parameters
if (\is_object($menu)) {
// Get show_page_heading from com_menu global settings
$params[$hash]->def('show_page_heading', $temp->get('show_page_heading'));
$params[$hash]->merge($menu->getParams());
$title = $menu->title;
} else {
// Merge com_menu global settings
$params[$hash]->merge($temp);
// If supplied, use page title
$title = $temp->get('page_title', $title);
}
$params[$hash]->def('page_title', $title);
$params[$hash]->def('page_description', $description);
$params[$hash]->def('page_rights', $rights);
$params[$hash]->def('robots', $robots);
}
return $params[$hash];
}
/**
* Return a reference to the Pathway object.
*
* @param string $name The name of the application.
* @param array $options An optional associative array of configuration settings.
*
* @return Pathway A Pathway object
*
* @since 3.2
*/
public function getPathway($name = 'site', $options = [])
{
return parent::getPathway($name, $options);
}
/**
* Return a reference to the Router object.
*
* @param string $name The name of the application.
* @param array $options An optional associative array of configuration settings.
*
* @return \Joomla\CMS\Router\Router
*
* @since 3.2
*
* @deprecated 4.3 will be removed in 6.0
* Inject the router or load it from the dependency injection container
* Example: Factory::getContainer()->get(SiteRouter::class);
*/
public static function getRouter($name = 'site', array $options = [])
{
return parent::getRouter($name, $options);
}
/**
* Gets the name of the current template.
*
* @param boolean $params True to return the template parameters
*
* @return string|\stdClass The name of the template if the params argument is false. The template object if the params argument is true.
*
* @since 3.2
* @throws \InvalidArgumentException
*/
public function getTemplate($params = false)
{
if (\is_object($this->template)) {
if ($this->template->parent) {
if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) {
if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) {
throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template));
}
}
} elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) {
throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template));
}
if ($params) {
return $this->template;
}
return $this->template->template;
}
// Get the id of the active menu item
$menu = $this->getMenu();
$item = $menu->getActive();
if (!$item) {
$item = $menu->getItem($this->input->getInt('Itemid', null));
}
$id = 0;
if (\is_object($item)) {
// Valid item retrieved
$id = $item->template_style_id;
}
$tid = $this->input->getUint('templateStyle', 0);
if (is_numeric($tid) && (int) $tid > 0) {
$id = (int) $tid;
}
/** @var OutputController $cache */
$cache = $this->getCacheControllerFactory()->createCacheController('output', ['defaultgroup' => 'com_templates']);
if ($this->getLanguageFilter()) {
$tag = $this->getLanguage()->getTag();
} else {
$tag = '';
}
$cacheId = 'templates0' . $tag;
if ($cache->contains($cacheId)) {
$templates = $cache->get($cacheId);
} else {
$templates = $this->bootComponent('templates')->getMVCFactory()
->createModel('Style', 'Administrator')->getSiteTemplates();
foreach ($templates as &$template) {
// Create home element
if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) {
$template_home = clone $template;
}
$template->params = new Registry($template->params);
}
// Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later
unset($template);
// Add home element, after loop to avoid double execution
if (isset($template_home)) {
$template_home->params = new Registry($template_home->params);
$templates[0] = $template_home;
}
$cache->store($templates, $cacheId);
}
$template = $templates[$id] ?? $templates[0];
// Allows for overriding the active template from the request
$template_override = $this->input->getCmd('template', '');
// Only set template override if it is a valid template (= it exists and is enabled)
if (!empty($template_override)) {
if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) {
foreach ($templates as $tmpl) {
if ($tmpl->template === $template_override) {
$template = $tmpl;
break;
}
}
}
}
// Need to filter the default value as well
$template->template = InputFilter::getInstance()->clean($template->template, 'cmd');
// Fallback template
if (!empty($template->parent)) {
if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) {
if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) {
$this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error');
// Try to find data for 'cassiopeia' template
$original_tmpl = $template->template;
foreach ($templates as $tmpl) {
if ($tmpl->template === 'cassiopeia') {
$template = $tmpl;
break;
}
}
// Check, the data were found and if template really exists
if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) {
throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl));
}
}
}
} elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) {
$this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error');
// Try to find data for 'cassiopeia' template
$original_tmpl = $template->template;
foreach ($templates as $tmpl) {
if ($tmpl->template === 'cassiopeia') {
$template = $tmpl;
break;
}
}
// Check, the data were found and if template really exists
if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) {
throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl));
}
}
// Cache the result
$this->template = $template;
if ($params) {
return $template;
}
return $template->template;
}
/**
* Initialise the application.
*
* @param array $options An optional associative array of configuration settings.
*
* @return void
*
* @since 3.2
*/
protected function initialiseApp($options = [])
{
$user = Factory::getUser();
// If the user is a guest we populate it with the guest user group.
if ($user->guest) {
$guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1);
$user->groups = [$guestUsergroup];
}
if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) {
$pluginParams = new Registry($plugin->params);
$this->setLanguageFilter(true);
$this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1);
}
if (empty($options['language'])) {
// Detect the specified language
$lang = $this->input->getString('language', null);
// Make sure that the user's language exists
if ($lang && LanguageHelper::exists($lang)) {
$options['language'] = $lang;
}
}
if (empty($options['language']) && $this->getLanguageFilter()) {
// Detect cookie language
$lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string');
// Make sure that the user's language exists
if ($lang && LanguageHelper::exists($lang)) {
$options['language'] = $lang;
}
}
if (empty($options['language'])) {
// Detect user language
$lang = $user->getParam('language');
// Make sure that the user's language exists
if ($lang && LanguageHelper::exists($lang)) {
$options['language'] = $lang;
}
}
if (empty($options['language']) && $this->getDetectBrowser()) {
// Detect browser language
$lang = LanguageHelper::detectLanguage();
// Make sure that the user's language exists
if ($lang && LanguageHelper::exists($lang)) {
$options['language'] = $lang;
}
}
if (empty($options['language'])) {
// Detect default language
$params = ComponentHelper::getParams('com_languages');
$options['language'] = $params->get('site', $this->get('language', 'en-GB'));
}
// One last check to make sure we have something
if (!LanguageHelper::exists($options['language'])) {
$lang = $this->config->get('language', 'en-GB');
if (LanguageHelper::exists($lang)) {
$options['language'] = $lang;
} else {
// As a last ditch fail to english
$options['language'] = 'en-GB';
}
}
// Finish initialisation
parent::initialiseApp($options);
}
/**
* Load the library language files for the application
*
* @return void
*
* @since 3.6.3
*/
protected function loadLibraryLanguage()
{
/*
* Try the lib_joomla file in the current language (without allowing the loading of the file in the default language)
* Fallback to the default language if necessary
*/
$this->getLanguage()->load('lib_joomla', JPATH_SITE)
|| $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR);
}
/**
* Login authentication function
*
* @param array $credentials Array('username' => string, 'password' => string)
* @param array $options Array('remember' => boolean)
*
* @return boolean True on success.
*
* @since 3.2
*/
public function login($credentials, $options = [])
{
// Set the application login entry point
if (!\array_key_exists('entry_url', $options)) {
$options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login';
}
// Set the access control action to check.
$options['action'] = 'core.login.site';
return parent::login($credentials, $options);
}
/**
* Rendering is the process of pushing the document buffers into the template
* placeholders, retrieving data from the document and pushing it into
* the application response buffer.
*
* @return void
*
* @since 3.2
*/
protected function render()
{
switch ($this->document->getType()) {
case 'feed':
// No special processing for feeds
break;
case 'html':
default:
$template = $this->getTemplate(true);
$file = $this->input->get('tmpl', 'index');
if ($file === 'offline' && !$this->get('offline')) {
$this->set('themeFile', 'index.php');
}
if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) {
$this->setUserState('users.login.form.data', ['return' => Uri::getInstance()->toString()]);
$this->set('themeFile', 'offline.php');
$this->setHeader('Status', '503 Service Temporarily Unavailable', 'true');
}
if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) {
$this->set('themeFile', 'component.php');
}
// Ensure themeFile is set by now
if ($this->get('themeFile') == '') {
$this->set('themeFile', $file . '.php');
}
// Pass the parent template to the state
$this->set('themeInherits', $template->parent);
break;
}
parent::render();
}
/**
* Route the application.
*
* Routing is the process of examining the request environment to determine which
* component should receive the request. The component optional parameters
* are then set in the request object to be processed when the application is being
* dispatched.
*
* @return void
*
* @since 3.2
*/
protected function route()
{
// Get the full request URI.
$uri = clone Uri::getInstance();
// It is not possible to inject the SiteRouter as it requires a SiteApplication
// and we would end in an infinite loop
$result = $this->getContainer()->get(SiteRouter::class)->parse($uri, true);
$active = $this->getMenu()->getActive();
if (
$active !== null
&& $active->type === 'alias'
&& $active->getParams()->get('alias_redirect')
&& \in_array($this->input->getMethod(), ['GET', 'HEAD'], true)
) {
$item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions'));
if ($item !== null) {
$oldUri = clone Uri::getInstance();
if ($oldUri->getVar('Itemid') == $active->id) {
$oldUri->setVar('Itemid', $item->id);
}
$base = Uri::base(true);
$oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1));
$activePathPrefix = StringHelper::strtolower($active->route);
$position = strpos($oldPath, $activePathPrefix);
if ($position !== false) {
$oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix)));
$this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true);
$this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false);
$this->sendHeaders();
$this->redirect((string) $oldUri, 301);
}
}
}
foreach ($result as $key => $value) {
$this->input->def($key, $value);
}
// Trigger the onAfterRoute event.
PluginHelper::importPlugin('system', null, true, $this->getDispatcher());
$this->dispatchEvent(
'onAfterRoute',
new AfterRouteEvent('onAfterRoute', ['subject' => $this])
);
$Itemid = $this->input->getInt('Itemid', null);
$this->authorise($Itemid);
}
/**
* Set the current state of the detect browser option.
*
* @param boolean $state The new state of the detect browser option
*
* @return boolean The previous state
*
* @since 3.2
*/
public function setDetectBrowser($state = false)
{
$old = $this->getDetectBrowser();
$this->detect_browser = $state;
return $old;
}
/**
* Set the current state of the language filter.
*
* @param boolean $state The new state of the language filter
*
* @return boolean The previous state
*
* @since 3.2
*/
public function setLanguageFilter($state = false)
{
$old = $this->getLanguageFilter();
$this->language_filter = $state;
return $old;
}
/**
* Overrides the default template that would be used
*
* @param \stdClass|string $template The template name or definition
* @param mixed $styleParams The template style parameters
*
* @return void
*
* @since 3.2
*/
public function setTemplate($template, $styleParams = null)
{
if (\is_object($template)) {
$templateName = empty($template->template)
? ''
: $template->template;
$templateInheritable = empty($template->inheritable)
? 0
: $template->inheritable;
$templateParent = empty($template->parent)
? ''
: $template->parent;
$templateParams = empty($template->params)
? $styleParams
: $template->params;
} else {
$templateName = $template;
$templateInheritable = 0;
$templateParent = '';
$templateParams = $styleParams;
}
if (is_dir(JPATH_THEMES . '/' . $templateName)) {
$this->template = new \stdClass();
$this->template->template = $templateName;
if ($templateParams instanceof Registry) {
$this->template->params = $templateParams;
} else {
$this->template->params = new Registry($templateParams);
}
$this->template->inheritable = $templateInheritable;
$this->template->parent = $templateParent;
// Store the template and its params to the config
$this->set('theme', $this->template->template);
$this->set('themeParams', $this->template->params);
}
}
}

View File

@ -0,0 +1,468 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Application;
use Joomla\Application\AbstractWebApplication;
use Joomla\Application\Web\WebClient;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Event\Application\AfterExecuteEvent;
use Joomla\CMS\Event\Application\AfterRenderEvent;
use Joomla\CMS\Event\Application\AfterRespondEvent;
use Joomla\CMS\Event\Application\BeforeExecuteEvent;
use Joomla\CMS\Event\Application\BeforeRenderEvent;
use Joomla\CMS\Event\Application\BeforeRespondEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Input\Input;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\Version;
use Joomla\Filter\OutputFilter;
use Joomla\Registry\Registry;
use Joomla\Session\SessionEvent;
use Psr\Http\Message\ResponseInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Base class for a Joomla! Web application.
*
* @since 2.5.0
*/
abstract class WebApplication extends AbstractWebApplication
{
use EventAware;
use IdentityAware;
/**
* The application component title.
*
* @var string
* @since 4.3.0
*/
public $JComponentTitle;
/**
* The item associations
*
* @var integer
* @since 4.3.0
*
* @deprecated 4.4.0 will be removed in 6.0 as this property is not used anymore
*/
public $item_associations;
/**
* The application document object.
*
* @var Document
* @since 1.7.3
*/
protected $document;
/**
* The application language object.
*
* @var Language
* @since 1.7.3
*/
protected $language;
/**
* The application instance.
*
* @var static
* @since 1.7.3
*/
protected static $instance;
/**
* Class constructor.
*
* @param ?Input $input An optional argument to provide dependency injection for the application's
* input object. If the argument is a JInput object that object will become
* the application's input object, otherwise a default input object is created.
* @param ?Registry $config An optional argument to provide dependency injection for the application's
* config object. If the argument is a Registry object that object will become
* the application's config object, otherwise a default config object is created.
* @param ?WebClient $client An optional argument to provide dependency injection for the application's
* client object. If the argument is a WebClient object that object will become
* the application's client object, otherwise a default client object is created.
* @param ?ResponseInterface $response An optional argument to provide dependency injection for the application's
* response object. If the argument is a ResponseInterface object that object
* will become the application's response object, otherwise a default response
* object is created.
*
* @since 1.7.3
*/
public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, ResponseInterface $response = null)
{
// Ensure we have a CMS Input object otherwise the DI for \Joomla\CMS\Session\Storage\JoomlaStorage fails
$input = $input ?: new Input();
parent::__construct($input, $config, $client, $response);
// Set the execution datetime and timestamp;
$this->set('execution.datetime', gmdate('Y-m-d H:i:s'));
$this->set('execution.timestamp', time());
// Set the system URIs.
$this->loadSystemUris();
}
/**
* Returns a reference to the global WebApplication object, only creating it if it doesn't already exist.
*
* This method must be invoked as: $web = WebApplication::getInstance();
*
* @param string $name The name (optional) of the WebApplication class to instantiate.
*
* @return WebApplication
*
* @since 1.7.3
* @throws \RuntimeException
*
* @deprecated 4.0 will be removed in 6.0
* Use the application service in the DI container instead
* Example: \Joomla\CMS\Factory::getContainer()->get($name)
*/
public static function getInstance($name = null)
{
// Only create the object if it doesn't exist.
if (empty(static::$instance)) {
if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) {
throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500);
}
static::$instance = new $name();
}
return static::$instance;
}
/**
* Execute the application.
*
* @return void
*
* @since 1.7.3
*/
public function execute()
{
// Trigger the onBeforeExecute event.
$this->dispatchEvent(
'onBeforeExecute',
new BeforeExecuteEvent('onBeforeExecute', ['subject' => $this])
);
// Perform application routines.
$this->doExecute();
// Trigger the onAfterExecute event.
$this->dispatchEvent(
'onAfterExecute',
new AfterExecuteEvent('onAfterExecute', ['subject' => $this])
);
// If we have an application document object, render it.
if ($this->document instanceof Document) {
// Trigger the onBeforeRender event.
$this->dispatchEvent(
'onBeforeRender',
new BeforeRenderEvent('onBeforeRender', ['subject' => $this])
);
// Render the application output.
$this->render();
// Trigger the onAfterRender event.
$this->dispatchEvent(
'onAfterRender',
new AfterRenderEvent('onAfterRender', ['subject' => $this])
);
}
// If gzip compression is enabled in configuration and the server is compliant, compress the output.
if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) {
$this->compress();
}
// Trigger the onBeforeRespond event.
$this->dispatchEvent(
'onBeforeRespond',
new BeforeRespondEvent('onBeforeRespond', ['subject' => $this])
);
// Send the application response.
$this->respond();
// Trigger the onAfterRespond event.
$this->dispatchEvent(
'onAfterRespond',
new AfterRespondEvent('onAfterRespond', ['subject' => $this])
);
}
/**
* Rendering is the process of pushing the document buffers into the template
* placeholders, retrieving data from the document and pushing it into
* the application response buffer.
*
* @return void
*
* @since 1.7.3
*/
protected function render()
{
// Setup the document options.
$options = [
'template' => $this->get('theme'),
'file' => $this->get('themeFile', 'index.php'),
'params' => $this->get('themeParams'),
'templateInherits' => $this->get('themeInherits'),
];
if ($this->get('themes.base')) {
$options['directory'] = $this->get('themes.base');
} else {
// Fall back to constants.
$options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes';
}
// Parse the document.
$this->document->parse($options);
// Render the document.
$data = $this->document->render($this->get('cache_enabled'), $options);
// Set the application output data.
$this->setBody($data);
}
/**
* Method to get the application document object.
*
* @return Document The document object
*
* @since 1.7.3
*/
public function getDocument()
{
return $this->document;
}
/**
* Method to get the application language object.
*
* @return Language The language object
*
* @since 1.7.3
*/
public function getLanguage()
{
return $this->language;
}
/**
* Flush the media version to refresh versionable assets
*
* @return void
*
* @since 3.2
*/
public function flushAssets()
{
(new Version())->refreshMediaVersion();
}
/**
* Allows the application to load a custom or default document.
*
* The logic and options for creating this object are adequately generic for default cases
* but for many applications it will make sense to override this method and create a document,
* if required, based on more specific needs.
*
* @param ?Document $document An optional document object. If omitted, the factory document is created.
*
* @return WebApplication This method is chainable.
*
* @since 1.7.3
*/
public function loadDocument(Document $document = null)
{
$this->document = $document ?? Factory::getDocument();
return $this;
}
/**
* Allows the application to load a custom or default language.
*
* The logic and options for creating this object are adequately generic for default cases
* but for many applications it will make sense to override this method and create a language,
* if required, based on more specific needs.
*
* @param ?Language $language An optional language object. If omitted, the factory language is created.
*
* @return WebApplication This method is chainable.
*
* @since 1.7.3
*/
public function loadLanguage(Language $language = null)
{
$this->language = $language ?? Factory::getLanguage();
OutputFilter::setLanguage($this->language);
return $this;
}
/**
* Allows the application to load a custom or default session.
*
* The logic and options for creating this object are adequately generic for default cases
* but for many applications it will make sense to override this method and create a session,
* if required, based on more specific needs.
*
* @param ?Session $session An optional session object. If omitted, the session is created.
*
* @return WebApplication This method is chainable.
*
* @since 1.7.3
*
* @deprecated 4.3 will be removed in 6.0
* The session should be injected as a service.
*/
public function loadSession(Session $session = null)
{
$this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', ['category' => 'deprecated']);
return $this;
}
/**
* After the session has been started we need to populate it with some default values.
*
* @param SessionEvent $event Session event being triggered
*
* @return void
*
* @since 3.0.1
*/
public function afterSessionStart(SessionEvent $event)
{
$session = $event->getSession();
if ($session->isNew()) {
$session->set('registry', new Registry());
$session->set('user', new User());
}
// Ensure the identity is loaded
if (!$this->getIdentity()) {
$this->loadIdentity($session->get('user'));
}
}
/**
* Method to load the system URI strings for the application.
*
* @param string $requestUri An optional request URI to use instead of detecting one from the
* server environment variables.
*
* @return void
*
* @since 1.7.3
*/
protected function loadSystemUris($requestUri = null)
{
// Set the request URI.
if (!empty($requestUri)) {
$this->set('uri.request', $requestUri);
} else {
$this->set('uri.request', $this->detectRequestUri());
}
// Check to see if an explicit base URI has been set.
$siteUri = trim($this->get('site_uri', ''));
if ($siteUri !== '') {
$uri = Uri::getInstance($siteUri);
$path = $uri->toString(['path']);
} else {
// No explicit base URI was set so we need to detect it.
// Start with the requested URI.
$uri = Uri::getInstance($this->get('uri.request'));
// If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF.
if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) {
// We aren't expecting PATH_INFO within PHP_SELF so this should work.
$path = \dirname($_SERVER['PHP_SELF']);
} else {
// Pretty much everything else should be handled with SCRIPT_NAME.
$path = \dirname($_SERVER['SCRIPT_NAME']);
}
}
$host = $uri->toString(['scheme', 'user', 'pass', 'host', 'port']);
// Check if the path includes "index.php".
if (strpos($path, 'index.php') !== false) {
// Remove the index.php portion of the path.
$path = substr_replace($path, '', strpos($path, 'index.php'), 9);
}
$path = rtrim($path, '/\\');
// Set the base URI both as just a path and as the full URI.
$this->set('uri.base.full', $host . $path . '/');
$this->set('uri.base.host', $host);
$this->set('uri.base.path', $path . '/');
// Set the extended (non-base) part of the request URI as the route.
if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) {
$this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full'))));
}
// Get an explicitly set media URI is present.
$mediaURI = trim($this->get('media_uri', ''));
if ($mediaURI) {
if (strpos($mediaURI, '://') !== false) {
$this->set('uri.media.full', $mediaURI);
$this->set('uri.media.path', $mediaURI);
} else {
// Normalise slashes.
$mediaURI = trim($mediaURI, '/\\');
$mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/';
$this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI);
$this->set('uri.media.path', $mediaURI);
}
} else {
// No explicit media URI was set, build it dynamically from the base uri.
$this->set('uri.media.full', $this->get('uri.base.full') . 'media/');
$this->set('uri.media.path', $this->get('uri.base.path') . 'media/');
}
}
/**
* Retrieve the application configuration object.
*
* @return Registry
*
* @since 4.0.0
*/
public function getConfig()
{
return $this->config;
}
}

View File

@ -0,0 +1,291 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Association;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Association Extension Helper
*
* @since 3.7.0
*/
abstract class AssociationExtensionHelper implements AssociationExtensionInterface
{
/**
* The extension name
*
* @var array $extension
*
* @since 3.7.0
*/
protected $extension = 'com_??';
/**
* Array of item types
*
* @var array $itemTypes
*
* @since 3.7.0
*/
protected $itemTypes = [];
/**
* Has the extension association support
*
* @var boolean $associationsSupport
*
* @since 3.7.0
*/
protected $associationsSupport = false;
/**
* Checks if the extension supports associations
*
* @return boolean Supports the extension associations
*
* @since 3.7.0
*/
public function hasAssociationsSupport()
{
return $this->associationsSupport;
}
/**
* Get the item types
*
* @return array Array of item types
*
* @since 3.7.0
*/
public function getItemTypes()
{
return $this->itemTypes;
}
/**
* Get the associated items for an item
*
* @param string $typeName The item type
* @param int $itemId The id of item for which we need the associated items
*
* @return array
*
* @since 3.7.0
*/
public function getAssociationList($typeName, $itemId)
{
$items = [];
$associations = $this->getAssociations($typeName, $itemId);
foreach ($associations as $key => $association) {
$items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false);
}
return $items;
}
/**
* Get information about the type
*
* @param string $typeName The item type
*
* @return array Array of item types
*
* @since 3.7.0
*/
public function getType($typeName = '')
{
$fields = $this->getFieldsTemplate();
$tables = [];
$joins = [];
$support = $this->getSupportTemplate();
$title = '';
return [
'fields' => $fields,
'support' => $support,
'tables' => $tables,
'joins' => $joins,
'title' => $title,
];
}
/**
* Get information about the fields the type provides
*
* @param string $typeName The item type
*
* @return array Array of support information
*
* @since 3.7.0
*/
public function getTypeFields($typeName)
{
return $this->getTypeInformation($typeName, 'fields');
}
/**
* Get information about the fields the type provides
*
* @param string $typeName The item type
*
* @return array Array of support information
*
* @since 3.7.0
*/
public function getTypeSupport($typeName)
{
return $this->getTypeInformation($typeName, 'support');
}
/**
* Get information about the tables the type use
*
* @param string $typeName The item type
*
* @return array Array of support information
*
* @since 3.7.0
*/
public function getTypeTables($typeName)
{
return $this->getTypeInformation($typeName, 'tables');
}
/**
* Get information about the table joins for the type
*
* @param string $typeName The item type
*
* @return array Array of support information
*
* @since 3.7.0
*/
public function getTypeJoins($typeName)
{
return $this->getTypeInformation($typeName, 'joins');
}
/**
* Get the type title
*
* @param string $typeName The item type
*
* @return string The type title
*
* @since 3.7.0
*/
public function getTypeTitle($typeName)
{
$type = $this->getType($typeName);
if (!\array_key_exists('title', $type)) {
return '';
}
return $type['title'];
}
/**
* Get information about the type
*
* @param string $typeName The item type
* @param string $part part of the information
*
* @return array Array of support information
*
* @since 3.7.0
*/
private function getTypeInformation($typeName, $part = 'support')
{
$type = $this->getType($typeName);
if (!\array_key_exists($part, $type)) {
return [];
}
return $type[$part];
}
/**
* Get a table field name for a type
*
* @param string $typeName The item type
* @param string $fieldName The item type
*
* @return string
*
* @since 3.7.0
*/
public function getTypeFieldName($typeName, $fieldName)
{
$fields = $this->getTypeFields($typeName);
if (!\array_key_exists($fieldName, $fields)) {
return '';
}
$tmp = $fields[$fieldName];
$pos = strpos($tmp, '.');
if ($pos === false) {
return $tmp;
}
return substr($tmp, $pos + 1);
}
/**
* Get default values for support array
*
* @return array
*
* @since 3.7.0
*/
protected function getSupportTemplate()
{
return [
'state' => false,
'acl' => false,
'checkout' => false,
];
}
/**
* Get default values for fields array
*
* @return array
*
* @since 3.7.0
*/
protected function getFieldsTemplate()
{
return [
'id' => 'a.id',
'title' => 'a.title',
'alias' => 'a.alias',
'ordering' => 'a.ordering',
'menutype' => '',
'level' => '',
'catid' => 'a.catid',
'language' => 'a.language',
'access' => 'a.access',
'state' => 'a.state',
'created_user_id' => 'a.created_by',
'checked_out' => 'a.checked_out',
'checked_out_time' => 'a.checked_out_time',
];
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Association;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Association Extension Interface for the helper classes
*
* @since 3.7.0
*/
interface AssociationExtensionInterface
{
/**
* Checks if the extension supports associations
*
* @return boolean Supports the extension associations
*
* @since 3.7.0
*/
public function hasAssociationsSupport();
/**
* Method to get the associations for a given item.
*
* @param integer $id Id of the item
* @param string $view Name of the view
*
* @return array Array of associations for the item
*
* @since 4.0.0
*/
public function getAssociationsForItem($id = 0, $view = null);
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Association;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The association service.
*
* @since 4.0.0
*/
interface AssociationServiceInterface
{
/**
* Returns the associations extension helper class.
*
* @return AssociationExtensionInterface
*
* @since 4.0.0
*/
public function getAssociationsExtension(): AssociationExtensionInterface;
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Association;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Trait to implement AssociationServiceInterface
*
* @since 4.0.0
*/
trait AssociationServiceTrait
{
/**
* The association extension.
*
* @var AssociationExtensionInterface
*
* @since 4.0.0
*/
private $associationExtension = null;
/**
* Returns the associations extension helper class.
*
* @return AssociationExtensionInterface
*
* @since 4.0.0
*/
public function getAssociationsExtension(): AssociationExtensionInterface
{
return $this->associationExtension;
}
/**
* The association extension.
*
* @param AssociationExtensionInterface $associationExtension The extension
*
* @return void
*
* @since 4.0.0
*/
public function setAssociationExtension(AssociationExtensionInterface $associationExtension)
{
$this->associationExtension = $associationExtension;
}
}

View File

@ -0,0 +1,206 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication;
use Joomla\CMS\Event\User\AuthenticationEvent;
use Joomla\CMS\Event\User\AuthorisationEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Event\Dispatcher;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Authentication class, provides an interface for the Joomla authentication system
*
* @since 1.7.0
*/
class Authentication
{
use DispatcherAwareTrait;
/**
* This is the status code returned when the authentication is success (permit login)
*
* @var integer
* @since 1.7.0
*/
public const STATUS_SUCCESS = 1;
/**
* Status to indicate cancellation of authentication (unused)
*
* @var integer
* @since 1.7.0
*/
public const STATUS_CANCEL = 2;
/**
* This is the status code returned when the authentication failed (prevent login if no success)
*
* @var integer
* @since 1.7.0
*/
public const STATUS_FAILURE = 4;
/**
* This is the status code returned when the account has expired (prevent login)
*
* @var integer
* @since 1.7.0
*/
public const STATUS_EXPIRED = 8;
/**
* This is the status code returned when the account has been denied (prevent login)
*
* @var integer
* @since 1.7.0
*/
public const STATUS_DENIED = 16;
/**
* This is the status code returned when the account doesn't exist (not an error)
*
* @var integer
* @since 1.7.0
*/
public const STATUS_UNKNOWN = 32;
/**
* @var Authentication[] JAuthentication instances container.
* @since 1.7.3
*/
protected static $instance = [];
/**
* Plugin Type to run
*
* @var string
* @since 4.0.0
*/
protected $pluginType;
/**
* Constructor
*
* @param string $pluginType The plugin type to run authorisation and authentication on
* @param DispatcherInterface $dispatcher The event dispatcher we're going to use
*
* @since 1.7.0
*/
public function __construct(string $pluginType = 'authentication', DispatcherInterface $dispatcher = null)
{
// Set the dispatcher
if (!\is_object($dispatcher)) {
$dispatcher = Factory::getContainer()->get('dispatcher');
}
$this->setDispatcher($dispatcher);
$this->pluginType = $pluginType;
$isLoaded = PluginHelper::importPlugin($this->pluginType);
if (!$isLoaded) {
Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror');
}
}
/**
* Returns the global authentication object, only creating it
* if it doesn't already exist.
*
* @param string $pluginType The plugin type to run authorisation and authentication on
*
* @return Authentication The global Authentication object
*
* @since 1.7.0
*/
public static function getInstance(string $pluginType = 'authentication')
{
if (empty(self::$instance[$pluginType])) {
self::$instance[$pluginType] = new static($pluginType);
}
return self::$instance[$pluginType];
}
/**
* Finds out if a set of login credentials are valid by asking all observing
* objects to run their respective authentication routines.
*
* @param array $credentials Array holding the user credentials.
* @param array $options Array holding user options.
*
* @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin.
*
* @see AuthenticationResponse
* @since 1.7.0
*/
public function authenticate($credentials, $options = [])
{
// Create authentication response
$response = new AuthenticationResponse();
// Dispatch onUserAuthenticate event in the isolated dispatcher
$dispatcher = new Dispatcher();
PluginHelper::importPlugin($this->pluginType, null, true, $dispatcher);
$dispatcher->dispatch('onUserAuthenticate', new AuthenticationEvent('onUserAuthenticate', [
'credentials' => $credentials,
'options' => $options,
'subject' => $response,
]));
if (empty($response->username)) {
$response->username = $credentials['username'];
}
if (empty($response->fullname)) {
$response->fullname = $credentials['username'];
}
if (empty($response->password) && isset($credentials['password'])) {
$response->password = $credentials['password'];
}
return $response;
}
/**
* Authorises that a particular user should be able to login
*
* @param AuthenticationResponse $response response including username of the user to authorise
* @param array $options list of options
*
* @return AuthenticationResponse[] Array of authentication response objects
*
* @since 1.7.0
* @throws \Exception
*/
public function authorise($response, $options = [])
{
$dispatcher = $this->getDispatcher();
// Get plugins in case they haven't been imported already
PluginHelper::importPlugin('user', null, true, $dispatcher);
$event = new AuthorisationEvent('onUserAuthorisation', ['subject' => $response, 'options' => $options]);
$dispatcher->dispatch('onUserAuthorisation', $event);
return $event['result'] ?? [];
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Authentication response class, provides an object for storing user and error details
*
* @since 1.7.0
*/
class AuthenticationResponse
{
/**
* Response status (see status codes)
*
* @var string
* @since 1.7.0
*/
public $status = Authentication::STATUS_FAILURE;
/**
* The type of authentication that was successful
*
* @var string
* @since 1.7.0
*/
public $type = '';
/**
* The error message
*
* @var string
* @since 1.7.0
*/
public $error_message = '';
/**
* Any UTF-8 string that the End User wants to use as a username.
*
* @var string
* @since 1.7.0
*/
public $username = '';
/**
* Any UTF-8 string that the End User wants to use as a password.
*
* @var string
* @since 1.7.0
*/
public $password = '';
/**
* The email address of the End User as specified in section 3.4.1 of [RFC2822]
*
* @var string
* @since 1.7.0
*/
public $email = '';
/**
* UTF-8 string free text representation of the End User's full name.
*
* @var string
* @since 1.7.0
*/
public $fullname = '';
/**
* The End User's date of birth as YYYY-MM-DD. Any values whose representation uses
* fewer than the specified number of digits should be zero-padded. The length of this
* value MUST always be 10. If the End User user does not want to reveal any particular
* component of this value, it MUST be set to zero.
*
* For instance, if an End User wants to specify that their date of birth is in 1980, but
* not the month or day, the value returned SHALL be "1980-00-00".
*
* @var string
* @since 1.7.0
*/
public $birthdate = '';
/**
* The End User's gender, "M" for male, "F" for female.
*
* @var string
* @since 1.7.0
*/
public $gender = '';
/**
* UTF-8 string free text that SHOULD conform to the End User's country's postal system.
*
* @var string
* @since 1.7.0
*/
public $postcode = '';
/**
* The End User's country of residence as specified by ISO3166.
*
* @var string
* @since 1.7.0
*/
public $country = '';
/**
* End User's preferred language as specified by ISO639.
*
* @var string
* @since 1.7.0
*/
public $language = '';
/**
* ASCII string from TimeZone database
*
* @var string
* @since 1.7.0
*/
public $timezone = '';
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
use Joomla\Authentication\Password\Argon2iHandler as BaseArgon2iHandler;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Password handler for Argon2i hashed passwords
*
* @since 4.0.0
*/
class Argon2iHandler extends BaseArgon2iHandler implements CheckIfRehashNeededHandlerInterface
{
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool
{
return password_needs_rehash($hash, PASSWORD_ARGON2I);
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
use Joomla\Authentication\Password\Argon2idHandler as BaseArgon2idHandler;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Password handler for Argon2id hashed passwords
*
* @since 4.0.0
*/
class Argon2idHandler extends BaseArgon2idHandler implements CheckIfRehashNeededHandlerInterface
{
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool
{
return password_needs_rehash($hash, PASSWORD_ARGON2ID);
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
use Joomla\Authentication\Password\BCryptHandler as BaseBCryptHandler;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Password handler for BCrypt hashed passwords
*
* @since 4.0.0
*/
class BCryptHandler extends BaseBCryptHandler implements CheckIfRehashNeededHandlerInterface
{
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool
{
return password_needs_rehash($hash, PASSWORD_BCRYPT);
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
use Joomla\Authentication\Password\HandlerInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Password handler supporting testing against a chain of handlers
*
* @since 4.0.0
*/
class ChainedHandler implements HandlerInterface, CheckIfRehashNeededHandlerInterface
{
/**
* The password handlers in use by this chain.
*
* @var HandlerInterface[]
* @since 4.0.0
*/
private $handlers = [];
/**
* Add a handler to the chain
*
* @param HandlerInterface $handler The password handler to add
*
* @return void
*
* @since 4.0.0
*/
public function addHandler(HandlerInterface $handler)
{
$this->handlers[] = $handler;
}
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool
{
foreach ($this->handlers as $handler) {
if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) {
return true;
}
}
return false;
}
/**
* Generate a hash for a plaintext password
*
* @param string $plaintext The plaintext password to validate
* @param array $options Options for the hashing operation
*
* @return void
*
* @since 4.0.0
* @throws \RuntimeException
*/
public function hashPassword($plaintext, array $options = [])
{
throw new \RuntimeException('The chained password handler cannot be used to hash a password');
}
/**
* Check that the password handler is supported in this environment
*
* @return boolean
*
* @since 4.0.0
*/
public static function isSupported()
{
return true;
}
/**
* Validate a password
*
* @param string $plaintext The plain text password to validate
* @param string $hashed The password hash to validate against
*
* @return boolean
*
* @since 4.0.0
*/
public function validatePassword($plaintext, $hashed)
{
foreach ($this->handlers as $handler) {
if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface for a password handler which supports checking if the password requires rehashing
*
* @since 4.0.0
*/
interface CheckIfRehashNeededHandlerInterface
{
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool;
}

View File

@ -0,0 +1,96 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
use Joomla\Authentication\Password\HandlerInterface;
use Joomla\CMS\Crypt\Crypt;
use Joomla\CMS\User\UserHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Password handler for MD5 hashed passwords
*
* @since 4.0.0
*
* @deprecated 4.0 will be removed in 6.0
* Support for MD5 hashed passwords will be removed without replacement
*/
class MD5Handler implements HandlerInterface, CheckIfRehashNeededHandlerInterface
{
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool
{
return true;
}
/**
* Generate a hash for a plaintext password
*
* @param string $plaintext The plaintext password to validate
* @param array $options Options for the hashing operation
*
* @return string
*
* @since 4.0.0
*/
public function hashPassword($plaintext, array $options = [])
{
$salt = UserHelper::genRandomPassword(32);
$crypted = md5($plaintext . $salt);
return $crypted . ':' . $salt;
}
/**
* Check that the password handler is supported in this environment
*
* @return boolean
*
* @since 4.0.0
*/
public static function isSupported()
{
return true;
}
/**
* Validate a password
*
* @param string $plaintext The plain text password to validate
* @param string $hashed The password hash to validate against
*
* @return boolean
*
* @since 4.0.0
*/
public function validatePassword($plaintext, $hashed)
{
// Check the password
$parts = explode(':', $hashed);
$salt = @$parts[1];
// Compile the hash to compare
// If the salt is empty AND there is a ':' in the original hash, we must append ':' at the end
$testcrypt = md5($plaintext . $salt) . ($salt ? ':' . $salt : (strpos($hashed, ':') !== false ? ':' : ''));
return Crypt::timingSafeCompare($hashed, $testcrypt);
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication\Password;
use Joomla\Authentication\Password\HandlerInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Password handler for PHPass hashed passwords
*
* @since 4.0.0
*
* @deprecated 4.0 will be removed in 6.0
* Support for PHPass hashed passwords will be removed without replacement
*/
class PHPassHandler implements HandlerInterface, CheckIfRehashNeededHandlerInterface
{
/**
* Check if the password requires rehashing
*
* @param string $hash The password hash to check
*
* @return boolean
*
* @since 4.0.0
*/
public function checkIfRehashNeeded(string $hash): bool
{
return true;
}
/**
* Generate a hash for a plaintext password
*
* @param string $plaintext The plaintext password to validate
* @param array $options Options for the hashing operation
*
* @return string
*
* @since 4.0.0
*/
public function hashPassword($plaintext, array $options = [])
{
return $this->getPasswordHash()->HashPassword($plaintext);
}
/**
* Check that the password handler is supported in this environment
*
* @return boolean
*
* @since 4.0.0
*/
public static function isSupported()
{
return class_exists(\PasswordHash::class);
}
/**
* Validate a password
*
* @param string $plaintext The plain text password to validate
* @param string $hashed The password hash to validate against
*
* @return boolean
*
* @since 4.0.0
*/
public function validatePassword($plaintext, $hashed)
{
return $this->getPasswordHash()->CheckPassword($plaintext, $hashed);
}
/**
* Get an instance of the PasswordHash class
*
* @return \PasswordHash
*
* @since 4.0.0
*/
private function getPasswordHash(): \PasswordHash
{
return new \PasswordHash(10, true);
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright Copyright (C) 2022 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Authentication;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface class defining the necessary methods for an authentication plugin to be provider aware
* Please note: might be deprecated with Joomla 4.2
*
* @since 3.10.7
*/
interface ProviderAwareAuthenticationPluginInterface
{
/**
* Return if plugin acts as primary provider
*
* @return true
*
* @since 3.10.7
*/
public static function isPrimaryProvider();
/**
* Return provider name
*
* @return string
*
* @since 3.10.7
*/
public static function getProviderName();
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Autoload;
use Composer\Autoload\ClassLoader as ComposerClassLoader;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Decorate Composer ClassLoader for Joomla!
*
* For backward compatibility due to class aliasing in the CMS, the loadClass() method was modified to call
* the JLoader::applyAliasFor() method.
*
* @since 3.4
*/
class ClassLoader
{
/**
* The Composer class loader
*
* @var ComposerClassLoader
* @since 3.4
*/
private $loader;
/**
* Constructor
*
* @param ComposerClassLoader $loader Composer autoloader
*
* @since 3.4
*/
public function __construct(ComposerClassLoader $loader)
{
$this->loader = $loader;
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
*
* @return boolean|null True if loaded, null otherwise
*
* @since 3.4
*/
public function loadClass($class)
{
if ($result = $this->loader->loadClass($class)) {
\JLoader::applyAliasFor($class);
}
return $result;
}
}

View File

@ -0,0 +1,322 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Button;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The TaskButton class.
*
* @since 4.0.0
*/
class ActionButton
{
/**
* The button states profiles.
*
* @var array
*
* @since 4.0.0
*/
protected $states = [];
/**
* Default options for unknown state.
*
* @var array
*
* @since 4.0.0
*/
protected $unknownState = [
'value' => null,
'task' => '',
'icon' => 'question',
'title' => 'Unknown state',
'options' => [
'disabled' => false,
'only_icon' => false,
'tip' => true,
'tip_title' => '',
'task_prefix' => '',
'checkbox_name' => 'cb',
],
];
/**
* Options of this button set.
*
* @var Registry
*
* @since 4.0.0
*/
protected $options;
/**
* The layout path to render.
*
* @var string
*
* @since 4.0.0
*/
protected $layout = 'joomla.button.action-button';
/**
* ActionButton constructor.
*
* @param array $options The options for all buttons in this group.
*
* @since 4.0.0
*/
public function __construct(array $options = [])
{
$this->options = new Registry($options);
// Replace some dynamic values
$this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE');
$this->preprocess();
}
/**
* Configure this object.
*
* @return void
*
* @since 4.0.0
*/
protected function preprocess()
{
// Implement this method.
}
/**
* Add a state profile.
*
* @param integer $value The value of this state.
* @param string $task The task you want to execute after click this button.
* @param string $icon The icon to display for user.
* @param string $title Title text will show if we enable tooltips.
* @param array $options The button options, will override group options.
*
* @return static Return self to support chaining.
*
* @since 4.0.0
*/
public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self
{
// Force type to prevent null data
$this->states[$value] = [
'value' => $value,
'task' => $task,
'icon' => $icon,
'title' => $title,
'options' => $options,
];
return $this;
}
/**
* Get state profile by value name.
*
* @param integer $value The value name we want to get.
*
* @return array|null Return state profile or NULL.
*
* @since 4.0.0
*/
public function getState(int $value): ?array
{
return $this->states[$value] ?? null;
}
/**
* Remove a state by value name.
*
* @param integer $value Remove state by this value.
*
* @return static Return to support chaining.
*
* @since 4.0.0
*/
public function removeState(int $value): self
{
if (isset($this->states[$value])) {
unset($this->states[$value]);
}
return $this;
}
/**
* Render action button by item value.
*
* @param integer|null $value Current value of this item.
* @param integer|null $row The row number of this item.
* @param array $options The options to override group options.
*
* @return string Rendered HTML.
*
* @since 4.0.0
*
* @throws \InvalidArgumentException
*/
public function render(?int $value = null, ?int $row = null, array $options = []): string
{
$data = $this->getState($value) ?? $this->unknownState;
$data = ArrayHelper::mergeRecursive(
$this->unknownState,
$data,
[
'options' => $this->options->toArray(),
],
[
'options' => $options,
]
);
$data['row'] = $row;
$data['icon'] = $this->fetchIconClass($data['icon']);
return LayoutHelper::render($this->layout, $data);
}
/**
* Render to string.
*
* @return string
*
* @since 4.0.0
*/
public function __toString(): string
{
try {
return $this->render();
} catch (\Throwable $e) {
return (string) $e;
}
}
/**
* Method to get property layout.
*
* @return string
*
* @since 4.0.0
*/
public function getLayout(): string
{
return $this->layout;
}
/**
* Method to set property template.
*
* @param string $layout The layout path.
*
* @return static Return self to support chaining.
*
* @since 4.0.0
*/
public function setLayout(string $layout): self
{
$this->layout = $layout;
return $this;
}
/**
* Method to get property options.
*
* @return array
*
* @since 4.0.0
*/
public function getOptions(): array
{
return (array) $this->options->toArray();
}
/**
* Method to set property options.
*
* @param array $options The options of this button group.
*
* @return static Return self to support chaining.
*
* @since 4.0.0
*/
public function setOptions(array $options): self
{
$this->options = new Registry($options);
return $this;
}
/**
* Get an option value.
*
* @param string $name The option name.
* @param mixed $default Default value if not exists.
*
* @return mixed Return option value or default value.
*
* @since 4.0.0
*/
public function getOption(string $name, $default = null)
{
return $this->options->get($name, $default);
}
/**
* Set option value.
*
* @param string $name The option name.
* @param mixed $value The option value.
*
* @return static Return self to support chaining.
*
* @since 4.0.0
*/
public function setOption(string $name, $value): self
{
$this->options->set($name, $value);
return $this;
}
/**
* Method to get the CSS class name for an icon identifier.
*
* Can be redefined in the final class.
*
* @param string $identifier Icon identification string.
*
* @return string CSS class name.
*
* @since 4.0.0
*/
public function fetchIconClass(string $identifier): string
{
// It's an ugly hack, but this allows templates to define the icon classes for the toolbar
$layout = new FileLayout('joomla.button.iconclass');
return $layout->render(['icon' => $identifier]);
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Button;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The FeaturedButton class.
*
* @since 4.0.0
*/
class FeaturedButton extends ActionButton
{
/**
* Configure this object.
*
* @return void
*
* @since 4.0.0
*/
protected function preprocess()
{
$this->addState(
0,
'featured',
'icon-unfeatured',
Text::_('JGLOBAL_TOGGLE_FEATURED'),
['tip_title' => Text::_('JUNFEATURED')]
);
$this->addState(
1,
'unfeatured',
'icon-color-featured icon-star',
Text::_('JGLOBAL_TOGGLE_FEATURED'),
['tip_title' => Text::_('JFEATURED')]
);
}
/**
* Render action button by item value.
*
* @param integer|null $value Current value of this item.
* @param integer|null $row The row number of this item.
* @param array $options The options to override group options.
* @param string|Date $featuredUp The date which item featured up.
* @param string|Date $featuredDown The date which item featured down.
*
* @return string Rendered HTML.
*
* @since 4.0.0
*/
public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string
{
if ($featuredUp || $featuredDown) {
$bakState = $this->getState($value);
$default = $this->getState($value) ?? $this->unknownState;
$nowDate = Factory::getDate()->toUnix();
$tz = Factory::getUser()->getTimezone();
if (!\is_null($featuredUp)) {
$featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz);
}
if (!\is_null($featuredDown)) {
$featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz);
}
// Add tips and special titles
// Create special titles for featured items
if ($value === 1) {
// Create tip text, only we have featured up or down settings
$tips = [];
if ($featuredUp) {
$tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
if ($featuredDown) {
$tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
$tip = empty($tips) ? false : implode('<br>', $tips);
$default['title'] = $tip;
$options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM');
if ($featuredUp && $nowDate < $featuredUp->toUnix()) {
$options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM');
$default['icon'] = 'pending';
}
if ($featuredDown && $nowDate > $featuredDown->toUnix()) {
$options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM');
$default['icon'] = 'expired';
}
}
$this->states[$value] = $default;
$html = parent::render($value, $row, $options);
$this->states[$value] = $bakState;
return $html;
}
return parent::render($value, $row, $options);
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Button;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The PublishedButton class.
*
* @since 4.0.0
*/
class PublishedButton extends ActionButton
{
/**
* Configure this object.
*
* @return void
*
* @since 4.0.0
*/
protected function preprocess()
{
$this->addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]);
$this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]);
$this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]);
$this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]);
}
/**
* Render action button by item value.
*
* @param integer|null $value Current value of this item.
* @param integer|null $row The row number of this item.
* @param array $options The options to override group options.
* @param string|Date $publishUp The date which item publish up.
* @param string|Date $publishDown The date which item publish down.
*
* @return string Rendered HTML.
*
* @since 4.0.0
*/
public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string
{
if ($publishUp || $publishDown) {
$bakState = $this->getState($value);
$default = $this->getState($value) ?? $this->unknownState;
$nullDate = Factory::getDbo()->getNullDate();
$nowDate = Factory::getDate()->toUnix();
$tz = Factory::getUser()->getTimezone();
$publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false;
$publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false;
// Add tips and special titles
// Create special titles for published items
if ($value === 1) {
// Create tip text, only we have publish up or down settings
$tips = [];
if ($publishUp) {
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC'));
$tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH');
}
if ($publishDown) {
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
$tip = empty($tips) ? false : implode('<br>', $tips);
$default['title'] = $tip;
$options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM');
if ($publishUp && $nowDate < $publishUp->toUnix()) {
$options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM');
$default['icon'] = 'pending';
}
if ($publishDown && $nowDate > $publishDown->toUnix()) {
$options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM');
$default['icon'] = 'expired';
}
if (\array_key_exists('category_published', $options)) {
$categoryPublished = $options['category_published'];
if ($categoryPublished === 0) {
$options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED');
$default['icon'] = 'expired';
}
if ($categoryPublished === -2) {
$options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED');
$default['icon'] = 'expired';
}
}
}
$this->states[$value] = $default;
$html = parent::render($value, $row, $options);
$this->states[$value] = $bakState;
return $html;
}
return parent::render($value, $row, $options);
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Button;
use Joomla\CMS\Language\Text;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The PublishedButton class.
*
* @since 4.0.0
*/
class TransitionButton extends ActionButton
{
/**
* The layout path to render.
*
* @var string
*
* @since 4.0.0
*/
protected $layout = 'joomla.button.transition-button';
/**
* ActionButton constructor.
*
* @param array $options The options for all buttons in this group.
*
* @since 4.0.0
*/
public function __construct(array $options = [])
{
parent::__construct($options);
$this->unknownState['icon'] = 'shuffle';
$this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE');
$this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title'];
}
/**
* Render action button by item value.
*
* @param integer|null $value Current value of this item.
* @param integer|null $row The row number of this item.
* @param array $options The options to override group options.
*
* @return string Rendered HTML.
*
* @since 4.0.0
*/
public function render(?int $value = null, ?int $row = null, array $options = []): string
{
$default = $this->unknownState;
$options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']);
return parent::render($value, $row, $options);
}
}

View File

@ -0,0 +1,755 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache;
use Joomla\Application\Web\WebClient;
use Joomla\CMS\Cache\Exception\CacheExceptionInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Session\Session;
use Joomla\Filesystem\Path;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Cache base object
*
* @since 1.7.0
*/
class Cache
{
/**
* Storage handler
*
* @var CacheStorage[]
* @since 1.7.0
*/
public static $_handler = [];
/**
* Cache options
*
* @var array
* @since 1.7.0
*/
public $_options;
/**
* Constructor
*
* @param array $options Cache options
*
* @since 1.7.0
*/
public function __construct($options)
{
$app = Factory::getApplication();
$this->_options = [
'cachebase' => $app->get('cache_path', JPATH_CACHE),
'lifetime' => (int) $app->get('cachetime'),
'language' => $app->get('language', 'en-GB'),
'storage' => $app->get('cache_handler', ''),
'defaultgroup' => 'default',
'locking' => true,
'locktime' => 15,
'checkTime' => true,
'caching' => ($app->get('caching') >= 1),
];
// Overwrite default options with given options
foreach ($options as $option => $value) {
if (isset($options[$option]) && $options[$option] !== '') {
$this->_options[$option] = $options[$option];
}
}
if (empty($this->_options['storage'])) {
$this->setCaching(false);
}
}
/**
* Returns a reference to a cache adapter object, always creating it
*
* @param string $type The cache object type to instantiate
* @param array $options The array of options
*
* @return CacheController
*
* @since 1.7.0
*
* @deprecated 4.2 will be removed in 6.0
* Use the cache controller factory instead
* Example: Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($type, $options);
*/
public static function getInstance($type = 'output', $options = [])
{
@trigger_error(
sprintf(
'%s() is deprecated. The cache controller should be fetched from the factory.',
__METHOD__
),
E_USER_DEPRECATED
);
return Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($type, $options);
}
/**
* Get the storage handlers
*
* @return array
*
* @since 1.7.0
*/
public static function getStores()
{
$handlers = [];
// Get an iterator and loop through the driver classes.
$iterator = new \DirectoryIterator(__DIR__ . '/Storage');
/** @type $file \DirectoryIterator */
foreach ($iterator as $file) {
$fileName = $file->getFilename();
// Only load for php files.
if (!$file->isFile() || $file->getExtension() !== 'php' || $fileName === 'CacheStorageHelper.php') {
continue;
}
// Derive the class name from the type.
$class = str_ireplace('.php', '', __NAMESPACE__ . '\\Storage\\' . ucfirst(trim($fileName)));
// If the class doesn't exist we have nothing left to do but look at the next type. We did our best.
if (!class_exists($class)) {
continue;
}
// Sweet! Our class exists, so now we just need to know if it passes its test method.
if ($class::isSupported()) {
// Connector names should not have file extensions.
$handler = str_ireplace('Storage.php', '', $fileName);
$handler = str_ireplace('.php', '', $handler);
$handlers[] = strtolower($handler);
}
}
return $handlers;
}
/**
* Set caching enabled state
*
* @param boolean $enabled True to enable caching
*
* @return void
*
* @since 1.7.0
*/
public function setCaching($enabled)
{
$this->_options['caching'] = $enabled;
}
/**
* Get caching state
*
* @return boolean
*
* @since 1.7.0
*/
public function getCaching()
{
return $this->_options['caching'];
}
/**
* Set cache lifetime
*
* @param integer $lt Cache lifetime in minutes
*
* @return void
*
* @since 1.7.0
*/
public function setLifeTime($lt)
{
$this->_options['lifetime'] = $lt;
}
/**
* Check if the cache contains data stored by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.7.0
*/
public function contains($id, $group = null)
{
if (!$this->getCaching()) {
return false;
}
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
return $this->_getStorage()->contains($id, $group);
}
/**
* Get cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 1.7.0
*/
public function get($id, $group = null)
{
if (!$this->getCaching()) {
return false;
}
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
return $this->_getStorage()->get($id, $group, $this->_options['checkTime']);
}
/**
* Get a list of all cached data
*
* @return mixed Boolean false on failure or an object with a list of cache groups and data
*
* @since 1.7.0
*/
public function getAll()
{
if (!$this->getCaching()) {
return false;
}
return $this->_getStorage()->getAll();
}
/**
* Store the cached data by ID and group
*
* @param mixed $data The data to store
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function store($data, $id, $group = null)
{
if (!$this->getCaching()) {
return false;
}
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
// Get the storage and store the cached data
return $this->_getStorage()->store($id, $group, $data);
}
/**
* Remove a cached data entry by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function remove($id, $group = null)
{
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
try {
return $this->_getStorage()->remove($id, $group);
} catch (CacheExceptionInterface $e) {
if (!$this->getCaching()) {
return false;
}
throw $e;
}
}
/**
* Clean cache for a group given a mode.
*
* group mode : cleans all cache in the group
* notgroup mode : cleans all cache not in the group
*
* @param string $group The cache data group
* @param string $mode The mode for cleaning cache [group|notgroup]
*
* @return boolean True on success, false otherwise
*
* @since 1.7.0
*/
public function clean($group = null, $mode = 'group')
{
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
try {
return $this->_getStorage()->clean($group, $mode);
} catch (CacheExceptionInterface $e) {
if (!$this->getCaching()) {
return false;
}
throw $e;
}
}
/**
* Garbage collect expired cache data
*
* @return boolean
*
* @since 1.7.0
*/
public function gc()
{
try {
return $this->_getStorage()->gc();
} catch (CacheExceptionInterface $e) {
if (!$this->getCaching()) {
return false;
}
throw $e;
}
}
/**
* Set lock flag on cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param string $locktime The default locktime for locking the cache.
*
* @return \stdClass Object with properties of lock and locklooped
*
* @since 1.7.0
*/
public function lock($id, $group = null, $locktime = null)
{
$returning = new \stdClass();
$returning->locklooped = false;
if (!$this->getCaching()) {
$returning->locked = false;
return $returning;
}
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
// Get the default locktime
$locktime = $locktime ?: $this->_options['locktime'];
/*
* Allow storage handlers to perform locking on their own
* NOTE drivers with lock need also unlock or unlocking will fail because of false $id
*/
$handler = $this->_getStorage();
if ($this->_options['locking'] == true) {
$locked = $handler->lock($id, $group, $locktime);
if ($locked !== false) {
return $locked;
}
}
// Fallback
$curentlifetime = $this->_options['lifetime'];
// Set lifetime to locktime for storing in children
$this->_options['lifetime'] = $locktime;
$looptime = $locktime * 10;
$id2 = $id . '_lock';
if ($this->_options['locking'] == true) {
$data_lock = $handler->get($id2, $group, $this->_options['checkTime']);
} else {
$data_lock = false;
$returning->locked = false;
}
if ($data_lock !== false) {
$lock_counter = 0;
// Loop until you find that the lock has been released. That implies that data get from other thread has finished
while ($data_lock !== false) {
if ($lock_counter > $looptime) {
$returning->locked = false;
$returning->locklooped = true;
break;
}
usleep(100);
$data_lock = $handler->get($id2, $group, $this->_options['checkTime']);
$lock_counter++;
}
}
if ($this->_options['locking'] == true) {
$returning->locked = $handler->store($id2, $group, 1);
}
// Revert lifetime to previous one
$this->_options['lifetime'] = $curentlifetime;
return $returning;
}
/**
* Unset lock flag on cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function unlock($id, $group = null)
{
if (!$this->getCaching()) {
return false;
}
// Get the default group
$group = $group ?: $this->_options['defaultgroup'];
// Allow handlers to perform unlocking on their own
$handler = $this->_getStorage();
$unlocked = $handler->unlock($id, $group);
if ($unlocked !== false) {
return $unlocked;
}
// Fallback
return $handler->remove($id . '_lock', $group);
}
/**
* Get the cache storage handler
*
* @return CacheStorage
*
* @since 1.7.0
*/
public function &_getStorage()
{
$hash = md5(serialize($this->_options));
if (isset(self::$_handler[$hash])) {
return self::$_handler[$hash];
}
self::$_handler[$hash] = CacheStorage::getInstance($this->_options['storage'], $this->_options);
return self::$_handler[$hash];
}
/**
* Perform workarounds on retrieved cached data
*
* @param array $data Cached data
* @param array $options Array of options
*
* @return string Body of cached data
*
* @since 1.7.0
*/
public static function getWorkarounds($data, $options = [])
{
$app = Factory::getApplication();
$document = Factory::getDocument();
$body = null;
// Get the document head out of the cache.
if (
isset($options['mergehead']) && $options['mergehead'] == 1 && isset($data['head']) && !empty($data['head'])
&& method_exists($document, 'mergeHeadData')
) {
$document->mergeHeadData($data['head']);
} elseif (isset($data['head']) && method_exists($document, 'setHeadData')) {
$document->setHeadData($data['head']);
}
// Get the document MIME encoding out of the cache
if (isset($data['mime_encoding'])) {
$document->setMimeEncoding($data['mime_encoding'], true);
}
// If the pathway buffer is set in the cache data, get it.
if (isset($data['pathway']) && \is_array($data['pathway'])) {
// Push the pathway data into the pathway object.
$app->getPathway()->setPathway($data['pathway']);
}
// @todo check if the following is needed, seems like it should be in page cache
// If a module buffer is set in the cache data, get it.
if (isset($data['module']) && \is_array($data['module'])) {
// Iterate through the module positions and push them into the document buffer.
foreach ($data['module'] as $name => $contents) {
$document->setBuffer($contents, 'module', $name);
}
}
// Set cached headers.
if (isset($data['headers']) && $data['headers']) {
foreach ($data['headers'] as $header) {
$app->setHeader($header['name'], $header['value']);
}
}
// The following code searches for a token in the cached page and replaces it with the proper token.
if (isset($data['body'])) {
$token = Session::getFormToken();
$search = '#<input type="hidden" name="[0-9a-f]{32}" value="1">#';
$replacement = '<input type="hidden" name="' . $token . '" value="1">';
$data['body'] = preg_replace($search, $replacement, $data['body']);
$body = $data['body'];
}
// Get the document body out of the cache.
return $body;
}
/**
* Create workarounds for data to be cached
*
* @param string $data Cached data
* @param array $options Array of options
*
* @return array Data to be cached
*
* @since 1.7.0
*/
public static function setWorkarounds($data, $options = [])
{
$loptions = [
'nopathway' => 0,
'nohead' => 0,
'nomodules' => 0,
'modulemode' => 0,
];
if (isset($options['nopathway'])) {
$loptions['nopathway'] = $options['nopathway'];
}
if (isset($options['nohead'])) {
$loptions['nohead'] = $options['nohead'];
}
if (isset($options['nomodules'])) {
$loptions['nomodules'] = $options['nomodules'];
}
if (isset($options['modulemode'])) {
$loptions['modulemode'] = $options['modulemode'];
}
$app = Factory::getApplication();
$document = Factory::getDocument();
if ($loptions['nomodules'] != 1) {
// Get the modules buffer before component execution.
$buffer1 = $document->getBuffer();
if (!\is_array($buffer1)) {
$buffer1 = [];
}
// Make sure the module buffer is an array.
if (!isset($buffer1['module']) || !\is_array($buffer1['module'])) {
$buffer1['module'] = [];
}
}
// View body data
$cached = ['body' => $data];
// Document head data
if ($loptions['nohead'] != 1 && method_exists($document, 'getHeadData')) {
if ($loptions['modulemode'] == 1) {
$headNow = $document->getHeadData();
$unset = ['title', 'description', 'link', 'links', 'metaTags'];
foreach ($unset as $key) {
unset($headNow[$key]);
}
// Sanitize empty data
foreach (array_keys($headNow) as $key) {
if (!isset($headNow[$key]) || $headNow[$key] === []) {
unset($headNow[$key]);
}
}
$cached['head'] = $headNow;
} else {
$cached['head'] = $document->getHeadData();
// Document MIME encoding
$cached['mime_encoding'] = $document->getMimeEncoding();
}
}
// Pathway data
if ($app->isClient('site') && $loptions['nopathway'] != 1) {
$cached['pathway'] = $data['pathway'] ?? $app->getPathway()->getPathway();
}
if ($loptions['nomodules'] != 1) {
// @todo Check if the following is needed, seems like it should be in page cache
// Get the module buffer after component execution.
$buffer2 = $document->getBuffer();
if (!\is_array($buffer2)) {
$buffer2 = [];
}
// Make sure the module buffer is an array.
if (!isset($buffer2['module']) || !\is_array($buffer2['module'])) {
$buffer2['module'] = [];
}
// Compare the second module buffer against the first buffer.
$cached['module'] = array_diff_assoc($buffer2['module'], $buffer1['module']);
}
// Headers data
if (isset($options['headers']) && $options['headers']) {
$cached['headers'] = $app->getHeaders();
}
return $cached;
}
/**
* Create a safe ID for cached data from URL parameters
*
* @return string MD5 encoded cache ID
*
* @since 1.7.0
*/
public static function makeId()
{
$app = Factory::getApplication();
$registeredurlparams = new \stdClass();
// Get url parameters set by plugins
if (!empty($app->registeredurlparams)) {
$registeredurlparams = $app->registeredurlparams;
}
// Platform defaults
$defaulturlparams = [
'format' => 'WORD',
'option' => 'WORD',
'view' => 'WORD',
'layout' => 'WORD',
'tpl' => 'CMD',
'id' => 'INT',
];
// Use platform defaults if parameter doesn't already exist.
foreach ($defaulturlparams as $param => $type) {
if (!property_exists($registeredurlparams, $param)) {
$registeredurlparams->$param = $type;
}
}
$safeuriaddon = new \stdClass();
foreach ($registeredurlparams as $key => $value) {
$safeuriaddon->$key = $app->getInput()->get($key, null, $value);
}
return md5(serialize($safeuriaddon));
}
/**
* Set a prefix cache key if device calls for separate caching
*
* @return string
*
* @since 3.5
*/
public static function getPlatformPrefix()
{
// No prefix when Global Config is set to no platform specific prefix
if (!Factory::getApplication()->get('cache_platformprefix', false)) {
return '';
}
$webclient = new WebClient();
if ($webclient->mobile) {
return 'M-';
}
return '';
}
/**
* Add a directory where Cache should search for handlers. You may either pass a string or an array of directories.
*
* @param array|string $path A path to search.
*
* @return array An array with directory elements
*
* @since 1.7.0
*/
public static function addIncludePath($path = '')
{
static $paths;
if (!isset($paths)) {
$paths = [];
}
if (!empty($path) && !\in_array($path, $paths)) {
array_unshift($paths, Path::clean($path));
}
return $paths;
}
}

View File

@ -0,0 +1,169 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache;
use Joomla\CMS\Factory;
use Joomla\Filesystem\Path;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Public cache handler
*
* @since 1.7.0
* @mixin Cache
* @note As of 4.0 this class will be abstract
*/
class CacheController
{
/**
* Cache object
*
* @var Cache
* @since 1.7.0
*/
public $cache;
/**
* Array of options
*
* @var array
* @since 1.7.0
*/
public $options;
/**
* Constructor
*
* @param array $options Array of options
*
* @since 1.7.0
*/
public function __construct($options)
{
$this->cache = new Cache($options);
$this->options = &$this->cache->_options;
// Overwrite default options with given options
foreach ($options as $option => $value) {
if (isset($options[$option])) {
$this->options[$option] = $options[$option];
}
}
}
/**
* Magic method to proxy CacheController method calls to Cache
*
* @param string $name Name of the function
* @param array $arguments Array of arguments for the function
*
* @return mixed
*
* @since 1.7.0
*/
public function __call($name, $arguments)
{
return \call_user_func_array([$this->cache, $name], $arguments);
}
/**
* Returns a reference to a cache adapter object, always creating it
*
* @param string $type The cache object type to instantiate; default is output.
* @param array $options Array of options
*
* @return CacheController
*
* @since 1.7.0
* @throws \RuntimeException
*
* @deprecated 4.2 will be removed in 6.0
* Use the cache controller factory instead
* Example: Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($type, $options);
*/
public static function getInstance($type = 'output', $options = [])
{
@trigger_error(
sprintf(
'%s() is deprecated. The cache controller should be fetched from the factory.',
__METHOD__
),
E_USER_DEPRECATED
);
try {
return Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($type, $options);
} catch (\RuntimeException $e) {
$type = strtolower(preg_replace('/[^A-Z0-9_\.-]/i', '', $type));
$class = 'JCacheController' . ucfirst($type);
if (!class_exists($class)) {
// Search for the class file in the Cache include paths.
$path = Path::find(self::addIncludePath(), strtolower($type) . '.php');
if ($path !== false) {
\JLoader::register($class, $path);
}
// The class should now be loaded
if (!class_exists($class)) {
throw new \RuntimeException('Unable to load Cache Controller: ' . $type, 500);
}
// Only trigger a deprecation notice if the file and class are found
@trigger_error(
'Support for including cache controllers using path lookup is deprecated and will be removed in 5.0.'
. ' Use a custom cache controller factory instead.',
E_USER_DEPRECATED
);
}
return new $class($options);
}
}
/**
* Add a directory where Cache should search for controllers. You may either pass a string or an array of directories.
*
* @param array|string $path A path to search.
*
* @return array An array with directory elements
*
* @since 1.7.0
*
* @deprecated 4.2 will be removed in 6.0
* Use the cache controller factory instead
* Example: Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($type, $options);
*/
public static function addIncludePath($path = '')
{
static $paths;
if (!isset($paths)) {
$paths = [];
}
if (!empty($path) && !\in_array($path, $paths)) {
// Only trigger a deprecation notice when adding a lookup path
@trigger_error(
'Support for including cache controllers using path lookup is deprecated and will be removed in 5.0.'
. ' Use a custom cache controller factory instead.',
E_USER_DEPRECATED
);
array_unshift($paths, Path::clean($path));
}
return $paths;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Default factory for creating CacheController objects
*
* @since 4.0.0
*/
class CacheControllerFactory implements CacheControllerFactoryInterface
{
/**
* Method to get an instance of a cache controller.
*
* @param string $type The cache object type to instantiate
* @param array $options Array of options
*
* @return CacheController
*
* @since 4.0.0
* @throws \RuntimeException
*/
public function createCacheController($type = 'output', $options = []): CacheController
{
if (!$type) {
$type = 'output';
}
$type = strtolower(preg_replace('/[^A-Z0-9_\.-]/i', '', $type));
$class = __NAMESPACE__ . '\\Controller\\' . ucfirst($type) . 'Controller';
// The class should now be loaded
if (!class_exists($class)) {
throw new \RuntimeException('Unable to load Cache Controller: ' . $type, 500);
}
return new $class($options);
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface to be implemented by classes depending on a cache controller factory.
*
* @since 4.2.0
*/
interface CacheControllerFactoryAwareInterface
{
/**
* Set the cache controller factory to use.
*
* @param CacheControllerFactoryInterface $factory The cache controller factory to use.
*
* @return void
*
* @since 4.2.0
*/
public function setCacheControllerFactory(CacheControllerFactoryInterface $factory): void;
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Defines the trait for a CacheControllerFactoryInterface Aware Class.
*
* @since 4.2.0
*/
trait CacheControllerFactoryAwareTrait
{
/**
* CacheControllerFactoryInterface
*
* @var CacheControllerFactoryInterface
*
* @since 4.2.0
*/
private $cacheControllerFactory;
/**
* Get the CacheControllerFactoryInterface.
*
* @return CacheControllerFactoryInterface
*
* @since 4.2.0
*/
protected function getCacheControllerFactory(): CacheControllerFactoryInterface
{
if ($this->cacheControllerFactory) {
return $this->cacheControllerFactory;
}
@trigger_error(
sprintf('A cache controller is needed in %s. An UnexpectedValueException will be thrown in 5.0.', __CLASS__),
E_USER_DEPRECATED
);
return Factory::getContainer()->get(CacheControllerFactoryInterface::class);
}
/**
* Set the cache controller factory to use.
*
* @param CacheControllerFactoryInterface $cacheControllerFactory The cache controller factory to use.
*
* @return void
*
* @since 4.2.0
*/
public function setCacheControllerFactory(CacheControllerFactoryInterface $cacheControllerFactory = null): void
{
$this->cacheControllerFactory = $cacheControllerFactory;
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Interface defining a factory which can create CacheController objects
*
* @since 4.0.0
*/
interface CacheControllerFactoryInterface
{
/**
* Method to get an instance of a cache controller.
*
* @param string $type The cache object type to instantiate
* @param array $options Array of options
*
* @return CacheController
*
* @since 4.0.0
* @throws \RuntimeException
*/
public function createCacheController($type = 'output', $options = []): CacheController;
}

View File

@ -0,0 +1,384 @@
<?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\Cache;
use Joomla\CMS\Cache\Exception\UnsupportedCacheException;
use Joomla\CMS\Factory;
use Joomla\Filesystem\Path;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Abstract cache storage handler
*
* @since 1.7.0
* @note As of 4.0 this class will be abstract
*/
class CacheStorage
{
/**
* The raw object name
*
* @var string
* @since 1.7.0
*/
protected $rawname;
/**
* Time that the cache storage handler was instantiated
*
* @var integer
* @since 1.7.0
*/
public $_now;
/**
* Cache lifetime
*
* @var integer
* @since 1.7.0
*/
public $_lifetime;
/**
* Flag if locking is enabled
*
* @var boolean
* @since 1.7.0
*/
public $_locking;
/**
* Language code
*
* @var string
* @since 1.7.0
*/
public $_language;
/**
* Application name
*
* @var string
* @since 1.7.0
*/
public $_application;
/**
* Object hash
*
* @var string
* @since 1.7.0
*/
public $_hash;
/**
* The threshold
*
* @var integer
* @since 4.3.0
*/
public $_threshold;
/**
* Constructor
*
* @param array $options Optional parameters
*
* @since 1.7.0
*/
public function __construct($options = [])
{
$app = Factory::getApplication();
$this->_hash = md5($app->get('secret', ''));
$this->_application = $options['application'] ?? md5(JPATH_CONFIGURATION);
$this->_language = $options['language'] ?? 'en-GB';
$this->_locking = $options['locking'] ?? true;
$this->_lifetime = ($options['lifetime'] ?? $app->get('cachetime')) * 60;
$this->_now = $options['now'] ?? time();
// Set time threshold value. If the lifetime is not set, default to 60 (0 is BAD)
// _threshold is now available ONLY as a legacy (it's deprecated). It's no longer used in the core.
if (empty($this->_lifetime)) {
$this->_threshold = $this->_now - 60;
$this->_lifetime = 60;
} else {
$this->_threshold = $this->_now - $this->_lifetime;
}
}
/**
* Returns a cache storage handler object.
*
* @param string $handler The cache storage handler to instantiate
* @param array $options Array of handler options
*
* @return CacheStorage
*
* @since 1.7.0
* @throws \UnexpectedValueException
* @throws UnsupportedCacheException
*/
public static function getInstance($handler = null, $options = [])
{
static $now = null;
if (!isset($handler)) {
$handler = Factory::getApplication()->get('cache_handler');
if (empty($handler)) {
throw new \UnexpectedValueException('Cache Storage Handler not set.');
}
}
if (\is_null($now)) {
$now = time();
}
$options['now'] = $now;
// We can't cache this since options may change...
$handler = strtolower(preg_replace('/[^A-Z0-9_\.-]/i', '', $handler));
/** @var CacheStorage $class */
$class = __NAMESPACE__ . '\\Storage\\' . ucfirst($handler) . 'Storage';
if (!class_exists($class)) {
$class = 'JCacheStorage' . ucfirst($handler);
}
if (!class_exists($class)) {
// Search for the class file in the JCacheStorage include paths.
$path = Path::find(self::addIncludePath(), strtolower($handler) . '.php');
if ($path === false) {
throw new UnsupportedCacheException(sprintf('Unable to load Cache Storage: %s', $handler));
}
\JLoader::register($class, $path);
// The class should now be loaded
if (!class_exists($class)) {
throw new UnsupportedCacheException(sprintf('Unable to load Cache Storage: %s', $handler));
}
}
// Validate the cache storage is supported on this platform
if (!$class::isSupported()) {
throw new UnsupportedCacheException(sprintf('The %s Cache Storage is not supported on this platform.', $handler));
}
return new $class($options);
}
/**
* Check if the cache contains data stored by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.7.0
*/
public function contains($id, $group)
{
return false;
}
/**
* Get cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $checkTime True to verify cache time expiration threshold
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 1.7.0
*/
public function get($id, $group, $checkTime = true)
{
return false;
}
/**
* Get all cached data
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 1.7.0
*/
public function getAll()
{
return false;
}
/**
* Store the data to cache by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param string $data The data to store in cache
*
* @return boolean
*
* @since 1.7.0
*/
public function store($id, $group, $data)
{
return true;
}
/**
* Remove a cached data entry by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function remove($id, $group)
{
return true;
}
/**
* Clean cache for a group given a mode.
*
* group mode : cleans all cache in the group
* notgroup mode : cleans all cache not in the group
*
* @param string $group The cache data group
* @param string $mode The mode for cleaning cache [group|notgroup]
*
* @return boolean
*
* @since 1.7.0
*/
public function clean($group, $mode = null)
{
return true;
}
/**
* Flush all existing items in storage.
*
* @return boolean
*
* @since 3.6.3
*/
public function flush()
{
return true;
}
/**
* Garbage collect expired cache data
*
* @return boolean
*
* @since 1.7.0
*/
public function gc()
{
return true;
}
/**
* Test to see if the storage handler is available.
*
* @return boolean
*
* @since 3.0.0
*/
public static function isSupported()
{
return true;
}
/**
* Lock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param integer $locktime Cached item max lock time
*
* @return mixed Boolean false if locking failed or an object containing properties lock and locklooped
*
* @since 1.7.0
*/
public function lock($id, $group, $locktime)
{
return false;
}
/**
* Unlock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function unlock($id, $group = null)
{
return false;
}
/**
* Get a cache ID string from an ID/group pair
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return string
*
* @since 1.7.0
*/
protected function _getCacheId($id, $group)
{
$name = md5($this->_application . '-' . $id . '-' . $this->_language);
$this->rawname = $this->_hash . '-' . $name;
return Cache::getPlatformPrefix() . $this->_hash . '-cache-' . $group . '-' . $name;
}
/**
* Add a directory where CacheStorage should search for handlers. You may either pass a string or an array of directories.
*
* @param array|string $path A path to search.
*
* @return array An array with directory elements
*
* @since 1.7.0
*/
public static function addIncludePath($path = '')
{
static $paths;
if (!isset($paths)) {
$paths = [];
}
if (!empty($path) && !\in_array($path, $paths)) {
array_unshift($paths, Path::clean($path));
}
return $paths;
}
}

View File

@ -0,0 +1,208 @@
<?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\Cache\Controller;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheController;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Cache callback type object
*
* @since 1.7.0
*/
class CallbackController extends CacheController
{
/**
* Executes a cacheable callback if not found in cache else returns cached output and result
*
* @param callable $callback Callback or string shorthand for a callback
* @param array $args Callback arguments
* @param mixed $id Cache ID
* @param boolean $wrkarounds True to use workarounds
* @param array $woptions Workaround options
*
* @return mixed Result of the callback
*
* @since 1.7.0
*/
public function get($callback, $args = [], $id = false, $wrkarounds = false, $woptions = [])
{
if (!\is_array($args)) {
$referenceArgs = !empty($args) ? [&$args] : [];
} else {
$referenceArgs = &$args;
}
// Just execute the callback if caching is disabled.
if (empty($this->options['caching'])) {
return \call_user_func_array($callback, $referenceArgs);
}
if (!$id) {
// Generate an ID
$id = $this->_makeId($callback, $args);
}
$data = $this->cache->get($id);
$locktest = (object) ['locked' => null, 'locklooped' => null];
if ($data === false) {
$locktest = $this->cache->lock($id);
// If locklooped is true try to get the cached data again; it could exist now.
if ($locktest->locked === true && $locktest->locklooped === true) {
$data = $this->cache->get($id);
}
}
if ($data !== false) {
if ($locktest->locked === true) {
$this->cache->unlock($id);
}
$data = unserialize(trim($data));
if ($wrkarounds) {
echo Cache::getWorkarounds(
$data['output'],
['mergehead' => $woptions['mergehead'] ?? 0]
);
} else {
echo $data['output'];
}
return $data['result'];
}
if ($locktest->locked === false && $locktest->locklooped === true) {
// We can not store data because another process is in the middle of saving
return \call_user_func_array($callback, $referenceArgs);
}
$coptions = ['modulemode' => 0];
if (isset($woptions['modulemode']) && $woptions['modulemode'] == 1) {
/** @var HtmlDocument $document */
$document = Factory::getDocument();
if (method_exists($document, 'getHeadData')) {
$coptions['headerbefore'] = $document->getHeadData();
// Reset document head before rendering module. Module will cache only assets added by itself.
$document->resetHeadData();
$document->getWebAssetManager()->reset();
$coptions['modulemode'] = 1;
}
}
$coptions['nopathway'] = $woptions['nopathway'] ?? 1;
$coptions['nohead'] = $woptions['nohead'] ?? 1;
$coptions['nomodules'] = $woptions['nomodules'] ?? 1;
ob_start();
ob_implicit_flush(false);
$result = \call_user_func_array($callback, $referenceArgs);
$output = ob_get_clean();
$data = ['result' => $result];
if ($wrkarounds) {
$data['output'] = Cache::setWorkarounds($output, $coptions);
} else {
$data['output'] = $output;
}
// Restore document head data and merge module head data.
if ($coptions['modulemode'] == 1) {
$moduleHeadData = $document->getHeadData();
$document->resetHeadData();
$document->mergeHeadData($coptions['headerbefore']);
$document->mergeHeadData($moduleHeadData);
}
// Store the cache data
$this->cache->store(serialize($data), $id);
if ($locktest->locked === true) {
$this->cache->unlock($id);
}
echo $output;
return $result;
}
/**
* Store data to cache by ID and group
*
* @param mixed $data The data to store
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $wrkarounds True to use wrkarounds
*
* @return boolean True if cache stored
*
* @since 4.0.0
*/
public function store($data, $id, $group = null, $wrkarounds = true)
{
$locktest = $this->cache->lock($id, $group);
if ($locktest->locked === false && $locktest->locklooped === true) {
// We can not store data because another process is in the middle of saving
return false;
}
$result = $this->cache->store(serialize($data), $id, $group);
if ($locktest->locked === true) {
$this->cache->unlock($id, $group);
}
return $result;
}
/**
* Generate a callback cache ID
*
* @param mixed $callback Callback to cache
* @param array $args Arguments to the callback method to cache
*
* @return string MD5 Hash
*
* @since 1.7.0
*/
protected function _makeId($callback, $args)
{
if (\is_array($callback) && \is_object($callback[0])) {
$vars = get_object_vars($callback[0]);
$vars[] = strtolower(\get_class($callback[0]));
$callback[0] = $vars;
}
// A Closure can't be serialized, so to generate the ID we'll need to get its hash
if ($callback instanceof \closure) {
$hash = spl_object_hash($callback);
return md5($hash . serialize([$args]));
}
return md5(serialize([$callback, $args]));
}
}

View File

@ -0,0 +1,106 @@
<?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\Cache\Controller;
use Joomla\CMS\Cache\CacheController;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla Cache output type object
*
* @since 1.7.0
*/
class OutputController extends CacheController
{
/**
* Cache data ID
*
* @var string
* @since 1.7.0
*/
protected $_id;
/**
* Cache data group
*
* @var string
* @since 1.7.0
*/
protected $_group;
/**
* Get stored cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return mixed Boolean false on no result, cached object otherwise
*
* @since 1.7.0
*/
public function get($id, $group = null)
{
$data = $this->cache->get($id, $group);
if ($data === false) {
$locktest = $this->cache->lock($id, $group);
// If locklooped is true try to get the cached data again; it could exist now.
if ($locktest->locked === true && $locktest->locklooped === true) {
$data = $this->cache->get($id, $group);
}
if ($locktest->locked === true) {
$this->cache->unlock($id, $group);
}
}
// Check again because we might get it from second attempt
if ($data !== false) {
// Trim to fix unserialize errors
$data = unserialize(trim($data));
}
return $data;
}
/**
* Store data to cache by ID and group
*
* @param mixed $data The data to store
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $wrkarounds True to use wrkarounds
*
* @return boolean True if cache stored
*
* @since 1.7.0
*/
public function store($data, $id, $group = null, $wrkarounds = true)
{
$locktest = $this->cache->lock($id, $group);
if ($locktest->locked === false && $locktest->locklooped === true) {
// We can not store data because another process is in the middle of saving
return false;
}
$result = $this->cache->store(serialize($data), $id, $group);
if ($locktest->locked === true) {
$this->cache->unlock($id, $group);
}
return $result;
}
}

View File

@ -0,0 +1,217 @@
<?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\Cache\Controller;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheController;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Cache page type object
*
* @since 1.7.0
*/
class PageController extends CacheController
{
/**
* ID property for the cache page object.
*
* @var integer
* @since 1.7.0
*/
protected $_id;
/**
* Cache group
*
* @var string
* @since 1.7.0
*/
protected $_group;
/**
* Cache lock test
*
* @var \stdClass
* @since 1.7.0
*/
protected $_locktest = null;
/**
* Get the cached page data
*
* @param boolean $id The cache data ID
* @param string $group The cache data group
*
* @return mixed Boolean false on no result, cached object otherwise
*
* @since 1.7.0
*/
public function get($id = false, $group = 'page')
{
// If an id is not given, generate it from the request
if (!$id) {
$id = $this->_makeId();
}
// If the etag matches the page id ... set a no change header and exit : utilize browser cache
if (!headers_sent() && isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
$etag = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
if ($etag == $id) {
$browserCache = $this->options['browsercache'] ?? false;
if ($browserCache) {
$this->_noChange();
}
}
}
// We got a cache hit... set the etag header and echo the page data
$data = $this->cache->get($id, $group);
$this->_locktest = (object) ['locked' => null, 'locklooped' => null];
if ($data === false) {
$this->_locktest = $this->cache->lock($id, $group);
// If locklooped is true try to get the cached data again; it could exist now.
if ($this->_locktest->locked === true && $this->_locktest->locklooped === true) {
$data = $this->cache->get($id, $group);
}
}
if ($data !== false) {
if ($this->_locktest->locked === true) {
$this->cache->unlock($id, $group);
}
$data = unserialize(trim($data));
$data = Cache::getWorkarounds($data);
$this->_setEtag($id);
return $data;
}
// Set ID and group placeholders
$this->_id = $id;
$this->_group = $group;
return false;
}
/**
* Stop the cache buffer and store the cached data
*
* @param mixed $data The data to store
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $wrkarounds True to use workarounds
*
* @return boolean
*
* @since 1.7.0
*/
public function store($data, $id, $group = null, $wrkarounds = true)
{
if ($this->_locktest->locked === false && $this->_locktest->locklooped === true) {
// We can not store data because another process is in the middle of saving
return false;
}
// Get page data from the application object
if (!$data) {
$data = Factory::getApplication()->getBody();
// Only attempt to store if page data exists.
if (!$data) {
return false;
}
}
// Get id and group and reset the placeholders
if (!$id) {
$id = $this->_id;
}
if (!$group) {
$group = $this->_group;
}
if ($wrkarounds) {
$data = Cache::setWorkarounds(
$data,
[
'nopathway' => 1,
'nohead' => 1,
'nomodules' => 1,
'headers' => true,
]
);
}
$result = $this->cache->store(serialize($data), $id, $group);
if ($this->_locktest->locked === true) {
$this->cache->unlock($id, $group);
}
return $result;
}
/**
* Generate a page cache id
*
* @return string MD5 Hash
*
* @since 1.7.0
* @todo Discuss whether this should be coupled to a data hash or a request hash ... perhaps hashed with a serialized request
*/
protected function _makeId()
{
return Cache::makeId();
}
/**
* There is no change in page data so send an unmodified header and die gracefully
*
* @return void
*
* @since 1.7.0
*/
protected function _noChange()
{
$app = Factory::getApplication();
// Send not modified header and exit gracefully
$app->setHeader('Status', 304, true);
$app->sendHeaders();
$app->close();
}
/**
* Set the ETag header in the response
*
* @param string $etag The entity tag (etag) to set
*
* @return void
*
* @since 1.7.0
*/
protected function _setEtag($etag)
{
Factory::getApplication()->setHeader('ETag', '"' . $etag . '"', true);
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Controller;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheController;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Cache view type object
*
* @since 1.7.0
*/
class ViewController extends CacheController
{
/**
* Get the cached view data
*
* @param object $view The view object to cache output for
* @param string $method The method name of the view method to cache output for
* @param mixed $id The cache data ID
* @param boolean $wrkarounds True to enable workarounds.
*
* @return boolean True if the cache is hit (false else)
*
* @since 1.7.0
*/
public function get($view, $method = 'display', $id = false, $wrkarounds = true)
{
// If an id is not given generate it from the request
if (!$id) {
$id = $this->_makeId($view, $method);
}
$data = $this->cache->get($id);
$locktest = (object) ['locked' => null, 'locklooped' => null];
if ($data === false) {
$locktest = $this->cache->lock($id);
/*
* If the loop is completed and returned true it means the lock has been set.
* If looped is true try to get the cached data again; it could exist now.
*/
if ($locktest->locked === true && $locktest->locklooped === true) {
$data = $this->cache->get($id);
}
// False means that locking is either turned off or maxtime has been exceeded. Execute the view.
}
if ($data !== false) {
if ($locktest->locked === true) {
$this->cache->unlock($id);
}
$data = unserialize(trim($data));
if ($wrkarounds) {
echo Cache::getWorkarounds($data);
} else {
// No workarounds, so all data is stored in one piece
echo $data;
}
return true;
}
// No hit so we have to execute the view
if (!method_exists($view, $method)) {
return false;
}
if ($locktest->locked === false && $locktest->locklooped === true) {
// We can not store data because another process is in the middle of saving
$view->$method();
return false;
}
// Capture and echo output
ob_start();
ob_implicit_flush(false);
$view->$method();
$data = ob_get_clean();
echo $data;
/*
* For a view we have a special case. We need to cache not only the output from the view, but the state
* of the document head after the view has been rendered. This will allow us to properly cache any attached
* scripts or stylesheets or links or any other modifications that the view has made to the document object
*/
if ($wrkarounds) {
$data = Cache::setWorkarounds($data);
}
// Store the cache data
$this->cache->store(serialize($data), $id);
if ($locktest->locked === true) {
$this->cache->unlock($id);
}
return false;
}
/**
* Store data to cache by ID and group
*
* @param mixed $data The data to store
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $wrkarounds True to use wrkarounds
*
* @return boolean True if cache stored
*
* @since 4.0.0
*/
public function store($data, $id, $group = null, $wrkarounds = true)
{
$locktest = $this->cache->lock($id, $group);
if ($locktest->locked === false && $locktest->locklooped === true) {
// We can not store data because another process is in the middle of saving
return false;
}
$result = $this->cache->store(serialize($data), $id, $group);
if ($locktest->locked === true) {
$this->cache->unlock($id, $group);
}
return $result;
}
/**
* Generate a view cache ID.
*
* @param object $view The view object to cache output for
* @param string $method The method name to cache for the view object
*
* @return string MD5 Hash
*
* @since 1.7.0
*/
protected function _makeId($view, $method)
{
return md5(serialize([Cache::makeId(), \get_class($view), $method]));
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining an error connecting to the cache storage engine
*
* @since 3.6.3
*/
class CacheConnectingException extends \RuntimeException implements CacheExceptionInterface
{
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception interface defining a cache storage error
*
* @since 3.7.0
*/
interface CacheExceptionInterface
{
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining an unsupported cache storage object
*
* @since 3.6.3
*/
class UnsupportedCacheException extends \RuntimeException implements CacheExceptionInterface
{
}

View File

@ -0,0 +1,299 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Storage;
use Joomla\CMS\Cache\CacheStorage;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* APCu cache storage handler
*
* @link https://www.php.net/manual/en/ref.apcu.php
* @since 3.5
*/
class ApcuStorage extends CacheStorage
{
/**
* Check if the cache contains data stored by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.7.0
*/
public function contains($id, $group)
{
return apcu_exists($this->_getCacheId($id, $group));
}
/**
* Get cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $checkTime True to verify cache time expiration threshold
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 3.5
*/
public function get($id, $group, $checkTime = true)
{
return apcu_fetch($this->_getCacheId($id, $group));
}
/**
* Get all cached data
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 3.5
*/
public function getAll()
{
$allinfo = apcu_cache_info();
$keys = $allinfo['cache_list'];
$secret = $this->_hash;
$data = [];
foreach ($keys as $key) {
if (isset($key['info'])) {
// The internal key name changed with APCu 4.0.7 from key to info
$name = $key['info'];
} elseif (isset($key['entry_name'])) {
// Some APCu modules changed the internal key name from key to entry_name
$name = $key['entry_name'];
} else {
// A fall back for the old internal key name
$name = $key['key'];
}
$namearr = explode('-', $name);
if ($namearr !== false && $namearr[0] == $secret && $namearr[1] === 'cache') {
$group = $namearr[2];
if (!isset($data[$group])) {
$item = new CacheStorageHelper($group);
} else {
$item = $data[$group];
}
$item->updateSize($key['mem_size']);
$data[$group] = $item;
}
}
return $data;
}
/**
* Store the data to cache by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param string $data The data to store in cache
*
* @return boolean
*
* @since 3.5
*/
public function store($id, $group, $data)
{
return apcu_store($this->_getCacheId($id, $group), $data, $this->_lifetime);
}
/**
* Remove a cached data entry by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.5
*/
public function remove($id, $group)
{
$cache_id = $this->_getCacheId($id, $group);
// The apcu_delete function returns false if the ID does not exist
if (apcu_exists($cache_id)) {
return apcu_delete($cache_id);
}
return true;
}
/**
* Clean cache for a group given a mode.
*
* group mode : cleans all cache in the group
* notgroup mode : cleans all cache not in the group
*
* @param string $group The cache data group
* @param string $mode The mode for cleaning cache [group|notgroup]
*
* @return boolean
*
* @since 3.5
*/
public function clean($group, $mode = null)
{
$allinfo = apcu_cache_info();
$keys = $allinfo['cache_list'];
$secret = $this->_hash;
foreach ($keys as $key) {
if (isset($key['info'])) {
// The internal key name changed with APCu 4.0.7 from key to info
$internalKey = $key['info'];
} elseif (isset($key['entry_name'])) {
// Some APCu modules changed the internal key name from key to entry_name
$internalKey = $key['entry_name'];
} else {
// A fall back for the old internal key name
$internalKey = $key['key'];
}
if (strpos($internalKey, $secret . '-cache-' . $group . '-') === 0 xor $mode !== 'group') {
apcu_delete($internalKey);
}
}
return true;
}
/**
* Garbage collect expired cache data
*
* @return boolean
*
* @since 3.5
*/
public function gc()
{
$allinfo = apcu_cache_info();
$keys = $allinfo['cache_list'];
$secret = $this->_hash;
foreach ($keys as $key) {
if (isset($key['info'])) {
// The internal key name changed with APCu 4.0.7 from key to info
$internalKey = $key['info'];
} elseif (isset($key['entry_name'])) {
// Some APCu modules changed the internal key name from key to entry_name
$internalKey = $key['entry_name'];
} else {
// A fall back for the old internal key name
$internalKey = $key['key'];
}
if (strpos($internalKey, $secret . '-cache-')) {
apcu_fetch($internalKey);
}
}
return true;
}
/**
* Test to see if the storage handler is available.
*
* @return boolean
*
* @since 3.5
*/
public static function isSupported()
{
$supported = \extension_loaded('apcu') && ini_get('apc.enabled');
// If on the CLI interface, the `apc.enable_cli` option must also be enabled
if ($supported && PHP_SAPI === 'cli') {
$supported = ini_get('apc.enable_cli');
}
return (bool) $supported;
}
/**
* Lock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param integer $locktime Cached item max lock time
*
* @return mixed Boolean false if locking failed or an object containing properties lock and locklooped
*
* @since 3.5
*/
public function lock($id, $group, $locktime)
{
$returning = new \stdClass();
$returning->locklooped = false;
$looptime = $locktime * 10;
$cache_id = $this->_getCacheId($id, $group) . '_lock';
$data_lock = apcu_add($cache_id, 1, $locktime);
if ($data_lock === false) {
$lock_counter = 0;
// Loop until you find that the lock has been released.
// That implies that data get from other thread has finished
while ($data_lock === false) {
if ($lock_counter > $looptime) {
$returning->locked = false;
$returning->locklooped = true;
break;
}
usleep(100);
$data_lock = apcu_add($cache_id, 1, $locktime);
$lock_counter++;
}
}
$returning->locked = $data_lock;
return $returning;
}
/**
* Unlock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.5
*/
public function unlock($id, $group = null)
{
$cache_id = $this->_getCacheId($id, $group) . '_lock';
// The apcu_delete function returns false if the ID does not exist
if (apcu_exists($cache_id)) {
return apcu_delete($cache_id);
}
return true;
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Storage;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Cache storage helper functions.
*
* @since 1.7.0
*/
class CacheStorageHelper
{
/**
* Cache data group
*
* @var string
* @since 1.7.0
*/
public $group = '';
/**
* Cached item size
*
* @var int
* @since 1.7.0
*/
public $size = 0;
/**
* Counter
*
* @var int
* @since 1.7.0
*/
public $count = 0;
/**
* Constructor
*
* @param string $group The cache data group
*
* @since 1.7.0
*/
public function __construct($group)
{
$this->group = $group;
}
/**
* Increase cache items count.
*
* @param int $size Cached item size
*
* @return void
*
* @since 1.7.0
*/
public function updateSize($size)
{
$this->size += $size;
$this->count++;
}
}

View File

@ -0,0 +1,718 @@
<?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\Cache\Storage;
use Joomla\CMS\Cache\CacheStorage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\Filesystem\File;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* File cache storage handler
*
* @since 1.7.0
* @note For performance reasons this class does not use the Filesystem package's API
*/
class FileStorage extends CacheStorage
{
/**
* Root path
*
* @var string
* @since 1.7.0
*/
protected $_root;
/**
* Locked resources
*
* @var array
* @since 3.7.0
*
*/
protected $_locked_files = [];
/**
* Constructor
*
* @param array $options Optional parameters
*
* @since 1.7.0
*/
public function __construct($options = [])
{
parent::__construct($options);
$this->_root = $options['cachebase'];
// Workaround for php 5.3
$locked_files = &$this->_locked_files;
// Remove empty locked files at script shutdown.
$clearAtShutdown = function () use (&$locked_files) {
foreach ($locked_files as $path => $handle) {
if (\is_resource($handle)) {
@flock($handle, LOCK_UN);
@fclose($handle);
}
// Delete only the existing file if it is empty.
if (@filesize($path) === 0) {
File::invalidateFileCache($path);
@unlink($path);
}
unset($locked_files[$path]);
}
};
register_shutdown_function($clearAtShutdown);
}
/**
* Check if the cache contains data stored by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.7.0
*/
public function contains($id, $group)
{
return $this->_checkExpire($id, $group);
}
/**
* Get cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $checkTime True to verify cache time expiration threshold
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 1.7.0
*/
public function get($id, $group, $checkTime = true)
{
$path = $this->_getFilePath($id, $group);
$close = false;
if ($checkTime == false || ($checkTime == true && $this->_checkExpire($id, $group) === true)) {
if (file_exists($path)) {
if (isset($this->_locked_files[$path])) {
$_fileopen = $this->_locked_files[$path];
} else {
$_fileopen = @fopen($path, 'rb');
// There is no lock, we have to close file after store data
$close = true;
}
if ($_fileopen) {
// On Windows system we can not use file_get_contents on the file locked by yourself
$data = stream_get_contents($_fileopen);
if ($close) {
@fclose($_fileopen);
}
if ($data !== false) {
// Remove the initial die() statement
return str_replace('<?php die("Access Denied"); ?>#x#', '', $data);
}
}
}
}
return false;
}
/**
* Get all cached data
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 1.7.0
*/
public function getAll()
{
$path = $this->_root;
$folders = $this->_folders($path);
$data = [];
foreach ($folders as $folder) {
$files = $this->_filesInFolder($path . '/' . $folder);
$item = new CacheStorageHelper($folder);
foreach ($files as $file) {
$item->updateSize(filesize($path . '/' . $folder . '/' . $file));
}
$data[$folder] = $item;
}
return $data;
}
/**
* Store the data to cache by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param string $data The data to store in cache
*
* @return boolean
*
* @since 1.7.0
*/
public function store($id, $group, $data)
{
$path = $this->_getFilePath($id, $group);
$close = false;
// Prepend a die string
$data = '<?php die("Access Denied"); ?>#x#' . $data;
if (isset($this->_locked_files[$path])) {
$_fileopen = $this->_locked_files[$path];
// Because lock method uses flag c+b we have to truncate it manually
@ftruncate($_fileopen, 0);
} else {
$_fileopen = @fopen($path, 'wb');
// There is no lock, we have to close file after store data
$close = true;
}
if ($_fileopen) {
$length = \strlen($data);
$result = @fwrite($_fileopen, $data, $length);
if ($close) {
@fclose($_fileopen);
}
return $result === $length;
}
return false;
}
/**
* Remove a cached data entry by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function remove($id, $group)
{
$path = $this->_getFilePath($id, $group);
File::invalidateFileCache($path);
if (!@unlink($path)) {
return false;
}
return true;
}
/**
* Clean cache for a group given a mode.
*
* group mode : cleans all cache in the group
* notgroup mode : cleans all cache not in the group
*
* @param string $group The cache data group
* @param string $mode The mode for cleaning cache [group|notgroup]
*
* @return boolean
*
* @since 1.7.0
*/
public function clean($group, $mode = null)
{
$return = true;
$folder = $group;
if (trim($folder) == '') {
$mode = 'notgroup';
}
switch ($mode) {
case 'notgroup':
$folders = $this->_folders($this->_root);
for ($i = 0, $n = \count($folders); $i < $n; $i++) {
if ($folders[$i] != $folder) {
$return |= $this->_deleteFolder($this->_root . '/' . $folders[$i]);
}
}
break;
case 'group':
default:
if (is_dir($this->_root . '/' . $folder)) {
$return = $this->_deleteFolder($this->_root . '/' . $folder);
}
break;
}
return (bool) $return;
}
/**
* Garbage collect expired cache data
*
* @return boolean
*
* @since 1.7.0
*/
public function gc()
{
$result = true;
// Files older than lifeTime get deleted from cache
$files = $this->_filesInFolder($this->_root, '', true, true, ['.svn', 'CVS', '.DS_Store', '__MACOSX', 'index.html']);
foreach ($files as $file) {
$time = @filemtime($file);
if (($time + $this->_lifetime) < $this->_now || empty($time)) {
File::invalidateFileCache($file);
$result |= @unlink($file);
}
}
return (bool) $result;
}
/**
* Lock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param integer $locktime Cached item max lock time
*
* @return mixed Boolean false if locking failed or an object containing properties lock and locklooped
*
* @since 1.7.0
*/
public function lock($id, $group, $locktime)
{
$returning = new \stdClass();
$returning->locklooped = false;
$looptime = $locktime * 10;
$path = $this->_getFilePath($id, $group);
$_fileopen = @fopen($path, 'c+b');
if (!$_fileopen) {
$returning->locked = false;
return $returning;
}
$data_lock = (bool) @flock($_fileopen, LOCK_EX | LOCK_NB);
if ($data_lock === false) {
$lock_counter = 0;
// Loop until you find that the lock has been released.
// That implies that data get from other thread has finished
while ($data_lock === false) {
if ($lock_counter > $looptime) {
break;
}
usleep(100);
$data_lock = (bool) @flock($_fileopen, LOCK_EX | LOCK_NB);
$lock_counter++;
}
$returning->locklooped = true;
}
if ($data_lock === true) {
// Remember resource, flock release lock if you unset/close resource
$this->_locked_files[$path] = $_fileopen;
}
$returning->locked = $data_lock;
return $returning;
}
/**
* Unlock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 1.7.0
*/
public function unlock($id, $group = null)
{
$path = $this->_getFilePath($id, $group);
if (isset($this->_locked_files[$path])) {
$ret = (bool) @flock($this->_locked_files[$path], LOCK_UN);
@fclose($this->_locked_files[$path]);
unset($this->_locked_files[$path]);
return $ret;
}
return true;
}
/**
* Check if a cache object has expired
*
* Using @ error suppressor here because between if we did a file_exists() and then filemsize() there will
* be a little time space when another process can delete the file and then you get PHP Warning
*
* @param string $id Cache ID to check
* @param string $group The cache data group
*
* @return boolean True if the cache ID is valid
*
* @since 1.7.0
*/
protected function _checkExpire($id, $group)
{
$path = $this->_getFilePath($id, $group);
// Check prune period
if (file_exists($path)) {
$time = @filemtime($path);
if (($time + $this->_lifetime) < $this->_now || empty($time)) {
File::invalidateFileCache($path);
@unlink($path);
return false;
}
// If, right now, the file does not exist then return false
if (@filesize($path) == 0) {
return false;
}
return true;
}
return false;
}
/**
* Get a cache file path from an ID/group pair
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean|string The path to the data object or boolean false if the cache directory does not exist
*
* @since 1.7.0
*/
protected function _getFilePath($id, $group)
{
$name = $this->_getCacheId($id, $group);
$dir = $this->_root . '/' . $group;
// If the folder doesn't exist try to create it
if (!is_dir($dir)) {
// Make sure the index file is there
$indexFile = $dir . '/index.html';
@mkdir($dir) && file_put_contents($indexFile, '<!DOCTYPE html><title></title>');
}
// Make sure the folder exists
if (!is_dir($dir)) {
return false;
}
return $dir . '/' . $name . '.php';
}
/**
* Quickly delete a folder of files
*
* @param string $path The path to the folder to delete.
*
* @return boolean
*
* @since 1.7.0
*/
protected function _deleteFolder($path)
{
// Sanity check
if (!$path || !is_dir($path) || empty($this->_root)) {
// Bad programmer! Bad, bad programmer!
Log::add(__METHOD__ . ' ' . Text::_('JLIB_FILESYSTEM_ERROR_DELETE_BASE_DIRECTORY'), Log::WARNING, 'jerror');
return false;
}
$path = $this->_cleanPath($path);
// Check to make sure path is inside cache folder, we do not want to delete Joomla root!
$pos = strpos($path, $this->_cleanPath($this->_root));
if ($pos === false || $pos > 0) {
Log::add(__METHOD__ . ' ' . Text::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', __METHOD__, $path), Log::WARNING, 'jerror');
return false;
}
// Remove all the files in folder if they exist; disable all filtering
$files = $this->_filesInFolder($path, '.', false, true, [], []);
if (!empty($files) && !\is_array($files)) {
File::invalidateFileCache($files);
if (@unlink($files) !== true) {
return false;
}
} elseif (!empty($files) && \is_array($files)) {
foreach ($files as $file) {
$file = $this->_cleanPath($file);
// In case of restricted permissions we delete it one way or the other as long as the owner is either the webserver or the ftp
File::invalidateFileCache($file);
if (@unlink($file) !== true) {
Log::add(__METHOD__ . ' ' . Text::sprintf('JLIB_FILESYSTEM_DELETE_FAILED', basename($file)), Log::WARNING, 'jerror');
return false;
}
}
}
// Remove sub-folders of folder; disable all filtering
$folders = $this->_folders($path, '.', false, true, [], []);
foreach ($folders as $folder) {
if (is_link($folder)) {
// Don't descend into linked directories, just delete the link.
if (@unlink($folder) !== true) {
return false;
}
} elseif ($this->_deleteFolder($folder) !== true) {
return false;
}
}
// In case of restricted permissions we zap it one way or the other as long as the owner is either the webserver or the ftp
if (@rmdir($path)) {
return true;
}
Log::add(Text::sprintf('JLIB_FILESYSTEM_ERROR_FOLDER_DELETE', $path), Log::WARNING, 'jerror');
return false;
}
/**
* Function to strip additional / or \ in a path name
*
* @param string $path The path to clean
* @param string $ds Directory separator (optional)
*
* @return string The cleaned path
*
* @since 1.7.0
*/
protected function _cleanPath($path, $ds = DIRECTORY_SEPARATOR)
{
$path = trim($path);
if (empty($path)) {
return $this->_root;
}
// Remove double slashes and backslashes and convert all slashes and backslashes to DIRECTORY_SEPARATOR
$path = preg_replace('#[/\\\\]+#', $ds, $path);
return $path;
}
/**
* Utility function to quickly read the files in a folder.
*
* @param string $path The path of the folder to read.
* @param string $filter A filter for file names.
* @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
* @param boolean $fullpath True to return the full path to the file.
* @param array $exclude Array with names of files which should not be shown in the result.
* @param array $excludefilter Array of folder names to exclude
*
* @return array Files in the given folder.
*
* @since 1.7.0
*/
protected function _filesInFolder(
$path,
$filter = '.',
$recurse = false,
$fullpath = false,
$exclude = ['.svn', 'CVS', '.DS_Store', '__MACOSX'],
$excludefilter = ['^\..*', '.*~']
) {
$arr = [];
// Check to make sure the path valid and clean
$path = $this->_cleanPath($path);
// Is the path a folder?
if (!is_dir($path)) {
Log::add(__METHOD__ . ' ' . Text::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', __METHOD__, $path), Log::WARNING, 'jerror');
return false;
}
// Read the source directory.
if (!($handle = @opendir($path))) {
return $arr;
}
if (\count($excludefilter)) {
$excludefilter = '/(' . implode('|', $excludefilter) . ')/';
} else {
$excludefilter = '';
}
while (($file = readdir($handle)) !== false) {
if (($file != '.') && ($file != '..') && (!\in_array($file, $exclude)) && (!$excludefilter || !preg_match($excludefilter, $file))) {
$dir = $path . '/' . $file;
$isDir = is_dir($dir);
if ($isDir) {
if ($recurse) {
if (\is_int($recurse)) {
$arr2 = $this->_filesInFolder($dir, $filter, $recurse - 1, $fullpath);
} else {
$arr2 = $this->_filesInFolder($dir, $filter, $recurse, $fullpath);
}
$arr = array_merge($arr, $arr2);
}
} else {
if (preg_match("/$filter/", $file)) {
if ($fullpath) {
$arr[] = $path . '/' . $file;
} else {
$arr[] = $file;
}
}
}
}
}
closedir($handle);
return $arr;
}
/**
* Utility function to read the folders in a folder.
*
* @param string $path The path of the folder to read.
* @param string $filter A filter for folder names.
* @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
* @param boolean $fullpath True to return the full path to the folders.
* @param array $exclude Array with names of folders which should not be shown in the result.
* @param array $excludefilter Array with regular expressions matching folders which should not be shown in the result.
*
* @return array Folders in the given folder.
*
* @since 1.7.0
*/
protected function _folders(
$path,
$filter = '.',
$recurse = false,
$fullpath = false,
$exclude = ['.svn', 'CVS', '.DS_Store', '__MACOSX'],
$excludefilter = ['^\..*']
) {
$arr = [];
// Check to make sure the path valid and clean
$path = $this->_cleanPath($path);
// Is the path a folder?
if (!is_dir($path)) {
Log::add(__METHOD__ . ' ' . Text::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', __METHOD__, $path), Log::WARNING, 'jerror');
return false;
}
// Read the source directory
if (!($handle = @opendir($path))) {
return $arr;
}
if (\count($excludefilter)) {
$excludefilter_string = '/(' . implode('|', $excludefilter) . ')/';
} else {
$excludefilter_string = '';
}
while (($file = readdir($handle)) !== false) {
if (
($file != '.') && ($file != '..')
&& (!\in_array($file, $exclude))
&& (empty($excludefilter_string) || !preg_match($excludefilter_string, $file))
) {
$dir = $path . '/' . $file;
$isDir = is_dir($dir);
if ($isDir) {
// Removes filtered directories
if (preg_match("/$filter/", $file)) {
if ($fullpath) {
$arr[] = $dir;
} else {
$arr[] = $file;
}
}
if ($recurse) {
if (\is_int($recurse)) {
$arr2 = $this->_folders($dir, $filter, $recurse - 1, $fullpath, $exclude, $excludefilter);
} else {
$arr2 = $this->_folders($dir, $filter, $recurse, $fullpath, $exclude, $excludefilter);
}
$arr = array_merge($arr, $arr2);
}
}
}
}
closedir($handle);
return $arr;
}
}

View File

@ -0,0 +1,458 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Storage;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheStorage;
use Joomla\CMS\Cache\Exception\CacheConnectingException;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Memcached cache storage handler
*
* @link https://www.php.net/manual/en/book.memcached.php
* @since 3.0.0
*/
class MemcachedStorage extends CacheStorage
{
/**
* Memcached connection object
*
* @var \Memcached
* @since 3.0.0
*/
protected static $_db = null;
/**
* Payload compression level
*
* @var integer
* @since 3.0.0
*/
protected $_compress = 0;
/**
* Constructor
*
* @param array $options Optional parameters.
*
* @since 3.0.0
*/
public function __construct($options = [])
{
parent::__construct($options);
$this->_compress = Factory::getApplication()->get('memcached_compress', false) ? \Memcached::OPT_COMPRESSION : 0;
if (static::$_db === null) {
$this->getConnection();
}
}
/**
* Create the Memcached connection
*
* @return void
*
* @since 3.0.0
* @throws \RuntimeException
*/
protected function getConnection()
{
if (!static::isSupported()) {
throw new \RuntimeException('Memcached Extension is not available');
}
$app = Factory::getApplication();
$host = $app->get('memcached_server_host', 'localhost');
$port = $app->get('memcached_server_port', 11211);
// Create the memcached connection
if ($app->get('memcached_persist', true)) {
static::$_db = new \Memcached($this->_hash);
$servers = static::$_db->getServerList();
if ($servers && ($servers[0]['host'] != $host || $servers[0]['port'] != $port)) {
static::$_db->resetServerList();
$servers = [];
}
if (!$servers) {
static::$_db->addServer($host, $port);
}
} else {
static::$_db = new \Memcached();
static::$_db->addServer($host, $port);
}
static::$_db->setOption(\Memcached::OPT_COMPRESSION, $this->_compress);
$stats = static::$_db->getStats();
$result = !empty($stats["$host:$port"]) && $stats["$host:$port"]['pid'] > 0;
if (!$result) {
// Null out the connection to inform the constructor it will need to attempt to connect if this class is instantiated again
static::$_db = null;
throw new CacheConnectingException('Could not connect to memcached server');
}
}
/**
* Get a cache_id string from an id/group pair
*
* @param string $id The cache data id
* @param string $group The cache data group
*
* @return string The cache_id string
*
* @since 1.7.0
*/
protected function _getCacheId($id, $group)
{
$prefix = Cache::getPlatformPrefix();
$length = \strlen($prefix);
$cache_id = parent::_getCacheId($id, $group);
if ($length) {
// Memcached use suffix instead of prefix
$cache_id = substr($cache_id, $length) . strrev($prefix);
}
return $cache_id;
}
/**
* Check if the cache contains data stored by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.7.0
*/
public function contains($id, $group)
{
static::$_db->get($this->_getCacheId($id, $group));
return static::$_db->getResultCode() !== \Memcached::RES_NOTFOUND;
}
/**
* Get cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $checkTime True to verify cache time expiration threshold
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 3.0.0
*/
public function get($id, $group, $checkTime = true)
{
return static::$_db->get($this->_getCacheId($id, $group));
}
/**
* Get all cached data
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 3.0.0
*/
public function getAll()
{
$keys = static::$_db->get($this->_hash . '-index');
$secret = $this->_hash;
$data = [];
if (\is_array($keys)) {
foreach ($keys as $key) {
if (empty($key)) {
continue;
}
$namearr = explode('-', $key->name);
if ($namearr !== false && $namearr[0] == $secret && $namearr[1] === 'cache') {
$group = $namearr[2];
if (!isset($data[$group])) {
$item = new CacheStorageHelper($group);
} else {
$item = $data[$group];
}
$item->updateSize($key->size);
$data[$group] = $item;
}
}
}
return $data;
}
/**
* Store the data to cache by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param string $data The data to store in cache
*
* @return boolean
*
* @since 3.0.0
*/
public function store($id, $group, $data)
{
$cache_id = $this->_getCacheId($id, $group);
if (!$this->lockindex()) {
return false;
}
$index = static::$_db->get($this->_hash . '-index');
if (!\is_array($index)) {
$index = [];
}
$tmparr = new \stdClass();
$tmparr->name = $cache_id;
$tmparr->size = \strlen($data);
$index[] = $tmparr;
static::$_db->set($this->_hash . '-index', $index, 0);
$this->unlockindex();
static::$_db->set($cache_id, $data, $this->_lifetime);
return true;
}
/**
* Remove a cached data entry by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.0.0
*/
public function remove($id, $group)
{
$cache_id = $this->_getCacheId($id, $group);
if (!$this->lockindex()) {
return false;
}
$index = static::$_db->get($this->_hash . '-index');
if (\is_array($index)) {
foreach ($index as $key => $value) {
if ($value->name == $cache_id) {
unset($index[$key]);
static::$_db->set($this->_hash . '-index', $index, 0);
break;
}
}
}
$this->unlockindex();
return static::$_db->delete($cache_id);
}
/**
* Clean cache for a group given a mode.
*
* group mode : cleans all cache in the group
* notgroup mode : cleans all cache not in the group
*
* @param string $group The cache data group
* @param string $mode The mode for cleaning cache [group|notgroup]
*
* @return boolean
*
* @since 3.0.0
*/
public function clean($group, $mode = null)
{
if (!$this->lockindex()) {
return false;
}
$index = static::$_db->get($this->_hash . '-index');
if (\is_array($index)) {
$prefix = $this->_hash . '-cache-' . $group . '-';
foreach ($index as $key => $value) {
if (strpos($value->name, $prefix) === 0 xor $mode !== 'group') {
static::$_db->delete($value->name);
unset($index[$key]);
}
}
static::$_db->set($this->_hash . '-index', $index, 0);
}
$this->unlockindex();
return true;
}
/**
* Flush all existing items in storage.
*
* @return boolean
*
* @since 3.6.3
*/
public function flush()
{
if (!$this->lockindex()) {
return false;
}
return static::$_db->flush();
}
/**
* Test to see if the storage handler is available.
*
* @return boolean
*
* @since 3.0.0
*/
public static function isSupported()
{
/*
* GAE and HHVM have both had instances where Memcached the class was defined but no extension was loaded.
* If the class is there, we can assume support.
*/
return class_exists('Memcached');
}
/**
* Lock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param integer $locktime Cached item max lock time
*
* @return mixed Boolean false if locking failed or an object containing properties lock and locklooped
*
* @since 3.0.0
*/
public function lock($id, $group, $locktime)
{
$returning = new \stdClass();
$returning->locklooped = false;
$looptime = $locktime * 10;
$cache_id = $this->_getCacheId($id, $group);
$data_lock = static::$_db->add($cache_id . '_lock', 1, $locktime);
if ($data_lock === false) {
$lock_counter = 0;
// Loop until you find that the lock has been released.
// That implies that data get from other thread has finished.
while ($data_lock === false) {
if ($lock_counter > $looptime) {
break;
}
usleep(100);
$data_lock = static::$_db->add($cache_id . '_lock', 1, $locktime);
$lock_counter++;
}
$returning->locklooped = true;
}
$returning->locked = $data_lock;
return $returning;
}
/**
* Unlock cached item
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.0.0
*/
public function unlock($id, $group = null)
{
$cache_id = $this->_getCacheId($id, $group) . '_lock';
return static::$_db->delete($cache_id);
}
/**
* Lock cache index
*
* @return boolean
*
* @since 3.0.0
*/
protected function lockindex()
{
$looptime = 300;
$data_lock = static::$_db->add($this->_hash . '-index_lock', 1, 30);
if ($data_lock === false) {
$lock_counter = 0;
// Loop until you find that the lock has been released. that implies that data get from other thread has finished
while ($data_lock === false) {
if ($lock_counter > $looptime) {
return false;
}
usleep(100);
$data_lock = static::$_db->add($this->_hash . '-index_lock', 1, 30);
$lock_counter++;
}
}
return true;
}
/**
* Unlock cache index
*
* @return boolean
*
* @since 3.0.0
*/
protected function unlockindex()
{
return static::$_db->delete($this->_hash . '-index_lock');
}
}

View File

@ -0,0 +1,324 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Cache\Storage;
use Joomla\CMS\Cache\CacheStorage;
use Joomla\CMS\Cache\Exception\CacheConnectingException;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Redis cache storage handler for PECL
*
* @since 3.4
*/
class RedisStorage extends CacheStorage
{
/**
* Redis connection object
*
* @var \Redis
* @since 3.4
*/
protected static $_redis = null;
/**
* Persistent session flag
*
* @var boolean
* @since 3.4
*/
protected $_persistent = false;
/**
* Constructor
*
* @param array $options Optional parameters.
*
* @since 3.4
*/
public function __construct($options = [])
{
parent::__construct($options);
if (static::$_redis === null) {
$this->getConnection();
}
}
/**
* Create the Redis connection
*
* @return \Redis|boolean Redis connection object on success, boolean on failure
*
* @since 3.4
* @note As of 4.0 this method will throw a JCacheExceptionConnecting object on connection failure
*/
protected function getConnection()
{
if (static::isSupported() == false) {
return false;
}
$app = Factory::getApplication();
$this->_persistent = $app->get('redis_persist', true);
$server = [
'host' => $app->get('redis_server_host', 'localhost'),
'port' => $app->get('redis_server_port', 6379),
'auth' => $app->get('redis_server_auth', null),
'db' => (int) $app->get('redis_server_db', null),
];
// If you are trying to connect to a socket file, ignore the supplied port
if ($server['host'][0] === '/') {
$server['port'] = 0;
}
static::$_redis = new \Redis();
try {
if ($this->_persistent) {
$connection = static::$_redis->pconnect($server['host'], $server['port']);
} else {
$connection = static::$_redis->connect($server['host'], $server['port']);
}
} catch (\RedisException $e) {
$connection = false;
Log::add($e->getMessage(), Log::DEBUG);
}
if ($connection == false) {
static::$_redis = null;
throw new CacheConnectingException('Redis connection failed', 500);
}
try {
$auth = $server['auth'] ? static::$_redis->auth($server['auth']) : true;
} catch (\RedisException $e) {
$auth = false;
Log::add($e->getMessage(), Log::DEBUG);
}
if ($auth === false) {
static::$_redis = null;
throw new CacheConnectingException('Redis authentication failed', 500);
}
$select = static::$_redis->select($server['db']);
if ($select == false) {
static::$_redis = null;
throw new CacheConnectingException('Redis failed to select database', 500);
}
try {
static::$_redis->ping();
} catch (\RedisException $e) {
static::$_redis = null;
throw new CacheConnectingException('Redis ping failed', 500);
}
return static::$_redis;
}
/**
* Check if the cache contains data stored by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.7.0
*/
public function contains($id, $group)
{
if (static::isConnected() == false) {
return false;
}
// Redis exists returns integer values lets convert that to boolean see: https://redis.io/commands/exists
return (bool) static::$_redis->exists($this->_getCacheId($id, $group));
}
/**
* Get cached data by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param boolean $checkTime True to verify cache time expiration threshold
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 3.4
*/
public function get($id, $group, $checkTime = true)
{
if (static::isConnected() == false) {
return false;
}
return static::$_redis->get($this->_getCacheId($id, $group));
}
/**
* Get all cached data
*
* @return mixed Boolean false on failure or a cached data object
*
* @since 3.4
*/
public function getAll()
{
if (static::isConnected() == false) {
return false;
}
$allKeys = static::$_redis->keys('*');
$data = [];
$secret = $this->_hash;
if (!empty($allKeys)) {
foreach ($allKeys as $key) {
$namearr = explode('-', $key);
if ($namearr !== false && $namearr[0] == $secret && $namearr[1] === 'cache') {
$group = $namearr[2];
if (!isset($data[$group])) {
$item = new CacheStorageHelper($group);
} else {
$item = $data[$group];
}
$item->updateSize(\strlen($key) * 8);
$data[$group] = $item;
}
}
}
return $data;
}
/**
* Store the data to cache by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
* @param string $data The data to store in cache
*
* @return boolean
*
* @since 3.4
*/
public function store($id, $group, $data)
{
if (static::isConnected() == false) {
return false;
}
static::$_redis->setex($this->_getCacheId($id, $group), $this->_lifetime, $data);
return true;
}
/**
* Remove a cached data entry by ID and group
*
* @param string $id The cache data ID
* @param string $group The cache data group
*
* @return boolean
*
* @since 3.4
*/
public function remove($id, $group)
{
if (static::isConnected() == false) {
return false;
}
return (bool) static::$_redis->del($this->_getCacheId($id, $group));
}
/**
* Clean cache for a group given a mode.
*
* group mode : cleans all cache in the group
* notgroup mode : cleans all cache not in the group
*
* @param string $group The cache data group
* @param string $mode The mode for cleaning cache [group|notgroup]
*
* @return boolean
*
* @since 3.4
*/
public function clean($group, $mode = null)
{
if (static::isConnected() == false) {
return false;
}
$allKeys = static::$_redis->keys('*');
if ($allKeys === false) {
$allKeys = [];
}
$secret = $this->_hash;
foreach ($allKeys as $key) {
if (strpos($key, $secret . '-cache-' . $group . '-') === 0 && $mode === 'group') {
static::$_redis->del($key);
}
if (strpos($key, $secret . '-cache-' . $group . '-') !== 0 && $mode !== 'group') {
static::$_redis->del($key);
}
}
return true;
}
/**
* Test to see if the storage handler is available.
*
* @return boolean
*
* @since 3.4
*/
public static function isSupported()
{
return class_exists('\\Redis');
}
/**
* Test to see if the Redis connection is available.
*
* @return boolean
*
* @since 3.4
*/
public static function isConnected()
{
return static::$_redis instanceof \Redis;
}
}

View File

@ -0,0 +1,292 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Captcha;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Form\Field\CaptchaField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Captcha base object
*
* @abstract
* @since 2.5
*/
class Captcha implements DispatcherAwareInterface
{
use DispatcherAwareTrait;
/**
* Captcha Plugin object
*
* @var CMSPlugin
* @since 2.5
*
* @deprecated Should use Provider instance
*/
private $captcha;
/**
* Captcha Provider instance
*
* @var CaptchaProviderInterface
* @since 5.0.0
*/
private $provider;
/**
* Editor Plugin name
*
* @var string
* @since 2.5
*/
private $name;
/**
* Array of instances of this class.
*
* @var Captcha[]
* @since 2.5
*/
private static $instances = [];
/**
* Class constructor.
*
* @param string $captcha The plugin to use.
* @param array $options Associative array of options.
*
* @since 2.5
* @throws \RuntimeException
*/
public function __construct($captcha, $options)
{
$this->name = $captcha;
/** @var CaptchaRegistry $registry */
$registry = $options['registry'] ?? Factory::getContainer()->get(CaptchaRegistry::class);
if ($registry->has($captcha)) {
$this->provider = $registry->get($captcha);
} else {
@trigger_error(
'Use of legacy Captcha is deprecated. Use onCaptchaSetup event to register your Captcha provider.',
E_USER_DEPRECATED
);
if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) {
$this->setDispatcher($options['dispatcher']);
} else {
$this->setDispatcher(Factory::getApplication()->getDispatcher());
}
$this->_load($options);
}
}
/**
* Returns the global Captcha object, only creating it
* if it doesn't already exist.
*
* @param string $captcha The plugin to use.
* @param array $options Associative array of options.
*
* @return Captcha|null Instance of this class.
*
* @since 2.5
* @throws \RuntimeException
*/
public static function getInstance($captcha, array $options = [])
{
$signature = md5(serialize([$captcha, $options]));
if (empty(self::$instances[$signature])) {
self::$instances[$signature] = new Captcha($captcha, $options);
}
return self::$instances[$signature];
}
/**
* Fire the onInit event to initialise the captcha plugin.
*
* @param string $id The id of the field.
*
* @return boolean True on success
*
* @since 2.5
* @throws \RuntimeException
*
* @deprecated Without replacement
*/
public function initialise($id)
{
if ($this->provider) {
return true;
}
$arg = ['id' => $id];
$this->update('onInit', $arg);
return true;
}
/**
* Get the HTML for the captcha.
*
* @param string $name The control name.
* @param string $id The id for the control.
* @param string $class Value for the HTML class attribute
*
* @return string The return value of the function "onDisplay" of the selected Plugin.
*
* @since 2.5
* @throws \RuntimeException
*/
public function display($name, $id, $class = '')
{
if ($this->provider) {
return $this->provider->display($name, [
'id' => $id ?: $name,
'class' => $class,
]);
}
// Check if captcha is already loaded.
if ($this->captcha === null) {
return '';
}
// Initialise the Captcha.
if (!$this->initialise($id)) {
return '';
}
$arg = [
'name' => $name,
'id' => $id ?: $name,
'class' => $class,
];
$result = $this->update('onDisplay', $arg);
return $result;
}
/**
* Checks if the answer is correct.
*
* @param string $code The answer.
*
* @return bool Whether the provided answer was correct
*
* @since 2.5
* @throws \RuntimeException
*/
public function checkAnswer($code)
{
if ($this->provider) {
return $this->provider->checkAnswer($code);
}
// Check if captcha is already loaded
if ($this->captcha === null) {
return false;
}
$arg = ['code' => $code];
$result = $this->update('onCheckAnswer', $arg);
return $result;
}
/**
* Method to react on the setup of a captcha field. Gives the possibility
* to change the field and/or the XML element for the field.
*
* @param CaptchaField $field Captcha field instance
* @param \SimpleXMLElement $element XML form definition
*
* @return void
*/
public function setupField(CaptchaField $field, \SimpleXMLElement $element)
{
if ($this->provider) {
$this->provider->setupField($field, $element);
return;
}
if ($this->captcha === null) {
return;
}
$arg = ['field' => $field, 'element' => $element];
return $this->update('onSetupField', $arg);
}
/**
* Method to call the captcha callback if it exist.
*
* @param string $name Callback name
* @param array &$args Arguments
*
* @return mixed
*
* @since 4.0.0
*
* @deprecated Without replacement
*/
private function update($name, &$args)
{
if (method_exists($this->captcha, $name)) {
return \call_user_func_array([$this->captcha, $name], array_values($args));
}
return null;
}
/**
* Load the Captcha plugin.
*
* @param array $options Associative array of options.
*
* @return void
*
* @since 2.5
* @throws \RuntimeException
*
* @deprecated Should use CaptchaRegistry
*/
private function _load(array $options = [])
{
// Build the path to the needed captcha plugin
$name = InputFilter::getInstance()->clean($this->name, 'cmd');
// Boot the editor plugin
$this->captcha = Factory::getApplication()->bootPlugin($name, 'captcha');
// Check if the editor can be loaded
if (!$this->captcha) {
throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name));
}
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Captcha;
use Joomla\CMS\Form\FormField;
/**
* Captcha Provider Interface
*
* @since 5.0.0
*/
interface CaptchaProviderInterface
{
/**
* Return Captcha name, CMD string.
*
* @return string
* @since 5.0.0
*/
public function getName(): string;
/**
* Gets the challenge HTML
*
* @param string $name Input name
* @param array $attributes The class of the field
*
* @return string The HTML to be embedded in the form
*
* @since 5.0.0
*
* @throws \RuntimeException
*/
public function display(string $name = '', array $attributes = []): string;
/**
* Calls an HTTP POST function to verify if the user's guess was correct.
*
* @param string $code Answer provided by user
*
* @return bool If the answer is correct, false otherwise
*
* @since 5.0.0
*
* @throws \RuntimeException
*/
public function checkAnswer(string $code = null): bool;
/**
* Method to react on the setup of a captcha field. Gives the possibility
* to change the field and/or the XML element for the field.
*
* @param FormField $field Captcha field instance
* @param \SimpleXMLElement $element XML form definition
*
* @return void
*
* @since 5.0.0
*
* @throws \RuntimeException
*/
public function setupField(FormField $field, \SimpleXMLElement $element): void;
}

View File

@ -0,0 +1,119 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Captcha;
use Joomla\CMS\Captcha\Exception\CaptchaNotFoundException;
use Joomla\CMS\Event\Captcha\CaptchaSetupEvent;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Captcha Registry class
* @since 5.0.0
*/
final class CaptchaRegistry implements DispatcherAwareInterface
{
use DispatcherAwareTrait;
/**
* List of registered elements
*
* @var CaptchaProviderInterface[]
* @since 5.0.0
*/
private $registry = [];
/**
* Internal flag of initialisation
*
* @var boolean
* @since 5.0.0
*/
private $initialised = false;
/**
* Return list of all registered elements
*
* @return CaptchaProviderInterface[]
* @since 5.0.0
*/
public function getAll(): array
{
return array_values($this->registry);
}
/**
* Check whether the element exists in the registry.
*
* @param string $name Element name
*
* @return bool
* @since 5.0.0
*/
public function has(string $name): bool
{
return !empty($this->registry[$name]);
}
/**
* Return element by name.
*
* @param string $name Element name
*
* @return CaptchaProviderInterface
* @throws CaptchaNotFoundException
* @since 5.0.0
*/
public function get(string $name): CaptchaProviderInterface
{
if (empty($this->registry[$name])) {
throw new CaptchaNotFoundException(sprintf('Captcha element "%s" not found in the registry.', $name));
}
return $this->registry[$name];
}
/**
* Register element in registry, add new or override existing.
*
* @param CaptchaProviderInterface $instance
*
* @return static
* @since 5.0.0
*/
public function add(CaptchaProviderInterface $instance)
{
$this->registry[$instance->getName()] = $instance;
return $this;
}
/**
* Trigger event to allow register the element through plugins.
*
* @return static
* @since 5.0.0
*/
public function initRegistry()
{
if (!$this->initialised) {
$this->initialised = true;
$event = new CaptchaSetupEvent('onCaptchaSetup', ['subject' => $this]);
$this->getDispatcher()->dispatch($event->getName(), $event);
}
return $this;
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Captcha\Exception;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining a missing element
*
* @since 5.0.0
*/
class CaptchaNotFoundException extends \RuntimeException
{
}

View File

@ -0,0 +1,76 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Captcha\Google;
use Joomla\CMS\Http\HttpFactory;
use Joomla\Http\Exception\InvalidResponseCodeException;
use Joomla\Http\Http;
use ReCaptcha\RequestMethod;
use ReCaptcha\RequestParameters;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Bridges the Joomla! HTTP API to the Google Recaptcha RequestMethod interface for a POST request.
*
* @since 3.9.0
*/
final class HttpBridgePostRequestMethod implements RequestMethod
{
/**
* URL to which requests are sent.
*
* @var string
* @since 3.9.0
*/
public const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
/**
* The HTTP adapter
*
* @var Http
* @since 3.9.0
*/
private $http;
/**
* Class constructor.
*
* @param Http|null $http The HTTP adapter
*
* @since 3.9.0
*/
public function __construct(Http $http = null)
{
$this->http = $http ?: HttpFactory::getHttp();
}
/**
* Submit the request with the specified parameters.
*
* @param RequestParameters $params Request parameters
*
* @return string Body of the reCAPTCHA response
*
* @since 3.9.0
*/
public function submit(RequestParameters $params)
{
try {
$response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray());
return (string) $response->getBody();
} catch (InvalidResponseCodeException $exception) {
return '';
}
}
}

View File

@ -0,0 +1,439 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\Exception\DatabaseNotFoundException;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Categories Class.
*
* @since 1.6
*/
class Categories implements CategoryInterface, DatabaseAwareInterface
{
use DatabaseAwareTrait;
/**
* Array to hold the object instances
*
* @var Categories[]
* @since 1.6
*/
public static $instances = [];
/**
* Array of category nodes
*
* @var CategoryNode[]
* @since 1.6
*/
protected $_nodes;
/**
* Array of checked categories -- used to save values when _nodes are null
*
* @var boolean[]
* @since 1.6
*/
protected $_checkedCategories;
/**
* Name of the extension the categories belong to
*
* @var string
* @since 1.6
*/
protected $_extension;
/**
* Name of the linked content table to get category content count
*
* @var string
* @since 1.6
*/
protected $_table;
/**
* Name of the category field
*
* @var string
* @since 1.6
*/
protected $_field;
/**
* Name of the key field
*
* @var string
* @since 1.6
*/
protected $_key;
/**
* Name of the items state field
*
* @var string
* @since 1.6
*/
protected $_statefield;
/**
* Array of options
*
* @var array
* @since 1.6
*/
protected $_options = [];
/**
* Class constructor
*
* @param array $options Array of options
*
* @since 1.6
*/
public function __construct($options)
{
$this->_extension = $options['extension'];
$this->_table = $options['table'];
$this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid';
$this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id';
$this->_statefield = $options['statefield'] ?? 'state';
$options['access'] = $options['access'] ?? 'true';
$options['published'] = $options['published'] ?? 1;
$options['countItems'] = $options['countItems'] ?? 0;
$options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0;
$this->_options = $options;
}
/**
* Returns a reference to a Categories object
*
* @param string $extension Name of the categories extension
* @param array $options An array of options
*
* @return Categories|boolean Categories object on success, boolean false if an object does not exist
*
* @since 1.6
*
* @deprecated 4.0 will be removed in 6.0
* Use the ComponentInterface to get the categories
* Example: Factory::getApplication()->bootComponent($component)->getCategory($options, $section);
*/
public static function getInstance($extension, $options = [])
{
$hash = md5(strtolower($extension) . serialize($options));
if (isset(self::$instances[$hash])) {
return self::$instances[$hash];
}
$categories = null;
try {
$parts = explode('.', $extension, 2);
$component = Factory::getApplication()->bootComponent($parts[0]);
if ($component instanceof CategoryServiceInterface) {
$categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : '');
}
} catch (SectionNotFoundException $e) {
$categories = null;
}
self::$instances[$hash] = $categories;
return self::$instances[$hash];
}
/**
* Loads a specific category and all its children in a CategoryNode object.
*
* @param mixed $id an optional id integer or equal to 'root'
* @param boolean $forceload True to force the _load method to execute
*
* @return CategoryNode|null CategoryNode object or null if $id is not valid
*
* @since 1.6
*/
public function get($id = 'root', $forceload = false)
{
if ($id !== 'root') {
$id = (int) $id;
if ($id == 0) {
$id = 'root';
}
}
// If this $id has not been processed yet, execute the _load method
if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) {
$this->_load($id);
}
// If we already have a value in _nodes for this $id, then use it.
if (isset($this->_nodes[$id])) {
return $this->_nodes[$id];
}
return null;
}
/**
* Returns the extension of the category.
*
* @return string The extension
*
* @since 3.9.0
*/
public function getExtension()
{
return $this->_extension;
}
/**
* Returns options.
*
* @return array
*
* @since 4.3.2
*/
public function getOptions()
{
return $this->_options;
}
/**
* Load method
*
* @param int|string $id Id of category to load
*
* @return void
*
* @since 1.6
*/
protected function _load($id)
{
try {
$db = $this->getDatabase();
} catch (DatabaseNotFoundException $e) {
@trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED);
$db = Factory::getContainer()->get(DatabaseInterface::class);
}
$app = Factory::getApplication();
$user = Factory::getUser();
$extension = $this->_extension;
if ($id !== 'root') {
$id = (int) $id;
if ($id === 0) {
$id = 'root';
}
}
// Record that has this $id has been checked
$this->_checkedCategories[$id] = true;
$query = $db->getQuery(true)
->select(
[
$db->quoteName('c.id'),
$db->quoteName('c.asset_id'),
$db->quoteName('c.access'),
$db->quoteName('c.alias'),
$db->quoteName('c.checked_out'),
$db->quoteName('c.checked_out_time'),
$db->quoteName('c.created_time'),
$db->quoteName('c.created_user_id'),
$db->quoteName('c.description'),
$db->quoteName('c.extension'),
$db->quoteName('c.hits'),
$db->quoteName('c.language'),
$db->quoteName('c.level'),
$db->quoteName('c.lft'),
$db->quoteName('c.metadata'),
$db->quoteName('c.metadesc'),
$db->quoteName('c.metakey'),
$db->quoteName('c.modified_time'),
$db->quoteName('c.note'),
$db->quoteName('c.params'),
$db->quoteName('c.parent_id'),
$db->quoteName('c.path'),
$db->quoteName('c.published'),
$db->quoteName('c.rgt'),
$db->quoteName('c.title'),
$db->quoteName('c.modified_user_id'),
$db->quoteName('c.version'),
]
);
$case_when = ' CASE WHEN ';
$case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0');
$case_when .= ' THEN ';
$c_id = $query->castAsChar($db->quoteName('c.id'));
$case_when .= $query->concatenate([$c_id, $db->quoteName('c.alias')], ':');
$case_when .= ' ELSE ';
$case_when .= $c_id . ' END as ' . $db->quoteName('slug');
$query->select($case_when)
->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')')
->bind(':extension', $extension);
if ($this->_options['access']) {
$groups = $user->getAuthorisedViewLevels();
$query->whereIn($db->quoteName('c.access'), $groups);
}
if ($this->_options['published'] == 1) {
$query->where($db->quoteName('c.published') . ' = 1');
}
$query->order($db->quoteName('c.lft'));
// Note: s for selected id
if ($id !== 'root') {
// Get the selected category
$query->from($db->quoteName('#__categories', 's'))
->where($db->quoteName('s.id') . ' = :id')
->bind(':id', $id, ParameterType::INTEGER);
if ($app->isClient('site') && Multilanguage::isEnabled()) {
// For the most part, we use c.lft column, which index is properly used instead of c.rgt
$query->join(
'INNER',
$db->quoteName('#__categories', 'c'),
'(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft')
. ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt')
. ' AND ' . $db->quoteName('c.language')
. ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))'
. ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft')
. ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')'
);
} else {
$query->join(
'INNER',
$db->quoteName('#__categories', 'c'),
'(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft')
. ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')'
. ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft')
. ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')'
);
}
} else {
$query->from($db->quoteName('#__categories', 'c'));
if ($app->isClient('site') && Multilanguage::isEnabled()) {
$query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING);
}
}
// Note: i for item
if ($this->_options['countItems'] == 1) {
$subQuery = $db->getQuery(true)
->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')')
->from($db->quoteName($db->escape($this->_table), 'i'))
->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id'));
if ($this->_options['published'] == 1) {
$subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1');
}
if ($this->_options['currentlang'] !== 0) {
$subQuery->where(
$db->quoteName('i.language')
. ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')'
);
}
$query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems'));
}
// Get the results
$db->setQuery($query);
$results = $db->loadObjectList('id');
$childrenLoaded = false;
if (\count($results)) {
// Foreach categories
foreach ($results as $result) {
// Deal with root category
if ($result->id == 1) {
$result->id = 'root';
}
// Deal with parent_id
if ($result->parent_id == 1) {
$result->parent_id = 'root';
}
// Create the node
if (!isset($this->_nodes[$result->id])) {
// Create the CategoryNode and add to _nodes
$this->_nodes[$result->id] = new CategoryNode($result, $this);
// If this is not root and if the current node's parent is in the list or the current node parent is 0
if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) {
// Compute relationship between node and its parent - set the parent in the _nodes field
$this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]);
}
// If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0),
// then remove the node from the list
if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) {
unset($this->_nodes[$result->id]);
continue;
}
if ($result->id == $id || $childrenLoaded) {
$this->_nodes[$result->id]->setAllLoaded();
$childrenLoaded = true;
}
} elseif ($result->id == $id || $childrenLoaded) {
// Create the CategoryNode
$this->_nodes[$result->id] = new CategoryNode($result, $this);
if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) {
// Compute relationship between node and its parent
$this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]);
}
// If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0),
// then remove the node from the list
if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) {
unset($this->_nodes[$result->id]);
continue;
}
if ($result->id == $id || $childrenLoaded) {
$this->_nodes[$result->id]->setAllLoaded();
$childrenLoaded = true;
}
}
}
} else {
$this->_nodes[$id] = null;
}
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Option based categories factory.
*
* @since 3.10.0
*/
class CategoryFactory implements CategoryFactoryInterface
{
use DatabaseAwareTrait;
/**
* The namespace to create the categories from.
*
* @var string
* @since 4.0.0
*/
private $namespace;
/**
* The namespace must be like:
* Joomla\Component\Content
*
* @param string $namespace The namespace
*
* @since 4.0.0
*/
public function __construct($namespace)
{
$this->namespace = $namespace;
}
/**
* Creates a category.
*
* @param array $options The options
* @param string $section The section
*
* @return CategoryInterface
*
* @since 3.10.0
*
* @throws SectionNotFoundException
*/
public function createCategory(array $options = [], string $section = ''): CategoryInterface
{
$className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category';
if (!class_exists($className)) {
throw new SectionNotFoundException();
}
$category = new $className($options);
if ($category instanceof DatabaseAwareInterface) {
$category->setDatabase($this->getDatabase());
}
return $category;
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Category factory interface
*
* @since 3.10.0
*/
interface CategoryFactoryInterface
{
/**
* Creates a category.
*
* @param array $options The options
* @param string $section The section
*
* @return CategoryInterface
*
* @since 3.10.0
*
* @throws SectionNotFoundException
*/
public function createCategory(array $options = [], string $section = ''): CategoryInterface;
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* The category interface.
*
* @since 3.10.0
*/
interface CategoryInterface
{
/**
* Loads a specific category and all its children in a CategoryNode object.
*
* @param mixed $id an optional id integer or equal to 'root'
* @param boolean $forceload True to force the _load method to execute
*
* @return CategoryNode|null CategoryNode object or null if $id is not valid
*
* @since 3.10.0
*/
public function get($id = 'root', $forceload = false);
}

View File

@ -0,0 +1,528 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
use Joomla\CMS\Factory;
use Joomla\CMS\Object\LegacyErrorHandlingTrait;
use Joomla\CMS\Object\LegacyPropertyManagementTrait;
use Joomla\CMS\Tree\NodeInterface;
use Joomla\CMS\Tree\NodeTrait;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Helper class to load Categorytree
*
* @since 1.6
*/
#[\AllowDynamicProperties]
class CategoryNode implements NodeInterface
{
use LegacyErrorHandlingTrait;
use LegacyPropertyManagementTrait;
use NodeTrait;
/**
* Primary key
*
* @var integer
* @since 1.6
*/
public $id;
/**
* The id of the category in the asset table
*
* @var integer
* @since 1.6
*/
public $asset_id;
/**
* The id of the parent of category in the asset table, 0 for category root
*
* @var integer
* @since 1.6
*/
public $parent_id;
/**
* The lft value for this category in the category tree
*
* @var integer
* @since 1.6
*/
public $lft;
/**
* The rgt value for this category in the category tree
*
* @var integer
* @since 1.6
*/
public $rgt;
/**
* The depth of this category's position in the category tree
*
* @var integer
* @since 1.6
*/
public $level;
/**
* The extension this category is associated with
*
* @var integer
* @since 1.6
*/
public $extension;
/**
* The menu title for the category (a short name)
*
* @var string
* @since 1.6
*/
public $title;
/**
* The alias for the category
*
* @var string
* @since 1.6
*/
public $alias;
/**
* Description of the category.
*
* @var string
* @since 1.6
*/
public $description;
/**
* The publication status of the category
*
* @var boolean
* @since 1.6
*/
public $published;
/**
* Whether the category is or is not checked out
*
* @var boolean
* @since 1.6
*/
public $checked_out;
/**
* The time at which the category was checked out
*
* @var string
* @since 1.6
*/
public $checked_out_time;
/**
* Access level for the category
*
* @var integer
* @since 1.6
*/
public $access;
/**
* JSON string of parameters
*
* @var string
* @since 1.6
*/
public $params;
/**
* Metadata description
*
* @var string
* @since 1.6
*/
public $metadesc;
/**
* Keywords for metadata
*
* @var string
* @since 1.6
*/
public $metakey;
/**
* JSON string of other metadata
*
* @var string
* @since 1.6
*/
public $metadata;
/**
* The ID of the user who created the category
*
* @var integer
* @since 1.6
*/
public $created_user_id;
/**
* The time at which the category was created
*
* @var string
* @since 1.6
*/
public $created_time;
/**
* The ID of the user who last modified the category
*
* @var integer
* @since 1.6
*/
public $modified_user_id;
/**
* The time at which the category was modified
*
* @var string
* @since 1.6
*/
public $modified_time;
/**
* Number of times the category has been viewed
*
* @var integer
* @since 1.6
*/
public $hits;
/**
* The language for the category in xx-XX format
*
* @var string
* @since 1.6
*/
public $language;
/**
* Number of items in this category or descendants of this category
*
* @var integer
* @since 1.6
*/
public $numitems;
/**
* Slug for the category (used in URL)
*
* @var string
* @since 1.6
*/
public $slug;
/**
* Array of assets
*
* @var array
* @since 1.6
*/
public $assets;
/**
* Path from root to this category
*
* @var array
* @since 1.6
*/
protected $_path = [];
/**
* Flag if all children have been loaded
*
* @var boolean
* @since 1.6
*/
protected $_allChildrenloaded = false;
/**
* Constructor of this tree
*
* @var Categories
* @since 1.6
*/
protected $_constructor;
/**
* Class constructor
*
* @param array $category The category data.
* @param Categories $constructor The tree constructor.
*
* @since 1.6
*/
public function __construct($category = null, $constructor = null)
{
if ($category) {
$this->setProperties($category);
if ($constructor) {
$this->_constructor = $constructor;
}
}
}
/**
* Set the parent of this category
*
* If the category already has a parent, the link is unset
*
* @param NodeInterface $parent CategoryNode for the parent to be set or null
*
* @return void
*
* @since 1.6
*/
public function setParent(NodeInterface $parent)
{
if (!\is_null($this->_parent)) {
$key = array_search($this, $this->_parent->_children);
unset($this->_parent->_children[$key]);
}
$this->_parent = $parent;
$this->_parent->_children[] = &$this;
if (\count($this->_parent->_children) > 1) {
end($this->_parent->_children);
$this->_leftSibling = prev($this->_parent->_children);
$this->_leftSibling->_rightsibling = &$this;
}
if ($this->parent_id != 1) {
$this->_path = $parent->getPath();
}
$this->_path[$this->id] = $this->id . ':' . $this->alias;
}
/**
* Get the children of this node
*
* @param boolean $recursive False by default
*
* @return CategoryNode[] The children
*
* @since 1.6
*/
public function &getChildren($recursive = false)
{
if (!$this->_allChildrenloaded) {
$temp = $this->_constructor->get($this->id, true);
if ($temp) {
$this->_children = $temp->getChildren();
$this->_leftSibling = $temp->getSibling(false);
$this->_rightSibling = $temp->getSibling(true);
$this->setAllLoaded();
}
}
if ($recursive) {
$items = [];
foreach ($this->_children as $child) {
$items[] = $child;
$items = array_merge($items, $child->getChildren(true));
}
return $items;
}
return $this->_children;
}
/**
* Returns the right or left sibling of a category
*
* @param boolean $right If set to false, returns the left sibling
*
* @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side.
*
* @since 1.6
*/
public function getSibling($right = true)
{
if (!$this->_allChildrenloaded) {
$temp = $this->_constructor->get($this->id, true);
$this->_children = $temp->getChildren();
$this->_leftSibling = $temp->getSibling(false);
$this->_rightSibling = $temp->getSibling(true);
$this->setAllLoaded();
}
if ($right) {
return $this->_rightSibling;
}
return $this->_leftSibling;
}
/**
* Returns the category parameters
*
* @return Registry
*
* @since 1.6
*/
public function getParams()
{
if (!($this->params instanceof Registry)) {
$this->params = new Registry($this->params);
}
return $this->params;
}
/**
* Returns the category metadata
*
* @return Registry A Registry object containing the metadata
*
* @since 1.6
*/
public function getMetadata()
{
if (!($this->metadata instanceof Registry)) {
$this->metadata = new Registry($this->metadata);
}
return $this->metadata;
}
/**
* Returns the category path to the root category
*
* @return array
*
* @since 1.6
*/
public function getPath()
{
return $this->_path;
}
/**
* Returns the user that created the category
*
* @param boolean $modifiedUser Returns the modified_user when set to true
*
* @return \Joomla\CMS\User\User A User object containing a userid
*
* @since 1.6
*/
public function getAuthor($modifiedUser = false)
{
if ($modifiedUser) {
return Factory::getUser($this->modified_user_id);
}
return Factory::getUser($this->created_user_id);
}
/**
* Set to load all children
*
* @return void
*
* @since 1.6
*/
public function setAllLoaded()
{
$this->_allChildrenloaded = true;
foreach ($this->_children as $child) {
$child->setAllLoaded();
}
}
/**
* Returns the number of items.
*
* @param boolean $recursive If false number of children, if true number of descendants
*
* @return integer Number of children or descendants
*
* @since 1.6
*/
public function getNumItems($recursive = false)
{
if ($recursive) {
$count = $this->numitems;
foreach ($this->getChildren() as $child) {
$count += $child->getNumItems(true);
}
return $count;
}
return $this->numitems;
}
/**
* Serialize the node.
*
* @since 4.3.2
*/
public function __serialize()
{
$vars = get_object_vars($this);
// Store constructor as array of options.
if ($this->_constructor) {
$vars['_constructor'] = $this->_constructor->getOptions();
}
return $vars;
}
/**
* Unserialize the node.
*
* @param array $data
*
* @since 4.3.2
*/
public function __unserialize($data)
{
foreach ($data as $key => $value) {
$this->$key = $value;
}
// Restore constructor from array of options.
if ($this->_constructor) {
$this->_constructor = Categories::getInstance($this->_constructor['extension'], $this->_constructor);
}
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
use Joomla\CMS\Form\Form;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Access to component specific categories.
*
* @since 4.0.0
*/
interface CategoryServiceInterface
{
/**
* Returns the category service.
*
* @param array $options The options
* @param string $section The section
*
* @return CategoryInterface
*
* @since 4.0.0
* @throws SectionNotFoundException
*/
public function getCategory(array $options = [], $section = ''): CategoryInterface;
/**
* Adds Count Items for Category Manager.
*
* @param \stdClass[] $items The category objects
* @param string $section The section
*
* @return void
*
* @since 4.0.0
* @throws \Exception
*/
public function countItems(array $items, string $section);
/**
* Prepares the category form
*
* @param Form $form The form to change
* @param array|object $data The form data
*
* @return void
*/
public function prepareForm(Form $form, $data);
}

View File

@ -0,0 +1,127 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Helper\ContentHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Trait for component categories service.
*
* @since 4.0.0
*/
trait CategoryServiceTrait
{
/**
* The categories factory
*
* @var CategoryFactoryInterface
*
* @since 4.0.0
*/
private $categoryFactory;
/**
* Returns the category service.
*
* @param array $options The options
* @param string $section The section
*
* @return CategoryInterface
*
* @since 4.0.0
* @throws SectionNotFoundException
*/
public function getCategory(array $options = [], $section = ''): CategoryInterface
{
return $this->categoryFactory->createCategory($options, $section);
}
/**
* Sets the internal category factory.
*
* @param CategoryFactoryInterface $categoryFactory The categories factory
*
* @return void
*
* @since 4.0.0
*/
public function setCategoryFactory(CategoryFactoryInterface $categoryFactory)
{
$this->categoryFactory = $categoryFactory;
}
/**
* Adds Count Items for Category Manager.
*
* @param \stdClass[] $items The category objects
* @param string $section The section
*
* @return void
*
* @since 4.0.0
* @throws \Exception
*/
public function countItems(array $items, string $section)
{
$config = (object) [
'related_tbl' => $this->getTableNameForSection($section),
'state_col' => $this->getStateColumnForSection($section),
'group_col' => 'catid',
'relation_type' => 'category_or_group',
];
ContentHelper::countRelations($items, $config);
}
/**
* Prepares the category form
*
* @param Form $form The form to change
* @param array|object $data The form data
*
* @return void
*/
public function prepareForm(Form $form, $data)
{
}
/**
* Returns the table for the count items functions for the given section.
*
* @param string $section The section
*
* @return string|null
*
* @since 4.0.0
*/
protected function getTableNameForSection(string $section = null)
{
return null;
}
/**
* Returns the state column for the count items functions for the given section.
*
* @param string $section The section
*
* @return string|null
*
* @since 4.0.0
*/
protected function getStateColumnForSection(string $section = null)
{
return 'state';
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Categories;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Representing when a category section cannot be found.
*
* @since 3.10.0
*/
class SectionNotFoundException extends \RuntimeException
{
}

View File

@ -0,0 +1,385 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Changelog;
use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Object\LegacyErrorHandlingTrait;
use Joomla\CMS\Object\LegacyPropertyManagementTrait;
use Joomla\CMS\Version;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Changelog class.
*
* @since 4.0.0
*/
class Changelog
{
use LegacyErrorHandlingTrait;
use LegacyPropertyManagementTrait;
/**
* Update manifest `<element>` element
*
* @var string
* @since 4.0.0
*/
protected $element;
/**
* Update manifest `<type>` element
*
* @var string
* @since 4.0.0
*/
protected $type;
/**
* Update manifest `<version>` element
*
* @var string
* @since 4.0.0
*/
protected $version;
/**
* Update manifest `<security>` element
*
* @var array
* @since 4.0.0
*/
protected $security = [];
/**
* Update manifest `<fix>` element
*
* @var array
* @since 4.0.0
*/
protected $fix = [];
/**
* Update manifest `<language>` element
*
* @var array
* @since 4.0.0
*/
protected $language = [];
/**
* Update manifest `<addition>` element
*
* @var array
* @since 4.0.0
*/
protected $addition = [];
/**
* Update manifest `<change>` elements
*
* @var array
* @since 4.0.0
*/
protected $change = [];
/**
* Update manifest `<remove>` element
*
* @var array
* @since 4.0.0
*/
protected $remove = [];
/**
* Update manifest `<maintainer>` element
*
* @var array
* @since 4.0.0
*/
protected $note = [];
/**
* List of node items
*
* @var array
* @since 4.0.0
*/
private $items = [];
/**
* Resource handle for the XML Parser
*
* @var resource
* @since 4.0.0
*/
protected $xmlParser;
/**
* Element call stack
*
* @var array
* @since 4.0.0
*/
protected $stack = ['base'];
/**
* Object containing the current update data
*
* @var \stdClass
* @since 4.0.0
*/
protected $currentChangelog;
/**
* The version to match the changelog
*
* @var string
* @since 4.0.0
*/
private $matchVersion = '';
/**
* Object containing the latest changelog data
*
* @var \stdClass
* @since 4.0.0
*/
protected $latest;
/**
* Gets the reference to the current direct parent
*
* @return string
*
* @since 4.0.0
*/
protected function getStackLocation()
{
return implode('->', $this->stack);
}
/**
* Get the last position in stack count
*
* @return string
*
* @since 4.0.0
*/
protected function getLastTag()
{
return $this->stack[\count($this->stack) - 1];
}
/**
* Set the version to match.
*
* @param string $version The version to match
*
* @return void
*
* @since 4.0.0
*/
public function setVersion(string $version)
{
$this->matchVersion = $version;
}
/**
* XML Start Element callback
*
* @param object $parser Parser object
* @param string $name Name of the tag found
* @param array $attrs Attributes of the tag
*
* @return void
*
* @note This is public because it is called externally
* @since 1.7.0
*/
public function startElement($parser, $name, $attrs = [])
{
$this->stack[] = $name;
$tag = $this->getStackLocation();
// Reset the data
if (isset($this->$tag)) {
$this->$tag->data = '';
}
// Skip technical elements
if ($name === 'CHANGELOGS' || $name === 'CHANGELOG' || $name === 'ITEM') {
return;
}
$name = strtolower($name);
if (!isset($this->currentChangelog->$name)) {
$this->currentChangelog->$name = new \stdClass();
}
$this->currentChangelog->$name->data = '';
foreach ($attrs as $key => $data) {
$key = strtolower($key);
$this->currentChangelog->$name->$key = $data;
}
}
/**
* Callback for closing the element
*
* @param object $parser Parser object
* @param string $name Name of element that was closed
*
* @return void
*
* @note This is public because it is called externally
* @since 1.7.0
*/
public function endElement($parser, $name)
{
array_pop($this->stack);
switch ($name) {
case 'SECURITY':
case 'FIX':
case 'LANGUAGE':
case 'ADDITION':
case 'CHANGE':
case 'REMOVE':
case 'NOTE':
$name = strtolower($name);
$this->currentChangelog->$name->data = $this->items;
$this->items = [];
break;
case 'CHANGELOG':
if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) {
$this->latest = $this->currentChangelog;
}
// No version match, empty it
$this->currentChangelog = new \stdClass();
break;
case 'CHANGELOGS':
// If the latest item is set then we transfer it to where we want to
if (isset($this->latest)) {
foreach (get_object_vars($this->latest) as $key => $val) {
$this->$key = $val;
}
unset($this->latest);
unset($this->currentChangelog);
} elseif (isset($this->currentChangelog)) {
// The update might be for an older version of j!
unset($this->currentChangelog);
}
break;
}
}
/**
* Character Parser Function
*
* @param object $parser Parser object.
* @param object $data The data.
*
* @return void
*
* @note This is public because its called externally.
* @since 1.7.0
*/
public function characterData($parser, $data)
{
$tag = $this->getLastTag();
switch ($tag) {
case 'ITEM':
$this->items[] = $data;
break;
case 'SECURITY':
case 'FIX':
case 'LANGUAGE':
case 'ADDITION':
case 'CHANGE':
case 'REMOVE':
case 'NOTE':
break;
default:
// Throw the data for this item together
$tag = strtolower($tag);
if (isset($this->currentChangelog->$tag)) {
$this->currentChangelog->$tag->data .= $data;
}
break;
}
}
/**
* Loads an XML file from a URL.
*
* @param string $url The URL.
*
* @return boolean True on success
*
* @since 4.0.0
*/
public function loadFromXml($url)
{
$version = new Version();
$httpOption = new Registry();
$httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false));
try {
$http = HttpFactory::getHttp($httpOption);
$response = $http->get($url);
} catch (\RuntimeException $e) {
$response = null;
}
if ($response === null || $response->code !== 200) {
// @todo: Add a 'mark bad' setting here somehow
Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror');
return false;
}
$this->currentChangelog = new \stdClass();
$this->xmlParser = xml_parser_create('');
xml_set_object($this->xmlParser, $this);
xml_set_element_handler($this->xmlParser, 'startElement', 'endElement');
xml_set_character_data_handler($this->xmlParser, 'characterData');
if (!xml_parse($this->xmlParser, $response->body)) {
Log::add(
sprintf(
'XML error: %s at line %d',
xml_error_string(xml_get_error_code($this->xmlParser)),
xml_get_current_line_number($this->xmlParser)
),
Log::WARNING,
'updater'
);
return false;
}
xml_parser_free($this->xmlParser);
return true;
}
}

View File

@ -0,0 +1,217 @@
<?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\Client;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Client helper class
*
* @since 1.7.0
*/
class ClientHelper
{
/**
* Method to return the array of client layer configuration options
*
* @param string $client Client name, currently only 'ftp' is supported
* @param boolean $force Forces re-creation of the login credentials. Set this to
* true if login credentials in the session storage have changed
*
* @return array Client layer configuration options, consisting of at least
* these fields: enabled, host, port, user, pass, root
*
* @since 1.7.0
*/
public static function getCredentials($client, $force = false)
{
static $credentials = [];
$client = strtolower($client);
if (!isset($credentials[$client]) || $force) {
$app = Factory::getApplication();
// Fetch the client layer configuration options for the specific client
switch ($client) {
case 'ftp':
$options = [
'enabled' => $app->get('ftp_enable'),
'host' => $app->get('ftp_host'),
'port' => $app->get('ftp_port'),
'user' => $app->get('ftp_user'),
'pass' => $app->get('ftp_pass'),
'root' => $app->get('ftp_root'),
];
break;
default:
$options = ['enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''];
break;
}
// If user and pass are not set in global config lets see if they are in the session
if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) {
$session = Factory::getSession();
$options['user'] = $session->get($client . '.user', null, 'JClientHelper');
$options['pass'] = $session->get($client . '.pass', null, 'JClientHelper');
}
// If user or pass are missing, disable this client
if ($options['user'] == '' || $options['pass'] == '') {
$options['enabled'] = false;
}
// Save the credentials for later use
$credentials[$client] = $options;
}
return $credentials[$client];
}
/**
* Method to set client login credentials
*
* @param string $client Client name, currently only 'ftp' is supported
* @param string $user Username
* @param string $pass Password
*
* @return boolean True if the given login credentials have been set and are valid
*
* @since 1.7.0
*/
public static function setCredentials($client, $user, $pass)
{
$return = false;
$client = strtolower($client);
// Test if the given credentials are valid
switch ($client) {
case 'ftp':
$app = Factory::getApplication();
$options = ['enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')];
if ($options['enabled']) {
$ftp = FtpClient::getInstance($options['host'], $options['port']);
// Test the connection and try to log in
if ($ftp->isConnected()) {
if ($ftp->login($user, $pass)) {
$return = true;
}
$ftp->quit();
}
}
break;
default:
break;
}
if ($return) {
// Save valid credentials to the session
$session = Factory::getSession();
$session->set($client . '.user', $user, 'JClientHelper');
$session->set($client . '.pass', $pass, 'JClientHelper');
// Force re-creation of the data saved within JClientHelper::getCredentials()
self::getCredentials($client, true);
}
return $return;
}
/**
* Method to determine if client login credentials are present
*
* @param string $client Client name, currently only 'ftp' is supported
*
* @return boolean True if login credentials are available
*
* @since 1.7.0
*/
public static function hasCredentials($client)
{
$return = false;
$client = strtolower($client);
// Get (unmodified) credentials for this client
switch ($client) {
case 'ftp':
$app = Factory::getApplication();
$options = ['enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')];
break;
default:
$options = ['enabled' => false, 'user' => '', 'pass' => ''];
break;
}
if ($options['enabled'] == false) {
// The client is disabled in global config, so let's pretend we are OK
$return = true;
} elseif ($options['user'] != '' && $options['pass'] != '') {
// Login credentials are available in global config
$return = true;
} else {
// Check if login credentials are available in the session
$session = Factory::getSession();
$user = $session->get($client . '.user', null, 'JClientHelper');
$pass = $session->get($client . '.pass', null, 'JClientHelper');
if ($user != '' && $pass != '') {
$return = true;
}
}
return $return;
}
/**
* Determine whether input fields for client settings need to be shown
*
* If valid credentials were passed along with the request, they are saved to the session.
* This functions returns an exception if invalid credentials have been given or if the
* connection to the server failed for some other reason.
*
* @param string $client The name of the client.
*
* @return boolean True if credentials are present
*
* @since 1.7.0
* @throws \InvalidArgumentException if credentials invalid
*/
public static function setCredentialsFromRequest($client)
{
// Determine whether FTP credentials have been passed along with the current request
$input = Factory::getApplication()->getInput();
$user = $input->post->getString('username', null);
$pass = $input->post->getString('password', null);
if ($user != '' && $pass != '') {
// Add credentials to the session
if (!self::setCredentials($client, $user, $pass)) {
throw new \InvalidArgumentException('Invalid user credentials');
}
$return = false;
} else {
// Just determine if the FTP input fields need to be shown
$return = !self::hasCredentials('ftp');
}
return $return;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,458 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Component;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Cache\Controller\CallbackController;
use Joomla\CMS\Cache\Exception\CacheExceptionInterface;
use Joomla\CMS\Component\Exception\MissingComponentException;
use Joomla\CMS\Dispatcher\ApiDispatcher;
use Joomla\CMS\Dispatcher\ComponentDispatcher;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Profiler\Profiler;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Component helper class
*
* @since 1.5
*/
class ComponentHelper
{
/**
* The component list cache
*
* @var ComponentRecord[]
* @since 1.6
*/
protected static $components = [];
/**
* Get the component information.
*
* @param string $option The component option.
* @param boolean $strict If set and the component does not exist, the enabled attribute will be set to false.
*
* @return ComponentRecord An object with the information for the component.
*
* @since 1.5
*/
public static function getComponent($option, $strict = false)
{
$components = static::getComponents();
if (isset($components[$option])) {
return $components[$option];
}
$result = new ComponentRecord();
$result->enabled = $strict ? false : true;
$result->setParams(new Registry());
return $result;
}
/**
* Checks if the component is enabled
*
* @param string $option The component option.
*
* @return boolean
*
* @since 1.5
*/
public static function isEnabled($option)
{
$components = static::getComponents();
return isset($components[$option]) && $components[$option]->enabled;
}
/**
* Checks if a component is installed
*
* @param string $option The component option.
*
* @return integer
*
* @since 3.4
*/
public static function isInstalled($option)
{
$components = static::getComponents();
return isset($components[$option]) ? 1 : 0;
}
/**
* Gets the parameter object for the component
*
* @param string $option The option for the component.
* @param boolean $strict If set and the component does not exist, false will be returned
*
* @return Registry A Registry object.
*
* @see Registry
* @since 1.5
*/
public static function getParams($option, $strict = false)
{
return static::getComponent($option, $strict)->getParams();
}
/**
* Applies the global text filters to arbitrary text as per settings for current user groups
*
* @param string $text The string to filter
*
* @return string The filtered string
*
* @since 2.5
*/
public static function filterText($text)
{
// Punyencoding utf8 email addresses
$text = InputFilter::getInstance()->emailToPunycode($text);
// Filter settings
$config = static::getParams('com_config');
$user = Factory::getUser();
$userGroups = Access::getGroupsByUser($user->get('id'));
$filters = $config->get('filters');
$forbiddenListTags = [];
$forbiddenListAttributes = [];
$customListTags = [];
$customListAttributes = [];
$allowedListTags = [];
$allowedListAttributes = [];
$allowedList = false;
$forbiddenList = false;
$customList = false;
$unfiltered = false;
// Cycle through each of the user groups the user is in.
// Remember they are included in the Public group as well.
foreach ($userGroups as $groupId) {
// May have added a group by not saved the filters.
if (!isset($filters->$groupId)) {
continue;
}
// Each group the user is in could have different filtering properties.
$filterData = $filters->$groupId;
$filterType = strtoupper($filterData->filter_type);
if ($filterType === 'NH') {
// Maximum HTML filtering.
} elseif ($filterType === 'NONE') {
// No HTML filtering.
$unfiltered = true;
} else {
// Forbidden list or allowed list.
// Preprocess the tags and attributes.
$tags = explode(',', $filterData->filter_tags);
$attributes = explode(',', $filterData->filter_attributes);
$tempTags = [];
$tempAttributes = [];
foreach ($tags as $tag) {
$tag = trim($tag);
if ($tag) {
$tempTags[] = $tag;
}
}
foreach ($attributes as $attribute) {
$attribute = trim($attribute);
if ($attribute) {
$tempAttributes[] = $attribute;
}
}
// Collect the forbidden list or allowed list tags and attributes.
// Each list is cumulative.
if ($filterType === 'BL') {
$forbiddenList = true;
$forbiddenListTags = array_merge($forbiddenListTags, $tempTags);
$forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes);
} elseif ($filterType === 'CBL') {
// Only set to true if Tags or Attributes were added
if ($tempTags || $tempAttributes) {
$customList = true;
$customListTags = array_merge($customListTags, $tempTags);
$customListAttributes = array_merge($customListAttributes, $tempAttributes);
}
} elseif ($filterType === 'WL') {
$allowedList = true;
$allowedListTags = array_merge($allowedListTags, $tempTags);
$allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes);
}
}
}
// Remove duplicates before processing (because the forbidden list uses both sets of arrays).
$forbiddenListTags = array_unique($forbiddenListTags);
$forbiddenListAttributes = array_unique($forbiddenListAttributes);
$customListTags = array_unique($customListTags);
$customListAttributes = array_unique($customListAttributes);
$allowedListTags = array_unique($allowedListTags);
$allowedListAttributes = array_unique($allowedListAttributes);
if (!$unfiltered) {
// Custom Forbidden list precedes Default forbidden list.
if ($customList) {
$filter = InputFilter::getInstance([], [], 1, 1);
// Override filter's default forbidden tags and attributes
if ($customListTags) {
$filter->blockedTags = $customListTags;
}
if ($customListAttributes) {
$filter->blockedAttributes = $customListAttributes;
}
} elseif ($forbiddenList) {
// Forbidden list takes second precedence.
// Remove the allowed tags and attributes from the forbidden list.
$forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags);
$forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes);
$filter = InputFilter::getInstance(
$forbiddenListTags,
$forbiddenListAttributes,
InputFilter::ONLY_BLOCK_DEFINED_TAGS,
InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES
);
// Remove the allowed tags from filter's default forbidden list.
if ($allowedListTags) {
$filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags);
}
// Remove the allowed attributes from filter's default forbidden list.
if ($allowedListAttributes) {
$filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes);
}
} elseif ($allowedList) {
// Allowed lists take third precedence.
// Turn off XSS auto clean
$filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0);
} else {
// No HTML takes last place.
$filter = InputFilter::getInstance();
}
$text = $filter->clean($text, 'html');
}
return $text;
}
/**
* Render the component.
*
* @param string $option The component option.
* @param array $params The component parameters
*
* @return string
*
* @since 1.5
* @throws MissingComponentException
*/
public static function renderComponent($option, $params = [])
{
$app = Factory::getApplication();
$lang = Factory::getLanguage();
if (!$app->isClient('api')) {
// Load template language files.
$template = $app->getTemplate(true)->template;
$lang->load('tpl_' . $template, JPATH_BASE)
|| $lang->load('tpl_' . $template, JPATH_THEMES . "/$template");
}
if (empty($option)) {
throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404);
}
if (JDEBUG) {
Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option);
}
// Record the scope
$scope = $app->scope;
// Set scope to component name
$app->scope = $option;
// Build the component path.
$option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option);
// Define component path.
if (!\defined('JPATH_COMPONENT')) {
/**
* Defines the path to the active component for the request
*
* Note this constant is application aware and is different for each application (site/admin).
*
* @var string
* @since 1.5
*
* @deprecated 4.3 will be removed in 6.0
* Will be removed without replacement
*/
\define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option);
}
if (!\defined('JPATH_COMPONENT_SITE')) {
/**
* Defines the path to the site element of the active component for the request
*
* @var string
* @since 1.5
*
* @deprecated 4.3 will be removed in 6.0
* Will be removed without replacement
*/
\define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option);
}
if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) {
/**
* Defines the path to the admin element of the active component for the request
*
* @var string
* @since 1.5
*
* @deprecated 4.3 will be removed in 6.0
* Will be removed without replacement
*/
\define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option);
}
// If component is disabled throw error
if (!static::isEnabled($option)) {
throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404);
}
ob_start();
$app->bootComponent($option)->getDispatcher($app)->dispatch();
$contents = ob_get_clean();
// Revert the scope
$app->scope = $scope;
if (JDEBUG) {
Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option);
}
return $contents;
}
/**
* Load the installed components into the components property.
*
* @return boolean True on success
*
* @since 3.2
*/
protected static function load()
{
$loader = function () {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null]))
->from($db->quoteName('#__extensions'))
->where(
[
$db->quoteName('type') . ' = ' . $db->quote('component'),
$db->quoteName('state') . ' = 0',
$db->quoteName('enabled') . ' = 1',
]
);
$components = [];
$db->setQuery($query);
foreach ($db->getIterator() as $component) {
$components[$component->option] = new ComponentRecord((array) $component);
}
return $components;
};
/** @var CallbackController $cache */
$cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']);
try {
static::$components = $cache->get($loader, [], __METHOD__);
} catch (CacheExceptionInterface $e) {
static::$components = $loader();
}
return true;
}
/**
* Get installed components
*
* @return ComponentRecord[] The components property
*
* @since 3.6.3
*/
public static function getComponents()
{
if (empty(static::$components)) {
static::load();
}
return static::$components;
}
/**
* Returns the component name (eg. com_content) for the given object based on the class name.
* If the object is not namespaced, then the alternative name is used.
*
* @param object $object The object controller or model
* @param string $alternativeName Mostly the value of getName() from the object
*
* @return string The name
*
* @since 4.0.0
*/
public static function getComponentName($object, string $alternativeName): string
{
$reflect = new \ReflectionClass($object);
if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) {
return 'com_' . strtolower($alternativeName);
}
$from = strpos($reflect->getNamespaceName(), '\\Component');
$to = strpos(substr($reflect->getNamespaceName(), $from + 11), '\\');
return 'com_' . strtolower(substr($reflect->getNamespaceName(), $from + 11, $to));
}
}

View File

@ -0,0 +1,158 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Component;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Object representing a component extension record
*
* @since 3.7.0
*/
class ComponentRecord
{
/**
* Primary key
*
* @var integer
* @since 3.7.0
*/
public $id;
/**
* The component name
*
* @var integer
* @since 3.7.0
*/
public $option;
/**
* The component parameters
*
* @var string|Registry
* @since 3.7.0
* @note This field is protected to require reading this field to proxy through the getter to convert the params to a Registry instance
*/
protected $params;
/**
* The extension namespace
*
* @var string
* @since 4.0.0
*/
public $namespace;
/**
* Indicates if this component is enabled
*
* @var integer
* @since 3.7.0
*/
public $enabled;
/**
* Class constructor
*
* @param array $data The component record data to load
*
* @since 3.7.0
*/
public function __construct($data = [])
{
foreach ((array) $data as $key => $value) {
$this->$key = $value;
}
}
/**
* Method to get certain otherwise inaccessible properties from the form field object.
*
* @param string $name The property name for which to get the value.
*
* @return mixed The property value or null.
*
* @since 3.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Access the item parameters through the `getParams()` method
* Example:
* $componentRecord->getParams();
*/
public function __get($name)
{
if ($name === 'params') {
return $this->getParams();
}
return $this->$name;
}
/**
* Method to set certain otherwise inaccessible properties of the form field object.
*
* @param string $name The property name for which to set the value.
* @param mixed $value The value of the property.
*
* @return void
*
* @since 3.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Set the item parameters through the `setParams()` method
* Example:
* $componentRecord->setParams($value);
*/
public function __set($name, $value)
{
if ($name === 'params') {
$this->setParams($value);
return;
}
$this->$name = $value;
}
/**
* Returns the menu item parameters
*
* @return Registry
*
* @since 3.7.0
*/
public function getParams()
{
if (!($this->params instanceof Registry)) {
$this->params = new Registry($this->params);
}
return $this->params;
}
/**
* Sets the menu item parameters
*
* @param Registry|string $params The data to be stored as the parameters
*
* @return void
*
* @since 3.7.0
*/
public function setParams($params)
{
$this->params = $params;
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Component\Exception;
use Joomla\CMS\Router\Exception\RouteNotFoundException;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Exception class defining an error for a missing component
*
* @since 3.7.0
*/
class MissingComponentException extends RouteNotFoundException
{
/**
* Constructor
*
* @param string $message The Exception message to throw.
* @param integer $code The Exception code.
* @param \Exception $previous The previous exception used for the exception chaining.
*
* @since 3.7.0
*/
public function __construct($message = '', $code = 404, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Component\Router;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Base component routing class
*
* @since 3.3
*/
abstract class RouterBase implements RouterInterface
{
/**
* Application object to use in the router
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 3.4
*/
public $app;
/**
* Menu object to use in the router
*
* @var \Joomla\CMS\Menu\AbstractMenu
* @since 3.4
*/
public $menu;
/**
* Class constructor.
*
* @param \Joomla\CMS\Application\CMSApplication $app Application-object that the router should use
* @param \Joomla\CMS\Menu\AbstractMenu $menu Menu-object that the router should use
*
* @since 3.4
*/
public function __construct($app = null, $menu = null)
{
if ($app) {
$this->app = $app;
} else {
$this->app = Factory::getApplication();
}
if ($menu) {
$this->menu = $menu;
} else {
$this->menu = $this->app->getMenu();
}
}
/**
* Generic method to preprocess a URL
*
* @param array $query An associative array of URL arguments
*
* @return array The URL arguments to use to assemble the subsequent URL.
*
* @since 3.3
*/
public function preprocess($query)
{
return $query;
}
}

Some files were not shown because too many files have changed in this diff Show More