primo commit
This commit is contained in:
337
libraries/fof40/Autoloader/Autoloader.php
Normal file
337
libraries/fof40/Autoloader/Autoloader.php
Normal 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();
|
||||
338
libraries/fof40/Cli/Application.php
Normal file
338
libraries/fof40/Cli/Application.php
Normal 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');
|
||||
134
libraries/fof40/Cli/Joomla3.php
Normal file
134
libraries/fof40/Cli/Joomla3.php
Normal 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;
|
||||
}
|
||||
}
|
||||
196
libraries/fof40/Cli/Joomla4.php
Normal file
196
libraries/fof40/Cli/Joomla4.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
69
libraries/fof40/Cli/Traits/CGIModeAware.php
Normal file
69
libraries/fof40/Cli/Traits/CGIModeAware.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
204
libraries/fof40/Cli/Traits/CustomOptionsAware.php
Normal file
204
libraries/fof40/Cli/Traits/CustomOptionsAware.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
55
libraries/fof40/Cli/Traits/JoomlaConfigAware.php
Normal file
55
libraries/fof40/Cli/Traits/JoomlaConfigAware.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
73
libraries/fof40/Cli/Traits/MemStatsAware.php
Normal file
73
libraries/fof40/Cli/Traits/MemStatsAware.php
Normal 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)";
|
||||
}
|
||||
}
|
||||
}
|
||||
64
libraries/fof40/Cli/Traits/MessageAware.php
Normal file
64
libraries/fof40/Cli/Traits/MessageAware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
100
libraries/fof40/Cli/Traits/TimeAgoAware.php
Normal file
100
libraries/fof40/Cli/Traits/TimeAgoAware.php
Normal 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).';
|
||||
}
|
||||
|
||||
}
|
||||
67
libraries/fof40/Cli/wrong_php.php
Normal file
67
libraries/fof40/Cli/wrong_php.php
Normal 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.
|
||||
|
||||
229
libraries/fof40/Configuration/Configuration.php
Normal file
229
libraries/fof40/Configuration/Configuration.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
78
libraries/fof40/Configuration/Domain/Authentication.php
Normal file
78
libraries/fof40/Configuration/Domain/Authentication.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
libraries/fof40/Configuration/Domain/Container.php
Normal file
78
libraries/fof40/Configuration/Domain/Container.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
libraries/fof40/Configuration/Domain/Dispatcher.php
Normal file
78
libraries/fof40/Configuration/Domain/Dispatcher.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
libraries/fof40/Configuration/Domain/DomainInterface.php
Normal file
43
libraries/fof40/Configuration/Domain/DomainInterface.php
Normal 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);
|
||||
}
|
||||
358
libraries/fof40/Configuration/Domain/Models.php
Normal file
358
libraries/fof40/Configuration/Domain/Models.php
Normal 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;
|
||||
}
|
||||
}
|
||||
300
libraries/fof40/Configuration/Domain/Views.php
Normal file
300
libraries/fof40/Configuration/Domain/Views.php
Normal 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;
|
||||
}
|
||||
}
|
||||
734
libraries/fof40/Container/Container.php
Normal file
734
libraries/fof40/Container/Container.php
Normal 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);
|
||||
}
|
||||
}
|
||||
50
libraries/fof40/Container/ContainerBase.php
Normal file
50
libraries/fof40/Container/ContainerBase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
30
libraries/fof40/Container/Exception/NoComponent.php
Normal file
30
libraries/fof40/Container/Exception/NoComponent.php
Normal 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);
|
||||
}
|
||||
}
|
||||
1252
libraries/fof40/Controller/Controller.php
Normal file
1252
libraries/fof40/Controller/Controller.php
Normal file
File diff suppressed because it is too large
Load Diff
1695
libraries/fof40/Controller/DataController.php
Normal file
1695
libraries/fof40/Controller/DataController.php
Normal file
File diff suppressed because it is too large
Load Diff
19
libraries/fof40/Controller/Exception/CannotGetName.php
Normal file
19
libraries/fof40/Controller/Exception/CannotGetName.php
Normal 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
|
||||
{
|
||||
}
|
||||
20
libraries/fof40/Controller/Exception/ItemNotFound.php
Normal file
20
libraries/fof40/Controller/Exception/ItemNotFound.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
30
libraries/fof40/Controller/Exception/LockedRecord.php
Normal file
30
libraries/fof40/Controller/Exception/LockedRecord.php
Normal 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);
|
||||
}
|
||||
}
|
||||
19
libraries/fof40/Controller/Exception/NotADataModel.php
Normal file
19
libraries/fof40/Controller/Exception/NotADataModel.php
Normal 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
|
||||
{
|
||||
}
|
||||
19
libraries/fof40/Controller/Exception/NotADataView.php
Normal file
19
libraries/fof40/Controller/Exception/NotADataView.php
Normal 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
|
||||
{
|
||||
}
|
||||
19
libraries/fof40/Controller/Exception/TaskNotFound.php
Normal file
19
libraries/fof40/Controller/Exception/TaskNotFound.php
Normal 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
|
||||
{
|
||||
}
|
||||
82
libraries/fof40/Controller/Mixin/PredefinedTaskList.php
Normal file
82
libraries/fof40/Controller/Mixin/PredefinedTaskList.php
Normal 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));
|
||||
|
||||
}
|
||||
}
|
||||
1059
libraries/fof40/Database/Installer.php
Normal file
1059
libraries/fof40/Database/Installer.php
Normal file
File diff suppressed because it is too large
Load Diff
472
libraries/fof40/Date/Date.php
Normal file
472
libraries/fof40/Date/Date.php
Normal 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');
|
||||
}
|
||||
}
|
||||
154
libraries/fof40/Date/DateDecorator.php
Normal file
154
libraries/fof40/Date/DateDecorator.php
Normal 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();
|
||||
}
|
||||
}
|
||||
348
libraries/fof40/Date/TimezoneWrangler.php
Normal file
348
libraries/fof40/Date/TimezoneWrangler.php
Normal 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);
|
||||
}
|
||||
}
|
||||
348
libraries/fof40/Dispatcher/Dispatcher.php
Normal file
348
libraries/fof40/Dispatcher/Dispatcher.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
libraries/fof40/Dispatcher/Exception/AccessForbidden.php
Normal file
31
libraries/fof40/Dispatcher/Exception/AccessForbidden.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
83
libraries/fof40/Dispatcher/Mixin/ViewAliases.php
Normal file
83
libraries/fof40/Dispatcher/Mixin/ViewAliases.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
libraries/fof40/Download/Adapter/AbstractAdapter.php
Normal file
139
libraries/fof40/Download/Adapter/AbstractAdapter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
278
libraries/fof40/Download/Adapter/Curl.php
Normal file
278
libraries/fof40/Download/Adapter/Curl.php
Normal 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;
|
||||
}
|
||||
}
|
||||
164
libraries/fof40/Download/Adapter/Fopen.php
Normal file
164
libraries/fof40/Download/Adapter/Fopen.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
501
libraries/fof40/Download/Download.php
Normal file
501
libraries/fof40/Download/Download.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
libraries/fof40/Download/DownloadInterface.php
Normal file
87
libraries/fof40/Download/DownloadInterface.php
Normal 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;
|
||||
}
|
||||
17
libraries/fof40/Download/Exception/DownloadError.php
Normal file
17
libraries/fof40/Download/Exception/DownloadError.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
289
libraries/fof40/Encrypt/Aes.php
Normal file
289
libraries/fof40/Encrypt/Aes.php
Normal 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;
|
||||
}
|
||||
}
|
||||
88
libraries/fof40/Encrypt/AesAdapter/AbstractAdapter.php
Normal file
88
libraries/fof40/Encrypt/AesAdapter/AbstractAdapter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
71
libraries/fof40/Encrypt/AesAdapter/AdapterInterface.php
Normal file
71
libraries/fof40/Encrypt/AesAdapter/AdapterInterface.php
Normal 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;
|
||||
}
|
||||
168
libraries/fof40/Encrypt/AesAdapter/OpenSSL.php
Normal file
168
libraries/fof40/Encrypt/AesAdapter/OpenSSL.php
Normal 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);
|
||||
}
|
||||
}
|
||||
208
libraries/fof40/Encrypt/Base32.php
Normal file
208
libraries/fof40/Encrypt/Base32.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
278
libraries/fof40/Encrypt/EncryptService.php
Normal file
278
libraries/fof40/Encrypt/EncryptService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
69
libraries/fof40/Encrypt/Randval.php
Normal file
69
libraries/fof40/Encrypt/Randval.php
Normal 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;
|
||||
}
|
||||
}
|
||||
22
libraries/fof40/Encrypt/RandvalInterface.php
Normal file
22
libraries/fof40/Encrypt/RandvalInterface.php
Normal 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;
|
||||
}
|
||||
186
libraries/fof40/Encrypt/Totp.php
Normal file
186
libraries/fof40/Encrypt/Totp.php
Normal 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];
|
||||
}
|
||||
}
|
||||
291
libraries/fof40/Event/Dispatcher.php
Normal file
291
libraries/fof40/Event/Dispatcher.php
Normal 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;
|
||||
}
|
||||
}
|
||||
46
libraries/fof40/Event/Observable.php
Normal file
46
libraries/fof40/Event/Observable.php
Normal 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;
|
||||
}
|
||||
70
libraries/fof40/Event/Observer.php
Normal file
70
libraries/fof40/Event/Observer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
386
libraries/fof40/Factory/BasicFactory.php
Normal file
386
libraries/fof40/Factory/BasicFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
25
libraries/fof40/Factory/Exception/ControllerNotFound.php
Normal file
25
libraries/fof40/Factory/Exception/ControllerNotFound.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
25
libraries/fof40/Factory/Exception/DispatcherNotFound.php
Normal file
25
libraries/fof40/Factory/Exception/DispatcherNotFound.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
25
libraries/fof40/Factory/Exception/ModelNotFound.php
Normal file
25
libraries/fof40/Factory/Exception/ModelNotFound.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
25
libraries/fof40/Factory/Exception/ToolbarNotFound.php
Normal file
25
libraries/fof40/Factory/Exception/ToolbarNotFound.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
25
libraries/fof40/Factory/Exception/ViewNotFound.php
Normal file
25
libraries/fof40/Factory/Exception/ViewNotFound.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
110
libraries/fof40/Factory/FactoryInterface.php
Normal file
110
libraries/fof40/Factory/FactoryInterface.php
Normal 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;
|
||||
}
|
||||
55
libraries/fof40/Factory/Magic/BaseFactory.php
Normal file
55
libraries/fof40/Factory/Magic/BaseFactory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
76
libraries/fof40/Factory/Magic/ControllerFactory.php
Normal file
76
libraries/fof40/Factory/Magic/ControllerFactory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
41
libraries/fof40/Factory/Magic/DispatcherFactory.php
Normal file
41
libraries/fof40/Factory/Magic/DispatcherFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
86
libraries/fof40/Factory/Magic/ModelFactory.php
Normal file
86
libraries/fof40/Factory/Magic/ModelFactory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
68
libraries/fof40/Factory/Magic/ViewFactory.php
Normal file
68
libraries/fof40/Factory/Magic/ViewFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
168
libraries/fof40/Factory/MagicFactory.php
Normal file
168
libraries/fof40/Factory/MagicFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
208
libraries/fof40/Factory/MagicSwitchFactory.php
Normal file
208
libraries/fof40/Factory/MagicSwitchFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
266
libraries/fof40/Factory/SwitchFactory.php
Normal file
266
libraries/fof40/Factory/SwitchFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
923
libraries/fof40/Html/FEFHelper/BrowseView.php
Normal file
923
libraries/fof40/Html/FEFHelper/BrowseView.php
Normal 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' => '— ' . Text::_($placeholder) . ' —',
|
||||
'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' => '— ' . Text::_($placeholder) . ' —',
|
||||
'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'] = '— ' . $placeholder . ' —';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
867
libraries/fof40/Html/FEFHelper/browse.php
Normal file
867
libraries/fof40/Html/FEFHelper/browse.php
Normal 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> ';
|
||||
$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> ';
|
||||
$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;
|
||||
}
|
||||
}
|
||||
60
libraries/fof40/Html/FEFHelper/edit.php
Normal file
60
libraries/fof40/Html/FEFHelper/edit.php
Normal 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);
|
||||
}
|
||||
}
|
||||
882
libraries/fof40/Html/FEFHelper/select.php
Normal file
882
libraries/fof40/Html/FEFHelper/select.php
Normal 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
|
||||
* "<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;
|
||||
}
|
||||
}
|
||||
56
libraries/fof40/Html/Fields/fancyradio.php
Normal file
56
libraries/fof40/Html/Fields/fancyradio.php
Normal 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);
|
||||
}
|
||||
}
|
||||
466
libraries/fof40/Html/SelectOptions.php
Normal file
466
libraries/fof40/Html/SelectOptions.php
Normal 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);
|
||||
}
|
||||
}
|
||||
613
libraries/fof40/IP/IPHelper.php
Normal file
613
libraries/fof40/IP/IPHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
420
libraries/fof40/Inflector/Inflector.php
Normal file
420
libraries/fof40/Inflector/Inflector.php
Normal 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;
|
||||
}
|
||||
}
|
||||
275
libraries/fof40/Input/Input.php
Normal file
275
libraries/fof40/Input/Input.php
Normal 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) ?? [];
|
||||
}
|
||||
|
||||
}
|
||||
870
libraries/fof40/InstallScript/BaseInstaller.php
Normal file
870
libraries/fof40/InstallScript/BaseInstaller.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1400
libraries/fof40/InstallScript/Component.php
Normal file
1400
libraries/fof40/InstallScript/Component.php
Normal file
File diff suppressed because it is too large
Load Diff
238
libraries/fof40/InstallScript/Module.php
Normal file
238
libraries/fof40/InstallScript/Module.php
Normal 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);
|
||||
}
|
||||
}
|
||||
250
libraries/fof40/InstallScript/Plugin.php
Normal file
250
libraries/fof40/InstallScript/Plugin.php
Normal 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;
|
||||
}
|
||||
}
|
||||
350
libraries/fof40/JoomlaAbstraction/CacheCleaner.php
Normal file
350
libraries/fof40/JoomlaAbstraction/CacheCleaner.php
Normal 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;
|
||||
}
|
||||
}
|
||||
158
libraries/fof40/JoomlaAbstraction/ComponentVersion.php
Normal file
158
libraries/fof40/JoomlaAbstraction/ComponentVersion.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
196
libraries/fof40/JoomlaAbstraction/DynamicGroups.php
Normal file
196
libraries/fof40/JoomlaAbstraction/DynamicGroups.php
Normal 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);
|
||||
}
|
||||
}
|
||||
86
libraries/fof40/Layout/LayoutFile.php
Normal file
86
libraries/fof40/Layout/LayoutFile.php
Normal 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;
|
||||
}
|
||||
}
|
||||
46
libraries/fof40/Layout/LayoutHelper.php
Normal file
46
libraries/fof40/Layout/LayoutHelper.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
4162
libraries/fof40/Model/DataModel.php
Normal file
4162
libraries/fof40/Model/DataModel.php
Normal file
File diff suppressed because it is too large
Load Diff
76
libraries/fof40/Model/DataModel/Behaviour/Access.php
Normal file
76
libraries/fof40/Model/DataModel/Behaviour/Access.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
libraries/fof40/Model/DataModel/Behaviour/Assets.php
Normal file
198
libraries/fof40/Model/DataModel/Behaviour/Assets.php
Normal 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;
|
||||
}
|
||||
}
|
||||
100
libraries/fof40/Model/DataModel/Behaviour/ContentHistory.php
Normal file
100
libraries/fof40/Model/DataModel/Behaviour/ContentHistory.php
Normal 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();
|
||||
}
|
||||
}
|
||||
72
libraries/fof40/Model/DataModel/Behaviour/Created.php
Normal file
72
libraries/fof40/Model/DataModel/Behaviour/Created.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
libraries/fof40/Model/DataModel/Behaviour/EmptyNonZero.php
Normal file
36
libraries/fof40/Model/DataModel/Behaviour/EmptyNonZero.php
Normal 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);
|
||||
}
|
||||
}
|
||||
75
libraries/fof40/Model/DataModel/Behaviour/Enabled.php
Normal file
75
libraries/fof40/Model/DataModel/Behaviour/Enabled.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
libraries/fof40/Model/DataModel/Behaviour/Filters.php
Normal file
133
libraries/fof40/Model/DataModel/Behaviour/Filters.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
libraries/fof40/Model/DataModel/Behaviour/Language.php
Normal file
171
libraries/fof40/Model/DataModel/Behaviour/Language.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
libraries/fof40/Model/DataModel/Behaviour/Modified.php
Normal file
71
libraries/fof40/Model/DataModel/Behaviour/Modified.php
Normal 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
Reference in New Issue
Block a user