primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,337 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Autoloader;
defined('_JEXEC') || die;
// Do not put the JEXEC or die check on this file (necessary omission for testing)
use InvalidArgumentException;
/**
* A PSR-4 class autoloader. This is a modified version of Composer's ClassLoader class
*
* @codeCoverageIgnore
*/
class Autoloader
{
/**
* Class aliases. Maps an old, obsolete class name to the new one.
*
* @var array
*/
protected static $aliases = [
'FOF40\Utils\CacheCleaner' => 'FOF40\JoomlaAbstraction\CacheCleaner',
'FOF40\Utils\ComponentVersion' => 'FOF40\JoomlaAbstraction\ComponentVersion',
'FOF40\Utils\DynamicGroups' => 'FOF40\JoomlaAbstraction\DynamicGroups',
'FOF40\Utils\FEFHelper\BrowseView' => 'FOF40\Html\FEFHelper\BrowseView',
'FOF40\Utils\InstallScript\BaseInstaller' => 'FOF40\InstallScript\BaseInstaller',
'FOF40\Utils\InstallScript\Component' => 'FOF40\InstallScript\Component',
'FOF40\Utils\InstallScript\Module' => 'FOF40\InstallScript\Module',
'FOF40\Utils\InstallScript\Plugin' => 'FOF40\InstallScript\Plugin',
'FOF40\Utils\InstallScript' => 'FOF40\InstallScript\Component',
'FOF40\Utils\Ip' => 'FOF40\IP\IPHelper',
'FOF40\Utils\SelectOptions' => 'FOF40\Html\SelectOptions',
'FOF40\Utils\TimezoneWrangler' => 'FOF40\Date\TimezoneWrangler',
];
/** @var Autoloader The static instance of this autoloader */
private static $instance;
/** @var array Lengths of PSR-4 prefixes */
private $prefixLengths = [];
/** @var array Prefix to directory map */
private $prefixDirs = [];
/** @var array Fall-back directories */
private $fallbackDirs = [];
/**
* @return Autoloader
*/
public static function getInstance(): self
{
if (!is_object(self::$instance))
{
self::$instance = new Autoloader();
}
return self::$instance;
}
/**
* Returns the prefix to directory map
*
* @return array
*
* @noinspection PhpUnused
*/
public function getPrefixes(): array
{
return $this->prefixDirs;
}
/**
* Returns the list of fall=back directories
*
* @return array
*
* @noinspection PhpUnused
*/
public function getFallbackDirs(): array
{
return $this->fallbackDirs;
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prefixing to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-0 base directories
* @param boolean $prepend Whether to prefix the directories
*
* @return $this for chaining
*
* @throws InvalidArgumentException When the prefix is invalid
*/
public function addMap(string $prefix, $paths, bool $prepend = false): self
{
if (!is_string($paths) && !is_array($paths))
{
throw new InvalidArgumentException(sprintf('%s::%s -- $paths expects a string or array', __CLASS__, __METHOD__));
}
if ($prefix !== '')
{
$prefix = ltrim($prefix, '\\');
}
if ($prefix === '')
{
// Register directories for the root namespace.
if ($prepend)
{
$this->fallbackDirs = array_merge(
(array) $paths,
$this->fallbackDirs
);
$this->fallbackDirs = array_unique($this->fallbackDirs);
}
else
{
$this->fallbackDirs = array_merge(
$this->fallbackDirs,
(array) $paths
);
$this->fallbackDirs = array_unique($this->fallbackDirs);
}
}
elseif (!isset($this->prefixDirs[$prefix]))
{
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1])
{
throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengths[$prefix[0]][$prefix] = $length;
$this->prefixDirs[$prefix] = (array) $paths;
}
elseif ($prepend)
{
// Prepend directories for an already registered namespace.
$this->prefixDirs[$prefix] = array_merge(
(array) $paths,
$this->prefixDirs[$prefix]
);
$this->prefixDirs[$prefix] = array_unique($this->prefixDirs[$prefix]);
}
else
{
// Append directories for an already registered namespace.
$this->prefixDirs[$prefix] = array_merge(
$this->prefixDirs[$prefix],
(array) $paths
);
$this->prefixDirs[$prefix] = array_unique($this->prefixDirs[$prefix]);
}
return $this;
}
/**
* Does the autoloader have a map for the specified prefix?
*
* @param string $prefix
*
* @return bool
*/
public function hasMap($prefix)
{
return isset($this->prefixDirs[$prefix]);
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @return void
*
* @throws InvalidArgumentException When the prefix is invalid
* @noinspection PhpUnused
*/
public function setMap(string $prefix, $paths)
{
if ($prefix !== '')
{
$prefix = ltrim($prefix, '\\');
}
if ($prefix === '')
{
$this->fallbackDirs = (array) $paths;
}
else
{
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1])
{
throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengths[$prefix[0]][$prefix] = $length;
$this->prefixDirs[$prefix] = (array) $paths;
}
}
/**
* Registers this instance as an autoloader.
*
* @param boolean $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register([$this, 'loadClass'], true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister([$this, 'loadClass']);
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
*
* @return boolean|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if (class_exists($class, false))
{
return null;
}
if ($file = $this->findFile($class))
{
/** @noinspection PhpIncludeInspection */
include $file;
return true;
}
if (array_key_exists($class, self::$aliases))
{
$newClass = self::$aliases[$class];
$foundAliasedClass = $this->loadClass($newClass);
if ($foundAliasedClass === true)
{
class_alias($newClass, $class);
return true;
}
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0])
{
$class = substr($class, 1);
}
// FEFHelper lookup
if (substr($class, 0, 7) == 'FEFHelp' && file_exists($file = realpath(__DIR__ . '/..') . '/Html/FEFHelper/' . strtolower(substr($class, 7)) . '.php'))
{
return $file;
}
// PSR-4 lookup
$logicalPath = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
$first = $class[0];
if (isset($this->prefixLengths[$first]))
{
foreach ($this->prefixLengths[$first] as $prefix => $length)
{
if (0 === strpos($class, $prefix))
{
foreach ($this->prefixDirs[$prefix] as $dir)
{
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPath, $length)))
{
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirs as $dir)
{
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPath))
{
return $file;
}
}
return false;
}
}
// Register the current namespace with the autoloader
Autoloader::getInstance()->addMap('FOF40\\', [realpath(__DIR__ . '/..')]);
Autoloader::getInstance()->register();

View File

@ -0,0 +1,338 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
// Do not put the JEXEC or die check on this file
/**
* FOF-powered Joomla! CLI application implementation.
*
* Get all the power of Joomla in CLI without all the awkward decisions which make CLI scripts fail on many common,
* commercial hosting environments. We've been doing that in our software before Joomla got CLI support. We know of all
* the pitfalls and this little gem here will work around most of them (or at least fail gracefully).
*
* Your CLI script must begin with the following boilerplate code:
*
* // Boilerplate -- START
* define('_JEXEC', 1);
*
* foreach ([__DIR__, getcwd()] as $curdir)
* {
* if (file_exists($curdir . '/defines.php'))
* {
* define('JPATH_BASE', realpath($curdir . '/..'));
* require_once $curdir . '/defines.php';
*
* break;
* }
*
* if (file_exists($curdir . '/../includes/defines.php'))
* {
* define('JPATH_BASE', realpath($curdir . '/..'));
* require_once $curdir . '/../includes/defines.php';
*
* break;
* }
* }
*
* defined('JPATH_LIBRARIES') || die ('This script must be placed in or run from the cli folder of your site.');
*
* require_once JPATH_LIBRARIES . '/fof40/Cli/Application.php';
* // Boilerplate -- END
*
* Create a class which extends FOFCliApplication and implements doExecute, e.g.
*
* // Class definition -- START
* class YourClassName extends FOFCliApplication
* {
* protected function doExecute()
* {
* // Do something useful
* }
* }
* // Class definition -- END
*
* Finally, execute your script with:
*
* // Execute script -- START
* FOFCliApplication::getInstance('YourClassName')->execute();
* // Execute script -- END
*
* You can optionally define $minphp before the boilerplate code to enforce a different minimum PHP version.
*/
// Abort immediately when this file is executed from a web SAPI
if (array_key_exists('REQUEST_METHOD', $_SERVER))
{
die('This is a command line script. You are not allowed to access it over the web.');
}
// Work around some badly configured servers which print out notices
if (function_exists('error_reporting'))
{
$oldLevel = error_reporting(E_ERROR | E_NOTICE | E_DEPRECATED);
}
// Minimum PHP version check
if (!isset($minphp))
{
$minphp = '5.6.0';
}
if (version_compare(PHP_VERSION, $minphp, 'lt'))
{
require_once __DIR__ . '/wrong_php.php';
die;
}
// Required by scripts written for old Joomla! versions.
define('DS', DIRECTORY_SEPARATOR);
/**
* Timezone fix
*
* This piece of code was originally put here because some PHP 5.3 servers forgot to declare a default timezone.
* Unfortunately it's still required because some hosts STILL forget to provide a timezone in their php.ini files or,
* worse, use invalid timezone names.
*/
if (function_exists('date_default_timezone_get') && function_exists('date_default_timezone_set'))
{
$serverTimezone = @date_default_timezone_get();
// Do I have no timezone set?
if (empty($serverTimezone) || !is_string($serverTimezone))
{
$serverTimezone = 'UTC';
}
// Do I have an invalid timezone?
try
{
$testTimeZone = new DateTimeZone($serverTimezone);
}
catch (\Exception $e)
{
$serverTimezone = 'UTC';
}
// Set the default timezone to a correct thing
@date_default_timezone_set($serverTimezone);
}
// This is not necessary if you have used the boilerplate code.
if (!isset($curdir) && !defined('JPATH_ROOT'))
{
foreach ([__DIR__ . '/../../../cli', getcwd()] as $curdir)
{
if (file_exists($curdir . '/defines.php'))
{
define('JPATH_BASE', realpath($curdir . '/..'));
require_once $curdir . '/defines.php';
break;
}
if (file_exists($curdir . '/../includes/defines.php'))
{
define('JPATH_BASE', realpath($curdir . '/..'));
require_once $curdir . '/../includes/defines.php';
break;
}
}
defined('JPATH_LIBRARIES') || die ('This script must be placed in or run from the cli folder of your site.');
}
// Restore the error reporting before importing Joomla core code
if (function_exists('error_reporting'))
{
error_reporting($oldLevel);
}
// Awkward Joomla version detection before we can actually load Joomla! itself
$joomlaMajorVersion = 3;
$joomlaMinorVersion = 0;
$jVersionFile = JPATH_LIBRARIES . '/src/Version.php';
if ($versionFileContents = @file_get_contents($jVersionFile))
{
preg_match("/MAJOR_VERSION\s*=\s*(\d*)\s*;/", $versionFileContents, $versionMatches);
$joomlaMajorVersion = (int) $versionMatches[1];
preg_match("/MINOR_VERSION\s*=\s*(\d*)\s*;/", $versionFileContents, $versionMatches);
$joomlaMinorVersion = (int) $versionMatches[1];
}
// Load the Trait files
include_once __DIR__ . '/Traits/CGIModeAware.php';
include_once __DIR__ . '/Traits/CustomOptionsAware.php';
include_once __DIR__ . '/Traits/JoomlaConfigAware.php';
include_once __DIR__ . '/Traits/MemStatsAware.php';
include_once __DIR__ . '/Traits/MessageAware.php';
include_once __DIR__ . '/Traits/TimeAgoAware.php';
// The actual implementation of the CliApplication depends on the Joomla version we're running under
switch ($joomlaMajorVersion)
{
case 3:
default:
require_once __DIR__ . '/Joomla3.php';
abstract class FOFApplicationCLI extends FOFCliApplicationJoomla3
{
}
;
break;
case 4:
require_once __DIR__ . '/Joomla4.php';
abstract class FOFApplicationCLI extends FOFCliApplicationJoomla4
{
}
;
break;
}
/**
* A default exception handler. Catches all unhandled exceptions, displays debug information about them and sets the
* error level to 254.
*
* @param Throwable $ex The Exception / Error being handled
*/
function FOFCliExceptionHandler($ex)
{
echo "\n\n";
echo "********** ERROR! **********\n\n";
echo $ex->getMessage();
echo "\n\nTechnical information:\n\n";
echo "Code: " . $ex->getCode() . "\n";
echo "File: " . $ex->getFile() . "\n";
echo "Line: " . $ex->getLine() . "\n";
echo "\nStack Trace:\n\n" . $ex->getTraceAsString();
echo "\n\n";
exit(254);
}
/**
* Timeout handler
*
* This function is registered as a shutdown script. If a catchable timeout occurs it will detect it and print a helpful
* error message instead of just dying cold. The error level is set to 253 in this case.
*
* @return void
*/
function FOFCliTimeoutHandler()
{
$connection_status = connection_status();
if ($connection_status == 0)
{
// Normal script termination, do not report an error.
return;
}
echo "\n\n";
echo "********** ERROR! **********\n\n";
if ($connection_status == 1)
{
echo <<< END
The process was aborted on user's request.
This usually means that you pressed CTRL-C to terminate the script (if you're
running it from a terminal / SSH session), or that your host's CRON daemon
aborted the execution of this script.
If you are running this script through a CRON job and saw this message, please
contact your host and request an increase in the timeout limit for CRON jobs.
Moreover you need to ask them to increase the max_execution_time in the
php.ini file or, even better, set it to 0.
END;
}
else
{
echo <<< END
This script has timed out. As a result, the process has FAILED to complete.
Your host applies a maximum execution time for CRON jobs which is too low for
this script to work properly. Please contact your host and request an increase
in the timeout limit for CRON jobs. Moreover you need to ask them to increase
the max_execution_time in the php.ini file or, even better, set it to 0.
END;
if (!function_exists('php_ini_loaded_file'))
{
echo "\n\n";
return;
}
$ini_location = php_ini_loaded_file();
echo <<<END
The php.ini file your host will need to modify is located at:
$ini_location
Info for the host: the location above is reported by PHP's php_ini_loaded_file() method.
END;
echo "\n\n";
exit(253);
}
}
/**
* Error handler. It tries to catch fatal errors and report them in a meaningful way. Obviously it only works for
* catchable fatal errors. It sets the error level to 252.
*
* IMPORTANT! Under PHP 7 the default exception handler will be called instead, including when there is a non-catchable
* fatal error.
*
* @param int $errno Error number
* @param string $errstr Error string, tells us what went wrong
* @param string $errfile Full path to file where the error occurred
* @param int $errline Line number where the error occurred
*
* @return void
*/
function FOFCliErrorHandler($errno, $errstr, $errfile, $errline)
{
switch ($errno)
{
case E_ERROR:
case E_USER_ERROR:
echo "\n\n";
echo "********** ERROR! **********\n\n";
echo "PHP Fatal Error: $errstr";
echo "\n\nTechnical information:\n\n";
echo "File: " . $errfile . "\n";
echo "Line: " . $errline . "\n";
echo "\nStack Trace:\n\n" . debug_backtrace();
echo "\n\n";
exit(252);
break;
default:
break;
}
}
/**
* Custom default handlers for otherwise unhandled exceptions and PHP catchable errors.
*
* Moreover, we register a shutdown function to catch timeouts and SIGTERM signals, because some hosts *are* monsters.
*/
set_exception_handler('FOFCliExceptionHandler');
set_error_handler('FOFCliErrorHandler', E_ERROR | E_USER_ERROR);
register_shutdown_function('FOFCliTimeoutHandler');

View File

@ -0,0 +1,134 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
// Do not put the JEXEC or die check on this file
use FOF40\Cli\Traits\CGIModeAware;
use FOF40\Cli\Traits\CustomOptionsAware;
use FOF40\Cli\Traits\JoomlaConfigAware;
use FOF40\Cli\Traits\MemStatsAware;
use FOF40\Cli\Traits\MessageAware;
use FOF40\Cli\Traits\TimeAgoAware;
use FOF40\Utils\CliSessionHandler;
use Joomla\CMS\Application\CliApplication;
use Joomla\CMS\Input\Cli;
// Load the legacy Joomla! include files (Joomla! 3 only)
include_once JPATH_LIBRARIES . '/import.legacy.php';
// Load the CMS import file if it exists (newer Joomla! 3 versions and Joomla! 4)
$cmsImportFilePath = JPATH_LIBRARIES . '/cms.php';
if (@file_exists($cmsImportFilePath))
{
@include_once $cmsImportFilePath;
}
/**
* Base class for a Joomla! command line application. Adapted from JCli / JApplicationCli
*/
abstract class FOFCliApplicationJoomla3 extends CliApplication
{
use CGIModeAware, CustomOptionsAware, JoomlaConfigAware, MemStatsAware, MessageAware, TimeAgoAware;
private $allowedToClose = false;
public static function getInstance($name = null)
{
// Load the Joomla global configuration in JFactory. This must happen BEFORE loading FOF.
JFactory::getConfig(JPATH_CONFIGURATION . '/configuration.php');
// Load FOF
if (!defined('FOF40_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof40/include.php'))
{
throw new RuntimeException('Cannot load FOF', 500);
}
// Create a CLI-specific session
JFactory::$session = JSession::getInstance('none', [
'expire' => 84400,
], new CliSessionHandler());
$instance = parent::getInstance($name);
JFactory::$application = $instance;
return $instance;
}
public function __construct(Cli $input = null, \Joomla\Registry\Registry $config = null, \JEventDispatcher $dispatcher = null)
{
// Some servers only provide a CGI executable. While not ideal for running CLI applications we can make do.
$this->detectAndWorkAroundCGIMode();
// Initialize custom options handling which is a bit more straightforward than Input\Cli.
$this->initialiseCustomOptions();
parent::__construct($input, $config, $dispatcher);
/**
* Allow the application to close.
*
* This is required to allow CliApplication to execute under CGI mode. The checks performed in the parent
* constructor will call close() if the application does not run pure CLI mode. However, some hosts only provide
* the PHP CGI binary for executing CLI scripts. While wrong it will work in most cases. By default close() will
* do nothing, thereby allowing the parent constructor to call it without a problem. Finally, we set this flag
* to true to allow doExecute() to call close() and actually close the application properly. Yeehaw!
*/
$this->allowedToClose = true;
}
/**
* Method to close the application.
*
* See the constructor for details on why it works the way it works.
*
* @param integer $code The exit code (optional; default is 0).
*
* @return void
*
* @codeCoverageIgnore
* @since 1.0
*/
public function close($code = 0)
{
// See the constructor for details
if (!$this->allowedToClose)
{
return;
}
exit($code);
}
/**
* Gets the name of the current running application.
*
* @return string The name of the application.
*
* @since 4.0.0
*/
public function getName()
{
return get_class($this);
}
/**
* 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 \Joomla\CMS\Menu\AbstractMenu|null A AbstractMenu object or null if not set.
*
* @since 4.0.0
*/
public function getMenu($name = null, $options = [])
{
return null;
}
}

View File

@ -0,0 +1,196 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
// Do not put the JEXEC or die check on this file
use FOF40\Cli\Traits\CGIModeAware;
use FOF40\Cli\Traits\CustomOptionsAware;
use FOF40\Cli\Traits\JoomlaConfigAware;
use FOF40\Cli\Traits\MemStatsAware;
use FOF40\Cli\Traits\TimeAgoAware;
use Joomla\CMS\Application\CliApplication;
use Joomla\CMS\Application\ExtensionNamespaceMapper;
use Joomla\CMS\Factory;
use Joomla\Event\Dispatcher;
use Joomla\Registry\Registry;
use Joomla\Session\SessionInterface;
/**
* Load the legacy Joomla! include files
*
* Despite Joomla complaining about it with an E_DEPRECATED notice, if you use bootstrap.php instead of
* import.legacy.php you get an HTML error page (yes, under CLI!) which is kinda daft.
*/
if (function_exists('error_reporting'))
{
$oldErrorReporting = @error_reporting(E_ERROR | E_NOTICE | E_DEPRECATED);
}
include_once JPATH_LIBRARIES . '/import.legacy.php';
if (function_exists('error_reporting'))
{
@error_reporting($oldErrorReporting);
}
// Load the Framework (J4 beta 1 and later) or CMS import file (J4 a12 and lower)
$cmsImportFilePath = JPATH_BASE . '/includes/framework.php';
$cmsImportFilePathOld = JPATH_LIBRARIES . '/cms.php';
if (@file_exists($cmsImportFilePath))
{
@include_once $cmsImportFilePath;
// Boot the DI container
$container = \Joomla\CMS\Factory::getContainer();
/*
* Alias the session service keys to the CLI session service as that is the primary session backend for this application
*
* In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects
* is supported. This includes aliases for aliased class names, and the keys for aliased class names should be considered
* deprecated to be removed when the class name alias is removed as well.
*/
$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');
}
elseif (@file_exists($cmsImportFilePathOld))
{
@include_once $cmsImportFilePathOld;
}
/**
* Base class for a Joomla! command line application. Adapted from JCli / JApplicationCli
*/
abstract class FOFCliApplicationJoomla4 extends CliApplication
{
use CGIModeAware, CustomOptionsAware, JoomlaConfigAware, MemStatsAware, TimeAgoAware, ExtensionNamespaceMapper;
private $allowedToClose = false;
public static function getInstance($name = null)
{
$instance = parent::getInstance($name);
Factory::$application = $instance;
/**
* Load FOF.
*
* In Joomla 4 this must happen after we have set up the application in the factory because Factory::getLanguage
* goes through the application object to retrieve the configuration.
*/
if (!defined('FOF40_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof40/include.php'))
{
throw new RuntimeException('Cannot load FOF', 500);
}
return $instance;
}
public function __construct(\Joomla\Input\Input $input = null, Registry $config = null, \Joomla\CMS\Application\CLI\CliOutput $output = null, \Joomla\CMS\Application\CLI\CliInput $cliInput = null, \Joomla\Event\DispatcherInterface $dispatcher = null, \Joomla\DI\Container $container = null)
{
// Some servers only provide a CGI executable. While not ideal for running CLI applications we can make do.
$this->detectAndWorkAroundCGIMode();
// We need to tell Joomla to register its default namespace conventions
$this->createExtensionNamespaceMap();
// Initialize custom options handling which is a bit more straightforward than Input\Cli.
$this->initialiseCustomOptions();
// Default configuration: Joomla Global Configuration
if (empty($config))
{
$config = new Registry($this->fetchConfigurationData());
}
if (empty($dispatcher))
{
$dispatcher = new Dispatcher();
}
parent::__construct($input, $config, $output, $cliInput, $dispatcher, $container);
/**
* Allow the application to close.
*
* This is required to allow CliApplication to execute under CGI mode. The checks performed in the parent
* constructor will call close() if the application does not run pure CLI mode. However, some hosts only provide
* the PHP CGI binary for executing CLI scripts. While wrong it will work in most cases. By default close() will
* do nothing, thereby allowing the parent constructor to call it without a problem. Finally, we set this flag
* to true to allow doExecute() to call close() and actually close the application properly. Yeehaw!
*/
$this->allowedToClose = true;
}
/**
* Method to close the application.
*
* See the constructor for details on why it works the way it works.
*
* @param integer $code The exit code (optional; default is 0).
*
* @return void
*
* @codeCoverageIgnore
* @since 1.0
*/
public function close($code = 0)
{
// See the constructor for details
if (!$this->allowedToClose)
{
return;
}
exit($code);
}
/**
* Gets the name of the current running application.
*
* @return string The name of the application.
*
* @since 4.0.0
*/
public function getName()
{
return get_class($this);
}
/**
* 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 \Joomla\CMS\Menu\AbstractMenu|null A AbstractMenu object or null if not set.
*
* @since 4.0.0
*/
public function getMenu($name = null, $options = [])
{
return null;
}
/**
* Method to get the application session object.
*
* @return SessionInterface The session object
*
* @since 4.0.0
*/
public function getSession()
{
return $this->getContainer()->get('session.cli');
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Cli\Traits;
defined('_JEXEC') || die;
/**
* CGI Mode detection and workaround
*
* Some hosts only give access to the PHP CGI binary, even for running CLI scripts. While problematic, it mostly works.
* This trait detects PHP-CGI and manipulates $_GET in such a way that we populate the $argv and $argc global variables
* in the same way that PHP-CLI would set them. This allows the CLI input object to work. Moreover, we unset the PHP
* execution time limit, if possible, to prevent accidental timeouts.
*
* @package FOF40\Cli\Traits
*/
trait CGIModeAware
{
/**
* Detect if we are running under CGI mode. In this case it populates the global $argv and $argc parameters off the
* CGI input ($_GET superglobal).
*/
private function detectAndWorkAroundCGIMode()
{
// This code only executes when running under CGI. So let's detect it first.
$cgiMode = (!defined('STDOUT') || !defined('STDIN') || !isset($_SERVER['argv']));
if (!$cgiMode)
{
return;
}
// CGI mode has a time limit. Unset it to prevent timeouts.
if (function_exists('set_time_limit'))
{
set_time_limit(0);
}
// Convert $_GET into the appropriate $argv representation. This allows Input\Cli to work under PHP-CGI.
$query = "";
if (!empty($_GET))
{
foreach ($_GET as $k => $v)
{
$query .= " $k";
if ($v != "")
{
$query .= "=$v";
}
}
}
$query = ltrim($query);
global $argv, $argc;
$argv = explode(' ', $query);
$argc = count($argv);
$_SERVER['argv'] = $argv;
}
}

View File

@ -0,0 +1,204 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Cli\Traits;
defined('_JEXEC') || die;
use JFilterInput;
use Joomla\CMS\Filter\InputFilter;
/**
* Implements a simpler, more straightforward options parser than the Joomla CLI input object. It supports short options
* when the Joomla CLI input object doesn't. Eventually this will go away and we can use something like Symfony Console
* instead.
*
* @package FOF40\Cli\Traits
*/
trait CustomOptionsAware
{
/**
* POSIX-style CLI options. Access them with through the getOption method.
*
* @var array
*/
protected static $cliOptions = [];
/**
* Filter object to use for custom options parsing.
*
* @var JFilterInput|InputFilter
*/
protected $filter = null;
/**
* Initializes the custom CLI options parsing
*
* @return void
*/
protected function initialiseCustomOptions()
{
// Create a new JFilterInput
if (class_exists('JFilterInput'))
{
$this->filter = JFilterInput::getInstance();
}
else
{
$this->filter = InputFilter::getInstance();
}
// Parse the POSIX options
$this->parseOptions();
}
/**
* Parses POSIX command line options and sets the self::$cliOptions associative array. Each array item contains
* a single dimensional array of values. Arguments without a dash are silently ignored.
*
* This works much better than JInputCli since it allows you to use all valid POSIX ways of defining CLI parameters.
*
* @return void
*/
protected function parseOptions()
{
global $argc, $argv;
// Workaround for PHP-CGI
if (!isset($argc) && !isset($argv))
{
$query = "";
if (!empty($_GET))
{
foreach ($_GET as $k => $v)
{
$query .= " $k";
if ($v != "")
{
$query .= "=$v";
}
}
}
$query = ltrim($query);
$argv = explode(' ', $query);
$argc = count($argv);
}
$currentName = "";
$options = [];
for ($i = 1; $i < $argc; $i++)
{
$argument = $argv[$i];
$value = $argument;
if (strpos($argument, "-") === 0)
{
$argument = ltrim($argument, '-');
$name = $argument;
$value = null;
if (strstr($argument, '='))
{
list($name, $value) = explode('=', $argument, 2);
}
$currentName = $name;
if (!isset($options[$currentName]) || ($options[$currentName] == null))
{
$options[$currentName] = [];
}
}
if ((!is_null($value)) && (!is_null($currentName)))
{
$key = null;
if (strstr($value, '='))
{
$parts = explode('=', $value, 2);
$key = $parts[0];
$value = $parts[1];
}
$values = $options[$currentName];
if (is_null($values))
{
$values = [];
}
if (is_null($key))
{
array_push($values, $value);
}
else
{
$values[$key] = $value;
}
$options[$currentName] = $values;
}
}
self::$cliOptions = $options;
}
/**
* Returns the value of a command line option. This does NOT use JInputCLI. You MUST run parseOptions before.
*
* @param string $key The full name of the option, e.g. "foobar"
* @param mixed $default The default value to return
* @param string $type Joomla! filter type, e.g. cmd, int, bool and so on.
*
* @return mixed The value of the option
*/
protected function getOption($key, $default = null, $type = 'raw')
{
// If the key doesn't exist set it to the default value
if (!array_key_exists($key, self::$cliOptions))
{
self::$cliOptions[$key] = is_array($default) ? $default : [$default];
}
$type = strtolower($type);
if ($type == 'array')
{
return self::$cliOptions[$key];
}
$value = null;
if (!empty(self::$cliOptions[$key]))
{
$value = self::$cliOptions[$key][0];
}
return $this->filterVariable($value, $type);
}
/**
* Filter a variable using JInputFilter
*
* @param mixed $var The variable to filter
* @param string $type The filter type, default 'cmd'
*
* @return mixed The filtered value
*/
protected function filterVariable($var, $type = 'cmd')
{
return $this->filter->clean($var, $type);
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Cli\Traits;
defined('_JEXEC') || die;
/**
* Allows the CLI application to use the Joomla Global Configuration parameters as its own configuration.
*
* @package FOF40\Cli\Traits
*/
trait JoomlaConfigAware
{
/**
* Method to load the application configuration, returning it as an object or array
*
* This can be overridden in subclasses if you don't want to fetch config from a PHP class file.
*
* @param string|null $file The filepath to the file containing the configuration class. Default: Joomla's
* configuration.php
* @param string $className The name of the PHP class holding the configuration. Default: JConfig
*
* @return mixed Either an array or object to be loaded into the configuration object.
*/
protected function fetchConfigurationData($file = null, $className = 'JConfig')
{
// Set the configuration file name.
if (empty($file))
{
$file = JPATH_BASE . '/configuration.php';
}
// Import the configuration file.
if (!is_file($file))
{
return [];
}
include_once $file;
// Instantiate the configuration object.
if (!class_exists('JConfig'))
{
return [];
}
return new $className();
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Cli\Traits;
defined('_JEXEC') || die;
/**
* Memory statistics
*
* This is an optional trait which allows the developer to print memory usage statistics and format byte sizes into
* human-readable strings.
*
* @package FOF40\Cli\Traits
*/
trait MemStatsAware
{
/**
* Formats a number of bytes in human readable format
*
* @param int $size The size in bytes to format, e.g. 8254862
*
* @return string The human-readable representation of the byte size, e.g. "7.87 Mb"
*/
protected function formatByteSize($size)
{
$unit = ['b', 'KB', 'MB', 'GB', 'TB', 'PB'];
return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
}
/**
* Returns the current memory usage, formatted
*
* @return string
*/
protected function memUsage()
{
if (function_exists('memory_get_usage'))
{
$size = memory_get_usage();
return $this->formatByteSize($size);
}
else
{
return "(unknown)";
}
}
/**
* Returns the peak memory usage, formatted
*
* @return string
*/
protected function peakMemUsage()
{
if (function_exists('memory_get_peak_usage'))
{
$size = memory_get_peak_usage();
return $this->formatByteSize($size);
}
else
{
return "(unknown)";
}
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Cli\Traits;
defined('_JEXEC') || die;
/**
* Sometimes other extensions will try to enqueue messages to the application. Methods for those tasks only exists in
* web applications, so we have to replicate their behavior in CLI environment or fatal errors will occur
*
* @package FOF40\Cli\Traits
*/
trait MessageAware
{
/** @var array Queue holding all messages */
protected $messageQueue = [];
/**
* @param $msg
* @param $type
*
* @return void
*/
public function enqueueMessage($msg, $type)
{
// Don't add empty messages.
if (trim($msg) === '')
{
return;
}
$message = ['message' => $msg, 'type' => strtolower($type)];
if (!in_array($message, $this->messageQueue))
{
// Enqueue the message.
$this->messageQueue[] = $message;
}
}
/**
* Loosely based on Joomla getMessageQueue
*
* @param bool $clear
*
* @return array
*/
public function getMessageQueue($clear = false)
{
$messageQueue = $this->messageQueue;
if ($clear)
{
$this->messageQueue = [];
}
return $messageQueue;
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Cli\Traits;
defined('_JEXEC') || die;
/**
* Allows the developer to show the relative time difference between two timestamps.
*
* @package FOF40\Cli\Traits
*/
trait TimeAgoAware
{
/**
* Returns the relative time difference between two timestamps in a human readable format
*
* @param int $referenceTimestamp Timestamp of the reference date/time
* @param int|null $currentTimestamp Timestamp of the current date/time. Null for time().
* @param string $timeUnit Time unit. One of s, m, h, d, or y.
* @param bool $autoSuffix Add "ago" / "from now" suffix?
*
* @return string For example, "10 seconds ago"
*/
protected function timeAgo($referenceTimestamp = 0, $currentTimestamp = null, $timeUnit = '', $autoSuffix = true)
{
if (is_null($currentTimestamp))
{
$currentTimestamp = time();
}
// Raw time difference
$raw = $currentTimestamp - $referenceTimestamp;
$clean = abs($raw);
$calcNum = [
['s', 60],
['m', 60 * 60],
['h', 60 * 60 * 60],
['d', 60 * 60 * 60 * 24],
['y', 60 * 60 * 60 * 24 * 365],
];
$calc = [
's' => [1, 'second'],
'm' => [60, 'minute'],
'h' => [60 * 60, 'hour'],
'd' => [60 * 60 * 24, 'day'],
'y' => [60 * 60 * 24 * 365, 'year'],
];
$effectiveTimeUnit = $timeUnit;
if ($timeUnit == '')
{
$effectiveTimeUnit = 's';
for ($i = 0; $i < count($calcNum); $i++)
{
if ($clean <= $calcNum[$i][1])
{
$effectiveTimeUnit = $calcNum[$i][0];
$i = count($calcNum);
}
}
}
$timeDifference = floor($clean / $calc[$effectiveTimeUnit][0]);
$textSuffix = '';
if ($autoSuffix == true && ($currentTimestamp == time()))
{
if ($raw < 0)
{
$textSuffix = ' from now';
}
else
{
$textSuffix = ' ago';
}
}
if ($referenceTimestamp != 0)
{
if ($timeDifference == 1)
{
return $timeDifference . ' ' . $calc[$effectiveTimeUnit][1] . ' ' . $textSuffix;
}
return $timeDifference . ' ' . $calc[$effectiveTimeUnit][1] . 's ' . $textSuffix;
}
return '(no reference timestamp was provided).';
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
/** @var string $minphp */
if (!isset($minphp))
{
die;
}
?>
================================================================================
WARNING! Incompatible PHP version <?php echo PHP_VERSION ?> (required: <?php echo $minphp ?> or later)
================================================================================
This script must be run using PHP version <?php echo $minphp ?> or later. Your server is
currently using a much older version which would cause this script to crash. As
a result we have aborted execution of the script. Please contact your host and
ask them for the correct path to the PHP CLI binary for PHP <?php echo $minphp ?> or later, then
edit your CRON job and replace your current path to PHP with the one your host
gave you.
For your information, the current PHP version information is as follows.
PATH: <?php echo PHP_BINDIR ?>
VERSION: <?php echo PHP_VERSION ?>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
IMPORTANT!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PHP version numbers are NOT decimals! Trailing zeros do matter. For example,
PHP 5.3.28 is twenty four versions newer (greater than) than PHP 5.3.4.
Please consult https://www.akeeba.com/how-do-version-numbers-work.html
Further clarifications:
1. There is no possible way that you are receiving this message in error. We
are using the PHP_VERSION constant to detect the PHP version you are
currently using. This is what PHP itself reports as its own version. It
simply cannot lie.
2. Even though your *site* may be running in a higher PHP version that the one
reported above, your CRON scripts will most likely not be running under it.
This has to do with the fact that your site DOES NOT run under the command
line and there are different executable files (binaries) for the web and
command line versions of PHP.
3. Please note that we cannot provide support about this error as the solution
depends only on your server setup. The only people who know how your server
is set up are your host's technicians. Therefore we can only advise you to
contact your host and request them the correct path to the PHP CLI binary.
Let us stress out that only your host knows and can give this information
to you.
4. The latest published versions of PHP can be found at http://www.php.net/
Any older version is considered insecure and must not be used on a
production site. If your server uses a much older version of PHP than those
published in the URL above please notify your host that their servers are
insecure and in need of an update.
This script will now terminate. Goodbye.

View File

@ -0,0 +1,229 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration;
use FOF40\Container\Container;
defined('_JEXEC') || die;
/**
* Reads and parses the fof.xml file in the back-end of a FOF-powered component,
* provisioning the data to the rest of the FOF framework
*
* @since 2.1
*/
class Configuration
{
/**
* Cache of FOF components' configuration variables
*
* @var array
*/
public static $configurations = [];
/**
* The component's container
*
* @var Container
*/
protected $container;
private $domains = null;
function __construct(Container $c)
{
$this->container = $c;
$this->parseComponent();
}
/**
* Returns the value of a variable. Variables use a dot notation, e.g.
* view.config.whatever where the first part is the domain, the rest of the
* parts specify the path to the variable.
*
* @param string $variable The variable name
* @param mixed $default The default value, or null if not specified
*
* @return mixed The value of the variable
*/
public function get(string $variable, $default = null)
{
$domains = $this->getDomains();
[$domain, $var] = explode('.', $variable, 2);
if (!in_array(ucfirst($domain), $domains))
{
return $default;
}
$class = '\\FOF40\\Configuration\\Domain\\' . ucfirst($domain);
/** @var \FOF40\Configuration\Domain\DomainInterface $o */
$o = new $class;
return $o->get(self::$configurations[$this->container->componentName], $var, $default);
}
/**
* Gets a list of the available configuration domain adapters
*
* @return array A list of the available domains
*/
protected function getDomains(): array
{
if (is_null($this->domains))
{
$filesystem = $this->container->filesystem;
$files = $filesystem->folderFiles(__DIR__ . '/Domain', '.php');
if (!empty($files))
{
foreach ($files as $file)
{
$domain = basename($file, '.php');
if ($domain == 'DomainInterface')
{
continue;
}
$domain = preg_replace('/[^A-Za-z0-9]/', '', $domain);
$this->domains[] = $domain;
}
$this->domains = array_unique($this->domains);
}
}
return $this->domains;
}
/**
* Parses the configuration of the specified component
*
* @return void
*/
protected function parseComponent(): void
{
if ($this->container->platform->isCli())
{
$order = ['cli', 'backend'];
}
elseif ($this->container->platform->isBackend())
{
$order = ['backend'];
}
else
{
$order = ['frontend'];
}
$order[] = 'common';
$order = array_reverse($order);
self::$configurations[$this->container->componentName] = [];
foreach ([false, true] as $userConfig)
{
foreach ($order as $area)
{
$config = $this->parseComponentArea($area, $userConfig);
self::$configurations[$this->container->componentName] = array_replace_recursive(self::$configurations[$this->container->componentName], $config);
}
}
}
/**
* Parses the configuration options of a specific component area
*
* @param string $area Which area to parse (frontend, backend, cli)
* @param bool $userConfig When true the user configuration (fof.user.xml) file will be read
*
* @return array A hash array with the configuration data
*/
protected function parseComponentArea(string $area, bool $userConfig = false): array
{
$component = $this->container->componentName;
// Initialise the return array
$ret = [];
// Get the folders of the component
$componentPaths = $this->container->platform->getComponentBaseDirs($component);
$filesystem = $this->container->filesystem;
$path = $componentPaths['admin'];
if (isset($this->container['backEndPath']))
{
$path = $this->container['backEndPath'];
}
// This line unfortunately doesn't work with Unit Tests because JPath depends on the JPATH_SITE constant :(
// $path = $filesystem->pathCheck($path);
// Check that the path exists
if (!$filesystem->folderExists($path))
{
return $ret;
}
// Read the filename if it exists
$filename = $path . '/fof.xml';
if ($userConfig)
{
$filename = $path . '/fof.user.xml';
}
if (!$filesystem->fileExists($filename) && !file_exists($filename))
{
return $ret;
}
$data = file_get_contents($filename);
// Load the XML data in a SimpleXMLElement object
$xml = simplexml_load_string($data);
if (!($xml instanceof \SimpleXMLElement))
{
return $ret;
}
// Get this area's data
$areaData = $xml->xpath('//' . $area);
if (empty($areaData))
{
return $ret;
}
$xml = array_shift($areaData);
// Parse individual configuration domains
$domains = $this->getDomains();
foreach ($domains as $dom)
{
$class = '\\FOF40\\Configuration\\Domain\\' . ucfirst($dom);
if (class_exists($class, true))
{
/** @var \FOF40\Configuration\Domain\DomainInterface $o */
$o = new $class;
$o->parseDomain($xml, $ret);
}
}
// Finally, return the result
return $ret;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration\Domain;
use SimpleXMLElement;
defined('_JEXEC') || die;
/**
* Configuration parser for the authentication-specific settings
*
* @since 2.1
*/
class Authentication implements DomainInterface
{
/**
* Parse the XML data, adding them to the $ret array
*
* @param SimpleXMLElement $xml The XML data of the component's configuration area
* @param array &$ret The parsed data, in the form of a hash array
*
* @return void
*/
public function parseDomain(SimpleXMLElement $xml, array &$ret): void
{
// Initialise
$ret['authentication'] = [];
// Parse the dispatcher configuration
$authenticationData = $xml->authentication;
// Sanity check
if (empty($authenticationData))
{
return;
}
$options = $xml->xpath('authentication/option');
foreach ($options as $option)
{
$key = (string) $option['name'];
$ret['authentication'][$key] = (string) $option;
}
}
/**
* Return a configuration variable
*
* @param string &$configuration Configuration variables (hashed array)
* @param string $var The variable we want to fetch
* @param mixed $default Default value
*
* @return mixed The variable's value
*/
public function get(array &$configuration, string $var, $default = null)
{
if ($var == '*')
{
return $configuration['authentication'];
}
if (isset($configuration['authentication'][$var]))
{
return $configuration['authentication'][$var];
}
else
{
return $default;
}
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration\Domain;
use SimpleXMLElement;
defined('_JEXEC') || die;
/**
* Configuration parser for the Container-specific settings
*
* @since 2.1
*/
class Container implements DomainInterface
{
/**
* Parse the XML data, adding them to the $ret array
*
* @param SimpleXMLElement $xml The XML data of the component's configuration area
* @param array &$ret The parsed data, in the form of a hash array
*
* @return void
*/
public function parseDomain(SimpleXMLElement $xml, array &$ret): void
{
// Initialise
$ret['container'] = [];
// Parse the dispatcher configuration
$containerData = $xml->container;
// Sanity check
if (empty($containerData))
{
return;
}
$options = $xml->xpath('container/option');
foreach ($options as $option)
{
$key = (string) $option['name'];
$ret['container'][$key] = (string) $option;
}
}
/**
* Return a configuration variable
*
* @param string &$configuration Configuration variables (hashed array)
* @param string $var The variable we want to fetch
* @param mixed $default Default value
*
* @return mixed The variable's value
*/
public function get(array &$configuration, string $var, $default = null)
{
if ($var == '*')
{
return $configuration['container'];
}
if (isset($configuration['container'][$var]))
{
return $configuration['container'][$var];
}
else
{
return $default;
}
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration\Domain;
use SimpleXMLElement;
defined('_JEXEC') || die;
/**
* Configuration parser for the dispatcher-specific settings
*
* @since 2.1
*/
class Dispatcher implements DomainInterface
{
/**
* Parse the XML data, adding them to the $ret array
*
* @param SimpleXMLElement $xml The XML data of the component's configuration area
* @param array &$ret The parsed data, in the form of a hash array
*
* @return void
*/
public function parseDomain(SimpleXMLElement $xml, array &$ret): void
{
// Initialise
$ret['dispatcher'] = [];
// Parse the dispatcher configuration
$dispatcherData = $xml->dispatcher;
// Sanity check
if (empty($dispatcherData))
{
return;
}
$options = $xml->xpath('dispatcher/option');
foreach ($options as $option)
{
$key = (string) $option['name'];
$ret['dispatcher'][$key] = (string) $option;
}
}
/**
* Return a configuration variable
*
* @param string &$configuration Configuration variables (hashed array)
* @param string $var The variable we want to fetch
* @param mixed $default Default value
*
* @return mixed The variable's value
*/
public function get(array &$configuration, string $var, $default = null)
{
if ($var == '*')
{
return $configuration['dispatcher'];
}
if (isset($configuration['dispatcher'][$var]))
{
return $configuration['dispatcher'][$var];
}
else
{
return $default;
}
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration\Domain;
use SimpleXMLElement;
defined('_JEXEC') || die;
/**
* The Interface of a FOF configuration domain class. The methods are used to parse and
* provision sensible information to consumers. The Configuration class acts as an
* adapter to the domain classes.
*
* @since 2.1
*/
interface DomainInterface
{
/**
* Parse the XML data, adding them to the $ret array
*
* @param SimpleXMLElement $xml The XML data of the component's configuration area
* @param array &$ret The parsed data, in the form of a hash array
*
* @return void
*/
public function parseDomain(SimpleXMLElement $xml, array &$ret): void;
/**
* Return a configuration variable
*
* @param array &$configuration Configuration variables (hashed array)
* @param string $var The variable we want to fetch
* @param mixed $default Default value
*
* @return mixed The variable's value
*/
public function get(array &$configuration, string $var, $default = null);
}

View File

@ -0,0 +1,358 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration\Domain;
use SimpleXMLElement;
defined('_JEXEC') || die;
/**
* Configuration parser for the models-specific settings
*
* @since 2.1
*/
class Models implements DomainInterface
{
/**
* Parse the XML data, adding them to the $ret array
*
* @param SimpleXMLElement $xml The XML data of the component's configuration area
* @param array &$ret The parsed data, in the form of a hash array
*
* @return void
*/
public function parseDomain(SimpleXMLElement $xml, array &$ret): void
{
// Initialise
$ret['models'] = [];
// Parse model configuration
$modelsData = $xml->xpath('model');
// Sanity check
if (empty($modelsData))
{
return;
}
foreach ($modelsData as $aModel)
{
$key = (string) $aModel['name'];
$ret['models'][$key]['behaviors'] = [];
$ret['models'][$key]['behaviorsMerge'] = false;
$ret['models'][$key]['tablealias'] = $aModel->xpath('tablealias');
$ret['models'][$key]['fields'] = [];
$ret['models'][$key]['relations'] = [];
$ret['models'][$key]['config'] = [];
// Parse configuration
$optionData = $aModel->xpath('config/option');
foreach ($optionData as $option)
{
$k = (string) $option['name'];
$ret['models'][$key]['config'][$k] = (string) $option;
}
// Parse field aliases
$fieldData = $aModel->xpath('field');
foreach ($fieldData as $field)
{
$k = (string) $field['name'];
$ret['models'][$key]['fields'][$k] = (string) $field;
}
// Parse behaviours
$behaviorsData = (string) $aModel->behaviors;
$behaviorsMerge = (string) $aModel->behaviors['merge'];
if (!empty($behaviorsMerge))
{
$behaviorsMerge = trim($behaviorsMerge);
$behaviorsMerge = strtoupper($behaviorsMerge);
if (in_array($behaviorsMerge, ['1', 'YES', 'ON', 'TRUE']))
{
$ret['models'][$key]['behaviorsMerge'] = true;
}
}
if (!empty($behaviorsData))
{
$behaviorsData = explode(',', $behaviorsData);
foreach ($behaviorsData as $behavior)
{
$behavior = trim($behavior);
if (empty($behavior))
{
continue;
}
$ret['models'][$key]['behaviors'][] = $behavior;
}
}
// Parse relations
$relationsData = $aModel->xpath('relation');
foreach ($relationsData as $relationData)
{
$type = (string) $relationData['type'];
$itemName = (string) $relationData['name'];
if (empty($type) || empty($itemName))
{
continue;
}
$modelClass = (string) $relationData['foreignModelClass'];
$localKey = (string) $relationData['localKey'];
$foreignKey = (string) $relationData['foreignKey'];
$pivotTable = (string) $relationData['pivotTable'];
$ourPivotKey = (string) $relationData['pivotLocalKey'];
$theirPivotKey = (string) $relationData['pivotForeignKey'];
$relation = [
'type' => $type,
'itemName' => $itemName,
'foreignModelClass' => empty($modelClass) ? null : $modelClass,
'localKey' => empty($localKey) ? null : $localKey,
'foreignKey' => empty($foreignKey) ? null : $foreignKey,
];
if (!empty($ourPivotKey) || !empty($theirPivotKey) || !empty($pivotTable))
{
$relation['pivotLocalKey'] = empty($ourPivotKey) ? null : $ourPivotKey;
$relation['pivotForeignKey'] = empty($theirPivotKey) ? null : $theirPivotKey;
$relation['pivotTable'] = empty($pivotTable) ? null : $pivotTable;
}
$ret['models'][$key]['relations'][] = $relation;
}
}
}
/**
* Return a configuration variable
*
* @param string &$configuration Configuration variables (hashed array)
* @param string $var The variable we want to fetch
* @param mixed $default Default value
*
* @return mixed The variable's value
*/
public function get(array &$configuration, string $var, $default = null)
{
$parts = explode('.', $var);
$view = $parts[0];
$method = 'get' . ucfirst($parts[1]);
if (!method_exists($this, $method))
{
return $default;
}
array_shift($parts);
array_shift($parts);
return $this->$method($view, $configuration, $parts, $default);
}
/**
* Internal method to return the magic field mapping
*
* @param string $model The model for which we will be fetching a field map
* @param array & $configuration The configuration parameters hash array
* @param array $params Extra options
* @param string|array|null $default Default magic field mapping; empty if not defined
*
* @return string|array|null Field map
*/
protected function getField(string $model, array &$configuration, array $params, $default = '')
{
$fieldmap = [];
if (isset($configuration['models']['*']) && isset($configuration['models']['*']['fields']))
{
$fieldmap = $configuration['models']['*']['fields'];
}
if (isset($configuration['models'][$model]) && isset($configuration['models'][$model]['fields']))
{
$fieldmap = array_merge($fieldmap, $configuration['models'][$model]['fields']);
}
$map = $default;
if (empty($params[0]) || ($params[0] == '*'))
{
$map = $fieldmap;
}
elseif (isset($fieldmap[$params[0]]))
{
$map = $fieldmap[$params[0]];
}
return $map;
}
/**
* Internal method to get model alias
*
* @param string $model The model for which we will be fetching table alias
* @param array $configuration [IN/OUT] The configuration parameters hash array
* @param array $params Ignored
* @param string|null $default Default table alias
*
* @return string|null Table alias
*/
protected function getTablealias(string $model, array &$configuration, array $params = [], ?string $default = null): ?string
{
$tableMap = [];
if (isset($configuration['models']['*']['tablealias']))
{
$tableMap = $configuration['models']['*']['tablealias'];
}
if (isset($configuration['models'][$model]['tablealias']))
{
$tableMap = array_merge($tableMap, $configuration['models'][$model]['tablealias']);
}
if (empty($tableMap))
{
return null;
}
return $tableMap[0];
}
/**
* Internal method to get model behaviours
*
* @param string $model The model for which we will be fetching behaviours
* @param array & $configuration The configuration parameters hash array
* @param array $params Unused
* @param array|null $default Default behaviour
*
* @return array|null Model behaviours
*/
protected function getBehaviors(string $model, array &$configuration, array $params = [], ?array $default = []): ?array
{
$behaviors = $default;
if (isset($configuration['models']['*'])
&& isset($configuration['models']['*']['behaviors'])
)
{
$behaviors = $configuration['models']['*']['behaviors'];
}
if (isset($configuration['models'][$model])
&& isset($configuration['models'][$model]['behaviors'])
)
{
$merge = false;
if (isset($configuration['models'][$model])
&& isset($configuration['models'][$model]['behaviorsMerge'])
)
{
$merge = (bool) $configuration['models'][$model]['behaviorsMerge'];
}
if ($merge)
{
$behaviors = array_merge($behaviors, $configuration['models'][$model]['behaviors']);
}
else
{
$behaviors = $configuration['models'][$model]['behaviors'];
}
}
return $behaviors;
}
/**
* Internal method to get model relations
*
* @param string $model The model for which we will be fetching relations
* @param array & $configuration The configuration parameters hash array
* @param array $params Unused
* @param array|null $default Default relations
*
* @return array|null Model relations
*/
protected function getRelations(string $model, array &$configuration, array $params = [], ?array $default = []): ?array
{
$relations = $default;
if (isset($configuration['models']['*'])
&& isset($configuration['models']['*']['relations'])
)
{
$relations = $configuration['models']['*']['relations'];
}
if (isset($configuration['models'][$model])
&& isset($configuration['models'][$model]['relations'])
)
{
$relations = $configuration['models'][$model]['relations'];
}
return $relations;
}
/**
* Internal method to return the a configuration option for the Model.
*
* @param string $model The view for which we will be fetching a task map
* @param array & $configuration The configuration parameters hash array
* @param array $params Extra options; key 0 defines the option variable we want to fetch
* @param string|array|null $default Default option; null if not defined
*
* @return string|array|null The setting for the requested option
*/
protected function getConfig(string $model, array &$configuration, array $params = [], $default = null)
{
$ret = $default;
$config = [];
if (isset($configuration['models']['*']['config']))
{
$config = $configuration['models']['*']['config'];
}
if (isset($configuration['models'][$model]['config']))
{
$config = array_merge($config, $configuration['models'][$model]['config']);
}
if (empty($params) || empty($params[0]))
{
return $config;
}
if (isset($config[$params[0]]))
{
$ret = $config[$params[0]];
}
return $ret;
}
}

View File

@ -0,0 +1,300 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Configuration\Domain;
use SimpleXMLElement;
defined('_JEXEC') || die;
/**
* Configuration parser for the view-specific settings
*
* @since 2.1
*/
class Views implements DomainInterface
{
/**
* Parse the XML data, adding them to the $ret array
*
* @param SimpleXMLElement $xml The XML data of the component's configuration area
* @param array &$ret The parsed data, in the form of a hash array
*
* @return void
*/
public function parseDomain(SimpleXMLElement $xml, array &$ret): void
{
// Initialise
$ret['views'] = [];
// Parse view configuration
$viewData = $xml->xpath('view');
// Sanity check
if (empty($viewData))
{
return;
}
foreach ($viewData as $aView)
{
$key = (string) $aView['name'];
// Parse ACL options
$ret['views'][$key]['acl'] = [];
$aclData = $aView->xpath('acl/task');
foreach ($aclData as $acl)
{
$k = (string) $acl['name'];
$ret['views'][$key]['acl'][$k] = (string) $acl;
}
// Parse taskmap
$ret['views'][$key]['taskmap'] = [];
$taskmapData = $aView->xpath('taskmap/task');
foreach ($taskmapData as $map)
{
$k = (string) $map['name'];
$ret['views'][$key]['taskmap'][$k] = (string) $map;
}
// Parse controller configuration
$ret['views'][$key]['config'] = [];
$optionData = $aView->xpath('config/option');
foreach ($optionData as $option)
{
$k = (string) $option['name'];
$ret['views'][$key]['config'][$k] = (string) $option;
}
// Parse the toolbar
$ret['views'][$key]['toolbar'] = [];
$toolBars = $aView->xpath('toolbar');
foreach ($toolBars as $toolBar)
{
$taskName = isset($toolBar['task']) ? (string) $toolBar['task'] : '*';
// If a toolbar title is specified, create a title element.
if (isset($toolBar['title']))
{
$ret['views'][$key]['toolbar'][$taskName]['title'] = [
'value' => (string) $toolBar['title'],
];
}
// Parse the toolbar buttons data
$toolbarData = $toolBar->xpath('button');
foreach ($toolbarData as $button)
{
$k = (string) $button['type'];
$ret['views'][$key]['toolbar'][$taskName][$k] = current($button->attributes());
$ret['views'][$key]['toolbar'][$taskName][$k]['value'] = (string) $button;
}
}
}
}
/**
* Return a configuration variable
*
* @param string &$configuration Configuration variables (hashed array)
* @param string $var The variable we want to fetch
* @param mixed $default Default value
*
* @return mixed The variable's value
*/
public function get(array &$configuration, string $var, $default = null)
{
$parts = explode('.', $var);
$view = $parts[0];
$method = 'get' . ucfirst($parts[1]);
if (!method_exists($this, $method))
{
return $default;
}
array_shift($parts);
array_shift($parts);
return $this->$method($view, $configuration, $parts, $default);
}
/**
* Internal function to return the task map for a view
*
* @param string $view The view for which we will be fetching a task map
* @param array &$configuration The configuration parameters hash array
* @param array $params Extra options (not used)
* @param array $default ßDefault task map; empty array if not provided
*
* @return array The task map as a hash array in the format task => method
*/
protected function getTaskmap(string $view, array &$configuration, array $params = [], ?array $default = []): ?array
{
$taskmap = [];
if (isset($configuration['views']['*']) && isset($configuration['views']['*']['taskmap']))
{
$taskmap = $configuration['views']['*']['taskmap'];
}
if (isset($configuration['views'][$view]) && isset($configuration['views'][$view]['taskmap']))
{
$taskmap = array_merge($taskmap, $configuration['views'][$view]['taskmap']);
}
if (empty($taskmap))
{
return $default;
}
return $taskmap;
}
/**
* Internal method to return the ACL mapping (privilege required to access
* a specific task) for the given view's tasks
*
* @param string $view The view for which we will be fetching a task map
* @param array &$configuration The configuration parameters hash array
* @param array $params Extra options; key 0 defines the task we want to fetch
* @param string $default Default ACL option; empty (no ACL check) if not defined
*
* @return string|array The privilege required to access this view
*/
protected function getAcl(string $view, array &$configuration, array $params = [], ?string $default = '')
{
$aclmap = [];
if (isset($configuration['views']['*']) && isset($configuration['views']['*']['acl']))
{
$aclmap = $configuration['views']['*']['acl'];
}
if (isset($configuration['views'][$view]) && isset($configuration['views'][$view]['acl']))
{
$aclmap = array_merge($aclmap, $configuration['views'][$view]['acl']);
}
$acl = $default;
if (empty($params) || empty($params[0]))
{
return $aclmap;
}
if (isset($aclmap['*']))
{
$acl = $aclmap['*'];
}
if (isset($aclmap[$params[0]]))
{
$acl = $aclmap[$params[0]];
}
return $acl;
}
/**
* Internal method to return the a configuration option for the view. These
* are equivalent to $config array options passed to the Controller
*
* @param string $view The view for which we will be fetching a task map
* @param array & $configuration The configuration parameters hash array
* @param array $params Extra options; key 0 defines the option variable we want to fetch
* @param string|array|null $default Default option; null if not defined
*
* @return string|array|null The setting for the requested option
*/
protected function getConfig(string $view, array &$configuration, array $params = [], $default = null)
{
$ret = $default;
$config = [];
if (isset($configuration['views']['*']['config']))
{
$config = $configuration['views']['*']['config'];
}
if (isset($configuration['views'][$view]['config']))
{
$config = array_merge($config, $configuration['views'][$view]['config']);
}
if (empty($params) || empty($params[0]))
{
return $config;
}
if (isset($config[$params[0]]))
{
$ret = $config[$params[0]];
}
return $ret;
}
/**
* Internal method to return the toolbar infos.
*
* @param string $view The view for which we will be fetching buttons
* @param array & $configuration The configuration parameters hash array
* @param array $params Extra options
* @param array|null $default Default option
*
* @return array|null The toolbar data for this view
*/
protected function getToolbar(string $view, array &$configuration, array $params = [], ?array $default = []): ?array
{
$toolbar = [];
if (isset($configuration['views']['*'])
&& isset($configuration['views']['*']['toolbar'])
&& isset($configuration['views']['*']['toolbar']['*']))
{
$toolbar = $configuration['views']['*']['toolbar']['*'];
}
if (isset($configuration['views']['*'])
&& isset($configuration['views']['*']['toolbar'])
&& isset($configuration['views']['*']['toolbar'][$params[0]]))
{
$toolbar = array_merge($toolbar, $configuration['views']['*']['toolbar'][$params[0]]);
}
if (isset($configuration['views'][$view])
&& isset($configuration['views'][$view]['toolbar'])
&& isset($configuration['views'][$view]['toolbar']['*']))
{
$toolbar = array_merge($toolbar, $configuration['views'][$view]['toolbar']['*']);
}
if (isset($configuration['views'][$view])
&& isset($configuration['views'][$view]['toolbar'])
&& isset($configuration['views'][$view]['toolbar'][$params[0]]))
{
$toolbar = array_merge($toolbar, $configuration['views'][$view]['toolbar'][$params[0]]);
}
if (empty($toolbar))
{
return $default;
}
return $toolbar;
}
}

View File

@ -0,0 +1,734 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Container;
defined('_JEXEC') || die;
use FOF40\Autoloader\Autoloader;
use FOF40\Configuration\Configuration;
use FOF40\Dispatcher\Dispatcher;
use FOF40\Encrypt\EncryptService;
use FOF40\Factory\FactoryInterface;
use FOF40\Inflector\Inflector;
use FOF40\Input\Input as FOFInput;
use FOF40\Params\Params;
use FOF40\Platform\FilesystemInterface;
use FOF40\Platform\Joomla\Filesystem as JoomlaFilesystem;
use FOF40\Platform\PlatformInterface;
use FOF40\Render\RenderInterface;
use FOF40\Template\Template;
use FOF40\Toolbar\Toolbar;
use FOF40\TransparentAuthentication\TransparentAuthentication as TransparentAuth;
use FOF40\Utils\MediaVersion;
use FOF40\View\Compiler\Blade;
use JDatabaseDriver;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Session\Session;
use Joomla\Input\Input as JoomlaInput;
/**
* Dependency injection container for FOF-powered components.
*
* The properties below (except componentName, bareComponentName and the ones marked with property-read) can be
* configured in the fof.xml component configuration file.
*
* Sample fof.xml:
*
* <fof>
* <common>
* <container>
* <option name="componentNamespace"><![CDATA[MyCompany\MyApplication]]></option>
* <option name="frontEndPath"><![CDATA[%PUBLIC%\components\com_application]]></option>
* <option name="factoryClass">magic</option>
* </container>
* </common>
* </fof>
*
* The paths can use the variables %ROOT%, %PUBLIC%, %ADMIN%, %TMP%, %LOG% i.e. all the path keys returned by
* Platform's
* getPlatformBaseDirs() method in uppercase and surrounded by percent signs.
*
*
* @property string $componentName The name of the component (com_something)
* @property string $bareComponentName The name of the component without com_ (something)
* @property string $componentNamespace The namespace of the component's classes (\Foobar)
* @property string $frontEndPath The absolute path to the front-end files
* @property string $backEndPath The absolute path to the back-end files
* @property string $thisPath The preferred path (e.g. backEndPath for Admin
* application)
* @property string $rendererClass View renderer classname. Must implement
* RenderInterface
* @property string $factoryClass MVC Factory classname, default
* FOF40\Factory\BasicFactory
* @property string $platformClass Platform classname, default
* FOF40\Platform\Joomla\Platform
* @property MediaVersion $mediaVersion A version string for media files in forms.
*
* @property-read Configuration $appConfig The application configuration registry
* @property-read Blade $blade The Blade view template compiler engine
* @property-read JDatabaseDriver $db The database connection object
* @property-read Dispatcher $dispatcher The component's dispatcher
* @property-read FactoryInterface $factory The MVC object factory
* @property-read FilesystemInterface $filesystem The filesystem abstraction layer object
* @property-read Inflector $inflector The English word inflector
* @property-read Params $params The component's params
* @property-read FOFInput $input The input object
* @property-read PlatformInterface $platform The platform abstraction layer object
* @property-read RenderInterface $renderer The view renderer
* @property-read Session $session Joomla! session storage
* @property-read Template $template The template helper
* @property-read TransparentAuth $transparentAuth Transparent authentication handler
* @property-read Toolbar $toolbar The component's toolbar
* @property-read EncryptService $crypto The component's data encryption service
*/
class Container extends ContainerBase
{
/**
* Cache of created container instances
*
* @var array
*/
protected static $instances = [];
/**
* Public constructor. This does NOT go through the fof.xml file. You are advised to use getInstance() instead.
*
* @param array $values Overrides for the container configuration and services
*
* @throws \FOF40\Container\Exception\NoComponent If no component name is specified
*/
public function __construct(array $values = [])
{
// Initialise
$this->bareComponentName = '';
$this->componentName = '';
$this->componentNamespace = '';
$this->frontEndPath = '';
$this->backEndPath = '';
$this->thisPath = '';
$this->factoryClass = 'FOF40\\Factory\\BasicFactory';
$this->platformClass = 'FOF40\\Platform\\Joomla\\Platform';
$initMediaVersion = null;
if (isset($values['mediaVersion']) && !is_object($values['mediaVersion']))
{
$initMediaVersion = $values['mediaVersion'];
unset($values['mediaVersion']);
}
// Try to construct this container object
parent::__construct($values);
// Make sure we have a component name
if (empty($this['componentName']))
{
throw new Exception\NoComponent;
}
$bareComponent = substr($this->componentName, 4);
$this['bareComponentName'] = $bareComponent;
// Try to guess the component's namespace
if (empty($this['componentNamespace']))
{
$this->componentNamespace = ucfirst($bareComponent);
}
else
{
$this->componentNamespace = trim($this->componentNamespace, '\\');
}
// Make sure we have front-end and back-end paths
if (empty($this['frontEndPath']))
{
$this->frontEndPath = JPATH_SITE . '/components/' . $this->componentName;
}
if (empty($this['backEndPath']))
{
$this->backEndPath = JPATH_ADMINISTRATOR . '/components/' . $this->componentName;
}
// Get the namespaces for the front-end and back-end parts of the component
$frontEndNamespace = '\\' . $this->componentNamespace . '\\Site\\';
$backEndNamespace = '\\' . $this->componentNamespace . '\\Admin\\';
// Special case: if the frontend and backend paths are identical, we don't use the Site and Admin namespace
// suffixes after $this->componentNamespace (so you may use FOF with WebApplication apps)
if ($this->frontEndPath == $this->backEndPath)
{
$frontEndNamespace = '\\' . $this->componentNamespace . '\\';
$backEndNamespace = '\\' . $this->componentNamespace . '\\';
}
// Do we have to register the component's namespaces with the autoloader?
$autoloader = Autoloader::getInstance();
if (!$autoloader->hasMap($frontEndNamespace))
{
$autoloader->addMap($frontEndNamespace, $this->frontEndPath);
}
if (!$autoloader->hasMap($backEndNamespace))
{
$autoloader->addMap($backEndNamespace, $this->backEndPath);
}
// Inflector service
if (!isset($this['inflector']))
{
$this['inflector'] = function (Container $c) {
return new Inflector();
};
}
// Filesystem abstraction service
if (!isset($this['filesystem']))
{
$this['filesystem'] = function (Container $c) {
return new JoomlaFilesystem($c);
};
}
// Platform abstraction service
if (!isset($this['platform']))
{
if (empty($c['platformClass']))
{
$c['platformClass'] = 'FOF40\\Platform\\Joomla\\Platform';
}
$this['platform'] = function (Container $c) {
$className = $c['platformClass'];
return new $className($c);
};
}
if (empty($this['thisPath']))
{
$this['thisPath'] = $this['frontEndPath'];
if ($this->platform->isBackend())
{
$this['thisPath'] = $this['backEndPath'];
}
}
// MVC Factory service
if (!isset($this['factory']))
{
$this['factory'] = function (Container $c) {
if (empty($c['factoryClass']))
{
$c['factoryClass'] = 'FOF40\\Factory\\BasicFactory';
}
if (strpos($c['factoryClass'], '\\') === false)
{
$class = $c->getNamespacePrefix() . 'Factory\\' . $c['factoryClass'];
$c['factoryClass'] = class_exists($class) ? $class : '\\FOF40\\Factory\\' . ucfirst($c['factoryClass']) . 'Factory';
}
if (!class_exists($c['factoryClass'], true))
{
$c['factoryClass'] = 'FOF40\\Factory\\BasicFactory';
}
$factoryClass = $c['factoryClass'];
/** @var FactoryInterface $factory */
$factory = new $factoryClass($c);
if (isset($c['section']))
{
$factory->setSection($c['section']);
}
return $factory;
};
}
// Component Configuration service
if (!isset($this['appConfig']))
{
$this['appConfig'] = function (Container $c) {
$class = $c->getNamespacePrefix() . 'Configuration\\Configuration';
if (!class_exists($class, true))
{
$class = '\\FOF40\\Configuration\\Configuration';
}
return new $class($c);
};
}
// Component Params service
if (!isset($this['params']))
{
$this['params'] = function (Container $c) {
return new Params($c);
};
}
// Blade view template compiler service
if (!isset($this['blade']))
{
$this['blade'] = function (Container $c) {
return new Blade($c);
};
}
// Database Driver service
if (!isset($this['db']))
{
$this['db'] = function (Container $c) {
return $c->platform->getDbo();
};
}
// Request Dispatcher service
if (!isset($this['dispatcher']))
{
$this['dispatcher'] = function (Container $c) {
return $c->factory->dispatcher();
};
}
// Component toolbar provider
if (!isset($this['toolbar']))
{
$this['toolbar'] = function (Container $c) {
return $c->factory->toolbar();
};
}
// Component toolbar provider
if (!isset($this['transparentAuth']))
{
$this['transparentAuth'] = function (Container $c) {
return $c->factory->transparentAuthentication();
};
}
// View renderer
if (!isset($this['renderer']))
{
$this['renderer'] = function (Container $c) {
if (isset($c['rendererClass']) && class_exists($c['rendererClass']))
{
$class = $c['rendererClass'];
$renderer = new $class($c);
if ($renderer instanceof RenderInterface)
{
return $renderer;
}
}
$filesystem = $c->filesystem;
// Try loading the stock renderers shipped with FOF
$path = __DIR__ . '/../Render/';
$renderFiles = $filesystem->folderFiles($path, '.php');
$renderer = null;
$priority = 0;
foreach ($renderFiles as $filename)
{
if ($filename == 'RenderBase.php')
{
continue;
}
if ($filename == 'RenderInterface.php')
{
continue;
}
$className = 'FOF40\\Render\\' . basename($filename, '.php');
if (!class_exists($className, true))
{
continue;
}
/** @var RenderInterface $o */
$o = new $className($c);
$info = $o->getInformation();
if (($info->enabled ?? []) === [])
{
continue;
}
if ($info->priority > $priority)
{
$priority = $info->priority;
$renderer = $o;
}
}
return $renderer;
};
}
// Input Access service
if (isset($this['input']) && is_array($this['input']))
{
if (empty($this['input']))
{
$this['input'] = [];
}
// This swap is necessary to prevent infinite recursion
$this['rawInputData'] = array_merge($this['input']);
unset($this['input']);
$this['input'] = function (Container $c) {
$input = new FOFInput($c['rawInputData']);
unset($c['rawInputData']);
return $input;
};
}
if (!isset($this['input']))
{
$this['input'] = function () {
return new FOFInput();
};
}
// Session service
if (!isset($this['session']))
{
$this['session'] = function (Container $c) {
return JoomlaFactory::getSession();
};
}
// Template service
if (!isset($this['template']))
{
$this['template'] = function (Container $c) {
return new Template($c);
};
}
// Media version string
if (!isset($this['mediaVersion']))
{
$this['mediaVersion'] = function (Container $c) {
return new MediaVersion($c);
};
if (!is_null($initMediaVersion))
{
$this['mediaVersion']->setMediaVersion($initMediaVersion);
}
}
// Encryption / cryptography service
if (!isset($this['crypto']))
{
$this['crypto'] = function (Container $c) {
return new EncryptService($c);
};
}
}
/**
* Returns a container instance for a specific component. This method goes through fof.xml to read the default
* configuration values for the container. You are advised to use this unless you have a specific reason for
* instantiating a Container without going through the fof.xml file.
*
* Pass the value 'tempInstance' => true in the $values array to get a temporary instance. Otherwise you will get
* the cached instance of the previously created container.
*
* @param string $component The component you want to get a container for, e.g. com_foobar.
* @param array $values Container configuration overrides you want to apply. Optional.
* @param string $section The application section (site, admin) you want to fetch. Any other value results in
* auto-detection.
*
* @return \FOF40\Container\Container
*/
public static function &getInstance($component, array $values = [], $section = 'auto')
{
$tempInstance = false;
if (isset($values['tempInstance']))
{
$tempInstance = $values['tempInstance'];
unset($values['tempInstance']);
}
if ($tempInstance)
{
return self::makeInstance($component, $values, $section);
}
$signature = md5($component . '@' . $section);
if (!isset(self::$instances[$signature]))
{
self::$instances[$signature] = self::makeInstance($component, $values, $section);
}
return self::$instances[$signature];
}
/**
* Returns a temporary container instance for a specific component.
*
* @param string $component The component you want to get a container for, e.g. com_foobar.
* @param array $values Container configuration overrides you want to apply. Optional.
* @param string $section The application section (site, admin) you want to fetch. Any other value results in
* auto-detection.
*
* @return \FOF40\Container\Container
*
* @throws Exception\NoComponent
*/
protected static function &makeInstance($component, array $values = [], $section = 'auto')
{
// Try to auto-detect some defaults
$tmpConfig = array_merge($values, ['componentName' => $component]);
$tmpContainer = new Container($tmpConfig);
if (!in_array($section, ['site', 'admin']))
{
$section = $tmpContainer->platform->isBackend() ? 'admin' : 'site';
}
$appConfig = $tmpContainer->appConfig;
// Get the namespace from fof.xml
$namespace = $appConfig->get('container.componentNamespace', null);
// $values always overrides $namespace and fof.xml
if (isset($values['componentNamespace']))
{
$namespace = $values['componentNamespace'];
}
// If there is no namespace set, try to guess it.
if (empty($namespace))
{
$bareComponent = $component;
if (substr($component, 0, 4) == 'com_')
{
$bareComponent = substr($component, 4);
}
$namespace = ucfirst($bareComponent);
}
// Get the default front-end/back-end paths
$frontEndPath = $appConfig->get('container.frontEndPath', JPATH_SITE . '/components/' . $component);
$backEndPath = $appConfig->get('container.backEndPath', JPATH_ADMINISTRATOR . '/components/' . $component);
// Parse path variables if necessary
$frontEndPath = $tmpContainer->parsePathVariables($frontEndPath);
$backEndPath = $tmpContainer->parsePathVariables($backEndPath);
// Apply path overrides
if (isset($values['frontEndPath']))
{
$frontEndPath = $values['frontEndPath'];
}
if (isset($values['backEndPath']))
{
$backEndPath = $values['backEndPath'];
}
$thisPath = ($section == 'admin') ? $backEndPath : $frontEndPath;
// Get the namespaces for the front-end and back-end parts of the component
$frontEndNamespace = '\\' . $namespace . '\\Site\\';
$backEndNamespace = '\\' . $namespace . '\\Admin\\';
// Special case: if the frontend and backend paths are identical, we don't use the Site and Admin namespace
// suffixes after $this->componentNamespace (so you may use FOF with WebApplication apps)
if ($frontEndPath == $backEndPath)
{
$frontEndNamespace = '\\' . $namespace . '\\';
$backEndNamespace = '\\' . $namespace . '\\';
}
// Do we have to register the component's namespaces with the autoloader?
$autoloader = Autoloader::getInstance();
if (!$autoloader->hasMap($frontEndNamespace))
{
$autoloader->addMap($frontEndNamespace, $frontEndPath);
}
if (!$autoloader->hasMap($backEndNamespace))
{
$autoloader->addMap($backEndNamespace, $backEndPath);
}
// Get the Container class name
$classNamespace = ($section == 'admin') ? $backEndNamespace : $frontEndNamespace;
$class = $classNamespace . 'Container';
// Get the values overrides from fof.xml
$values = array_merge([
'factoryClass' => '\\FOF40\\Factory\\BasicFactory',
'platformClass' => '\\FOF40\\Platform\\Joomla\\Platform',
'section' => $section,
], $values);
$values = array_merge($values, [
'componentName' => $component,
'componentNamespace' => $namespace,
'frontEndPath' => $frontEndPath,
'backEndPath' => $backEndPath,
'thisPath' => $thisPath,
'rendererClass' => $appConfig->get('container.rendererClass', null),
'factoryClass' => $appConfig->get('container.factoryClass', $values['factoryClass']),
'platformClass' => $appConfig->get('container.platformClass', $values['platformClass']),
]);
if (empty($values['rendererClass']))
{
unset ($values['rendererClass']);
}
$mediaVersion = $appConfig->get('container.mediaVersion', null);
unset($appConfig);
unset($tmpConfig);
unset($tmpContainer);
$container = class_exists($class, true) ? new $class($values) : new Container($values);
if (!is_null($mediaVersion))
{
$container->mediaVersion->setMediaVersion($mediaVersion);
}
return $container;
}
/**
* The container SHOULD NEVER be serialised. If this happens, it means that any of the installed version is doing
* something REALLY BAD, so let's die and inform the user of what it's going on.
*/
public function __sleep()
{
// If the site is in debug mode we die and let the user figure it out
if (defined('JDEBUG') && JDEBUG)
{
$msg = <<< END
Something on your site is broken and tries to save the plugin state in the cache. This is a major security issue and
will cause your site to not work properly. Go to your site's backend, Global Configuration and set Caching to OFF as a
temporary solution. Possible causes: older versions of JoomlaShine templates, JomSocial, BetterPreview and other third
party Joomla! extensions.
END;
die($msg);
}
// Otherwise we serialise the Container
return ['values', 'factories', 'protected', 'frozen', 'raw', 'keys'];
}
/**
* Get the applicable namespace prefix for a component section. Possible sections:
* auto Auto-detect which is the current component section
* inverse The inverse area than auto
* site Frontend
* admin Backend
*
* @param string $section The section you want to get information for
*
* @return string The namespace prefix for the component's classes, e.g. \Foobar\Example\Site\
*/
public function getNamespacePrefix(string $section = 'auto'): string
{
// Get the namespaces for the front-end and back-end parts of the component
$frontEndNamespace = '\\' . $this->componentNamespace . '\\Site\\';
$backEndNamespace = '\\' . $this->componentNamespace . '\\Admin\\';
// Special case: if the frontend and backend paths are identical, we don't use the Site and Admin namespace
// suffixes after $this->componentNamespace (so you may use FOF with WebApplication apps)
if ($this->frontEndPath === $this->backEndPath)
{
$frontEndNamespace = '\\' . $this->componentNamespace . '\\';
$backEndNamespace = '\\' . $this->componentNamespace . '\\';
}
switch ($section)
{
default:
case 'auto':
if ($this->platform->isBackend())
{
return $backEndNamespace;
}
else
{
return $frontEndNamespace;
}
break;
case 'inverse':
if ($this->platform->isBackend())
{
return $frontEndNamespace;
}
return $backEndNamespace;
case 'site':
return $frontEndNamespace;
case 'admin':
return $backEndNamespace;
}
}
/**
* Replace the path variables in the $path string.
*
* The recognized variables are:
* * %root% Path to the site root
* * %public% Path to the public area of the site
* * %admin% Path to the administrative area of the site
* * %api% Path to the API application area of the site
* * %tmp% Path to the temp directory
* * %log% Path to the log directory
*
* @param string $path
*
* @return mixed
*/
public function parsePathVariables(string $path)
{
$platformDirs = $this->platform->getPlatformBaseDirs();
// root public admin tmp log
$search = array_map(function ($x) {
return '%' . strtoupper($x) . '%';
}, array_keys($platformDirs));
$replace = array_values($platformDirs);
return str_replace($search, $replace, $path);
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Container;
defined('_JEXEC') || die;
use FOF40\Pimple\Container;
class ContainerBase extends Container
{
/**
* Magic getter for alternative syntax, e.g. $container->foo instead of $container['foo']
*
* @param string $name
*
* @return mixed
*
* @throws \InvalidArgumentException if the identifier is not defined
*/
function __get(string $name)
{
return $this->offsetGet($name);
}
/**
* Magic setter for alternative syntax, e.g. $container->foo instead of $container['foo']
*
* @param string $name The unique identifier for the parameter or object
* @param mixed $value The value of the parameter or a closure for a service
*
* @throws \RuntimeException Prevent override of a frozen service
*/
function __set(string $name, $value)
{
// Special backwards compatible handling for the mediaVersion service
if ($name == 'mediaVersion')
{
$this[$name]->setMediaVersion($value);
return;
}
$this->offsetSet($name, $value);
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Container\Exception;
defined('_JEXEC') || die;
use Exception;
class NoComponent extends \Exception
{
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
{
if (empty($message))
{
$message = 'No component specified building the Container object';
}
if (empty($code))
{
$code = 500;
}
parent::__construct($message, $code, $previous);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Exception;
defined('_JEXEC') || die;
use RuntimeException;
/**
* Exception thrown when we can't get a Controller's name
*/
class CannotGetName extends RuntimeException
{
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Exception;
defined('_JEXEC') || die;
use RuntimeException;
/**
* Exception thrown when we can't find the requested item in a read task
*/
class ItemNotFound extends RuntimeException
{
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
/**
* Exception thrown when the provided Model is locked for writing by another user
*/
class LockedRecord extends RuntimeException
{
public function __construct(string $message = "", int $code = 403, Exception $previous = null)
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_CONTROLLER_ERR_LOCKED');
}
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Exception;
defined('_JEXEC') || die;
use InvalidArgumentException;
/**
* Exception thrown when the provided Model is not a DataModel
*/
class NotADataModel extends InvalidArgumentException
{
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Exception;
defined('_JEXEC') || die;
use InvalidArgumentException;
/**
* Exception thrown when the provided View does not implement DataViewInterface
*/
class NotADataView extends InvalidArgumentException
{
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Exception;
defined('_JEXEC') || die;
use InvalidArgumentException;
/**
* Exception thrown when we can't find a suitable method to handle the requested task
*/
class TaskNotFound extends InvalidArgumentException
{
}

View File

@ -0,0 +1,82 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Controller\Mixin;
defined('_JEXEC') || die;
use FOF40\Controller\Controller;
/**
* Force a Controller to allow access to specific tasks only, no matter which tasks are already defined in this
* Controller.
*
* Include this Trait and then in your constructor do this:
* $this->setPredefinedTaskList(['atask', 'anothertask', 'something']);
*
* WARNING: If you override execute() you will need to copy the logic from this trait's execute() method.
*/
trait PredefinedTaskList
{
/**
* A list of predefined tasks. Trying to access any other task will result in the first task of this list being
* executed instead.
*
* @var array
*/
protected $predefinedTaskList = [];
/**
* Overrides the execute method to implement the predefined task list feature
*
* @param string $task The task to execute
*
* @return mixed The controller task result
*/
public function execute(string $task)
{
if (!in_array($task, $this->predefinedTaskList))
{
$task = reset($this->predefinedTaskList);
}
return parent::execute($task);
}
/**
* Sets the predefined task list and registers the first task in the list as the Controller's default task
*
* @param array $taskList The task list to register
*
* @return void
*/
public function setPredefinedTaskList(array $taskList): void
{
/** @var Controller $this */
// First, unregister all known tasks which are not in the taskList
$allTasks = $this->getTasks();
foreach ($allTasks as $task)
{
if (in_array($task, $taskList))
{
continue;
}
$this->unregisterTask($task);
}
// Set the predefined task list
$this->predefinedTaskList = $taskList;
// Set the default task
$this->registerDefaultTask(reset($this->predefinedTaskList));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,472 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Date;
defined('_JEXEC') || die;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
use JDatabaseDriver;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Language\Text;
/**
* The Date class is a fork of Joomla's Date. We had to fork that code in April 2017 when Joomla! 7.0 was released with
* an untested change that completely broke date handling on PHP 7.0 and earlier versions. Since we can no longer trust
* Joomla's core date and time handling we are providing our own, stable code.
*
* Date is a class that stores a date and provides logic to manipulate and render that date in a variety of formats.
*
* @method Date|bool add(DateInterval $interval) Adds an amount of days, months, years, hours, minutes and seconds
* to a Date object.
* @method Date|bool sub(DateInterval $interval) Subtracts an amount of days, months, years, hours, minutes and
* seconds from a Date object.
* @method Date|bool modify($modify) Alter the timestamp of this object by incre-/decrementing in a
* format accepted by strtotime().
*
* @property-read string $daysinmonth t - Number of days in the given month.
* @property-read string $dayofweek N - ISO-8601 numeric representation of the day of the week.
* @property-read string $dayofyear z - The day of the year (starting from 0).
* @property-read boolean $isleapyear L - Whether it's a leap year.
* @property-read string $day d - Day of the month, 2 digits with leading zeros.
* @property-read string $hour H - 24-hour format of an hour with leading zeros.
* @property-read string $minute i - Minutes with leading zeros.
* @property-read string $second s - Seconds with leading zeros.
* @property-read string $microsecond u - Microseconds with leading zeros.
* @property-read string $month m - Numeric representation of a month, with leading zeros.
* @property-read string $ordinal S - English ordinal suffix for the day of the month, 2 characters.
* @property-read string $week W - ISO-8601 week number of year, weeks starting on Monday.
* @property-read string $year Y - A full numeric representation of a year, 4 digits.
*/
class Date extends DateTime
{
public const DAY_ABBR = "\x021\x03";
public const DAY_NAME = "\x022\x03";
public const MONTH_ABBR = "\x023\x03";
public const MONTH_NAME = "\x024\x03";
/**
* The format string to be applied when using the __toString() magic method.
*
* @var string
*/
public static $format = 'Y-m-d H:i:s';
/**
* Placeholder for a DateTimeZone object with GMT as the time zone.
*
* @var object
*/
protected static $gmt;
/**
* Placeholder for a DateTimeZone object with the default server
* time zone as the time zone.
*
* @var object
*/
protected static $stz;
/**
* The DateTimeZone object for usage in rending dates as strings.
*
* @var DateTimeZone
*/
protected $tz;
/**
* Constructor.
*
* @param string|null $date String in a format accepted by strtotime(), defaults to "now".
* @param string|DateTimeZone $tz Time zone to be used for the date. Might be a string or a DateTimeZone
* object.
*
* @throws Exception
*/
public function __construct(?string $date = 'now', $tz = null)
{
// Create the base GMT and server time zone objects.
if (empty(self::$gmt) || empty(self::$stz))
{
self::$gmt = new DateTimeZone('GMT');
self::$stz = new DateTimeZone(@date_default_timezone_get());
}
// If the time zone object is not set, attempt to build it.
if (!($tz instanceof DateTimeZone))
{
if ($tz === null)
{
$tz = self::$gmt;
}
elseif (is_string($tz))
{
$tz = new DateTimeZone($tz);
}
}
// On PHP 7.1 and later use an integer timestamp, without microseconds, to preserve backwards compatibility.
// See http://php.net/manual/en/migration71.incompatible.php#migration71.incompatible.datetime-microseconds
if (($date === 'now') || empty($date))
{
$date = time();
}
// If the date is numeric assume a unix timestamp and convert it.
date_default_timezone_set('UTC');
$date = is_numeric($date) ? date('c', $date) : $date;
// Call the DateTime constructor.
parent::__construct($date, $tz);
// Reset the timezone for 3rd party libraries/extension that does not use Date
date_default_timezone_set(self::$stz->getName());
// Set the timezone object for access later.
$this->tz = $tz;
}
/**
* Proxy for new Date().
*
* @param string $date String in a format accepted by strtotime(), defaults to "now".
* @param string|DateTimeZone $tz Time zone to be used for the date.
*
* @return Date
* @throws Exception
*/
public static function getInstance(string $date = 'now', $tz = null)
{
return new Date($date, $tz);
}
/**
* Magic method to access properties of the date given by class to the format method.
*
* @param string $name The name of the property.
*
* @return mixed A value if the property name is valid, null otherwise.
*/
public function __get(string $name)
{
$value = null;
switch ($name)
{
case 'daysinmonth':
$value = $this->format('t', true);
break;
case 'dayofweek':
$value = $this->format('N', true);
break;
case 'dayofyear':
$value = $this->format('z', true);
break;
case 'isleapyear':
$value = (boolean) $this->format('L', true);
break;
case 'day':
$value = $this->format('d', true);
break;
case 'hour':
$value = $this->format('H', true);
break;
case 'minute':
$value = $this->format('i', true);
break;
case 'second':
$value = $this->format('s', true);
break;
case 'month':
$value = $this->format('m', true);
break;
case 'ordinal':
$value = $this->format('S', true);
break;
case 'week':
$value = $this->format('W', true);
break;
case 'year':
$value = $this->format('Y', true);
break;
default:
$trace = debug_backtrace();
trigger_error(
'Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'],
E_USER_NOTICE
);
}
return $value;
}
/**
* Magic method to render the date object in the format specified in the public
* static member Date::$format.
*
* @return string The date as a formatted string.
*/
public function __toString(): string
{
return (string) parent::format(self::$format);
}
/**
* Translates day of week number to a string.
*
* @param integer $day The numeric day of the week.
* @param boolean $abbr Return the abbreviated day string?
*
* @return string The day of the week.
*/
public function dayToString(int $day, bool $abbr = false): string
{
switch ($day)
{
case 0:
default:
return $abbr ? Text::_('SUN') : Text::_('SUNDAY');
case 1:
return $abbr ? Text::_('MON') : Text::_('MONDAY');
case 2:
return $abbr ? Text::_('TUE') : Text::_('TUESDAY');
case 3:
return $abbr ? Text::_('WED') : Text::_('WEDNESDAY');
case 4:
return $abbr ? Text::_('THU') : Text::_('THURSDAY');
case 5:
return $abbr ? Text::_('FRI') : Text::_('FRIDAY');
case 6:
return $abbr ? Text::_('SAT') : Text::_('SATURDAY');
}
}
/**
* Gets the date as a formatted string in a local calendar.
*
* @param string $format The date format specification string (see {@link PHP_MANUAL#date})
* @param boolean $local True to return the date string in the local time zone, false to return it in GMT.
* @param boolean $translate True to translate localised strings
*
* @return string The date string in the specified format format.
*/
public function calendar(string $format, bool $local = false, bool $translate = true): string
{
return $this->format($format, $local, $translate);
}
/**
* Gets the date as a formatted string.
*
* @param string $format The date format specification string (see {@link PHP_MANUAL#date})
* @param boolean $local True to return the date string in the local time zone, false to return it in GMT.
* @param boolean $translate True to translate localised strings
*
* @return string The date string in the specified format format.
*/
#[\ReturnTypeWillChange]
public function format($format, bool $local = false, bool $translate = true): string
{
if ($translate)
{
// Do string replacements for date format options that can be translated.
$format = preg_replace('/(^|[^\\\])D/', "\\1" . self::DAY_ABBR, $format);
$format = preg_replace('/(^|[^\\\])l/', "\\1" . self::DAY_NAME, $format);
$format = preg_replace('/(^|[^\\\])M/', "\\1" . self::MONTH_ABBR, $format);
$format = preg_replace('/(^|[^\\\])F/', "\\1" . self::MONTH_NAME, $format);
}
// If the returned time should not be local use GMT.
if ($local == false && !empty(self::$gmt))
{
parent::setTimezone(self::$gmt);
}
// Format the date.
$return = parent::format($format);
if ($translate)
{
// Manually modify the month and day strings in the formatted time.
if (strpos($return, self::DAY_ABBR) !== false)
{
$return = str_replace(self::DAY_ABBR, $this->dayToString(parent::format('w'), true), $return);
}
if (strpos($return, self::DAY_NAME) !== false)
{
$return = str_replace(self::DAY_NAME, $this->dayToString(parent::format('w')), $return);
}
if (strpos($return, self::MONTH_ABBR) !== false)
{
$return = str_replace(self::MONTH_ABBR, $this->monthToString(parent::format('n'), true), $return);
}
if (strpos($return, self::MONTH_NAME) !== false)
{
$return = str_replace(self::MONTH_NAME, $this->monthToString(parent::format('n')), $return);
}
}
if ($local == false && !empty($this->tz))
{
parent::setTimezone($this->tz);
}
return $return;
}
/**
* Get the time offset from GMT in hours or seconds.
*
* @param boolean $hours True to return the value in hours.
*
* @return float The time offset from GMT either in hours or in seconds.
*/
public function getOffsetFromGmt(bool $hours = false): float
{
return (float) $hours ? ($this->tz->getOffset($this) / 3600) : $this->tz->getOffset($this);
}
/**
* Translates month number to a string.
*
* @param integer $month The numeric month of the year.
* @param boolean $abbr If true, return the abbreviated month string
*
* @return string The month of the year.
*/
public function monthToString(int $month, bool $abbr = false)
{
switch ($month)
{
case 1:
default:
return $abbr ? Text::_('JANUARY_SHORT') : Text::_('JANUARY');
case 2:
return $abbr ? Text::_('FEBRUARY_SHORT') : Text::_('FEBRUARY');
case 3:
return $abbr ? Text::_('MARCH_SHORT') : Text::_('MARCH');
case 4:
return $abbr ? Text::_('APRIL_SHORT') : Text::_('APRIL');
case 5:
return $abbr ? Text::_('MAY_SHORT') : Text::_('MAY');
case 6:
return $abbr ? Text::_('JUNE_SHORT') : Text::_('JUNE');
case 7:
return $abbr ? Text::_('JULY_SHORT') : Text::_('JULY');
case 8:
return $abbr ? Text::_('AUGUST_SHORT') : Text::_('AUGUST');
case 9:
return $abbr ? Text::_('SEPTEMBER_SHORT') : Text::_('SEPTEMBER');
case 10:
return $abbr ? Text::_('OCTOBER_SHORT') : Text::_('OCTOBER');
case 11:
return $abbr ? Text::_('NOVEMBER_SHORT') : Text::_('NOVEMBER');
case 12:
return $abbr ? Text::_('DECEMBER_SHORT') : Text::_('DECEMBER');
}
}
/**
* Method to wrap the setTimezone() function and set the internal time zone object.
*
* @param DateTimeZone $tz The new DateTimeZone object.
*
* @return Date
*
* @note This method can't be type hinted due to a PHP bug: https://bugs.php.net/bug.php?id=61483
*/
public function setTimezone($tz): self
{
$this->tz = $tz;
date_timezone_set($this, $tz);
return $this;
}
/**
* Gets the date as an ISO 8601 string. IETF RFC 3339 defines the ISO 8601 format
* and it can be found at the IETF Web site.
*
* @param boolean $local True to return the date string in the local time zone, false to return it in GMT.
*
* @return string The date string in ISO 8601 format.
*
* @link http://www.ietf.org/rfc/rfc3339.txt
*/
public function toISO8601(bool $local = false): string
{
return $this->format(DateTime::RFC3339, $local, false);
}
/**
* Gets the date as an SQL datetime string.
*
* @param boolean $local True to return the date string in the local time zone, false to return it in
* GMT.
* @param JDatabaseDriver $db The database driver or null to use Factory::getDbo()
*
* @return string The date string in SQL datetime format.
*
* @link http://dev.mysql.com/doc/refman/5.0/en/datetime.html
*/
public function toSql(bool $local = false, JDatabaseDriver $db = null): string
{
if ($db === null)
{
$db = JoomlaFactory::getDbo();
}
return $this->format($db->getDateFormat(), $local, false);
}
/**
* Gets the date as an RFC 822 string. IETF RFC 2822 supersedes RFC 822 and its definition
* can be found at the IETF Web site.
*
* @param boolean $local True to return the date string in the local time zone, false to return it in GMT.
*
* @return string The date string in RFC 822 format.
*
* @link http://www.ietf.org/rfc/rfc2822.txt
*/
public function toRFC822(bool $local = false): string
{
return $this->format(DateTime::RFC2822, $local, false);
}
/**
* Gets the date as UNIX time stamp.
*
* @return integer The date as a UNIX timestamp.
*/
public function toUnix(): int
{
return (int) parent::format('U');
}
}

View File

@ -0,0 +1,154 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Date;
defined('_JEXEC') || die;
use DateTime;
use DateTimeZone;
use JDatabaseDriver;
/**
* This decorator will get any DateTime descendant and turn it into a FOF40\Date\Date compatible class. If the methods
* specific to Date are available they will be used. Otherwise a new Date object will be spun from the information
* in the decorated DateTime object and the results of a call to its method will be returned.
*/
class DateDecorator extends Date
{
/**
* The decorated object
*
* @param string $date String in a format accepted by strtotime(), defaults to "now".
* @param string|DateTimeZone $tz Time zone to be used for the date. Might be a string or a DateTimeZone
* object.
*
* @var DateTime
*/
protected $decorated;
public function __construct(string $date = 'now', $tz = null)
{
$this->decorated = (is_object($date) && ($date instanceof DateTime)) ? $date : new Date($date, $tz);
$timestamp = $this->decorated->toISO8601(true);
parent::__construct($timestamp);
$this->setTimezone($this->decorated->getTimezone());
}
public static function getInstance(string $date = 'now', $tz = null): self
{
$coreObject = new Date($date, $tz);
return new DateDecorator($coreObject);
}
/**
* Magic method to access properties of the date given by class to the format method.
*
* @param string $name The name of the property.
*
* @return mixed A value if the property name is valid, null otherwise.
*/
public function __get(string $name)
{
return $this->decorated->$name;
}
// Note to self: ignore phpStorm; we must NOT use a typehint for $interval
public function __call(string $name, array $arguments = [])
{
if (method_exists($this->decorated, $name))
{
return call_user_func_array([$this->decorated, $name], $arguments);
}
throw new \InvalidArgumentException("Date object does not have a $name method");
}
// Note to self: ignore phpStorm; we must NOT use a typehint for $interval
public function sub($interval)
{
// Note to self: ignore phpStorm; we must NOT use a typehint for $interval
return $this->decorated->sub($interval);
}
public function add($interval)
{
// Note to self: ignore phpStorm; we must NOT use a typehint for $interval
return $this->decorated->add($interval);
}
public function modify($modify)
{
return $this->decorated->modify($modify);
}
public function __toString(): string
{
return (string) $this->decorated;
}
public function dayToString(int $day, bool $abbr = false): string
{
return $this->decorated->dayToString($day, $abbr);
}
public function calendar(string $format, bool $local = false, bool $translate = true): string
{
return $this->decorated->calendar($format, $local, $translate);
}
public function format($format, bool $local = false, bool $translate = true): string
{
if (($this->decorated instanceof Date) || ($this->decorated instanceof \Joomla\CMS\Date\Date))
{
return $this->decorated->format($format, $local, $translate);
}
return $this->decorated->format($format);
}
public function getOffsetFromGmt(bool $hours = false): float
{
return $this->decorated->getOffsetFromGMT($hours);
}
public function monthToString(int $month, bool $abbr = false)
{
return $this->decorated->monthToString($month, $abbr);
}
public function setTimezone($tz): Date
{
return $this->decorated->setTimezone($tz);
}
public function toISO8601(bool $local = false): string
{
return $this->decorated->toISO8601($local);
}
public function toSql(bool $local = false, JDatabaseDriver $db = null): string
{
return $this->decorated->toSql($local, $db);
}
public function toRFC822(bool $local = false): string
{
return $this->decorated->toRFC822($local);
}
public function toUnix(): int
{
return $this->decorated->toUnix();
}
}

View File

@ -0,0 +1,348 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Date;
defined('_JEXEC') || die;
use DateTime;
use DateTimeZone;
use Exception;
use FOF40\Container\Container;
use Joomla\CMS\User\User;
/**
* A helper class to wrangle timezones, as used by Joomla!.
*
* @package FOF40\Utils
*
* @since 3.1.3
*/
class TimezoneWrangler
{
/**
* The default timestamp format string to use when one is not provided
*
* @var string
*/
protected $defaultFormat = 'Y-m-d H:i:s T';
/**
* When set, this timezone will be used instead of the Joomla! applicable timezone for the user.
*
* @var DateTimeZone
*/
protected $forcedTimezone;
/**
* Cache of user IDs to applicable timezones
*
* @var array
*/
protected $userToTimezone = [];
/**
* The component container for which we are created
*
* @var Container
*/
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Get the default timestamp format to use when one is not provided
*
* @return string
*/
public function getDefaultFormat(): string
{
return $this->defaultFormat;
}
/**
* Set the default timestamp format to use when one is not provided
*
* @param string $defaultFormat
*
* @return void
*/
public function setDefaultFormat(string $defaultFormat): void
{
$this->defaultFormat = $defaultFormat;
}
/**
* Returns the forced timezone which is used instead of the applicable Joomla! timezone.
*
* @return DateTimeZone
*/
public function getForcedTimezone(): DateTimeZone
{
return $this->forcedTimezone;
}
/**
* Sets the forced timezone which is used instead of the applicable Joomla! timezone. If the new timezone is
* different than the existing one we will also reset the user to timezone cache.
*
* @param DateTimeZone|string $forcedTimezone
*
* @return void
*/
public function setForcedTimezone($forcedTimezone): void
{
// Are we unsetting the forced TZ?
if (empty($forcedTimezone))
{
$this->forcedTimezone = null;
$this->resetCache();
return;
}
// If the new TZ is a string we have to create an object
if (is_string($forcedTimezone))
{
$forcedTimezone = new DateTimeZone($forcedTimezone);
}
$oldTZ = '';
if (is_object($this->forcedTimezone) && ($this->forcedTimezone instanceof DateTimeZone))
{
$oldTZ = $this->forcedTimezone->getName();
}
if ($oldTZ === $forcedTimezone->getName())
{
return;
}
$this->forcedTimezone = $forcedTimezone;
$this->resetCache();
}
/**
* Reset the user to timezone cache. This is done automatically every time you change the forced timezone.
*/
public function resetCache(): void
{
$this->userToTimezone = [];
}
/**
* Get the applicable timezone for a user. If the user is not a guest and they have a timezone set up in their
* profile it will be used. Otherwise we fall back to the Server Timezone as set up in Global Configuration. If that
* fails, we use GMT. However, if you have used a non-blank forced timezone that will be used instead, circumventing
* this calculation. Therefore the returned timezone is one of the following, by descending order of priority:
* - Forced timezone
* - User's timezone (explicitly set in their user profile)
* - Server Timezone (from Joomla's Global Configuration)
* - GMT
*
* @param User|null $user
*
* @return DateTimeZone
*/
public function getApplicableTimezone(?User $user = null): DateTimeZone
{
// If we have a forced timezone use it instead of trying to figure anything out.
if (is_object($this->forcedTimezone))
{
return $this->forcedTimezone;
}
// No user? Get the current user.
if (is_null($user))
{
$user = $this->container->platform->getUser();
}
// If there is a cached timezone return that instead.
if (isset($this->userToTimezone[$user->id]))
{
return $this->userToTimezone[$user->id];
}
// Prefer the user timezone if it's set.
if (!$user->guest)
{
$tz = $user->getParam('timezone', null);
if (!empty($tz))
{
try
{
$this->userToTimezone[$user->id] = new DateTimeZone($tz);
return $this->userToTimezone[$user->id];
}
catch (Exception $e)
{
}
}
}
// Get the Server Timezone from Global Configuration with a fallback to GMT
$tz = $this->container->platform->getConfig()->get('offset', 'GMT');
try
{
$this->userToTimezone[$user->id] = new DateTimeZone($tz);
}
catch (Exception $e)
{
// If an invalid timezone was set we get to use GMT
$this->userToTimezone[$user->id] = new DateTimeZone('GMT');
}
return $this->userToTimezone[$user->id];
}
/**
* Returns a FOF Date object with its timezone set to the user's applicable timezone.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and Joomla Date), an integer (UNIX timestamp) or a date string.
* If no timezone is specified in a date string we assume it's GMT.
*
* @param User $user Applicable user for timezone calculation. Null = current user.
* @param mixed $time Source time. Leave blank for current date/time.
*
* @return Date
*/
public function getLocalDateTime(?User $user, $time = null): Date
{
$time = empty($time) ? 'now' : $time;
$date = new Date($time);
$tz = $this->getApplicableTimezone($user);
$date->setTimezone($tz);
return $date;
}
/**
* Returns a FOF Date object with its timezone set to GMT.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and Joomla Date), an integer (UNIX timestamp) or a date string.
* If no timezone is specified in a date string we assume it's the user's applicable timezone.
*
* @param User $user
* @param mixed $time
*
* @return Date
*/
public function getGMTDateTime(?User $user, $time): Date
{
$time = empty($time) ? 'now' : $time;
$tz = $this->getApplicableTimezone($user);
$date = new Date($time, $tz);
$gmtTimezone = new DateTimeZone('GMT');
$date->setTimezone($gmtTimezone);
return $date;
}
/**
* Returns a formatted date string in the user's applicable timezone.
*
* If no format is specified we will use $defaultFormat.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and Joomla Date), an integer (UNIX timestamp) or a date string.
* If no timezone is specified in a date string we assume it's GMT.
*
* $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). CMSApplication does that
* for you automatically. If you're under CLI, a custom WebApplication etc you will probably have to load this file
* manually.
*
* @param string|null $format Timestamp format. If empty $defaultFormat is used.
* @param User|null $user Applicable user for timezone calculation. Null = current
* user.
* @param DateTime|Date|string|int|null $time Source time. Leave blank for current date/time.
* @param bool $translate Translate day of week and month names?
*
* @return string
*/
public function getLocalTimeStamp(?string $format = null, ?User $user = null, $time = null, bool $translate = false): string
{
$date = $this->getLocalDateTime($user, $time);
$format = empty($format) ? $this->defaultFormat : $format;
return $date->format($format, true, $translate);
}
/**
* Returns a formatted date string in the GMT timezone.
*
* If no format is specified we will use $defaultFormat.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and Joomla Date), an integer (UNIX timestamp) or a date string.
* If no timezone is specified in a date string we assume it's the user's applicable timezone.
*
* $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). CMSApplication does that
* for you automatically. If you're under CLI, a custom WebApplication etc you will probably have to load this file
* manually.
*
* @param string|null $format Timestamp format. If empty $defaultFormat is used.
* @param User|null $user Applicable user for timezone calculation. Null = current
* user.
* @param DateTime|Date|string|int|null $time Source time. Leave blank for current date/time.
* @param bool $translate Translate day of week and month names?
*
* @return string
*/
public function getGMTTimeStamp(?string $format = null, ?User $user = null, $time = null, bool $translate = false): string
{
$date = $this->localToGMT($user, $time);
$format = empty($format) ? $this->defaultFormat : $format;
return $date->format($format, true, $translate);
}
/**
* Convert a local time back to GMT. Returns a Date object with its timezone set to GMT.
*
* This is an alias to getGMTDateTime
*
* @param string|Date $time
* @param User|null $user
*
* @return Date
*/
public function localToGMT($time, ?User $user = null): Date
{
return $this->getGMTDateTime($user, $time);
}
/**
* Convert a GMT time to local timezone. Returns a Date object with its timezone set to the applicable user's TZ.
*
* This is an alias to getLocalDateTime
*
* @param string|Date $time
* @param User|null $user
*
* @return Date
*/
public function GMTToLocal($time, ?User $user = null): Date
{
return $this->getLocalDateTime($user, $time);
}
}

View File

@ -0,0 +1,348 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Dispatcher;
defined('_JEXEC') || die;
use Exception;
use FOF40\Container\Container;
use FOF40\Controller\Controller;
use FOF40\Dispatcher\Exception\AccessForbidden;
use FOF40\TransparentAuthentication\TransparentAuthentication;
/**
* A generic MVC dispatcher
*
* @property-read \FOF40\Input\Input $input The input object (magic __get returns the Input from the Container)
*/
class Dispatcher
{
/** @var string The name of the default view, in case none is specified */
public $defaultView;
/** @var array Local cache of the dispatcher configuration */
protected $config = [];
/** @var Container The container we belong to */
protected $container;
/** @var string The view which will be rendered by the dispatcher */
protected $view;
/** @var string The layout for rendering the view */
protected $layout;
/** @var Controller The controller which will be used */
protected $controller;
/** @var bool Is this user transparently logged in? */
protected $isTransparentlyLoggedIn = false;
/**
* Public constructor
*
* The $config array can contain the following optional values:
* defaultView string The view to render if none is specified in $input
*
* Do note that $config is passed to the Controller and through it to the Model and View. Please see these classes
* for more information on the configuration variables they accept.
*
* @param Container $container
* @param array $config
*/
public function __construct(Container $container, array $config = [])
{
$this->container = $container;
$this->config = $config;
$this->defaultView = $container->appConfig->get('dispatcher.defaultView', $this->defaultView);
if (isset($config['defaultView']))
{
$this->defaultView = $config['defaultView'];
}
$this->supportCustomViewAndTaskParameters();
// Get the default values for the view and layout names
$this->view = $this->input->getCmd('view', null);
$this->layout = $this->input->getCmd('layout', null);
// Not redundant; you may pass an empty but non-null view which is invalid, so we need the fallback
if (empty($this->view))
{
$this->view = $this->defaultView;
$this->container->input->set('view', $this->view);
}
}
/**
* Magic get method. Handles magic properties:
* $this->input mapped to $this->container->input
*
* @param string $name The property to fetch
*
* @return mixed|null
*/
public function __get(string $name)
{
// Handle $this->input
if ($name == 'input')
{
return $this->container->input;
}
// Property not found; raise error
$trace = debug_backtrace();
trigger_error(
'Undefined property via __get(): ' . $name .
' in ' . $trace[0]['file'] .
' on line ' . $trace[0]['line'],
E_USER_NOTICE);
return null;
}
/**
* The main code of the Dispatcher. It spawns the necessary controller and
* runs it.
*
* @return void
*
* @throws AccessForbidden When the access is forbidden
* @throws Exception For displaying an error page
*/
public function dispatch(): void
{
// Load the translations for this component;
$this->container->platform->loadTranslations($this->container->componentName);
// Perform transparent authentication
if ($this->container->platform->getUser()->guest)
{
$this->transparentAuthenticationLogin();
}
// Get the event names (different for CLI)
$onBeforeEventName = 'onBeforeDispatch';
$onAfterEventName = 'onAfterDispatch';
if ($this->container->platform->isCli())
{
$onBeforeEventName = 'onBeforeDispatchCLI';
$onAfterEventName = 'onAfterDispatchCLI';
}
try
{
$result = $this->triggerEvent($onBeforeEventName);
$error = '';
}
catch (\Exception $e)
{
$result = false;
$error = $e->getMessage();
}
if ($result === false)
{
if ($this->container->platform->isCli())
{
$this->container->platform->setHeader('Status', '403 Forbidden', true);
}
$this->transparentAuthenticationLogout();
$this->container->platform->showErrorPage(new AccessForbidden);
}
// Get and execute the controller
$view = $this->input->getCmd('view', $this->defaultView);
$task = $this->input->getCmd('task', 'default');
if (empty($task))
{
$task = 'default';
$this->input->set('task', $task);
}
try
{
$this->controller = $this->container->factory->controller($view, $this->config);
$status = $this->controller->execute($task);
}
catch (Exception $e)
{
$this->container->platform->showErrorPage($e);
// Redundant; just to make code sniffers happy
return;
}
if ($status !== false)
{
try
{
$this->triggerEvent($onAfterEventName);
}
catch (\Exception $e)
{
$status = false;
}
}
if ($status === false)
{
if ($this->container->platform->isCli())
{
$this->container->platform->setHeader('Status', '403 Forbidden', true);
}
$this->transparentAuthenticationLogout();
$this->container->platform->showErrorPage(new AccessForbidden);
}
$this->transparentAuthenticationLogout();
$this->controller->redirect();
}
/**
* Returns a reference to the Controller object currently in use by the dispatcher
*
* @return Controller|null
*/
public function &getController(): ?Controller
{
return $this->controller;
}
/**
* Triggers an object-specific event. The event runs both locally if a suitable method exists and through the
* Joomla! plugin system. A true/false return value is expected. The first false return cancels the event.
*
* EXAMPLE
* Component: com_foobar, Object name: item, Event: onBeforeDispatch, Arguments: array(123, 456)
* The event calls:
* 1. $this->onBeforeDispatch(123, 456)
* 2. Joomla! plugin event onComFoobarDispatcherBeforeDispatch($this, 123, 456)
*
* @param string $event The name of the event, typically named onPredicateVerb e.g. onBeforeKick
* @param array $arguments The arguments to pass to the event handlers
*
* @return bool
*/
protected function triggerEvent(string $event, array $arguments = []): bool
{
$result = true;
// If there is an object method for this event, call it
if (method_exists($this, $event))
{
$result = $this->{$event}(...$arguments);
}
if ($result === false)
{
return false;
}
// All other event handlers live outside this object, therefore they need to be passed a reference to this
// objects as the first argument.
array_unshift($arguments, $this);
// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
$prefix = '';
if (substr($event, 0, 2) == 'on')
{
$prefix = 'on';
$event = substr($event, 2);
}
// Get the component/model prefix for the event
$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Dispatcher';
// The event name will be something like onComFoobarItemsBeforeSomething
$event = $prefix . $event;
// Call the Joomla! plugins
$results = $this->container->platform->runPlugins($event, $arguments);
return !in_array(false, $results, true);
}
/**
* Handles the transparent authentication log in
*/
protected function transparentAuthenticationLogin(): void
{
/** @var TransparentAuthentication $transparentAuth */
$transparentAuth = $this->container->transparentAuth;
$authInfo = $transparentAuth->getTransparentAuthenticationCredentials();
if (empty($authInfo))
{
return;
}
$this->isTransparentlyLoggedIn = $this->container->platform->loginUser($authInfo);
}
/**
* Handles the transparent authentication log out
*/
protected function transparentAuthenticationLogout(): void
{
if (!$this->isTransparentlyLoggedIn)
{
return;
}
/** @var TransparentAuthentication $transparentAuth */
$transparentAuth = $this->container->transparentAuth;
if (!$transparentAuth->getLogoutOnExit())
{
return;
}
$this->container->platform->logoutUser();
}
/**
* Adds support for akview/aktask in lieu of view and task.
*
* This is for future-proofing FOF in case Joomla assigns special meaning to view and task, e.g. by trying to find a
* specific controller / task class instead of letting the component's front-end router handle it. If that happens
* FOF components can have a single Joomla-compatible view/task which launches the Dispatcher and perform internal
* routing using akview/aktask.
*
* @return void
* @since 3.6.3
*/
private function supportCustomViewAndTaskParameters()
{
$view = $this->input->getCmd('akview', null);
$task = $this->input->getCmd('aktask', null);
if (!is_null($view))
{
$this->input->remove('akview');
$this->input->set('view', $view);
}
if (!is_null($task))
{
$this->input->remove('aktask');
$this->input->set('task', $task);
}
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Dispatcher\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
/**
* Exception thrown when the access to the requested resource is forbidden under the current execution context
*/
class AccessForbidden extends RuntimeException
{
public function __construct(string $message = "", int $code = 403, Exception $previous = null)
{
if (empty($message))
{
$message = Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN');
}
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,83 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Dispatcher\Mixin;
defined('_JEXEC') || die;
// Protect from unauthorized access
use FOF40\Dispatcher\Dispatcher;
use Joomla\CMS\Uri\Uri;
/**
* Lets you create view aliases. When you access a view alias the real view is loaded instead. You can optionally have
* an HTTPS 301 redirection for GET requests to URLs that use the view name alias.
*
* IMPORTANT: This is a mixin (or, as we call it in PHP, a trait). Traits require PHP 5.4 or later. If you opt to use
* this trait your component will no longer work under PHP 5.3.
*
* Usage:
*
* • Override $viewNameAliases with your view names map.
* • If you want to issue HTTP 301 for GET requests set $permanentAliasRedirectionOnGET to true.
* • If you have an onBeforeDispatch method remember to alias and call this traits' onBeforeDispatch method at the top.
*
* Regarding the last point, if you've never used traits before, the code looks like this. Top of the class:
* use ViewAliases {
* onBeforeDispatch as onBeforeDispatchViewAliases;
* }
* and inside your custom onBeforeDispatch method, the first statement should be:
* $this->onBeforeDispatchViewAliases();
* Simple!
*/
trait ViewAliases
{
/**
* Maps view name aliases to actual views. The format is 'alias' => 'RealView'.
*
* @var array
*/
protected $viewNameAliases = [];
/**
* If set to true, any GET request to the alias view will result in an HTTP 301 permanent redirection to the real
* view name.
*
* This does NOT apply to POST, PUT, DELETE etc URLs. When you submit form data you cannot have a redirection. The
* browser will _typically_ not resend the submitted data.
*
* @var bool
*/
protected $permanentAliasRedirectionOnGET = false;
/**
* Transparently replaces old view names with their counterparts.
*
* If you are overriding this method in your component remember to alias it and call it from your overridden method.
*/
protected function onBeforeDispatch(): void
{
if (!array_key_exists($this->view, $this->viewNameAliases))
{
return;
}
$this->view = $this->viewNameAliases[$this->view];
$this->container->input->set('view', $this->view);
// Perform HTTP 301 Moved permanently redirection on GET requests if requested to do so
if ($this->permanentAliasRedirectionOnGET && isset($_SERVER['REQUEST_METHOD'])
&& (strtoupper($_SERVER['REQUEST_METHOD']) == 'GET')
)
{
$url = Uri::getInstance();
$url->setVar('view', $this->view);
$this->container->platform->redirect($url, 301);
}
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Download\Adapter;
defined('_JEXEC') || die;
use FOF40\Download\DownloadInterface;
use FOF40\Download\Exception\DownloadError;
abstract class AbstractAdapter implements DownloadInterface
{
/**
* Load order priority
*
* @var int
*/
public $priority = 100;
/**
* Name of the adapter (identical to filename)
*
* @var string
*/
public $name = '';
/**
* Is this adapter supported in the current execution environment?
*
* @var bool
*/
public $isSupported = false;
/**
* Does this adapter support chunked downloads?
*
* @var bool
*/
public $supportsChunkDownload = false;
/**
* Does this adapter support querying the remote file's size?
*
* @var bool
*/
public $supportsFileSize = false;
/**
* Does this download adapter support downloading files in chunks?
*
* @return boolean True if chunk download is supported
*/
public function supportsChunkDownload(): bool
{
return $this->supportsChunkDownload;
}
/**
* Does this download adapter support reading the size of a remote file?
*
* @return boolean True if remote file size determination is supported
*/
public function supportsFileSize(): bool
{
return $this->supportsFileSize;
}
/**
* Is this download class supported in the current server environment?
*
* @return boolean True if this server environment supports this download class
*/
public function isSupported(): bool
{
return $this->isSupported;
}
/**
* Get the priority of this adapter. If multiple download adapters are
* supported on a site, the one with the highest priority will be
* used.
*
* @return int
*/
public function getPriority(): int
{
return $this->priority;
}
/**
* Returns the name of this download adapter in use
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Download a part (or the whole) of a remote URL and return the downloaded
* data. You are supposed to check the size of the returned data. If it's
* smaller than what you expected you've reached end of file. If it's empty
* you have tried reading past EOF. If it's larger than what you expected
* the server doesn't support chunk downloads.
*
* If this class' supportsChunkDownload returns false you should assume
* that the $from and $to parameters will be ignored.
*
* @param string $url The remote file's URL
* @param integer $from Byte range to start downloading from. Use null for start of file.
* @param integer $to Byte range to stop downloading. Use null to download the entire file ($from is ignored)
* @param array $params Additional params that will be added before performing the download
*
* @return string The raw file data retrieved from the remote URL.
*
* @throws DownloadError A generic exception is thrown on error
*/
public function downloadAndReturn(string $url, ?int $from = null, ?int $to = null, array $params = []): string
{
return '';
}
/**
* Get the size of a remote file in bytes
*
* @param string $url The remote file's URL
*
* @return integer The file size, or -1 if the remote server doesn't support this feature
*/
public function getFileSize(string $url): int
{
return -1;
}
}

View File

@ -0,0 +1,278 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Download\Adapter;
defined('_JEXEC') || die;
use FOF40\Download\DownloadInterface;
use FOF40\Download\Exception\DownloadError;
use Joomla\CMS\Language\Text;
/**
* A download adapter using the cURL PHP integration
*/
class Curl extends AbstractAdapter implements DownloadInterface
{
protected $headers = [];
public function __construct()
{
$this->priority = 110;
$this->supportsFileSize = true;
$this->supportsChunkDownload = true;
$this->name = 'curl';
$this->isSupported = function_exists('curl_init') && function_exists('curl_exec') && function_exists('curl_close');
}
/**
* Download a part (or the whole) of a remote URL and return the downloaded
* data. You are supposed to check the size of the returned data. If it's
* smaller than what you expected you've reached end of file. If it's empty
* you have tried reading past EOF. If it's larger than what you expected
* the server doesn't support chunk downloads.
*
* If this class' supportsChunkDownload returns false you should assume
* that the $from and $to parameters will be ignored.
*
* @param string $url The remote file's URL
* @param integer $from Byte range to start downloading from. Use null for start of file.
* @param integer $to Byte range to stop downloading. Use null to download the entire file ($from is
* ignored)
* @param array $params Additional params that will be added before performing the download
*
* @return string The raw file data retrieved from the remote URL.
*
* @throws DownloadError A generic exception is thrown on error
*/
public function downloadAndReturn(string $url, ?int $from = null, ?int $to = null, array $params = []): string
{
$ch = curl_init();
if (empty($from))
{
$from = 0;
}
if (empty($to))
{
$to = 0;
}
if ($to < $from)
{
$temp = $to;
$to = $from;
$from = $temp;
unset($temp);
}
$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSLVERSION, 0);
curl_setopt($ch, CURLOPT_CAINFO, $caCertPath);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, [$this, 'reponseHeaderCallback']);
if (!(empty($from) && empty($to)))
{
curl_setopt($ch, CURLOPT_RANGE, "$from-$to");
}
if (!is_array($params))
{
$params = [];
}
$patched_accept_encoding = false;
// Work around LiteSpeed sending compressed output under HTTP/2 when no encoding was requested
// See https://github.com/joomla/joomla-cms/issues/21423#issuecomment-410941000
if (defined('CURLOPT_ACCEPT_ENCODING'))
{
if (!array_key_exists(CURLOPT_ACCEPT_ENCODING, $params))
{
$params[CURLOPT_ACCEPT_ENCODING] = 'identity';
}
$patched_accept_encoding = true;
}
foreach ($params as $k => $v)
{
// I couldn't patch the accept encoding header (missing constant), so I'll check if we manually set it
if (!$patched_accept_encoding && $k == CURLOPT_HTTPHEADER)
{
foreach ($v as $custom_header)
{
// Ok, we explicitly set the Accept-Encoding header, so we consider it patched
if (stripos($custom_header, 'Accept-Encoding') !== false)
{
$patched_accept_encoding = true;
}
}
}
@curl_setopt($ch, $k, $v);
}
// Accept encoding wasn't patched, let's manually do that
if (!$patched_accept_encoding)
{
@curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept-Encoding: identity']);
$patched_accept_encoding = true;
}
$result = curl_exec($ch);
$errno = curl_errno($ch);
$errmsg = curl_error($ch);
$error = '';
$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($result === false)
{
$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_CURL_ERROR', $errno, $errmsg);
}
elseif (($http_status >= 300) && ($http_status <= 399) && isset($this->headers['location']) && !empty($this->headers['location']))
{
return $this->downloadAndReturn($this->headers['location'], $from, $to, $params);
}
elseif ($http_status > 399)
{
$result = false;
$errno = $http_status;
$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_HTTPERROR', $http_status);
}
curl_close($ch);
if ($result === false)
{
throw new DownloadError($error, $errno);
}
else
{
return $result;
}
}
/**
* Get the size of a remote file in bytes
*
* @param string $url The remote file's URL
*
* @return integer The file size, or -1 if the remote server doesn't support this feature
*/
public function getFileSize(string $url): int
{
$result = -1;
$ch = curl_init();
curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSLVERSION, 0);
$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CAINFO, $caCertPath);
$data = curl_exec($ch);
curl_close($ch);
if ($data)
{
$content_length = "unknown";
$status = "unknown";
$redirection = null;
if (preg_match("/^HTTP\/1\.[01] (\d\d\d)/i", $data, $matches))
{
$status = (int) $matches[1];
}
if (preg_match("/Content-Length: (\d+)/i", $data, $matches))
{
$content_length = (int) $matches[1];
}
if (preg_match("/Location: (.*)/i", $data, $matches))
{
$redirection = (int) $matches[1];
}
if ($status == 200 || ($status > 300 && $status <= 308))
{
$result = $content_length;
}
if (($status > 300) && ($status <= 308))
{
if (!empty($redirection))
{
return $this->getFileSize($redirection);
}
return -1;
}
}
return (int) $result;
}
/**
* Handles the HTTP headers returned by cURL
*
* @param resource $ch cURL resource handle (unused)
* @param string $data Each header line, as returned by the server
*
* @return int The length of the $data string
*/
protected function reponseHeaderCallback($ch, string $data): int
{
$strlen = strlen($data);
if (($strlen) <= 2)
{
return $strlen;
}
if (substr($data, 0, 4) == 'HTTP')
{
return $strlen;
}
if (strpos($data, ':') === false)
{
return $strlen;
}
[$header, $value] = explode(': ', trim($data), 2);
$this->headers[strtolower($header)] = $value;
return $strlen;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Download\Adapter;
defined('_JEXEC') || die;
use FOF40\Download\DownloadInterface;
use FOF40\Download\Exception\DownloadError;
use Joomla\CMS\Language\Text;
/**
* A download adapter using URL fopen() wrappers
*/
class Fopen extends AbstractAdapter implements DownloadInterface
{
public function __construct()
{
$this->priority = 100;
$this->supportsFileSize = false;
$this->supportsChunkDownload = true;
$this->name = 'fopen';
$this->isSupported = !function_exists('ini_get') ? false : ini_get('allow_url_fopen');
}
/**
* Download a part (or the whole) of a remote URL and return the downloaded
* data. You are supposed to check the size of the returned data. If it's
* smaller than what you expected you've reached end of file. If it's empty
* you have tried reading past EOF. If it's larger than what you expected
* the server doesn't support chunk downloads.
*
* If this class' supportsChunkDownload returns false you should assume
* that the $from and $to parameters will be ignored.
*
* @param string $url The remote file's URL
* @param integer $from Byte range to start downloading from. Use null for start of file.
* @param integer $to Byte range to stop downloading. Use null to download the entire file ($from is
* ignored)
* @param array $params Additional params that will be added before performing the download
*
* @return string The raw file data retrieved from the remote URL.
*
* @throws DownloadError A generic exception is thrown on error
*/
public function downloadAndReturn(string $url, ?int $from = null, ?int $to = null, array $params = []): string
{
if (empty($from))
{
$from = 0;
}
if (empty($to))
{
$to = 0;
}
if ($to < $from)
{
$temp = $to;
$to = $from;
$from = $temp;
unset($temp);
}
if (!(empty($from) && empty($to)))
{
$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';
$options = [
'http' => [
'method' => 'GET',
'header' => "Range: bytes=$from-$to\r\n",
],
'ssl' => [
'verify_peer' => true,
'cafile' => $caCertPath,
'verify_depth' => 5,
],
];
$options = array_merge($options, $params);
$context = stream_context_create($options);
$result = @file_get_contents($url, false, $context, $from - $to + 1);
}
else
{
$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';
$options = [
'http' => [
'method' => 'GET',
],
'ssl' => [
'verify_peer' => true,
'cafile' => $caCertPath,
'verify_depth' => 5,
],
];
$options = array_merge($options, $params);
$context = stream_context_create($options);
$result = @file_get_contents($url, false, $context);
}
global $http_response_header_test;
if (!isset($http_response_header) && empty($http_response_header_test))
{
$error = Text::_('LIB_FOF40_DOWNLOAD_ERR_FOPEN_ERROR');
throw new DownloadError($error, 404);
}
else
{
// Used for testing
if (!isset($http_response_header) && !empty($http_response_header_test))
{
$http_response_header = $http_response_header_test;
}
$http_code = 200;
$nLines = count($http_response_header);
for ($i = $nLines - 1; $i >= 0; $i--)
{
$line = $http_response_header[$i];
if (strncasecmp("HTTP", $line, 4) == 0)
{
$response = explode(' ', $line);
$http_code = $response[1];
break;
}
}
if ($http_code >= 299)
{
$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_HTTPERROR', $http_code);
throw new DownloadError($error, $http_code);
}
}
if ($result === false)
{
$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_FOPEN_ERROR');
throw new DownloadError($error, 1);
}
else
{
return $result;
}
}
}

View File

@ -0,0 +1,501 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Download;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Download\Exception\DownloadError;
use FOF40\Timer\Timer;
use Joomla\CMS\Language\Text;
class Download
{
/**
* The component container object
*
* @var Container
*/
protected $container;
/**
* Parameters passed from the GUI when importing from URL
*
* @var array
*/
private $params = [];
/**
* The download adapter which will be used by this class
*
* @var DownloadInterface
*/
private $adapter;
/**
* Additional params that will be passed to the adapter while performing the download
*
* @var array
*/
private $adapterOptions = [];
/**
* Public constructor
*
* @param Container $c The component container
*/
public function __construct(Container $c)
{
$this->container = $c;
// Find the best fitting adapter
$allAdapters = self::getFiles(__DIR__ . '/Adapter', [], ['AbstractAdapter.php']);
$priority = 0;
foreach ($allAdapters as $adapterInfo)
{
/** @var Adapter\AbstractAdapter $adapter */
$adapter = new $adapterInfo['classname'];
if (!$adapter->isSupported())
{
continue;
}
if ($adapter->priority > $priority)
{
$this->adapter = $adapter;
$priority = $adapter->priority;
}
}
// Load the language strings
$c->platform->loadTranslations('lib_fof40');
}
/**
* This method will crawl a starting directory and get all the valid files
* that will be analyzed by __construct. Then it organizes them into an
* associative array.
*
* @param string $path Folder where we should start looking
* @param array $ignoreFolders Folder ignore list
* @param array $ignoreFiles File ignore list
*
* @return array Associative array, where the `fullpath` key contains the path to the file,
* and the `classname` key contains the name of the class
*/
protected static function getFiles(string $path, array $ignoreFolders = [], array $ignoreFiles = []): array
{
$return = [];
$files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles);
// Ok, I got the files, now I have to organize them
foreach ($files as $file)
{
$clean = str_replace($path, '', $file);
$clean = trim(str_replace('\\', '/', $clean), '/');
$parts = explode('/', $clean);
$return[] = [
'fullpath' => $file,
'classname' => '\\FOF40\\Download\\Adapter\\' . ucfirst(basename($parts[0], '.php')),
];
}
return $return;
}
/**
* Recursive function that will scan every directory unless it's in the
* ignore list. Files that aren't in the ignore list are returned.
*
* @param string $path Folder where we should start looking
* @param array $ignoreFolders Folder ignore list
* @param array $ignoreFiles File ignore list
*
* @return array List of all the files
*/
protected static function scanDirectory(string $path, array $ignoreFolders = [], array $ignoreFiles = []): array
{
$return = [];
$handle = @opendir($path);
if (!$handle)
{
return $return;
}
while (($file = readdir($handle)) !== false)
{
if ($file == '.' || $file == '..')
{
continue;
}
$fullpath = $path . '/' . $file;
if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles)))
{
continue;
}
if (is_dir($fullpath))
{
$return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return);
}
else
{
$return[] = $path . '/' . $file;
}
}
return $return;
}
/**
* Forces the use of a specific adapter
*
* @param string $className The name of the class or the name of the adapter
*/
public function setAdapter(?string $className = null): void
{
if (is_null($className))
{
return;
}
$adapter = null;
if (class_exists($className, true))
{
$adapter = new $className;
}
elseif (class_exists('\\FOF40\\Download\\Adapter\\' . ucfirst($className)))
{
$className = '\\FOF40\\Download\\Adapter\\' . ucfirst($className);
$adapter = new $className;
}
if (!is_object($adapter))
{
return;
}
if (!$adapter instanceof DownloadInterface)
{
return;
}
$this->adapter = $adapter;
}
/**
* Returns the name of the current adapter
*
* @return string
*/
public function getAdapterName(): string
{
if (is_object($this->adapter))
{
$class = get_class($this->adapter);
return strtolower(str_ireplace('FOF40\\Download\\Adapter\\', '', $class));
}
return '';
}
/**
* Returns the additional options for the adapter
*
* @return array
*
* @codeCoverageIgnore
*/
public function getAdapterOptions(): array
{
return $this->adapterOptions;
}
/**
* Sets the additional options for the adapter
*
* @param array $options
*
* @codeCoverageIgnore
*/
public function setAdapterOptions(array $options): void
{
$this->adapterOptions = $options;
}
/**
* Download data from a URL and return it.
*
* Important note about ranges: byte ranges start at 0. This means that the first 500 bytes of a file are from 0
* to 499, NOT from 1 to 500. If you ask more bytes than there are in the file or a range which is invalid or does
* not exist this method will return false.
*
* @param string $url The URL to download from
* @param int $from Byte range to start downloading from. Use null (default) for start of file.
* @param int $to Byte range to stop downloading. Use null to download the entire file ($from will be
* ignored!)
*
* @return string The downloaded data or null on failure
*/
public function getFromURL(string $url, ?int $from = null, ?int $to = null): ?string
{
try
{
return $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
}
catch (DownloadError $e)
{
return null;
}
}
/**
* Performs the staggered download of file.
*
* @param array $params A parameters array, as sent by the user interface
*
* @return array A return status array
*/
public function importFromURL(array $params): array
{
$this->params = $params;
// Fetch data
$url = $this->getParam('url');
$localFilename = $this->getParam('localFilename');
$frag = $this->getParam('frag', -1);
$totalSize = $this->getParam('totalSize', -1);
$doneSize = $this->getParam('doneSize', -1);
$maxExecTime = $this->getParam('maxExecTime', 5);
$runTimeBias = $this->getParam('runTimeBias', 75);
$length = $this->getParam('length', 1048576);
if (empty($localFilename))
{
$localFilename = basename($url);
if (strpos($localFilename, '?') !== false)
{
$paramsPos = strpos($localFilename, '?');
$localFilename = substr($localFilename, 0, $paramsPos - 1);
$platformBaseDirectories = $this->container->platform->getPlatformBaseDirs();
$tmpDir = $platformBaseDirectories['tmp'];
$tmpDir = rtrim($tmpDir, '/\\');
$localFilename = $tmpDir . '/' . $localFilename;
}
}
// Init retArray
$retArray = [
"status" => true,
"error" => '',
"frag" => $frag,
"totalSize" => $totalSize,
"doneSize" => $doneSize,
"percent" => 0,
"localfile" => $localFilename,
];
try
{
$timer = new Timer($maxExecTime, $runTimeBias);
$start = $timer->getRunningTime(); // Mark the start of this download
$break = false; // Don't break the step
do
{
// Do we have to initialize the file?
if ($frag == -1)
{
// Currently downloaded size
$doneSize = 0;
if (@file_exists($localFilename))
{
@unlink($localFilename);
}
// Delete and touch the output file
$fp = @fopen($localFilename, 'w');
if ($fp !== false)
{
@fclose($fp);
}
// Init
$frag = 0;
$retArray['totalSize'] = $this->adapter->getFileSize($url);
if ($retArray['totalSize'] <= 0)
{
$retArray['totalSize'] = 0;
}
$totalSize = $retArray['totalSize'];
}
// Calculate from and length
$from = $frag * $length;
$to = $length + $from - 1;
// Try to download the first frag
$required_time = 1.0;
$error = '';
try
{
$result = $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
}
catch (DownloadError $e)
{
$result = false;
$error = $e->getMessage();
}
if ($result === false)
{
// Failed download
if ($frag == 0)
{
// Failure to download first frag = failure to download. Period.
$retArray['status'] = false;
$retArray['error'] = $error;
return $retArray;
}
else
{
// Since this is a staggered download, consider this normal and finish
$frag = -1;
$totalSize = $doneSize;
$break = true;
}
}
// Add the currently downloaded frag to the total size of downloaded files
if ($result !== false)
{
$fileSize = strlen($result);
$doneSize += $fileSize;
// Append the file
$fp = @fopen($localFilename, 'a');
if ($fp === false)
{
// Can't open the file for writing
$retArray['status'] = false;
$retArray['error'] = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE', $localFilename);
return $retArray;
}
fwrite($fp, $result);
fclose($fp);
$frag++;
if (($fileSize < $length) || ($fileSize > $length)
|| (($totalSize == $doneSize) && ($totalSize > 0))
)
{
// A partial download or a download larger than the frag size means we are done
$frag = -1;
//debugMsg("-- Import complete (partial download of last frag)");
$totalSize = $doneSize;
$break = true;
}
}
// Advance the frag pointer and mark the end
$end = $timer->getRunningTime();
// Do we predict that we have enough time?
$required_time = max(1.1 * ($end - $start), $required_time);
if ($required_time > (10 - $end + $start))
{
$break = true;
}
$start = $end;
} while (($timer->getTimeLeft() > 0) && !$break);
if ($frag == -1)
{
$percent = 100;
}
elseif ($doneSize <= 0)
{
$percent = 0;
}
elseif ($totalSize > 0)
{
$percent = 100 * ($doneSize / $totalSize);
}
else
{
$percent = 0;
}
// Update $retArray
$retArray = [
"status" => true,
"error" => '',
"frag" => $frag,
"totalSize" => $totalSize,
"doneSize" => $doneSize,
"percent" => $percent,
];
}
catch (DownloadError $e)
{
$retArray['status'] = false;
$retArray['error'] = $e->getMessage();
}
return $retArray;
}
/**
* Used to decode the $params array
*
* @param string $key The parameter key you want to retrieve the value for
* @param mixed $default The default value, if none is specified
*
* @return mixed The value for this parameter key
*/
private function getParam(string $key, $default = null)
{
if (array_key_exists($key, $this->params))
{
return $this->params[$key];
}
else
{
return $default;
}
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Download;
defined('_JEXEC') || die;
use FOF40\Download\Exception\DownloadError;
/**
* Interface DownloadInterface
*
* @codeCoverageIgnore
*/
interface DownloadInterface
{
/**
* Does this download adapter support downloading files in chunks?
*
* @return boolean True if chunk download is supported
*/
public function supportsChunkDownload(): bool;
/**
* Does this download adapter support reading the size of a remote file?
*
* @return boolean True if remote file size determination is supported
*/
public function supportsFileSize(): bool;
/**
* Is this download class supported in the current server environment?
*
* @return boolean True if this server environment supports this download class
*/
public function isSupported(): bool;
/**
* Get the priority of this adapter. If multiple download adapters are
* supported on a site, the one with the highest priority will be
* used.
*
* @return int
*/
public function getPriority(): int;
/**
* Returns the name of this download adapter in use
*
* @return string
*/
public function getName(): string;
/**
* Download a part (or the whole) of a remote URL and return the downloaded
* data. You are supposed to check the size of the returned data. If it's
* smaller than what you expected you've reached end of file. If it's empty
* you have tried reading past EOF. If it's larger than what you expected
* the server doesn't support chunk downloads.
*
* If this class' supportsChunkDownload returns false you should assume
* that the $from and $to parameters will be ignored.
*
* @param string $url The remote file's URL
* @param int|null $from Byte range to start downloading from. Use null for start of file.
* @param int|null $to Byte range to stop downloading. Use null to download the entire file ($from is ignored)
* @param array $params Additional params that will be added before performing the download
*
* @return string The raw file data retrieved from the remote URL.
*
* @throws DownloadError A generic exception is thrown on error
*/
public function downloadAndReturn(string $url, ?int $from = null, ?int $to = null, array $params = []): string;
/**
* Get the size of a remote file in bytes
*
* @param string $url The remote file's URL
*
* @return integer The file size, or -1 if the remote server doesn't support this feature
*/
public function getFileSize(string $url): int;
}

View File

@ -0,0 +1,17 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Download\Exception;
defined('_JEXEC') || die;
use RuntimeException;
class DownloadError extends RuntimeException
{
}

View File

@ -0,0 +1,289 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt;
defined('_JEXEC') || die;
use FOF40\Encrypt\AesAdapter\AdapterInterface;
use FOF40\Encrypt\AesAdapter\OpenSSL;
/**
* A simple abstraction to AES encryption
*
* Usage:
*
* // Create a new instance.
* $aes = new Aes();
* // Set the encryption password. It's expanded to a key automatically.
* $aes->setPassword('yourPassword');
* // Encrypt something.
* $cipherText = $aes->encryptString($sourcePlainText);
* // Decrypt something
* $plainText = $aes->decryptString($sourceCipherText);
*/
class Aes
{
/**
* The cipher key.
*
* @var string
*/
private $key = '';
/**
* The AES encryption adapter in use.
*
* @var AdapterInterface
*/
private $adapter;
/**
* Initialise the AES encryption object.
*
* @param string $mode Encryption mode. Can be ebc or cbc. We recommend using cbc.
*/
public function __construct(string $mode = 'cbc')
{
$this->adapter = new OpenSSL();
$this->adapter->setEncryptionMode($mode);
}
/**
* Is AES encryption supported by this PHP installation?
*
* @return boolean
*/
public static function isSupported(): bool
{
$adapter = new OpenSSL();
if (!$adapter->isSupported())
{
return false;
}
if (!\function_exists('base64_encode'))
{
return false;
}
if (!\function_exists('base64_decode'))
{
return false;
}
if (!\function_exists('hash_algos'))
{
return false;
}
$algorithms = \hash_algos();
return in_array('sha256', $algorithms);
}
/**
* Sets the password for this instance.
*
* @param string $password The password (either user-provided password or binary encryption key) to use
*/
public function setPassword(string $password)
{
$this->key = $password;
}
/**
* Encrypts a string using AES
*
* @param string $stringToEncrypt The plaintext to encrypt
* @param bool $base64encoded Should I Base64-encode the result?
*
* @return string The cryptotext. Please note that the first 16 bytes of
* the raw string is the IV (initialisation vector) which
* is necessary for decoding the string.
*/
public function encryptString(string $stringToEncrypt, bool $base64encoded = true): string
{
$blockSize = $this->adapter->getBlockSize();
$randVal = new Randval();
$iv = $randVal->generate($blockSize);
$key = $this->getExpandedKey($blockSize, $iv);
$cipherText = $this->adapter->encrypt($stringToEncrypt, $key, $iv);
// Optionally pass the result through Base64 encoding
if ($base64encoded)
{
$cipherText = base64_encode($cipherText);
}
// Return the result
return $cipherText;
}
/**
* Decrypts a ciphertext into a plaintext string using AES
*
* @param string $stringToDecrypt The ciphertext to decrypt. The first 16 bytes of the raw string must contain
* the IV (initialisation vector).
* @param bool $base64encoded Should I Base64-decode the data before decryption?
* @param bool $legacy Use legacy key expansion? Use it to decrypt date encrypted with FOF 3.
*
* @return string The plain text string
*/
public function decryptString(string $stringToDecrypt, bool $base64encoded = true, bool $legacy = false): string
{
if ($base64encoded)
{
$stringToDecrypt = base64_decode($stringToDecrypt);
}
// Extract IV
$iv_size = $this->adapter->getBlockSize();
$strLen = function_exists('mb_strlen') ? mb_strlen($stringToDecrypt, 'ASCII') : strlen($stringToDecrypt);
// If the string is not big enough to have an Initialization Vector in front then, clearly, it is not encrypted.
if ($strLen < $iv_size)
{
return '';
}
// Get the IV, the key and decrypt the string
$iv = substr($stringToDecrypt, 0, $iv_size);
$key = $this->getExpandedKey($iv_size, $iv, $legacy);
return $this->adapter->decrypt($stringToDecrypt, $key);
}
/**
* Performs key expansion using PBKDF2
*
* CAVEAT: If your password ($this->key) is the same size as $blockSize you don't get key expansion. Practically,
* it means that you should avoid using 16 byte passwords.
*
* @param int $blockSize Block size in bytes. This should always be 16 since we only deal with 128-bit AES
* here.
* @param string $iv The initial vector. Use Randval::generate($blockSize)
* @param bool $legacy Use legacy key expansion? Only ever use to decrypt data encrypted with FOF 3.
*
* @return string
*/
public function getExpandedKey(int $blockSize, string $iv, bool $legacy = false): string
{
$key = $legacy ? $this->legacyKey($this->key) : $this->key;
$passLength = strlen($key);
if (function_exists('mb_strlen'))
{
$passLength = mb_strlen($key, 'ASCII');
}
if ($passLength !== $blockSize)
{
$iterations = 1000;
$salt = $this->adapter->resizeKey($iv, 16);
$key = hash_pbkdf2('sha256', $this->key, $salt, $iterations, $blockSize, true);
}
return $key;
}
/**
* Process the password the same way FOF 3 did.
*
* This is a very bad idea. It would get a password, calculate its SHA-256 and throw half of it away. The rest was
* used as the encryption key. In FOF 4 we use a far more sane key expansion using PKKDF2 with SHA-256 and 1000
* rounds.
*
* @param $password
*
* @return string
* @since 4.0.0
*/
private function legacyKey($password): string
{
$passLength = strlen($password);
if (function_exists('mb_strlen'))
{
$passLength = mb_strlen($password, 'ASCII');
}
if ($passLength === 32)
{
return $password;
}
// Legacy mode was doing something stupid, requiring a key of 32 bytes. DO NOT USE LEGACY MODE!
// Legacy mode: use the sha256 of the password
$key = hash('sha256', $password, true);
// We have to trim or zero pad the password (we end up throwing half of it away in Rijndael-128 / AES...)
$key = $this->adapter->resizeKey($key, $this->adapter->getBlockSize());
return $key;
}
}
/**
* Compatibility mode for servers lacking the hash_pbkdf2 PHP function (typically, the hash extension is installed but
* PBKDF2 was not compiled into it). This is really slow but since it's used sparingly you shouldn't notice a
* substantial performance degradation under most circumstances.
*/
if (!function_exists('hash_pbkdf2'))
{
function hash_pbkdf2($algo, $password, $salt, $count, $length = 0, $raw_output = false)
{
if (!in_array(strtolower($algo), hash_algos()))
{
trigger_error(__FUNCTION__ . '(): Unknown hashing algorithm: ' . $algo, E_USER_WARNING);
}
if (!is_numeric($count))
{
trigger_error(__FUNCTION__ . '(): expects parameter 4 to be long, ' . gettype($count) . ' given', E_USER_WARNING);
}
if (!is_numeric($length))
{
trigger_error(__FUNCTION__ . '(): expects parameter 5 to be long, ' . gettype($length) . ' given', E_USER_WARNING);
}
if ($count <= 0)
{
trigger_error(__FUNCTION__ . '(): Iterations must be a positive integer: ' . $count, E_USER_WARNING);
}
if ($length < 0)
{
trigger_error(__FUNCTION__ . '(): Length must be greater than or equal to 0: ' . $length, E_USER_WARNING);
}
$output = '';
$block_count = $length ? ceil($length / strlen(hash($algo, '', $raw_output))) : 1;
for ($i = 1; $i <= $block_count; $i++)
{
$last = $xorsum = hash_hmac($algo, $salt . pack('N', $i), $password, true);
for ($j = 1; $j < $count; $j++)
{
$xorsum ^= ($last = hash_hmac($algo, $last, $password, true));
}
$output .= $xorsum;
}
if (!$raw_output)
{
$output = bin2hex($output);
}
return $length ? substr($output, 0, $length) : $output;
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt\AesAdapter;
defined('_JEXEC') || die();
/**
* Abstract AES encryption class
*/
abstract class AbstractAdapter
{
/**
* Trims or zero-pads a key / IV
*
* @param string $key The key or IV to treat
* @param int $size The block size of the currently used algorithm
*
* @return null|string Null if $key is null, treated string of $size byte length otherwise
*/
public function resizeKey(string $key, int $size): ?string
{
if (empty($key))
{
return null;
}
$keyLength = strlen($key);
if (function_exists('mb_strlen'))
{
$keyLength = mb_strlen($key, 'ASCII');
}
if ($keyLength === $size)
{
return $key;
}
if ($keyLength > $size)
{
if (function_exists('mb_substr'))
{
return mb_substr($key, 0, $size, 'ASCII');
}
return substr($key, 0, $size);
}
return $key . str_repeat("\0", ($size - $keyLength));
}
/**
* Returns null bytes to append to the string so that it's zero padded to the specified block size
*
* @param string $string The binary string which will be zero padded
* @param int $blockSize The block size
*
* @return string The zero bytes to append to the string to zero pad it to $blockSize
*/
protected function getZeroPadding(string $string, int $blockSize): string
{
$stringSize = strlen($string);
if (function_exists('mb_strlen'))
{
$stringSize = mb_strlen($string, 'ASCII');
}
if ($stringSize === $blockSize)
{
return '';
}
if ($stringSize < $blockSize)
{
return str_repeat("\0", $blockSize - $stringSize);
}
$paddingBytes = $stringSize % $blockSize;
return str_repeat("\0", $blockSize - $paddingBytes);
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt\AesAdapter;
defined('_JEXEC') || die;
/**
* Interface for AES encryption adapters
*/
interface AdapterInterface
{
/**
* Sets the AES encryption mode.
*
* @param string $mode Choose between CBC (recommended) or ECB
*
* @return void
*/
public function setEncryptionMode(string $mode = 'cbc'): void;
/**
* Encrypts a string. Returns the raw binary ciphertext.
*
* WARNING: The plaintext is zero-padded to the algorithm's block size. You are advised to store the size of the
* plaintext and trim the string to that length upon decryption.
*
* @param string $plainText The plaintext to encrypt
* @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size)
* @param null|string $iv The initialization vector (for CBC mode algorithms)
*
* @return string The raw encrypted binary string.
*/
public function encrypt(string $plainText, string $key, ?string $iv = null): string;
/**
* Decrypts a string. Returns the raw binary plaintext.
*
* $ciphertext MUST start with the IV followed by the ciphertext, even for EBC data (the first block of data is
* dropped in EBC mode since there is no concept of IV in EBC).
*
* WARNING: The returned plaintext is zero-padded to the algorithm's block size during encryption. You are advised
* to trim the string to the original plaintext's length upon decryption. While rtrim($decrypted, "\0") sounds
* appealing it's NOT the correct approach for binary data (zero bytes may actually be part of your plaintext, not
* just padding!).
*
* @param string $cipherText The ciphertext to encrypt
* @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size)
*
* @return string The raw unencrypted binary string.
*/
public function decrypt(string $cipherText, string $key): string;
/**
* Returns the encryption block size in bytes
*
* @return int
*/
public function getBlockSize(): int;
/**
* Is this adapter supported?
*
* @return bool
*/
public function isSupported(): bool;
}

View File

@ -0,0 +1,168 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt\AesAdapter;
defined('_JEXEC') || die;
use FOF40\Encrypt\Randval;
class OpenSSL extends AbstractAdapter implements AdapterInterface
{
/**
* The OpenSSL options for encryption / decryption
*
* PHP 5.3 does not have the constants OPENSSL_RAW_DATA and OPENSSL_ZERO_PADDING. In fact, the parameter
* is called $raw_data and is a boolean. Since integer 1 is equivalent to boolean TRUE in PHP we can get
* away with initializing this parameter with the integer 1.
*
* @var int
*/
protected $openSSLOptions = 1;
/**
* The encryption method to use
*
* @var string
*/
protected $method = 'aes-128-cbc';
public function __construct()
{
/**
* PHP 5.4 and later replaced the $raw_data parameter with the $options parameter. Instead of a boolean we need
* to pass some flags.
*
* See http://stackoverflow.com/questions/24707007/using-openssl-raw-data-param-in-openssl-decrypt-with-php-5-3#24707117
*/
$this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
}
public function setEncryptionMode(string $mode = 'cbc'): void
{
static $availableAlgorithms = null;
static $defaultAlgo = 'aes-128-cbc';
if (!is_array($availableAlgorithms))
{
$availableAlgorithms = openssl_get_cipher_methods();
foreach ([
'aes-256-cbc', 'aes-256-ecb', 'aes-192-cbc',
'aes-192-ecb', 'aes-128-cbc', 'aes-128-ecb',
] as $algo)
{
if (in_array($algo, $availableAlgorithms))
{
$defaultAlgo = $algo;
break;
}
}
}
$mode = strtolower($mode);
if (!in_array($mode, ['cbc', 'ebc']))
{
$mode = 'cbc';
}
$algo = 'aes-128-' . $mode;
if (!in_array($algo, $availableAlgorithms))
{
$algo = $defaultAlgo;
}
$this->method = $algo;
}
public function encrypt(string $plainText, string $key, ?string $iv = null): string
{
$iv_size = $this->getBlockSize();
$key = $this->resizeKey($key, $iv_size);
$iv = $this->resizeKey($iv, $iv_size);
if (empty($iv))
{
$randVal = new Randval();
$iv = $randVal->generate($iv_size);
}
$plainText .= $this->getZeroPadding($plainText, $iv_size);
$cipherText = openssl_encrypt($plainText, $this->method, $key, $this->openSSLOptions, $iv);
return $iv . $cipherText;
}
public function decrypt(string $cipherText, string $key): string
{
$iv_size = $this->getBlockSize();
$key = $this->resizeKey($key, $iv_size);
$iv = substr($cipherText, 0, $iv_size);
$cipherText = substr($cipherText, $iv_size);
return openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv);
}
public function isSupported(): bool
{
if (!\function_exists('openssl_get_cipher_methods'))
{
return false;
}
if (!\function_exists('openssl_random_pseudo_bytes'))
{
return false;
}
if (!\function_exists('openssl_cipher_iv_length'))
{
return false;
}
if (!\function_exists('openssl_encrypt'))
{
return false;
}
if (!\function_exists('openssl_decrypt'))
{
return false;
}
if (!\function_exists('hash'))
{
return false;
}
if (!\function_exists('hash_algos'))
{
return false;
}
$algorithms = \openssl_get_cipher_methods();
if (!in_array('aes-128-cbc', $algorithms))
{
return false;
}
$algorithms = \hash_algos();
return in_array('sha256', $algorithms);
}
/**
* @return int
*/
public function getBlockSize(): int
{
return openssl_cipher_iv_length($this->method);
}
}

View File

@ -0,0 +1,208 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt;
defined('_JEXEC') || die;
use InvalidArgumentException;
/**
* Base32 encoding class, used by the TOTP
*/
class Base32
{
/**
* CSRFC3548
*
* The character set as defined by RFC3548
* @link http://www.ietf.org/rfc/rfc3548.txt
*/
const CSRFC3548 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Convert any string to a base32 string
* This should be binary safe...
*
* @param string $str The string to convert
*
* @return string The converted base32 string
*/
public function encode(string $str): string
{
return $this->fromBin($this->str2bin($str));
}
/**
* Convert any base32 string to a normal sctring
* This should be binary safe...
*
* @param string $str The base32 string to convert
*
* @return string The normal string
*/
public function decode(string $str): string
{
$str = strtoupper($str);
return $this->bin2str($this->tobin($str));
}
/**
* Converts any ascii string to a binary string
*
* @param string $str The string you want to convert
*
* @return string String of 0's and 1's
*/
private function str2bin(string $str): string
{
$chrs = unpack('C*', $str);
return vsprintf(str_repeat('%08b', is_array($chrs) || $chrs instanceof \Countable ? count($chrs) : 0), $chrs);
}
/**
* Converts a binary string to an ascii string
*
* @param string $str The string of 0's and 1's you want to convert
*
* @return string The ascii output
*
* @throws InvalidArgumentException
*/
private function bin2str(string $str): string
{
if (strlen($str) % 8 > 0)
{
throw new InvalidArgumentException('Length must be divisible by 8');
}
if (!preg_match('/^[01]+$/', $str))
{
throw new InvalidArgumentException('Only 0\'s and 1\'s are permitted');
}
preg_match_all('/.{8}/', $str, $chrs);
$chrs = array_map('bindec', $chrs[0]);
// I'm just being slack here
array_unshift($chrs, 'C*');
return call_user_func_array('pack', $chrs);
}
/**
* Converts a correct binary string to base32
*
* @param string $str The string of 0's and 1's you want to convert
*
* @return string String encoded as base32
*
* @throws InvalidArgumentException
*/
private function fromBin(string $str): string
{
if (strlen($str) % 8 > 0)
{
throw new InvalidArgumentException('Length must be divisible by 8');
}
if (!preg_match('/^[01]+$/', $str))
{
throw new InvalidArgumentException('Only 0\'s and 1\'s are permitted');
}
// Base32 works on the first 5 bits of a byte, so we insert blanks to pad it out
$str = preg_replace('/(.{5})/', '000$1', $str);
// We need a string divisible by 5
$length = strlen($str);
$rbits = $length & 7;
if ($rbits > 0)
{
// Excessive bits need to be padded
$ebits = substr($str, $length - $rbits);
$str = substr($str, 0, $length - $rbits);
$str .= "000$ebits" . str_repeat('0', 5 - strlen($ebits));
}
preg_match_all('/.{8}/', $str, $chrs);
$chrs = array_map([$this, 'mapCharset'], $chrs[0]);
return implode('', $chrs);
}
/**
* Accepts a base32 string and returns an ascii binary string
*
* @param string $str The base32 string to convert
*
* @return string Ascii binary string
*
* @throws InvalidArgumentException
*/
private function toBin(string $str): string
{
if (!preg_match('/^[' . self::CSRFC3548 . ']+$/', $str))
{
throw new InvalidArgumentException('Base64 string must match character set');
}
// Convert the base32 string back to a binary string
$str = join('', array_map([$this, 'mapBin'], str_split($str)));
// Remove the extra 0's we added
$str = preg_replace('/000(.{5})/', '$1', $str);
// Remove padding if necessary
$length = strlen($str);
$rbits = $length & 7;
if ($rbits > 0)
{
$str = substr($str, 0, $length - $rbits);
}
return $str;
}
/**
* Used with array_map to map the bits from a binary string
* directly into a base32 character set
*
* @param string $str The string of 0's and 1's you want to convert
*
* @return string Resulting base32 character
*
* @access private
*/
private function mapCharset(string $str): string
{
// Huh!
$x = self::CSRFC3548;
return $x[bindec($str)];
}
/**
* Used with array_map to map the characters from a base32
* character set directly into a binary string
*
* @param string $chr The character to map
*
* @return string String of 0's and 1's
*
* @access private
*/
private function mapBin(string $chr): string
{
return sprintf('%08b', strpos(self::CSRFC3548, $chr));
}
}

View File

@ -0,0 +1,278 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt;
defined('_JEXEC') || die;
use FOF40\Container\Container;
/**
* Data encryption service for FOF-based components.
*
* This service allows you to transparently encrypt and decrypt *text* plaintext data. Use it to provide encryption for
* sensitive or personal data stored in your database. Please remember:
*
* - The default behavior is to create a file with a random key on your component's root. If the file cannot be created
* the encryption is turned off.
* - The key file is only created when you access the service. If you never use this service nothing happens (for
* backwards compatibility).
* - You have to manually encrypt and decrypt data. It won't happen magically.
* - Encrypted data cannot be searched unless you implement your own, slow, search algorithm.
* - Data encryption is meant to be used on top of, not instead of, any other security measures for your site.
* - Data encryption only protects against exploits targeting the database. If the attacker *also* gains read access to
* your filesystem OR if the attacker gains read / write access to the filesystem the encryption won't protect you.
* This is a full compromise of your site. At this point you're pwned and nothing can protect you. If you don't
* understand this simple truth do NOT use encryption.
* - This is meant as a simple and basic encryption layer. It has not been independently verified. Use at your own risk.
*
* This service has the following FOF application configuration parameters which can be declared under the "container"
* key (e.g. the "name" attribute of the fof.xml elements under fof > common > container > option):
*
* - encrypt_key_file The path to the key file, relative to the component's backend root and WITHOUT the .php extension
* - encrypt_key_const The constant for the key. By default it is COMPONENTNAME_FOF_ENCRYPT_SERVICE_SECRETKEY where
* COMPONENTNAME corresponds to the uppercase com_componentname without the com_ prefix.
*
* @package FOF40\Encrypt
*
* @since 3.3.2
*/
class EncryptService
{
/**
* The component's container
*
* @var Container
* @since 3.3.2
*/
private $container;
/**
* The encryption engine used by this service
*
* @var Aes
* @since 3.3.2
*/
private $aes;
/**
* EncryptService constructor.
*
* @param Container $c The FOF component container
*
* @since 3.3.2
*/
public function __construct(Container $c)
{
$this->container = $c;
$this->initialize();
}
/**
* Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128###
*
* @param string $data The plaintext data
*
* @return string The ciphertext, prefixed by ###AES128###
*
* @since 3.3.2
*/
public function encrypt(string $data): string
{
if (!is_object($this->aes))
{
return $data;
}
$encrypted = $this->aes->encryptString($data, true);
return '###AES128###' . $encrypted;
}
/**
* Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext.
*
* @param string $data The ciphertext, prefixed by ###AES128###
* @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3.
*
* @return string The plaintext data
*
* @since 3.3.2
*/
public function decrypt(string $data, bool $legacy = false): string
{
if (substr($data, 0, 12) != '###AES128###')
{
return $data;
}
$data = substr($data, 12);
if (!is_object($this->aes))
{
return $data;
}
$decrypted = $this->aes->decryptString($data, true, $legacy);
// Decrypted data is null byte padded. We have to remove the padding before proceeding.
return rtrim($decrypted, "\0");
}
/**
* Initialize the AES cryptography object
*
* @return void
* @since 3.3.2
*
*/
private function initialize(): void
{
if (is_object($this->aes))
{
return;
}
$password = $this->getPassword();
if (empty($password))
{
return;
}
$this->aes = new Aes('cbc');
$this->aes->setPassword($password);
}
/**
* Returns the path to the secret key file
*
* @return string
*
* @since 3.3.2
*/
private function getPasswordFilePath(): string
{
$default = 'encrypt_service_key';
$baseName = $this->container->appConfig->get('container.encrypt_key_file', $default);
$baseName = trim($baseName, '/\\');
return $this->container->backEndPath . '/' . $baseName . '.php';
}
/**
* Get the name of the constant where the secret key is stored. Remember that this is searched first, before a new
* key file is created. You can define this constant anywhere in your code loaded before the encryption service is
* first used to prevent a key file being created.
*
* @return string
*
* @since 3.3.2
*/
private function getConstantName(): string
{
$default = strtoupper($this->container->bareComponentName) . '_FOF_ENCRYPT_SERVICE_SECRETKEY';
return $this->container->appConfig->get('container.encrypt_key_const', $default);
}
/**
* Returns the password used to encrypt information in the component
*
* @return string
*
* @since 3.3.2
*/
private function getPassword(): string
{
$constantName = $this->getConstantName();
// If we have already read the file just return the key
if (defined($constantName))
{
return constant($constantName);
}
// Do I have a secret key file?
$filePath = $this->getPasswordFilePath();
// I can't get the path to the file. Cut our losses and assume we can get no key.
if (empty($filePath))
{
define($constantName, '');
return '';
}
// If not, try to create one.
if (!file_exists($filePath))
{
$this->makePasswordFile();
}
// We failed to create a new file? Cut our losses and assume we can get no key.
if (!file_exists($filePath) || !is_readable($filePath))
{
define($constantName, '');
return '';
}
// Try to include the key file
include_once $filePath;
// The key file contains garbage. Treason! Cut our losses and assume we can get no key.
if (!defined($constantName))
{
define($constantName, '');
return '';
}
// Finally, return the key which was defined in the file (happy path).
return constant($constantName);
}
/**
* Create a new secret key file using a long, randomly generated password. The password generator uses a crypto-safe
* pseudorandom number generator (PRNG) to ensure suitability of the password for encrypting data at rest.
*
* @return void
*
* @since 3.3.2
*/
private function makePasswordFile(): void
{
// Get the path to the new secret key file.
$filePath = $this->getPasswordFilePath();
// I can't get the path to the file. Sorry.
if (empty($filePath))
{
return;
}
$randval = new Randval();
$secretKey = $randval->getRandomPassword(64);
$constantName = $this->getConstantName();
$fileContent = "<?" . 'ph' . "p\n\n";
$fileContent .= <<< END
defined('_JEXEC') or die;
/**
* This file is automatically generated. It contains a secret key used for encrypting data by the component. Please do
* not remove, edit or manually replace this file. It will render your existing encrypted data unreadable forever.
*/
define('$constantName', '$secretKey');
END;
$this->container->filesystem->fileWrite($filePath, $fileContent);
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt;
defined('_JEXEC') || die();
/**
* Generates cryptographically-secure random values.
*/
class Randval implements RandvalInterface
{
/**
* Returns a cryptographically secure random value.
*
* Since we only run on PHP 7+ we can use random_bytes(), which internally uses a crypto safe PRNG. If the function
* doesn't exist, Joomla already loads a secure polyfill.
*
* The reason this method exists is backwards compatibility with older versions of FOF. It also allows us to quickly
* address any future issues if Joomla drops the polyfill or otherwise find problems with PHP's random_bytes() on
* some weird host (you can't be too carefull when releasing mass-distributed software).
*
* @param integer $bytes How many bytes to return
*
* @return string
*/
public function generate(int $bytes = 32): string
{
return random_bytes($bytes);
}
/**
* Return a randomly generated password using safe characters (a-z, A-Z, 0-9).
*
* @param int $length How many characters long should the password be. Default is 64.
*
* @return string
*
* @since 3.3.2
*/
public function getRandomPassword($length = 64)
{
$salt = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$base = strlen($salt);
$makepass = '';
/*
* Start with a cryptographic strength random string, then convert it to
* a string with the numeric base of the salt.
* Shift the base conversion on each character so the character
* distribution is even, and randomize the start shift so it's not
* predictable.
*/
$random = $this->generate($length + 1);
$shift = ord($random[0]);
for ($i = 1; $i <= $length; ++$i)
{
$makepass .= $salt[($shift + ord($random[$i])) % $base];
$shift += ord($random[$i]);
}
return $makepass;
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt;
defined('_JEXEC') || die();
interface RandvalInterface
{
/**
* Returns a cryptographically secure random value.
*
* @param int $bytes How many random bytes do you want to be returned?
*
* @return string
*/
public function generate(int $bytes = 32): string;
}

View File

@ -0,0 +1,186 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Encrypt;
defined('_JEXEC') || die;
class Totp
{
/**
* @var int The length of the resulting passcode (default: 6 digits)
*/
private $passCodeLength = 6;
/**
* @var number The PIN modulo. It is set automatically to log10(passCodeLength)
*/
private $pinModulo;
/**
* The length of the secret key, in characters (default: 10)
*
* @var int
*/
private $secretLength = 10;
/**
* The time step between successive TOTPs in seconds (default: 30 seconds)
*
* @var int
*/
private $timeStep = 30;
/**
* The Base32 encoder class
*
* @var Base32|null
*/
private $base32;
/**
* Initialises an RFC6238-compatible TOTP generator. Please note that this
* class does not implement the constraint in the last paragraph of §5.2
* of RFC6238. It's up to you to ensure that the same user/device does not
* retry validation within the same Time Step.
*
* @param int $timeStep The Time Step (in seconds). Use 30 to be compatible with Google Authenticator.
* @param int $passCodeLength The generated passcode length. Default: 6 digits.
* @param int $secretLength The length of the secret key. Default: 10 bytes (80 bits).
* @param Base32 $base32 The base32 en/decrypter
*/
public function __construct(int $timeStep = 30, int $passCodeLength = 6, int $secretLength = 10, Base32 $base32 = null)
{
$this->timeStep = $timeStep;
$this->passCodeLength = $passCodeLength;
$this->secretLength = $secretLength;
$this->pinModulo = 10 ** $this->passCodeLength;
$this->base32 = is_null($base32) ? new Base32() : $base32;
}
/**
* Get the time period based on the $time timestamp and the Time Step
* defined. If $time is skipped or set to null the current timestamp will
* be used.
*
* @param int|null $time Timestamp
*
* @return int The time period since the UNIX Epoch
*/
public function getPeriod(?int $time = null): int
{
if (is_null($time))
{
$time = time();
}
return floor($time / $this->timeStep);
}
/**
* Check is the given passcode $code is a valid TOTP generated using secret
* key $secret
*
* @param string $secret The Base32-encoded secret key
* @param string $code The passcode to check
* @param int $time The time to check it against. Leave null to check for the current server time.
*
* @return boolean True if the code is valid
*/
public function checkCode(string $secret, string $code, int $time = null): bool
{
$time = $this->getPeriod($time);
for ($i = -1; $i <= 1; $i++)
{
if ($this->getCode($secret, ($time + $i) * $this->timeStep) === $code)
{
return true;
}
}
return false;
}
/**
* Gets the TOTP passcode for a given secret key $secret and a given UNIX
* timestamp $time
*
* @param string $secret The Base32-encoded secret key
* @param int $time UNIX timestamp
*
* @return string
*/
public function getCode(string $secret, ?int $time = null): string
{
$period = $this->getPeriod($time);
$secret = $this->base32->decode($secret);
$time = pack("N", $period);
$time = str_pad($time, 8, chr(0), STR_PAD_LEFT);
$hash = hash_hmac('sha1', $time, $secret, true);
$offset = ord(substr($hash, -1));
$offset &= 0xF;
$truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
return str_pad($truncatedHash % $this->pinModulo, $this->passCodeLength, "0", STR_PAD_LEFT);
}
/**
* Returns a QR code URL for easy setup of TOTP apps like Google Authenticator
*
* @param string $user User
* @param string $hostname Hostname
* @param string $secret Secret string
*
* @return string
*/
public function getUrl(string $user, string $hostname, string $secret): string
{
$url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret);
$encoder = "https://chart.googleapis.com/chart?chs=200x200&chld=Q|2&cht=qr&chl=";
return $encoder . urlencode($url);
}
/**
* Generates a (semi-)random Secret Key for TOTP generation
*
* @return string
*/
public function generateSecret(): string
{
$secret = "";
for ($i = 1; $i <= $this->secretLength; $i++)
{
$c = random_int(0, 255);
$secret .= pack("c", $c);
}
return $this->base32->encode($secret);
}
/**
* Extracts a part of a hash as an integer
*
* @param string $bytes The hash
* @param string $start The char to start from (0 = first char)
*
* @return string
*/
protected function hashToInt(string $bytes, string $start): string
{
$input = substr($bytes, $start, strlen($bytes) - $start);
$val2 = unpack("N", substr($input, 0, 4));
return $val2[1];
}
}

View File

@ -0,0 +1,291 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Event;
defined('_JEXEC') || die;
use FOF40\Container\Container;
class Dispatcher implements Observable
{
/** @var Container The container this event dispatcher is attached to */
protected $container;
/** @var array The observers attached to the dispatcher */
protected $observers = [];
/** @var array Maps events to observers */
protected $events = [];
/**
* Public constructor
*
* @param Container $container The container this event dispatcher is attached to
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Returns the container this event dispatcher is attached to
*
* @return Container
*/
public function getContainer(): Container
{
return $this->container;
}
/**
* Attaches an observer to the object
*
* @param Observer $observer The observer to attach
*
* @return static Ourselves, for chaining
*/
public function attach(Observer $observer)
{
$className = get_class($observer);
// Make sure this observer is not already registered
if (isset($this->observers[$className]))
{
return $this;
}
// Attach observer
$this->observers[$className] = $observer;
// Register the observable events
$events = $observer->getObservableEvents();
foreach ($events as $event)
{
$event = strtolower($event);
if (!isset($this->events[$event]))
{
$this->events[$event] = [$className];
}
else
{
$this->events[$event][] = $className;
}
}
return $this;
}
/**
* Detaches an observer from the object
*
* @param Observer $observer The observer to detach
*
* @return static Ourselves, for chaining
*/
public function detach(Observer $observer)
{
$className = get_class($observer);
// Make sure this observer is already registered
if (!isset($this->observers[$className]))
{
return $this;
}
// Unregister the observable events
$events = $observer->getObservableEvents();
foreach ($events as $event)
{
$event = strtolower($event);
if (isset($this->events[$event]))
{
$key = array_search($className, $this->events[$event]);
if ($key !== false)
{
unset($this->events[$event][$key]);
if (empty($this->events[$event]))
{
unset ($this->events[$event]);
}
}
}
}
// Detach observer
unset($this->observers[$className]);
return $this;
}
/**
* Is an observer object already registered with this dispatcher?
*
* @param Observer $observer The observer to check if it's attached
*
* @return boolean
*/
public function hasObserver(Observer $observer): bool
{
$className = get_class($observer);
return $this->hasObserverClass($className);
}
/**
* Is there an observer of the specified class already registered with this dispatcher?
*
* @param string $className The observer class name to check if it's attached
*
* @return boolean
*/
public function hasObserverClass(string $className): bool
{
return isset($this->observers[$className]);
}
/**
* Returns an observer attached to this behaviours dispatcher by its class name
*
* @param string $className The class name of the observer object to return
*
* @return null|Observer
*/
public function getObserverByClass(string $className): ?Observer
{
if (!$this->hasObserverClass($className))
{
return null;
}
return $this->observers[$className];
}
/**
* Triggers an event in the attached observers
*
* @param string $event The event to attach
* @param array $args Arguments to the event handler
*
* @return array
*/
public function trigger(string $event, array $args = []): array
{
$event = strtolower($event);
$result = [];
// Make sure the event is known to us, otherwise return an empty array
if (!isset($this->events[$event]) || empty($this->events[$event]))
{
return $result;
}
foreach ($this->events[$event] as $className)
{
// Make sure the observer exists.
if (!isset($this->observers[$className]))
{
continue;
}
// Get the observer
$observer = $this->observers[$className];
// Make sure the method exists
if (!method_exists($observer, $event))
{
continue;
}
$result[] = $observer->{$event}(...$args);
}
// Return the observers' result in an array
return $result;
}
/**
* Asks each observer to handle an event based on the provided arguments. The first observer to return a non-null
* result wins. This is a *very* simplistic implementation of the Chain of Command pattern.
*
* @param string $event The event name to handle
* @param array $args The arguments to the event
*
* @return mixed Null if the event can't be handled by any observer
*/
public function chainHandle(string $event, array $args = [])
{
$event = strtolower($event);
$result = null;
// Make sure the event is known to us, otherwise return an empty array
if (!isset($this->events[$event]) || empty($this->events[$event]))
{
return $result;
}
foreach ($this->events[$event] as $className)
{
// Make sure the observer exists.
if (!isset($this->observers[$className]))
{
continue;
}
// Get the observer
$observer = $this->observers[$className];
// Make sure the method exists
if (!method_exists($observer, $event))
{
continue;
}
// Call the event handler and add its output to the return value. The switch allows for execution up to 2x
// faster than using call_user_func_array
switch (count($args))
{
case 0:
$result = $observer->{$event}();
break;
case 1:
$result = $observer->{$event}($args[0]);
break;
case 2:
$result = $observer->{$event}($args[0], $args[1]);
break;
case 3:
$result = $observer->{$event}($args[0], $args[1], $args[2]);
break;
case 4:
$result = $observer->{$event}($args[0], $args[1], $args[2], $args[3]);
break;
case 5:
$result = $observer->{$event}($args[0], $args[1], $args[2], $args[3], $args[4]);
break;
default:
$result = call_user_func_array([$observer, $event], $args);
break;
}
if (!is_null($result))
{
return $result;
}
}
// Return the observers' result in an array
return $result;
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Event;
defined('_JEXEC') || die;
/**
* Interface Observable
*
* @codeCoverageIgnore
*/
interface Observable
{
/**
* Attaches an observer to the object
*
* @param Observer $observer The observer to attach
*
* @return static Ourselves, for chaining
*/
public function attach(Observer $observer);
/**
* Detaches an observer from the object
*
* @param Observer $observer The observer to detach
*
* @return static Ourselves, for chaining
*/
public function detach(Observer $observer);
/**
* Triggers an event in the attached observers
*
* @param string $event The event to attach
* @param array $args Arguments to the event handler
*
* @return array
*/
public function trigger(string $event, array $args = []): array;
}

View File

@ -0,0 +1,70 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Event;
defined('_JEXEC') || die;
use ReflectionMethod;
use ReflectionObject;
class Observer
{
/** @var Observable The object to observe */
protected $subject;
protected $events;
/**
* Creates the observer and attaches it to the observable subject object
*
* @param Observable $subject The observable object to attach the observer to
*/
function __construct(Observable &$subject)
{
// Attach this observer to the subject
$subject->attach($this);
// Store a reference to the subject object
$this->subject = $subject;
}
/**
* Returns the list of events observable by this observer. Set the $this->events array manually for faster
* processing, or let this method use reflection to return a list of all public methods.
*
* @return array
*/
public function getObservableEvents()
{
if (is_null($this->events))
{
// Assign an empty array to protect us from behaviours without any valid method
$this->events = [];
$reflection = new ReflectionObject($this);
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $m)
{
if ($m->name == 'getObservableEvents')
{
continue;
}
if ($m->name == '__construct')
{
continue;
}
$this->events[] = $m->name;
}
}
return $this->events;
}
}

View File

@ -0,0 +1,386 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory;
defined('_JEXEC') || die;
use Exception;
use FOF40\Container\Container;
use FOF40\Controller\Controller;
use FOF40\Dispatcher\Dispatcher;
use FOF40\Factory\Exception\ControllerNotFound;
use FOF40\Factory\Exception\DispatcherNotFound;
use FOF40\Factory\Exception\ModelNotFound;
use FOF40\Factory\Exception\ToolbarNotFound;
use FOF40\Factory\Exception\TransparentAuthenticationNotFound;
use FOF40\Factory\Exception\ViewNotFound;
use FOF40\Model\Model;
use FOF40\Toolbar\Toolbar;
use FOF40\TransparentAuthentication\TransparentAuthentication;
use FOF40\View\View;
use FOF40\View\ViewTemplateFinder;
use RuntimeException;
/**
* MVC object factory. This implements the basic functionality, i.e. creating MVC objects only if the classes exist in
* the same component section (front-end, back-end) you are currently running in. The Dispatcher and Toolbar will be
* created from default objects if specialised classes are not found in your application.
*/
class BasicFactory implements FactoryInterface
{
/** @var Container The container we belong to */
protected $container;
/**
* Section used to build the namespace prefix. We have to pass it since in CLI we need
* to force the section we're in (ie Site or Admin). {@see \FOF40\Container\Container::getNamespacePrefix() } for
* valid values
*
* @var string
*/
protected $section = 'auto';
/**
* Public constructor for the factory object
*
* @param Container $container The container we belong to
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Create a new Controller object
*
* @param string $viewName The name of the view we're getting a Controller for.
* @param array $config Optional MVC configuration values for the Controller object.
*
* @return Controller
*/
public function controller(string $viewName, array $config = []): Controller
{
$controllerClass = $this->container->getNamespacePrefix($this->getSection()) . 'Controller\\' . ucfirst($viewName);
try
{
return $this->createController($controllerClass, $config);
}
catch (ControllerNotFound $e)
{
}
$controllerClass = $this->container->getNamespacePrefix($this->getSection()) . 'Controller\\' . ucfirst($this->container->inflector->singularize($viewName));
return $this->createController($controllerClass, $config);
}
/**
* Create a new Model object
*
* @param string $viewName The name of the view we're getting a Model for.
* @param array $config Optional MVC configuration values for the Model object.
*
* @return Model
*/
public function model(string $viewName, array $config = []): Model
{
$modelClass = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\' . ucfirst($viewName);
try
{
return $this->createModel($modelClass, $config);
}
catch (ModelNotFound $e)
{
}
$modelClass = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\' . ucfirst($this->container->inflector->singularize($viewName));
return $this->createModel($modelClass, $config);
}
/**
* Create a new View object
*
* @param string $viewName The name of the view we're getting a View object for.
* @param string $viewType The type of the View object. By default it's "html".
* @param array $config Optional MVC configuration values for the View object.
*
* @return View
*/
public function view(string $viewName, $viewType = 'html', array $config = []): View
{
$container = $this->container;
$prefix = $this->container->getNamespacePrefix($this->getSection());
$viewClass = $prefix . 'View\\' . ucfirst($viewName) . '\\' . ucfirst($viewType);
try
{
return $this->createView($viewClass, $config);
}
catch (ViewNotFound $e)
{
}
$viewClass = $prefix . 'View\\' . ucfirst($container->inflector->singularize($viewName)) . '\\' . ucfirst($viewType);
return $this->createView($viewClass, $config);
}
/**
* Creates a new Dispatcher
*
* @param array $config The configuration values for the Dispatcher object
*
* @return Dispatcher
*/
public function dispatcher(array $config = []): Dispatcher
{
$dispatcherClass = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\Dispatcher';
try
{
return $this->createDispatcher($dispatcherClass, $config);
}
catch (DispatcherNotFound $e)
{
// Not found. Return the default Dispatcher
return new Dispatcher($this->container, $config);
}
}
/**
* Creates a new Toolbar
*
* @param array $config The configuration values for the Toolbar object
*
* @return Toolbar
*/
public function toolbar(array $config = []): Toolbar
{
$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'Toolbar\\Toolbar';
try
{
return $this->createToolbar($toolbarClass, $config);
}
catch (ToolbarNotFound $e)
{
// Not found. Return the default Toolbar
return new Toolbar($this->container, $config);
}
}
/**
* Creates a new TransparentAuthentication handler
*
* @param array $config The configuration values for the TransparentAuthentication object
*
* @return TransparentAuthentication
*/
public function transparentAuthentication(array $config = []): TransparentAuthentication
{
$authClass = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\TransparentAuthentication';
try
{
return $this->createTransparentAuthentication($authClass, $config);
}
catch (TransparentAuthenticationNotFound $e)
{
// Not found. Return the default TA
return new TransparentAuthentication($this->container, $config);
}
}
/**
* Creates a view template finder object for a specific View
*
* The default configuration is:
* Look for .php, .blade.php files; default layout "default"; no default sub-template;
* look only for the specified view; do NOT fall back to the default layout or sub-template;
* look for templates ONLY in site or admin, depending on where we're running from
*
* @param View $view The view this view template finder will be attached to
* @param array $config Configuration variables for the object
*
* @return ViewTemplateFinder
*
* @throws Exception
*/
public function viewFinder(View $view, array $config = []): ViewTemplateFinder
{
// Initialise the configuration with the default values
$defaultConfig = [
'extensions' => ['.php', '.blade.php'],
'defaultLayout' => 'default',
'defaultTpl' => '',
'strictView' => true,
'strictTpl' => true,
'strictLayout' => true,
'sidePrefix' => 'auto',
];
$config = array_merge($defaultConfig, $config);
// Apply fof.xml overrides
$appConfig = $this->container->appConfig;
$key = "views." . ucfirst($view->getName()) . ".config";
$fofXmlConfig = [
'extensions' => $appConfig->get("$key.templateExtensions", $config['extensions']),
'strictView' => $appConfig->get("$key.templateStrictView", $config['strictView']),
'strictTpl' => $appConfig->get("$key.templateStrictTpl", $config['strictTpl']),
'strictLayout' => $appConfig->get("$key.templateStrictLayout", $config['strictLayout']),
'sidePrefix' => $appConfig->get("$key.templateLocation", $config['sidePrefix']),
];
$config = array_merge($config, $fofXmlConfig);
// Create the new view template finder object
return new ViewTemplateFinder($view, $config);
}
/**
* @return string
*/
public function getSection(): string
{
return $this->section;
}
/**
* @param string $section
*/
public function setSection(string $section): void
{
$this->section = $section;
}
/**
* Creates a Controller object
*
* @param string $controllerClass The fully qualified class name for the Controller
* @param array $config Optional MVC configuration values for the Controller object.
*
* @return Controller
*
* @throws RuntimeException If the $controllerClass does not exist
*/
protected function createController(string $controllerClass, array $config = []): Controller
{
if (!class_exists($controllerClass))
{
throw new ControllerNotFound($controllerClass);
}
return new $controllerClass($this->container, $config);
}
/**
* Creates a Model object
*
* @param string $modelClass The fully qualified class name for the Model
* @param array $config Optional MVC configuration values for the Model object.
*
* @return Model
*
* @throws RuntimeException If the $modelClass does not exist
*/
protected function createModel(string $modelClass, array $config = []): Model
{
if (!class_exists($modelClass))
{
throw new ModelNotFound($modelClass);
}
return new $modelClass($this->container, $config);
}
/**
* Creates a View object
*
* @param string $viewClass The fully qualified class name for the View
* @param array $config Optional MVC configuration values for the View object.
*
* @return View
*
* @throws RuntimeException If the $viewClass does not exist
*/
protected function createView(string $viewClass, array $config = []): View
{
if (!class_exists($viewClass))
{
throw new ViewNotFound($viewClass);
}
return new $viewClass($this->container, $config);
}
/**
* Creates a Toolbar object
*
* @param string $toolbarClass The fully qualified class name for the Toolbar
* @param array $config The configuration values for the Toolbar object
*
* @return Toolbar
*
* @throws RuntimeException If the $toolbarClass does not exist
*/
protected function createToolbar(string $toolbarClass, array $config = []): Toolbar
{
if (!class_exists($toolbarClass))
{
throw new ToolbarNotFound($toolbarClass);
}
return new $toolbarClass($this->container, $config);
}
/**
* Creates a Dispatcher object
*
* @param string $dispatcherClass The fully qualified class name for the Dispatcher
* @param array $config The configuration values for the Dispatcher object
*
* @return Dispatcher
*
* @throws RuntimeException If the $dispatcherClass does not exist
*/
protected function createDispatcher(string $dispatcherClass, array $config = []): Dispatcher
{
if (!class_exists($dispatcherClass))
{
throw new DispatcherNotFound($dispatcherClass);
}
return new $dispatcherClass($this->container, $config);
}
/**
* Creates a TransparentAuthentication object
*
* @param string $authClass The fully qualified class name for the TransparentAuthentication
* @param array $config The configuration values for the TransparentAuthentication object
*
* @return TransparentAuthentication
*
* @throws RuntimeException If the $authClass does not exist
*/
protected function createTransparentAuthentication(string $authClass, array $config): TransparentAuthentication
{
if (!class_exists($authClass))
{
throw new TransparentAuthenticationNotFound($authClass);
}
return new $authClass($this->container, $config);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
class ControllerNotFound extends RuntimeException
{
public function __construct(string $controller, int $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_CONTROLLER_ERR_NOT_FOUND', $controller);
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
class DispatcherNotFound extends RuntimeException
{
public function __construct(string $dispatcherClass, int $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_DISPATCHER_ERR_NOT_FOUND', $dispatcherClass);
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
class ModelNotFound extends RuntimeException
{
public function __construct(string $modelClass, int $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_NOT_FOUND', $modelClass);
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
class ToolbarNotFound extends RuntimeException
{
public function __construct(string $toolbarClass, int $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_TOOLBAR_ERR_NOT_FOUND', $toolbarClass);
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
class TransparentAuthenticationNotFound extends RuntimeException
{
public function __construct(string $taClass, int $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_TRANSPARENTAUTH_ERR_NOT_FOUND', $taClass);
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;
class ViewNotFound extends RuntimeException
{
public function __construct(string $viewClass, int $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_VIEW_ERR_NOT_FOUND', $viewClass);
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Controller\Controller;
use FOF40\Dispatcher\Dispatcher;
use FOF40\Model\Model;
use FOF40\Toolbar\Toolbar;
use FOF40\TransparentAuthentication\TransparentAuthentication;
use FOF40\View\View;
use FOF40\View\ViewTemplateFinder;
/**
* Interface for the MVC object factory
*/
interface FactoryInterface
{
/**
* Public constructor for the factory object
*
* @param Container $container The container we belong to
*/
public function __construct(Container $container);
/**
* Create a new Controller object
*
* @param string $viewName The name of the view we're getting a Controller for.
* @param array $config Optional MVC configuration values for the Controller object.
*
* @return Controller
*/
public function controller(string $viewName, array $config = []): Controller;
/**
* Create a new Model object
*
* @param string $viewName The name of the view we're getting a Model for.
* @param array $config Optional MVC configuration values for the Model object.
*
* @return Model
*/
public function model(string $viewName, array $config = []): Model;
/**
* Create a new View object
*
* @param string $viewName The name of the view we're getting a View object for.
* @param string $viewType The type of the View object. By default it's "html".
* @param array $config Optional MVC configuration values for the View object.
*
* @return View
*/
public function view(string $viewName, $viewType = 'html', array $config = []): View;
/**
* Creates a new Toolbar
*
* @param array $config The configuration values for the Toolbar object
*
* @return Toolbar
*/
public function toolbar(array $config = []): Toolbar;
/**
* Creates a new Dispatcher
*
* @param array $config The configuration values for the Dispatcher object
*
* @return Dispatcher
*/
public function dispatcher(array $config = []): Dispatcher;
/**
* Creates a new TransparentAuthentication handler
*
* @param array $config The configuration values for the TransparentAuthentication object
*
* @return TransparentAuthentication
*/
public function transparentAuthentication(array $config = []): TransparentAuthentication;
/**
* Creates a view template finder object for a specific View
*
* @param View $view The view this view template finder will be attached to
* @param array $config Configuration variables for the object
*
* @return ViewTemplateFinder
*/
public function viewFinder(View $view, array $config = []): ViewTemplateFinder;
/**
* @return string
*/
public function getSection(): string;
/**
* @param string $section
*/
public function setSection(string $section): void;
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Magic;
defined('_JEXEC') || die;
use FOF40\Container\Container;
abstract class BaseFactory
{
/**
* @var Container|null The container where this factory belongs to
*/
protected $container;
/**
* Section used to build the namespace prefix. We have to pass it since in CLI we need
* to force the section we're in (ie Site or Admin). {@see \FOF40\Container\Container::getNamespacePrefix() } for
* valid values
*
* @var string
*/
protected $section = 'auto';
/**
* Public constructor
*
* @param Container $container The container we belong to
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* @return string
*/
public function getSection(): string
{
return $this->section;
}
/**
* @param string $section
*/
public function setSection(string $section): void
{
$this->section = $section;
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Magic;
defined('_JEXEC') || die;
use FOF40\Controller\DataController;
use FOF40\Factory\Exception\ControllerNotFound;
/**
* Creates a DataController object instance based on the information provided by the fof.xml configuration file
*/
class ControllerFactory extends BaseFactory
{
/**
* Create a new object instance
*
* @param string $name The name of the class we're making
* @param array $config The config parameters which override the fof.xml information
*
* @return DataController A new DataController object
*/
public function make(string $name = null, array $config = []): DataController
{
if (empty($name))
{
throw new ControllerNotFound($name);
}
$appConfig = $this->container->appConfig;
$name = ucfirst($name);
$defaultConfig = [
'name' => $name,
'default_task' => $appConfig->get("views.$name.config.default_task", 'main'),
'autoRouting' => $appConfig->get("views.$name.config.autoRouting", 1),
'csrfProtection' => $appConfig->get("views.$name.config.csrfProtection", 2),
'viewName' => $appConfig->get("views.$name.config.viewName", null),
'modelName' => $appConfig->get("views.$name.config.modelName", null),
'taskPrivileges' => $appConfig->get("views.$name.acl"),
'cacheableTasks' => $appConfig->get("views.$name.config.cacheableTasks", [
'browse',
'read',
]),
'taskMap' => $appConfig->get("views.$name.taskmap"),
];
$config = array_merge($defaultConfig, $config);
$className = $this->container->getNamespacePrefix($this->getSection()) . 'Controller\\DefaultDataController';
if (!class_exists($className, true))
{
$className = 'FOF40\\Controller\\DataController';
}
$controller = new $className($this->container, $config);
$taskMap = $config['taskMap'];
if (is_array($taskMap) && !empty($taskMap))
{
foreach ($taskMap as $virtualTask => $method)
{
$controller->registerTask($virtualTask, $method);
}
}
return $controller;
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Magic;
defined('_JEXEC') || die;
use FOF40\Dispatcher\Dispatcher;
/**
* Creates a Dispatcher object instance based on the information provided by the fof.xml configuration file
*/
class DispatcherFactory extends BaseFactory
{
/**
* Create a new object instance
*
* @param array $config The config parameters which override the fof.xml information
*
* @return Dispatcher A new Dispatcher object
*/
public function make(array $config = []): Dispatcher
{
$appConfig = $this->container->appConfig;
$defaultConfig = $appConfig->get('dispatcher.*');
$config = array_merge($defaultConfig, $config);
$className = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\DefaultDispatcher';
if (!class_exists($className, true))
{
$className = '\\FOF40\\Dispatcher\\Dispatcher';
}
return new $className($this->container, $config);
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Magic;
defined('_JEXEC') || die;
use FOF40\Factory\Exception\ModelNotFound;
use FOF40\Model\DataModel;
use FOF40\Model\TreeModel;
/**
* Creates a DataModel/TreeModel object instance based on the information provided by the fof.xml configuration file
*/
class ModelFactory extends BaseFactory
{
/**
* Create a new object instance
*
* @param string $name The name of the class we're making
* @param array $config The config parameters which override the fof.xml information
*
* @return TreeModel|DataModel A new TreeModel or DataModel object
*/
public function make(string $name = null, array $config = []): DataModel
{
if (empty($name))
{
throw new ModelNotFound($name);
}
$appConfig = $this->container->appConfig;
$name = ucfirst($name);
$defaultConfig = [
'name' => $name,
'use_populate' => $appConfig->get("models.$name.config.use_populate"),
'ignore_request' => $appConfig->get("models.$name.config.ignore_request"),
'tableName' => $appConfig->get("models.$name.config.tbl"),
'idFieldName' => $appConfig->get("models.$name.config.tbl_key"),
'knownFields' => $appConfig->get("models.$name.config.knownFields", null),
'autoChecks' => $appConfig->get("models.$name.config.autoChecks"),
'contentType' => $appConfig->get("models.$name.config.contentType"),
'fieldsSkipChecks' => $appConfig->get("models.$name.config.fieldsSkipChecks", []),
'aliasFields' => $appConfig->get("models.$name.field", []),
'behaviours' => $appConfig->get("models.$name.behaviors", []),
'fillable_fields' => $appConfig->get("models.$name.config.fillable_fields", []),
'guarded_fields' => $appConfig->get("models.$name.config.guarded_fields", []),
'relations' => $appConfig->get("models.$name.relations", []),
];
$config = array_merge($defaultConfig, $config);
// Get the default class names
$dataModelClassName = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\DefaultDataModel';
if (!class_exists($dataModelClassName, true))
{
$dataModelClassName = '\\FOF40\\Model\\DataModel';
}
$treeModelClassName = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\DefaultTreeModel';
if (!class_exists($treeModelClassName, true))
{
$treeModelClassName = '\\FOF40\\Model\\TreeModel';
}
try
{
// First try creating a TreeModel
$model = new $treeModelClassName($this->container, $config);
}
catch (DataModel\Exception\TreeIncompatibleTable $e)
{
// If the table isn't a nested set, create a regular DataModel
$model = new $dataModelClassName($this->container, $config);
}
return $model;
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Magic;
defined('_JEXEC') || die;
use FOF40\TransparentAuthentication\TransparentAuthentication;
/**
* Creates a TransparentAuthentication object instance based on the information provided by the fof.xml configuration
* file
*/
class TransparentAuthenticationFactory extends BaseFactory
{
/**
* Create a new object instance
*
* @param array $config The config parameters which override the fof.xml information
*
* @return TransparentAuthentication A new TransparentAuthentication object
*/
public function make(array $config = []): TransparentAuthentication
{
$appConfig = $this->container->appConfig;
$defaultConfig = $appConfig->get('authentication.*');
$config = array_merge($defaultConfig, $config);
$className = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\DefaultTransparentAuthentication';
if (!class_exists($className, true))
{
$className = '\\FOF40\\TransparentAuthentication\\TransparentAuthentication';
}
return new $className($this->container, $config);
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory\Magic;
defined('_JEXEC') || die;
use FOF40\Factory\Exception\ViewNotFound;
use FOF40\View\View;
/**
* Creates a DataModel/TreeModel object instance based on the information provided by the fof.xml configuration file
*/
class ViewFactory extends BaseFactory
{
/**
* Create a new object instance
*
* @param string $name The name of the class we're making
* @param string $viewType The view type, default html, possible values html, form, raw, json, csv
* @param array $config The config parameters which override the fof.xml information
*
* @return View A DataViewInterface view
*/
public function make(string $name = null, string $viewType = 'html', array $config = []): View
{
if (empty($name))
{
throw new ViewNotFound("[name : type] = [$name : $viewType]");
}
$appConfig = $this->container->appConfig;
$name = ucfirst($name);
$defaultConfig = [
'name' => $name,
'template_path' => $appConfig->get("views.$name.config.template_path"),
'layout' => $appConfig->get("views.$name.config.layout"),
// You can pass something like .php => Class1, .foo.bar => Class 2
'viewEngineMap' => $appConfig->get("views.$name.config.viewEngineMap"),
];
$config = array_merge($defaultConfig, $config);
$className = $this->container->getNamespacePrefix($this->getSection()) . 'View\\DataView\\Default' . ucfirst($viewType);
if (!class_exists($className, true))
{
$className = '\\FOF40\\View\\DataView\\' . ucfirst($viewType);
}
if (!class_exists($className, true))
{
$className = $this->container->getNamespacePrefix($this->getSection()) . 'View\\DataView\\DefaultHtml';
}
if (!class_exists($className))
{
$className = '\\FOF40\\View\\DataView\\Html';
}
return new $className($this->container, $config);
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory;
defined('_JEXEC') || die;
use FOF40\Controller\Controller;
use FOF40\Dispatcher\Dispatcher;
use FOF40\Factory\Exception\ControllerNotFound;
use FOF40\Factory\Exception\DispatcherNotFound;
use FOF40\Factory\Exception\ModelNotFound;
use FOF40\Factory\Exception\TransparentAuthenticationNotFound;
use FOF40\Factory\Exception\ViewNotFound;
use FOF40\Factory\Magic\DispatcherFactory;
use FOF40\Factory\Magic\TransparentAuthenticationFactory;
use FOF40\Model\Model;
use FOF40\Toolbar\Toolbar;
use FOF40\TransparentAuthentication\TransparentAuthentication;
use FOF40\View\View;
/**
* Magic MVC object factory. This factory will "magically" create MVC objects even if the respective classes do not
* exist, based on information in your fof.xml file.
*
* Note: This factory class will ONLY look for MVC objects in the same component section (front-end, back-end) you are
* currently running in. If they are not found a new one will be created magically.
*/
class MagicFactory extends BasicFactory implements FactoryInterface
{
/**
* Create a new Controller object
*
* @param string $viewName The name of the view we're getting a Controller for.
* @param array $config Optional MVC configuration values for the Controller object.
*
* @return Controller
*/
public function controller(string $viewName, array $config = []): Controller
{
try
{
return parent::controller($viewName, $config);
}
catch (ControllerNotFound $e)
{
$magic = new Magic\ControllerFactory($this->container);
return $magic->make($viewName, $config);
}
}
/**
* Create a new Model object
*
* @param string $viewName The name of the view we're getting a Model for.
* @param array $config Optional MVC configuration values for the Model object.
*
* @return Model
*/
public function model(string $viewName, array $config = []): Model
{
try
{
return parent::model($viewName, $config);
}
catch (ModelNotFound $e)
{
$magic = new Magic\ModelFactory($this->container);
return $magic->make($viewName, $config);
}
}
/**
* Create a new View object
*
* @param string $viewName The name of the view we're getting a View object for.
* @param string $viewType The type of the View object. By default it's "html".
* @param array $config Optional MVC configuration values for the View object.
*
* @return View
*/
public function view(string $viewName, $viewType = 'html', array $config = []): View
{
try
{
return parent::view($viewName, $viewType, $config);
}
catch (ViewNotFound $e)
{
$magic = new Magic\ViewFactory($this->container);
return $magic->make($viewName, $viewType, $config);
}
}
/**
* Creates a new Toolbar
*
* @param array $config The configuration values for the Toolbar object
*
* @return Toolbar
*/
public function toolbar(array $config = []): Toolbar
{
$appConfig = $this->container->appConfig;
$defaultConfig = [
'useConfigurationFile' => true,
'renderFrontendButtons' => in_array($appConfig->get("views.*.config.renderFrontendButtons"), [
true, 'true', 'yes', 'on', 1,
]),
'renderFrontendSubmenu' => in_array($appConfig->get("views.*.config.renderFrontendSubmenu"), [
true, 'true', 'yes', 'on', 1,
]),
];
$config = array_merge($defaultConfig, $config);
return parent::toolbar($config);
}
public function dispatcher(array $config = []): Dispatcher
{
$dispatcherClass = $this->container->getNamespacePrefix() . 'Dispatcher\\Dispatcher';
try
{
return $this->createDispatcher($dispatcherClass, $config);
}
catch (DispatcherNotFound $e)
{
// Not found. Return the magically created Dispatcher
$magic = new DispatcherFactory($this->container);
return $magic->make($config);
}
}
/**
* Creates a new TransparentAuthentication handler
*
* @param array $config The configuration values for the TransparentAuthentication object
*
* @return TransparentAuthentication
*/
public function transparentAuthentication(array $config = []): TransparentAuthentication
{
$authClass = $this->container->getNamespacePrefix() . 'TransparentAuthentication\\TransparentAuthentication';
try
{
return $this->createTransparentAuthentication($authClass, $config);
}
catch (TransparentAuthenticationNotFound $e)
{
// Not found. Return the magically created TA
$magic = new TransparentAuthenticationFactory($this->container);
return $magic->make($config);
}
}
}

View File

@ -0,0 +1,208 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory;
defined('_JEXEC') || die;
use FOF40\Controller\Controller;
use FOF40\Dispatcher\Dispatcher;
use FOF40\Factory\Exception\ControllerNotFound;
use FOF40\Factory\Exception\DispatcherNotFound;
use FOF40\Factory\Exception\ModelNotFound;
use FOF40\Factory\Exception\TransparentAuthenticationNotFound;
use FOF40\Factory\Exception\ViewNotFound;
use FOF40\Factory\Magic\DispatcherFactory;
use FOF40\Factory\Magic\TransparentAuthenticationFactory;
use FOF40\Model\Model;
use FOF40\Toolbar\Toolbar;
use FOF40\TransparentAuthentication\TransparentAuthentication;
use FOF40\View\View;
/**
* Magic MVC object factory. This factory will "magically" create MVC objects even if the respective classes do not
* exist, based on information in your fof.xml file.
*
* Note: This factory class will look for MVC objects in BOTH component sections (front-end, back-end), not just the one
* you are currently running in. If no class is found a new object will be created magically. This is the same behaviour
* as FOF 2.x.
*/
class MagicSwitchFactory extends SwitchFactory implements FactoryInterface
{
/**
* Create a new Controller object
*
* @param string $viewName The name of the view we're getting a Controller for.
* @param array $config Optional MVC configuration values for the Controller object.
*
* @return Controller
*/
public function controller(string $viewName, array $config = []): Controller
{
try
{
return parent::controller($viewName, $config);
}
catch (ControllerNotFound $e)
{
$magic = new Magic\ControllerFactory($this->container);
// Let's pass the section override (if any)
$magic->setSection($this->getSection());
return $magic->make($viewName, $config);
}
}
/**
* Create a new Model object
*
* @param string $viewName The name of the view we're getting a Model for.
* @param array $config Optional MVC configuration values for the Model object.
*
* @return Model
*/
public function model(string $viewName, array $config = []): Model
{
try
{
return parent::model($viewName, $config);
}
catch (ModelNotFound $e)
{
$magic = new Magic\ModelFactory($this->container);
// Let's pass the section override (if any)
$magic->setSection($this->getSection());
return $magic->make($viewName, $config);
}
}
/**
* Create a new View object
*
* @param string $viewName The name of the view we're getting a View object for.
* @param string $viewType The type of the View object. By default it's "html".
* @param array $config Optional MVC configuration values for the View object.
*
* @return View
*/
public function view(string $viewName, $viewType = 'html', array $config = []): View
{
try
{
return parent::view($viewName, $viewType, $config);
}
catch (ViewNotFound $e)
{
$magic = new Magic\ViewFactory($this->container);
// Let's pass the section override (if any)
$magic->setSection($this->getSection());
return $magic->make($viewName, $viewType, $config);
}
}
/**
* Creates a new Toolbar
*
* @param array $config The configuration values for the Toolbar object
*
* @return Toolbar
*/
public function toolbar(array $config = []): Toolbar
{
$appConfig = $this->container->appConfig;
$defaultConfig = [
'useConfigurationFile' => true,
'renderFrontendButtons' => in_array($appConfig->get("views.*.config.renderFrontendButtons"), [
true, 'true', 'yes', 'on', 1,
]),
'renderFrontendSubmenu' => in_array($appConfig->get("views.*.config.renderFrontendSubmenu"), [
true, 'true', 'yes', 'on', 1,
]),
];
$config = array_merge($defaultConfig, $config);
return parent::toolbar($config);
}
/**
* Creates a new Dispatcher
*
* @param array $config The configuration values for the Dispatcher object
*
* @return Dispatcher
*/
public function dispatcher(array $config = []): Dispatcher
{
$dispatcherClass = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\Dispatcher';
try
{
return $this->createDispatcher($dispatcherClass, $config);
}
catch (DispatcherNotFound $e)
{
// Not found. Let's go on.
}
$dispatcherClass = $this->container->getNamespacePrefix('inverse') . 'Dispatcher\\Dispatcher';
try
{
return $this->createDispatcher($dispatcherClass, $config);
}
catch (DispatcherNotFound $e)
{
// Not found. Return the magically created Dispatcher
$magic = new DispatcherFactory($this->container);
// Let's pass the section override (if any)
$magic->setSection($this->getSection());
return $magic->make($config);
}
}
/**
* Creates a new TransparentAuthentication
*
* @param array $config The configuration values for the TransparentAuthentication object
*
* @return TransparentAuthentication
*/
public function transparentAuthentication(array $config = []): TransparentAuthentication
{
$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\TransparentAuthentication';
try
{
return $this->createTransparentAuthentication($toolbarClass, $config);
}
catch (TransparentAuthenticationNotFound $e)
{
// Not found. Let's go on.
}
$toolbarClass = $this->container->getNamespacePrefix('inverse') . 'TransparentAuthentication\\TransparentAuthentication';
try
{
return $this->createTransparentAuthentication($toolbarClass, $config);
}
catch (TransparentAuthenticationNotFound $e)
{
// Not found. Return the magically created TransparentAuthentication
$magic = new TransparentAuthenticationFactory($this->container);
// Let's pass the section override (if any)
$magic->setSection($this->getSection());
return $magic->make($config);
}
}
}

View File

@ -0,0 +1,266 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Factory;
defined('_JEXEC') || die;
use Exception;
use FOF40\Controller\Controller;
use FOF40\Dispatcher\Dispatcher;
use FOF40\Factory\Exception\ControllerNotFound;
use FOF40\Factory\Exception\DispatcherNotFound;
use FOF40\Factory\Exception\ModelNotFound;
use FOF40\Factory\Exception\ToolbarNotFound;
use FOF40\Factory\Exception\TransparentAuthenticationNotFound;
use FOF40\Factory\Exception\ViewNotFound;
use FOF40\Model\Model;
use FOF40\Toolbar\Toolbar;
use FOF40\TransparentAuthentication\TransparentAuthentication;
use FOF40\View\View;
use FOF40\View\ViewTemplateFinder;
/**
* MVC object factory. This implements the advanced functionality, i.e. creating MVC objects only if the classes exist
* in any component section (front-end, back-end). For example, if you're in the front-end and a Model class doesn't
* exist there but does exist in the back-end then the back-end class will be returned.
*
* The Dispatcher and Toolbar will be created from default objects if specialised classes are not found in your application.
*/
class SwitchFactory extends BasicFactory implements FactoryInterface
{
/**
* Create a new Controller object
*
* @param string $viewName The name of the view we're getting a Controller for.
* @param array $config Optional MVC configuration values for the Controller object.
*
* @return Controller
*/
public function controller(string $viewName, array $config = []): Controller
{
try
{
return parent::controller($viewName, $config);
}
catch (ControllerNotFound $e)
{
}
$controllerClass = $this->container->getNamespacePrefix('inverse') . 'Controller\\' . ucfirst($viewName);
try
{
return $this->createController($controllerClass, $config);
}
catch (ControllerNotFound $e)
{
}
$controllerClass = $this->container->getNamespacePrefix('inverse') . 'Controller\\' . ucfirst($this->container->inflector->singularize($viewName));
return $this->createController($controllerClass, $config);
}
/**
* Create a new Model object
*
* @param string $viewName The name of the view we're getting a Model for.
* @param array $config Optional MVC configuration values for the Model object.
*
* @return Model
*/
public function model(string $viewName, array $config = []): Model
{
try
{
return parent::model($viewName, $config);
}
catch (ModelNotFound $e)
{
}
$modelClass = $this->container->getNamespacePrefix('inverse') . 'Model\\' . ucfirst($viewName);
try
{
return $this->createModel($modelClass, $config);
}
catch (ModelNotFound $e)
{
$modelClass = $this->container->getNamespacePrefix('inverse') . 'Model\\' . ucfirst($this->container->inflector->singularize($viewName));
return $this->createModel($modelClass, $config);
}
}
/**
* Create a new View object
*
* @param string $viewName The name of the view we're getting a View object for.
* @param string $viewType The type of the View object. By default it's "html".
* @param array $config Optional MVC configuration values for the View object.
*
* @return View
*/
public function view(string $viewName, $viewType = 'html', array $config = []): View
{
try
{
return parent::view($viewName, $viewType, $config);
}
catch (ViewNotFound $e)
{
}
$viewClass = $this->container->getNamespacePrefix('inverse') . 'View\\' . ucfirst($viewName) . '\\' . ucfirst($viewType);
try
{
return $this->createView($viewClass, $config);
}
catch (ViewNotFound $e)
{
$viewClass = $this->container->getNamespacePrefix('inverse') . 'View\\' . ucfirst($this->container->inflector->singularize($viewName)) . '\\' . ucfirst($viewType);
return $this->createView($viewClass, $config);
}
}
/**
* Creates a new Dispatcher
*
* @param array $config The configuration values for the Dispatcher object
*
* @return Dispatcher
*/
public function dispatcher(array $config = []): Dispatcher
{
$dispatcherClass = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\Dispatcher';
try
{
return $this->createDispatcher($dispatcherClass, $config);
}
catch (DispatcherNotFound $e)
{
// Not found. Let's go on.
}
$dispatcherClass = $this->container->getNamespacePrefix('inverse') . 'Dispatcher\\Dispatcher';
try
{
return $this->createDispatcher($dispatcherClass, $config);
}
catch (DispatcherNotFound $e)
{
// Not found. Return the default Dispatcher
return new Dispatcher($this->container, $config);
}
}
/**
* Creates a new Toolbar
*
* @param array $config The configuration values for the Toolbar object
*
* @return Toolbar
*/
public function toolbar(array $config = []): Toolbar
{
$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'Toolbar\\Toolbar';
try
{
return $this->createToolbar($toolbarClass, $config);
}
catch (ToolbarNotFound $e)
{
// Not found. Let's go on.
}
$toolbarClass = $this->container->getNamespacePrefix('inverse') . 'Toolbar\\Toolbar';
try
{
return $this->createToolbar($toolbarClass, $config);
}
catch (ToolbarNotFound $e)
{
// Not found. Return the default Toolbar
return new Toolbar($this->container, $config);
}
}
/**
* Creates a new TransparentAuthentication
*
* @param array $config The configuration values for the TransparentAuthentication object
*
* @return TransparentAuthentication
*/
public function transparentAuthentication(array $config = []): TransparentAuthentication
{
$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\TransparentAuthentication';
try
{
return $this->createTransparentAuthentication($toolbarClass, $config);
}
catch (TransparentAuthenticationNotFound $e)
{
// Not found. Let's go on.
}
$toolbarClass = $this->container->getNamespacePrefix('inverse') . 'TransparentAuthentication\\TransparentAuthentication';
try
{
return $this->createTransparentAuthentication($toolbarClass, $config);
}
catch (TransparentAuthenticationNotFound $e)
{
// Not found. Return the default TransparentAuthentication
return new TransparentAuthentication($this->container, $config);
}
}
/**
* Creates a view template finder object for a specific View.
*
* The default configuration is:
* Look for .php, .blade.php files; default layout "default"; no default sub-template;
* look for both pluralised and singular views; fall back to the default layout without sub-template;
* look for templates in both site and admin
*
* @param View $view The view this view template finder will be attached to
* @param array $config Configuration variables for the object
*
* @return mixed
*
* @throws Exception
*/
public function viewFinder(View $view, array $config = []): ViewTemplateFinder
{
// Initialise the configuration with the default values
$defaultConfig = [
'extensions' => ['.php', '.blade.php'],
'defaultLayout' => 'default',
'defaultTpl' => '',
'strictView' => false,
'strictTpl' => false,
'strictLayout' => false,
'sidePrefix' => 'any',
];
$config = array_merge($defaultConfig, $config);
return parent::viewFinder($view, $config);
}
}

View File

@ -0,0 +1,923 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Html\FEFHelper;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Html\SelectOptions;
use FOF40\Model\DataModel;
use FOF40\Utils\ArrayHelper;
use FOF40\View\DataView\DataViewInterface;
use FOF40\View\View;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
/**
* An HTML helper for Browse views.
*
* It reintroduces a FEF-friendly of some of the functionality found in FOF 3's Header and Field classes. These
* helpers are also accessible through Blade, making the transition from XML forms to Blade templates easier.
*
* @since 3.3.0
*/
abstract class BrowseView
{
/**
* Caches the results of getOptionsFromModel keyed by a hash. The hash is computed by the model
* name, the model state and the options passed to getOptionsFromModel.
*
* @var array
*/
private static $cacheModelOptions = [];
/**
* Get the translation key for a field's label
*
* @param string $fieldName The field name
*
* @return string
*
* @since 3.3.0
*/
public static function fieldLabelKey(string $fieldName): string
{
$view = self::getViewFromBacktrace();
try
{
$inflector = $view->getContainer()->inflector;
$viewName = $inflector->singularize($view->getName());
$altViewName = $inflector->pluralize($view->getName());
$componentName = $view->getContainer()->componentName;
$keys = [
strtoupper($componentName . '_' . $viewName . '_FIELD_' . $fieldName),
strtoupper($componentName . '_' . $altViewName . '_FIELD_' . $fieldName),
strtoupper($componentName . '_' . $viewName . '_' . $fieldName),
strtoupper($componentName . '_' . $altViewName . '_' . $fieldName),
];
foreach ($keys as $key)
{
if (Text::_($key) !== $key)
{
return $key;
}
}
return $keys[0];
}
catch (\Exception $e)
{
return ucfirst($fieldName);
}
}
/**
* Returns the label for a field (translated)
*
* @param string $fieldName The field name
*
* @return string
*/
public static function fieldLabel(string $fieldName): string
{
return Text::_(self::fieldLabelKey($fieldName));
}
/**
* Return a table field header which sorts the table by that field upon clicking
*
* @param string $field The name of the field
* @param string|null $langKey (optional) The language key for the header to be displayed
*
* @return string
*/
public static function sortgrid(string $field, ?string $langKey = null): string
{
/** @var DataViewInterface $view */
$view = self::getViewFromBacktrace();
if (is_null($langKey))
{
$langKey = self::fieldLabelKey($field);
}
return HTMLHelper::_('FEFHelp.browse.sort', $langKey, $field, $view->getLists()->order_Dir, $view->getLists()->order, $view->getTask());
}
/**
* Create a browse view filter from values returned by a model
*
* @param string $localField Field name
* @param string $modelTitleField Foreign model field for drop-down display values
* @param null $modelName Foreign model name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function modelFilter(string $localField, string $modelTitleField = 'title', ?string $modelName = null,
?string $placeholder = null, array $params = []): string
{
/** @var DataModel $model */
$model = self::getViewFromBacktrace()->getModel();
if (empty($modelName))
{
$modelName = $model->getForeignModelNameFor($localField);
}
if (is_null($placeholder))
{
$placeholder = self::fieldLabelKey($localField);
}
$params = array_merge([
'list.none' => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
'value_field' => $modelTitleField,
'fof.autosubmit' => true,
], $params);
return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
}
/**
* Display a text filter (search box)
*
* @param string $localField The name of the model field. Used when getting the filter state.
* @param string $searchField The INPUT element's name. Default: "filter_$localField".
* @param string $placeholder The Text language key for the placeholder. Default: extrapolate from $localField.
* @param array $attributes HTML attributes for the INPUT element.
*
* @return string
*
* @since 3.3.0
*/
public static function searchFilter(string $localField, ?string $searchField = null, ?string $placeholder = null,
array $attributes = []): string
{
/** @var DataModel $model */
$view = self::getViewFromBacktrace();
$model = $view->getModel();
$searchField = empty($searchField) ? $localField : $searchField;
$placeholder = empty($placeholder) ? self::fieldLabelKey($localField) : $placeholder;
$attributes['type'] = $attributes['type'] ?? 'text';
$attributes['name'] = $searchField;
$attributes['id'] = !isset($attributes['id']) ? "filter_$localField" : $attributes['id'];
$attributes['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder'];
$attributes['title'] = $attributes['title'] ?? $attributes['placeholder'];
$attributes['value'] = $view->escape($model->getState($localField));
if (!isset($attributes['onchange']))
{
$attributes['class'] = trim(($attributes['class'] ?? '') . ' akeebaCommonEventsOnChangeSubmit');
$attributes['data-akeebasubmittarget'] = $attributes['data-akeebasubmittarget'] ?? 'adminForm';
}
// Remove null attributes and collapse into a string
$attributes = array_filter($attributes, function ($v) {
return !is_null($v);
});
$attributes = ArrayHelper::toString($attributes);
return "<input $attributes />";
}
/**
* Create a browse view filter with dropdown values
*
* @param string $localField Field name
* @param array $options The HTMLHelper options list to use
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function selectFilter(string $localField, array $options, ?string $placeholder = null,
array $params = []): string
{
/** @var DataModel $model */
$model = self::getViewFromBacktrace()->getModel();
if (is_null($placeholder))
{
$placeholder = self::fieldLabelKey($localField);
}
$params = array_merge([
'list.none' => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
'fof.autosubmit' => true,
], $params);
return self::genericSelect($localField, $options, $model->getState($localField), $params);
}
/**
* View access dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function accessFilter(string $localField, ?string $placeholder = null, array $params = []): string
{
return self::selectFilter($localField, SelectOptions::getOptions('access', $params), $placeholder, $params);
}
/**
* Published state dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function publishedFilter(string $localField, ?string $placeholder = null, array $params = []): string
{
return self::selectFilter($localField, SelectOptions::getOptions('published', $params), $placeholder, $params);
}
/**
* Create a select box from the values returned by a model
*
* @param string $name Field name
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param mixed $currentValue The currently selected value
* @param array $params Passed to optionsFromModel and genericSelect
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
* values
*
* @return string
*
* @see self::getOptionsFromModel
* @see self::getOptionsFromSource
* @see self::genericSelect
*
* @since 3.3.0
*/
public static function modelSelect(string $name, string $modelName, $currentValue, array $params = [],
array $modelState = [], array $options = []): string
{
$params = array_merge([
'fof.autosubmit' => true,
], $params);
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
return self::genericSelect($name, $options, $currentValue, $params);
}
/**
* Get a (human readable) title from a (typically numeric, foreign key) key value using the data
* returned by a DataModel.
*
* @param string $value The key value
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param array $params Passed to getOptionsFromModel
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
* values
*
* @return string
*
* @see self::getOptionsFromModel
* @see self::getOptionsFromSource
* @see self::genericSelect
*
* @since 3.3.0
*/
public static function modelOptionName(string $value, ?string $modelName = null, array $params = [],
array $modelState = [], array $options = []): ?string
{
if (!isset($params['cache']))
{
$params['cache'] = true;
}
if (!isset($params['none_as_zero']))
{
$params['none_as_zero'] = true;
}
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
return self::getOptionName($value, $options);
}
/**
* Gets the active option's label given an array of HTMLHelper options
*
* @param mixed $selected The currently selected value
* @param array $data The HTMLHelper options to parse
* @param string $optKey Key name, default: value
* @param string $optText Value name, default: text
* @param bool $selectFirst Should I automatically select the first option? Default: true
*
* @return mixed The label of the currently selected option
*/
public static function getOptionName($selected, array $data, string $optKey = 'value', string $optText = 'text', bool $selectFirst = true): ?string
{
$ret = null;
foreach ($data as $elementKey => &$element)
{
if (is_array($element))
{
$key = $optKey === null ? $elementKey : $element[$optKey];
$text = $element[$optText];
}
elseif (is_object($element))
{
$key = $optKey === null ? $elementKey : $element->$optKey;
$text = $element->$optText;
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
if (is_null($ret) && $selectFirst && ($selected == $key))
{
$ret = $text;
}
elseif ($selected == $key)
{
$ret = $text;
}
}
return $ret;
}
/**
* Create a generic select list based on a bunch of options. Option sources will be merged into the provided
* options automatically.
*
* Parameters:
* - format.depth The current indent depth.
* - format.eol The end of line string, default is linefeed.
* - format.indent The string to use for indentation, default is tab.
* - groups If set, looks for keys with the value "<optgroup>" and synthesizes groups from them. Deprecated.
* Default: true.
* - list.select Either the value of one selected option or an array of selected options. Default: $currentValue.
* - list.translate If true, text and labels are translated via Text::_(). Default is false.
* - list.attr HTML element attributes (key/value array or string)
* - list.none Placeholder for no selection (creates an option with an empty string key)
* - option.id The property in each option array to use as the selection id attribute. Defaults: null.
* - option.key The property in each option array to use as the Default: "value". If set to null, the index of the
* option array is used.
* - option.label The property in each option array to use as the selection label attribute. Default: null
* - option.text The property in each option array to use as the displayed text. Default: "text". If set to null,
* the option array is assumed to be a list of displayable scalars.
* - option.attr The property in each option array to use for additional selection attributes. Defaults: null.
* - option.disable: The property that will hold the disabled state. Defaults to "disable".
* - fof.autosubmit Should I auto-submit the form on change? Default: true
* - fof.formname Form to auto-submit. Default: adminForm
* - class CSS class to apply
* - size Size attribute for the input
* - multiple Is this a multiple select? Default: false.
* - required Is this a required field? Default: false.
* - autofocus Should I focus this field automatically? Default: false
* - disabled Is this a disabled field? Default: false
* - readonly Render as a readonly field with hidden inputs? Overrides 'disabled'. Default: false
* - onchange Custom onchange handler. Overrides fof.autosubmit. Default: NULL (use fof.autosubmit).
*
* @param string $name
* @param array $options
* @param mixed $currentValue
* @param array $params
*
* @return string
*
* @since 3.3.0
*/
public static function genericSelect(string $name, array $options, $currentValue, array $params = []): string
{
$params = array_merge([
'format.depth' => 0,
'format.eol' => "\n",
'format.indent' => "\t",
'groups' => true,
'list.select' => $currentValue,
'list.translate' => false,
'option.id' => null,
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
'option.attr' => null,
'option.disable' => 'disable',
'list.attr' => '',
'list.none' => '',
'id' => null,
'fof.autosubmit' => true,
'fof.formname' => 'adminForm',
'class' => '',
'size' => '',
'multiple' => false,
'required' => false,
'autofocus' => false,
'disabled' => false,
'onchange' => null,
'readonly' => false,
], $params);
$currentValue = $params['list.select'];
$classes = $params['class'] ?? '';
$classes = is_array($classes) ? implode(' ', $classes) : $classes;
// If fof.autosubmit is enabled and onchange is not set we will add our own handler
if ($params['fof.autosubmit'] && is_null($params['onchange']))
{
$formName = $params['fof.formname'] ?: 'adminForm';
$classes .= ' akeebaCommonEventsOnChangeSubmit';
$params['data-akeebasubmittarget'] = $formName;
}
// Construct SELECT element's attributes
$attr = [
'class' => trim($classes) ?: null,
'size' => ($params['size'] ?? null) ?: null,
'multiple' => ($params['multiple'] ?? null) ?: null,
'required' => ($params['required'] ?? false) ?: null,
'aria-required' => ($params['required'] ?? false) ? 'true' : null,
'autofocus' => ($params['autofocus'] ?? false) ?: null,
'disabled' => (($params['disabled'] ?? false) || ($params['readonly'] ?? false)) ?: null,
'onchange' => $params['onchange'] ?? null,
];
$attr = array_filter($attr, function ($x) {
return !is_null($x);
});
// We merge the constructed SELECT element's attributes with the 'list.attr' array, if it was provided
$params['list.attr'] = array_merge($attr, (($params['list.attr'] ?? []) ?: []));
// Merge the options with those fetched from a source (e.g. another Helper object)
$options = array_merge($options, self::getOptionsFromSource($params));
if (!empty($params['list.none']))
{
array_unshift($options, HTMLHelper::_('FEFHelp.select.option', '', Text::_($params['list.none'])));
}
$html = [];
// Create a read-only list (no name) with hidden input(s) to store the value(s).
if ($params['readonly'])
{
$html[] = HTMLHelper::_('FEFHelp.select.genericlist', $options, $name, $params);
// E.g. form field type tag sends $this->value as array
if ($params['multiple'] && is_array($currentValue))
{
if (count($currentValue) === 0)
{
$currentValue[] = '';
}
foreach ($currentValue as $value)
{
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
}
}
else
{
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
}
}
else
// Create a regular list.
{
$html[] = HTMLHelper::_('FEFHelp.select.genericlist', $options, $name, $params);
}
return implode($html);
}
/**
* Replace tags that reference fields with their values
*
* @param string $text Text to process
* @param DataModel $item The DataModel instance to get values from
*
* @return string Text with tags replace
*
* @since 3.3.0
*/
public static function parseFieldTags(string $text, DataModel $item): string
{
$ret = $text;
if (empty($item))
{
return $ret;
}
/**
* Replace [ITEM:ID] in the URL with the item's key value (usually: the auto-incrementing numeric ID)
*/
$replace = $item->getId();
$ret = str_replace('[ITEM:ID]', $replace, $ret);
// Replace the [ITEMID] in the URL with the current Itemid parameter
$ret = str_replace('[ITEMID]', $item->getContainer()->input->getInt('Itemid', 0), $ret);
// Replace the [TOKEN] in the URL with the Joomla! form token
$ret = str_replace('[TOKEN]', $item->getContainer()->platform->getToken(true), $ret);
// Replace other field variables in the URL
$data = $item->getData();
foreach ($data as $field => $value)
{
// Skip non-processable values
if (is_array($value) || is_object($value))
{
continue;
}
$search = '[ITEM:' . strtoupper($field) . ']';
$ret = str_replace($search, $value, $ret);
}
return $ret;
}
/**
* Get the FOF View from the backtrace of the static call. MAGIC!
*
* @return View
*
* @since 3.3.0
*/
public static function getViewFromBacktrace(): View
{
// In case we are on a brain-dead host
if (!function_exists('debug_backtrace'))
{
throw new \RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
}
/**
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
* will expand my search by another 2 entries and so on until I either find a container or I stop
* finding new call stack entries.
*/
$lastNumberOfEntries = 0;
$limit = 4;
$skip = 0;
$container = null;
while (true)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
if (count($backtrace) === $lastNumberOfEntries)
{
throw new \RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
$lastNumberOfEntries = count($backtrace);
if ($skip)
{
$backtrace = array_slice($backtrace, $skip);
}
foreach ($backtrace as $bt)
{
if (!isset($bt['object']))
{
continue;
}
if ($bt['object'] instanceof View)
{
return $bt['object'];
}
}
$skip = $limit;
$limit += 2;
}
}
/**
* Get HTMLHelper options from an alternate source, e.g. a helper. This is useful for adding arbitrary options
* which are either dynamic or you do not want to inline to your view, e.g. reusable options across
* different views.
*
* The attribs can be:
* source_file The file to load. You can use FOF's URIs such as 'admin:com_foobar/foo/bar'
* source_class The class to use
* source_method The static method to use on source_class
* source_key Use * if you're returning a key/value array. Otherwise the array key for the key (ID)
* value.
* source_value Use * if you're returning a key/value array. Otherwise the array key for the displayed
* value. source_translate Should I pass the value field through Text? Default: true source_format Set
* to "optionsobject" if you're returning an array of HTMLHelper options. Ignored otherwise.
*
* @param array $attribs
*
* @return array
*
* @since 3.3.0
*/
private static function getOptionsFromSource(array $attribs = []): array
{
$options = [];
$container = self::getContainerFromBacktrace();
$attribs = array_merge([
'source_file' => '',
'source_class' => '',
'source_method' => '',
'source_key' => '*',
'source_value' => '*',
'source_translate' => true,
'source_format' => '',
], $attribs);
$source_file = $attribs['source_file'];
$source_class = $attribs['source_class'];
$source_method = $attribs['source_method'];
$source_key = $attribs['source_key'];
$source_value = $attribs['source_value'];
$source_translate = $attribs['source_translate'];
$source_format = $attribs['source_format'];
if ($source_class && $source_method)
{
// Maybe we have to load a file?
if (!empty($source_file))
{
$source_file = $container->template->parsePath($source_file, true);
if ($container->filesystem->fileExists($source_file))
{
include $source_file;
}
}
// Make sure the class exists
// ...and so does the option
if (class_exists($source_class, true) && in_array($source_method, get_class_methods($source_class)))
{
// Get the data from the class
if ($source_format == 'optionsobject')
{
$options = array_merge($options, $source_class::$source_method());
}
else
{
$source_data = $source_class::$source_method();
// Loop through the data and prime the $options array
foreach ($source_data as $k => $v)
{
$key = (empty($source_key) || ($source_key == '*')) ? $k : @$v[$source_key];
$value = (empty($source_value) || ($source_value == '*')) ? $v : @$v[$source_value];
if ($source_translate)
{
$value = Text::_($value);
}
$options[] = HTMLHelper::_('FEFHelp.select.option', $key, $value, 'value', 'text');
}
}
}
}
reset($options);
return $options;
}
/**
* Get HTMLHelper options from the values returned by a model.
*
* The params can be:
* key_field The model field used for the OPTION's key. Default: the model's ID field.
* value_field The model field used for the OPTION's displayed value. You must provide it.
* apply_access Should I apply Joomla ACLs to the model? Default: FALSE.
* none Placeholder for no selection. Default: NULL (no placeholder).
* none_as_zero When true, the 'none' placeholder applies to values '' **AND** '0' (empty string and zero)
* translate Should I pass the values through Text? Default: TRUE.
* with Array of relation names for eager loading.
* cache Cache the results for faster reuse
*
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param array $params Parameters which define which options to get from the model
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
* values
*
* @return mixed
*
* @since 3.3.0
*/
private static function getOptionsFromModel(string $modelName, array $params = [], array $modelState = [],
array $options = []): array
{
// Let's find the FOF DI container from the call stack
$container = self::getContainerFromBacktrace();
// Explode model name into component name and prefix
$componentName = $container->componentName;
$mName = $modelName;
if (strpos($modelName, '.') !== false)
{
[$componentName, $mName] = explode('.', $mName, 2);
}
if ($componentName !== $container->componentName)
{
$container = Container::getInstance($componentName);
}
/** @var DataModel $model */
$model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false);
$defaultParams = [
'key_field' => $model->getKeyName(),
'value_field' => 'title',
'apply_access' => false,
'none' => null,
'none_as_zero' => false,
'translate' => true,
'with' => [],
];
$params = array_merge($defaultParams, $params);
$cache = isset($params['cache']) && $params['cache'];
$cacheKey = null;
if ($cache)
{
$cacheKey = sha1(print_r([
$model->getContainer()->componentName,
$model->getName(),
$params['key_field'],
$params['value_field'],
$params['apply_access'],
$params['none'],
$params['translate'],
$params['with'],
$modelState,
], true));
}
if ($cache && isset(self::$cacheModelOptions[$cacheKey]))
{
return self::$cacheModelOptions[$cacheKey];
}
if (empty($params['none']) && !is_null($params['none']))
{
$langKey = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName());
$placeholder = Text::_($langKey);
if ($langKey !== $placeholder)
{
$params['none'] = '&mdash; ' . $placeholder . ' &mdash;';
}
}
if (!empty($params['none']))
{
$options[] = HTMLHelper::_('FEFHelp.select.option', null, Text::_($params['none']));
if ($params['none_as_zero'])
{
$options[] = HTMLHelper::_('FEFHelp.select.option', 0, Text::_($params['none']));
}
}
if ($params['apply_access'])
{
$model->applyAccessFiltering();
}
if (!is_null($params['with']))
{
$model->with($params['with']);
}
// Set the model's state, if applicable
foreach ($modelState as $stateKey => $stateValue)
{
$model->setState($stateKey, $stateValue);
}
// Set the query and get the result list.
$items = $model->get(true);
foreach ($items as $item)
{
$value = $item->{$params['value_field']};
if ($params['translate'])
{
$value = Text::_($value);
}
$options[] = HTMLHelper::_('FEFHelp.select.option', $item->{$params['key_field']}, $value);
}
if ($cache)
{
self::$cacheModelOptions[$cacheKey] = $options;
}
return $options;
}
/**
* Get the FOF DI container from the backtrace of the static call. MAGIC!
*
* @return Container
*
* @since 3.3.0
*/
private static function getContainerFromBacktrace(): Container
{
// In case we are on a brain-dead host
if (!function_exists('debug_backtrace'))
{
throw new \RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
}
/**
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
* will expand my search by another 2 entries and so on until I either find a container or I stop
* finding new call stack entries.
*/
$lastNumberOfEntries = 0;
$limit = 4;
$skip = 0;
$container = null;
while (true)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
if (count($backtrace) === $lastNumberOfEntries)
{
throw new \RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
$lastNumberOfEntries = count($backtrace);
if ($skip !== 0)
{
$backtrace = array_slice($backtrace, $skip);
}
foreach ($backtrace as $bt)
{
if (!isset($bt['object']))
{
continue;
}
if (!method_exists($bt['object'], 'getContainer'))
{
continue;
}
return $bt['object']->getContainer();
}
$skip = $limit;
$limit += 2;
}
}
}

View File

@ -0,0 +1,867 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use FOF40\Html\FEFHelper\BrowseView;
use FOF40\Model\DataModel;
use FOF40\Utils\ArrayHelper;
use FOF40\View\DataView\DataViewInterface;
use FOF40\View\DataView\Raw as DataViewRaw;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Pagination\Pagination;
/**
* Custom JHtml (HTMLHelper) class. Offers browse view controls compatible with Akeeba Frontend
* Framework (FEF).
*
* Call these methods as HTMLHelper::_('FEFHelp.browse.methodName', $parameter1, $parameter2, ...)
*
* @noinspection PhpIllegalPsrClassPathInspection
*/
abstract class FEFHelpBrowse
{
/**
* Returns an action button on the browse view's table
*
* @param integer $i The row index
* @param string $task The task to fire when the button is clicked
* @param string|array $prefix An optional task prefix or an array of options
* @param string $active_title An optional active tooltip to display if $enable is true
* @param string $inactive_title An optional inactive tooltip to display if $enable is true
* @param boolean $tip An optional setting for tooltip
* @param string $active_class An optional active HTML class
* @param string $inactive_class An optional inactive HTML class
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function action(int $i, string $task, $prefix = '', string $active_title = '',
string $inactive_title = '', bool $tip = false,
string $active_class = '', string $inactive_class = '',
bool $enabled = true, bool $translate = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$active_title = array_key_exists('active_title', $options) ? $options['active_title'] : $active_title;
$inactive_title = array_key_exists('inactive_title', $options) ? $options['inactive_title'] : $inactive_title;
$tip = array_key_exists('tip', $options) ? $options['tip'] : $tip;
$active_class = array_key_exists('active_class', $options) ? $options['active_class'] : $active_class;
$inactive_class = array_key_exists('inactive_class', $options) ? $options['inactive_class'] : $inactive_class;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
if ($tip)
{
$title = $enabled ? $active_title : $inactive_title;
$title = $translate ? Text::_($title) : $title;
$title = HTMLHelper::_('tooltipText', $title, '', 0);
}
if ($enabled)
{
$btnColor = 'grey';
if (substr($active_class, 0, 2) == '--')
{
[$btnColor, $active_class] = explode(' ', $active_class, 2);
$btnColor = ltrim($btnColor, '-');
}
$html = [];
$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini ' . ($active_class === 'publish' ? ' active' : '') . ($tip ? ' hasTooltip' : '') . '"';
$html[] = ' href="javascript:void(0);" onclick="return Joomla.listItemTask(\'' . $checkbox . $i . '\',\'' . $prefix . $task . '\')"';
$html[] = $tip ? ' title="' . $title . '"' : '';
$html[] = '>';
$html[] = '<span class="akion-' . $active_class . '" aria-hidden="true"></span>&ensp;';
$html[] = '</a>';
return implode($html);
}
$btnColor = 'grey';
if (substr($inactive_class, 0, 2) == '--')
{
[$btnColor, $inactive_class] = explode(' ', $inactive_class, 2);
$btnColor = ltrim($btnColor, '-');
}
$html = [];
$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini disabled akeebagrid' . ($tip ? ' hasTooltip' : '') . '"';
$html[] = $tip ? ' title="' . $title . '"' : '';
$html[] = '>';
if ($active_class === 'protected')
{
$inactive_class = 'locked';
}
$html[] = '<span class="akion-' . $inactive_class . '"></span>&ensp;';
$html[] = '</a>';
return implode($html);
}
/**
* Returns a state change button on the browse view's table
*
* @param array $states array of value/state. Each state is an array of the form
* (task, text, active title, inactive title, tip (boolean), HTML active class,
* HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active
* title,
* 'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active
* class,
* 'inactive_class'=>html inactive class)
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function state(array $states, int $value, int $i, $prefix = '', bool $enabled = true,
bool $translate = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$state = ArrayHelper::getValue($states, (int) $value, $states[0]);
$task = array_key_exists('task', $state) ? $state['task'] : $state[0];
$text = array_key_exists('text', $state) ? $state['text'] : (array_key_exists(1, $state) ? $state[1] : '');
$active_title = array_key_exists('active_title', $state) ? $state['active_title'] : (array_key_exists(2, $state) ? $state[2] : '');
$inactive_title = array_key_exists('inactive_title', $state) ? $state['inactive_title'] : (array_key_exists(3, $state) ? $state[3] : '');
$tip = array_key_exists('tip', $state) ? $state['tip'] : (array_key_exists(4, $state) ? $state[4] : false);
$active_class = array_key_exists('active_class', $state) ? $state['active_class'] : (array_key_exists(5, $state) ? $state[5] : '');
$inactive_class = array_key_exists('inactive_class', $state) ? $state['inactive_class'] : (array_key_exists(6, $state) ? $state[6] : '');
return static::action(
$i, $task, $prefix, $active_title, $inactive_title, $tip,
$active_class, $inactive_class, $enabled, $translate, $checkbox
);
}
/**
* Returns a published state on the browse view's table
*
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
* @param string $publish_up An optional start publishing date.
* @param string $publish_down An optional finish publishing date.
*
* @return string The HTML markup
*
* @see self::state()
*
* @since 3.3.0
*/
public static function published(int $value, int $i, $prefix = '', bool $enabled = true, string $checkbox = 'cb',
?string $publish_up = null, ?string $publish_down = null): string
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
/**
* Format:
*
* (task, text, active title, inactive title, tip (boolean), active icon class (without akion-), inactive icon class (without akion-))
*/
$states = [
1 => [
'unpublish', 'JPUBLISHED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JPUBLISHED', true, '--green checkmark',
'--green checkmark',
],
0 => [
'publish', 'JUNPUBLISHED', 'JLIB_HTML_PUBLISH_ITEM', 'JUNPUBLISHED', true, '--red close', '--red close',
],
2 => [
'unpublish', 'JARCHIVED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JARCHIVED', true, '--orange ion-ios-box',
'--orange ion-ios-box',
],
-2 => [
'publish', 'JTRASHED', 'JLIB_HTML_PUBLISH_ITEM', 'JTRASHED', true, '--dark trash-a', '--dark trash-a',
],
];
// Special state for dates
if ($publish_up || $publish_down)
{
$nullDate = JoomlaFactory::getDbo()->getNullDate();
$nowDate = JoomlaFactory::getDate()->toUnix();
$tz = JoomlaFactory::getUser()->getTimezone();
$publish_up = (!empty($publish_up) && ($publish_up != $nullDate)) ? JoomlaFactory::getDate($publish_up, 'UTC')->setTimeZone($tz) : false;
$publish_down = (!empty($publish_down) && ($publish_down != $nullDate)) ? JoomlaFactory::getDate($publish_down, 'UTC')->setTimeZone($tz) : false;
// Create tip text, only we have publish up or down settings
$tips = [];
if ($publish_up)
{
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publish_up, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
if ($publish_down)
{
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publish_down, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
$tip = empty($tips) ? false : implode('<br />', $tips);
// Add tips and special titles
foreach (array_keys($states) as $key)
{
// Create special titles for published items
if ($key == 1)
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_ITEM';
if (!empty($publish_up) && ($publish_up != $nullDate) && $nowDate < $publish_up->toUnix())
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_PENDING_ITEM';
$states[$key][5] = $states[$key][6] = 'android-time';
}
if (!empty($publish_down) && ($publish_down != $nullDate) && $nowDate > $publish_down->toUnix())
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_EXPIRED_ITEM';
$states[$key][5] = $states[$key][6] = 'alert';
}
}
// Add tips to titles
if ($tip)
{
$states[$key][1] = Text::_($states[$key][1]);
$states[$key][2] = Text::_($states[$key][2]) . '<br />' . $tip;
$states[$key][3] = Text::_($states[$key][3]) . '<br />' . $tip;
$states[$key][4] = true;
}
}
return static::state($states, $value, $i, [
'prefix' => $prefix, 'translate' => !$tip,
], $enabled, true, $checkbox);
}
return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
}
/**
* Returns an isDefault state on the browse view's table
*
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @see self::state()
* @since 3.3.0
*/
public static function isdefault(int $value, int $i, $prefix = '', bool $enabled = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$states = [
0 => ['setDefault', '', 'JLIB_HTML_SETDEFAULT_ITEM', '', 1, 'android-star-outline', 'android-star-outline'],
1 => [
'unsetDefault', 'JDEFAULT', 'JLIB_HTML_UNSETDEFAULT_ITEM', 'JDEFAULT', 1, 'android-star',
'android-star',
],
];
return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
}
/**
* Returns a checked-out icon
*
* @param integer $i The row index.
* @param string $editorName The name of the editor.
* @param string $time The time that the object was checked out.
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled True to enable the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function checkedout(int $i, string $editorName, string $time, $prefix = '', bool $enabled = false,
string $checkbox = 'cb'): string
{
HTMLHelper::_('bootstrap.tooltip');
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$text = $editorName . '<br />' . HTMLHelper::_('date', $time, Text::_('DATE_FORMAT_LC')) . '<br />' . HTMLHelper::_('date', $time, 'H:i');
$active_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKIN'), $text, 0);
$inactive_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKED_OUT'), $text, 0);
return static::action(
$i, 'checkin', $prefix, html_entity_decode($active_title, ENT_QUOTES, 'UTF-8'),
html_entity_decode($inactive_title, ENT_QUOTES, 'UTF-8'), true, 'locked', 'locked', $enabled, false, $checkbox
);
}
/**
* Returns the drag'n'drop reordering field for Browse views
*
* @param string $orderingField The name of the field you're ordering by
* @param string $order The order value of the current row
* @param string $class CSS class for the ordering value INPUT field
* @param string $icon CSS class for the d'n'd handle icon
* @param string $inactiveIcon CSS class for the d'n'd disabled icon
* @param DataViewInterface $view The view you're rendering against. Leave null for auto-detection.
*
* @return string
*/
public static function order(string $orderingField, ?string $order, string $class = 'input-sm',
string $icon = 'akion-android-more-vertical',
string $inactiveIcon = 'akion-android-more-vertical',
DataViewInterface $view = null): string
{
$order = $order ?? 'asc';
/** @var \FOF40\View\DataView\Html $view */
if (is_null($view))
{
$view = BrowseView::getViewFromBacktrace();
}
$dndOrderingActive = $view->getLists()->order == $orderingField;
// Default inactive ordering
$html = '<span class="sortable-handler inactive" >';
$html .= '<span class="' . $icon . '"></span>';
$html .= '</span>';
// The modern drag'n'drop method
if ($view->getPerms()->editstate)
{
$disableClassName = '';
$disabledLabel = '';
// DO NOT REMOVE! It will initialize Joomla libraries and javascript functions
$hasAjaxOrderingSupport = $view->hasAjaxOrderingSupport();
if (!is_array($hasAjaxOrderingSupport) || !$hasAjaxOrderingSupport['saveOrder'])
{
$disabledLabel = Text::_('JORDERINGDISABLED');
$disableClassName = 'inactive tip-top hasTooltip';
}
$orderClass = $dndOrderingActive ? 'order-enabled' : 'order-disabled';
$html = '<div class="' . $orderClass . '">';
$html .= '<span class="sortable-handler ' . $disableClassName . '" title="' . $disabledLabel . '">';
$html .= '<span class="' . ($disableClassName ? $inactiveIcon : $icon) . '"></span>';
$html .= '</span>';
if ($dndOrderingActive)
{
$html .= '<input type="text" name="order[]" style="display: none" size="5" class="' . $class . ' text-area-order" value="' . $order . '" />';
}
$html .= '</div>';
}
return $html;
}
/**
* Returns the drag'n'drop reordering table header for Browse views
*
* @param string $orderingField The name of the field you're ordering by
* @param string $icon CSS class for the d'n'd handle icon
*
* @return string
*/
public static function orderfield(string $orderingField = 'ordering', string $icon = 'akion-stats-bars'): string
{
$title = Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN');
$orderingLabel = Text::_('JFIELD_ORDERING_LABEL');
return <<< HTML
<a href="#"
onclick="Joomla.tableOrdering('{$orderingField}','asc','');return false;"
class="hasPopover"
title="{$orderingLabel}"
data-content="{$title}"
data-placement="top"
>
<span class="{$icon}"></span>
</a>
HTML;
}
/**
* Creates an order-up action icon.
*
* @param integer $i The row index.
* @param string $task An optional task to fire.
* @param string|array $prefix An optional task prefix or an array of options
* @param string $text An optional text to display
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function orderUp(int $i, string $task = 'orderup', $prefix = '', string $text = 'JLIB_HTML_MOVE_UP',
bool $enabled = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$text = array_key_exists('text', $options) ? $options['text'] : $text;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
return static::action($i, $task, $prefix, $text, $text, false, 'arrow-up-b', 'arrow-up-b', $enabled, true, $checkbox);
}
/**
* Creates an order-down action icon.
*
* @param integer $i The row index.
* @param string $task An optional task to fire.
* @param string|array $prefix An optional task prefix or an array of options
* @param string $text An optional text to display
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function orderDown(int $i, string $task = 'orderdown', string $prefix = '',
string $text = 'JLIB_HTML_MOVE_DOWN', bool $enabled = true,
string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$text = array_key_exists('text', $options) ? $options['text'] : $text;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
return static::action($i, $task, $prefix, $text, $text, false, 'arrow-down-b', 'arrow-down-b', $enabled, true, $checkbox);
}
/**
* Table header for a field which changes the sort order when clicked
*
* @param string $title The link title
* @param string $order The order field for the column
* @param string $direction The current direction
* @param string $selected The selected ordering
* @param string $task An optional task override
* @param string $new_direction An optional direction for the new column
* @param string $tip An optional text shown as tooltip title instead of $title
* @param string $form An optional form selector
*
* @return string
*
* @since 3.3.0
*/
public static function sort(string $title, string $order, ?string $direction = 'asc', string $selected = '',
?string $task = null, string $new_direction = 'asc', string $tip = '',
?string $form = null): string
{
HTMLHelper::_('behavior.core');
HTMLHelper::_('bootstrap.popover');
$direction = strtolower($direction ?? 'asc');
$icon = ['akion-android-arrow-dropup', 'akion-android-arrow-dropdown'];
$index = (int) ($direction === 'desc');
if ($order !== $selected)
{
$direction = $new_direction;
}
else
{
$direction = $direction === 'desc' ? 'asc' : 'desc';
}
if ($form)
{
$form = ', document.getElementById(\'' . $form . '\')';
}
$html = '<a href="#" onclick="Joomla.tableOrdering(\'' . $order . '\',\'' . $direction . '\',\'' . $task . '\'' . $form . ');return false;"'
. ' class="hasPopover" title="' . htmlspecialchars(Text::_($tip ?: $title)) . '"'
. ' data-content="' . htmlspecialchars(Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN')) . '" data-placement="top">';
if (isset($title['0']) && $title['0'] === '<')
{
$html .= $title;
}
else
{
$html .= Text::_($title);
}
if ($order === $selected)
{
$html .= '<span class="' . $icon[$index] . '"></span>';
}
return $html . '</a>';
}
/**
* Method to check all checkboxes on the browse view's table
*
* @param string $name The name of the form element
* @param string $tip The text shown as tooltip title instead of $tip
* @param string $action The action to perform on clicking the checkbox
*
* @return string
*
* @since 3.3.0
*/
public static function checkall(string $name = 'checkall-toggle', string $tip = 'JGLOBAL_CHECK_ALL',
string $action = 'Joomla.checkAll(this)'): string
{
HTMLHelper::_('behavior.core');
HTMLHelper::_('bootstrap.tooltip');
return '<input type="checkbox" name="' . $name . '" value="" class="hasTooltip" title="' . HTMLHelper::_('tooltipText', $tip)
. '" onclick="' . $action . '" />';
}
/**
* Method to create a checkbox for a grid row.
*
* @param integer $rowNum The row index
* @param mixed $recId The record id
* @param boolean $checkedOut True if item is checked out
* @param string $name The name of the form element
* @param string $stub The name of stub identifier
*
* @return mixed String of html with a checkbox if item is not checked out, empty if checked out.
*
* @since 3.3.0
*/
public static function id(int $rowNum, $recId, bool $checkedOut = false, string $name = 'cid',
string $stub = 'cb'): string
{
return $checkedOut ? '' : '<input type="checkbox" id="' . $stub . $rowNum . '" name="' . $name . '[]" value="' . $recId
. '" onclick="Joomla.isChecked(this.checked);" />';
}
/**
* Include the necessary JavaScript for the browse view's table order feature
*
* @param string $orderBy Filed by which we are currently sorting the table.
* @param bool $return Should I return the JS? Default: false (= add to the page's head)
*
* @return string
*/
public static function orderjs(string $orderBy, bool $return = false): ?string
{
$js = <<< JS
Joomla.orderTable = function()
{
var table = document.getElementById("sortTable");
var direction = document.getElementById("directionTable");
var order = table.options[table.selectedIndex].value;
var dirn = 'asc';
if (order !== '$orderBy')
{
dirn = 'asc';
}
else {
dirn = direction.options[direction.selectedIndex].value;
}
Joomla.tableOrdering(order, dirn);
};
JS;
if ($return)
{
return $js;
}
try
{
JoomlaFactory::getApplication()->getDocument()->addScriptDeclaration($js);
}
catch (Exception $e)
{
// If we have no application, well, not having table sorting JS is the least of your worries...
}
return null;
}
/**
* Returns the table ordering / pagination header for a browse view: number of records to display, order direction,
* order by field.
*
* @param DataViewRaw $view The view you're rendering against. If not provided we will guess it using
* MAGIC.
* @param array $sortFields Array of field name => description for the ordering fields in the dropdown.
* If not provided we will use all the fields available in the model.
* @param Pagination $pagination The Joomla pagination object. If not provided we fetch it from the view.
* @param string $sortBy Order by field name. If not provided we fetch it from the view.
* @param string $order_Dir Order direction. If not provided we fetch it from the view.
*
* @return string
*
* @since 3.3.0
*/
public static function orderheader(DataViewRaw $view = null, array $sortFields = [], Pagination $pagination = null,
?string $sortBy = null, ?string $order_Dir = null): string
{
if (is_null($view))
{
$view = BrowseView::getViewFromBacktrace();
}
$showBrowsePagination = $view->showBrowsePagination ?? true;
$showBrowseOrdering = $view->showBrowseOrdering ?? true;
$showBrowseOrderBy = $view->showBrowseOrderBy ?? true;
if (!$showBrowsePagination && !$showBrowseOrdering && !$showBrowseOrderBy)
{
return '';
}
if (empty($sortFields))
{
/** @var DataModel $model */
$model = $view->getModel();
$sortFields = $view->getLists()->sortFields ?? [];
$sortFields = empty($sortFields) ? self::getSortFields($model) : $sortFields;
}
if (empty($pagination))
{
$pagination = $view->getPagination();
}
if (empty($sortBy))
{
$sortBy = $view->getLists()->order;
}
if (empty($order_Dir))
{
$order_Dir = $view->getLists()->order_Dir;
if (empty($order_Dir))
{
$order_Dir = 'asc';
}
}
// Static hidden text labels
$limitLabel = Text::_('JFIELD_PLG_SEARCH_SEARCHLIMIT_DESC');
$orderingDecr = Text::_('JFIELD_ORDERING_DESC');
$sortByLabel = Text::_('JGLOBAL_SORT_BY');
// Order direction dropdown
$directionSelect = HTMLHelper::_('FEFHelp.select.genericlist', [
'' => $orderingDecr,
'asc' => Text::_('JGLOBAL_ORDER_ASCENDING'),
'desc' => Text::_('JGLOBAL_ORDER_DESCENDING'),
], 'directionTable', [
'id' => 'directionTable',
'list.select' => $order_Dir,
'list.attr' => [
'class' => 'input-medium custom-select akeebaCommonEventsOnChangeOrderTable',
],
]);
// Sort by field dropdown
$sortTable = HTMLHelper::_('FEFHelp.select.genericlist', array_merge([
'' => Text::_('JGLOBAL_SORT_BY'),
], $sortFields), 'sortTable', [
'id' => 'sortTable',
'list.select' => $sortBy,
'list.attr' => [
'class' => 'input-medium custom-select akeebaCommonEventsOnChangeOrderTable',
],
]);
$html = '';
if ($showBrowsePagination)
{
$html .= <<< HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="limit" class="element-invisible">
$limitLabel
</label>
{$pagination->getLimitBox()}
</div>
HTML;
}
if ($showBrowseOrdering)
{
$html .= <<< HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="directionTable" class="element-invisible">
$orderingDecr
</label>
$directionSelect
</div>
HTML;
}
if ($showBrowseOrderBy)
{
$html .= <<< HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="sortTable" class="element-invisible">
{$sortByLabel}
</label>
$sortTable
</div>
HTML;
}
return $html;
}
/**
* Get the default sort fields from a model. It creates a hash array where the keys are the model's field names and
* the values are the translation keys for their names, following FOF's naming conventions.
*
* @param DataModel $model The model for which we get the sort fields
*
* @return array
*
* @since 3.3.0
*/
private static function getSortFields(DataModel $model): array
{
$sortFields = [];
$idField = $model->getIdFieldName() ?: 'id';
$defaultFieldLabels = [
'publish_up' => 'JGLOBAL_FIELD_PUBLISH_UP_LABEL',
'publish_down' => 'JGLOBAL_FIELD_PUBLISH_DOWN_LABEL',
'created_by' => 'JGLOBAL_FIELD_CREATED_BY_LABEL',
'created_on' => 'JGLOBAL_FIELD_CREATED_LABEL',
'modified_by' => 'JGLOBAL_FIELD_MODIFIED_BY_LABEL',
'modified_on' => 'JGLOBAL_FIELD_MODIFIED_LABEL',
'ordering' => 'JGLOBAL_FIELD_FIELD_ORDERING_LABEL',
'id' => 'JGLOBAL_FIELD_ID_LABEL',
'hits' => 'JGLOBAL_HITS',
'title' => 'JGLOBAL_TITLE',
'user_id' => 'JGLOBAL_USERNAME',
'username' => 'JGLOBAL_USERNAME',
];
$componentName = $model->getContainer()->componentName;
$viewNameSingular = $model->getContainer()->inflector->singularize($model->getName());
$viewNamePlural = $model->getContainer()->inflector->pluralize($model->getName());
foreach (array_keys($model->getFields()) as $field)
{
$possibleKeys = [
$componentName . '_' . $viewNamePlural . '_FIELD_' . $field,
$componentName . '_' . $viewNamePlural . '_' . $field,
$componentName . '_' . $viewNameSingular . '_FIELD_' . $field,
$componentName . '_' . $viewNameSingular . '_' . $field,
];
if (array_key_exists($field, $defaultFieldLabels))
{
$possibleKeys[] = $defaultFieldLabels[$field];
}
if ($field === $idField)
{
$possibleKeys[] = $defaultFieldLabels['id'];
}
$fieldLabel = '';
foreach ($possibleKeys as $langKey)
{
$langKey = strtoupper($langKey);
$fieldLabel = Text::_($langKey);
if ($fieldLabel !== $langKey)
{
break;
}
$fieldLabel = '';
}
if (!empty($fieldLabel))
{
$sortFields[$field] = (new Joomla\Filter\InputFilter())->clean($fieldLabel);
}
}
return $sortFields;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory as JoomlaFactory;
/**
* Custom JHtml (HTMLHelper) class. Offers edit (form) view controls compatible with Akeeba Frontend
* Framework (FEF).
*
* Call these methods as HTMLHelper::_('FEFHelp.edit.methodName', $parameter1, $parameter2, ...)
*/
abstract class FEFHelpEdit
{
public static function editor(string $fieldName, ?string $value, array $params = []): string
{
$params = array_merge([
'id' => null,
'editor' => null,
'width' => '100%',
'height' => 500,
'columns' => 50,
'rows' => 20,
'created_by' => null,
'asset_id' => null,
'buttons' => true,
'hide' => false,
], $params);
$editorType = $params['editor'];
if (is_null($editorType))
{
$editorType = JoomlaFactory::getConfig()->get('editor');
$user = JoomlaFactory::getUser();
if (!$user->guest)
{
$editorType = $user->getParam('editor', $editorType);
}
}
if (is_null($params['id']))
{
$params['id'] = $fieldName;
}
$editor = Editor::getInstance($editorType);
return $editor->display($fieldName, $value, $params['width'], $params['height'],
$params['columns'], $params['rows'], $params['buttons'], $params['id'],
$params['asset_id'], $params['created_by'], $params);
}
}

View File

@ -0,0 +1,882 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use FOF40\Utils\ArrayHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
/**
* Custom JHtml (HTMLHelper) class. Offers selects compatible with Akeeba Frontend Framework (FEF)
*
* Call these methods as HTMLHelper::_('FEFHelp.select.methodName', $parameter1, $parameter2, ...)
*
* @noinspection PhpIllegalPsrClassPathInspection
*/
abstract class FEFHelpSelect
{
/**
* Default values for options. Organized by option group.
*
* @var array
*/
protected static $optionDefaults = [
'option' => [
'option.attr' => null,
'option.disable' => 'disable',
'option.id' => null,
'option.key' => 'value',
'option.key.toHtml' => true,
'option.label' => null,
'option.label.toHtml' => true,
'option.text' => 'text',
'option.text.toHtml' => true,
'option.class' => 'class',
'option.onclick' => 'onclick',
],
];
/**
* Generates a yes/no radio list.
*
* @param string $name The value of the HTML name attribute
* @param array $attribs Additional HTML attributes for the `<select>` tag
* @param mixed $selected The key that is selected
* @param string $yes Language key for Yes
* @param string $no Language key for no
* @param mixed $id The id for the field or false for no id
*
* @return string HTML for the radio list
*
* @see JFormFieldRadio
*/
public static function booleanlist($name, $attribs = [], $selected = null, $yes = 'JYES', $no = 'JNO', $id = false)
{
$options = [
\Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.option', '0', \Joomla\CMS\Language\Text::_($no)),
\Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.option', '1', \Joomla\CMS\Language\Text::_($yes)),
];
$attribs = array_merge(['forSelect' => 1], $attribs);
return \Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.radiolist', $options, $name, $attribs, 'value', 'text', (int) $selected, $id);
}
/**
* Generates a searchable HTML selection list (Chosen on J3, Choices.js on J4).
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see JHtml::$formatOptions}.
* Selection options, see {@see JHtmlSelect::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
* @since 3.7.2
*/
public static function smartlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false, $translate = false)
{
$innerList = self::genericlist($data, $name, $attribs, $optKey, $optText, $selected, $idtag, $translate);
// Joomla 3: Use Chosen
if (version_compare(JVERSION, '3.999.999', 'le'))
{
HTMLHelper::_('formbehavior.chosen');
return $innerList;
}
// Joomla 4: Use the joomla-field-fancy-select using choices.js
try
{
\Joomla\CMS\Factory::getApplication()->getDocument()->getWebAssetManager()
->usePreset('choicesjs')
->useScript('webcomponent.field-fancy-select');
}
catch (Exception $e)
{
return $innerList;
}
$j4Attr = array_filter([
'class' => $attribs['class'] ?? null,
'placeholder' => $attribs['placeholder'] ?? null,
], function ($x) {
return !empty($x);
});
$dataAttribute = '';
if (isset($attribs['dataAttribute']))
{
$dataAttribute = is_string($attribs['dataAttribute']) ? $attribs['dataAttribute'] : '';
}
if ((bool) ($attribs['allowCustom'] ?? false))
{
$dataAttribute .= ' allow-custom new-item-prefix="#new#"';
}
$remoteSearchUrl = $attribs['remoteSearchURL'] ?? null;
$remoteSearch = ((bool) ($attribs['remoteSearch'] ?? false)) && !empty($remoteSearchUrl);
$termKey = $attribs['termKey'] ?? 'like';
$minTermLength = $attribs['minTermLength'] ?? 3;
if ($remoteSearch)
{
$dataAttribute .= ' remote-search';
$j4Attr['url'] = $remoteSearchUrl;
$j4Attr['term-key'] = $termKey;
$j4Attr['min-term-length'] = $minTermLength;
}
if (isset($attribs['required']))
{
$j4Attr['class'] = ($j4Attr['class'] ?? '') . ' required';
$dataAttribute .= ' required';
}
if (isset($attribs['readonly']))
{
return $innerList;
}
return sprintf("<joomla-field-fancy-select %s %s>%s</joomla-field-fancy-select>", ArrayHelper::toString($j4Attr), $dataAttribute, $innerList);
}
/**
* Generates an HTML selection list.
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see HTMLHelper::$formatOptions}.
* Selection options, see {@see HTMLHelper::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
*/
public static function genericlist(array $data, string $name, ?array $attribs = null, string $optKey = 'value',
string $optText = 'text', $selected = null, $idtag = false,
bool $translate = false): string
{
// Set default options
$options = array_merge(HTMLHelper::$formatOptions, ['format.depth' => 0, 'id' => false]);
if (is_array($attribs) && func_num_args() === 3)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
}
else
{
// Get options from the parameters
$options['id'] = $idtag;
$options['list.attr'] = $attribs;
$options['list.translate'] = $translate;
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(['[', ']', ' '], '', $id);
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
return $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol']
. static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol'];
}
/**
* Generates a grouped HTML selection list from nested arrays.
*
* @param array $data An array of groups, each of which is an array of options.
* @param string $name The value of the HTML name attribute
* @param array $options Options, an array of key/value pairs. Valid options are:
* Format options, {@see HTMLHelper::$formatOptions}.
* Selection options. See {@see HTMLHelper::options()}.
* group.id: The property in each group to use as the group id
* attribute. Defaults to none.
* group.label: The property in each group to use as the group
* label. Defaults to "text". If set to null, the data array index key is
* used.
* group.items: The property in each group to use as the array of
* items in the group. Defaults to "items". If set to null, group.id and
* group. label are forced to null and the data element is assumed to be a
* list of selections.
* id: Value to use as the select element id attribute. Defaults to
* the same as the name.
* list.attr: Attributes for the select element. Can be a string or
* an array of key/value pairs. Defaults to none.
* list.select: either the value of one selected option or an array
* of selected options. Default: none.
* list.translate: Boolean. If set, text and labels are translated via
* Text::_().
*
* @return string HTML for the select list
*
* @throws RuntimeException If a group has contents that cannot be processed.
*/
public static function groupedlist(array $data, string $name, array $options = []): string
{
// Set default options and overwrite with anything passed in
$options = array_merge(
HTMLHelper::$formatOptions,
[
'format.depth' => 0, 'group.items' => 'items', 'group.label' => 'text', 'group.label.toHtml' => true,
'id' => false,
],
$options
);
// Apply option rules
if ($options['group.items'] === null)
{
$options['group.label'] = null;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(['[', ']', ' '], '', $id);
// Disable groups in the options.
$options['groups'] = false;
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
$html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol'];
$groupIndent = str_repeat($options['format.indent'], $options['format.depth']++);
foreach ($data as $dataKey => $group)
{
$label = $dataKey;
$id = '';
$noGroup = is_int($dataKey);
if ($options['group.items'] == null)
{
// Sub-list is an associative array
$subList = $group;
}
elseif (is_array($group))
{
// Sub-list is in an element of an array.
$subList = $group[$options['group.items']];
if (isset($group[$options['group.label']]))
{
$label = $group[$options['group.label']];
$noGroup = false;
}
if (isset($options['group.id']) && isset($group[$options['group.id']]))
{
$id = $group[$options['group.id']];
$noGroup = false;
}
}
elseif (is_object($group))
{
// Sub-list is in a property of an object
$subList = $group->{$options['group.items']};
if (isset($group->{$options['group.label']}))
{
$label = $group->{$options['group.label']};
$noGroup = false;
}
if (isset($options['group.id']) && isset($group->{$options['group.id']}))
{
$id = $group->{$options['group.id']};
$noGroup = false;
}
}
else
{
throw new RuntimeException('Invalid group contents.', 1);
}
if ($noGroup)
{
$html .= static::options($subList, $options);
}
else
{
$html .= $groupIndent . '<optgroup' . (empty($id) ? '' : ' id="' . $id . '"') . ' label="'
. ($options['group.label.toHtml'] ? htmlspecialchars($label, ENT_COMPAT, 'UTF-8') : $label) . '">' . $options['format.eol']
. static::options($subList, $options) . $groupIndent . '</optgroup>' . $options['format.eol'];
}
}
return $html . ($baseIndent . '</select>' . $options['format.eol']);
}
/**
* Generates a selection list of integers.
*
* @param integer $start The start integer
* @param integer $end The end integer
* @param integer $inc The increment
* @param string $name The value of the HTML name attribute
* @param mixed $attribs Additional HTML attributes for the `<select>` tag, an array of
* attributes, or an array of options. Treated as options if it is the last
* argument passed.
* @param mixed $selected The key that is selected
* @param string $format The printf format to be applied to the number
*
* @return string HTML for the select list
*/
public static function integerlist(int $start, int $end, int $inc, string $name, ?array $attribs = null,
$selected = null, string $format = ''): string
{
// Set default options
$options = array_merge(HTMLHelper::$formatOptions, ['format.depth' => 0, 'option.format' => '', 'id' => null]);
if (is_array($attribs) && func_num_args() === 5)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
// Extract the format and remove it from downstream options
$format = $options['option.format'];
unset($options['option.format']);
}
else
{
// Get options from the parameters
$options['list.attr'] = $attribs;
$options['list.select'] = $selected;
}
$start = (int) $start;
$end = (int) $end;
$inc = (int) $inc;
$data = [];
for ($i = $start; $i <= $end; $i += $inc)
{
$data[$i] = $format ? sprintf($format, $i) : $i;
}
// Tell genericlist() to use array keys
$options['option.key'] = null;
return HTMLHelper::_('FEFHelp.select.genericlist', $data, $name, $options);
}
/**
* Create an object that represents an option in an option list.
*
* @param string $value The value of the option
* @param string $text The text for the option
* @param mixed $optKey If a string, the returned object property name for
* the value. If an array, options. Valid options are:
* attr: String|array. Additional attributes for this option.
* Defaults to none.
* disable: Boolean. If set, this option is disabled.
* label: String. The value for the option label.
* option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* option.key: The property that will hold the selection value.
* Defaults to "value".
* option.label: The property in each option array to use as the
* selection label attribute. If a "label" option is provided, defaults to
* "label", if no label is given, defaults to null (none).
* option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The property that will hold the the displayed text. This
* parameter is ignored if an options array is passed.
* @param boolean $disable Not used.
*
* @return stdClass
*/
public static function option(?string $value, string $text = '', $optKey = 'value', string $optText = 'text',
bool $disable = false)
{
$options = [
'attr' => null,
'disable' => false,
'option.attr' => null,
'option.disable' => 'disable',
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
];
if (is_array($optKey))
{
// Merge in caller's options
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['disable'] = $disable;
}
$obj = new stdClass;
$obj->{$options['option.key']} = $value;
$obj->{$options['option.text']} = trim($text) ? $text : $value;
/*
* If a label is provided, save it. If no label is provided and there is
* a label name, initialise to an empty string.
*/
$hasProperty = $options['option.label'] !== null;
if (isset($options['label']))
{
$labelProperty = $hasProperty ? $options['option.label'] : 'label';
$obj->$labelProperty = $options['label'];
}
elseif ($hasProperty)
{
$obj->{$options['option.label']} = '';
}
// Set attributes only if there is a property and a value
if ($options['attr'] !== null)
{
$obj->{$options['option.attr']} = $options['attr'];
}
// Set disable only if it has a property and a value
if ($options['disable'] !== null)
{
$obj->{$options['option.disable']} = $options['disable'];
}
return $obj;
}
/**
* Generates the option tags for an HTML select list (with no select tag
* surrounding the options).
*
* @param array $arr An array of objects, arrays, or values.
* @param mixed $optKey If a string, this is the name of the object variable for
* the option value. If null, the index of the array of objects is used. If
* an array, this is a set of options, as key/value pairs. Valid options are:
* -Format options, {@see HTMLHelper::$formatOptions}.
* -groups: Boolean. If set, looks for keys with the value
* "&lt;optgroup>" and synthesizes groups from them. Deprecated. Defaults
* true for backwards compatibility.
* -list.select: either the value of one selected option or an array
* of selected options. Default: none.
* -list.translate: Boolean. If set, text and labels are translated via
* Text::_(). Default is false.
* -option.id: The property in each option array to use as the
* selection id attribute. Defaults to none.
* -option.key: The property in each option array to use as the
* selection value. Defaults to "value". If set to null, the index of the
* option array is used.
* -option.label: The property in each option array to use as the
* selection label attribute. Defaults to null (none).
* -option.text: The property in each option array to use as the
* displayed text. Defaults to "text". If set to null, the option array is
* assumed to be a list of displayable scalars.
* -option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* -option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* -option.key: The property that will hold the selection value.
* Defaults to "value".
* -option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string)
* @param boolean $translate Translate the option values.
*
* @return string HTML for the select list
*/
public static function options(array $arr, $optKey = 'value', string $optText = 'text',
?string $selected = null, bool $translate = false): string
{
$options = array_merge(
HTMLHelper::$formatOptions,
static::$optionDefaults['option'],
['format.depth' => 0, 'groups' => true, 'list.select' => null, 'list.translate' => false]
);
if (is_array($optKey))
{
// Set default options and overwrite with anything passed in
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
$options['list.translate'] = $translate;
}
$html = '';
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']);
foreach ($arr as $elementKey => &$element)
{
$attr = '';
$extra = '';
$label = '';
$id = '';
if (is_array($element))
{
$key = $options['option.key'] === null ? $elementKey : $element[$options['option.key']];
$text = $element[$options['option.text']];
if (isset($element[$options['option.attr']]))
{
$attr = $element[$options['option.attr']];
}
if (isset($element[$options['option.id']]))
{
$id = $element[$options['option.id']];
}
if (isset($element[$options['option.label']]))
{
$label = $element[$options['option.label']];
}
if (isset($element[$options['option.disable']]) && $element[$options['option.disable']])
{
$extra .= ' disabled="disabled"';
}
}
elseif (is_object($element))
{
$key = $options['option.key'] === null ? $elementKey : $element->{$options['option.key']};
$text = $element->{$options['option.text']};
if (isset($element->{$options['option.attr']}))
{
$attr = $element->{$options['option.attr']};
}
if (isset($element->{$options['option.id']}))
{
$id = $element->{$options['option.id']};
}
if (isset($element->{$options['option.label']}))
{
$label = $element->{$options['option.label']};
}
if (isset($element->{$options['option.disable']}) && $element->{$options['option.disable']})
{
$extra .= ' disabled="disabled"';
}
if (isset($element->{$options['option.class']}) && $element->{$options['option.class']})
{
$extra .= ' class="' . $element->{$options['option.class']} . '"';
}
if (isset($element->{$options['option.onclick']}) && $element->{$options['option.onclick']})
{
$extra .= ' onclick="' . $element->{$options['option.onclick']} . '"';
}
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
/*
* The use of options that contain optgroup HTML elements was
* somewhat hacked for J1.5. J1.6 introduces the grouplist() method
* to handle this better. The old solution is retained through the
* "groups" option, which defaults true in J1.6, but should be
* deprecated at some point in the future.
*/
$key = (string) $key;
if ($key === '<OPTGROUP>' && $options['groups'])
{
$html .= $baseIndent . '<optgroup label="' . ($options['list.translate'] ? Text::_($text) : $text) . '">' . $options['format.eol'];
$baseIndent = str_repeat($options['format.indent'], ++$options['format.depth']);
}
elseif ($key === '</OPTGROUP>' && $options['groups'])
{
$baseIndent = str_repeat($options['format.indent'], --$options['format.depth']);
$html .= $baseIndent . '</optgroup>' . $options['format.eol'];
}
else
{
// If no string after hyphen - take hyphen out
$splitText = explode(' - ', $text, 2);
$text = $splitText[0];
if (isset($splitText[1]) && $splitText[1] !== '' && !preg_match('/^[\s]+$/', $splitText[1]))
{
$text .= ' - ' . $splitText[1];
}
if (!empty($label) && $options['list.translate'])
{
$label = Text::_($label);
}
if ($options['option.label.toHtml'])
{
$label = htmlentities($label);
}
if (is_array($attr))
{
$attr = ArrayHelper::toString($attr);
}
else
{
$attr = trim($attr);
}
$extra = ($id ? ' id="' . $id . '"' : '') . ($label ? ' label="' . $label . '"' : '') . ($attr ? ' ' . $attr : '') . $extra;
if (is_array($options['list.select']))
{
foreach ($options['list.select'] as $val)
{
$key2 = is_object($val) ? $val->{$options['option.key']} : $val;
if ($key == $key2)
{
$extra .= ' selected="selected"';
break;
}
}
}
elseif ((string) $key === (string) $options['list.select'])
{
$extra .= ' selected="selected"';
}
if ($options['list.translate'])
{
$text = Text::_($text);
}
// Generate the option, encoding as required
$html .= $baseIndent . '<option value="' . ($options['option.key.toHtml'] ? htmlspecialchars($key, ENT_COMPAT, 'UTF-8') : $key) . '"'
. $extra . '>';
$html .= $options['option.text.toHtml'] ? htmlentities(html_entity_decode($text, ENT_COMPAT, 'UTF-8'), ENT_COMPAT, 'UTF-8') : $text;
$html .= '</option>' . $options['format.eol'];
}
}
return $html;
}
/**
* Generates an HTML radio list.
*
* @param array $data An array of objects
* @param string $name The value of the HTML name attribute
* @param string $attribs Additional HTML attributes for the `<select>` tag
* @param mixed $optKey The key that is selected
* @param string $optText The name of the object variable for the option value
* @param mixed $selected The name of the object variable for the option text
* @param boolean $idtag Value of the field id or null by default
* @param boolean $translate True if options will be translated
*
* @return string HTML for the select list
*/
public static function radiolist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
$translate = false)
{
$forSelect = false;
if (isset($attribs['forSelect']))
{
$forSelect = (bool) ($attribs['forSelect']);
unset($attribs['forSelect']);
}
if (is_array($attribs))
{
$attribs = ArrayHelper::toString($attribs);
}
$id_text = empty($idtag) ? $name : $idtag;
$html = '';
foreach ($data as $optionObject)
{
$optionValue = $optionObject->$optKey;
$labelText = $translate ? \Joomla\CMS\Language\Text::_($optionObject->$optText) : $optionObject->$optText;
$id = ($optionObject->id ?? null);
$extra = '';
$id = $id ? $optionObject->id : $id_text . $optionValue;
if (is_array($selected))
{
foreach ($selected as $val)
{
$k2 = is_object($val) ? $val->$optKey : $val;
if ($optionValue == $k2)
{
$extra .= ' selected="selected" ';
break;
}
}
}
else
{
$extra .= ((string) $optionValue === (string) $selected ? ' checked="checked" ' : '');
}
if ($forSelect)
{
$html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />';
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">' . $labelText . '</label>';
}
else
{
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">';
$html .= "\n\t\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />' . $labelText;
$html .= "\n\t" . '</label>';
}
}
return $html . "\n";
}
/**
* Creates two radio buttons styled with FEF to appear as a YES/NO switch
*
* @param string $name Name of the field
* @param mixed $selected Selected field
* @param array $attribs Additional attributes to add to the switch
*
* @return string The HTML for the switch
*/
public static function booleanswitch(string $name, $selected, array $attribs = []): string
{
if (empty($attribs))
{
$attribs = ['class' => 'akeeba-toggle'];
}
elseif (isset($attribs['class']))
{
$attribs['class'] .= ' akeeba-toggle';
}
else
{
$attribs['class'] = 'akeeba-toggle';
}
$temp = '';
foreach ($attribs as $key => $value)
{
$temp .= $key . ' = "' . $value . '"';
}
$attribs = $temp;
$checked_1 = $selected ? '' : 'checked ';
$checked_2 = $selected ? 'checked ' : '';
$html = '<div ' . $attribs . '>';
$html .= '<input type="radio" class="radio-yes" name="' . $name . '" ' . $checked_2 . 'id="' . $name . '-2" value="1">';
$html .= '<label for="' . $name . '-2" class="green">' . Text::_('JYES') . '</label>';
$html .= '<input type="radio" class="radio-no" name="' . $name . '" ' . $checked_1 . 'id="' . $name . '-1" value="0">';
$html .= '<label for="' . $name . '-1" class="red">' . Text::_('JNO') . '</label>';
$html .= '</div>';
return $html;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die();
use Joomla\CMS\Form\FormHelper;
// Prevent PHP fatal errors if this somehow gets accidentally loaded multiple times
if (class_exists('JFormFieldFancyradio'))
{
return;
}
// Load the base form field class
FormHelper::loadFieldClass('radio');
/**
* Yes/No switcher, compatible with Joomla 3 and 4
*
* ## How to use
*
* 1. Create a folder in your project for custom Joomla form fields, e.g. components/com_example/fields
* 2. Create a new file called `fancyradio.php` with the content
* ```php
* defined('_JEXEC') || die();
* require_once JPATH_LIBRARIES . '/fof40/Html/Fields/fancyradio.php';
* ```
*
* @package Joomla\CMS\Form\Field
*
* @since 1.0.0
* @noinspection PhpUnused
* @noinspection PhpIllegalPsrClassPathInspection
*/
class JFormFieldFancyradio extends JFormFieldRadio
{
public function __construct($form = null)
{
if (version_compare(JVERSION, '3.999.999', 'gt'))
{
// Joomla 4.0 and later.
$this->layout = 'joomla.form.field.radio.switcher';
}
else
{
// Joomla 3.x. Yes, 3.10 does have the layout but I am playing it safe.
$this->layout = 'joomla.form.field.radio';
}
parent::__construct($form);
}
}

View File

@ -0,0 +1,466 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Html;
defined('_JEXEC') || die;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use stdClass;
/**
* Returns arrays of HTMLHelper select options for Joomla-specific information such as access levels.
*
* @method static array access() access(array $params)
* @method static array usergroups() usergroups(array $params)
* @method static array cachehandlers() cachehandlers(array $params)
* @method static array components() components(array $params)
* @method static array languages() languages(array $params)
* @method static array published() published(array $params)
*/
class SelectOptions
{
private static $cache = [];
/**
* Magic method to handle static calls
*
* @param string $name The name of the static method being called
* @param array $arguments Optional arguments, if they are supported by the options type.
*
* @return mixed
* @since 3.3.0
*/
public static function __callStatic(string $name, array $arguments = [])
{
return self::getOptions($name, $arguments);
}
/**
* Get a list of Joomla options of the type you specify. Supported types
* - access View access levels
* - usergroups User groups
* - cachehandlers Cache handlers
* - components Installed components accessible by the current user
* - languages Site or administrator languages
* - published Published status
*
* Global params:
* - cache Should I returned cached data? Default: true.
*
* See the private static methods of this class for more information on params.
*
* @param string $type The options type to get
* @param array $params Optional arguments, if they are supported by the options type.
*
* @return stdClass[]
* @since 3.3.0
* @api
*/
public static function getOptions(string $type, array $params = []): array
{
if ((substr($type, 0, 1) == '_') || !method_exists(__CLASS__, '_api_' . $type))
{
throw new \InvalidArgumentException(__CLASS__ . "does not support option type '$type'.");
}
$useCache = true;
if (isset($params['cache']))
{
$useCache = isset($params['cache']);
unset($params['cache']);
}
$cacheKey = sha1($type . '--' . print_r($params, true));
$fetchNew = !$useCache || ($useCache && !isset(self::$cache[$cacheKey]));
$ret = [];
if ($fetchNew)
{
$ret = forward_static_call_array([__CLASS__, '_api_' . $type], [$params]);
}
if (!$useCache)
{
return $ret;
}
if ($fetchNew)
{
self::$cache[$cacheKey] = $ret;
}
return self::$cache[$cacheKey];
}
/**
* Joomla! Access Levels (previously: view access levels)
*
* Available params:
* - allLevels: Show an option for all levels (default: false)
*
* @param array $params Parameters
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @see \Joomla\CMS\HTML\Helpers\Access::level()
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_access(array $params = []): array
{
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('a.id', 'value') . ', ' . $db->quoteName('a.title', 'text'))
->from($db->quoteName('#__viewlevels', 'a'))
->group($db->quoteName(['a.id', 'a.title', 'a.ordering']))
->order($db->quoteName('a.ordering') . ' ASC')
->order($db->quoteName('title') . ' ASC');
// Get the options.
$db->setQuery($query);
$options = $db->loadObjectList() ?? [];
if (isset($params['allLevels']) && $params['allLevels'])
{
array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_LEVELS')));
}
return $options;
}
/**
* Joomla! User Groups
*
* Available params:
* - allGroups: Show an option for all groups (default: false)
*
* @param array $params Parameters
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @see \Joomla\CMS\HTML\Helpers\Access::usergroup()
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_usergroups(array $params = []): array
{
$options = array_values(UserGroupsHelper::getInstance()->getAll());
for ($i = 0, $n = count($options); $i < $n; $i++)
{
$options[$i]->value = $options[$i]->id;
$options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title;
}
// If all usergroups is allowed, push it into the array.
if (isset($params['allGroups']) && $params['allGroups'])
{
array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_GROUPS')));
}
return $options;
}
/**
* Joomla cache handlers
*
* @param array $params Ignored
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_cachehandlers(array $params = []): array
{
$options = [];
// Convert to name => name array.
foreach (Cache::getStores() as $store)
{
$options[] = HTMLHelper::_('select.option', $store, Text::_('JLIB_FORM_VALUE_CACHE_' . $store), 'value', 'text');
}
return $options;
}
/**
* Get a list of all installed components and also translates them.
*
* Available params:
* - client_ids Array of Joomla application client IDs
*
* @param array $params
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_components(array $params = []): array
{
$db = JoomlaFactory::getDbo();
// Check for client_ids override
$client_ids = $params['client_ids'] ?? [0, 1];
if (is_string($client_ids))
{
$client_ids = explode(',', $client_ids);
}
// Calculate client_ids where clause
$client_ids = array_map(function ($client_id) use ($db) {
return $db->q((int) trim($client_id));
}, $client_ids);
$query = $db->getQuery(true)
->select(
[
$db->qn('name'),
$db->qn('element'),
$db->qn('client_id'),
$db->qn('manifest_cache'),
]
)
->from($db->qn('#__extensions'))
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('client_id') . ' IN (' . implode(',', $client_ids) . ')');
$components = $db->setQuery($query)->loadObjectList('element');
// Convert to array of objects, so we can use sortObjects()
// Also translate component names with Text::_()
$aComponents = [];
$user = JoomlaFactory::getUser();
foreach ($components as $component)
{
// Don't show components in the list where the user doesn't have access for
// TODO: perhaps add an option for this
if (!$user->authorise('core.manage', $component->element))
{
continue;
}
$aComponents[$component->element] = (object) [
'value' => $component->element,
'text' => self::_translateComponentName($component),
];
}
// Reorder the components array, because the alphabetical
// ordering changed due to the Text::_() translation
uasort(
$aComponents,
function ($a, $b) {
return strcasecmp($a->text, $b->text);
}
);
return $aComponents;
}
/**
* Method to get the field options.
*
* Available params:
* - client 'site' (default) or 'administrator'
* - none Text to show for "all languages" option, use empty string to remove it
*
* @param array $params
*
* @return object[] Languages for the specified client
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_languages(array $params): array
{
$client = $params['client'] ?? 'site';
if (!in_array($client, ['site', 'administrator']))
{
$client = 'site';
}
// Make sure the languages are sorted base on locale instead of random sorting
$options = LanguageHelper::createLanguageList(null, constant('JPATH_' . strtoupper($client)), true, true);
if (count($options) > 1)
{
usort(
$options,
function ($a, $b) {
return strcmp($a['value'], $b['value']);
}
);
}
$none = $params['none'] ?? '*';
if (!empty($none))
{
array_unshift($options, HTMLHelper::_('select.option', '*', Text::_($none)));
}
return $options;
}
/**
* Options for a Published field
*
* Params:
* - none Placeholder for no selection (empty key). Default: null.
* - published Show "Published"? Default: true
* - unpublished Show "Unpublished"? Default: true
* - archived Show "Archived"? Default: false
* - trash Show "Trashed"? Default: false
* - all Show "All" option? This is different than none, the key is '*'. Default: false
*
* @param array $params
*
* @return array
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_published(array $params = []): array
{
$config = array_merge([
'none' => '',
'published' => true,
'unpublished' => true,
'archived' => false,
'trash' => false,
'all' => false,
], $params);
$options = [];
if (!empty($config['none']))
{
$options[] = HTMLHelper::_('select.option', '', Text::_($config['none']));
}
if ($config['published'])
{
$options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED'));
}
if ($config['unpublished'])
{
$options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED'));
}
if ($config['archived'])
{
$options[] = HTMLHelper::_('select.option', '2', Text::_('JARCHIVED'));
}
if ($config['trash'])
{
$options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED'));
}
if ($config['all'])
{
$options[] = HTMLHelper::_('select.option', '*', Text::_('JALL'));
}
return $options;
}
/**
* Options for a Published field
*
* Params:
* - none Placeholder for no selection (empty key). Default: null.
*
* @param array $params
*
* @return array
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_boolean(array $params = []): array
{
$config = array_merge([
'none' => '',
], $params);
$options = [];
if (!empty($config['none']))
{
$options[] = HTMLHelper::_('select.option', '', Text::_($config['none']));
}
$options[] = HTMLHelper::_('select.option', '1', Text::_('JYES'));
$options[] = HTMLHelper::_('select.option', '0', Text::_('JNO'));
return $options;
}
/**
* Translate a component name
*
* @param object $item The component object
*
* @return string $text The translated name of the extension
* @since 3.3.0
* @internal
*
* @see /administrator/com_installer/models/extension.php
*/
private static function _translateComponentName(object $item): string
{
// Map the manifest cache to $item. This is needed to get the name from the
// manifest_cache and NOT from the name column, else some Text::_() translations fails.
$mData = json_decode($item->manifest_cache);
if ($mData)
{
foreach ($mData as $key => $value)
{
if ($key == 'type')
{
// Ignore the type field
continue;
}
$item->$key = $value;
}
}
$lang = JoomlaFactory::getLanguage();
$source = JPATH_ADMINISTRATOR . '/components/' . $item->element;
$lang->load("$item->element.sys", JPATH_ADMINISTRATOR, null, false, false)
|| $lang->load("$item->element.sys", $source, null, false, false)
|| $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, $lang->getDefault(), false, false)
|| $lang->load("$item->element.sys", $source, $lang->getDefault(), false, false);
return Text::_($item->name);
}
}

View File

@ -0,0 +1,613 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\IP;
defined('_JEXEC') || die;
/**
* IP address helper
*
* Makes sure that we get the real IP of the user
*/
class IPHelper
{
/**
* The IP address of the current visitor
*
* @var string
*/
protected static $ip;
/**
* Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers?
*
* @var bool
*/
protected static $allowIpOverrides = true;
/**
* See self::detectAndCleanIP and setUseFirstIpInChain
*
* If this is enabled (default) self::detectAndCleanIP will return the FIRST IP in case there is an IP chain coming
* for example from an X-Forwarded-For HTTP header. When set to false it will simulate the old behavior in FOF up to
* and including 3.1.1 which returned the LAST IP in the list.
*
* @var bool
*/
protected static $useFirstIpInChain = true;
/**
* List of headers we should check to know if the user is behind a proxy. PLEASE NOTE: ORDER MATTERS!
*
* @var array
*/
protected static $proxyHeaders = [
'HTTP_CF_CONNECTING_IP', // CloudFlare
'HTTP_X_FORWARDED_FOR', // Standard for transparent proxy (e.g. NginX)
'HTTP_X_SUCURI_CLIENTIP', // Sucuri firewall uses its own header
];
/**
* Set the $useFirstIpInChain flag. See above.
*
* @param bool $value
*/
public static function setUseFirstIpInChain(bool $value = true): void
{
self::$useFirstIpInChain = $value;
}
/**
* Getter for the list of proxy headers we can check
*
* @return array
*/
public static function getProxyHeaders(): array
{
return static::$proxyHeaders;
}
/**
* Get the current visitor's IP address
*
* @return string
*/
public static function getIp(): string
{
if (is_null(static::$ip))
{
$ip = self::detectAndCleanIP();
if (!empty($ip) && ($ip != '0.0.0.0') && function_exists('inet_pton') && function_exists('inet_ntop'))
{
$myIP = @inet_pton($ip);
if ($myIP !== false)
{
$ip = inet_ntop($myIP);
}
}
static::setIp($ip);
}
return static::$ip;
}
/**
* Set the IP address of the current visitor
*
* @param string $ip
*
* @return void
*/
public static function setIp(string $ip): void
{
static::$ip = $ip;
}
/**
* Checks if an IP is contained in a list of IPs or IP expressions
*
* @param string $ip The IPv4/IPv6 address to check
* @param array|string $ipTable An IP expression (or a comma-separated or array list of IP expressions) to
* check against
*
* @return boolean True if it's in the list
*/
public static function IPinList(string $ip, $ipTable = ''): bool
{
// No point proceeding with an empty IP list
if (empty($ipTable))
{
return false;
}
// If the IP list is not an array, convert it to an array
if (!is_array($ipTable))
{
if (strpos($ipTable, ',') !== false)
{
$ipTable = explode(',', $ipTable);
$ipTable = array_map(function ($x) {
return trim($x);
}, $ipTable);
}
else
{
$ipTable = trim($ipTable);
$ipTable = [$ipTable];
}
}
// If no IP address is found, return false
if ($ip == '0.0.0.0')
{
return false;
}
// If no IP is given, return false
if (empty($ip))
{
return false;
}
// Sanity check
if (!function_exists('inet_pton'))
{
return false;
}
// Get the IP's in_adds representation
$myIP = @inet_pton($ip);
// If the IP is in an unrecognisable format, quite
if ($myIP === false)
{
return false;
}
$ipv6 = self::isIPv6($ip);
foreach ($ipTable as $ipExpression)
{
$ipExpression = trim($ipExpression);
// Inclusive IP range, i.e. 123.123.123.123-124.125.126.127
if (strstr($ipExpression, '-'))
{
[$from, $to] = explode('-', $ipExpression, 2);
if ($ipv6 && (!self::isIPv6($from) || !self::isIPv6($to)))
{
// Do not apply IPv4 filtering on an IPv6 address
continue;
}
elseif (!$ipv6 && (self::isIPv6($from) || self::isIPv6($to)))
{
// Do not apply IPv6 filtering on an IPv4 address
continue;
}
$from = @inet_pton(trim($from));
$to = @inet_pton(trim($to));
// Sanity check
if (($from === false) || ($to === false))
{
continue;
}
// Swap from/to if they're in the wrong order
if ($from > $to)
{
[$from, $to] = [$to, $from];
}
if (($myIP >= $from) && ($myIP <= $to))
{
return true;
}
}
// Netmask or CIDR provided
elseif (strstr($ipExpression, '/'))
{
$binaryip = self::inet_to_bits($myIP);
[$net, $maskbits] = explode('/', $ipExpression, 2);
if ($ipv6 && !self::isIPv6($net))
{
// Do not apply IPv4 filtering on an IPv6 address
continue;
}
elseif (!$ipv6 && self::isIPv6($net))
{
// Do not apply IPv6 filtering on an IPv4 address
continue;
}
elseif ($ipv6 && strstr($maskbits, ':'))
{
// Perform an IPv6 CIDR check
if (self::checkIPv6CIDR($myIP, $ipExpression))
{
return true;
}
// If we didn't match it proceed to the next expression
continue;
}
elseif (!$ipv6 && strstr($maskbits, '.'))
{
// Convert IPv4 netmask to CIDR
$long = ip2long($maskbits);
$base = ip2long('255.255.255.255');
$maskbits = 32 - log(($long ^ $base) + 1, 2);
}
// Convert network IP to in_addr representation
$net = @inet_pton($net);
// Sanity check
if ($net === false)
{
continue;
}
// Get the network's binary representation
$binarynet = self::inet_to_bits($net);
$expectedNumberOfBits = $ipv6 ? 128 : 24;
$binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);
// Check the corresponding bits of the IP and the network
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
if ($ip_net_bits === $net_bits)
{
return true;
}
}
elseif ($ipv6)
{
$ipExpression = trim($ipExpression);
if (!self::isIPv6($ipExpression))
{
continue;
}
$ipCheck = @inet_pton($ipExpression);
if ($ipCheck === false)
{
continue;
}
if ($ipCheck === $myIP)
{
return true;
}
}
else
{
// Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123]
$dots = 0;
if (substr($ipExpression, -1) == '.')
{
// Partial IP address. Convert to CIDR and re-match
foreach (count_chars($ipExpression, 1) as $i => $val)
{
if ($i == 46)
{
$dots = $val;
}
}
$netmask = '255.255.255.255';
switch ($dots)
{
case 1:
$netmask = '255.0.0.0';
$ipExpression .= '0.0.0';
break;
case 2:
$netmask = '255.255.0.0';
$ipExpression .= '0.0';
break;
case 3:
$netmask = '255.255.255.0';
$ipExpression .= '0';
break;
default:
$dots = 0;
}
if ($dots)
{
$binaryip = self::inet_to_bits($myIP);
// Convert netmask to CIDR
$long = ip2long($netmask);
$base = ip2long('255.255.255.255');
$maskbits = 32 - log(($long ^ $base) + 1, 2);
$net = @inet_pton($ipExpression);
// Sanity check
if ($net === false)
{
continue;
}
// Get the network's binary representation
$binarynet = self::inet_to_bits($net);
$expectedNumberOfBits = $ipv6 ? 128 : 24;
$binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);
// Check the corresponding bits of the IP and the network
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
if ($ip_net_bits === $net_bits)
{
return true;
}
}
}
if (!$dots)
{
$ip = @inet_pton(trim($ipExpression));
if ($ip == $myIP)
{
return true;
}
}
}
}
return false;
}
/**
* Works around the REMOTE_ADDR not containing the user's IP
*/
public static function workaroundIPIssues(): void
{
$ip = self::getIp();
if (array_key_exists('REMOTE_ADDR', $_SERVER) && ($_SERVER['REMOTE_ADDR'] == $ip))
{
return;
}
if (array_key_exists('REMOTE_ADDR', $_SERVER))
{
$_SERVER['FOF_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
}
elseif (function_exists('getenv'))
{
if (getenv('REMOTE_ADDR'))
{
$_SERVER['FOF_REMOTE_ADDR'] = getenv('REMOTE_ADDR');
}
}
$_SERVER['REMOTE_ADDR'] = $ip;
}
/**
* Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header?
*
* @param bool $newState True to allow the override
*
* @return void
*/
public static function setAllowIpOverrides(bool $newState): void
{
self::$allowIpOverrides = $newState;
}
/**
* Is it an IPv6 IP address?
*
* @param string $ip An IPv4 or IPv6 address
*
* @return boolean True if it's IPv6
*/
protected static function isIPv6(string $ip): bool
{
return strstr($ip, ':') !== false;
}
/**
* Gets the visitor's IP address. Automatically handles reverse proxies
* reporting the IPs of intermediate devices, like load balancers. Examples:
* https://www.akeeba.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html
* http://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips
* The solution used is assuming that the first IP address is the external one (unless $useFirstIpInChain is set to
* false)
*
* @return string
*/
protected static function detectAndCleanIP(): string
{
$ip = static::detectIP();
if ((strstr($ip, ',') !== false) || (strstr($ip, ' ') !== false))
{
$ip = str_replace(' ', ',', $ip);
$ip = str_replace(',,', ',', $ip);
$ips = explode(',', $ip);
$ip = '';
// Loop until we're running out of parts or we have a hit
while ($ips)
{
$ip = array_shift($ips);
$ip = trim($ip);
if (self::$useFirstIpInChain)
{
return self::cleanIP($ip);
}
}
}
return self::cleanIP($ip);
}
protected static function cleanIP(string $ip): string
{
$ip = trim($ip);
$ip = strtoupper($ip);
/**
* Work around IPv4-mapped addresses.
*
* IPv4 addresses may be embedded in an IPv6 address. This is always 80 zeroes, 16 ones and the IPv4 address.
* In all possible IPv6 notations this is:
* 0:0:0:0:0:FFFF:192.168.1.1
* ::FFFF:192.168.1.1
* ::FFFF:C0A8:0101
*
* @see http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding-2.htm
*/
if ((strpos($ip, '::FFFF:') === 0) || (strpos($ip, '0:0:0:0:0:FFFF:') === 0))
{
// Fast path: the embedded IPv4 is in decimal notation.
if (strstr($ip, '.') !== false)
{
return substr($ip, strrpos($ip, ':') + 1);
}
// Get the embedded IPv4 (in hex notation)
$ip = substr($ip, strpos($ip, ':FFFF:') + 6);
// Convert each 16-bit WORD to decimal
[$word1, $word2] = explode(':', $ip);
$word1 = hexdec($word1);
$word2 = hexdec($word2);
$longIp = $word1 * 65536 + $word2;
return long2ip($longIp);
}
return $ip;
}
/**
* Gets the visitor's IP address
*
* @return string
*/
protected static function detectIP(): string
{
// Normally the $_SERVER superglobal is set
if (isset($_SERVER))
{
// Do we have IP overrides enabled?
if (static::$allowIpOverrides)
{
// If so, check for every proxy header
foreach (static::$proxyHeaders as $header)
{
if (array_key_exists($header, $_SERVER))
{
return $_SERVER[$header];
}
}
}
// CLI applications
if (!array_key_exists('REMOTE_ADDR', $_SERVER))
{
return '';
}
// Normal, non-proxied server or server behind a transparent proxy
return $_SERVER['REMOTE_ADDR'];
}
// This part is executed on PHP running as CGI, or on SAPIs which do
// not set the $_SERVER superglobal
// If getenv() is disabled, you're screwed
if (!function_exists('getenv'))
{
return '';
}
// Do we have IP overrides enabled?
if (static::$allowIpOverrides)
{
// If so, check for every proxy header
foreach (static::$proxyHeaders as $header)
{
if (getenv($header))
{
return getenv($header);
}
}
}
// Normal, non-proxied server or server behind a transparent proxy
if (getenv('REMOTE_ADDR'))
{
return getenv('REMOTE_ADDR');
}
// Catch-all case for broken servers and CLI applications
return '';
}
/**
* Converts inet_pton output to bits string
*
* @param string $inet The in_addr representation of an IPv4 or IPv6 address
*
* @return string
*/
protected static function inet_to_bits(string $inet): string
{
$unpacked = strlen($inet) == 4 ? unpack('C4', $inet) : unpack('C16', $inet);
$binaryip = '';
foreach ($unpacked as $byte)
{
$binaryip .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
}
/**
* Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet
*
* @param string $ip The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A
* @param string $cidrnet The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64
*
* @return bool
*/
protected static function checkIPv6CIDR(string $ip, string $cidrnet): bool
{
$ip = inet_pton($ip);
$binaryip = self::inet_to_bits($ip);
[$net, $maskbits] = explode('/', $cidrnet);
$net = inet_pton($net);
$binarynet = self::inet_to_bits($net);
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
return $ip_net_bits === $net_bits;
}
}

View File

@ -0,0 +1,420 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Inflector;
defined('_JEXEC') || die;
/**
* An Inflector to pluralize and singularize English nouns.
*/
class Inflector
{
/**
* Rules for pluralizing and singularizing of nouns.
*
* @var array
*/
protected $rules = [
// Pluralization rules. The regex on the left transforms to the regex on the right.
'pluralization' => [
'/move$/i' => 'moves',
'/sex$/i' => 'sexes',
'/child$/i' => 'children',
'/children$/i' => 'children',
'/man$/i' => 'men',
'/men$/i' => 'men',
'/foot$/i' => 'feet',
'/feet$/i' => 'feet',
'/person$/i' => 'people',
'/people$/i' => 'people',
'/taxon$/i' => 'taxa',
'/taxa$/i' => 'taxa',
'/(quiz)$/i' => '$1zes',
'/^(ox)$/i' => '$1en',
'/oxen$/i' => 'oxen',
'/(m|l)ouse$/i' => '$1ice',
'/(m|l)ice$/i' => '$1ice',
'/(matr|vert|ind|suff)ix|ex$/i' => '$1ices',
'/(x|ch|ss|sh)$/i' => '$1es',
'/([^aeiouy]|qu)y$/i' => '$1ies',
'/(?:([^f])fe|([lr])f)$/i' => '$1$2ves',
'/sis$/i' => 'ses',
'/([ti]|addend)um$/i' => '$1a',
'/([ti]|addend)a$/i' => '$1a',
'/(alumn|formul)a$/i' => '$1ae',
'/(alumn|formul)ae$/i' => '$1ae',
'/(buffal|tomat|her)o$/i' => '$1oes',
'/(bu)s$/i' => '$1ses',
'/(campu)s$/i' => '$1ses',
'/(alias|status)$/i' => '$1es',
'/(octop|vir)us$/i' => '$1i',
'/(octop|vir)i$/i' => '$1i',
'/(gen)us$/i' => '$1era',
'/(gen)era$/i' => '$1era',
'/(ax|test)is$/i' => '$1es',
'/s$/i' => 's',
'/$/' => 's',
],
// Singularization rules. The regex on the left transforms to the regex on the right.
'singularization' => [
'/cookies$/i' => 'cookie',
'/moves$/i' => 'move',
'/sexes$/i' => 'sex',
'/children$/i' => 'child',
'/men$/i' => 'man',
'/feet$/i' => 'foot',
'/people$/i' => 'person',
'/taxa$/i' => 'taxon',
'/databases$/i' => 'database',
'/menus$/i' => 'menu',
'/(quiz)zes$/i' => '\1',
'/(matr|suff)ices$/i' => '\1ix',
'/(vert|ind|cod)ices$/i' => '\1ex',
'/^(ox)en/i' => '\1',
'/(alias|status)es$/i' => '\1',
'/(tomato|hero|buffalo)es$/i' => '\1',
'/([octop|vir])i$/i' => '\1us',
'/(gen)era$/i' => '\1us',
'/(cris|^ax|test)es$/i' => '\1is',
'/is$/i' => 'is',
'/us$/i' => 'us',
'/ias$/i' => 'ias',
'/(shoe)s$/i' => '\1',
'/(o)es$/i' => '\1e',
'/(bus)es$/i' => '\1',
'/(campus)es$/i' => '\1',
'/([m|l])ice$/i' => '\1ouse',
'/(x|ch|ss|sh)es$/i' => '\1',
'/(m)ovies$/i' => '\1ovie',
'/(s)eries$/i' => '\1eries',
'/(v)ies$/i' => '\1ie',
'/([^aeiouy]|qu)ies$/i' => '\1y',
'/([lr])ves$/i' => '\1f',
'/(tive)s$/i' => '\1',
'/(hive)s$/i' => '\1',
'/([^f])ves$/i' => '\1fe',
'/(^analy)ses$/i' => '\1sis',
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
'/([ti]|addend)a$/i' => '\1um',
'/(alumn|formul)ae$/i' => '$1a',
'/(n)ews$/i' => '\1ews',
'/(.*)ss$/i' => '\1ss',
'/(.*)s$/i' => '\1',
],
// Uncountable objects are always singular
'uncountable' => [
'aircraft',
'cannon',
'deer',
'equipment',
'fish',
'information',
'money',
'moose',
'news',
'rice',
'series',
'sheep',
'species',
'swine',
],
];
/**
* Cache of pluralized and singularized nouns.
*
* @var array
*/
protected $cache = [
'singularized' => [],
'pluralized' => [],
];
/**
* Removes the cache of pluralised and singularised words. Useful when you want to replace word pairs.
*
* @return void
*/
public function deleteCache(): void
{
$this->cache['pluralized'] = [];
$this->cache['singularized'] = [];
}
/**
* Add a word to the cache, useful to make exceptions or to add words in other languages.
*
* @param string $singular word.
* @param string $plural word.
*
* @return void
*/
public function addWord(string $singular, string $plural): void
{
$this->cache['pluralized'][$singular] = $plural;
$this->cache['singularized'][$plural] = $singular;
}
/**
* Singular English word to plural.
*
* @param string $word word to pluralize.
*
* @return string Plural noun.
*/
public function pluralize(string $word): string
{
// Get the cached noun of it exists
if (isset($this->cache['pluralized'][$word]))
{
return $this->cache['pluralized'][$word];
}
// Check if the noun is already in plural form, i.e. in the singularized cache
if (isset($this->cache['singularized'][$word]))
{
return $word;
}
// Create the plural noun
if (in_array($word, $this->rules['uncountable']))
{
$_cache['pluralized'][$word] = $word;
return $word;
}
foreach ($this->rules['pluralization'] as $regexp => $replacement)
{
$matches = null;
$plural = preg_replace($regexp, $replacement, $word, -1, $matches);
if ($matches > 0)
{
$_cache['pluralized'][$word] = $plural;
return $plural;
}
}
return $word;
}
/**
* Plural English word to singular.
*
* @param string $word Word to singularize.
*
* @return string Singular noun.
*/
public function singularize(string $word): string
{
// Get the cached noun of it exists
if (isset($this->cache['singularized'][$word]))
{
return $this->cache['singularized'][$word];
}
// Check if the noun is already in singular form, i.e. in the pluralized cache
if (isset($this->cache['pluralized'][$word]))
{
return $word;
}
// Create the singular noun
if (in_array($word, $this->rules['uncountable']))
{
$_cache['singularized'][$word] = $word;
return $word;
}
foreach ($this->rules['singularization'] as $regexp => $replacement)
{
$matches = null;
$singular = preg_replace($regexp, $replacement, $word, -1, $matches);
if ($matches > 0)
{
$_cache['singularized'][$word] = $singular;
return $singular;
}
}
return $word;
}
/**
* Returns given word as CamelCased.
*
* Converts a word like "foo_bar" or "foo bar" to "FooBar". It
* will remove non alphanumeric characters from the word, so
* "who's online" will be converted to "WhoSOnline"
*
* @param string $word Word to convert to camel case.
*
* @return string UpperCamelCasedWord
*/
public function camelize(string $word): string
{
$word = preg_replace('/[^a-zA-Z0-9\s]/', ' ', $word);
return str_replace(' ', '', ucwords(strtolower(str_replace('_', ' ', $word))));
}
/**
* Converts a word "into_it_s_underscored_version"
*
* Convert any "CamelCased" or "ordinary Word" into an "underscored_word".
*
* @param string $word Word to underscore
*
* @return string Underscored word
*/
public function underscore(string $word): string
{
$word = preg_replace('/(\s)+/', '_', $word);
return strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $word));
}
/**
* Convert any "CamelCased" word into an array of strings
*
* Returns an array of strings each of which is a substring of string formed
* by splitting it at the camelcased letters.
*
* @param string $word Word to explode
*
* @return string[] Array of strings
*/
public function explode(string $word): array
{
return explode('_', self::underscore($word));
}
/**
* Convert an array of strings into a "CamelCased" word.
*
* @param string[] $words Array of words to implode
*
* @return string UpperCamelCasedWord
*/
public function implode(array $words): string
{
return self::camelize(implode('_', $words));
}
/**
* Returns a human-readable string from $word.
*
* Returns a human-readable string from $word, by replacing
* underscores with a space, and by upper-casing the initial
* character by default.
*
* @param string $word String to "humanize"
*
* @return string Human-readable word
*/
public function humanize(string $word): string
{
return ucwords(strtolower(str_replace("_", " ", $word)));
}
/**
* Returns camelBacked version of a string. Same as camelize but first char is lowercased.
*
* @param string $string String to be camelBacked.
*
* @return string
*
* @see self::camelize()
*/
public function variablize(string $string): string
{
$string = self::camelize(self::underscore($string));
$result = strtolower(substr($string, 0, 1));
return preg_replace('/\\w/', $result, $string, 1);
}
/**
* Check to see if an English word is singular
*
* @param string $string The word to check
*
* @return boolean
*/
public function isSingular(string $string): bool
{
// Check cache assuming the string is plural.
$singular = $this->cache['singularized'][$string] ?? null;
$plural = $singular && isset($this->cache['pluralized'][$singular]) ? $this->cache['pluralized'][$singular] : null;
if ($singular && $plural)
{
return $plural != $string;
}
// If string is not in the cache, try to pluralize and singularize it.
return self::singularize(self::pluralize($string)) === $string;
}
/**
* Check to see if an English word is plural.
*
* @param string $string String to be checked.
*
* @return boolean
*/
public function isPlural(string $string): bool
{
// Uncountable objects are always singular (e.g. information)
if (in_array($string, $this->rules['uncountable']))
{
return false;
}
// Check cache assuming the string is singular.
$plural = $this->cache['pluralized'][$string] ?? null;
$singular = $plural && isset($this->cache['singularized'][$plural]) ? $this->cache['singularized'][$plural] : null;
if ($plural && $singular)
{
return $singular != $string;
}
// If string is not in the cache, try to singularize and pluralize it.
return self::pluralize(self::singularize($string)) === $string;
}
/**
* Gets a part of a CamelCased word by index.
*
* Use a negative index to start at the last part of the word (-1 is the
* last part)
*
* @param string $string Word
* @param integer $index Index of the part
* @param string|null $default Default value
*
* @return string|null
*/
public function getPart(string $string, int $index, ?string $default = null): ?string
{
$parts = self::explode($string);
if ($index < 0)
{
$index = count($parts) + $index;
}
return $parts[$index] ?? $default;
}
}

View File

@ -0,0 +1,275 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Input;
defined('_JEXEC') || die;
use Exception;
use JFilterInput;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\Input\Input as JoomlaInput;
use ReflectionObject;
class Input extends JoomlaInput
{
/**
* Public constructor. Overridden to allow specifying the global input array
* to use as a string and instantiate from an object holding variables.
*
* @param array|string|object|null $source Source data; set null to use $_REQUEST
* @param array $options Filter options
*/
public function __construct($source = null, array $options = [])
{
$hash = null;
if (is_string($source))
{
$hash = strtoupper($source);
if (!in_array($hash, ['GET', 'POST', 'FILES', 'COOKIE', 'ENV', 'SERVER', 'REQUEST']))
{
$hash = 'REQUEST';
}
$source = $this->extractJoomlaSource($hash);
}
elseif (is_object($source) && ($source instanceof Input))
{
$source = $source->getData();
}
elseif (is_object($source) && ($source instanceof JoomlaInput))
{
$serialised = $source->serialize();
[$xOptions, $xData, $xInput] = unserialize($serialised);
unset ($xOptions);
unset ($xInput);
unset ($source);
$source = $xData;
unset ($xData);
}
elseif (is_object($source))
{
try
{
$source = (array) $source;
}
catch (Exception $exc)
{
$source = null;
}
}
/** @noinspection PhpStatementHasEmptyBodyInspection */
elseif (is_array($source))
{
// Nothing, it's already an array
}
else
{
// Any other case
$source = null;
}
// TODO Joomla 4 -- get the data from the application input
// If we are not sure use the REQUEST array
if (empty($source))
{
$source = $this->extractJoomlaSource();
}
parent::__construct($source, $options);
}
/**
* Gets a value from the input data. Overridden to allow specifying a filter
* mask.
*
* @param string $name Name of the value to get.
* @param mixed $default Default value to return if variable does not exist.
* @param string $filter Filter to apply to the value.
* @param int $mask The filter mask
*
* @return mixed The filtered input value.
*/
public function get($name, $default = null, $filter = 'cmd', $mask = 0)
{
if (isset($this->data[$name]))
{
return $this->_cleanVar($this->data[$name], $mask, $filter);
}
return $default;
}
/**
* Remove a key from the input data.
*
* @param string $name The key name to remove from the input data.
*
* @return void
* @since 3.6.3
*/
public function remove($name)
{
if (!isset($this->data[$name]))
{
return;
}
unset($this->data[$name]);
}
/**
* Returns a copy of the raw data stored in the class
*
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* Override all the raw data stored in the class. USE SPARINGLY.
*
* @param array $data
*/
public function setData(array $data): void
{
$this->data = $data;
}
/**
* Magic method to get filtered input data.
*
* @param mixed $name Name of the value to get.
* @param string $arguments [0] The name of the variable [1] The default value [2] Mask
*
* @return boolean The filtered boolean input value.
*/
public function __call($name, $arguments)
{
if (substr($name, 0, 3) == 'get')
{
$filter = substr($name, 3);
$default = null;
$mask = 0;
if (isset($arguments[1]))
{
$default = $arguments[1];
}
if (isset($arguments[2]))
{
$mask = $arguments[2];
}
return $this->get($arguments[0], $default, $filter, $mask);
}
}
/**
* Custom filter implementation. Works better with arrays and allows the use
* of a filter mask.
*
* @param mixed $var The variable (value) to clean
* @param integer $mask The clean mask
* @param string $type The variable type
*
* @return mixed
*/
protected function _cleanVar($var, $mask = 0, $type = null)
{
if (is_array($var))
{
$temp = [];
foreach ($var as $k => $v)
{
$temp[$k] = self::_cleanVar($v, $mask);
}
return $temp;
}
// If the no trim flag is not set, trim the variable
if (!($mask & 1) && is_string($var))
{
$var = trim($var);
}
// Now we handle input filtering
if ($mask & 2)
{
// If the allow raw flag is set, do not modify the variable
}
elseif ($mask & 4)
{
// If the allow HTML flag is set, apply a safe HTML filter to the variable
if (version_compare(JVERSION, '3.999.999', 'le'))
{
$safeHtmlFilter = InputFilter::getInstance(null, null, 1, 1);
}
else
{
/**
* @noinspection PhpDeprecationInspection
*/
$safeHtmlFilter = JFilterInput::getInstance([], [], 1, 1);
}
$var = $safeHtmlFilter->clean($var, $type);
}
else
{
$var = $this->filter->clean($var, $type);
}
return $var;
}
/**
* @param string $hash
*
* @return array
*/
protected function extractJoomlaSource($hash = 'REQUEST')
{
if (!in_array(strtoupper($hash), ['GET', 'POST', 'FILES', 'COOKIE', 'ENV', 'SERVER', 'REQUEST']))
{
$hash = 'REQUEST';
}
$hash = strtolower($hash);
try
{
$input = Factory::getApplication()->input;
}
catch (Exception $e)
{
$input = new JoomlaInput();
}
if ($hash !== 'request')
{
$input = $input->{$hash};
}
$refObject = new ReflectionObject($input);
$refProp = $refObject->getProperty('data');
$refProp->setAccessible(true);
return $refProp->getValue($input) ?? [];
}
}

View File

@ -0,0 +1,870 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\InstallScript;
defined('_JEXEC') || die;
use DirectoryIterator;
use Exception;
use FOF40\Container\Container;
use FOF40\Template\Template;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Installer\Installer as JoomlaInstaller;
use Joomla\CMS\Log\Log;
use Throwable;
class BaseInstaller
{
public $componentName;
/**
* The minimum PHP version required to install this extension
*
* @var string
*/
protected $minimumPHPVersion = '7.2.0';
/**
* The minimum Joomla! version required to install this extension
*
* @var string
*/
protected $minimumJoomlaVersion = '3.9.0';
/**
* The maximum Joomla! version this extension can be installed on
*
* @var string
*/
protected $maximumJoomlaVersion = '4.999.999';
/**
* Post-installation message definitions for Joomla! 3.2 or later.
*
* This array contains the message definitions for the Post-installation Messages component added in Joomla! 3.2 and
* later versions. Each element is also a hashed array. For the keys used in these message definitions please see
* addPostInstallationMessage
*
* @var array
*/
protected $postInstallationMessages = [];
/**
* Recursively copy a bunch of files, but only if the source and target file have a different size.
*
* @param string $source Path to copy FROM
* @param string $dest Path to copy TO
* @param array $ignored List of entries to ignore (first level entries are taken into account)
*
* @return void
*/
protected function recursiveConditionalCopy(string $source, string $dest, array $ignored = []): void
{
// Make sure source and destination exist
if (!@is_dir($source))
{
return;
}
if (!@is_dir($dest) && !@mkdir($dest, 0755))
{
Folder::create($dest, 0755);
}
if (!@is_dir($dest))
{
$this->log(__CLASS__ . ": Cannot create folder $dest");
return;
}
// List the contents of the source folder
try
{
$di = new DirectoryIterator($source);
}
catch (Exception $e)
{
return;
}
// Process each entry
foreach ($di as $entry)
{
// Ignore dot dirs (. and ..)
if ($entry->isDot())
{
continue;
}
$sourcePath = $entry->getPathname();
$fileName = $entry->getFilename();
// Do not copy ignored files
if (!empty($ignored) && in_array($fileName, $ignored))
{
continue;
}
// If it's a directory do a recursive copy
if ($entry->isDir())
{
$this->recursiveConditionalCopy($sourcePath, $dest . DIRECTORY_SEPARATOR . $fileName);
continue;
}
// If it's a file check if it's missing or identical
$mustCopy = false;
$targetPath = $dest . DIRECTORY_SEPARATOR . $fileName;
if (!@is_file($targetPath))
{
$mustCopy = true;
}
else
{
$sourceSize = @filesize($sourcePath);
$targetSize = @filesize($targetPath);
$mustCopy = $sourceSize !== $targetSize;
if ((substr($targetPath, -4) === '.php') && function_exists('opcache_invalidate'))
{
/** @noinspection PhpComposerExtensionStubsInspection */
opcache_invalidate($targetPath);
}
}
if (!$mustCopy)
{
continue;
}
if (!@copy($sourcePath, $targetPath) && !File::copy($sourcePath, $targetPath))
{
$this->log(__CLASS__ . ": Cannot copy $sourcePath to $targetPath");
}
}
}
/**
* Try to log a warning / error with Joomla
*
* @param string $message The message to write to the log
* @param bool $error Is this an error? If not, it's a warning. (default: false)
* @param string $category Log category, default jerror
*
* @return void
*/
protected function log(string $message, bool $error = false, string $category = 'jerror'): void
{
// Just in case...
if (!class_exists('\Joomla\CMS\Log\Log', true))
{
return;
}
$priority = $error ? Log::ERROR : Log::WARNING;
try
{
Log::add($message, $priority, $category);
}
catch (Exception $e)
{
// Swallow the exception.
}
}
/**
* Check that the server meets the minimum PHP version requirements.
*
* @return bool
*/
protected function checkPHPVersion(): bool
{
if (!empty($this->minimumPHPVersion))
{
if (defined('PHP_VERSION'))
{
$version = PHP_VERSION;
}
elseif (function_exists('phpversion'))
{
$version = phpversion();
}
else
{
$version = '5.0.0'; // all bets are off!
}
if (!version_compare($version, $this->minimumPHPVersion, 'ge'))
{
$msg = "<p>You need PHP $this->minimumPHPVersion or later to install this extension</p>";
$this->log($msg);
return false;
}
}
return true;
}
/**
* Check the minimum and maximum Joomla! versions for this extension
*
* @return bool
*/
protected function checkJoomlaVersion(): bool
{
if (!empty($this->minimumJoomlaVersion) && !version_compare(JVERSION, $this->minimumJoomlaVersion, 'ge'))
{
$msg = "<p>You need Joomla! $this->minimumJoomlaVersion or later to install this extension</p>";
$this->log($msg);
return false;
}
// Check the maximum Joomla! version
if (!empty($this->maximumJoomlaVersion) && !version_compare(JVERSION, $this->maximumJoomlaVersion, 'le'))
{
$msg = "<p>You need Joomla! $this->maximumJoomlaVersion or earlier to install this extension</p>";
$this->log($msg);
return false;
}
return true;
}
/**
* Clear PHP opcode caches
*
* @return void
* @noinspection PhpComposerExtensionStubsInspection
*/
protected function clearOpcodeCaches(): void
{
// Always reset the OPcache if it's enabled. Otherwise there's a good chance the server will not know we are
// replacing .php scripts. This is a major concern since PHP 5.5 included and enabled OPcache by default.
if (function_exists('opcache_reset'))
{
opcache_reset();
}
// Also do that for APC cache
elseif (function_exists('apc_clear_cache'))
{
@apc_clear_cache();
}
}
/**
* Get the dependencies for a package from the #__akeeba_common table
*
* @param string $package The package
*
* @return array The dependencies
*/
protected function getDependencies(string $package): array
{
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)
->select($db->qn('value'))
->from($db->qn('#__akeeba_common'))
->where($db->qn('key') . ' = ' . $db->q($package));
try
{
$dependencies = $db->setQuery($query)->loadResult();
$dependencies = json_decode($dependencies, true);
if (empty($dependencies))
{
$dependencies = [];
}
}
catch (Exception $e)
{
$dependencies = [];
}
return $dependencies;
}
/**
* Sets the dependencies for a package into the #__akeeba_common table
*
* @param string $package The package
* @param array $dependencies The dependencies list
*/
protected function setDependencies(string $package, array $dependencies): void
{
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)
->delete('#__akeeba_common')
->where($db->qn('key') . ' = ' . $db->q($package));
try
{
$db->setQuery($query)->execute();
}
catch (Exception $e)
{
// Do nothing if the old key wasn't found
}
$object = (object) [
'key' => $package,
'value' => json_encode($dependencies),
];
try
{
$db->insertObject('#__akeeba_common', $object, 'key');
}
catch (Exception $e)
{
// Do nothing if the old key wasn't found
}
}
/**
* Adds a package dependency to #__akeeba_common
*
* @param string $package The package
* @param string $dependency The dependency to add
*/
protected function addDependency(string $package, string $dependency): void
{
$dependencies = $this->getDependencies($package);
if (!in_array($dependency, $dependencies))
{
$dependencies[] = $dependency;
$this->setDependencies($package, $dependencies);
}
}
/**
* Removes a package dependency from #__akeeba_common
*
* @param string $package The package
* @param string $dependency The dependency to remove
*/
protected function removeDependency(string $package, string $dependency): void
{
$dependencies = $this->getDependencies($package);
if (in_array($dependency, $dependencies))
{
$index = array_search($dependency, $dependencies);
unset($dependencies[$index]);
$this->setDependencies($package, $dependencies);
}
}
/**
* Do I have a dependency for a package in #__akeeba_common
*
* @param string $package The package
* @param string $dependency The dependency to check for
*
* @return bool
*/
protected function hasDependency(string $package, string $dependency): bool
{
$dependencies = $this->getDependencies($package);
return in_array($dependency, $dependencies);
}
/**
* Adds or updates a post-installation message (PIM) definition for Joomla! 3.2 or later. You can use this in your
* post-installation script using this code:
*
* The $options array contains the following mandatory keys:
*
* extension_id The numeric ID of the extension this message is for (see the #__extensions table)
*
* type One of message, link or action. Their meaning is:
* message Informative message. The user can dismiss it.
* link The action button links to a URL. The URL is defined in the action parameter.
* action A PHP action takes place when the action button is clicked. You need to specify the
* action_file (RAD path to the PHP file) and action (PHP function name) keys. See
* below for more information.
*
* title_key The Text language key for the title of this PIM
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE
*
* description_key The Text language key for the main body (description) of this PIM
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION
*
* action_key The Text language key for the action button. Ignored and not required when type=message
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION
*
* language_extension The extension name which holds the language keys used above. For example, com_foobar,
* mod_something, plg_system_whatever, tpl_mytemplate
*
* language_client_id Should we load the front-end (0) or back-end (1) language keys?
*
* version_introduced Which was the version of your extension where this message appeared for the first time?
* Example: 3.2.1
*
* enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1.
*
* condition_file The RAD path to a PHP file containing a PHP function which determines whether this message
* should be shown to the user. @param array $options See description
*
* @return void
*
* @throws Exception
* @see Template::parsePath() for RAD path format. Joomla! will include this file
* before calling the function defined in the action key below.
* Example: admin://components/com_foobar/helpers/postinstall.php
*
* action The name of a PHP function which will be used to run the action of this PIM. This must be
* a
* simple PHP user function (not a class method, static method etc) which returns no result.
* Example: com_foobar_postinstall_messageone_action
*
* @see Template::parsePath() for RAD path format. Joomla!
* will include this file before calling the condition_method.
* Example: admin://components/com_foobar/helpers/postinstall.php
*
* condition_method The name of a PHP function which will be used to determine whether to show this message to
* the user. This must be a simple PHP user function (not a class method, static method etc)
* which returns true to show the message and false to hide it. This function is defined in
* the condition_file. Example: com_foobar_postinstall_messageone_condition
*
* When type=message no additional keys are required.
*
* When type=link the following additional keys are required:
*
* action The URL which will open when the user clicks on the PIM's action button
* Example: index.php?option=com_foobar&view=tools&task=installSampleData
*
* Then type=action the following additional keys are required:
*
* action_file The RAD path to a PHP file containing a PHP function which performs the action of this
* PIM.
*
*/
protected function addPostInstallationMessage(array $options): void
{
// Make sure there are options set
if (!is_array($options))
{
throw new Exception('Post-installation message definitions must be of type array', 500);
}
// Initialise array keys
$defaultOptions = [
'extension_id' => '',
'type' => '',
'title_key' => '',
'description_key' => '',
'action_key' => '',
'language_extension' => '',
'language_client_id' => '',
'action_file' => '',
'action' => '',
'condition_file' => '',
'condition_method' => '',
'version_introduced' => '',
'enabled' => '1',
];
$options = array_merge($defaultOptions, $options);
// Array normalisation. Removes array keys not belonging to a definition.
$defaultKeys = array_keys($defaultOptions);
$allKeys = array_keys($options);
$extraKeys = array_diff($allKeys, $defaultKeys);
foreach ($extraKeys as $key)
{
unset($options[$key]);
}
// Normalisation of integer values
$options['extension_id'] = (int) $options['extension_id'];
$options['language_client_id'] = (int) $options['language_client_id'];
$options['enabled'] = (int) $options['enabled'];
// Normalisation of 0/1 values
foreach (['language_client_id', 'enabled'] as $key)
{
$options[$key] = $options[$key] ? 1 : 0;
}
// Make sure there's an extension_id
if (!(int) $options['extension_id'])
{
throw new Exception('Post-installation message definitions need an extension_id', 500);
}
// Make sure there's a valid type
if (!in_array($options['type'], ['message', 'link', 'action']))
{
throw new Exception('Post-installation message definitions need to declare a type of message, link or action', 500);
}
// Make sure there's a title key
if (empty($options['title_key']))
{
throw new Exception('Post-installation message definitions need a title key', 500);
}
// Make sure there's a description key
if (empty($options['description_key']))
{
throw new Exception('Post-installation message definitions need a description key', 500);
}
// If the type is anything other than message you need an action key
if (($options['type'] != 'message') && empty($options['action_key']))
{
throw new Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500);
}
// You must specify the language extension
if (empty($options['language_extension']))
{
throw new Exception('Post-installation message definitions need to specify which extension contains their language keys', 500);
}
try
{
$container = Container::getInstance($this->componentName);
}
catch (Exception $e)
{
$container = Container::getInstance('com_fake');
}
$templateUtils = new Template($container);
// The action file and method are only required for the "action" type
if ($options['type'] == 'action')
{
if (empty($options['action_file']))
{
throw new Exception('Post-installation message definitions need an action file when they are of type "action"', 500);
}
$file_path = $templateUtils->parsePath($options['action_file'], true);
if (!@is_file($file_path))
{
throw new Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500);
}
if (empty($options['action']))
{
throw new Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500);
}
}
if (($options['type'] == 'link') && empty($options['link']))
{
throw new Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500);
}
// The condition file and method are only required when the type is not "message"
if ($options['type'] != 'message')
{
if (empty($options['condition_file']))
{
throw new Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500);
}
$file_path = $templateUtils->parsePath($options['condition_file'], true);
if (!@is_file($file_path))
{
throw new Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500);
}
if (empty($options['condition_method']))
{
throw new Exception('Post-installation message definitions need a condition method (function name) when they are of type "' . $options['type'] . '"', 500);
}
}
// Check if the definition exists
$tableName = '#__postinstall_messages';
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->qn($tableName))
->where($db->qn('extension_id') . ' = ' . $db->q($options['extension_id']))
->where($db->qn('type') . ' = ' . $db->q($options['type']))
->where($db->qn('title_key') . ' = ' . $db->q($options['title_key']));
$existingRow = $db->setQuery($query)->loadAssoc();
// Is the existing definition the same as the one we're trying to save (ignore the enabled flag)?
if (!empty($existingRow))
{
$same = true;
foreach ($options as $k => $v)
{
if ($k == 'enabled')
{
continue;
}
if ($existingRow[$k] != $v)
{
$same = false;
break;
}
}
// Trying to add the same row as the existing one; quit
if ($same)
{
return;
}
// Otherwise it's not the same row. Remove the old row before insert a new one.
$query = $db->getQuery(true)
->delete($db->qn($tableName))
->where($db->q('extension_id') . ' = ' . $db->q($options['extension_id']))
->where($db->q('type') . ' = ' . $db->q($options['type']))
->where($db->q('title_key') . ' = ' . $db->q($options['title_key']));
$db->setQuery($query)->execute();
}
// Insert the new row
$options = (object) $options;
$db->insertObject($tableName, $options);
}
/**
* Applies the post-installation messages for Joomla! 3.2 or later
*
* @return void
*/
protected function _applyPostInstallationMessages(): void
{
// Make sure there are post-installation messages
if (empty($this->postInstallationMessages))
{
return;
}
// Get the extension ID for our component
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
try
{
$ids = $db->loadColumn();
}
catch (Exception $exc)
{
return;
}
if (empty($ids))
{
return;
}
$extension_id = array_shift($ids);
foreach ($this->postInstallationMessages as $message)
{
$message['extension_id'] = $extension_id;
$this->addPostInstallationMessage($message);
}
}
/**
* Uninstalls the post-installation messages for Joomla! 3.2 or later
*
* @return void
*/
protected function uninstallPostInstallationMessages(): void
{
// Make sure there are post-installation messages
if (empty($this->postInstallationMessages))
{
return;
}
// Get the extension ID for our component
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
try
{
$ids = $db->loadColumn();
}
catch (Exception $exc)
{
return;
}
if (empty($ids))
{
return;
}
$extension_id = array_shift($ids);
$query = $db->getQuery(true)
->delete($db->qn('#__postinstall_messages'))
->where($db->qn('extension_id') . ' = ' . $db->q($extension_id));
try
{
$db->setQuery($query)->execute();
}
catch (Exception $e)
{
return;
}
}
/**
* Uninstalls FOF 3 if nothing else depends on it.
*
* @return void
*/
protected function uninstallFOF3IfNecessary()
{
// Only uninstall FOF 3.x when no other software depends on it still.
if (count($this->getDependencies('fof30')) !== 0)
{
return;
}
// We will look for both legacy lib_fof30 and newer file_fof30 package types
$packages = [
'library' => 'lib_fof30',
'file' => 'file_fof30',
];
$db = JoomlaFactory::getDbo();
foreach ($packages as $type => $element)
{
// Get the extension ID for the FOF 3.x package we're uninstalling
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q($type))
->where($db->qn('element') . ' = ' . $db->q($element));
$db->setQuery($query);
try
{
$id = $db->loadResult();
}
catch (Exception $exc)
{
continue;
}
// Was the extension installed anyway?
if (empty($id))
{
continue;
}
// Okay, try to uninstall it. Failure is always an option.
try
{
(new JoomlaInstaller)->uninstall($type, $id);
}
catch (Throwable $e)
{
continue;
}
}
}
/**
* Uninstalls FOF 4 if nothing else depends on it.
*
* @return void
*/
protected function uninstallFOF4IfNecessary()
{
// Only uninstall FOF 3.x when no other software depends on it still.
if (count($this->getDependencies('fof40')) !== 0)
{
return;
}
$packages = [
'file' => 'file_fof40',
];
$db = JoomlaFactory::getDbo();
foreach ($packages as $type => $element)
{
// Get the extension ID for the FOF 3.x package we're uninstalling
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q($type))
->where($db->qn('element') . ' = ' . $db->q($element));
$db->setQuery($query);
try
{
$id = $db->loadResult();
}
catch (Exception $exc)
{
continue;
}
// Was the extension installed anyway?
if (empty($id))
{
continue;
}
// Okay, try to uninstall it. Failure is always an option.
try
{
(new JoomlaInstaller)->uninstall($type, $id);
}
catch (Throwable $e)
{
continue;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,238 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\InstallScript;
defined('_JEXEC') || die;
use Exception;
use FOF40\Database\Installer as DatabaseInstaller;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Installer\Adapter\ModuleAdapter;
use Joomla\CMS\Log\Log;
// In case FOF's autoloader is not present yet, e.g. new installation
if (!class_exists('FOF40\\InstallScript\\BaseInstaller', true))
{
require_once __DIR__ . '/BaseInstaller.php';
}
/**
* A helper class which you can use to create module installation scripts.
*
* Example usage: class Mod_ExampleInstallerScript extends FOF40\Utils\InstallScript\Module
*
* This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
* Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
*/
class Module extends BaseInstaller
{
/**
* Which side of the site is this module installed in? Use 'site' or 'administrator'.
*
* @var string
*/
protected $moduleClient = 'site';
/**
* The modules's name, e.g. mod_foobar. Auto-filled from the class name.
*
* @var string
*/
protected $moduleName = '';
/**
* The path where the schema XML files are stored. The path is relative to the folder which contains the extension's
* files.
*
* @var string
*/
protected $schemaXmlPath = 'sql/xml';
/**
* Module installer script constructor.
*/
public function __construct()
{
// Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary.
if (empty($this->moduleName))
{
$class = get_class($this);
$words = preg_replace('/(\s)+/', '_', $class);
$words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words));
$classParts = explode('_', $words);
$this->moduleName = 'mod_' . $classParts[2];
}
}
/**
* Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to
* tell Joomla! if it should abort the installation.
*
* @param string $type Installation type (install, update, discover_install)
* @param ModuleAdapter $parent Parent object
*
* @return boolean True to let the installation proceed, false to halt the installation
* @noinspection PhpUnusedParameterInspection
*/
public function preflight(string $type, ModuleAdapter $parent): bool
{
// Do not run on uninstall.
if ($type === 'uninstall')
{
return true;
}
// Check the minimum PHP version
if (!$this->checkPHPVersion())
{
return false;
}
// Check the minimum Joomla! version
if (!$this->checkJoomlaVersion())
{
return false;
}
// Clear op-code caches to prevent any cached code issues
$this->clearOpcodeCaches();
return true;
}
/**
* Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
* or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
* database updates and similar housekeeping functions.
*
* @param string $type install, update or discover_update
* @param ModuleAdapter $parent Parent object
*
* @return void
* @throws Exception
*
*/
public function postflight(string $type, ModuleAdapter $parent): void
{
// Do not run on uninstall.
if ($type === 'uninstall')
{
return;
}
// Add ourselves to the list of extensions depending on FOF40
$this->addDependency('fof40', $this->getDependencyName());
$this->removeDependency('fof30', $this->getDependencyName());
// Install or update database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
if (@is_dir($schemaPath))
{
$dbInstaller = new DatabaseInstaller(JoomlaFactory::getDbo(), $schemaPath);
$dbInstaller->updateSchema();
}
// Make sure everything is copied properly
$this->bugfixFilesNotCopiedOnUpdate($parent);
// Add post-installation messages on Joomla! 3.2 and later
$this->_applyPostInstallationMessages();
// Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded.
$this->clearOpcodeCaches();
// Finally, see if FOF 3.x is obsolete and remove it.
// $this->uninstallFOF3IfNecessary();
}
/**
* Runs on uninstallation
*
* @param ModuleAdapter $parent The parent object
*/
public function uninstall(ModuleAdapter $parent): void
{
// Uninstall database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
// Uninstall database
if (@is_dir($schemaPath))
{
$dbInstaller = new DatabaseInstaller(JoomlaFactory::getDbo(), $schemaPath);
$dbInstaller->removeSchema();
}
// Uninstall post-installation messages on Joomla! 3.2 and later
$this->uninstallPostInstallationMessages();
// Remove ourselves from the list of extensions depending of FOF 4
$this->removeDependency('fof40', $this->getDependencyName());
// Uninstall FOF 4 if nothing else depends on it
$this->uninstallFOF4IfNecessary();
}
/**
* Fix for Joomla bug: sometimes files are not copied on update.
*
* We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files /
* folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more
* added / modified files and folders you have. We are trying to work around it by retrying the copy operation
* ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's
* extensions.
*
* @param ModuleAdapter $parent
*/
protected function bugfixFilesNotCopiedOnUpdate(ModuleAdapter $parent): void
{
Log::add("Joomla! extension update workaround for $this->moduleClient module $this->moduleName", Log::INFO, 'fof4_extension_installation');
$temporarySource = $parent->getParent()->getPath('source');
$rootFolder = ($this->moduleClient == 'site') ? JPATH_SITE : JPATH_ADMINISTRATOR;
$copyMap = [
// Module files
$temporarySource => $rootFolder . '/modules/' . $this->moduleName,
// Language
$temporarySource . '/language' => $rootFolder . '/language',
// Media files
$temporarySource . '/media' => JPATH_ROOT . '/media/' . $this->moduleName,
];
foreach ($copyMap as $source => $target)
{
\Joomla\CMS\Log\Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof4_extension_installation');
$ignored = [];
if ($source === $temporarySource)
{
$ignored = [
'index.html', 'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md',
'script.php', 'language', 'media',
];
}
$this->recursiveConditionalCopy($source, $target, $ignored);
}
}
/**
* Get the extension name for FOF dependency tracking, e.g. mod_site_foobar
*
* @return string
*/
protected function getDependencyName(): string
{
return 'mod_' . strtolower($this->moduleClient) . '_' . substr($this->moduleName, 4);
}
}

View File

@ -0,0 +1,250 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\InstallScript;
defined('_JEXEC') || die;
use Exception;
use FOF40\Database\Installer;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Installer\Adapter\PluginAdapter;
use Joomla\CMS\Log\Log;
// In case FOF's autoloader is not present yet, e.g. new installation
if (!class_exists('FOF40\\InstallScript\\BaseInstaller', true))
{
require_once __DIR__ . '/BaseInstaller.php';
}
/**
* A helper class which you can use to create plugin installation scripts.
*
* Example usage: class PlgSystemExampleInstallerScript extends FOF40\Utils\InstallScript\Module
*
* NB: The class name is always Plg<Plugin Folder><Plugin Name>InstallerScript per Joomla's conventions.
*
* This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
* Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
*/
class Plugin extends BaseInstaller
{
/**
* The plugins's name, e.g. foobar (for plg_system_foobar). Auto-filled from the class name.
*
* @var string
*/
protected $pluginName = '';
/**
* The plugins's folder, e.g. system (for plg_system_foobar). Auto-filled from the class name.
*
* @var string
*/
protected $pluginFolder = '';
/**
* The path where the schema XML files are stored. The path is relative to the folder which contains the extension's
* files.
*
* @var string
*/
protected $schemaXmlPath = 'sql/xml';
/**
* Plugin installer script constructor.
*/
public function __construct()
{
// Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary.
if (empty($this->pluginFolder) || empty($this->pluginName))
{
$class = get_class($this);
$words = preg_replace('/(\s)+/', '_', $class);
$words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words));
$classParts = explode('_', $words);
if (empty($this->pluginFolder))
{
$this->pluginFolder = $classParts[1];
}
if (empty($this->pluginName))
{
$this->pluginName = $classParts[2];
}
}
}
/**
* Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to
* tell Joomla! if it should abort the installation.
*
* @param string $type Installation type (install, update, discover_install)
* @param PluginAdapter $parent Parent object
*
* @return boolean True to let the installation proceed, false to halt the installation
* @noinspection PhpUnusedParameterInspection
*/
public function preflight(string $type, PluginAdapter $parent): bool
{
// Do not run on uninstall.
if ($type === 'uninstall')
{
return true;
}
// Check the minimum PHP version
if (!$this->checkPHPVersion())
{
return false;
}
// Check the minimum Joomla! version
if (!$this->checkJoomlaVersion())
{
return false;
}
// Clear op-code caches to prevent any cached code issues
$this->clearOpcodeCaches();
return true;
}
/**
* Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
* or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
* database updates and similar housekeeping functions.
*
* @param string $type install, update or discover_update
* @param PluginAdapter $parent Parent object
*
* @return void
* @throws Exception
*
*/
public function postflight(string $type, PluginAdapter $parent): void
{
// Do not run on uninstall.
if ($type === 'uninstall')
{
return;
}
// Add ourselves to the list of extensions depending on FOF40
$dependencyName = $this->getDependencyName();
$this->addDependency('fof40', $dependencyName);
$this->removeDependency('fof30', $dependencyName);
// Install or update database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
if (@is_dir($schemaPath))
{
$dbInstaller = new Installer(JoomlaFactory::getDbo(), $schemaPath);
$dbInstaller->updateSchema();
}
// Make sure everything is copied properly
$this->bugfixFilesNotCopiedOnUpdate($parent);
// Add post-installation messages on Joomla! 3.2 and later
$this->_applyPostInstallationMessages();
// Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded.
$this->clearOpcodeCaches();
// Finally, see if FOF 3.x is obsolete and remove it.
// $this->uninstallFOF3IfNecessary();
}
/**
* Runs on uninstallation
*
* @param PluginAdapter $parent The parent object
*/
public function uninstall(PluginAdapter $parent): void
{
// Uninstall database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
// Uninstall database
if (@is_dir($schemaPath))
{
$dbInstaller = new Installer(JoomlaFactory::getDbo(), $schemaPath);
$dbInstaller->removeSchema();
}
// Uninstall post-installation messages on Joomla! 3.2 and later
$this->uninstallPostInstallationMessages();
// Remove ourselves from the list of extensions depending on FOF40
$dependencyName = $this->getDependencyName();
// Remove ourselves from the list of extensions depending of FOF 4
$this->removeDependency('fof40', $dependencyName);
// Uninstall FOF 4 if nothing else depends on it
$this->uninstallFOF4IfNecessary();
}
/**
* Fix for Joomla bug: sometimes files are not copied on update.
*
* We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files /
* folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more
* added / modified files and folders you have. We are trying to work around it by retrying the copy operation
* ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's
* extensions.
*
* @param PluginAdapter $parent
*/
protected function bugfixFilesNotCopiedOnUpdate(PluginAdapter $parent): void
{
Log::add("Joomla! extension update workaround for $this->pluginFolder plugin $this->pluginName", Log::INFO, 'fof4_extension_installation');
$temporarySource = $parent->getParent()->getPath('source');
$copyMap = [
// Plugin files
$temporarySource => JPATH_ROOT . '/plugins/' . $this->pluginFolder . '/' . $this->pluginName,
// Language (always stored in administrator for plugins)
$temporarySource . '/language' => JPATH_ADMINISTRATOR . '/language',
// Media files, e.g. /media/plg_system_foobar
$temporarySource . '/media' => JPATH_ROOT . '/media/' . $this->getDependencyName(),
];
foreach ($copyMap as $source => $target)
{
Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof4_extension_installation');
$ignored = [];
if ($source === $temporarySource)
{
$ignored = [
'index.html', 'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md',
'script.php', 'language', 'media',
];
}
$this->recursiveConditionalCopy($source, $target, $ignored);
}
}
/**
* Get the extension name for FOF dependency tracking, e.g. plg_system_foobar
*
* @return string
*/
protected function getDependencyName(): string
{
return 'plg_' . strtolower($this->pluginFolder) . '_' . $this->pluginName;
}
}

View File

@ -0,0 +1,350 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\JoomlaAbstraction;
defined('_JEXEC') || die;
use Exception;
use FOF40\Container\Container;
use Joomla\Application\AbstractApplication;
use Joomla\Application\ConfigurationAwareApplicationInterface;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Cache\Controller\CallbackController;
use Joomla\CMS\Cache\Exception\CacheExceptionInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Registry\Registry;
use Throwable;
/**
* A utility class to help you quickly clean the Joomla! cache
*/
class CacheCleaner
{
/**
* Clears the com_modules and com_plugins cache. You need to call this whenever you alter the publish state or
* parameters of a module or plugin from your code.
*
* @return void
*/
public static function clearPluginsAndModulesCache()
{
self::clearPluginsCache();
self::clearModulesCache();
}
/**
* Clears the com_plugins cache. You need to call this whenever you alter the publish state or parameters of a
* plugin from your code.
*
* @return void
*/
public static function clearPluginsCache()
{
self::clearCacheGroups(['com_plugins'], [0, 1]);
}
/**
* Clears the com_modules cache. You need to call this whenever you alter the publish state or parameters of a
* module from your code.
*
* @return void
*/
public static function clearModulesCache()
{
self::clearCacheGroups(['com_modules'], [0, 1]);
}
/**
* Clears the specified cache groups.
*
* @param array $clearGroups Which cache groups to clear. Usually this is com_yourcomponent to clear
* your component's cache.
* @param array $cacheClients Which cache clients to clear. 0 is the back-end, 1 is the front-end. If you
* do not specify anything, both cache clients will be cleared.
* @param string|null $event An event to run upon trying to clear the cache. Empty string to disable. If
* NULL and the group is "com_content" I will trigger onContentCleanCache.
*
* @return void
* @throws Exception
*/
public static function clearCacheGroups(array $clearGroups, array $cacheClients = [
0, 1,
], ?string $event = null): void
{
// Early return on nonsensical input
if (empty($clearGroups) || empty($cacheClients))
{
return;
}
// Make sure I have an application object
try
{
$app = Factory::getApplication();
}
catch (Exception $e)
{
return;
}
// If there's no application object things will break; let's get outta here.
if (!is_object($app))
{
return;
}
$isJoomla4 = version_compare(JVERSION, '3.9999.9999', 'gt');
// Loop all groups to clean
foreach ($clearGroups as $group)
{
// Groups must be non-empty strings
if (empty($group) || !is_string($group))
{
continue;
}
// Loop all clients (applications)
foreach ($cacheClients as $client_id)
{
$client_id = (int) ($client_id ?? 0);
$options = $isJoomla4
? self::clearCacheGroupJoomla4($group, $client_id, $app)
: self::clearCacheGroupJoomla3($group, $client_id, $app);
// Do not call any events if I failed to clean the cache using the core Joomla API
if (!($options['result'] ?? false))
{
return;
}
/**
* If you're cleaning com_content and you have passed no event name I will use onContentCleanCache.
*/
if ($group === 'com_content')
{
$cacheCleaningEvent = $event ?: 'onContentCleanCache';
}
/**
* Call Joomla's cache cleaning plugin event (e.g. onContentCleanCache) as well.
*
* @see BaseDatabaseModel::cleanCache()
*/
if (empty($cacheCleaningEvent))
{
continue;
}
$fakeContainer = Container::getInstance('com_FOOBAR');
$fakeContainer->platform->runPlugins($cacheCleaningEvent, $options);
}
}
}
/**
* Clean a cache group on Joomla 3
*
* @param string $group The cache to clean, e.g. com_content
* @param int $client_id The application ID for which the cache will be cleaned
* @param object $app The current CMS application. DO NOT TYPEHINT MORE SPECIFICALLY!
*
* @return array Cache controller options, including cleaning result
* @throws Exception
*/
private static function clearCacheGroupJoomla3(string $group, int $client_id, object $app): array
{
$options = [
'defaultgroup' => $group,
'cachebase' => ($client_id) ? self::getAppConfigParam($app, 'cache_path', JPATH_SITE . '/cache') : JPATH_ADMINISTRATOR . '/cache',
'result' => true,
];
try
{
$cache = Cache::getInstance('callback', $options);
/** @noinspection PhpUndefinedMethodInspection Available via __call(), not tagged in Joomla core */
$cache->clean();
}
catch (Throwable $e)
{
$options['result'] = false;
}
return $options;
}
/**
* Clean a cache group on Joomla 4
*
* @param string $group The cache to clean, e.g. com_content
* @param int $client_id The application ID for which the cache will be cleaned
* @param object $app The current CMS application. DO NOT TYPEHINT MORE SPECIFICALLY!
*
* @return array Cache controller options, including cleaning result
* @throws Exception
*/
private static function clearCacheGroupJoomla4(string $group, int $client_id, object $app): array
{
// Get the default cache folder. Start by using the JPATH_CACHE constant.
$cacheBaseDefault = JPATH_CACHE;
$appClientId = 0;
if (method_exists($app, 'getClientId'))
{
$appClientId = $app->getClientId();
}
// -- If we are asked to clean cache on the other side of the application we need to find a new cache base
if ($client_id != $appClientId)
{
$cacheBaseDefault = (($client_id) ? JPATH_SITE : JPATH_ADMINISTRATOR) . '/cache';
}
// Get the cache controller's options
$options = [
'defaultgroup' => $group,
'cachebase' => self::getAppConfigParam($app, 'cache_path', $cacheBaseDefault),
'result' => true,
];
try
{
$container = Factory::getContainer();
if (empty($container))
{
throw new \RuntimeException('Cannot get Joomla 4 application container');
}
/** @var CacheControllerFactoryInterface $cacheControllerFactory */
$cacheControllerFactory = $container->get('cache.controller.factory');
if (empty($cacheControllerFactory))
{
throw new \RuntimeException('Cannot get Joomla 4 cache controller factory');
}
/** @var CallbackController $cache */
$cache = $cacheControllerFactory->createCacheController('callback', $options);
if (empty($cache) || !property_exists($cache, 'cache') || !method_exists($cache->cache, 'clean'))
{
throw new \RuntimeException('Cannot get Joomla 4 cache controller');
}
$cache->cache->clean();
}
catch (CacheExceptionInterface $exception)
{
$options['result'] = false;
}
catch (Throwable $e)
{
$options['result'] = false;
}
return $options;
}
private static function getAppConfigParam(?object $app, string $key, $default = null)
{
/**
* Any kind of Joomla CMS, Web, API or CLI application extends from AbstractApplication and has the get()
* method to return application configuration parameters.
*/
if (is_object($app) && ($app instanceof AbstractApplication))
{
return $app->get($key, $default);
}
/**
* A custom application may instead implement the Joomla\Application\ConfigurationAwareApplicationInterface
* interface (Joomla 4+), in whihc case it has the get() method to return application configuration parameters.
*/
if (is_object($app)
&& interface_exists('Joomla\Application\ConfigurationAwareApplicationInterface', true)
&& ($app instanceof ConfigurationAwareApplicationInterface))
{
return $app->get($key, $default);
}
/**
* A Joomla 3 custom application may simply implement the get() method without implementing an interface.
*/
if (is_object($app) && method_exists($app, 'get'))
{
return $app->get($key, $default);
}
/**
* At this point the $app variable is not an object or is something I can't use. Does the Joomla Factory still
* has the legacy static method getConfig() to get the application configuration? If so, use it.
*/
if (method_exists(Factory::class, 'getConfig'))
{
try
{
$jConfig = Factory::getConfig();
if (is_object($jConfig) && ($jConfig instanceof Registry))
{
$jConfig->get($key, $default);
}
}
catch (Throwable $e)
{
/**
* Factory tries to go through the application object. It might fail if there is a custom application
* which doesn't implement the interfaces Factory expects. In this case we get a Fatal Error whcih we
* can trap and fall through to the next if-block.
*/
}
}
/**
* When we are here all hope is nearly lost. We have to do a crude approximation of Joomla Factory's code to
* create an application configuration Registry object and retrieve the configuration values. This will work as
* long as the JConfig class (defined in configuration.php) has been loaded.
*/
$configPath = defined('JPATH_CONFIGURATION') ? JPATH_CONFIGURATION :
(defined('JPATH_ROOT') ? JPATH_ROOT : null);
$configPath = $configPath ?? (__DIR__ . '/../../..');
$configFile = $configPath . '/configuration.php';
if (!class_exists('JConfig') && @file_exists($configFile) && @is_file($configFile) && @is_readable($configFile))
{
require_once $configFile;
}
if (class_exists('JConfig'))
{
try
{
$jConfig = new Registry();
$configObject = new \JConfig();
$jConfig->loadObject($configObject);
return $jConfig->get($key, $default);
}
catch (Throwable $e)
{
return $default;
}
}
/**
* All hope is lost. I can't find the application configuration. I am returning the default value and hope stuff
* won't break spectacularly...
*/
return $default;
}
}

View File

@ -0,0 +1,158 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\JoomlaAbstraction;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Factory as JoomlaFactory;
use SimpleXMLElement;
/**
* Retrieve the version of a component from the cached XML manifest or, if it's not present, the version recorded in the
* database.
*/
abstract class ComponentVersion
{
/**
* A cache with the version numbers of components
*
* @var array
*
* @since 3.1.5
*/
private static $version = array();
/**
* Get a component's version. The XML manifest on disk will be tried first. If it's not there or does not have a
* version string the manifest cache in the database is tried. If that fails a fake version number will be returned.
*
* @param string $component The name of the component, e.g. com_foobar
*
* @return string The version string
*
* @since 3.1.5
*/
public static function getFor(string $component): string
{
if (!isset(self::$version[$component]))
{
self::$version[$component] = null;
}
if (is_null(self::$version[$component]))
{
self::$version[$component] = self::getVersionFromManifest($component);
}
if (is_null(self::$version[$component]))
{
self::$version[$component] = self::getVersionFromDatabase($component);
}
if (is_null(self::$version[$component]))
{
self::$version[$component] = 'dev-' . str_replace(' ', '_', microtime(false));
}
return self::$version[$component];
}
/**
* Get a component's version from the manifest cache in the database
*
* @param string $component The component's bname
*
* @return string|null The component version or null if none is defined
*
* @since 3.1.5
*/
private static function getVersionFromDatabase(string $component): ?string
{
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)
->select($db->qn('manifest_cache'))
->from($db->qn('#__extensions'))
->where($db->qn('element') . ' = ' . $db->q($component))
->where($db->qn('type') . ' = ' . $db->q('component'));
try
{
$json = $db->setQuery($query)->loadResult();
}
catch (Exception $e)
{
return null;
}
if (empty($json))
{
return null;
}
$options = json_decode($json, true);
if (empty($options))
{
return null;
}
if (!isset($options['version']))
{
return null;
}
return $options['version'];
}
/**
* Get a component's version from the manifest file on disk. IMPORTANT! The manifest for com_something must be named
* something.xml.
*
* @param string $component The component's bname
*
* @return string The component version or null if none is defined
*
* @since 1.2.0
*/
private static function getVersionFromManifest(string $component): ?string
{
$bareComponent = str_replace('com_', '', $component);
$file = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . $bareComponent . '.xml';
if (!is_file($file) || !is_readable($file))
{
return null;
}
$data = @file_get_contents($file);
if (empty($data))
{
return null;
}
try
{
$xml = new SimpleXMLElement($data, LIBXML_COMPACT | LIBXML_NONET | LIBXML_ERR_NONE);
}
catch (Exception $e)
{
return null;
}
$versionNode = $xml->xpath('/extension/version');
if (empty($versionNode))
{
return null;
}
return (string)($versionNode[0]);
}
}

View File

@ -0,0 +1,196 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\JoomlaAbstraction;
defined('_JEXEC') || die;
use FOF40\Container\Container;
/**
* Dynamic user to user group assignment.
*
* This class allows you to add / remove the currently logged in user to a user group without writing the information to
* the database. This is useful when you want to allow core and third party code to allow or prohibit display of
* information and / or taking actions based on a condition controlled in your code.
*/
class DynamicGroups
{
/**
* Add the current user to a user group just for this page load.
*
* @param int $groupID The group ID to add the current user into.
*
* @return void
*/
public static function addGroup(int $groupID): void
{
self::addRemoveGroup($groupID, true);
self::cleanUpUserObjectCache();
}
/**
* Remove the current user from a user group just for this page load.
*
* @param int $groupID The group ID to remove the current user from.
*
* @return void
*/
public static function removeGroup(int $groupID): void
{
self::addRemoveGroup($groupID, false);
self::cleanUpUserObjectCache();
}
/**
* Internal function to add or remove the current user from a user group just for this page load.
*
* @param int $groupID The group ID to add / remove the current user from.
* @param bool $add Add (true) or remove (false) the user?
*
* @return void
*/
protected static function addRemoveGroup(int $groupID, bool $add): void
{
// Get a fake container (we need it for its platform interface)
$container = Container::getInstance('com_FOOBAR');
/**
* Make sure that Joomla has retrieved the user's groups from the database.
*
* By going through the User object's getAuthorisedGroups we force Joomla to go through Access::getGroupsByUser
* which retrieves the information from the database and caches it into the Access helper class.
*/
$container->platform->getUser()->getAuthorisedGroups();
$container->platform->getUser($container->platform->getUser()->id)->getAuthorisedGroups();
/**
* Now we can get a Reflection object into Joomla's Access helper class and manipulate its groupsByUser cache.
*/
$className = 'Joomla\\CMS\\Access\\Access';
try
{
$reflectedAccess = new \ReflectionClass($className);
}
catch (\ReflectionException $e)
{
// This should never happen!
$container->platform->logDebug('Cannot locate the Joomla\\CMS\\Access\\Access class. Is your Joomla installation broken or too old / too new?');
return;
}
$groupsByUser = $reflectedAccess->getProperty('groupsByUser');
$groupsByUser->setAccessible(true);
$rawGroupsByUser = $groupsByUser->getValue();
/**
* Next up, we need to manipulate the keys of the cache which contain user to user group assignments.
*
* $rawGroupsByUser (Access::$groupsByUser) stored the group ownership as userID:recursive e.g. 0:1 for the
* default user, recursive. We need to deal with four keys: 0:1, 0:0, myID:1 and myID:0
*/
$user = $container->platform->getUser();
$keys = ['0:1', '0:0', $user->id . ':1', $user->id . ':0'];
foreach ($keys as $key)
{
if (!array_key_exists($key, $rawGroupsByUser))
{
continue;
}
$groups = $rawGroupsByUser[$key];
if ($add)
{
if (in_array($groupID, $groups))
{
continue;
}
$groups[] = $groupID;
}
else
{
if (!in_array($groupID, $groups))
{
continue;
}
$removeKey = array_search($groupID, $groups);
unset($groups[$removeKey]);
}
$rawGroupsByUser[$key] = $groups;
}
// We can commit our changes back to the cache property and make it publicly inaccessible again.
$groupsByUser->setValue(null, $rawGroupsByUser);
$groupsByUser->setAccessible(false);
/**
* We are not done. Caching user groups is only one aspect of Joomla access management. Joomla also caches the
* identities, i.e. the user group assignment per user, in a different cache. We need to reset it to for our
* user.
*
* Do note that we CAN NOT use clearStatics since that also clears the user group assignment which we assigned
* dynamically. Therefore calling it would destroy our work so far.
*/
$refProperty = $reflectedAccess->getProperty('identities');
$refProperty->setAccessible(true);
$identities = $refProperty->getValue();
$keys = array($user->id, 0);
foreach ($keys as $key)
{
if (!array_key_exists($key, $identities))
{
continue;
}
unset($identities[$key]);
}
$refProperty->setValue(null, $identities);
$refProperty->setAccessible(false);
}
/**
* Clean up the current user's authenticated groups cache.
*
* @return void
*/
protected static function cleanUpUserObjectCache(): void
{
// Get a fake container (we need it for its platform interface)
$container = Container::getInstance('com_FOOBAR');
$user = $container->platform->getUser();
$reflectedUser = new \ReflectionObject($user);
// Clear the user group cache
$refProperty = $reflectedUser->getProperty('_authGroups');
$refProperty->setAccessible(true);
$refProperty->setValue($user, array());
$refProperty->setAccessible(false);
// Clear the view access level cache
$refProperty = $reflectedUser->getProperty('_authLevels');
$refProperty->setAccessible(true);
$refProperty->setValue($user, array());
$refProperty->setAccessible(false);
// Clear the authenticated actions cache. I haven't seen it used anywhere but it's there, so...
$refProperty = $reflectedUser->getProperty('_authActions');
$refProperty->setAccessible(true);
$refProperty->setValue($user, array());
$refProperty->setAccessible(false);
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Layout;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use Joomla\CMS\Layout\FileLayout;
/**
* Base class for rendering a display layout
* loaded from from a layout file
*
* This class searches for Joomla! version override Layouts. For example,
* if you have run this under Joomla! 3.0 and you try to load
* mylayout.default it will automatically search for the
* layout files default.j30.php, default.j3.php and default.php, in this
* order.
*
* @package FrameworkOnFramework
*/
class LayoutFile extends FileLayout
{
/** @var Container The component container */
public $container;
/**
* Method to finds the full real file path, checking possible overrides
*
* @return string The full path to the layout file
*/
protected function getPath()
{
if (is_null($this->container))
{
$component = $this->options->get('component');
$this->container = Container::getInstance($component);
}
$filesystem = $this->container->filesystem;
if (is_null($this->fullPath) && !empty($this->layoutId))
{
$parts = explode('.', $this->layoutId);
$file = array_pop($parts);
$filePath = implode('/', $parts);
$suffixes = $this->container->platform->getTemplateSuffixes();
foreach ($suffixes as $suffix)
{
$files[] = $file . $suffix . '.php';
}
$files[] = $file . '.php';
$platformDirs = $this->container->platform->getPlatformBaseDirs();
$prefix = $this->container->platform->isBackend() ? $platformDirs['admin'] : $platformDirs['root'];
$possiblePaths = [
$prefix . '/templates/' . $this->container->platform->getTemplate() . '/html/layouts/' . $filePath,
$this->basePath . '/' . $filePath,
$platformDirs['root'] . '/layouts/' . $filePath,
];
reset($files);
foreach ($files as $fileName)
{
if (!is_null($this->fullPath))
{
break;
}
$this->fullPath = $filesystem->pathFind($possiblePaths, $fileName);
}
}
return $this->fullPath;
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Layout;
defined('_JEXEC') || die;
use FOF40\Container\Container;
class LayoutHelper
{
/**
* A default base path that will be used if none is provided when calling the render method.
* Note that FileLayout itself will default to JPATH_ROOT . '/layouts' if no basePath is supplied at all
*
* @var string
*/
public static $defaultBasePath = '';
/**
* Method to render the layout.
*
* @param Container $container The container of your component
* @param string $layoutFile Dot separated path to the layout file, relative to base path
* @param array $displayData Array with values to be used inside the layout file to build displayed output
* @param string $basePath Base path to use when loading layout files
*
* @return string
*/
public static function render(Container $container, string $layoutFile, array $displayData = [], string $basePath = ''): string
{
$basePath = empty($basePath) ? self::$defaultBasePath : $basePath;
// Make sure we send null to LayoutFile if no path set
$basePath = empty($basePath) ? null : $basePath;
$layout = new LayoutFile($layoutFile, $basePath);
$layout->container = $container;
return $layout->render($displayData);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,76 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to filter access to items based on the viewing access levels.
*
* @since 2.1
*/
class Access extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
// Make sure the field actually exists
if (!$model->hasField('access'))
{
return;
}
$model->applyAccessFiltering(null);
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('access'))
{
return;
}
// Get the user
$user = $model->getContainer()->platform->getUser();
$recordAccessLevel = $model->getFieldValue('access', null);
// Filter by authorised access levels
if (!in_array($recordAccessLevel, $user->getAuthorisedViewLevels()))
{
$model->reset(true);
}
}
}

View File

@ -0,0 +1,198 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use Joomla\CMS\Access\Rules;
use Joomla\CMS\Factory;
use Joomla\CMS\Table\Asset;
/**
* FOF model behavior class to add Joomla! ACL assets support
*
* @since 2.1
*/
class Assets extends Observer
{
public function onAfterSave(DataModel &$model)
{
if (!$model->hasField('asset_id') || !$model->isAssetsTracked())
{
return true;
}
$assetFieldAlias = $model->getFieldAlias('asset_id');
$currentAssetId = $model->getFieldValue('asset_id');
unset($model->$assetFieldAlias);
// Create the object used for inserting/updating data to the database
$fields = $model->getTableFields();
// Let's remove the asset_id field, since we unset the property above and we would get a PHP notice
if (isset($fields[$assetFieldAlias]))
{
unset($fields[$assetFieldAlias]);
}
// Asset Tracking
$parentId = $model->getAssetParentId();
$name = $model->getAssetName();
$title = $model->getAssetTitle();
$asset = new Asset(Factory::getDbo());
$asset->loadByName($name);
// Re-inject the asset id.
$this->$assetFieldAlias = $asset->id;
// Check for an error.
$error = $asset->getError();
// Since we are using \Joomla\CMS\Table\Table, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!empty($error))
{
throw new \Exception($error);
}
// @codeCoverageIgnoreEnd
// Specify how a new or moved node asset is inserted into the tree.
// Since we're unsetting the table field before, this statement is always true...
if (empty($model->$assetFieldAlias) || $asset->parent_id !== $parentId)
{
$asset->setLocation($parentId, 'last-child');
}
// Prepare the asset to be stored.
$asset->parent_id = $parentId;
$asset->name = $name;
$asset->title = $title;
if ($model->getRules() instanceof Rules)
{
$asset->rules = (string) $model->getRules();
}
// Since we are using \Joomla\CMS\Table\Table, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->check() || !$asset->store())
{
throw new \Exception($asset->getError());
}
// @codeCoverageIgnoreEnd
// Create an asset_id or heal one that is corrupted.
if (empty($model->$assetFieldAlias) || (($currentAssetId != $model->$assetFieldAlias) && !empty($model->$assetFieldAlias)))
{
// Update the asset_id field in this table.
$model->$assetFieldAlias = (int) $asset->id;
$k = $model->getKeyName();
$db = $model->getDbo();
$query = $db->getQuery(true)
->update($db->qn($model->getTableName()))
->set($db->qn($assetFieldAlias) . ' = ' . (int) $model->$assetFieldAlias)
->where($db->qn($k) . ' = ' . (int) $model->$k);
$db->setQuery($query)->execute();
}
return true;
}
public function onAfterBind(DataModel &$model, &$src)
{
if (!$model->isAssetsTracked())
{
return true;
}
$rawRules = [];
if (is_array($src) && array_key_exists('rules', $src) && is_array($src['rules']))
{
$rawRules = $src['rules'];
}
elseif (is_object($src) && isset($src->rules) && is_array($src->rules))
{
$rawRules = $src->rules;
}
if (empty($rawRules))
{
return true;
}
// Bind the rules.
if (isset($rawRules) && is_array($rawRules))
{
// We have to manually remove any empty value, since they will be converted to int,
// and "Inherited" values will become "Denied". Joomla is doing this manually, too.
$rules = [];
foreach ($rawRules as $action => $ids)
{
// Build the rules array.
$rules[$action] = [];
foreach ($ids as $id => $p)
{
if ($p !== '')
{
$rules[$action][$id] = $p == '1' || $p == 'true';
}
}
}
$model->setRules($rules);
}
return true;
}
public function onBeforeDelete(DataModel &$model, $oid)
{
if (!$model->isAssetsTracked())
{
return true;
}
$k = $model->getKeyName();
// If the table is not loaded, let's try to load it with the id
if (!$model->$k)
{
$model->load($oid);
}
// If I have an invalid assetName I have to stop
$name = $model->getAssetName();
// Do NOT touch \Joomla\CMS\Table\Table here -- we are loading the core asset table which is a \Joomla\CMS\Table\Table, not a FOF Table
$asset = new Asset(Factory::getDbo());
if ($asset->loadByName($name))
{
// Since we are using \Joomla\CMS\Table\Table, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->delete())
{
throw new \Exception($asset->getError());
}
// @codeCoverageIgnoreEnd
}
return true;
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use ContenthistoryHelper;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
/**
* FOF model behavior class to add Joomla! content history support
*
* @since 2.1
*/
class ContentHistory extends Observer
{
/** @var ContentHistoryHelper */
protected $historyHelper;
/**
* The event which runs after storing (saving) data to the database
*
* @param DataModel &$model The model which calls this event
*
* @return boolean True to allow saving without an error
*/
public function onAfterSave(DataModel &$model)
{
$model->checkContentType();
$componentParams = $model->getContainer()->params;
if ($componentParams->get('save_history', 0))
{
if (!$this->historyHelper)
{
$this->historyHelper = new ContentHistoryHelper($model->getContentType());
}
$this->historyHelper->store($model);
}
return true;
}
/**
* The event which runs before deleting a record
*
* @param DataModel &$model The model which calls this event
* @param integer $oid The PK value of the record to delete
*
* @return boolean True to allow the deletion
*/
public function onBeforeDelete(DataModel &$model, $oid)
{
$componentParams = $model->getContainer()->params;
if ($componentParams->get('save_history', 0))
{
if (!$this->historyHelper)
{
$this->historyHelper = new ContentHistoryHelper($model->getContentType());
}
$this->historyHelper->deleteHistory($model);
}
return true;
}
/**
* This event runs after publishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterPublish(DataModel &$model)
{
$model->updateUcmContent();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterUnpublish(DataModel &$model)
{
$model->updateUcmContent();
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
/**
* FOF model behavior class to updated the created_by and created_on fields on newly created records.
*
* This behaviour is added to DataModel by default. If you want to remove it you need to do
* $this->behavioursDispatcher->removeBehaviour('Created');
*
* @since 3.0
*/
class Created extends Observer
{
/**
* Add the created_on and created_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
* so that we can fill them in through this behaviour.
*
* @param DataModel $model
*/
public function onBeforeCheck(DataModel &$model)
{
$model->addSkipCheckField('created_on');
$model->addSkipCheckField('created_by');
}
/**
* @param DataModel $model
* @param \stdClass $dataObject
*/
public function onBeforeCreate(DataModel &$model, &$dataObject)
{
// Handle the created_on field
if ($model->hasField('created_on'))
{
$nullDate = $model->isNullableField('created_on') ? null : $model->getDbo()->getNullDate();
$created_on = $model->getFieldValue('created_on');
if (empty($created_on) || ($created_on == $nullDate))
{
$model->setFieldValue('created_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));
$createdOnField = $model->getFieldAlias('created_on');
$dataObject->$createdOnField = $model->getFieldValue('created_on');
}
}
// Handle the created_by field
if ($model->hasField('created_by'))
{
$created_by = $model->getFieldValue('created_by');
if (empty($created_by))
{
$model->setFieldValue('created_by', $model->getContainer()->platform->getUser()->id);
$createdByField = $model->getFieldAlias('created_by');
$dataObject->$createdByField = $model->getFieldValue('created_by');
}
}
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to let the Filters behaviour know that zero value is a valid filter value
*
* @since 2.1
*/
class EmptyNonZero extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
$model->setBehaviorParam('filterZero', 1);
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to filter access to items based on the enabled status
*
* @since 2.1
*/
class Enabled extends Observer
{
/**
* This event runs before we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onBeforeBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
// Make sure the field actually exists
if (!$model->hasField('enabled'))
{
return;
}
$fieldName = $model->getFieldAlias('enabled');
$db = $model->getDbo();
$model->whereRaw($db->qn($fieldName) . ' = ' . $db->q(1));
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('enabled'))
{
return;
}
// Filter by enabled status
if (!$model->getFieldValue('enabled', 0))
{
$model->reset(true);
}
}
}

View File

@ -0,0 +1,133 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;
class Filters extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
$tableKey = $model->getIdFieldName();
$db = $model->getDbo();
$fields = $model->getTableFields();
$blacklist = $model->getBlacklistFilters();
$filterZero = $model->getBehaviorParam('filterZero', null);
$tableAlias = $model->getBehaviorParam('tableAlias', null);
foreach ($fields as $fieldname => $fieldmeta)
{
if (in_array($fieldname, $blacklist))
{
continue;
}
$fieldInfo = (object)array(
'name' => $fieldname,
'type' => $fieldmeta->Type,
'filterZero' => $filterZero,
'tableAlias' => $tableAlias,
);
$filterName = $fieldInfo->name;
$filterState = $model->getState($filterName, null);
// Special primary key handling: if ignore request is set we'll also look for an 'id' state variable if a
// state variable by the same name as the key doesn't exist. If ignore request is not set in the model we
// do not filter by 'id' since this interferes with going from an edit page to a browse page (the list is
// filtered by id without user controls to unset it).
if ($fieldInfo->name == $tableKey)
{
$filterState = $model->getState($filterName, null);
if (!$model->getIgnoreRequest())
{
continue;
}
if (empty($filterState))
{
$filterState = $model->getState('id', null);
}
}
$field = DataModel\Filter\AbstractFilter::getField($fieldInfo, array('dbo' => $db));
if (!is_object($field) || !($field instanceof DataModel\Filter\AbstractFilter))
{
continue;
}
if ((is_array($filterState) && (
array_key_exists('value', $filterState) ||
array_key_exists('from', $filterState) ||
array_key_exists('to', $filterState)
)) || is_object($filterState))
{
$options = new Registry($filterState);
}
else
{
$options = new Registry();
$options->set('value', $filterState);
}
$methods = $field->getSearchMethods();
$method = $options->get('method', $field->getDefaultSearchMethod());
if (!in_array($method, $methods))
{
$method = 'exact';
}
switch ($method)
{
case 'between':
case 'outside':
case 'range' :
$sql = $field->$method($options->get('from', null), $options->get('to', null), $options->get('include', false));
break;
case 'interval':
case 'modulo':
$sql = $field->$method($options->get('value', null), $options->get('interval'));
break;
case 'search':
$sql = $field->$method($options->get('value', null), $options->get('operator', '='));
break;
case 'exact':
case 'partial':
default:
$sql = $field->$method($options->get('value', null));
break;
}
if ($sql)
{
$query->where($sql);
}
}
}
}

View File

@ -0,0 +1,171 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
/**
* FOF model behavior class to filter front-end access to items
* based on the language.
*
* @since 2.1
*/
class Language extends Observer
{
/** @var \PlgSystemLanguageFilter */
protected $lang_filter_plugin;
/**
* This event runs before we have built the query used to fetch a record
* list in a model. It is used to blacklist the language filter
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The model which calls this event
*
* @return void
* @noinspection PhpUnusedParameterInspection
*/
public function onBeforeBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
if ($model->getContainer()->platform->isFrontend())
{
$model->blacklistFilters('language');
}
// Make sure the field actually exists AND we're not in CLI
if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
{
return;
}
/** @var SiteApplication $app */
$app = JoomlaFactory::getApplication();
$hasLanguageFilter = method_exists($app, 'getLanguageFilter');
if ($hasLanguageFilter)
{
$hasLanguageFilter = $app->getLanguageFilter();
}
if (!$hasLanguageFilter)
{
return;
}
// Ask Joomla for the plugin only if we don't already have it. Useful for tests
if(!$this->lang_filter_plugin)
{
$this->lang_filter_plugin = PluginHelper::getPlugin('system', 'languagefilter');
}
$lang_filter_params = new Registry($this->lang_filter_plugin->params);
$languages = array('*');
if ($lang_filter_params->get('remove_default_prefix'))
{
// Get default site language
$platform = $model->getContainer()->platform;
$lg = $platform->getLanguage();
$languages[] = $lg->getTag();
}
else
{
// We have to use JoomlaInput since the language fragment is not set in the $_REQUEST, thus we won't have it in our model
// TODO Double check the previous assumption
$languages[] = JoomlaFactory::getApplication()->input->getCmd('language', '*');
}
// Filter out double languages
$languages = array_unique($languages);
// And filter the query output by these languages
$db = $model->getDbo();
$languages = array_map(array($db, 'quote'), $languages);
$fieldName = $model->getFieldAlias('language');
$model->whereRaw($db->qn($fieldName) . ' IN(' . implode(', ', $languages) . ')');
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists AND we're not in CLI
if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
{
return;
}
// Make sure it is a multilingual site and get a list of languages
/** @var SiteApplication $app */
$app = JoomlaFactory::getApplication();
$hasLanguageFilter = method_exists($app, 'getLanguageFilter');
if ($hasLanguageFilter)
{
$hasLanguageFilter = $app->getLanguageFilter();
}
if (!$hasLanguageFilter)
{
return;
}
// Ask Joomla for the plugin only if we don't already have it. Useful for tests
if(!$this->lang_filter_plugin)
{
$this->lang_filter_plugin = PluginHelper::getPlugin('system', 'languagefilter');
}
$lang_filter_params = new Registry($this->lang_filter_plugin->params);
$languages = array('*');
if ($lang_filter_params->get('remove_default_prefix'))
{
// Get default site language
$lg = $model->getContainer()->platform->getLanguage();
$languages[] = $lg->getTag();
}
else
{
$languages[] = JoomlaFactory::getApplication()->input->getCmd('language', '*');
}
// Filter out double languages
$languages = array_unique($languages);
// Filter by language
if (!in_array($model->getFieldValue('language'), $languages))
{
$model->reset();
}
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to updated the modified_by and modified_on fields on newly created records.
*
* This behaviour is added to DataModel by default. If you want to remove it you need to do
* $this->behavioursDispatcher->removeBehaviour('Modified');
*
* @since 3.0
*/
class Modified extends Observer
{
/**
* Add the modified_on and modified_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
* so that we can fill them in through this behaviour.
*
* @param DataModel $model
*/
public function onBeforeCheck(DataModel &$model)
{
$model->addSkipCheckField('modified_on');
$model->addSkipCheckField('modified_by');
}
/**
* @param DataModel $model
* @param \stdClass $dataObject
*/
public function onBeforeUpdate(DataModel &$model, &$dataObject)
{
// Make sure we're not modifying a locked record
$userId = $model->getContainer()->platform->getUser()->id;
$isLocked = $model->isLocked($userId);
if ($isLocked)
{
return;
}
// Handle the modified_on field
if ($model->hasField('modified_on'))
{
$model->setFieldValue('modified_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));
$modifiedOnField = $model->getFieldAlias('modified_on');
$dataObject->$modifiedOnField = $model->getFieldValue('modified_on');
}
// Handle the modified_by field
if ($model->hasField('modified_by'))
{
$model->setFieldValue('modified_by', $userId);
$modifiedByField = $model->getFieldAlias('modified_by');
$dataObject->$modifiedByField = $model->getFieldValue('modified_by');
}
}
}

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