Файловый менеджер - Редактировать - /home/wuectly/www/03cbe/fof40.tar
Назад
Cli/Joomla4.php 0000604 00000013540 15245560676 0007310 0 ustar 00 <?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'); } } Cli/Joomla3.php 0000604 00000007471 15245560676 0007315 0 ustar 00 <?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; } } Cli/Traits/CGIModeAware.php 0000604 00000003255 15245560676 0011442 0 ustar 00 <?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; } } Cli/Traits/TimeAgoAware.php 0000604 00000004423 15245560676 0011556 0 ustar 00 <?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).'; } } Cli/Traits/MessageAware.php 0000604 00000002334 15245560676 0011614 0 ustar 00 <?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; } } Cli/Traits/MemStatsAware.php 0000604 00000002642 15245560676 0011767 0 ustar 00 <?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)"; } } } Cli/Traits/CustomOptionsAware.php 0000604 00000010223 15245560676 0013052 0 ustar 00 <?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); } } Cli/Traits/JoomlaConfigAware.php 0000604 00000002576 15245560676 0012607 0 ustar 00 <?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(); } } Cli/wrong_php.php 0000604 00000005413 15245560676 0010006 0 ustar 00 <?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. Cli/Application.php 0000604 00000022254 15245560676 0010250 0 ustar 00 <?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'); Container/Container.php 0000604 00000052556 15245560676 0011152 0 ustar 00 <?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); } } Container/Exception/NoComponent.php 0000604 00000001116 15245560676 0013407 0 ustar 00 <?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); } } Container/ContainerBase.php 0000604 00000002221 15245560676 0011725 0 ustar 00 <?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); } } Configuration/Configuration.php 0000604 00000012201 15245560676 0012703 0 ustar 00 <?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; } } Configuration/Domain/Views.php 0000604 00000020406 15245560676 0012406 0 ustar 00 <?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; } } Configuration/Domain/Dispatcher.php 0000604 00000003242 15245560676 0013376 0 ustar 00 <?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; } } } Configuration/Domain/DomainInterface.php 0000604 00000002311 15245560676 0014334 0 ustar 00 <?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); } Configuration/Domain/Authentication.php 0000604 00000003322 15245560676 0014266 0 ustar 00 <?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; } } } Configuration/Domain/Container.php 0000604 00000003226 15245560676 0013234 0 ustar 00 <?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; } } } Configuration/Domain/Models.php 0000604 00000023102 15245560676 0012530 0 ustar 00 <?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; } } Encrypt/Base32.php 0000604 00000011263 15245560676 0007737 0 ustar 00 <?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)); } } Encrypt/RandvalInterface.php 0000604 00000000726 15245560676 0012132 0 ustar 00 <?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; } Encrypt/Aes.php 0000604 00000017320 15245560676 0007430 0 ustar 00 <?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; } } Encrypt/Randval.php 0000604 00000003730 15245560676 0010307 0 ustar 00 <?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; } } Encrypt/AesAdapter/AbstractAdapter.php 0000604 00000003503 15245560676 0013773 0 ustar 00 <?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); } } Encrypt/AesAdapter/AdapterInterface.php 0000604 00000004444 15245560676 0014135 0 ustar 00 <?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; } Encrypt/AesAdapter/OpenSSL.php 0000604 00000007131 15245560676 0012213 0 ustar 00 <?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); } } Encrypt/EncryptService.php 0000604 00000016557 15245560676 0011700 0 ustar 00 <?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); } } Encrypt/Totp.php 0000604 00000011422 15245560676 0007643 0 ustar 00 <?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]; } } ViewTemplates/Common/user_select.j4.fef.blade.php 0000604 00000015416 15245560676 0015757 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ defined('_JEXEC') || die; use Joomla\CMS\Factory;use Joomla\CMS\Language\Text;use Joomla\CMS\Uri\Uri; /** * User entry field, allowing selection of a user from a modal dialog * * Use this by extending it (I'm using -at- instead of the actual at-sign) * -at-include('any:lib_fof40/Common/user_select', $params) * * This is the variant used when using the FEF renderer under Joomla 4. * * $params is an array defining the following keys (they are expanded into local scope vars automatically): * * @var string $field The user field's name, e.g. "user_id" * @var \FOF40\Model\DataModel $item The item we're editing. The user ID is stored in $item->{$field} * @var string $id The id of the field, default is $field * @var bool $readonly Is this a read only field? Default: false * @var string $placeholder Placeholder text, also used as the button's tooltip * @var bool $required Is a value required for this field? Default: false * @var string $width Width of the modal box (default: 800) * @var string $height Height of the modal box (default: 500) * @var string $autocomplete Autocomplete attribute for the field. * @var boolean $autofocus Is autofocus enabled? * @var string $class Classes for the input. * @var string $description Description of the field. * @var boolean $disabled Is this field disabled? * @var string $group Group the field belongs to. <fields> section in form XML. * @var boolean $hidden Is this field hidden in the form? * @var string $id DOM id of the field. * @var string $label Label of the field. * @var string $labelclass Classes to apply to the label. * @var boolean $multiple Does this field support multiple values? * @var string $onchange Onchange attribute for the field. * @var string $onclick Onclick attribute for the field. * @var string $pattern Pattern (Reg Ex) of value of the form field. * @var boolean $readonly Is this field read only? * @var boolean $repeat Allows extensions to duplicate elements. * @var boolean $required Is this field required? * @var integer $size Size attribute of the input. * @var boolean $spellcheck Spellcheck state for the form field. * @var string $validate Validation rules to apply. * @var mixed $groups The filtering groups (null means no filtering) * @var mixed $excluded The users to exclude from the list of users * @var string $dataAttribute Miscellaneous data attributes preprocessed for HTML output * @var array $dataAttributes Miscellaneous data attribute for eg, data-*. * * Variables made automatically available to us by FOF: * * @var \FOF40\View\DataView\DataViewInterface $this */ $id = isset($id) ? $id : $field; $readonly = isset($readonly) ? ($readonly ? true : false) : false; $placeholder = isset($placeholder) ? Text::_($placeholder) : Text::_('JLIB_FORM_SELECT_USER'); $userID = $item->getFieldValue($field, 0); $user = $item->getContainer()->platform->getUser($userID); $width = isset($width) ? $width : 800; $height = isset($height) ? $height : 500; $class = isset($class) ? $class : ''; $size = isset($size) ? $size : 0; $onchange = isset($onchange) ? $onchange : ''; $userName = (is_object($user) && ($user instanceof \Joomla\CMS\User\User) && !$user->guest) ? $user->name : Text::_('JLIB_FORM_SELECT_USER'); if (!$readonly) { Factory::getDocument()->getWebAssetManager() ->useScript('webcomponent.field-user'); } $uri = new Uri('index.php?option=com_users&view=users&layout=modal&tmpl=component&required=0'); $uri->setVar('field', $this->escape($id)); if ($required) { $uri->setVar('required', 1); } if (!empty($groups)) { $uri->setVar('groups', base64_encode(json_encode($groups))); } if (!empty($excluded)) { $uri->setVar('excluded', base64_encode(json_encode($excluded))); } // Invalidate the input value if no user selected if ($this->escape($userName) === Text::_('JLIB_FORM_SELECT_USER')) { $userName = ''; } $inputAttributes = array( 'type' => 'text', 'id' => $id, 'class' => 'form-control field-user-input-name', 'value' => $this->escape($userName) ); if ($class) { $inputAttributes['class'] .= ' ' . $class; } if ($size) { $inputAttributes['size'] = (int) $size; } if ($required) { $inputAttributes['required'] = 'required'; } if (!$readonly) { $inputAttributes['placeholder'] = $placeholder; } ?> <?php // Create a dummy text field with the user name. ?> <joomla-field-user class="field-user-wrapper" url="<?php echo (string) $uri; ?>" modal=".modal" modal-width="100%" modal-height="400px" input=".field-user-input" input-name=".field-user-input-name" button-select=".userSelectModal_{{{ $field }}}"> <div class="akeeba-input-group"> <input {{ \FOF40\Utils\ArrayHelper::toString($inputAttributes) }} {{ $dataAttribute ?? '' }} readonly> @if (!$readonly) <span class="akeeba-input-group-btn"> <button type="button" class="akeeba-btn--grey userSelectModal_{{{ $field }}}" title="{{{ $placeholder }}}"> <span class="akion-person" aria-hidden="true" aria-label="{{ $placeholder }}"></span> </button> </span> @endif </div> {{-- Create the real field, hidden, that stored the user id.--}} @if(!$readonly) <input type="hidden" id="{{ $id }}_id" name="{{ $field }}" value="{{{ $userID }}}" class="field-user-input {{ $class ? (string) $class : '' }}" data-onchange="{{{ $onchange }}}"> @jhtml( 'bootstrap.renderModal', 'userModal_' . $id, array( 'url' => $uri, 'title' => $placeholder, 'closeButton' => true, 'height' => '100%', 'width' => '100%', 'modalWidth' => $width / 10, 'bodyHeight' => $height / 10, 'footer' => '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>', ) ) @endif </joomla-field-user> ViewTemplates/Common/browse.fef.blade.php 0000604 00000012564 15245560676 0014430 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ defined('_JEXEC') || die; /** * Template for Browse views using the FEF renderer * * Use this by extending it (I'm using -at- instead of the actual at-sign) * -at-extends('any:lib_fof40/Common/browse') * * Override the following sections in your Blade template: * * browse-page-top * Content to put above the form * * browse-page-bottom * Content to put below the form * * browse-filters * Filters to place above the table. They are placed inside an inline form. Wrap them in * <div class="akeeba-filter-element akeeba-form-group"> * * browse-table-header * The table header. At the very least you need to add the table column headers. You can * optionally add one or more <tr> with filters at the top. * * browse-table-body-withrecords * Loop through the records and create <tr>s. * * browse-table-body-norecords * [ Optional ] The <tr> to show when no records are present. Default is the "no records" text. * * browse-table-footer * [ Optional ] The table footer. By default that's just the pagination footer. * * browse-hidden-fields * [ Optional ] Any additional hidden INPUTs to add to the form. By default this is empty. * The default hidden fields (option, view, task, ordering fields, boxchecked and token) can * not be removed. * * Do not override any other section. The overridden sections should be closed with -at-override instead of -at-stop. *//** @var \FOF40\View\DataView\Html $this */ $ajaxOrderingSupport = $this->hasAjaxOrderingSupport(); ?> {{-- Allow tooltips, used in grid headers --}} @if (version_compare(JVERSION, '3.999.999', 'le')) @jhtml('behavior.tooltip') @endif {{-- Allow SHIFT+click to select multiple rows --}} @jhtml('behavior.multiselect') @section('browse-filters') {{-- Filters above the table. --}} @stop @section('browse-table-header') {{-- Table header. Column headers and optional filters displayed above the column headers. --}} @stop @section('browse-table-body-norecords') {{-- Table body shown when no records are present. --}} <tr> <td colspan="99"> @lang($this->getContainer()->componentName . '_COMMON_NORECORDS') </td> </tr> @stop @section('browse-table-body-withrecords') {{-- Table body shown when records are present. --}} <?php $i = 0; ?> @foreach($this->items as $row) <tr> {{-- You need to implement me! --}} </tr> @endforeach @stop @section('browse-table-footer') {{-- Table footer. The default is showing the pagination footer. --}} <tr> <td colspan="99" class="center"> {{ $this->pagination->getListFooter() }} </td> </tr> @stop @section('browse-hidden-fields') {{-- Put your additional hidden fields in this section --}} @stop @section('browse-ordering-bar') @jhtml('FEFHelp.browse.orderjs', $this->lists->order) @jhtml('FEFHelp.browse.orderheader', $this) @stop @yield('browse-page-top') {{-- Administrator form for browse views --}} <form action="index.php" method="post" name="adminForm" id="adminForm" class="akeeba-form"> {{-- Filters and ordering --}} <section class="akeeba-panel--33-66 akeeba-filter-bar-container"> <div class="akeeba-filter-bar akeeba-filter-bar--left akeeba-form-section akeeba-form--inline"> @yield('browse-filters') </div> <div class="akeeba-filter-bar akeeba-filter-bar--right"> @yield('browse-ordering-bar') </div> </section> <table class="akeeba-table akeeba-table--striped--hborder--hover" id="itemsList"> <thead> @yield('browse-table-header') </thead> <tfoot> @yield('browse-table-footer') </tfoot> <tbody @if(!is_null($ajaxOrderingSupport) && $ajaxOrderingSupport['saveOrder']) class="js-draggable" data-url="{{ $ajaxOrderingSupport['saveOrderURL'] }}" data-direction="{{ strtolower($this->getModel()->getState('filter_order_Dir', null, 'cmd')) }}" data-nested="{{ ($this->getModel() instanceof \FOF40\Model\TreeModel) ? 'true' : 'false' }}" @endif > @unless(count($this->items)) @yield('browse-table-body-norecords') @else @yield('browse-table-body-withrecords') @endunless </tbody> </table> {{-- Hidden form fields --}} <div class="akeeba-hidden-fields-container"> @section('browse-default-hidden-fields') <input type="hidden" name="option" id="option" value="{{{ $this->getContainer()->componentName }}}"/> <input type="hidden" name="view" id="view" value="{{{ $this->getName() }}}"/> <input type="hidden" name="boxchecked" id="boxchecked" value="0"/> <input type="hidden" name="task" id="task" value="{{{ $this->getTask() }}}"/> <input type="hidden" name="filter_order" id="filter_order" value="{{{ $this->lists->order }}}"/> <input type="hidden" name="filter_order_Dir" id="filter_order_Dir" value="{{{ $this->lists->order_Dir }}}"/> <input type="hidden" name="@token()" value="1"/> @show @yield('browse-hidden-fields') </div> </form> @yield('browse-page-bottom') ViewTemplates/Common/user_select.j3.fef.blade.php 0000604 00000005646 15245560676 0015762 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ defined('_JEXEC') || die; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; /** * User entry field, allowing selection of a user from a modal dialog * * Use this by extending it (I'm using -at- instead of the actual at-sign) * -at-include('any:lib_fof40/Common/user_select', $params) * * This is the variant used when using the FEF renderer under Joomla 3. * * $params is an array defining the following keys (they are expanded into local scope vars automatically): * * @var string $field The user field's name, e.g. "user_id" * @var \FOF40\Model\DataModel $item The item we're editing. The user ID is stored in $item->{$field} * @var string $id The id of the field, default is $field * @var bool $readonly Is this a read only field? Default: false * @var string $placeholder Placeholder text, also used as the button's tooltip * @var bool $required Is a value required for this field? Default: false * @var string $width Width of the modal box (default: 800) * @var string $height Height of the modal box (default: 500) * * Variables made automatically available to us by FOF: * * @var \FOF40\View\DataView\DataViewInterface $this */ $id = isset($id) ? $id : $field; $readonly = isset($readonly) ? ($readonly ? true : false) : false; $placeholder = isset($placeholder) ? JText::_($placeholder) : JText::_('JLIB_FORM_SELECT_USER'); $userID = $item->getFieldValue($field, 0); $user = $item->getContainer()->platform->getUser($userID); $width = isset($width) ? $width : 800; $height = isset($height) ? $height : 500; $class = isset($class) ? $class : ''; $size = isset($size) ? $size : 0; $uri = new JUri('index.php?option=com_users&view=users&layout=modal&tmpl=component'); $uri->setVar('required', (isset($required) ? ($required ? 1 : 0) : 0)); $uri->setVar('field', $field); $url = 'index.php' . $uri->toString(['query']); ?> @unless($readonly) @jhtml('behavior.modal', 'a.userSelectModal_' . $this->escape($field)) @jhtml('script', 'jui/fielduser.min.js', ['version' => 'auto', 'relative' => true]) @endunless <div class="akeeba-input-group"> <input readonly type="text" id="{{{ $field }}}" value="{{{ $user->username }}}" placeholder="{{{ $placeholder }}}"/> <span class="akeeba-input-group-btn"> <a href="@route($url)" class="akeeba-btn--grey userSelectModal_{{{ $field }}}" title="{{{ $placeholder }}}" rel="{handler: 'iframe', size: {x: {{$width}}, y: {{$height}} }}"> <span class="akion-person"></span> </a> </span> </div> @unless($readonly) <input type="hidden" id="{{{ $field }}}_id" name="{{{ $field }}}" value="{{ (int) $userID }}"/> @endunless ViewTemplates/Common/user_show.fef.blade.php 0000604 00000011554 15245560676 0015143 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ /** * User information display field * * Use this by extending it (I'm using -at- instead of the actual at-sign) * -at-include('any:lib_fof40/Common/user_show', $params) * * $params is an array defining the following keys (they are expanded into local scope vars automatically): * * @var \FOF40\Model\DataModel $item The record which holds the user ID in the $field property * @var string $field The name of the field in the current row containing the user ID * @var string $id The ID of the generated DIV * @var string $showUsername Should I display the username? * @var string $showEmail Should I display the email address? * @var string $showName Should I display the full name? * @var string $showID Should I display the numeric user ID? * @var string $showAvatar Should I display the avatar of the user? * @var string $showLink Should I display a link? * @var string $linkURL What link should I display? Default is com_users edit page (backend only). * @var string $avatarMethod Method to display an avatar: gravatar | plugin * @var string $avatarSize Size [pixels] of the avatar. Avatars are square. Size 64 means 64x64 px. * @var string $class Extra class to append * * Variables made automatically available to us by FOF: * * @var \FOF40\View\DataView\Raw $this */ defined('_JEXEC') || die; use FOF40\Html\FEFHelper\BrowseView; global $akeebaSubsShowUserCache; if (!isset($akeebaSubsShowUserCache)) { $akeebaSubsShowUserCache = []; } // Get field parameters $defaultParams = [ 'id' => '', 'showUsername' => true, 'showEmail' => true, 'showName' => true, 'showID' => true, 'showAvatar' => true, 'showLink' => true, 'linkURL' => null, 'avatarMethod' => 'gravatar', 'avatarSize' => 64, 'class' => '', ]; foreach ($defaultParams as $paramName => $paramValue) { if (!isset(${$paramName})) { ${$paramName} = $paramValue; } } unset($defaultParams, $paramName, $paramValue); // Initialization $value = $item->getFieldValue($field); $key = is_numeric($value) ? $value : 'empty'; // Get the user if (!array_key_exists($key, $akeebaSubsShowUserCache)) { $akeebaSubsShowUserCache[$key] = $this->getContainer()->platform->getUser($value); } $user = $akeebaSubsShowUserCache[$key]; // Get the field parameters if ($avatarMethod) { $avatarMethod = strtolower($avatarMethod); } if (!$linkURL && $this->getContainer()->platform->isBackend()) { $linkURL = 'index.php?option=com_users&task=user.edit&id=[USER:ID]'; } elseif (!$linkURL) { // If no link is defined in the front-end, we can't create a default link. Therefore, show no link. $showLink = false; } // Post-process the link URL if ($showLink) { $replacements = array( '[USER:ID]' => $user->id, '[USER:USERNAME]' => $user->username, '[USER:EMAIL]' => $user->email, '[USER:NAME]' => $user->name, ); foreach ($replacements as $key => $value) { $linkURL = str_replace($key, $value, $linkURL); } $linkURL = BrowseView::parseFieldTags($linkURL, $item); } // Get the avatar image, if necessary $avatarURL = ''; if ($showAvatar) { $avatarURL = ''; if ($avatarMethod == 'plugin') { // Use the user plugins to get an avatar $this->getContainer()->platform->importPlugin('user'); $jResponse = $this->getContainer()->platform->runPlugins('onUserAvatar', array($user, $avatarSize)); if (!empty($jResponse)) { foreach ($jResponse as $response) { if ($response) { $avatarURL = $response; } } } } // Fall back to the Gravatar method if (empty($avatarURL)) { $md5 = md5($user->email); $avatarURL = 'https://secure.gravatar.com/avatar/' . $md5 . '.jpg?s=' . $avatarSize . '&d=mm'; } } ?> <div id="{{ $id }}" class="{{ $class }}"> @if($showAvatar) <img src="{{ $avatarURL }}" alt="{{ $showName ? $user->name : ($showUsername ? $user->username : '') }}" align="left" class="fof-usersfield-avatar" /> @endif @if($showLink) <a href="{{ $linkURL }}"> @endif @if($showUsername) <span class="fof-usersfield-username"> {{{ $user->username }}} </span> @endif @if($showID) <span class="fof-usersfield-id"> {{{ $user->id }}} </span> @endif @if($showName) <span class="fof-usersfield-name"> {{{ $user->name }}} </span> @endif @if($showEmail) <span class="fof-usersfield-email"> {{{ $user->email }}} </span> @endif @if($showLink) </a> @endif </div> ViewTemplates/Common/user_select.j4.blade.php 0000604 00000004370 15245560676 0015215 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ defined('_JEXEC') || die; use Joomla\CMS\Language\Text; /** * User entry field, allowing selection of a user from a modal dialog * * Use this by extending it (I'm using -at- instead of the actual at-sign) * -at-include('any:lib_fof40/Common/user_select', $params) * * This is the generic variant used in Joomla 4 (when NOT using the FEF renderer) * * $params is an array defining the following keys (they are expanded into local scope vars automatically): * * @var string $field The user field's name, e.g. "user_id" * @var \FOF40\Model\DataModel $item The item we're editing. The user ID is stored in $item->{$field} * @var string $id The id of the field, default is $field * @var bool $readonly Is this a read only field? Default: false * @var string $placeholder Placeholder text, also used as the button's tooltip * @var bool $required Is a value required for this field? Default: false * @var string $width Width of the modal box (default: 800) * @var string $height Height of the modal box (default: 500) * * Variables made automatically available to us by FOF: * * @var \FOF40\View\DataView\DataViewInterface $this */ $id = isset($id) ? $id : $field; $readonly = isset($readonly) ? ($readonly ? true : false) : false; $placeholder = isset($placeholder) ? Text::_($placeholder) : Text::_('JLIB_FORM_SELECT_USER'); $userID = $item->getFieldValue($field, 0); $user = $item->getContainer()->platform->getUser($userID); $width = isset($width) ? $width : 800; $height = isset($height) ? $height : 500; $class = isset($class) ? $class : ''; $size = isset($size) ? $size : 0; $onchange = isset($onchange) ? $onchange : ''; ?> @jlayout('joomla/form/field/user', [ 'name' => $field, 'id' => $id, 'class' => $class, 'size' => $size, 'value' => $userID, 'userName' => $user->name, 'hint' => $placeholder, 'readonly' => $readonly, 'required' => $required, 'onchange' => $onchange, 'dataAttribute' => '', ]) ViewTemplates/Common/edit.fef.blade.php 0000604 00000004110 15245560676 0014040 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ /** * Template for Edit (form) views using the FEF renderer * * Use this by extending it (I'm using -at- instead of the at-sign) * -at-extends('any:lib_fof40/Common/edit') * * Override the following sections in your Blade template: * * edit-page-top * Content to put above the form * * edit-page-bottom * Content to put below the form * * edit-form-body * The page's body, inside the form * * edit-hidden-fields * [ Optional ] Any additional hidden INPUTs to add to the form. By default this is empty. * The default hidden fields (option, view, task, ordering fields, boxchecked and token) can * not be removed. * * Do not override any other section. The overridden sections should be closed with -at-override instead of -at-stop. */ defined('_JEXEC') or die(); /** @var FOF40\View\DataView\Html $this */ ?> @section('edit-form-body') {{-- Put your form body in this section --}} @stop @section('edit-hidden-fields') {{-- Put your additional hidden fields in this section --}} @stop @yield('edit-page-top') {{-- Administrator form for browse views --}} <form action="index.php" method="post" name="adminForm" id="adminForm" class="akeeba-form--horizontal"> {{-- Main form body --}} @yield('edit-form-body') {{-- Hidden form fields --}} <div class="akeeba-hidden-fields-container"> @section('browse-default-hidden-fields') <input type="hidden" name="option" id="option" value="{{{ $this->getContainer()->componentName }}}"/> <input type="hidden" name="view" id="view" value="{{{ $this->getName() }}}"/> <input type="hidden" name="task" id="task" value="{{{ $this->getTask() }}}"/> <input type="hidden" name="id" id="id" value="{{{ $this->getItem()->getId() }}}"/> <input type="hidden" name="@token()" value="1"/> @show @yield('edit-hidden-fields') </div> </form> @yield('edit-page-bottom') ViewTemplates/Common/user_select.j3.blade.php 0000604 00000005570 15245560676 0015217 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or later */ defined('_JEXEC') || die; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; /** * User entry field, allowing selection of a user from a modal dialog * * Use this by extending it (I'm using -at- instead of the actual at-sign) * -at-include('any:lib_fof40/Common/user_select', $params) * * This is the generic variant used in Joomla 3 (when NOT using the FEF renderer) * * $params is an array defining the following keys (they are expanded into local scope vars automatically): * * @var string $field The user field's name, e.g. "user_id" * @var \FOF40\Model\DataModel $item The item we're editing. The user ID is stored in $item->{$field} * @var string $id The id of the field, default is $field * @var bool $readonly Is this a read only field? Default: false * @var string $placeholder Placeholder text, also used as the button's tooltip * @var bool $required Is a value required for this field? Default: false * @var string $width Width of the modal box (default: 800) * @var string $height Height of the modal box (default: 500) * * Variables made automatically available to us by FOF: * * @var \FOF40\View\DataView\DataViewInterface $this */ $id = isset($id) ? $id : $field; $readonly = isset($readonly) ? ($readonly ? true : false) : false; $placeholder = isset($placeholder) ? JText::_($placeholder) : JText::_('JLIB_FORM_SELECT_USER'); $userID = $item->getFieldValue($field, 0); $user = $item->getContainer()->platform->getUser($userID); $width = isset($width) ? $width : 800; $height = isset($height) ? $height : 500; $class = isset($class) ? $class : ''; $size = isset($size) ? $size : 0; $uri = new JUri('index.php?option=com_users&view=users&layout=modal&tmpl=component'); $uri->setVar('required', (isset($required) ? ($required ? 1 : 0) : 0)); $uri->setVar('field', $field); $url = 'index.php' . $uri->toString(['query']); ?> @unless($readonly) @jhtml('behavior.modal', 'a.userSelectModal_' . $this->escape($field)) @jhtml('script', 'jui/fielduser.min.js', ['version' => 'auto', 'relative' => true]) @endunless <div class="input-append"> <input readonly type="text" id="{{{ $field }}}" value="{{{ $user->username }}}" placeholder="{{{ $placeholder }}}"/> <a href="@route($url)" class="akeeba-btn--grey userSelectModal_{{{ $field }}}" title="{{{ $placeholder }}}" rel="{handler: 'iframe', size: {x: {{$width}}, y: {{$height}} }}"> <span class="akion-person"></span> </a> </div> @unless($readonly) <input type="hidden" id="{{{ $field }}}_id" name="{{{ $field }}}" value="{{ (int) $userID }}"/> @endunless Platform/Joomla/Filesystem.php 0000604 00000015403 15245560676 0012425 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Platform\Joomla; defined('_JEXEC') || die; use FOF40\Platform\Base\Filesystem as BaseFilesystem; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Path; /** * Abstraction for Joomla! filesystem API */ class Filesystem extends BaseFilesystem { /** * Does the file exists? * * @param $path string Path to the file to test * * @return bool */ public function fileExists(string $path): bool { return File::exists($path); } /** * Delete a file or array of files * * @param mixed $file The file name or an array of file names * * @return bool True on success * */ public function fileDelete($file): bool { if (!is_string($file) && !is_array($file)) { throw new \InvalidArgumentException(sprintf('%s::%s -- $file expects a string or an array', __CLASS__, __METHOD__)); } return File::delete($file); } /** * Copies a file * * @param string $src The path to the source file * @param string $dest The path to the destination file * @param string $path An optional base path to prefix to the file names * @param bool $use_streams True to use streams * * @return bool True on success */ public function fileCopy(string $src, string $dest, ?string $path = null, bool $use_streams = false): bool { return File::copy($src, $dest, $path, $use_streams); } /** * Write contents to a file * * @param string $file The full file path * @param string &$buffer The buffer to write * @param bool $use_streams Use streams * * @return bool True on success */ public function fileWrite(string $file, string &$buffer, bool $use_streams = false): bool { return File::write($file, $buffer, $use_streams); } /** * Checks for snooping outside of the file system root. * * @param string $path A file system path to check. * * @return string A cleaned version of the path or exit on error. * * @throws \Exception */ public function pathCheck(string $path): string { return Path::check($path); } /** * Function to strip additional / or \ in a path name. * * @param string $path The path to clean. * @param string $ds Directory separator (optional). * * @return string The cleaned path. * * @throws \UnexpectedValueException */ public function pathClean(string $path, string $ds = DIRECTORY_SEPARATOR): string { return Path::clean($path, $ds); } /** * Searches the directory paths for a given file. * * @param mixed $paths An path string or array of path strings to search in * @param string $file The file name to look for. * * @return string|null The full path and file name for the target file, or bool false if the file is not found * in any of the paths. */ public function pathFind($paths, string $file): ?string { if (!is_string($paths) && !is_array($paths)) { throw new \InvalidArgumentException(sprintf('%s::%s -- $paths expects a string or an array', __CLASS__, __METHOD__)); } $ret = Path::find($paths, $file); if (($ret === false) || ($ret === '')) { return null; } return $ret; } /** * Wrapper for the standard file_exists function * * @param string $path Folder name relative to installation dir * * @return bool True if path is a folder */ public function folderExists(string $path): bool { try { return Folder::exists($path); } catch (\Exception $e) { return false; } } /** * Utility function to read the files in a folder. * * @param string $path The path of the folder to read. * @param string $filter A filter for file names. * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the * maximum depth. * @param bool $full True to return the full path to the file. * @param array $exclude Array with names of files which should not be shown in the result. * @param array $excludefilter Array of filter to exclude * @param bool $naturalSort False for asort, true for natsort * @param bool $naturalSort False for asort, true for natsort * * @return array Files in the given folder. */ public function folderFiles(string $path, string $filter = '.', bool $recurse = false, bool $full = false, array $exclude = [ '.svn', 'CVS', '.DS_Store', '__MACOSX', ], array $excludefilter = ['^\..*', '.*~'], bool $naturalSort = false): array { // JFolder throws nonsense errors if the path is not a folder try { $path = Path::clean($path); } catch (\Exception $e) { return []; } if (!@is_dir($path)) { return []; } // Now call JFolder return Folder::files($path, $filter, $recurse, $full, $exclude, $excludefilter, $naturalSort); } /** * Utility function to read the folders in a folder. * * @param string $path The path of the folder to read. * @param string $filter A filter for folder names. * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the * maximum depth. * @param bool $full True to return the full path to the folders. * @param array $exclude Array with names of folders which should not be shown in the result. * @param array $excludefilter Array with regular expressions matching folders which should not be shown in * the result. * * @return array Folders in the given folder. */ public function folderFolders(string $path, string $filter = '.', bool $recurse = false, bool $full = false, array $exclude = [ '.svn', 'CVS', '.DS_Store', '__MACOSX', ], array $excludefilter = ['^\..*']): array { // JFolder throws idiotic errors if the path is not a folder try { $path = Path::clean($path); } catch (\Exception $e) { return []; } if (!@is_dir($path)) { return []; } // Now call JFolder return Folder::folders($path, $filter, $recurse, $full, $exclude, $excludefilter); } /** * Create a folder -- and all necessary parent folders. * * @param string $path A path to create from the base path. * @param integer $mode Directory permissions to set for folders created. 0755 by default. * * @return bool True if successful. */ public function folderCreate(string $path = '', int $mode = 0755): bool { return Folder::create($path, $mode); } } Platform/Joomla/Platform.php 0000604 00000115470 15245560676 0012072 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Platform\Joomla; defined('_JEXEC') || die; use ActionlogsModelActionlog; use DateTime; use DateTimeZone; use Exception; use FOF40\Container\Container; use FOF40\Date\Date; use FOF40\Date\DateDecorator; use FOF40\Input\Input; use FOF40\Platform\Base\Platform as BasePlatform; use InvalidArgumentException; use JDatabaseDriver; use JEventDispatcher; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Application\CliApplication; use Joomla\CMS\Application\CliApplication as JApplicationCli; use Joomla\CMS\Application\ConsoleApplication; use Joomla\CMS\Authentication\Authentication; use Joomla\CMS\Authentication\AuthenticationResponse; use Joomla\CMS\Cache\Cache; use Joomla\CMS\Document\Document; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Factory as JoomlaFactory; use Joomla\CMS\Language\Language; use Joomla\CMS\Log\Log; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Session\Session; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; use Joomla\CMS\User\UserHelper; use Joomla\CMS\Version as JoomlaVersion; use Joomla\Event\Event; use Joomla\Registry\Registry; /** * Part of the FOF Platform Abstraction Layer. * * This implements the platform class for Joomla! 3 and Joomla! 4 * * @since 2.1 */ class Platform extends BasePlatform { /** * Is this a CLI application? * * @var bool */ protected static $isCLI; /** * Is this an administrator application? * * @var bool */ protected static $isAdmin; /** * Is this an API application? * * @var bool */ protected static $isApi; /** * A fake session storage for CLI apps. Since CLI applications cannot have a session we are using a Registry object * we manage internally. * * @var Registry */ protected static $fakeSession; /** * The table and table field cache object, used to speed up database access * * @var Registry|null */ private $_cache; /** * Public constructor. * * Overridden to cater for CLI applications not having access to a session object. * * @param Container $c The component container */ public function __construct(Container $c) { parent::__construct($c); if ($this->isCli()) { self::$fakeSession = new Registry(); } } /** * Checks if the current script is run inside a valid CMS execution * * @return bool */ public function checkExecution(): bool { return defined('_JEXEC'); } /** * Raises an error, using the logic requested by the CMS (PHP Exception or dedicated class) * * @param integer $code * @param string $message * * @return void * * @throws Exception * * @deprecated 5.0 Use showErrorPage with a real exception instead */ public function raiseError(int $code, string $message): void { $this->showErrorPage(new Exception($message, $code)); } /** * Returns absolute path to directories used by the containing CMS/application. * * The return is a table with the following key: * * 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 * * @return array A hash array with keys root, public, admin, tmp and log. */ public function getPlatformBaseDirs(): array { return [ 'root' => JPATH_ROOT, 'public' => JPATH_SITE, 'media' => JPATH_SITE . '/media', 'admin' => JPATH_ADMINISTRATOR, 'api' => defined('JPATH_API') ? JPATH_API : (JPATH_ROOT . '/api'), 'tmp' => JoomlaFactory::getConfig()->get('tmp_path'), 'log' => JoomlaFactory::getConfig()->get('log_path'), ]; } /** * Returns the base (root) directories for a given component, i.e the application * which is running inside our main application (CMS, web app). * * The return is a table with the following keys: * * main The normal location of component files. For a back-end Joomla! * component this is the administrator/components/com_example * directory. * * alt The alternate location of component files. For a back-end * Joomla! component this is the front-end directory, e.g. * components/com_example * * site The location of the component files serving the public part of * the application. * * admin The location of the component files serving the administrative * part of the application. * * api The location of the component files serving the API part of the application * * All paths MUST be absolute. All paths MAY be the same if the * platform doesn't make a distinction between public and private parts, * or when the component does not provide both a public and private part. * All of the directories MUST be defined and non-empty. * * @param string $component The name of the component. For Joomla! this * is something like "com_example" * * @return array A hash array with keys main, alt, site and admin. */ public function getComponentBaseDirs(string $component): array { if (!$this->isBackend()) { $mainPath = JPATH_SITE . '/components/' . $component; $altPath = JPATH_ADMINISTRATOR . '/components/' . $component; } else { $mainPath = JPATH_ADMINISTRATOR . '/components/' . $component; $altPath = JPATH_SITE . '/components/' . $component; } return [ 'main' => $mainPath, 'alt' => $altPath, 'site' => JPATH_SITE . '/components/' . $component, 'admin' => JPATH_ADMINISTRATOR . '/components/' . $component, 'api' => (defined('JPATH_API') ? JPATH_API : (JPATH_ROOT . '/api')) . '/components/' . $component, ]; } /** * Returns the application's template name * * @param null|array $params An optional associative array of configuration settings * * @return string The template name. "system" is the fallback. */ public function getTemplate(?array $params = null): string { try { return JoomlaFactory::getApplication()->getTemplate($params ?? false); } catch (Exception $e) { return 'system'; } } /** * Get application-specific suffixes to use with template paths. This allows * you to look for view template overrides based on the application version. * * @return array A plain array of suffixes to try in template names */ public function getTemplateSuffixes(): array { $jversion = new JoomlaVersion; $versionParts = explode('.', $jversion->getShortVersion()); $majorVersion = array_shift($versionParts); return [ '.j' . str_replace('.', '', $jversion->getHelpVersion()), '.j' . $majorVersion, ]; } /** * Return the absolute path to the application's template overrides * directory for a specific component. We will use it to look for template * files instead of the regular component directories. If the application * does not have such a thing as template overrides return an empty string. * * @param string $component The name of the component for which to fetch the overrides * @param bool $absolute Should I return an absolute or relative path? * * @return string The path to the template overrides directory */ public function getTemplateOverridePath(string $component, bool $absolute = true): string { if (!$this->isCli()) { if ($absolute) { $path = JPATH_THEMES . '/'; } else { $path = $this->isBackend() ? 'administrator/templates/' : 'templates/'; } $directory = (substr($component, 0, 7) == 'media:/') ? ('media/' . substr($component, 7)) : ('html/' . $component); $path .= $this->getTemplate() . '/' . $directory; } else { $path = ''; } return $path; } /** * Load the translation files for a given component. * * @param string $component The name of the component, e.g. "com_example" * * @return void */ public function loadTranslations(string $component): void { $paths = $this->isBackend() ? [JPATH_ROOT, JPATH_ADMINISTRATOR] : [JPATH_ADMINISTRATOR, JPATH_ROOT]; $jlang = $this->getLanguage(); $jlang->load($component, $paths[0], 'en-GB', true); $jlang->load($component, $paths[0], null, true); $jlang->load($component, $paths[1], 'en-GB', true); $jlang->load($component, $paths[1], null, true); } /** * By default FOF will only use the Controller's onBefore* methods to * perform user authorisation. In some cases, like the Joomla! back-end, * you also need to perform component-wide user authorisation in the * Dispatcher. This method MUST implement this authorisation check. If you * do not need this in your platform, please always return true. * * @param string $component The name of the component. * * @return bool True to allow loading the component, false to halt loading */ public function authorizeAdmin(string $component): bool { if ($this->isBackend()) { // Master access check for the back-end, Joomla! 1.6 style. $user = $this->getUser(); if (!$user->authorise('core.manage', $component) && !$user->authorise('core.admin', $component) ) { return false; } } return true; } /** * Returns a user object. * * @param integer $id The user ID to load. Skip or use null to retrieve * the object for the currently logged in user. * * @return User The User object for the specified user */ public function getUser(?int $id = null): User { /** * If I'm in CLI I need load the User directly, otherwise JoomlaFactory will check the session (which doesn't exist * in CLI) */ if ($this->isCli()) { if ($id) { return User::getInstance($id) ?? new User(); } return new User(); } // Joomla 3 if (version_compare(JVERSION, '3.999.999', 'lt')) { return JoomlaFactory::getUser($id) ?? new User(); } // Joomla 4 if (is_null($id)) { return JoomlaFactory::getApplication()->getIdentity() ?? new User(); } return JoomlaFactory::getContainer()->get(UserFactoryInterface::class)->loadUserById($id) ?? new User(); } /** * Returns the Document object which handles this component's response. You * may also return null and FOF will a. try to figure out the output type by * examining the "format" input parameter (or fall back to "html") and b. * FOF will not attempt to load CSS and Javascript files (as it doesn't make * sense if there's no Document to handle them). * * @return Document|null */ public function getDocument(): ?Document { $document = null; if (!$this->isCli()) { try { $document = JoomlaFactory::getDocument(); } catch (Exception $exc) { $document = null; } } return $document; } /** * Returns an object to handle dates * * @param mixed $time The initial time * @param DateTimeZone|string|null $tzOffset The timezone offset * @param bool $locale Should I try to load a specific class for current language? * * @return Date object */ public function getDate(?string $time = 'now', $tzOffset = null, $locale = true): Date { $time = $time ?? $this->getDbo()->getNullDate() ?? 'now'; if (!is_string($time) && (!is_object($time) || !($time instanceof DateTime))) { throw new InvalidArgumentException(sprintf('%s::%s -- $time expects a string or a DateTime object', __CLASS__, __METHOD__)); } if ($locale) { // Work around a bug in Joomla! 3.7.0. if ($time == 'now') { $time = time(); } $coreObject = JoomlaFactory::getDate($time, $tzOffset); return new DateDecorator($coreObject); } else { return new Date($time, $tzOffset); } } /** * Return the Language instance of the CMS/application * * @return Language */ public function getLanguage(): Language { return JoomlaFactory::getLanguage(); } /** * Returns the database driver object of the CMS/application * * @return JDatabaseDriver */ public function getDbo(): JDatabaseDriver { return JoomlaFactory::getDbo(); } /** * This method will try retrieving a variable from the request (input) data. * If it doesn't exist it will be loaded from the user state, typically * stored in the session. If it doesn't exist there either, the $default * value will be used. If $setUserState is set to true, the retrieved * variable will be stored in the user session. * * @param string $key The user state key for the variable * @param string $request The request variable name for the variable * @param Input $input The Input object with the request (input) data * @param mixed $default The default value. Default: null * @param string $type The filter type for the variable data. Default: none (no filtering) * @param bool $setUserState Should I set the user state with the fetched value? * * @return mixed The value of the variable */ public function getUserStateFromRequest(string $key, string $request, Input $input, $default = null, string $type = 'none', bool $setUserState = true) { if ($this->isCli()) { $ret = $input->get($request, $default, $type); if ($ret === $default) { $input->set($request, $ret); } return $ret; } try { $app = JoomlaFactory::getApplication(); } catch (Exception $e) { $app = null; } $old_state = (!is_null($app) && method_exists($app, 'getUserState')) ? $app->getUserState($key, $default) : null; $cur_state = (!is_null($old_state)) ? $old_state : $default; $new_state = $input->get($request, null, $type); // Save the new value only if it was set in this request if ($setUserState) { if ($new_state !== null) { $app->setUserState($key, $new_state); } else { $new_state = $cur_state; } } elseif (is_null($new_state)) { $new_state = $cur_state; } return $new_state; } /** * Load plugins of a specific type. Obviously this seems to only be required * in the Joomla! CMS. * * @param string $type The type of the plugins to be loaded * * @return void * * @codeCoverageIgnore * @see PlatformInterface::importPlugin() * */ public function importPlugin(string $type): void { // Should I actually run the plugins? $runPlugins = $this->isAllowPluginsInCli() || !$this->isCli(); if ($runPlugins) { PluginHelper::importPlugin($type); } } /** * Execute plugins (system-level triggers) and fetch back an array with * their return values. * * @param string $event The event (trigger) name, e.g. onBeforeScratchMyEar * @param array $data A hash array of data sent to the plugins as part of the trigger * * @return array A simple array containing the results of the plugins triggered */ public function runPlugins(string $event, array $data = []): array { // Should I actually run the plugins? $runPlugins = $this->isAllowPluginsInCli() || !$this->isCli(); if ($runPlugins) { if (class_exists('JEventDispatcher')) { return JEventDispatcher::getInstance()->trigger($event, $data); } // If there's no JEventDispatcher try getting JApplication try { $app = JoomlaFactory::getApplication(); } catch (Exception $e) { // If I can't get JApplication I cannot run the plugins. return []; } // Joomla 3 and 4 have triggerEvent if (method_exists($app, 'triggerEvent')) { return $app->triggerEvent($event, $data); } // Joomla 5 (and possibly some 4.x versions) don't have triggerEvent. Go through the Events dispatcher. if (method_exists($app, 'getDispatcher') && class_exists('Joomla\Event\Event')) { try { $dispatcher = $app->getDispatcher(); } catch (\UnexpectedValueException $exception) { return []; } if ($data instanceof Event) { $eventObject = $data; } elseif (\is_array($data)) { $eventObject = new Event($event, $data); } else { throw new \InvalidArgumentException('The plugin data must either be an event or an array'); } $result = $dispatcher->dispatch($event, $eventObject); return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; } // No viable way to run the plugins :( return []; } else { return []; } } /** * Perform an ACL check. Please note that FOF uses by default the Joomla! * CMS convention for ACL privileges, e.g core.edit for the edit privilege. * If your platform uses different conventions you'll have to override the * FOF defaults using fof.xml or by specialising the controller. * * @param string $action The ACL privilege to check, e.g. core.edit * @param string|null $assetname The asset name to check, typically the component's name * * @return bool True if the user is allowed this action */ public function authorise(string $action, ?string $assetname = null): bool { if ($this->isCli()) { return true; } $ret = JoomlaFactory::getUser()->authorise($action, $assetname); // Work around Joomla returning null instead of false in some cases. return (bool) $ret; } /** * Is this the administrative section of the component? * * @return bool */ public function isBackend(): bool { [$isCli, $isAdmin, $isApi] = $this->isCliAdminApi(); return $isAdmin && !$isCli && !$isApi; } /** * Is this the public section of the component? * * @param bool $strict True to only confirm if we're under the 'site' client. False to confirm if we're under * either 'site' or 'api' client (both are front-end access). The default is false which * causes the method to return true when the application is either 'client' (HTML frontend) * or 'api' (JSON frontend). * * @return bool */ public function isFrontend(bool $strict = false): bool { [$isCli, $isAdmin, $isApi] = $this->isCliAdminApi(); if ($strict) { return !$isAdmin && !$isCli && !$isApi; } return !$isAdmin && !$isCli; } /** * Is this a component running in a CLI application? * * @return bool */ public function isCli(): bool { [$isCli, $isAdmin, $isApi] = $this->isCliAdminApi(); return !$isAdmin && !$isApi && $isCli; } /** * Is this a component running under the API application? * * @return bool */ public function isApi(): bool { [$isCli, $isAdmin, $isApi] = $this->isCliAdminApi(); return $isApi && !$isAdmin && !$isCli; } /** * Is the global FOF cache enabled? * * @return bool */ public function isGlobalFOFCacheEnabled(): bool { return !(defined('JDEBUG') && JDEBUG); } /** * Retrieves data from the cache. This is supposed to be used for system-side * FOF data, not application data. * * @param string $key The key of the data to retrieve * @param string|null $default The default value to return if the key is not found or the cache is not populated * * @return string|null The cached value */ public function getCache(string $key, ?string $default = null): ?string { $registry = $this->getCacheObject(); return $registry->get($key, $default); } /** * Saves something to the cache. This is supposed to be used for system-wide * FOF data, not application data. * * @param string $key The key of the data to save * @param string $content The actual data to save * * @return bool True on success */ public function setCache(string $key, string $content): bool { $registry = $this->getCacheObject(); $registry->set($key, $content); return $this->saveCache(); } /** * Clears the cache of system-wide FOF data. You are supposed to call this in * your components' installation script post-installation and post-upgrade * methods or whenever you are modifying the structure of database tables * accessed by FOF. Please note that FOF's cache never expires and is not * purged by Joomla!. You MUST use this method to manually purge the cache. * * @return bool True on success */ public function clearCache(): bool { $false = false; $cache = JoomlaFactory::getCache('fof', ''); return $cache->store($false, 'cache', 'fof'); } /** * Returns an object that holds the configuration of the current site. * * @return Registry * * @codeCoverageIgnore */ public function getConfig(): Registry { return JoomlaFactory::getConfig(); } /** * logs in a user * * @param array $authInfo Authentication information * * @return bool True on success */ public function loginUser(array $authInfo): bool { $options = ['remember' => false]; $response = new AuthenticationResponse(); $response->type = 'fof'; $response->status = Authentication::STATUS_FAILURE; if (isset($authInfo['username'])) { $authenticate = Authentication::getInstance(); $response = $authenticate->authenticate($authInfo, $options); } // Use our own authentication handler, onFOFUserAuthenticate, as a fallback if ($response->status != Authentication::STATUS_SUCCESS) { $this->container->platform->importPlugin('user'); $this->container->platform->importPlugin('fof'); $pluginResults = $this->container->platform->runPlugins('onFOFUserAuthenticate', [$authInfo, $options]); /** * Loop through all plugin results until we find a successful login. On failure we fall back to Joomla's * previous authentication response. */ foreach ($pluginResults as $result) { if (empty($result)) { continue; } if (!is_object($result) || !($result instanceof AuthenticationResponse)) { continue; } if ($result->status != Authentication::STATUS_SUCCESS) { continue; } $response = $result; break; } } // User failed to authenticate: maybe he enabled two factor authentication? // Let's try again "manually", skipping the check vs two factor auth // Due the big mess with encryption algorithms and libraries, we are doing this extra check only // if we're in Joomla 2.5.18+ or 3.2.1+ if ($response->status != Authentication::STATUS_SUCCESS && method_exists('\Joomla\CMS\User\UserHelper', 'verifyPassword')) { $db = JoomlaFactory::getDbo(); $query = $db->getQuery(true) ->select($db->qn(['id', 'password'])) ->from('#__users') ->where('username=' . $db->quote($authInfo['username'])); $result = $db->setQuery($query)->loadObject(); if ($result) { $match = UserHelper::verifyPassword($authInfo['password'], $result->password, $result->id); if ($match === true) { // Bring this in line with the rest of the system $user = $this->getUser($result->id); $response->email = $user->email; $response->fullname = $user->name; $response->language = $this->isBackend() ? $user->getParam('admin_language') : $user->getParam('language'); $response->status = Authentication::STATUS_SUCCESS; $response->error_message = ''; } } } if ($response->status == Authentication::STATUS_SUCCESS) { $this->importPlugin('user'); $results = $this->runPlugins('onLoginUser', [(array) $response, $options]); unset($results); // Just to make phpStorm happy $userid = UserHelper::getUserId($response->username); $user = $this->getUser($userid); $session = $this->container->session; $session->set('user', $user); return true; } return false; } /** * logs out a user * * @return bool True on success */ public function logoutUser(): bool { try { $app = JoomlaFactory::getApplication(); } catch (Exception $e) { return false; } $user = $this->getUser(); $options = ['remember' => false]; $parameters = [ 'username' => $user->username, 'id' => $user->id, ]; // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. if (!$app->get('shared_session', '0')) { $options['clientid'] = $app->getClientId(); } $ret = $app->triggerEvent('onUserLogout', [$parameters, $options]); return !in_array(false, $ret, true); } /** * Add a log file for FOF * * @param string $file * * @return void */ public function logAddLogger($file): void { Log::addLogger(['text_file' => $file], Log::ALL, ['fof']); } /** * Logs a deprecated practice. In Joomla! this results in the $message being output in the * deprecated log file, found in your site's log directory. * * @param string $message The deprecated practice log message * * @return void */ public function logDeprecated(string $message): void { Log::add($message, Log::WARNING, 'deprecated'); } /** * Adds a message to the application's debug log * * @param string $message * * @return void * * @codeCoverageIgnore */ public function logDebug(string $message): void { Log::add($message, Log::DEBUG, 'fof'); } /** @inheritDoc */ public function logUserAction($title, string $logText, string $extension, User $user = null): void { if (!is_string($title) && !is_array($title)) { throw new InvalidArgumentException(sprintf('%s::%s -- $title expects a string or an array', __CLASS__, __METHOD__)); } static $joomlaModelAdded = false; // User Actions Log is available only under Joomla 3.9+ if (version_compare(JVERSION, '3.9', 'lt')) { return; } // Do not perform logging if we're under CLI. Even if we _could_ have a logged user in CLI, ActionlogsModelActionlog // model always uses JoomlaFactory to fetch the current user, fetching data from the session. This means that under the CLI // (where there is no session) such session is started, causing warnings because usually output was already started before if ($this->isCli()) { return; } // Include required Joomla Model if (!$joomlaModelAdded) { BaseDatabaseModel::addIncludePath(JPATH_ROOT . '/administrator/components/com_actionlogs/models', 'ActionlogsModel'); $joomlaModelAdded = true; } $user = $this->getUser(); // No log for guest users if ($user->guest) { return; } $message = [ 'title' => $title, 'username' => $user->username, 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, ]; if (is_array($title)) { unset ($message['title']); $message = array_merge($message, $title); } /** @var ActionlogsModelActionlog $model * */ try { $model = BaseDatabaseModel::getInstance('Actionlog', 'ActionlogsModel'); $model->addLog([$message], $logText, $extension, $user->id); } catch (Exception $e) { // Ignore any error } } /** * Returns the root URI for the request. * * @param bool $pathonly If false, prepend the scheme, host and port information. Default is false. * @param string|null $path The path * * @return string The root URI string. * * @codeCoverageIgnore */ public function URIroot(bool $pathonly = false, ?string $path = null): string { return Uri::root($pathonly, $path); } /** * Returns the base URI for the request. * * @param bool $pathonly If false, prepend the scheme, host and port information. Default is false. * * @return string The base URI string */ public function URIbase(bool $pathonly = false): string { return Uri::base($pathonly); } /** * Method to set a response header. If the replace flag is set then all headers * with the given name will be replaced by the new one (only if the current platform supports header caching) * * @param string $name The name of the header to set. * @param string $value The value of the header to set. * @param bool $replace True to replace any headers with the same name. * * @return void * * @codeCoverageIgnore */ public function setHeader(string $name, string $value, bool $replace = false): void { try { JoomlaFactory::getApplication()->setHeader($name, $value, $replace); } catch (Exception $e) { return; } } /** * In platforms that perform header caching, send all headers. * * @return void * * @codeCoverageIgnore */ public function sendHeaders(): void { try { JoomlaFactory::getApplication()->sendHeaders(); } catch (Exception $e) { return; } } /** * Immediately terminate the containing application's execution * * @param int $code The result code which should be returned by the application * * @return void */ public function closeApplication(int $code = 0): void { // Necessary workaround for broken System - Page Cache plugin in Joomla! 3.7.0 $this->bugfixJoomlaCachePlugin(); try { JoomlaFactory::getApplication()->close($code); } catch (Exception $e) { exit($code); } } /** * Perform a redirection to a different page, optionally enqueuing a message for the user. * * @param string $url The URL to redirect to * @param int $status (optional) The HTTP redirection status code, default 303 (See Other) * @param string $msg (optional) A message to enqueue * @param string $type (optional) The message type, e.g. 'message' (default), 'warning' or 'error'. * * @return void */ public function redirect(string $url, int $status = 301, ?string $msg = null, string $type = 'message'): void { // Necessary workaround for broken System - Page Cache plugin in Joomla! 3.7.0 $this->bugfixJoomlaCachePlugin(); try { $app = JoomlaFactory::getApplication(); } catch (Exception $e) { die(sprintf('Please go to <a href="%s">%1$s</a>', $url)); } if (!empty($msg)) { if (empty($type)) { $type = 'message'; } $app->enqueueMessage($msg, $type); } // Joomla 4: redirecting to index.php in the backend takes you to the frontend. I need to address that. $isJoomla4 = version_compare(JVERSION, '3.999.999', 'gt'); $isBareIndex = substr($url, 0, 9) === 'index.php'; if ($isJoomla4 && $isBareIndex && $this->isBackend()) { $givenUri = new Uri($url); $newUri = new Uri(Uri::base()); $newUri->setQuery($givenUri->getQuery()); if ($givenUri->getFragment()) { $newUri->setFragment($givenUri->getFragment()); } $url = $newUri->toString(); } // Finally, do the redirection $app->redirect($url, $status); } /** * Handle an exception in a way that results to an error page. We use this under Joomla! to work around a bug in * Joomla! 3.7 which results in error pages leading to white pages because Joomla's System - Page Cache plugin is * broken. * * @param Exception $exception The exception to handle * * @throws Exception We rethrow the exception */ public function showErrorPage(Exception $exception): void { // Necessary workaround for broken System - Page Cache plugin in Joomla! 3.7.0 $this->bugfixJoomlaCachePlugin(); throw $exception; } /** * Set a variable in the user session * * @param string $name The name of the variable to set * @param string|null $value (optional) The value to set it to, default is null * @param string $namespace (optional) The variable's namespace e.g. the component name. Default: 'default' * * @return void */ public function setSessionVar(string $name, $value = null, string $namespace = 'default'): void { // CLI if ($this->isCli() && !class_exists('FOFApplicationCLI')) { static::$fakeSession->set("$namespace.$name", $value); return; } // Joomla 3 if (version_compare(JVERSION, '3.9999.9999', 'le')) { $this->container->session->set($name, $value, $namespace); } // Joomla 4 if (empty($namespace)) { $this->container->session->set($name, $value); return; } $registry = $this->container->session->get('registry'); if (is_null($registry)) { $registry = new Registry(); $this->container->session->set('registry', $registry); } $registry->set($namespace . '.' . $name, $value); } /** * Get a variable from the user session * * @param string $name The name of the variable to set * @param string $default (optional) The default value to return if the variable does not exit, default: null * @param string $namespace (optional) The variable's namespace e.g. the component name. Default: 'default' * * @return mixed */ public function getSessionVar(string $name, $default = null, $namespace = 'default') { // CLI if ($this->isCli() && !class_exists('FOFApplicationCLI')) { return static::$fakeSession->get("$namespace.$name", $default); } // Joomla 3 if (version_compare(JVERSION, '3.9999.9999', 'le')) { return $this->container->session->get($name, $default, $namespace); } // Joomla 4 if (empty($namespace)) { return $this->container->session->get($name, $default); } $registry = $this->container->session->get('registry'); if (is_null($registry)) { $registry = new Registry(); $this->container->session->set('registry', $registry); } return $registry->get($namespace . '.' . $name, $default); } /** * Unset a variable from the user session * * @param string $name The name of the variable to unset * @param string $namespace (optional) The variable's namespace e.g. the component name. Default: 'default' * * @return void */ public function unsetSessionVar(string $name, string $namespace = 'default'): void { $this->setSessionVar($name, null, $namespace); } /** * Return the session token. Two types of tokens can be returned: * * Session token ($formToken == false): Used for anti-spam protection of forms. This is specific to a session * object. * * Form token ($formToken == true): A secure hash of the user ID with the session token. Both the session and the * user are fetched from the application container. * * @param bool $formToken Should I return a form token? * @param bool $forceNew Should I force the creation of a new token? * * @return mixed */ public function getToken(bool $formToken = false, bool $forceNew = false): string { // For CLI apps we implement our own fake token system if ($this->isCli()) { $token = $this->getSessionVar('session.token'); // Create a token if (is_null($token) || $forceNew) { $token = UserHelper::genRandomPassword(32); $this->setSessionVar('session.token', $token); } if (!$formToken) { return $token; } $user = $this->getUser(); return ApplicationHelper::getHash($user->id . $token); } // Web application, go through the regular Joomla! API. if ($formToken) { return Session::getFormToken($forceNew); } return $this->container->session->getToken($forceNew); } /** @inheritDoc */ public function addScriptOptions($key, $value, $merge = true) { /** @var HtmlDocument $document */ $document = $this->getDocument(); if (!method_exists($document, 'addScriptOptions')) { return; } $document->addScriptOptions($key, $value, $merge); } /** @inheritDoc */ public function getScriptOptions($key = null) { /** @var HtmlDocument $document */ $document = $this->getDocument(); if (!method_exists($document, 'getScriptOptions')) { return []; } return $document->getScriptOptions($key); } /** * Main function to detect if we're running in a CLI environment, if we're admin or if it's an API application * * @return array isCLI and isAdmin. It's not an associative array, so we can use list(). */ protected function isCliAdminApi(): array { if (is_null(static::$isCLI) && is_null(static::$isAdmin)) { static::$isCLI = false; static::$isAdmin = false; static::$isApi = false; try { if (is_null(JoomlaFactory::$application)) { static::$isCLI = true; static::$isAdmin = false; return [static::$isCLI, static::$isAdmin, static::$isApi]; } $app = JoomlaFactory::getApplication(); static::$isCLI = $app instanceof Exception || $app instanceof CliApplication; if (class_exists('Joomla\CMS\Application\CliApplication')) { static::$isCLI = static::$isCLI || $app instanceof JApplicationCli; } if (class_exists('Joomla\CMS\Application\ConsoleApplication')) { static::$isCLI = static::$isCLI || ($app instanceof ConsoleApplication); } } catch (Exception $e) { static::$isCLI = true; } if (static::$isCLI) { return [static::$isCLI, static::$isAdmin, static::$isApi]; } try { $app = JoomlaFactory::getApplication(); } catch (Exception $e) { return [static::$isCLI, static::$isAdmin, static::$isApi]; } if (method_exists($app, 'isAdmin')) { static::$isAdmin = $app->isAdmin(); } elseif (method_exists($app, 'isClient')) { static::$isAdmin = $app->isClient('administrator'); static::$isApi = $app->isClient('api'); } } return [static::$isCLI, static::$isAdmin, static::$isApi]; } /** * Gets a reference to the cache object, loading it from the disk if * needed. * * @param bool $force Should I forcibly reload the registry? * * @return Registry */ private function &getCacheObject(bool $force = false): Registry { // Check if we have to load the cache file or we are forced to do that if (is_null($this->_cache) || $force) { // Try to get data from Joomla!'s cache $cache = JoomlaFactory::getCache('fof', ''); $this->_cache = $cache->get('cache', 'fof'); $isRegistry = is_object($this->_cache); if ($isRegistry) { $isRegistry = $this->_cache instanceof Registry; } if (!$isRegistry) { // Create a new Registry object $this->_cache = new Registry(); } } return $this->_cache; } /** * Save the cache object back to disk * * @return bool True on success */ private function saveCache(): bool { // Get the Registry object of our cached data $registry = $this->getCacheObject(); $cache = JoomlaFactory::getCache('fof', ''); return $cache->store($registry, 'cache', 'fof'); } /** * Joomla! 3.7 has a broken System - Page Cache plugin. When this plugin is enabled it FORCES the caching of all * pages as soon as Joomla! starts loading, before the plugin has a chance to request to not be cached. Event worse, * in case of a redirection, it doesn't try to remove the cache lock. This means that the next request will be * treated as though the result of the page should be cached. Since there is NO cache content for the page Joomla! * returns an empty response with a 200 OK header. This will, of course, get in the way of every single attempt to * perform a redirection in the frontend of the site. * * @return void */ private function bugfixJoomlaCachePlugin(): void { // Only do something when the System - Cache plugin is activated if (!class_exists('PlgSystemCache')) { return; } // Forcibly uncache the current request $options = [ 'defaultgroup' => 'page', 'browsercache' => false, 'caching' => false, ]; $cache_key = Uri::getInstance()->toString(); Cache::getInstance('page', $options)->cache->remove($cache_key, 'page'); } } Platform/PlatformInterface.php 0000604 00000044424 15245560676 0012472 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Platform; defined('_JEXEC') || die; use DateTimeZone; use Exception; use FOF40\Container\Container; use FOF40\Date\Date; use FOF40\Input\Input; use JDatabaseDriver; use Joomla\CMS\Document\Document; use Joomla\CMS\Language\Language; use Joomla\CMS\User\User; use Joomla\Registry\Registry; use JsonSerializable; /** * Part of the FOF Platform Abstraction Layer. It implements everything that * depends on the platform FOF is running under, e.g. the Joomla! CMS front-end, * the Joomla! CMS back-end, a CLI Joomla! Platform app, a bespoke Joomla! * Platform / Framework web application and so on. */ interface PlatformInterface { /** * Public constructor. * * @param Container $c The component container */ public function __construct(Container $c); /** * Checks if the current script is run inside a valid CMS execution * * @return bool */ public function checkExecution(): bool; /** * Raises an error, using the logic requested by the CMS (PHP Exception or dedicated class) * * @param integer $code * @param string $message * * @return void * * @throws Exception * * @deprecated 5.0 */ public function raiseError(int $code, string $message): void; /** * Returns the version number string of the CMS/application we're running in * * @return string * * @since 2.1.2 */ public function getPlatformVersion(): string; /** * Returns absolute path to directories used by the containing CMS/application. * * The return is a table with the following key: * * root Path to the site root * * public Path to the public area of the site * * admin Path to the administrative area of the site * * tmp Path to the temp directory * * log Path to the log directory * * @return array A hash array with keys root, public, admin, tmp and log. */ public function getPlatformBaseDirs(): array; /** * Returns the base (root) directories for a given component, i.e the application * which is running inside our main application (CMS, web app). * * The return is a table with the following keys: * * main The normal location of component files. For a back-end Joomla! * component this is the administrator/components/com_example * directory. * * alt The alternate location of component files. For a back-end * Joomla! component this is the front-end directory, e.g. * components/com_example * * site The location of the component files serving the public part of * the application. * * admin The location of the component files serving the administrative * part of the application. * * All paths MUST be absolute. All four paths MAY be the same if the * platform doesn't make a distinction between public and private parts, * or when the component does not provide both a public and private part. * All of the directories MUST be defined and non-empty. * * @param string $component The name of the component. For Joomla! this * is something like "com_example" * * @return array A hash array with keys main, alt, site and admin. */ public function getComponentBaseDirs(string $component): array; /** * Returns the application's template name * * @param null|array $params An optional associative array of configuration settings * * @return string The template name. "system" is the fallback. */ public function getTemplate(?array $params = null): string; /** * Get application-specific suffixes to use with template paths. This allows * you to look for view template overrides based on the application version. * * @return array A plain array of suffixes to try in template names */ public function getTemplateSuffixes(): array; /** * Return the absolute path to the application's template overrides * directory for a specific component. We will use it to look for template * files instead of the regular component directories. If the application * does not have such a thing as template overrides return an empty string. * * @param string $component The name of the component for which to fetch the overrides * @param bool $absolute Should I return an absolute or relative path? * * @return string The path to the template overrides directory */ public function getTemplateOverridePath(string $component, bool $absolute = true): string; /** * Load the translation files for a given component. * * @param string $component The name of the component, e.g. "com_example" * * @return void */ public function loadTranslations(string $component): void; /** * By default FOF will only use the Controller's onBefore* methods to * perform user authorisation. In some cases, like the Joomla! back-end, * you also need to perform component-wide user authorisation in the * Dispatcher. This method MUST implement this authorisation check. If you * do not need this in your platform, please always return true. * * @param string $component The name of the component. * * @return bool True to allow loading the component, false to halt loading */ public function authorizeAdmin(string $component): bool; /** * This method will try retrieving a variable from the request (input) data. * If it doesn't exist it will be loaded from the user state, typically * stored in the session. If it doesn't exist there either, the $default * value will be used. If $setUserState is set to true, the retrieved * variable will be stored in the user session. * * @param string $key The user state key for the variable * @param string $request The request variable name for the variable * @param Input $input The Input object with the request (input) data * @param mixed $default The default value. Default: null * @param string $type The filter type for the variable data. Default: none (no filtering) * @param bool $setUserState Should I set the user state with the fetched value? * * @return mixed The value of the variable */ public function getUserStateFromRequest(string $key, string $request, Input $input, $default = null, string $type = 'none', bool $setUserState = true); /** * Load plugins of a specific type. Obviously this seems to only be required * in the Joomla! CMS itself. * * @param string $type The type of the plugins to be loaded * * @return void */ public function importPlugin(string $type): void; /** * Execute plugins (system-level triggers) and fetch back an array with * their return values. * * @param string $event The event (trigger) name, e.g. onBeforeScratchMyEar * @param array $data A hash array of data sent to the plugins as part of the trigger * * @return array A simple array containing the results of the plugins triggered */ public function runPlugins(string $event, array $data = []): array; /** * Perform an ACL check. Please note that FOF uses by default the Joomla! * CMS convention for ACL privileges, e.g core.edit for the edit privilege. * If your platform uses different conventions you'll have to override the * FOF defaults using fof.xml or by specialising the controller. * * @param string $action The ACL privilege to check, e.g. core.edit * @param string|null $assetname The asset name to check, typically the component's name * * @return bool True if the user is allowed this action */ public function authorise(string $action, ?string $assetname = null): bool; /** * Returns a user object. * * @param integer $id The user ID to load. Skip or use null to retrieve * the object for the currently logged in user. * * @return User The User object for the specified user */ public function getUser(?int $id = null): User; /** * Returns the Document object which handles this component's response. You * may also return null and FOF will a. try to figure out the output type by * examining the "format" input parameter (or fall back to "html") and b. * FOF will not attempt to load CSS and Javascript files (as it doesn't make * sense if there's no Document to handle them). * * @return Document|null */ public function getDocument(): ?Document; /** * Returns an object to handle dates * * @param mixed $time The initial time * @param DateTimeZone|string|null $tzOffset The timezone offset * @param bool $locale Should I try to load a specific class for current language? * * @return Date object */ public function getDate(?string $time = 'now', $tzOffset = null, $locale = true): Date; /** * Return the Language instance of the CMS/application * * @return Language */ public function getLanguage(): Language; /** * Returns the database driver object of the CMS/application * * @return JDatabaseDriver */ public function getDbo(): JDatabaseDriver; /** * Is this the administrative section of the component? * * @return bool */ public function isBackend(): bool; /** * Is this the public section of the component? * * @param bool $strict True to only confirm if we're under the 'site' client. False to confirm if we're under * either 'site' or 'api' client (both are front-end access). The default is false which * causes the method to return true when the application is either 'client' (HTML frontend) * or 'api' (JSON frontend). * * @return bool */ public function isFrontend(bool $strict = false): bool; /** * Is this a component running in a CLI application? * * @return bool */ public function isCli(): bool; /** * Is this a component running in an API application? * * @return bool */ public function isApi(): bool; /** * Saves something to the cache. This is supposed to be used for system-wide * FOF data, not application data. * * @param string $key The key of the data to save * @param string $content The actual data to save * * @return bool True on success */ public function setCache(string $key, string $content): bool; /** * Retrieves data from the cache. This is supposed to be used for system-side * FOF data, not application data. * * @param string $key The key of the data to retrieve * @param string|null $default The default value to return if the key is not found or the cache is not populated * * @return string|null The cached value */ public function getCache(string $key, ?string $default = null): ?string; /** * Clears the cache of system-wide FOF data. You are supposed to call this in * your components' installation script post-installation and post-upgrade * methods or whenever you are modifying the structure of database tables * accessed by FOF. Please note that FOF's cache never expires and is not * purged by Joomla!. You MUST use this method to manually purge the cache. * * @return bool True on success */ public function clearCache(): bool; /** * Returns an object that holds the configuration of the current site. * * @return Registry */ public function getConfig(): Registry; /** * Is the global FOF cache enabled? * * @return bool */ public function isGlobalFOFCacheEnabled(): bool; /** * logs in a user * * @param array $authInfo Authentication information * * @return bool True on success */ public function loginUser(array $authInfo): bool; /** * logs out a user * * @return bool True on success */ public function logoutUser(): bool; /** * Add a log file for FOF * * @param string $file * * @return void */ public function logAddLogger($file): void; /** * Logs a deprecated practice. In Joomla! this results in the $message being output in the * deprecated log file, found in your site's log directory. * * @param string $message The deprecated practice log message * * @return void */ public function logDeprecated(string $message): void; /** * Adds a message to the application's debug log * * @param string $message * * @return void */ public function logDebug(string $message): void; /** * Adds a message * * @param string|array $title A title, or an array of additional fields to add to the log entry * @param string $logText The translation key to the log text * @param string $extension The name of the extension logging this entry * @param User|null $user The user the action is being logged for * * @return void */ public function logUserAction($title, string $logText, string $extension, User $user = null): void; /** * Returns the root URI for the request. * * @param bool $pathonly If false, prepend the scheme, host and port information. Default is false. * @param string|null $path The path * * @return string The root URI string. */ public function URIroot(bool $pathonly = false, ?string $path = null): string; /** * Returns the base URI for the request. * * @param bool $pathonly If false, prepend the scheme, host and port information. Default is false. * * @return string The base URI string */ public function URIbase(bool $pathonly = false): string; /** * Method to set a response header. If the replace flag is set then all headers * with the given name will be replaced by the new one (only if the current platform supports header caching) * * @param string $name The name of the header to set. * @param string $value The value of the header to set. * @param bool $replace True to replace any headers with the same name. * * @return void */ public function setHeader(string $name, string $value, bool $replace = false): void; /** * In platforms that perform header caching, send all headers. * * @return void */ public function sendHeaders(): void; /** * Immediately terminate the containing application's execution * * @param int $code The result code which should be returned by the application * * @return void */ public function closeApplication(int $code = 0): void; /** * Perform a redirection to a different page, optionally enqueuing a message for the user. * * @param string $url The URL to redirect to * @param int $status (optional) The HTTP redirection status code, default 301 * @param string $msg (optional) A message to enqueue * @param string $type (optional) The message type, e.g. 'message' (default), 'warning' or 'error'. * * @return void */ public function redirect(string $url, int $status = 301, ?string $msg = null, string $type = 'message'): void; /** * Handle an exception in a way that results to an error page. * * @param Exception $exception The exception to handle * * @throws Exception Possibly rethrown exception */ public function showErrorPage(Exception $exception): void; /** * Set a variable in the user session * * @param string $name The name of the variable to set * @param mixed $value (optional) The value to set it to, default is null * @param string $namespace (optional) The variable's namespace e.g. the component name. Default: 'default' * * @return void */ public function setSessionVar(string $name, $value = null, string $namespace = 'default'): void; /** * Get a variable from the user session * * @param string $name The name of the variable to set * @param mixed $default (optional) The default value to return if the variable does not exit, default: null * @param string $namespace (optional) The variable's namespace e.g. the component name. Default: 'default' * * @return mixed */ public function getSessionVar(string $name, $default = null, $namespace = 'default'); /** * Unset a variable from the user session * * @param string $name The name of the variable to unset * @param string $namespace (optional) The variable's namespace e.g. the component name. Default: 'default' * * @return void */ public function unsetSessionVar(string $name, string $namespace = 'default'): void; /** * Return the session token. Two types of tokens can be returned: * * Session token ($formToken == false): Used for anti-spam protection of forms. This is specific to a session * object. * * Form token ($formToken == true): A secure hash of the user ID with the session token. Both the session and the * user are fetched from the application container. * * @param bool $formToken Should I return a form token? * @param bool $forceNew Should I force the creation of a new token? * * @return mixed */ public function getToken(bool $formToken = false, bool $forceNew = false): string; /** * Are plugins allowed to run in CLI mode? * * @return bool */ public function isAllowPluginsInCli(): bool; /** * Set whether plugins are allowed to run in CLI mode * * @param bool $allowPluginsInCli */ public function setAllowPluginsInCli(bool $allowPluginsInCli): void; /** * Set a script option. * * This allows the backend code to set up configuration options for frontend (JavaScript) code in a way that's safe * for async / deferred scripts. The options are stored in the document's head as an inline JSON document. This * JSON document is then parsed by a JavaScript helper function which makes the options available to the scripts * that consume them. * * @param string $key The option key * @param mixed|JsonSerializable $value The option value. Must be a scalar or a JSON serializable object * @param bool $merge Should I merge an array value with existing stored values? Default: * true * * @return void */ public function addScriptOptions($key, $value, $merge = true); /** * Get a script option, or all of the script options * * @param string|null $key The script option to retrieve. Null for all options. * * @return array|mixed Options for given $key, or all script options */ public function getScriptOptions($key = null); } Platform/FilesystemInterface.php 0000604 00000012374 15245560676 0013031 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Platform; defined('_JEXEC') || die; use FOF40\Container\Container; interface FilesystemInterface { /** * Public constructor. * * @param \FOF40\Container\Container $c The component container */ public function __construct(Container $c); /** * Does the file exists? * * @param $path string Path to the file to test * * @return bool */ public function fileExists(string $path): bool; /** * Delete a file or array of files * * @param string|array $file The file name or an array of file names * * @return bool True on success * */ public function fileDelete($file): bool; /** * Copies a file * * @param string $src The path to the source file * @param string $dest The path to the destination file * @param string $path An optional base path to prefix to the file names * @param bool $use_streams True to use streams * * @return bool True on success */ public function fileCopy(string $src, string $dest, ?string $path = null, bool $use_streams = false): bool; /** * Write contents to a file * * @param string $file The full file path * @param string &$buffer The buffer to write * @param bool $use_streams Use streams * * @return bool True on success */ public function fileWrite(string $file, string &$buffer, bool $use_streams = false): bool; /** * Checks for snooping outside of the file system root. * * @param string $path A file system path to check. * * @return string A cleaned version of the path or exit on error. * * @throws \Exception */ public function pathCheck(string $path): string; /** * Function to strip additional / or \ in a path name. * * @param string $path The path to clean. * @param string $ds Directory separator (optional). * * @return string The cleaned path. * * @throws \UnexpectedValueException */ public function pathClean(string $path, string $ds = DIRECTORY_SEPARATOR): string; /** * Searches the directory paths for a given file. * * @param string|array $paths An path string or array of path strings to search in * @param string $file The file name to look for. * * @return string|null The full path and file name for the target file; null if the file is not found in any of the paths. */ public function pathFind($paths, string $file): ?string; /** * Wrapper for the standard file_exists function * * @param string $path Folder name relative to installation dir * * @return bool True if path is a folder */ public function folderExists(string $path): bool; /** * Utility function to read the files in a folder. * * @param string $path The path of the folder to read. * @param string $filter A filter for file names. * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth. * @param bool $full True to return the full path to the file. * @param array $exclude Array with names of files which should not be shown in the result. * @param array $excludefilter Array of filter to exclude * * @return array Files in the given folder. */ public function folderFiles(string $path, string $filter = '.', bool $recurse = false, bool $full = false, array $exclude = [ '.svn', 'CVS', '.DS_Store', '__MACOSX', ], array $excludefilter = ['^\..*', '.*~'], bool $naturalSort = false): array; /** * Utility function to read the folders in a folder. * * @param string $path The path of the folder to read. * @param string $filter A filter for folder names. * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth. * @param bool $full True to return the full path to the folders. * @param array $exclude Array with names of folders which should not be shown in the result. * @param array $excludefilter Array with regular expressions matching folders which should not be shown in the result. * * @return array Folders in the given folder. */ public function folderFolders(string $path, string $filter = '.', bool $recurse = false, bool $full = false, array $exclude = [ '.svn', 'CVS', '.DS_Store', '__MACOSX', ], array $excludefilter = ['^\..*']): array; /** * Create a folder -- and all necessary parent folders. * * @param string $path A path to create from the base path. * @param integer $mode Directory permissions to set for folders created. 0755 by default. * * @return bool True if successful. */ public function folderCreate(string $path = '', int $mode = 0755): bool; /** * Gets the extension of a file name * * @param string $file The file name * * @return string The file extension */ public function getExt(string $file): string; /** * Strips the last extension off of a file name * * @param string $file The file name * * @return string The file name without the extension */ public function stripExt(string $file): string; } Platform/Base/Platform.php 0000604 00000027411 15245560676 0011520 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Platform\Base; defined('_JEXEC') || die; use Exception; use FOF40\Container\Container; use FOF40\Input\Input; use FOF40\Platform\PlatformInterface; use Joomla\CMS\Document\Document; use Joomla\CMS\User\User; /** * Abstract implementation of the Platform integration * * @package FOF40\Platform\Base */ abstract class Platform implements PlatformInterface { /** @var Container The component container */ protected $container; /** @var bool Are plugins allowed to run in CLI mode? */ protected $allowPluginsInCli = false; /** * Public constructor. * * @param Container $c The component container */ public function __construct(Container $c) { $this->container = $c; } /** * Returns the base (root) directories for a given component, i.e the application * which is running inside our main application (CMS, web app). * * The return is a table with the following keys: * * main The normal location of component files. For a back-end Joomla! * component this is the administrator/components/com_example * directory. * * alt The alternate location of component files. For a back-end * Joomla! component this is the front-end directory, e.g. * components/com_example * * site The location of the component files serving the public part of * the application. * * admin The location of the component files serving the administrative * part of the application. * * All paths MUST be absolute. All four paths MAY be the same if the * platform doesn't make a distinction between public and private parts, * or when the component does not provide both a public and private part. * All of the directories MUST be defined and non-empty. * * @param string $component The name of the component. For Joomla! this * is something like "com_example" * * @return array A hash array with keys main, alt, site and admin. */ public function getComponentBaseDirs(string $component): array { return [ 'main' => '', 'alt' => '', 'site' => '', 'admin' => '', ]; } /** * Returns the application's template name * * @param null|array $params An optional associative array of configuration settings * * @return string The template name. System is the fallback. */ public function getTemplate(?array $params = null): string { return 'system'; } /** * Get application-specific suffixes to use with template paths. This allows * you to look for view template overrides based on the application version. * * @return array A plain array of suffixes to try in template names */ public function getTemplateSuffixes(): array { return []; } /** * Return the absolute path to the application's template overrides * directory for a specific component. We will use it to look for template * files instead of the regular component directories. If the application * does not have such a thing as template overrides return an empty string. * * @param string $component The name of the component for which to fetch the overrides * @param bool $absolute Should I return an absolute or relative path? * * @return string The path to the template overrides directory */ public function getTemplateOverridePath(string $component, bool $absolute = true): string { return ''; } /** * Load the translation files for a given component. * * @param string $component The name of the component. For Joomla! this * is something like "com_example" * * @return void */ public function loadTranslations(string $component): void { } /** * By default FOF will only use the Controller's onBefore* methods to * perform user authorisation. In some cases, like the Joomla! back-end, * you also need to perform component-wide user authorisation in the * Dispatcher. This method MUST implement this authorisation check. If you * do not need this in your platform, please always return true. * * @param string $component The name of the component. * * @return bool True to allow loading the component, false to halt loading */ public function authorizeAdmin(string $component): bool { return true; } /** * Returns a user object. * * @param integer $id The user ID to load. Skip or use null to retrieve * the object for the currently logged in user. * * @return User The User object for the specified user */ public function getUser(?int $id = null): User { return new User(); } /** * Returns the Document object which handles this component's response. You * may also return null and FOF will a. try to figure out the output type by * examining the "format" input parameter (or fall back to "html") and b. * FOF will not attempt to load CSS and Javascript files (as it doesn't make * sense if there's no Document to handle them). * * @return Document|null */ public function getDocument(): ?Document { return null; } /** * This method will try retrieving a variable from the request (input) data. * If it doesn't exist it will be loaded from the user state, typically * stored in the session. If it doesn't exist there either, the $default * value will be used. If $setUserState is set to true, the retrieved * variable will be stored in the user session. * * @param string $key The user state key for the variable * @param string $request The request variable name for the variable * @param Input $input The Input object with the request (input) data * @param mixed $default The default value. Default: null * @param string $type The filter type for the variable data. Default: none (no filtering) * @param bool $setUserState Should I set the user state with the fetched value? * * @return mixed The value of the variable */ public function getUserStateFromRequest(string $key, string $request, Input $input, $default = null, string $type = 'none', bool $setUserState = true) { return $input->get($request, $default, $type); } /** * Load plugins of a specific type. Obviously this seems to only be required * in the Joomla! CMS itself. * * @param string $type The type of the plugins to be loaded * * @return void */ public function importPlugin(string $type): void { } /** * Execute plugins (system-level triggers) and fetch back an array with * their return values. * * @param string $event The event (trigger) name, e.g. onBeforeScratchMyEar * @param array $data A hash array of data sent to the plugins as part of the trigger * * @return array A simple array containing the results of the plugins triggered */ public function runPlugins(string $event, array $data = []): array { return []; } /** * Perform an ACL check. Please note that FOF uses by default the Joomla! * CMS convention for ACL privileges, e.g core.edit for the edit privilege. * If your platform uses different conventions you'll have to override the * FOF defaults using fof.xml or by specialising the controller. * * @param string $action The ACL privilege to check, e.g. core.edit * @param string|null $assetname The asset name to check, typically the component's name * * @return bool True if the user is allowed this action */ public function authorise(string $action, ?string $assetname = null): bool { return true; } /** * Is this the administrative section of the component? * * @return boolean */ public function isBackend(): bool { return true; } /** * Is this the public section of the component? * * @param bool $strict True to only confirm if we're under the 'site' client. False to confirm if we're under * either 'site' or 'api' client (both are front-end access). The default is false which * causes the method to return true when the application is either 'client' (HTML frontend) * or 'api' (JSON frontend). * * @return bool */ public function isFrontend(bool $strict = false): bool { return true; } /** * Is this a component running in a CLI application? * * @return bool */ public function isCli(): bool { return true; } /** * Is this a component running under the API application? * * @return bool */ public function isApi(): bool { return true; } /** * Saves something to the cache. This is supposed to be used for system-wide * FOF data, not application data. * * @param string $key The key of the data to save * @param string $content The actual data to save * * @return bool True on success */ public function setCache(string $key, string $content): bool { return false; } /** * Retrieves data from the cache. This is supposed to be used for system-side * FOF data, not application data. * * @param string $key The key of the data to retrieve * @param string|null $default The default value to return if the key is not found or the cache is not populated * * @return string|null The cached value */ public function getCache(string $key, ?string $default = null): ?string { return false; } /** * Is the global FOF cache enabled? * * @return bool */ public function isGlobalFOFCacheEnabled(): bool { return true; } /** * Clears the cache of system-wide FOF data. You are supposed to call this in * your components' installation script post-installation and post-upgrade * methods or whenever you are modifying the structure of database tables * accessed by FOF. Please note that FOF's cache never expires and is not * purged by Joomla!. You MUST use this method to manually purge the cache. * * @return bool True on success */ public function clearCache(): bool { return false; } /** * logs in a user * * @param array $authInfo Authentication information * * @return bool True on success */ public function loginUser(array $authInfo): bool { return true; } /** * logs out a user * * @return bool True on success */ public function logoutUser(): bool { return true; } /** * Logs a deprecated practice. In Joomla! this results in the $message being output in the * deprecated log file, found in your site's log directory. * * @param string $message The deprecated practice log message * * @return void */ public function logDeprecated(string $message): void { // The default implementation does nothing. Override this in your platform classes. } /** @inheritDoc */ public function logUserAction($title, string $logText, string $extension, User $user = null): void { // The default implementation does nothing. Override this in your platform classes. } /** * Returns the version number string of the CMS/application we're running in * * @return string * * @since 2.1.2 */ public function getPlatformVersion(): string { return ''; } /** * Handle an exception in a way that results to an error page. * * @param Exception $exception The exception to handle * * @throws Exception Possibly rethrown exception */ public function showErrorPage(Exception $exception): void { throw $exception; } /** * Are plugins allowed to run in CLI mode? * * @return bool */ public function isAllowPluginsInCli(): bool { return $this->allowPluginsInCli; } /** * Set whether plugins are allowed to run in CLI mode * * @param bool $allowPluginsInCli */ public function setAllowPluginsInCli(bool $allowPluginsInCli): void { $this->allowPluginsInCli = $allowPluginsInCli; } } Platform/Base/Filesystem.php 0000604 00000004511 15245560676 0012054 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace FOF40\Platform\Base; defined('_JEXEC') || die; use FOF40\Container\Container; use FOF40\Platform\FilesystemInterface; abstract class Filesystem implements FilesystemInterface { /** * The list of paths where platform class files will be looked for * * @var array */ protected static $paths = []; /** @var Container The component container */ protected $container; /** * Public constructor. * * @param \FOF40\Container\Container $c The component container */ public function __construct(Container $c) { $this->container = $c; } /** * 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; } /** * Gets the extension of a file name * * @param string $file The file name * * @return string The file extension */ public function getExt(string $file): string { $dot = strrpos($file, '.') + 1; return substr($file, $dot); } /** * Strips the last extension off of a file name * * @param string $file The file name * * @return string The file name without the extension */ public function stripExt(string $file): string { return preg_replace('#\.[^.]*$#', '', $file); } } JoomlaAbstraction/CacheCleaner.php 0000604 00000023762 15245560676 0013213 0 ustar 00 <?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; } } JoomlaAbstraction/DynamicGroups.php 0000604 00000013343 15245560676 0013474 0 ustar 00 <?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); } } JoomlaAbstraction/ComponentVersion.php 0000604 00000006654 15245560676 0014227 0 ustar 00 <?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]); } } Download/Adapter/Fopen.php 0000604 00000010303 15245560676 0011464 0 ustar 00 <?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; } } } Download/Adapter/AbstractAdapter.php 0000604 00000006611 15245560676 0013470 0 ustar 00 <?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; } } Download/Adapter/Curl.php 0000604 00000016207 15245560676 0011333 0 ustar 00 <?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; } } Download/Exception/DownloadError.php 0000604 00000000464 15245560676 0013563 0 ustar 00 <?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 { } Download/DownloadInterface.php 0000604 00000005157 15245560676 0012440 0 ustar 00 <?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; } Download/Download.php 0000604 00000026301 15245560676 0010611 0 ustar 00 <?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; } } } Model/DataModel.php 0000604 00000331376 15245560676 0010200 0 ustar 00 <?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; defined('_JEXEC') || die; use FOF40\Container\Container; use FOF40\Controller\Exception\LockedRecord; use FOF40\Date\Date; use FOF40\Event\Dispatcher; use FOF40\Event\Observer; use FOF40\Model\DataModel\Collection as DataCollection; use FOF40\Model\DataModel\Exception\BaseException; use FOF40\Model\DataModel\Exception\CannotLockNotLoadedRecord; use FOF40\Model\DataModel\Exception\InvalidSearchMethod; use FOF40\Model\DataModel\Exception\NoAssetKey; use FOF40\Model\DataModel\Exception\NoContentType; use FOF40\Model\DataModel\Exception\NoItemsFound; use FOF40\Model\DataModel\Exception\NoTableColumns; use FOF40\Model\DataModel\Exception\RecordNotLoaded; use FOF40\Model\DataModel\Exception\SpecialColumnMissing; use FOF40\Model\DataModel\Relation\Exception\RelationNotFound; use FOF40\Model\DataModel\RelationManager; use FOF40\Utils\ArrayHelper; use Joomla\CMS\Access\Rules; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Table\Asset; use Joomla\CMS\Table\ContentHistory; use Joomla\CMS\Table\ContentType; use Joomla\CMS\Table\CoreContent; use Joomla\CMS\Table\TableInterface; use Joomla\CMS\UCM\UCMContent; /** * Data-aware model, implementing a convenient ORM * * Type hinting -- start * * * @method $this hasOne() hasOne(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null) * @method $this belongsTo() belongsTo(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null) * @method $this hasMany() hasMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null) * @method $this belongsToMany() belongsToMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null, string $pivotTable = null, string $pivotLocalKey = null, string $pivotForeignKey = null) * * @method $this filter_order() filter_order(string $orderingField) * @method $this filter_order_Dir() filter_order_Dir(string $direction) * @method $this limit() limit(int $limit) * @method $this limitstart() limitstart(int $limitStart) * @method $this enabled() enabled(int $enabled) * @method DataModel getNew() getNew(string $relationName) * * @property int $enabled Publish status of this record * @property int $ordering Sort ordering of this record * @property int $created_by ID of the user who created this record * @property string $created_on Date/time stamp of record creation * @property int $modified_by ID of the user who modified this record * @property string $modified_on Date/time stamp of record modification * @property int $locked_by ID of the user who locked this record * @property string $locked_on Date/time stamp of record locking * * Type hinting -- end */ class DataModel extends Model implements TableInterface { /** @var array A list of tables in the database */ protected static $tableCache = []; /** @var array A list of table fields, keyed per table */ protected static $tableFieldCache = []; /** @var array A list of permutations of the prefix with upper/lowercase letters */ protected static $prefixCasePermutations = []; /** @var array Table field name aliases, defined as aliasFieldName => actualFieldName */ protected $aliasFields = []; /** @var boolean Should I run automatic checks on the table data? */ protected $autoChecks = true; /** @var boolean Should I auto-fill the fields of the model object when constructing it? */ protected $autoFill = false; /** @var Dispatcher An event dispatcher for model behaviours */ protected $behavioursDispatcher; /** @var \JDatabaseDriver The database driver for this model */ protected $dbo; /** @var array Which fields should be exempt from automatic checks when autoChecks is enabled */ protected $fieldsSkipChecks = []; /** @var array Which fields should be auto-filled from the model state (by extent, the request)? */ protected $fillable = []; /** @var array Which fields should never be auto-filled from the model state (by extent, the request)? */ protected $guarded = []; /** @var string The identity field's name */ protected $idFieldName = ''; /** @var array A hash array with the table fields we know about and their information. Each key is the field name, the value is the field information */ protected $knownFields = []; /** @var array The data of the current record */ protected $recordData = []; /** @var boolean What will delete() do? True: trash (enabled set to -2); false: hard delete (remove from database) */ protected $softDelete = false; /** @var string The name of the database table we connect to */ protected $tableName = ''; /** @var array A collection of custom, additional where clauses to apply during buildQuery */ protected $whereClauses = []; /** @var RelationManager The relation manager of this model */ protected $relationManager; /** @var array A list of all eager loaded relations and their attached callbacks */ protected $eagerRelations = []; /** @var array A list of the relation filter definitions for this model */ protected $relationFilters = []; /** @var array A list of the relations which will be auto-touched by save() and touch() methods */ protected $touches = []; /** @var bool Should rows be tracked as ACL assets? */ protected $trackAssets = false; /** @var bool Does the resource support joomla tags? */ protected $has_tags = false; /** @var Rules The rules associated with this record. */ protected $rules; /** @var string The UCM content type (typically: com_something.viewname, e.g. com_foobar.items) */ protected $contentType; /** @var array Shared parameters for behaviors */ protected $behaviorParams = []; /** * The asset key for items in this table. It's usually something in the * com_example.viewname format. They asset name will be this key appended * with the item's ID, e.g. com_example.viewname.123 * * @var string */ protected $assetKey = ''; /** * Public constructor. Overrides the parent constructor, adding support for database-aware models. * * You can use the $config array to pass some configuration values to the object: * * tableName String The name of the database table to use. Default: #__appName_viewNamePlural (Ruby * on Rails convention) idFieldName String The table key field name. Default: * appName_viewNameSingular_id (Ruby on Rails convention) knownFields Array The known fields in the * table. Default: read from the table itself autoChecks Boolean Should I turn on automatic data * validation checks? fieldsSkipChecks Array List of fields which should not participate in automatic data * validation checks. aliasFields Array Associative array of "magic" field aliases. * behavioursDispatcher EventDispatcher The model behaviours event dispatcher. behaviourObservers Array The * model behaviour observers to attach to the behavioursDispatcher. behaviours Array A list of * behaviour names to instantiate and attach to the behavioursDispatcher. fillable_fields Array Which * fields should be auto-filled from the model state (by extent, the request)? guarded_fields Array Which * fields should never be auto-filled from the model state (by extent, the request)? relations Array * (hashed) The relations to autoload on model creation. contentType String The UCM content type, e.g. * "com_foobar.items" * * Setting either fillable_fields or guarded_fields turns on automatic filling of fields in the constructor. If * both * are set only guarded_fields is taken into account. Fields are not filled automatically outside the constructor. * * @param Container $container The configuration variables to this model * @param array $config Configuration values for this model * * @throws \FOF40\Model\DataModel\Exception\NoTableColumns * @see Model::__construct() * */ public function __construct(Container $container, array $config = []) { // First call the parent constructor. parent::__construct($container, $config); // Should I use a different database object? $this->dbo = $container->db; // Do I have a table name? if (isset($config['tableName'])) { $this->tableName = $config['tableName']; } elseif (empty($this->tableName)) { // The table name is by default: #__appName_viewNamePlural (Ruby on Rails convention) $viewPlural = $container->inflector->pluralize($this->getName()); $this->tableName = '#__' . strtolower($this->container->bareComponentName) . '_' . strtolower($viewPlural); } // Do I have a table key name? if (isset($config['idFieldName'])) { $this->idFieldName = $config['idFieldName']; } elseif (empty($this->idFieldName)) { // The default ID field is: appName_viewNameSingular_id (Ruby on Rails convention) $viewSingular = $container->inflector->singularize($this->getName()); $this->idFieldName = strtolower($this->container->bareComponentName) . '_' . strtolower($viewSingular) . '_id'; } // Do I have a list of known fields? if (isset($config['knownFields']) && !empty($config['knownFields'])) { if (!is_array($config['knownFields'])) { $config['knownFields'] = explode(',', $config['knownFields']); } $this->knownFields = $config['knownFields']; } else { // By default the known fields are fetched from the table itself (slow!) $this->knownFields = $this->getTableFields(); } if (empty($this->knownFields)) { throw new NoTableColumns(sprintf('Model %s could not fetch column list for the table %s', $this->getName(), $this->tableName)); } // Should I turn on autoChecks? if (isset($config['autoChecks'])) { if (!is_bool($config['autoChecks'])) { $config['autoChecks'] = strtolower($config['autoChecks']); $config['autoChecks'] = in_array($config['autoChecks'], ['yes', 'true', 'on', 1]); } $this->autoChecks = $config['autoChecks']; } // Should I exempt fields from autoChecks? if (isset($config['fieldsSkipChecks'])) { if (!is_array($config['fieldsSkipChecks'])) { $config['fieldsSkipChecks'] = explode(',', $config['fieldsSkipChecks']); $config['fieldsSkipChecks'] = array_map(function ($x) { return trim($x); }, $config['fieldsSkipChecks']); } $this->fieldsSkipChecks = $config['fieldsSkipChecks']; } // Do I have alias fields? if (isset($config['aliasFields'])) { $this->aliasFields = $config['aliasFields']; } // Do I have a behaviours dispatcher? if (isset($config['behavioursDispatcher']) && ($config['behavioursDispatcher'] instanceof Dispatcher)) { $this->behavioursDispatcher = $config['behavioursDispatcher']; } // Otherwise create the model behaviours dispatcher else { $this->behavioursDispatcher = new Dispatcher($this->container); } // Do I have an array of behaviour observers if (isset($config['behaviourObservers']) && is_array($config['behaviourObservers'])) { foreach ($config['behaviourObservers'] as $observer) { $this->behavioursDispatcher->attach($observer); } } // Do I have a list of behaviours? if (isset($config['behaviours']) && is_array($config['behaviours'])) { foreach ($config['behaviours'] as $behaviour) { $this->addBehaviour($behaviour); } } // Add extra behaviours foreach (['Created', 'Modified'] as $behaviour) { $this->addBehaviour($behaviour); } // Do I have a list of fillable fields? if (isset($config['fillable_fields']) && !empty($config['fillable_fields'])) { if (!is_array($config['fillable_fields'])) { $config['fillable_fields'] = explode(',', $config['fillable_fields']); $config['fillable_fields'] = array_map(function ($x) { return trim($x); }, $config['fillable_fields']); } $this->fillable = []; $this->autoFill = true; foreach ($config['fillable_fields'] as $field) { if (array_key_exists($field, $this->knownFields)) { $this->fillable[] = $field; } elseif (isset($this->aliasFields[$field])) { $this->fillable[] = $this->aliasFields[$field]; } } } // Do I have a list of guarded fields? if (isset($config['guarded_fields']) && !empty($config['guarded_fields'])) { if (!is_array($config['guarded_fields'])) { $config['guarded_fields'] = explode(',', $config['guarded_fields']); $config['guarded_fields'] = array_map(function ($x) { return trim($x); }, $config['guarded_fields']); } $this->guarded = []; $this->autoFill = true; foreach ($config['guarded_fields'] as $field) { if (array_key_exists($field, $this->knownFields)) { $this->guarded[] = $field; } elseif (isset($this->aliasFields[$field])) { $this->guarded[] = $this->aliasFields[$field]; } } } // If we are tracking assets, make sure an access field exists and initially set the default. $asset_id_field = $this->getFieldAlias('asset_id'); $access_field = $this->getFieldAlias('access'); if (array_key_exists($asset_id_field, $this->knownFields)) { $this->trackAssets = true; } /** * if ($this->trackAssets && array_key_exists($access_field, $this->knownFields) && !($this->getState($access_field, null))) * { * $this->$access_field = (int) $this->container->platform->getConfig()->get('access'); * } **/ $assetKey = $this->container->componentName . '.' . strtolower($container->inflector->singularize($this->getName())); $this->setAssetKey($assetKey); // Set the UCM content type if applicable if (isset($config['contentType'])) { $this->contentType = $config['contentType']; } // Do I have to auto-fill the fields? if ($this->autoFill) { $fields = !empty($this->guarded) ? array_keys($this->knownFields) : $this->fillable; foreach ($fields as $field) { if (in_array($field, $this->guarded)) { // Do not set guarded fields continue; } $stateValue = $this->getState($field); if (!is_null($stateValue)) { $this->setFieldValue($field, $stateValue); } } } // Create a relation manager $this->relationManager = new RelationManager($this); // Do I have a list of relations? if (isset($config['relations']) && is_array($config['relations'])) { foreach ($config['relations'] as $relConfig) { if (!is_array($relConfig)) { continue; } $defaultRelConfig = [ 'type' => 'hasOne', 'foreignModelClass' => null, 'localKey' => null, 'foreignKey' => null, 'pivotTable' => null, 'pivotLocalKey' => null, 'pivotForeignKey' => null, ]; $relConfig = array_merge($defaultRelConfig, $relConfig); $this->relationManager->addRelation($relConfig['itemName'], $relConfig['type'], $relConfig['foreignModelClass'], $relConfig['localKey'], $relConfig['foreignKey'], $relConfig['pivotTable'], $relConfig['pivotLocalKey'], $relConfig['pivotForeignKey']); } } // Initialise the data model foreach ($this->knownFields as $fieldName => $information) { // Initialize only the null or not yet set records if (!isset($this->recordData[$fieldName])) { $this->recordData[$fieldName] = $information->Default; } } // Trigger the onAfterConstruct event. This allows you to set up model state etc. $this->triggerEvent('onAfterConstruct'); } /** * Magic caller. It works like the magic setter and returns ourselves for chaining. If no arguments are passed we'll * only look for a scope filter. * * @param string $name * @param mixed $arguments * * @return static */ public function __call($name, $arguments) { // If no arguments are provided try mapping to the scopeSomething() method if (empty($arguments)) { $methodName = 'scope' . ucfirst($name); if (method_exists($this, $methodName)) { $this->{$methodName}(); return $this; } } // Implements getNew($relationName) if (($name == 'getNew') && (is_array($arguments) || $arguments instanceof \Countable ? count($arguments) : 0)) { return $this->relationManager->getNew($arguments[0]); } // Magically map relations to methods, e.g. $this->foobar will return the "foobar" relations' contents if ($this->relationManager->isMagicMethod($name)) { return call_user_func_array([$this->relationManager, $name], $arguments); } // Otherwise call the parent return parent::__call($name, $arguments); } /** * Magic checker on a property. It follows the same logic of the __get magic method, however, if nothing is found, * it won't return the state of a variable (we are checking if a property is set) * * @param string $name The name of the field to check * * @return bool Is the field set? */ public function __isset($name) { $value = null; $isState = false; if (substr($name, 0, 3) == 'flt') { $isState = true; $name = strtolower(substr($name, 3, 1)) . substr($name, 4); } // If $name is a field name, get its value if (!$isState && array_key_exists($name, $this->recordData)) { $value = $this->getFieldValue($name); } elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData)) { $name = $this->aliasFields[$name]; $value = $this->getFieldValue($name); } elseif ($this->relationManager->isMagicProperty($name)) { $value = $this->relationManager->$name; } // As the core function isset, the property must exists AND must be NOT null return ($value !== null); } /** * Magic getter. It will return the value of a field or, if no such field is found, the value of the relevant state * variable. * * Tip: Trying to get fltSomething will always return the value of the state variable "something" * * Tip: You can define custom field getter methods as getFieldNameAttribute, where FieldName is your field's name, * in CamelCase (even if the field name itself is in snake_case). * * @param string $name The name of the field / state variable to retrieve * * @return static|mixed */ public function __get($name) { // Handle $this->input if ($name == 'input') { return $this->container->input; } $isState = false; if (substr($name, 0, 3) == 'flt') { $isState = true; $name = strtolower(substr($name, 3, 1)) . substr($name, 4); } // If $name is a field name, get its value if (!$isState && array_key_exists($name, $this->recordData)) { return $this->getFieldValue($name); } elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData)) { $name = $this->aliasFields[$name]; return $this->getFieldValue($name); } elseif ($this->relationManager->isMagicProperty($name)) { return $this->relationManager->$name; } // If $name is not a field name, get the value of a state variable else { return $this->getState($name); } } /** * Magic setter. It will set the value of a field or the value of a dynamic scope filter, or the value of the * relevant state variable. * * Tip: Trying to set fltSomething will always return the value of the state variable "something" * * Tip: Trying to set scopeSomething will always return the value of the dynamic scope filter "something" * * Tip: You can define custom field setter methods as setFieldNameAttribute, where FieldName is your field's name, * in CamelCase (even if the field name itself is in snake_case). * * @param string $name The name of the field / scope / state variable to set * @param mixed $value The value to set * * @return void */ public function __set($name, $value) { $isState = false; $isScope = false; if (substr($name, 0, 3) == 'flt') { $isState = true; $name = strtolower(substr($name, 3, 1)) . substr($name, 4); } elseif (substr($name, 0, 5) == 'scope') { $isScope = true; $name = strtolower(substr($name, 5, 1)) . substr($name, 5); } // If $name is a field name, set its value if (!$isState && !$isScope && array_key_exists($name, $this->recordData)) { $this->setFieldValue($name, $value); } elseif (!$isState && !$isScope && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData)) { $name = $this->aliasFields[$name]; $this->setFieldValue($name, $value); } // If $name is a dynamic scope filter, set its value elseif ($isScope || method_exists($this, 'scope' . ucfirst($name))) { $method = 'scope' . ucfirst($name); $this->{$method}($value); } // If $name is not a field name, set the value of a state variable else { $this->setState($name, $value); } } /** * Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the * original object. The new object is set up to not save its stats, ignore the request when getting state variables * and comes with an empty state. The temporary object instance has its data reset as well. * * @return $this */ public function tmpInstance() { return parent::tmpInstance()->reset(true, true); } /** * Adds a known field to the DataModel. This is only necessary if you are using a custom buildQuery with JOINs or * field aliases. Please note that you need to make further modifications for bind() and save() to work in this * case. Please refer to the documentation blocks of these methods for more information. It is generally considered * a very BAD idea using JOINs instead of relations. It complicates your life and is bound to cause bugs that are * very hard to track back. * * Basically, if you find yourself using this method you are probably doing something very wrong or very advanced. * If you do not feel confident with debugging FOF code STOP WHATEVER YOU'RE DOING and rethink your Model. Why are * you using a JOIN? If you want to filter the records by a field found in another table you can still use * relations and whereHas with a callback. * * @param string $fieldName The name of the field * @param mixed $default Default value, used by reset() (default: null) * @param string $type Database type for the field. If unsure use 'integer', 'float' or 'text'. * @param bool $replace Should we replace an existing known field definition? * * @return $this Self, for chaining */ public function addKnownField($fieldName, $default = null, $type = 'integer', $replace = false) { if (array_key_exists($fieldName, $this->knownFields) && !$replace) { return $this; } $info = (object) [ 'Default' => $default, 'Type' => $type, 'Null' => 'YES', ]; $this->knownFields[$fieldName] = $info; // Initialize only the null or not yet set records if (!isset($this->recordData[$fieldName])) { $this->recordData[$fieldName] = $default; } return $this; } /** * Get the columns from database table. For TableInterface compatibility. * * @return mixed An array of the field names, or false if an error occurs. */ public function getFields() { return $this->getTableFields(); } /** * Get the columns from a database table. * * @param string $tableName Table name. If null current table is used * * @return mixed An array of the field names, or false if an error occurs. */ public function getTableFields($tableName = null) { // Make sure we have a list of tables in this db if (empty(static::$tableCache)) { static::$tableCache = $this->getDbo()->getTableList(); } if (!$tableName) { $tableName = $this->tableName; } // Try to load again column specifications if the table is not loaded OR if it's loaded and // the previous call returned an error if (!array_key_exists($tableName, static::$tableFieldCache) || (isset(static::$tableFieldCache[$tableName]) && !static::$tableFieldCache[$tableName]) ) { // Lookup the fields for this table only once. $name = $tableName; $prefix = $this->getDbo()->getPrefix(); $checkName = substr($name, 0, 3) == '#__' ? $prefix . substr($name, 3) : $name; // Iterate through all lower/uppercase permutations of the prefix if we have a prefix with at least one uppercase letter if (!in_array($checkName, static::$tableCache) && preg_match('/[A-Z]/', $prefix) && (substr($name, 0, 3) == '#__')) { $prefixPermutations = $this->getPrefixCasePermutations(); $partialCheckName = substr($name, 3); foreach ($prefixPermutations as $permutatedPrefix) { $checkName = $permutatedPrefix . $partialCheckName; if (in_array($checkName, static::$tableCache)) { break; } } } if (!in_array($checkName, static::$tableCache)) { // The table doesn't exist. Return false. static::$tableFieldCache[$tableName] = false; } else { $fields = $this->getDbo()->getTableColumns($name, false); if (empty($fields)) { $fields = false; } static::$tableFieldCache[$tableName] = $fields; } // PostgreSQL date type compatibility if (($this->getDbo()->name == 'postgresql') && (static::$tableFieldCache[$tableName] != false)) { foreach (static::$tableFieldCache[$tableName] as $field) { if (strtolower($field->type) != 'timestamp without time zone') { continue; } if (!stristr($field->Default, '\'::timestamp without time zone')) { continue; } [$date,] = explode('::', $field->Default, 2); $field->Default = trim($date, "'"); } } } return static::$tableFieldCache[$tableName]; } /** * Get the database connection associated with this data Model * * @return \JDatabaseDriver */ public function getDbo() { if (!is_object($this->dbo)) { $this->dbo = $this->container->db; } return $this->dbo; } /** * Returns the data currently bound to the model in an array format. Similar to toArray() but returns a copy instead * of the internal table itself. * * @return array */ public function getData() { $ret = []; foreach (array_keys($this->knownFields) as $field) { $ret[$field] = $this->getFieldValue($field); } return $ret; } /** * Return the value of the identity column of the currently loaded record * * @return mixed */ public function getId() { return $this->{$this->idFieldName}; } /** * Returns the name of the table's id field (primary key) name * * @return string */ public function getIdFieldName() { return $this->idFieldName; } /** * Alias of getIdFieldName. Used for TableInterface compatibility. * * @return string The name of the primary key for the table. * * @codeCoverageIgnore */ public function getKeyName() { return $this->getIdFieldName(); } /** * Returns the database table name this model talks to * * @return string */ public function getTableName() { return $this->tableName; } /** * Returns the value of a field. If a field is not set it uses the $default value. Automatically uses magic * getter variables if required. * * @param string $name The name of the field to retrieve * @param mixed $default Default value, if the field is not set and doesn't have a getter method * * @return mixed The value of the field */ public function getFieldValue($name, $default = null) { if (array_key_exists($name, $this->aliasFields)) { $name = $this->aliasFields[$name]; } if (!array_key_exists($name, $this->knownFields)) { return $default; } if (!isset($this->recordData[$name])) { $this->recordData[$name] = $default; } return $this->recordData[$name]; } /** * Sets the value of a field. * * @param string $name The name of the field to set * @param mixed $value The value to set it to * * @return void */ public function setFieldValue($name, $value = null) { if (array_key_exists($name, $this->aliasFields)) { $name = $this->aliasFields[$name]; } if (array_key_exists($name, $this->knownFields)) { $this->recordData[$name] = $value; } } /** * Applies the getSomethingAttribute methods to $this->recordData, converting the database representation of the * data to the record representation. $this->recordData is directly modified. * * @return void */ public function databaseDataToRecordData() { foreach ($this->recordData as $name => $value) { $method = $this->container->inflector->camelize('get_' . $name . '_attribute'); if (method_exists($this, $method)) { $this->recordData[$name] = $this->{$method}($value); } } } /** * Applies the setSomethingAttribute methods to $this->recordData, converting the record representation to database * representation. It does not modify $this->recordData, it returns a copy of the data array. * * If you are using custom knownFields to cater for table JOINs you need to override this method and _remove_ the * fields which do not belong to the table you are saving to. It's generally a bad idea using JOINs instead of * relations. You have been warned! * * @return array */ public function recordDataToDatabaseData() { $copy = array_merge($this->recordData); foreach ($copy as $name => $value) { $method = $this->container->inflector->camelize('set_' . $name . '_attribute'); if (method_exists($this, $method)) { $copy[$name] = $this->{$method}($value); } } return $copy; } /** * Does this model know about a field called $fieldName? Automatically uses aliases when necessary. * * @param string $fieldName Field name to check * * @return boolean True if the field exists */ public function hasField($fieldName) { $realFieldName = $this->getFieldAlias($fieldName); return array_key_exists($realFieldName, $this->knownFields); } /** * Is this field known to the model and marked as nullable in the database? * * Automatically uses aliases when necessary. * * @param string $fieldName Field name to check * * @return bool True if the field is nullable or doesn't exist */ public function isNullableField(string $fieldName): bool { if (!$this->hasField($fieldName)) { return true; } $realFieldName = $this->getFieldAlias($fieldName); return strtolower($this->knownFields[$realFieldName]->Null ?? 'YES') == 'yes'; } /** * Get the real name of a field name based on its alias. If the field is not aliased $alias is returned * * @param string $alias The field to get an alias for * * @return string The real name of the field */ public function getFieldAlias($alias) { if (array_key_exists($alias, $this->aliasFields)) { return $this->aliasFields[$alias]; } else { return $alias; } } /** * Returns an array mapping relation names to their local key field names. * * For example, given a relation "foobar" with local key name "example_item_id" it will return: * ["foobar" => "example_item_id"] * * @return array Array of [relationName => fieldName] arrays * * @throws \FOF40\Model\DataModel\Relation\Exception\RelationNotFound */ public function getRelationFields() { $fields = []; $relationNames = $this->relationManager->getRelationNames(); if (empty($relationNames)) { return $fields; } foreach ($relationNames as $name) { $fields[$name] = $this->relationManager->getRelation($name)->getLocalKey(); } return $fields; } /** * Returns the qualified foreign model name, in the format "componentName.modelName", for the specified model * field. First it checks the relations you have defined. If none is found it will try to parse the field name as * following the componentName_modelName_id naming convention (FOF best practice and recommendation). * * This feature is used by the Blade compiler. * * @param string $fieldName The field name for which we'll get a foreign model name * * @return string */ public function getForeignModelNameFor($fieldName) { // First look for a local field mapped in a relationship try { $relationMap = $this->getRelationFields(); $relationName = array_search($fieldName, $relationMap); if ($relationName !== false) { $model = $this->relationManager->getRelation($relationName)->getForeignModel(); $component = $model->getContainer()->componentName; $modelName = $model->getName(); return "$component.$modelName"; } } catch (RelationNotFound $e) { // Bummer. The relation cannot be found. I will fall back to parsing the field name. } // Do I have a field following the componentName_modelName_id format? $parts = explode('_', $fieldName); if ((substr($fieldName, -3) != '_id') || (count($parts) < 3)) { throw new \RuntimeException("Cannot determine the foreign model for local field '$fieldName'; it does not follow the expected component_model_id convention."); } $fieldName = substr($fieldName, 0, -3); [$component, $modelName] = explode('_', $fieldName, 2); $modelName = $this->container->inflector->camelize($modelName); return "$component.$modelName"; } /** * Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set * data, unless you provide a $data array. * * Special note if you are using a custom buildQuery with JOINs or field aliases: * You will need to override the recordDataToDatabaseData method. Make sure that you _remove_ or rename any fields * which do not exist in the table defined in $this->tableName. Otherwise Joomla! will not know how to insert / * update the data on the table and will throw an Exception denoting a database error. It is generally a BAD idea * using JOINs instead of relations. If unsure, use relations. * * @param null|array $data [Optional] Data to bind * @param string $orderingFilter A WHERE clause used to apply table item reordering * @param array $ignore A list of fields to ignore when binding $data * * @para boolean $resetRelations Should I automatically reset relations if relation-important fields are * changed? * * @return DataModel Self, for chaining */ public function save($data = null, $orderingFilter = '', $ignore = null, $resetRelations = true) { // Stash the primary key $oldPKValue = $this->getId(); // Call the onBeforeSave event $this->triggerEvent('onBeforeSave', [&$data]); // Get the relation to local field map and initialise the relationsAffected array $relationImportantFields = $this->getRelationFields(); $dataBeforeBind = []; // If we have relations we keep a copy of the data before bind. if (count($relationImportantFields) > 0) { $dataBeforeBind = array_merge($this->recordData); } // Bind any (optional) data. If no data is provided, the current record data is used if (!is_null($data)) { $this->bind($data, $ignore); } $isNewRecord = empty($oldPKValue) ? true : $oldPKValue != $this->getId(); // Check the validity of the data $this->check(); // Get the database object $db = $this->getDbo(); // Insert or update the record. Note that the object we use for insertion / update is the a copy holding // the transformed data. $dataObject = $this->recordDataToDatabaseData(); $dataObject = (object) $dataObject; if ($isNewRecord) { $this->triggerEvent('onBeforeCreate', [&$dataObject]); // Insert the new record $db->insertObject($this->tableName, $dataObject, $this->idFieldName); // Update ourselves with the new ID field's value $this->{$this->idFieldName} = $db->insertid(); // Rebase the relations with the newly created model if ($resetRelations) { $this->relationManager->rebase($this); } $this->triggerEvent('onAfterCreate'); } else { $this->triggerEvent('onBeforeUpdate', [&$dataObject]); $db->updateObject($this->tableName, $dataObject, $this->idFieldName, true); $this->triggerEvent('onAfterUpdate'); } // If an ordering filter is set, attempt reorder the rows in the table based on the filter and value. if ($orderingFilter) { $filterValue = $this->$orderingFilter; $this->reorder($orderingFilter ? $db->qn($orderingFilter) . ' = ' . $db->q($filterValue) : ''); } foreach ($this->touches as $relation) { $records = $this->getRelations()->getData($relation); if (!empty($records)) { if ($records instanceof DataModel) { $records = [$records]; } /** @var DataModel $record */ foreach ($records as $record) { $record->touch(); } } } // If we have relations we compare the data to the copy of the data before bind. if (count($relationImportantFields) && $resetRelations) { // Since array_diff_assoc doesn't work recursively we have to do it the EXCRUCIATINGLY SLOW WAY. Sad panda :( $keysRecord = (is_array($this->recordData) && !empty($this->recordData)) ? array_keys($this->recordData) : []; $keysBefore = (is_array($dataBeforeBind) && !empty($dataBeforeBind)) ? array_keys($dataBeforeBind) : []; $keysAll = array_merge($keysRecord, $keysBefore); $keysAll = array_unique($keysAll); $modifiedFields = []; foreach ($keysAll as $key) { if (!isset($dataBeforeBind[$key]) || !isset($this->recordData[$key])) { $modifiedFields[] = $key; } elseif ($dataBeforeBind[$key] != $this->recordData[$key]) { $modifiedFields[] = $key; } } unset ($dataBeforeBind); if (count($modifiedFields) > 0) { $relationsAffected = []; unset($modifiedData); foreach ($relationImportantFields as $relationName => $fieldName) { if (in_array($fieldName, $modifiedFields)) { $relationsAffected[] = $relationName; } } // Reset the relations which are affected by the save. This will force-reload the relations when you try to // access them again. $this->relationManager->resetRelationData($relationsAffected); } } // Finally, call the onAfterSave event $this->triggerEvent('onAfterSave'); return $this; } /** * Alias of save. For TableInterface compatibility. * * @param boolean $updateNulls Blatantly ignored. * * @return boolean True on success. */ public function store($updateNulls = false) { try { $this->save(); } catch (\Exception $e) { return false; } return true; } /** * Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set * data, unless you provide a $data array. On top of that, it also saves all specified relations. If $relations is * null it will save all relations known to this model. * * @param null|array $data [Optional] Data to bind * @param string $orderingFilter A WHERE clause used to apply table item reordering * @param array $ignore A list of fields to ignore when binding $data * @param array $relations Which relations to save with the model's record. Leave null for all * relations * * @return $this Self, for chaining */ public function push($data = null, $orderingFilter = '', $ignore = null, array $relations = null) { // Store the model's $touches definition $touches = $this->touches; $this->touches = is_array($relations) ? array_diff($this->touches, $relations) : []; // Save this record $this->save($data, $orderingFilter, $ignore, false); // Push all relations specified (or all relations if $relations is null) $relManager = $this->getRelations(); $allRelations = $relManager->getRelationNames(); foreach ($allRelations as $relationName) { if (!is_null($relations) && !in_array($relationName, $relations)) { continue; } $relManager->save($relationName); } // Restore the model's $touches definition $this->touches = $touches; // Return self for chaining return $this; } /** * Method to bind an associative array or object to the DataModel instance. This method optionally takes an array of * properties to ignore when binding. * * Special note if you are using a custom buildQuery with JOINs or field aliases: * You will need to use addKnownField to let FOF know that the fields from your JOINs and the aliased fields should * be bound to the record data. If you are using aliased fields you may also want to override the * databaseDataToRecordData method. Generally, it is a BAD idea using JOINs instead of relations. * * @param mixed $data An associative array or object to bind to the DataModel instance. * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. * * @return static Self, for chaining * * @throws \InvalidArgumentException * @throws \Exception */ public function bind($data, $ignore = []) { $this->triggerEvent('onBeforeBind', [&$data]); // If the source value is not an array or object return false. if (!is_object($data) && !is_array($data)) { throw new \InvalidArgumentException(Text::sprintf('LIB_FOF40_MODEL_ERR_BIND', get_class($this), gettype($data))); } // If the ignore value is a string, explode it over spaces. if (!is_array($ignore)) { $ignore = explode(' ', $ignore); } // Bind the source value, excluding the ignored fields. foreach (array_keys($this->recordData) as $k) { // Only process fields not in the ignore array. if (!in_array($k, $ignore)) { if (is_array($data) && isset($data[$k])) { $this->setFieldValue($k, $data[$k]); } elseif (is_object($data) && isset($data->$k)) { $this->setFieldValue($k, $data->$k); } } } // Perform data transformation $this->databaseDataToRecordData(); $this->triggerEvent('onAfterBind', [$data]); return $this; } /** * Check the data for validity. By default it only checks for fields declared as NOT NULL * * @return static Self, for chaining * * @throws \RuntimeException When the data bound to this record is invalid */ public function check() { if (!$this->autoChecks) { return $this; } // Run a custom event $this->triggerEvent('onBeforeCheck'); // Create a slug if there is a title and an empty slug $slugField = $this->getFieldAlias('slug'); $titleField = $this->getFieldAlias('title'); if ($this->hasField('title') && $this->hasField('slug') && !$this->$slugField) { $this->$slugField = ApplicationHelper::stringURLSafe($this->$titleField); } // Special handling of the ordering field if ($this->hasField('ordering') && is_null($this->getFieldValue('ordering'))) { $this->setFieldValue('ordering', 0); } foreach ($this->knownFields as $fieldName => $field) { // Never check the key if it's empty; an empty key is normal for new records if ($fieldName == $this->idFieldName) { continue; } $value = $this->$fieldName; if (isset($field->Null) && ($field->Null == 'NO') && empty($value) && !is_numeric($value) && !in_array($fieldName, $this->fieldsSkipChecks)) { if (!is_null($field->Default)) { $this->$fieldName = $field->Default; continue; } $text = $this->container->componentName . '_' . $this->container->inflector->singularize($this->getName()) . '_ERR_' . $fieldName . '_EMPTY'; throw new \RuntimeException(Text::_(strtoupper($text)), 500); } } return $this; } /** * Change the ordering of the records of the table * * @param string $where The WHERE clause of the SQL used to fetch the order * * @return static Self, for chaining * * @throws \UnexpectedValueException */ public function reorder($where = '') { // If there is no ordering field set an error and return false. if (!$this->hasField('ordering')) { throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName)); } $this->triggerEvent('onBeforeReorder', [&$where]); $order_field = $this->getFieldAlias('ordering'); $k = $this->getIdFieldName(); $db = $this->getDbo(); // Get the primary keys and ordering values for the selection. $query = $db->getQuery(true) ->select($db->qn($k) . ', ' . $db->qn($order_field)) ->from($db->qn($this->getTableName())) ->where($db->qn($order_field) . ' >= ' . $db->q(0)) ->order($db->qn($order_field) . 'ASC, ' . $db->qn($k) . 'ASC'); // Setup the extra where and ordering clause data. if (!empty($where)) { $query->where($where); } $rows = $db->setQuery($query)->loadObjectList(); // Compact the ordering values. foreach ($rows as $i => $row) { // Make sure the ordering is a positive integer. if ($row->$order_field < 0) { continue; } // Only update rows that are necessary. if ($row->$order_field == $i + 1) { continue; } // Update the row ordering field. $query = $db->getQuery(true) ->update($db->qn($this->getTableName())) ->set($db->qn($order_field) . ' = ' . $db->q($i + 1)) ->where($db->qn($k) . ' = ' . $db->q($row->$k)); $db->setQuery($query)->execute(); } $this->triggerEvent('onAfterReorder'); return $this; } /** * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause. * Negative numbers move the row up in the sequence and positive numbers move it down. * * @param integer $delta The direction and magnitude to move the row in the ordering sequence. * @param string $where WHERE clause to use for limiting the selection of rows to compact the * ordering values. * * @return static Self, for chaining * * @throws \UnexpectedValueException If the table does not support reordering * @throws \RuntimeException If the record is not loaded */ public function move($delta, $where = '') { if (!$this->hasField('ordering')) { throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName)); } $this->triggerEvent('onBeforeMove', [&$delta, &$where]); $ordering_field = $this->getFieldAlias('ordering'); // If the change is none, do nothing. if (empty($delta)) { $this->triggerEvent('onAfterMove'); return $this; } $k = $this->idFieldName; $db = $this->getDbo(); $query = $db->getQuery(true); // If the table is not loaded, return false if (empty($this->$k)) { throw new RecordNotLoaded(sprintf("Model %s does not have a loaded record", $this->getName())); } // Select the primary key and ordering values from the table. $query->select([ $db->qn($this->idFieldName), $db->qn($ordering_field), ] )->from($db->qn($this->tableName)); // If the movement delta is negative move the row up. if ($delta < 0) { $query->where($db->qn($ordering_field) . ' < ' . $db->q((int) $this->$ordering_field)); $query->order($db->qn($ordering_field) . ' DESC'); } // If the movement delta is positive move the row down. elseif ($delta > 0) { $query->where($db->qn($ordering_field) . ' > ' . $db->q((int) $this->$ordering_field)); $query->order($db->qn($ordering_field) . ' ASC'); } // Add the custom WHERE clause if set. if (!empty($where)) { $query->where($where); } // Select the first row with the criteria. $row = $db->setQuery($query, 0, 1)->loadObject(); // If a row is found, move the item. if (!empty($row)) { // Update the ordering field for this instance to the row's ordering value. $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($db->qn($ordering_field) . ' = ' . $db->q((int) $row->$ordering_field)) ->where($db->qn($k) . ' = ' . $db->q($this->$k)); $db->setQuery($query)->execute(); // Update the ordering field for the row to this instance's ordering value. $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($db->qn($ordering_field) . ' = ' . $db->q((int) $this->$ordering_field)) ->where($db->qn($k) . ' = ' . $db->q($row->$k)); $db->setQuery($query)->execute(); // Update the instance value. $this->$ordering_field = $row->$ordering_field; } $this->triggerEvent('onAfterMove'); return $this; } /** * Process a large collection of records a few at a time. * * @param integer $chunkSize How many records to process at once * @param callable $callback A callable to process each record * * @return $this Self, for chaining */ public function chunk($chunkSize, $callback) { $totalItems = $this->count(); if ($totalItems === 0) { return $this; } $start = 0; while ($start < ($totalItems - 1)) { $this->get(true, $start, $chunkSize)->transform($callback); $start += $chunkSize; } return $this; } /** * Get the number of all items * * @return integer */ public function count() { // Get a "count all" query $db = $this->getDbo(); $query = $this->buildQuery(true); $query->clear('select')->clear('order')->select('COUNT(*)'); // Run the "before build query" hook and behaviours $this->triggerEvent('onBuildCountQuery', [&$query]); return $db->setQuery($query)->loadResult(); } /** * Build the query to fetch data from the database * * @param boolean $overrideLimits Should I override limits * * @return \JDatabaseQuery The database query to use */ public function buildQuery($overrideLimits = false) { // Get a "select all" query $db = $this->getDbo(); $query = $db->getQuery(true) ->select('*') ->from($this->getTableName()); // Run the "before build query" hook and behaviours $this->triggerEvent('onBeforeBuildQuery', [&$query, $overrideLimits]); // Apply custom WHERE clauses if (count($this->whereClauses) > 0) { foreach ($this->whereClauses as $clause) { $query->where($clause); } } $order = $this->getState('filter_order', null, 'cmd'); if (!array_key_exists($order, $this->knownFields)) { $order = $this->getIdFieldName(); $this->setState('filter_order', $order); } $order = $db->qn($order); $dir = strtoupper($this->getState('filter_order_Dir', '', 'cmd')); if (!in_array($dir, ['ASC', 'DESC'])) { $dir = 'ASC'; $this->setState('filter_order_Dir', $dir); } $query->order($order . ' ' . $dir); // Run the "before after query" hook and behaviours $this->triggerEvent('onAfterBuildQuery', [&$query, $overrideLimits]); return $query; } /** * Returns a DataCollection iterator based on your currently set Model state * * @param boolean $overrideLimits Should I ignore limits set in the Model? * @param integer $limitstart How many items to skip from the start, only when $overrideLimits = true * @param integer $limit How many items to return, only when $overrideLimits = true * * @return DataCollection The data collection */ public function get($overrideLimits = false, $limitstart = 0, $limit = 0) { if (!$overrideLimits) { $limitstart = $this->getState('limitstart', 0); $limit = $this->getState('limit', 0); } $dataCollection = DataCollection::make($this->getItemsArray($limitstart, $limit, $overrideLimits)); $this->eagerLoad($dataCollection); return $dataCollection; } /** * Returns a raw array of DataModel instances based on your currently set Model state * * @param integer $limitstart How many items from the start to skip (0 = do not skip) * @param integer $limit How many items to return (0 = all) * @param bool $overrideLimits Set to true to override limitstart, limit and ordering * * @return array Array of DataModel objects */ public function &getItemsArray($limitstart = 0, $limit = 0, $overrideLimits = false) { $itemsTemp = $this->getRawDataArray($limitstart, $limit, $overrideLimits); $items = []; while (!empty($itemsTemp)) { $data = array_shift($itemsTemp); /** @var DataModel $item */ $item = clone $this; $item->clearState()->reset(); $item->bind($data); $items[$item->getId()] = $item; $item->relationManager = clone $this->relationManager; $item->relationManager->rebase($item); } $this->triggerEvent('onAfterGetItemsArray', [&$items]); return $items; } /** * Returns the raw data array, as fetched from the database, based on your currently set Model state * * @param integer $limitstart How many items from the start to skip (0 = do not skip) * @param integer $limit How many items to return (0 = all) * @param bool $overrideLimits Set to true to override limitstart, limit and ordering * * @return array Array of hashed arrays */ public function &getRawDataArray($limitstart = 0, $limit = 0, $overrideLimits = false) { $limitstart = max($limitstart, 0); $limit = max($limit, 0); $query = $this->buildQuery($overrideLimits); $db = $this->getDbo(); $db->setQuery($query, $limitstart, $limit); $rawData = $db->loadAssocList(); return $rawData; } /** * Eager loads the provided relations and assigns their data to a data collection * * @param DataCollection $dataCollection The data collection on which the eager loaded relations will be * applied * @param array|null $relations The relations to eager load. Leave empty to use the already defined * relations * * @return $this for chaining */ public function eagerLoad(DataCollection &$dataCollection, array $relations = null) { if (empty($relations)) { $relations = $this->eagerRelations; } // Apply eager loaded relations if ($dataCollection->count() && !empty($relations)) { $relationManager = $this->getRelations(); foreach ($relations as $relation => $callback) { // Did they give us a relation name without a callback? if (!is_callable($callback) && is_string($callback) && !empty($callback)) { $relation = $callback; $callback = null; } $relationData = $relationManager->getData($relation, $callback, $dataCollection); $foreignKeyMap = $relationManager->getForeignKeyMap($relation); /** @var DataModel $item */ foreach ($dataCollection as $item) { $item->getRelations()->setDataFromCollection($relation, $relationData, $foreignKeyMap); } } } return $this; } /** * Archive the record, i.e. set enabled to 2 * * @return $this For chaining */ public function archive() { if (!$this->getId()) { throw new RecordNotLoaded("Can't archive a not loaded DataModel"); } if (!$this->hasField('enabled')) { return $this; } $this->triggerEvent('onBeforeArchive'); $enabled = $this->getFieldAlias('enabled'); $this->$enabled = 2; $this->save(); $this->triggerEvent('onAfterArchive'); return $this; } /** * Trashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that record * is loaded before trying to trash it. Unlike a hard delete, trashing is a "soft delete", only setting the enabled * field to -2. * * @param mixed $id Primary key (id field) value * * @return $this for chaining */ public function trash($id = null) { if (!empty($id)) { $this->findOrFail($id); } $id = $this->getId(); if (!$id) { throw new RecordNotLoaded("Can't trash a not loaded DataModel"); } if (!$this->hasField('enabled')) { throw new SpecialColumnMissing("DataModel::trash method needs an 'enabled' field"); } $this->triggerEvent('onBeforeTrash', [&$id]); $enabled = $this->getFieldAlias('enabled'); $this->$enabled = -2; $this->save(); $this->triggerEvent('onAfterTrash', [&$id]); return $this; } /** * Change the publish state of a record. By default it will set it to 1 (published) unless you specify a different * value. * * @param int $state The publish state. Default: 1 (published). * * @return $this For chaining */ public function publish($state = 1) { if (!$this->getId()) { throw new RecordNotLoaded("Can't change the state of a not loaded DataModel"); } if (!$this->hasField('enabled')) { return $this; } $this->triggerEvent('onBeforePublish'); $enabled = $this->getFieldAlias('enabled'); $this->$enabled = $state; $this->save(); $this->triggerEvent('onAfterPublish'); return $this; } /** * Unpublish the record, i.e. set enabled to 0 * * @return $this For chaining */ public function unpublish() { if (!$this->getId()) { throw new RecordNotLoaded("Can't unpublish a not loaded DataModel"); } if (!$this->hasField('enabled')) { return $this; } $this->triggerEvent('onBeforeUnpublish'); $enabled = $this->getFieldAlias('enabled'); $this->$enabled = 0; $this->save(); $this->triggerEvent('onAfterUnpublish'); return $this; } /** * Untrashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that * record is loaded before trying to untrash it. Please note that enabled is set to 0 (unpublished) when you untrash * an item. * * @param mixed $id Primary key (id field) value * * @return $this for chaining */ public function restore($id = null) { if (!$this->hasField('enabled')) { return $this; } if (!empty($id)) { $this->findOrFail($id); } $id = $this->getId(); if (!$id) { throw new RecordNotLoaded("Can't change the state of a not loaded DataModel"); } $this->triggerEvent('onBeforeRestore', [&$id]); $enabled = $this->getFieldAlias('enabled'); $this->$enabled = 0; $this->save(); $this->triggerEvent('onAfterRestore', [&$id]); return $this; } /** * Creates a copy of the current record. After the copy is performed, the data model contains the data of the new * record. * * @param array|DataModel An associative array or object to bind to the DataModel instance. Allows you to * override values on the copied object. * * @return DataModel */ public function copy($data = null) { $this->triggerEvent('onBeforeCopy'); $this->{$this->idFieldName} = null; if ($this->hasField('created_by')) { $this->setFieldValue('created_by'); } if ($this->hasField('modified_by')) { $this->setFieldValue('modified_by'); } if ($this->hasField('locked_by')) { $this->setFieldValue('locked_by'); } if ($this->hasField('created_on')) { $this->setFieldValue('created_on'); } if ($this->hasField('modified_on')) { $this->setFieldValue('modified_on'); } if ($this->hasField('locked_on')) { $this->setFieldValue('locked_on'); } $result = $this->save($data); $this->triggerEvent('onAfterCopy', [&$result]); return $result; } /** * Check-in an item. This works similar to unlock() but performs additional checks. If the item is locked by another * user you need to have adequate ACL privileges to unlock it, i.e. core.admin or core.manage component-wide * privileges; core.edit.state privileges component-wide or per asset; or be the creator of the item and have * core.edit.own privileges component-wide or per asset. * * @return $this * * @throws LockedRecord If you don't have the privilege to check in this item */ public function checkIn($userId = null) { // If there is no loaded record we can't do much, I'm afraid if (!$this->getId()) { throw new RecordNotLoaded("Can't checkin a not loaded DataModel"); } // If the lock fields are missing we have nothing to do if (!$this->hasField('locked_by') && !$this->hasField('locked_on')) { return $this; } // If there's no locked_by field we just unlock and return if (!$this->hasField('locked_by')) { return $this->unlock(); } // If the current user and the user who locked the record are the same, unlock it. if (empty($userId)) { $userId = $this->container->platform->getUser()->id; } $lockedBy = $this->getFieldValue('locked_by'); if (empty($lockedBy) || ($lockedBy == $userId)) { return $this->unlock(); } // Get the component privileges $platform = $this->container->platform; $component = $this->container->componentName; $privileges = [ 'editown' => $platform->authorise('core.edit.own', $component), 'editstate' => $platform->authorise('core.edit.state', $component), 'admin' => $platform->authorise('core.admin', $component), 'manage' => $platform->authorise('core.manage', $component), ]; // If we are trackign assets get the item's privileges if ($this->isAssetsTracked()) { $assetKey = $this->getAssetKey(); $assetPrivileges = [ 'editown' => $platform->authorise('core.edit.own', $assetKey), 'editstate' => $platform->authorise('core.edit.state', $assetKey), ]; foreach ($assetPrivileges as $k => $v) { $privileges[$k] = $privileges[$k] || $v; } } // If you are a Super User, component manager or allowed to edit the state of records we unlock it if ($privileges['admin'] || $privileges['manage'] || $privileges['editstate']) { return $this->unlock(); } // If you are the owner of the record and have core.edit.own privilege we will unlock it. $owner = 0; if ($this->hasField('created_by')) { $owner = $this->getFieldValue('created_by'); } if ($privileges['editown'] && ($owner == $userId)) { return $this->unlock(); } // All else failed, you don't have the privilege to unlock this item. throw new LockedRecord; } /** * Reset the record data * * @param boolean $useDefaults Should I use the default values? Default: yes * @param boolean $resetRelations Should I reset the relations too? Default: no * * @return static Self, for chaining */ public function reset($useDefaults = true, $resetRelations = false) { $this->recordData = []; $this->whereClauses = []; foreach ($this->knownFields as $fieldName => $information) { $this->recordData[$fieldName] = $useDefaults ? $information->Default : null; } if ($resetRelations) { $this->relationManager->resetRelationData(); $this->eagerRelations = []; } $this->relationFilters = []; $this->triggerEvent('onAfterReset', [$useDefaults, $resetRelations]); return $this; } /** * Automatically performs a hard or soft delete, based on the value of $this->softDelete. A soft delete simply sets * enabled to -2 whereas a hard delete removes the data from the database. If you want to force a specific behaviour * directly call trash() for a soft delete or forceDelete() for a hard delete. * * @param mixed $id Primary key (id field) value * * @return $this for chaining */ public function delete($id = null) { if ($this->softDelete) { return $this->trash($id); } else { return $this->forceDelete($id); } } /** * Delete a record, either the currently loaded one or the one specified in $id. If an $id is specified that record * is loaded before trying to delete it. In the end the data model is reset. * * @param mixed $id Primary key (id field) value * * @return $this for chaining */ public function forceDelete($id = null) { if (!empty($id)) { $this->findOrFail($id); } $id = $this->getId(); if (!$id) { throw new RecordNotLoaded("Can't delete a not loaded DataModel object"); } $this->triggerEvent('onBeforeDelete', [&$id]); $db = $this->getDbo(); $query = $db->getQuery(true) ->delete() ->from($this->tableName) ->where($db->qn($this->idFieldName) . ' = ' . $db->q($id)); $db->setQuery($query)->execute(); $this->triggerEvent('onAfterDelete', [&$id]); $this->reset(); return $this; } /** * Generic check for whether dependencies exist for this object in the db schema. This method is NOT used by * default. If you want to use it you will have to override your delete(), trash() or forceDelete() method, * or create an onBeforeDelete and/or onBeforeTrash event handler. * * @param integer $oid The primary key of the record to delete * @param array $joins Any joins to foreign table, used to determine if dependent records exist * * @return void * * @throws \RuntimeException If you should not delete the record (the message tells you why) */ public function canDelete($oid = null, $joins = null) { $pkField = $this->getKeyName(); if ($oid) { $this->$pkField = (int) $oid; } if (!$this->$pkField) { throw new \InvalidArgumentException('Master table should be loaded or an ID should be passed'); } if (is_array($joins)) { $db = $this->getDbo(); $query = $db->getQuery(true) ->select($db->qn('master') . '.' . $db->qn($pkField)) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('master')); $tableNo = 0; foreach ($joins as $table) { // Sanity check on passed array $check = ['idfield', 'idalias', 'name', 'joinfield', 'label']; $result = array_intersect($check, array_keys($table)); if (count($result) != count($check)) { throw new \InvalidArgumentException('Join array missing some keys, please check the documentation'); } $tableNo++; $query->select( [ 'COUNT(DISTINCT ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['idfield']) . ') AS ' . $db->qn($table['idalias']), ] ); $query->join('LEFT', $db->qn($table['name']) . ' AS ' . $db->qn('t' . $tableNo) . ' ON ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['joinfield']) . ' = ' . $db->qn('master') . '.' . $db->qn($pkField) ); } $query->where($db->qn('master') . '.' . $db->qn($pkField) . ' = ' . $db->q($this->$pkField)); $query->group($db->qn('master') . '.' . $db->qn($pkField)); $this->getDbo()->setQuery((string) $query); $obj = $this->getDbo()->loadObject(); $msg = []; $i = 0; foreach ($joins as $table) { $pkField = $table['idalias']; if ($obj->$pkField > 0) { $msg[] = Text::_($table['label']); } $i++; } if (count($msg) > 0) { $option = $this->container->componentName; $comName = $this->container->bareComponentName; $tbl = $this->getTableName(); $tview = str_replace('#__' . $comName . '_', '', $tbl); $prefix = $option . '_' . $tview . '_NODELETE_'; $message = '<ul>'; foreach ($msg as $key) { $message .= '<li>' . Text::_(strtoupper($prefix . $key)) . '</li>'; } $message .= '</ul>'; throw new \RuntimeException($message); } } } /** * Find and load a single record based on the provided key values. If the record is not found an exception is thrown * * @param array|mixed $keys An optional primary key value to load the row by, or an array of fields to match. * If not set the "id" state variable or, if empty, the identity column's value is used * * @return static Self, for chaining * * @throws \RuntimeException When the row is not found */ public function findOrFail($keys = null) { $this->find($keys); // We have to assign the value, since empty() is not triggering the __get magic method // http://stackoverflow.com/questions/2045791/php-empty-on-get-accessor $value = $this->getId(); if (empty($value)) { throw new RecordNotLoaded; } return $this; } /** * Method to load a row from the database by primary key. Used for TableInterface compatibility. * * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. If * not set the instance property value is used. * @param boolean $reset True to reset the default values before loading the new row. * * @return boolean True if successful. False if row not found. * * @throws \RuntimeException * @throws \UnexpectedValueException * @link http://docs.joomla.org/JTable/load * @since 3.2 */ public function load($keys = null, $reset = true) { if ($reset) { $this->reset(); } try { $this->findOrFail($keys); } catch (\Exception $e) { return false; } return true; } /** * Find and load a single record based on the provided key values * * @param array|mixed $keys An optional primary key value to load the row by, or an array of fields to match. * If not set the "id" state variable or, if empty, the identity column's value is used * * @return static Self, for chaining */ public function find($keys = null) { // Execute the onBeforeLoad event $this->triggerEvent('onBeforeLoad', [&$keys]); // If we are not given any keys, try to get the ID from the state or the table data if (empty($keys)) { $id = $this->getState('id', 0); if (empty($id)) { $id = $this->getId(); } if (empty($id)) { $this->triggerEvent('onAfterLoad', [false, &$keys]); $this->reset(); return $this; } $keys = [$this->idFieldName => $id]; } elseif (!is_array($keys)) { if (empty($keys)) { $this->triggerEvent('onAfterLoad', [false, &$keys]); $this->reset(); return $this; } $keys = [$this->idFieldName => $keys]; } // Reset the table $this->reset(); // Get the query $db = $this->getDbo(); $query = $db->getQuery(true) ->select('*') ->from($db->qn($this->tableName)); // Apply key filters foreach ($keys as $filterKey => $filterValue) { if ($filterKey == 'id') { $filterKey = $this->getIdFieldName(); } if (array_key_exists($filterKey, $this->recordData)) { $query->where($db->qn($filterKey) . ' = ' . $db->q($filterValue)); } } // Get the row $db->setQuery($query); try { $row = $db->loadAssoc(); } catch (\Exception $e) { $row = null; } if (empty($row)) { $this->triggerEvent('onAfterLoad', [false, &$keys]); return $this; } // Bind the data $this->bind($row); $this->relationManager->rebase($this); // Execute the onAfterLoad event $this->triggerEvent('onAfterLoad', [true, &$keys]); return $this; } /** * Create a new record with the provided data * * @param array $data The data to use in the new record * * @return static Self, for chaining */ public function create($data) { return $this->reset()->bind($data)->save(); } /** * Return the first item found or create a new one based on the provided $data * * @param array $data Data for the newly created item * * @return static */ public function firstOrCreate($data) { $item = $this->get(true, 0, 1)->first(); if (is_null($item)) { $item = clone $this; $item->create($data); } return $item; } /** * Return the first item found or throw a \RuntimeException * * @return static * * @throws \RuntimeException */ public function firstOrFail() { $item = $this->get(true, 0, 1)->first(); if (is_null($item)) { throw new NoItemsFound(get_class($this)); } return $item; } /** * Return the first item found or create a new, blank one * * @return static */ public function firstOrNew() { $item = $this->get(true, 0, 1)->first(); if (is_null($item)) { $item = clone $this; $item->reset(); } return $item; } /** * Adds a behaviour by its name. It will search the following classes, in this order: * \component_namespace\Model\modelName\Behaviour\behaviourName * \component_namespace\Model\Behaviour\behaviourName * \FOF40\Model\DataModel\Behaviour\behaviourName * where: * component_namespace is the namespace of the component as defined in the container * modelName is the model's name, first character uppercase, e.g. Baz * behaviourName is the $behaviour parameter, first character uppercase, e.g. Something * * @param string $behaviour The behaviour's name * * @return $this Self, for chaining */ public function addBehaviour($behaviour) { $prefixes = [ $this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()), $this->container->getNamespacePrefix() . 'Model\\Behaviour', '\\FOF40\\Model\\DataModel\\Behaviour', ]; foreach ($prefixes as $prefix) { $className = $prefix . '\\' . ucfirst($behaviour); if (class_exists($className, true) && !$this->behavioursDispatcher->hasObserverClass($className)) { /** @var Observer $o */ $observer = new $className($this->behavioursDispatcher); $this->behavioursDispatcher->attach($observer); return $this; } } return $this; } /** * Removes a behaviour by its name. It will search the following classes, in this order: * \component_namespace\Model\modelName\Behaviour\behaviourName * \component_namespace\Model\DataModel\Behaviour\behaviourName * \FOF40\Model\DataModel\Behaviour\behaviourName * where: * component_namespace is the namespace of the component as defined in the container * modelName is the model's name, first character uppercase, e.g. Baz * behaviourName is the $behaviour parameter, first character uppercase, e.g. Something * * @param string $behaviour The behaviour's name * * @return $this Self, for chaining */ public function removeBehaviour($behaviour) { $prefixes = [ $this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()), $this->container->getNamespacePrefix() . 'Model\\Behaviour', '\\FOF40\\Model\\DataModel\\Behaviour', ]; foreach ($prefixes as $prefix) { $className = ltrim($prefix . '\\' . ucfirst($behaviour), '\\'); $observer = $this->behavioursDispatcher->getObserverByClass($className); if (is_null($observer)) { continue; } $this->behavioursDispatcher->detach($observer); return $this; } return $this; } /** * Gives you access to the behaviours dispatcher, allowing to attach/detach behaviour observers * * @return Dispatcher */ public function &getBehavioursDispatcher() { return $this->behavioursDispatcher; } /** * Set the field and direction of ordering for the query returned by buildQuery. * Alias of $this->setState('filter_order', $fieldName) and $this->setState('filter_order_Dir', $direction) * * @param string $fieldName The field name to order by * @param string $direction The direction to order by (ASC for ascending or DESC for descending) * * @return $this For chaining */ public function orderBy($fieldName, $direction = 'ASC') { $direction = strtoupper($direction); if (!in_array($direction, ['ASC', 'DESC'])) { $direction = 'ASC'; } $this->setState('filter_order', $fieldName); $this->setState('filter_order_Dir', $direction); return $this; } /** * Set the limitStart for the query, i.e. how many records to skip. * Alias of $this->setState('limitstart', $limitStart); * * @param integer $limitStart Records to skip from the start * * @return $this For chaining */ public function skip($limitStart = null) { // Only positive integers are allowed if (!is_int($limitStart) || $limitStart < 0 || !$limitStart) { $limitStart = 0; } $this->setState('limitstart', $limitStart); return $this; } /** * Set the limit for the query, i.e. how many records to return. * Alias of $this->setState('limit', $limit); * * @param integer $limit Maximum number of records to return * * @return $this For chaining */ public function take($limit = null) { // Only positive integers are allowed if (!is_int($limit) || $limit < 0 || !$limit) { $limit = 0; } $this->setState('limit', $limit); return $this; } /** * Return the record's data as an array * * @return array */ public function toArray() { return $this->recordData; } /** * Returns the record's data as a JSON string * * @param boolean $prettyPrint Should I format the JSON for pretty printing * * @return string */ public function toJson($prettyPrint = false) { if (defined('JSON_PRETTY_PRINT')) { $options = $prettyPrint ? JSON_PRETTY_PRINT : 0; } else { $options = 0; } return json_encode($this->recordData, $options); } /** * Touch a record, updating its modified_on and/or modified_by columns * * @param integer $userId Optional user ID of the user touching the record * * @return $this Self, for chaining */ public function touch($userId = null) { if (!$this->getId()) { throw new RecordNotLoaded("Can't touch a not loaded DataModel"); } if (!$this->hasField('modified_on') && !$this->hasField('modified_by')) { return $this; } $db = $this->getDbo(); $date = new Date(); // Update the created_on / modified_on if ($this->hasField('modified_on')) { $modified_on = $this->getFieldAlias('modified_on'); $this->$modified_on = $date->toSql(false, $db); } // Update the created_by / modified_by values if necessary if ($this->hasField('modified_by')) { if (empty($userId)) { $userId = $this->container->platform->getUser()->id; } $modified_by = $this->getFieldAlias('modified_by'); $this->$modified_by = $userId; } $this->save(); return $this; } /** * Lock a record by setting its locked_on and/or locked_by columns * * @param integer $userId * * @return $this Self, for chaining */ public function lock($userId = null) { if (!$this->getId()) { throw new CannotLockNotLoadedRecord; } if (!$this->hasField('locked_on') && !$this->hasField('locked_by')) { return $this; } $this->triggerEvent('onBeforeLock'); $db = $this->getDbo(); if ($this->hasField('locked_on')) { $date = new Date(); $locked_on = $this->getFieldAlias('locked_on'); $this->$locked_on = $date->toSql(false, $db); } if ($this->hasField('locked_by')) { if (empty($userId)) { $userId = $this->container->platform->getUser()->id; } $locked_by = $this->getFieldAlias('locked_by'); $this->$locked_by = $userId; } $this->save(); $this->triggerEvent('onAfterLock'); return $this; } /** * Unlock a record by resetting its locked_on and/or locked_by columns * * @return $this Self, for chaining */ public function unlock() { if (!$this->getId()) { throw new RecordNotLoaded("Can't unlock a not loaded DataModel"); } if (!$this->hasField('locked_on') && !$this->hasField('locked_by')) { return $this; } $this->triggerEvent('onBeforeUnlock'); $db = $this->getDbo(); if ($this->hasField('locked_on')) { $locked_on = $this->getFieldAlias('locked_on'); $this->$locked_on = $this->isNullableField('locked_on') ? null : $db->getNullDate(); } if ($this->hasField('locked_by')) { $locked_by = $this->getFieldAlias('locked_by'); $this->$locked_by = 0; } $this->save(); $this->triggerEvent('onAfterUnlock'); return $this; } /** * Is this record locked by a different user than $userId? * * @param integer $userId * * @return bool True if the record is locked */ public function isLocked($userId = null) { if (!$this->hasField('locked_on') && !$this->hasField('locked_by')) { return false; } $nullDate = $this->isNullableField('locked_on') ? null : $this->getDbo()->getNullDate(); // Get the locked_by / locked_on $locked_on = $nullDate; $locked_by = 0; if ($this->hasField('locked_on')) { $locked_on = $this->getFieldValue('locked_on', $nullDate); if (empty($locked_on)) { $locked_on = $nullDate; } } if ($this->hasField('locked_by')) { $locked_by = $this->getFieldValue('locked_by', 0); if (empty($locked_by)) { $locked_by = 0; } } $allowedUsers = [0]; if (!empty($userId)) { $allowedUsers[] = $userId; } if (in_array($locked_by, $allowedUsers)) { return false; } return !is_null($locked_on) && ($locked_on !== $nullDate); } /** * Automatically uses the Filters behaviour to filter records in the model based on your criteria. * * @param string $fieldName The field name to filter on * @param string $method The filtering method, e.g. <>, =, != and so on * @param mixed $values The value you're filtering on. Some filters (e.g. interval or between) require an * array of values * * @return $this For chaining */ public function where($fieldName, $method = '=', $values = null) { // Make sure the Filters behaviour is added to the model if (!$this->behavioursDispatcher->hasObserverClass('FOF40\\Model\\DataModel\\Behaviour\\Filters')) { $this->addBehaviour('filters'); } // If we are dealing with the primary key, let's set the field name to "id". This is a convention and it will // be used inside the Filters behaviour // -- Let's not do this. The Filters behaviour works just fine with the regular field name! /** * if ($fieldName == $this->getIdFieldName()) * { * $fieldName = 'id'; * } **/ $options = [ 'method' => $method, 'value' => $values, ]; // Handle method aliases switch ($method) { case '<>': $options['method'] = 'search'; $options['operator'] = '!='; break; case 'lt': $options['method'] = 'search'; $options['operator'] = '<'; break; case 'le': $options['method'] = 'search'; $options['operator'] = '<='; break; case 'gt': $options['method'] = 'search'; $options['operator'] = '>'; break; case 'ge': $options['method'] = 'search'; $options['operator'] = '>='; break; case 'eq': $options['method'] = 'search'; $options['operator'] = '='; break; case 'neq': case 'ne': $options['method'] = 'search'; $options['operator'] = '!='; break; case '<': case '!<': case '<=': case '!<=': case '>': case '!>': case '>=': case '!>=': case '!=': case '=': $options['method'] = 'search'; $options['operator'] = $method; break; case 'like': case '~': case '%': $options['method'] = 'partial'; break; case '==': case '=[]': case '=()': case 'in': $options['method'] = 'exact'; break; case '()': case '[]': case '[)': case '(]': $options['method'] = 'between'; break; case ')(': case ')[': case '](': case '][': $options['method'] = 'outside'; break; case '*=': case 'every': $options['method'] = 'interval'; break; case '?=': $options['method'] = 'search'; break; default: throw new InvalidSearchMethod('Method ' . $method . ' is unsupported'); } // Handle real methods switch ($options['method']) { case 'between': case 'outside': if (is_array($values) && (count($values) > 1)) { // Get the from and to values from the $values array if (isset($values['from']) && isset($values['to'])) { $options['from'] = $values['from']; $options['to'] = $values['to']; } else { $options['from'] = array_shift($values); $options['to'] = array_shift($values); } unset($options['value']); } else { // $values is not a from/to array. Treat as = (between) or != (outside) if (is_array($values)) { $values = array_shift($values); } $options['operator'] = ($options['method'] == 'between') ? '=' : '!='; $options['value'] = $values; $options['method'] = 'search'; } break; case 'interval': if (is_array($values) && (count($values) > 1)) { // Get the value and interval from the $values array if (isset($values['value']) && isset($values['interval'])) { $options['value'] = $values['value']; $options['interval'] = $values['interval']; } else { $options['value'] = array_shift($values); $options['interval'] = array_shift($values); } } else { // $values is not a value/interval array. Treat as = if (is_array($values)) { $values = array_shift($values); } $options['value'] = $values; $options['method'] = 'search'; $options['operator'] = '='; } break; case 'search': // We don't have to do anything if the operator is already set if (isset($options['operator'])) { break; } if (is_array($values) && (count($values) > 1)) { // Get the operator and value from the $values array if (isset($values['operator']) && isset($values['value'])) { $options['operator'] = $values['operator']; $options['value'] = $values['value']; } else { $options['operator'] = array_shift($values); $options['value'] = array_shift($values); } } break; } $this->setState($fieldName, $options); return $this; } /** * Add custom, pre-compiled WHERE clauses for use in buildQuery. The raw WHERE clause you specify is added as is to * the query generated by buildQuery. You are responsible for quoting and escaping the field names and data found * inside the WHERE clause. * * Using this method is a generally bad idea. You are better off overriding buildQuery and using state variables to * customise the query build built instead of using this method to push raw SQL to the query builder. Mixing your * business logic with raw SQL makes your application harder to maintain and refactor as dependencies to your * database schema creep in areas of your code that should have nothing to do with it. * * @param string $rawWhereClause The raw WHERE clause to add * * @return $this For chaining */ public function whereRaw($rawWhereClause) { $this->whereClauses[] = $rawWhereClause; return $this; } /** * Instructs the model to eager load the specified relations. The $relations array can have the format: * * array('relation1', 'relation2') * Eager load relation1 and relation2 without any callbacks * array('relation1' => $callable1, 'relation2' => $callable2) * Eager load relation1 with callback $callable1 etc * array('relation1', 'relation2' => $callable2) * Eager load relation1 without a callback, relation2 with callback $callable2 * * The callback must have the signature function(\JDatabaseQuery $query) and doesn't return a value. It is * supposed to modify the query directly. * * Please note that eager loaded relations produce their queries without going through the respective model. Instead * they generate a SQL query directly, then map the loaded results into a DataCollection. * * @param array $relations The relations to eager load. See above for more information. * * @return $this For chaining */ public function with(array $relations) { if (empty($relations)) { $this->eagerRelations = []; return $this; } $knownRelations = $this->relationManager->getRelationNames(); foreach ($relations as $k => $v) { if (is_callable($v)) { $relName = $k; $callback = $v; } else { $relName = $v; $callback = null; } if (in_array($relName, $knownRelations)) { $this->eagerRelations[$relName] = $callback; } } return $this; } /** * Filter the model based on the fulfilment of relations. For example: * $posts->has('comments', '>=', 10)->get(); * will return all posts with at least 10 comments. * * @param string $relation The relation to query * @param string $operator The comparison operator. Same operators as the where() method. * @param mixed $value The value(s) to compare against. * @param bool $replace When true (default) any existing relation filters for the same relation will be * replaced * * @return $this */ public function has($relation, $operator = '>=', $value = 1, $replace = true) { // Make sure the Filters behaviour is added to the model if (!$this->behavioursDispatcher->hasObserverClass('FOF40\\Model\\DataModel\\Behaviour\\RelationFilters')) { $this->addBehaviour('relationFilters'); } $filter = [ 'relation' => $relation, 'method' => $operator, 'operator' => $operator, 'value' => $value, ]; // Handle method aliases switch ($operator) { case '<>': $filter['method'] = 'search'; $filter['operator'] = '!='; break; case 'lt': $filter['method'] = 'search'; $filter['operator'] = '<'; break; case 'le': $filter['method'] = 'search'; $filter['operator'] = '<='; break; case 'gt': $filter['method'] = 'search'; $filter['operator'] = '>'; break; case 'ge': $filter['method'] = 'search'; $filter['operator'] = '>='; break; case 'eq': $filter['method'] = 'search'; $filter['operator'] = '='; break; case 'neq': case 'ne': $filter['method'] = 'search'; $filter['operator'] = '!='; break; case '<': case '!<': case '<=': case '!<=': case '>': case '!>': case '>=': case '!>=': case '!=': case '=': $filter['method'] = 'search'; $filter['operator'] = $operator; break; case 'like': case '~': case '%': $filter['method'] = 'partial'; break; case '==': case '=[]': case '=()': case 'in': $filter['method'] = 'exact'; break; case '()': case '[]': case '[)': case '(]': $filter['method'] = 'between'; break; case ')(': case ')[': case '](': case '][': $filter['method'] = 'outside'; break; case '*=': case 'every': $filter['method'] = 'interval'; break; case '?=': $filter['method'] = 'search'; break; case 'callback': $filter['method'] = 'callback'; $filter['operator'] = 'callback'; break; default: throw new InvalidSearchMethod('Operator ' . $operator . ' is unsupported'); } // Handle real methods switch ($filter['method']) { case 'between': case 'outside': if (is_array($value) && (count($value) > 1)) { // Get the from and to values from the $value array if (isset($value['from']) && isset($value['to'])) { $filter['from'] = $value['from']; $filter['to'] = $value['to']; } else { $filter['from'] = array_shift($value); $filter['to'] = array_shift($value); } unset($filter['value']); } else { // $value is not a from/to array. Treat as = (between) or != (outside) if (is_array($value)) { $value = array_shift($value); } $filter['operator'] = ($filter['method'] == 'between') ? '=' : '!='; $filter['value'] = $value; $filter['method'] = 'search'; } break; case 'interval': if (is_array($value) && (count($value) > 1)) { // Get the value and interval from the $value array if (isset($value['value']) && isset($value['interval'])) { $filter['value'] = $value['value']; $filter['interval'] = $value['interval']; } else { $filter['value'] = array_shift($value); $filter['interval'] = array_shift($value); } } else { // $value is not a value/interval array. Treat as = if (is_array($value)) { $value = array_shift($value); } $filter['value'] = $value; $filter['method'] = 'search'; $filter['operator'] = '='; } break; case 'search': // We don't have to do anything if the operator is already set if (isset($filter['operator'])) { break; } if ((is_array($value) || $value instanceof \Countable ? count($value) : 0) > 1) { // Get the operator and value from the $value array if (isset($value['operator']) && isset($value['value'])) { $filter['operator'] = $value['operator']; $filter['value'] = $value['value']; } else { $filter['operator'] = array_shift($value); $filter['value'] = array_shift($value); } } break; case 'callback': if (!is_callable($filter['value'])) { $filter['method'] = 'search'; $filter['operator'] = '='; $filter['value'] = 1; } break; } if ($replace && !empty($this->relationFilters)) { foreach ($this->relationFilters as $k => $v) { if ($v['relation'] == $relation) { unset ($this->relationFilters[$k]); } } } $this->relationFilters[] = $filter; return $this; } /** * Advanced model filtering on the fulfilment of relations. Unlike has() you can provide your own callback which * modifies the COUNT subquery used to compare against the relation. The $callBack has the signature * function(\JDatabaseQuery $query) * and MUST return a string. The $query you are passed is the COUNT subquery of the relation, e.g. * SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id * You have to return a WHERE clause for the model's query, e.g. * (SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id) BETWEEN 1 AND 20 * * @param string $relation The relation to query against * @param callable $callBack The callback to use for filtering * @param bool $replace When true (default) any existing relation filters for the same relation will be * replaced * * @return $this */ public function whereHas($relation, $callBack, $replace = true) { $this->has($relation, 'callback', $callBack, $replace); return $this; } /** * Returns the relations manager of the model * * @return RelationManager */ public function &getRelations() { return $this->relationManager; } /** * Gets the relation filter definitions, for use by the RelationFilters behaviour * * @return array */ public function getRelationFilters() { return $this->relationFilters; } /** * Returns the list of relations which are touched by save() and touch() * * @return array */ public function &getTouches() { return $this->touches; } /** * Method to get the rules for the record. * * @return Rules object */ public function getRules() { return $this->rules; } /** * Method to set rules for the record. * * @param mixed $input A Rules object, JSON string, or array. * * @return void */ public function setRules($input) { $this->rules = $input instanceof Rules ? $input : new Rules($input); } /** * Method to check if the record is treated as an ACL asset * * @return boolean [description] */ public function isAssetsTracked() { return $this->trackAssets; } /** * Method to manually set this record as ACL asset or not. * We have to do this since the automatic check is made in the constructor, but here we can't set any alias. * So, even if you have an alias for `asset_id`, it wouldn't be recognized and assets won't be tracked. * * @param $state */ public function setAssetsTracked($state) { $state = (bool) $state; $this->trackAssets = $state; } /** * Gets the has tags switch state * * @return bool */ public function hasTags() { return $this->has_tags; } /** * Sets the has tags switch state * * @param bool $newState */ public function setHasTags($newState = false) { $this->has_tags = $newState; } /** * Method to compute the default name of the asset. * The default name is in the form table_name.id * where id is the value of the primary key of the table. * * @return string * @throws NoAssetKey * */ public function getAssetName() { $k = $this->getKeyName(); // If there is no assetKey defined, stop here, or we'll get a wrong name if (!$this->assetKey || !$this->$k) { throw new NoAssetKey; } return $this->assetKey . '.' . (int) $this->$k; } /** * Method to compute the default name of the asset. * The default name is in the form table_name.id * where id is the value of the primary key of the table. * * @return string */ public function getAssetKey() { return $this->assetKey; } /** * This method sets the asset key for the items of this table. Obviously, it * is only meant to be used when you have a table with an asset field. * * @param string $assetKey The name of the asset key to use * * @return void */ public function setAssetKey($assetKey) { $this->assetKey = $assetKey; } /** * Method to return the title to use for the asset table. In * tracking the assets a title is kept for each asset so that there is some * context available in a unified access manager. Usually this would just * return $this->title or $this->name or whatever is being used for the * primary name of the row. If this method is not overridden, the asset name is used. * * @return string The string to use as the title in the asset table. * * @codeCoverageIgnore */ public function getAssetTitle() { return $this->getAssetName(); } /** * Method to get the parent asset under which to register this one. * By default, all assets are registered to the ROOT node with ID, * which will default to 1 if none exists. * The extended class can define a table and id to lookup. If the * asset does not exist it will be created. * * @param DataModel $model A model object for the asset parent. * @param integer $id Id to look up * * @return integer */ public function getAssetParentId($model = null, $id = null) { // For simple cases, parent to the asset root. $assets = new Asset($this->getDbo()); $rootId = $assets->getRootId(); if (!empty($rootId)) { return $rootId; } return 1; } /** * Method to load a row for editing from the version history table. * * @param integer $version_id Key to the version history table. * @param string $alias The type_alias in #__content_types * * @return boolean True on success * * @throws RecordNotLoaded * @throws BaseException * @since 2.3 * */ public function loadhistory($version_id, $alias) { // Only attempt to check the row in if it exists. if (empty($version_id)) { throw new RecordNotLoaded; } // Get an instance of the row to checkout. $historyTable = new ContentHistory(Factory::getDbo()); if (!$historyTable->load($version_id)) { throw new BaseException($historyTable->getError()); } $rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data)); $contentTypeTable = new ContentType(Factory::getDbo()); $typeId = $contentTypeTable->getTypeId($alias); if ($historyTable->ucm_type_id != $typeId) { $key = $this->getKeyName(); if (isset($rowArray[$key])) { $this->{$this->idFieldName} = $rowArray[$key]; $this->unlock(); } throw new BaseException(Text::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH')); } $this->setState('save_date', $historyTable->save_date); $this->setState('version_note', $historyTable->version_note); $this->bind($rowArray); return true; } /** * Applies view access level filtering for the specified user. Useful to * filter a front-end items listing. * * @param integer $userID The user ID to use. Skip it to use the currently logged in user. * * @return DataModel Reference to self */ public function applyAccessFiltering($userID = null) { if (!$this->hasField('access')) { return $this; } $user = $this->container->platform->getUser($userID); $accessField = $this->getFieldAlias('access'); $this->setState($accessField, $user->getAuthorisedViewLevels()); return $this; } /** * Get the content type for ucm * * @return string The content type alias * * @throws NoContentType If you have not set the contentType configuration variable */ public function getContentType() { if (!empty($this->contentType)) { return $this->contentType; } throw new NoContentType(get_class($this)); } /** * Check if a UCM content type exists for this resource, and * create it if it does not * * @param string $alias The content type alias (optional) * * @return null */ public function checkContentType($alias = null) { $contentType = new ContentType($this->getDbo()); if (!$alias) { $alias = $this->getContentType(); } $aliasParts = explode('.', $alias); // Fetch the extension name $component = $aliasParts[0]; $component = ComponentHelper::getComponent($component); // Fetch the name using the menu item $query = $this->getDbo()->getQuery(true); $query->select('title')->from('#__menu')->where('component_id = ' . (int) $component->id); $this->getDbo()->setQuery($query); $component_name = Text::_($this->getDbo()->loadResult()); $name = $component_name . ' ' . ucfirst($aliasParts[1]); // Create a new content type for our resource if (!$contentType->load(['type_alias' => $alias])) { $contentType->type_title = $name; $contentType->type_alias = $alias; $contentType->table = json_encode( [ 'special' => [ 'dbtable' => $this->getTableName(), 'key' => $this->getKeyName(), 'type' => $name, 'prefix' => $this->container->getNamespacePrefix() . '\\Model\\', 'class' => $this->getName(), 'config' => [], ], 'common' => [ 'dbtable' => '#__ucm_content', 'key' => 'ucm_id', 'type' => 'CoreContent', 'prefix' => 'JTable', 'config' => [], ], ] ); $contentType->field_mappings = json_encode( [ 'common' => [ 0 => [ "core_content_item_id" => $this->getKeyName(), "core_title" => $this->getUcmCoreAlias('title'), "core_state" => $this->getUcmCoreAlias('enabled'), "core_alias" => $this->getUcmCoreAlias('alias'), "core_created_time" => $this->getUcmCoreAlias('created_on'), "core_modified_time" => $this->getUcmCoreAlias('created_by'), "core_body" => $this->getUcmCoreAlias('body'), "core_hits" => $this->getUcmCoreAlias('hits'), "core_publish_up" => $this->getUcmCoreAlias('publish_up'), "core_publish_down" => $this->getUcmCoreAlias('publish_down'), "core_access" => $this->getUcmCoreAlias('access'), "core_params" => $this->getUcmCoreAlias('params'), "core_featured" => $this->getUcmCoreAlias('featured'), "core_metadata" => $this->getUcmCoreAlias('metadata'), "core_language" => $this->getUcmCoreAlias('language'), "core_images" => $this->getUcmCoreAlias('images'), "core_urls" => $this->getUcmCoreAlias('urls'), "core_version" => $this->getUcmCoreAlias('version'), "core_ordering" => $this->getUcmCoreAlias('ordering'), "core_metakey" => $this->getUcmCoreAlias('metakey'), "core_metadesc" => $this->getUcmCoreAlias('metadesc'), "core_catid" => $this->getUcmCoreAlias('cat_id'), "core_xreference" => $this->getUcmCoreAlias('xreference'), "asset_id" => $this->getUcmCoreAlias('asset_id'), ], ], 'special' => [ 0 => [ ], ], ] ); $ignoreFields = [ $this->getUcmCoreAlias('modified_on', null), $this->getUcmCoreAlias('modified_by', null), $this->getUcmCoreAlias('locked_by', null), $this->getUcmCoreAlias('locked_on', null), $this->getUcmCoreAlias('hits', null), $this->getUcmCoreAlias('version', null), ]; $contentType->content_history_options = json_encode( [ "ignoreChanges" => array_filter($ignoreFields, 'strlen'), ] ); $contentType->router = ''; $contentType->store(); } } /** * Set a behavior param * * @param string $name The name of the param you want to set * @param mixed $value The value to set * * @return $this Self, for chaining */ public function setBehaviorParam($name, $value) { $this->behaviorParams[$name] = $value; return $this; } /** * Get a behavior param * * @param string $name The name of the param you want to get * @param mixed $default The default value returned if not set * * @return mixed */ public function getBehaviorParam($name, $default = null) { return $this->behaviorParams[$name] ?? $default; } /** * Set or get the backlisted filters. * * Note: passing a null $list to get the filter blacklist is deprecated as of FOF 3.1. Pleas use getBlacklistFilters * instead. * * @param mixed $list A filter or list of filters to backlist. If null return the list of backlisted filter * @param boolean $reset Reset the blacklist if true * * @return null|array Return an array of value if $list is null */ public function blacklistFilters($list = null, $reset = false) { if (!isset($list)) { return $this->getBehaviorParam('blacklistFilters', []); } if (is_string($list)) { $list = (array) $list; } if (!$reset) { $list = array_unique(array_merge($this->getBehaviorParam('blacklistFilters', []), $list)); } $this->setBehaviorParam('blacklistFilters', $list); return null; } /** * Get the blacklisted filters. * * @return array */ public function getBlacklistFilters() { return $this->getBehaviorParam('blacklistFilters', []); } /** * This method is called by Joomla! itself when it needs to update the UCM content * * @return bool */ public function updateUcmContent() { // Process the tags $data = $this->getData(); $alias = $this->getContentType(); $ucmContentTable = new CoreContent(Factory::getDbo()); $ucm = new UCMContent($this, $alias); $ucmData = !empty($data) ? $ucm->mapData($data) : $ucm->ucmData; $primaryId = $ucm->getPrimaryKey($ucmData['common']['core_type_id'], $ucmData['common']['core_content_item_id']); $result = $ucmContentTable->load($primaryId); $result = $result && $ucmContentTable->bind($ucmData['common']); $result = $result && $ucmContentTable->check(); $result = $result && $ucmContentTable->store(); $ucmId = $ucmContentTable->core_content_id; return $result; } /** * Add a field to the list of fields to be ignored by the check() method * * @param string $fieldName The field to add (can be a field alias) * * @return void */ public function addSkipCheckField($fieldName) { if (!is_array($this->fieldsSkipChecks)) { $this->fieldsSkipChecks = []; } if (!$this->hasField($fieldName)) { return; } $fieldName = $this->getFieldAlias($fieldName); if (!in_array($fieldName, $this->fieldsSkipChecks)) { $this->fieldsSkipChecks[] = $fieldName; } } /** * Remove a field from the list of fields to be ignored by the check() method * * @param string $fieldName The field to remove (can be a field alias) * * @return void */ public function removeSkipCheckField($fieldName) { if (!is_array($this->fieldsSkipChecks)) { $this->fieldsSkipChecks = []; return; } if (!$this->hasField($fieldName)) { return; } $fieldName = $this->getFieldAlias($fieldName); if (in_array($fieldName, $this->fieldsSkipChecks)) { $index = array_search($fieldName, $this->fieldsSkipChecks); unset($this->fieldsSkipChecks[$index]); } } /** * Is a field present in the list of fields to be ignored by the check() method? * * @param string $fieldName The field to check (can be a field alias) * * @return bool True if the field is skipped from checks, false if not or if the field doesn't exist. */ public function hasSkipCheckField($fieldName) { if (!is_array($this->fieldsSkipChecks)) { $this->fieldsSkipChecks = []; return false; } if (!$this->hasField($fieldName)) { return false; } $fieldName = $this->getFieldAlias($fieldName); return in_array($fieldName, $this->fieldsSkipChecks); } /** * Loads the asset table related to this table. * This will help tests, too, since we can mock this function. * * @return bool|Asset False on failure, otherwise Asset */ protected function getAsset() { $name = $this->getAssetName(); // Do NOT touch Table here -- we are loading the core asset table which is a Joomla Table, not a FOF model $asset = new Asset(Factory::getDbo()); if ($asset->loadByName($name) === 0) { return false; } return $asset; } /** * Utility methods that fetches the column name for the field. * If it does not exists, returns a "null" string * * @param string $alias The alias for the column * @param string $null What to return if no column exists * * @return string The column name */ protected function getUcmCoreAlias($alias, $null = "null") { if (!$this->hasField($alias)) { return $null; } return $this->getFieldAlias($alias); } /** * Returns all lower and upper case permutations of the database prefix * * @return array */ protected function getPrefixCasePermutations() { if (empty(self::$prefixCasePermutations)) { $prefix = $this->getDbo()->getPrefix(); $suffix = ''; if (substr($prefix, -1) == '_') { $suffix = '_'; $prefix = substr($prefix, 0, -1); } $letters = str_split($prefix, 1); $permutations = ['']; foreach ($letters as $nextLetter) { $lower = strtolower($nextLetter); $upper = strtoupper($nextLetter); $ret = []; foreach ($permutations as $perm) { $ret[] = $perm . $lower; if ($lower !== $upper) { $ret[] = $perm . $upper; } $permutations = $ret; } } $permutations = array_merge([ strtolower($prefix), strtoupper($prefix), ], $permutations); $permutations = array_map(function ($x) use ($suffix) { return $x . $suffix; }, $permutations); self::$prefixCasePermutations = array_unique($permutations); } return self::$prefixCasePermutations; } } Model/DataModel/Relation/BelongsToMany.php 0000604 00000027161 15245560676 0014470 0 ustar 00 <?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\Relation; defined('_JEXEC') || die; use FOF40\Model\DataModel; use FOF40\Model\DataModel\Relation; /** * BelongsToMany (many-to-many) relation: one or more records of this model are related to one or more records in the * foreign model. * * For example, parentModel is Users and foreignModel is Groups. Each user can be assigned to many groups. Each group * can be assigned to many users. */ class BelongsToMany extends Relation { /** * Public constructor. Initialises the relation. * * @param DataModel $parentModel The data model we are attached to * @param string $foreignModelName The name of the foreign key's model in the format * "modelName@com_something" * @param string $localKey The local table key for this relation, default: parentModel's ID field * name * @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name * @param string $pivotTable For many-to-many relations, the pivot (glue) table * @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local * key * @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign * key * * @throws DataModel\Relation\Exception\PivotTableNotFound */ public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null) { parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey); if (empty($localKey)) { $this->localKey = $parentModel->getIdFieldName(); } if (empty($pivotLocalKey)) { $this->pivotLocalKey = $this->localKey; } if (empty($foreignKey)) { /** @var DataModel $foreignModel */ $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); $this->foreignKey = $foreignModel->getIdFieldName(); } if (empty($pivotForeignKey)) { $this->pivotForeignKey = $this->foreignKey; } if (empty($pivotTable)) { // Get the local model's name (e.g. "users") $localName = $parentModel->getName(); $localName = strtolower($localName); // Get the foreign model's name (e.g. "groups") if (!isset($foreignModel)) { /** @var DataModel $foreignModel */ $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); } $foreignName = $foreignModel->getName(); $foreignName = strtolower($foreignName); // Get the local model's app name $parentModelBareComponent = $parentModel->getContainer()->bareComponentName; $foreignModelBareComponent = $foreignModel->getContainer()->bareComponentName; // There are two possibilities for the table name: #__component_local_foreign or #__component_foreign_local. // There are also two possibilities for a component name (local or foreign model's) $db = $parentModel->getDbo(); $prefix = $db->getPrefix(); $tableNames = [ '#__' . strtolower($parentModelBareComponent) . '_' . $localName . '_' . $foreignName, '#__' . strtolower($parentModelBareComponent) . '_' . $foreignName . '_' . $localName, '#__' . strtolower($foreignModelBareComponent) . '_' . $localName . '_' . $foreignName, '#__' . strtolower($foreignModelBareComponent) . '_' . $foreignName . '_' . $localName, ]; $allTables = $db->getTableList(); $this->pivotTable = null; foreach ($tableNames as $tableName) { $checkName = $prefix . substr($tableName, 3); if (in_array($checkName, $allTables)) { $this->pivotTable = $tableName; } } if (empty($this->pivotTable)) { throw new DataModel\Relation\Exception\PivotTableNotFound("Pivot table for many-to-many relation between '$localName and '$foreignName' not found'"); } } } /** * Populates the internal $this->data collection from the contents of the provided collection. This is used by * DataModel to push the eager loaded data into each item's relation. * * @param DataModel\Collection $data The relation data to push into this relation * @param mixed $keyMap Passes around the local to foreign key map * * @return void */ public function setDataFromCollection(DataModel\Collection &$data, $keyMap = null) { $this->data = new DataModel\Collection(); if (!is_array($keyMap)) { return; } if (!empty($data)) { // Get the local key value $localKeyValue = $this->parentModel->getFieldValue($this->localKey); // Make sure this local key exists in the (cached) pivot table if (!isset($keyMap[$localKeyValue])) { return; } /** @var DataModel $item */ foreach ($data as $item) { // Only accept foreign items whose key is associated in the pivot table with our local key if (in_array($item->getFieldValue($this->foreignKey), $keyMap[$localKeyValue])) { $this->data->add($item); } } } } /** * Returns the count subquery for DataModel's has() and whereHas() methods. * * @param string $tableAlias The alias of the local table in the query. Leave blank to use the table's name. * * @return \JDatabaseQuery */ public function getCountSubquery($tableAlias = null) { /** @var DataModel $foreignModel */ $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); $db = $foreignModel->getDbo(); if (empty($tableAlias)) { $tableAlias = $this->parentModel->getTableName(); } return $db->getQuery(true) ->select('COUNT(*)') ->from($db->qn($foreignModel->getTableName()) . ' AS ' . $db->qn('reltbl')) ->innerJoin( $db->qn($this->pivotTable) . ' AS ' . $db->qn('pivotTable') . ' ON(' . $db->qn('pivotTable') . '.' . $db->qn($this->pivotForeignKey) . ' = ' . $db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey)) . ')' ) ->where( $db->qn('pivotTable') . '.' . $db->qn($this->pivotLocalKey) . ' =' . $db->qn($tableAlias) . '.' . $db->qn($this->parentModel->getFieldAlias($this->localKey)) ); } /** * Saves all related items. For many-to-many relations there are two things we have to do: * 1. Save all related items; and * 2. Overwrite the pivot table data with the new associations */ public function saveAll() { // Save all related items parent::saveAll(); $this->saveRelations(); } /** * Overwrite the pivot table data with the new associations */ public function saveRelations() { // Get all the new keys $newKeys = []; if ($this->data instanceof DataModel\Collection) { foreach ($this->data as $item) { if ($item instanceof DataModel) { $newKeys[] = $item->getId(); } elseif (!is_object($item)) { $newKeys[] = $item; } } } $newKeys = array_unique($newKeys); $db = $this->parentModel->getDbo(); $localKeyValue = $this->parentModel->getFieldValue($this->localKey); // Kill all existing relations in the pivot table $query = $db->getQuery(true) ->delete($db->qn($this->pivotTable)) ->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKeyValue)); $db->setQuery($query); $db->execute(); // Write the new relations to the database $protoQuery = $db->getQuery(true) ->insert($db->qn($this->pivotTable)) ->columns([$db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)]); $i = 0; $query = null; foreach ($newKeys as $key) { $i++; if (is_null($query)) { $query = clone $protoQuery; } $query->values($db->q($localKeyValue) . ', ' . $db->q($key)); if (($i % 50) == 0) { $db->setQuery($query); $db->execute(); $query = null; } } if (!is_null($query)) { $db->setQuery($query); $db->execute(); } } /** * This is not supported by the belongsTo relation * * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported */ public function getNew() { throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported for many-to-may relations. Please add/remove items from the relation data and use push() to effect changes."); } /** * Applies the relation filters to the foreign model when getData is called * * @param DataModel $foreignModel The foreign model you're operating on * @param DataModel\Collection $dataCollection If it's an eager loaded relation, the collection of loaded * parent records * * @return boolean Return false to force an empty data collection */ protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null) { $db = $this->parentModel->getDbo(); // Decide how to proceed, based on eager or lazy loading if (is_object($dataCollection)) { // Eager loaded relation if (!empty($dataCollection)) { // Get a list of local keys from the collection $values = []; /** @var $item DataModel */ foreach ($dataCollection as $item) { $v = $item->getFieldValue($this->localKey, null); if (!is_null($v)) { $values[] = $v; } } // Keep only unique values $values = array_unique($values); $values = array_map(function ($x) use (&$db) { return $db->q($x); }, $values); // Get the foreign keys from the glue table $query = $db->getQuery(true) ->select([$db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)]) ->from($db->qn($this->pivotTable)) ->where($db->qn($this->pivotLocalKey) . ' IN(' . implode(',', $values) . ')'); $db->setQuery($query); $foreignKeysUnmapped = $db->loadRowList(); $this->foreignKeyMap = []; $foreignKeys = []; foreach ($foreignKeysUnmapped as $unmapped) { $local = $unmapped[0]; $foreign = $unmapped[1]; if (!isset($this->foreignKeyMap[$local])) { $this->foreignKeyMap[$local] = []; } $this->foreignKeyMap[$local][] = $foreign; $foreignKeys[] = $foreign; } // Keep only unique values. However, the array keys are all screwed up. See below. $foreignKeys = array_unique($foreignKeys); // This looks stupid, but it's required to reset the array keys. Without it where() below fails. $foreignKeys = array_merge($foreignKeys); // Apply the filter if (!empty($foreignKeys)) { $foreignModel->where($this->foreignKey, 'in', $foreignKeys); } else { return false; } } else { return false; } } else { // Lazy loaded relation; get the single local key $localKey = $this->parentModel->getFieldValue($this->localKey, null); if (is_null($localKey) || ($localKey === '')) { return false; } $query = $db->getQuery(true) ->select($db->qn($this->pivotForeignKey)) ->from($db->qn($this->pivotTable)) ->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKey)); $db->setQuery($query); $foreignKeys = $db->loadColumn(); $this->foreignKeyMap[$localKey] = $foreignKeys; // If there are no foreign keys (no foreign items assigned to our item) we return false which then causes // the relation to return null, marking the lack of data. if (empty($foreignKeys)) { return false; } $foreignModel->where($this->foreignKey, 'in', $this->foreignKeyMap[$localKey]); } return true; } } Model/DataModel/Relation/HasMany.php 0000604 00000011631 15245560676 0013302 0 ustar 00 <?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\Relation; defined('_JEXEC') || die; use FOF40\Model\DataModel; use FOF40\Model\DataModel\Relation; /** * HasMany (1-to-many) relation: this model is a parent which has zero or more children in the foreign table * * For example, parentModel is Users and foreignModel is Articles. Each user has zero or more articles. */ class HasMany extends Relation { /** * Public constructor. Initialises the relation. * * @param DataModel $parentModel The data model we are attached to * @param string $foreignModelName The name of the foreign key's model in the format * "modelName@com_something" * @param string $localKey The local table key for this relation, default: parentModel's ID field * name * @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name * @param string $pivotTable IGNORED * @param string $pivotLocalKey IGNORED * @param string $pivotForeignKey IGNORED */ public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null) { parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey); if (empty($this->localKey)) { $this->localKey = $parentModel->getIdFieldName(); } if (empty($this->foreignKey)) { $this->foreignKey = $this->localKey; } } /** * Returns the count subquery for DataModel's has() and whereHas() methods. * * @param string $tableAlias The alias of the local table in the query. Leave blank to use the table's name. * * @return \JDatabaseQuery */ public function getCountSubquery($tableAlias = null) { // Get a model instance $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); $db = $foreignModel->getDbo(); if (empty($tableAlias)) { $tableAlias = $this->parentModel->getTableName(); } return $db->getQuery(true) ->select('COUNT(*)') ->from($db->qn($foreignModel->getTableName(), 'reltbl')) ->where($db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey)) . ' = ' . $db->qn($tableAlias) . '.' . $db->qn($this->parentModel->getFieldAlias($this->localKey))); } /** * Returns a new item of the foreignModel type, pre-initialised to fulfil this relation * * @return DataModel * * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported */ public function getNew() { // Get a model instance $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); // Prime the model $foreignModel->setFieldValue($this->foreignKey, $this->parentModel->getFieldValue($this->localKey)); // Make sure we do have a data list if (!($this->data instanceof DataModel\Collection)) { $this->getData(); } // Add the model to the data list $this->data->add($foreignModel); return $this->data->last(); } /** * Applies the relation filters to the foreign model when getData is called * * @param DataModel $foreignModel The foreign model you're operating on * @param DataModel\Collection $dataCollection If it's an eager loaded relation, the collection of loaded * parent records * * @return boolean Return false to force an empty data collection */ protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null) { // Decide how to proceed, based on eager or lazy loading if (is_object($dataCollection)) { // Eager loaded relation if (!empty($dataCollection)) { // Get a list of local keys from the collection $values = []; /** @var $item DataModel */ foreach ($dataCollection as $item) { $v = $item->getFieldValue($this->localKey, null); if (!is_null($v)) { $values[] = $v; } } // Keep only unique values. This double step is required to re-index the array and avoid issues with // Joomla Registry class. See issue #681 $values = array_values(array_unique($values)); // Apply the filter if (!empty($values)) { $foreignModel->where($this->foreignKey, 'in', $values); } else { return false; } } else { return false; } } else { // Lazy loaded relation; get the single local key $localKey = $this->parentModel->getFieldValue($this->localKey, null); if (is_null($localKey) || ($localKey === '')) { return false; } $foreignModel->where($this->foreignKey, '==', $localKey); } return true; } } Model/DataModel/Relation/HasOne.php 0000604 00000002551 15245560676 0013120 0 ustar 00 <?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\Relation; defined('_JEXEC') || die; use FOF40\Model\DataModel; use FOF40\Model\DataModel\Collection; /** * HasOne (straight 1-to-1) relation: this model is a parent which has exactly one child in the foreign table * * For example, parentModel is Users and foreignModel is Phones. Each uses has exactly one Phone. */ class HasOne extends HasMany { /** * Get the relation data. * * If you want to apply additional filtering to the foreign model, use the $callback. It can be any function, * static method, public method or closure with an interface of function(DataModel $foreignModel). You are not * supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do: * $foreignModel->setState('foo', 'bar') * * @param callable $callback The callback to run on the remote model. * @param Collection $dataCollection * * @return Collection|DataModel */ public function getData($callback = null, Collection $dataCollection = null) { if (is_null($dataCollection)) { return parent::getData($callback, $dataCollection)->first(); } else { return parent::getData($callback, $dataCollection); } } } Model/DataModel/Relation/BelongsTo.php 0000604 00000004617 15245560676 0013644 0 ustar 00 <?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\Relation; defined('_JEXEC') || die; use FOF40\Model\DataModel; /** * BelongsTo (reverse 1-to-1 or 1-to-many) relation: this model is a child which belongs to the foreign table * * For example, parentModel is Articles and foreignModel is Users. Each article belongs to one user. One user can have * one or more article. * * Example #2: parentModel is Phones and foreignModel is Users. Each phone belongs to one user. One user can have zero * or one phones. */ class BelongsTo extends HasOne { /** * Public constructor. Initialises the relation. * * @param DataModel $parentModel The data model we are attached to * @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something" * @param string $localKey The local table key for this relation, default: parentModel's ID field name * @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name * @param string $pivotTable IGNORED * @param string $pivotLocalKey IGNORED * @param string $pivotForeignKey IGNORED */ public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null) { parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey); if (empty($localKey)) { /** @var DataModel $foreignModel */ $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); $this->localKey = $foreignModel->getIdFieldName(); } if (empty($foreignKey)) { if (!isset($foreignModel)) { /** @var DataModel $foreignModel */ $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); } $this->foreignKey = $foreignModel->getIdFieldName(); } } /** * This is not supported by the belongsTo relation * * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported */ public function getNew() { throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported by the belongsTo relation type"); } } Model/DataModel/Relation/Exception/ForeignModelNotFound.php 0000604 00000000454 15245560676 0017730 0 ustar 00 <?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\Relation\Exception; defined('_JEXEC') || die; class ForeignModelNotFound extends \Exception {} Model/DataModel/Relation/Exception/RelationTypeNotFound.php 0000604 00000000454 15245560676 0017775 0 ustar 00 <?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\Relation\Exception; defined('_JEXEC') || die; class RelationTypeNotFound extends \Exception {} Model/DataModel/Relation/Exception/RelationNotFound.php 0000604 00000000450 15245560676 0017127 0 ustar 00 <?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\Relation\Exception; defined('_JEXEC') || die; class RelationNotFound extends \Exception {} Model/DataModel/Relation/Exception/PivotTableNotFound.php 0000604 00000000452 15245560676 0017425 0 ustar 00 <?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\Relation\Exception; defined('_JEXEC') || die; class PivotTableNotFound extends \Exception {} Model/DataModel/Relation/Exception/SaveNotSupported.php 0000604 00000000450 15245560676 0017162 0 ustar 00 <?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\Relation\Exception; defined('_JEXEC') || die; class SaveNotSupported extends \Exception {} Model/DataModel/Relation/Exception/NewNotSupported.php 0000604 00000000450 15245560676 0017015 0 ustar 00 <?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\Relation\Exception; defined('_JEXEC') || die; class NewNotSupported extends \Exception { } Model/DataModel/Exception/TreeInvalidLftRgt.php 0000604 00000000724 15245560676 0015455 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; abstract class TreeInvalidLftRgt extends \RuntimeException { public function __construct( $message = '', $code = 500, Exception $previous = null ) { parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeIncompatibleTable.php 0000604 00000001111 15245560676 0016311 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeIncompatibleTable extends \UnexpectedValueException { public function __construct( $tableName, $code = 500, Exception $previous = null ) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_INCOMPATIBLETABLE', $tableName); parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeInvalidLftRgtCurrent.php 0000604 00000001131 15245560676 0017011 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeInvalidLftRgtCurrent extends TreeInvalidLftRgt { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_CURRENT'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeInvalidLftRgtParent.php 0000604 00000001127 15245560676 0016625 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeInvalidLftRgtParent extends TreeInvalidLftRgt { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_PARENT'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/RecordNotLoaded.php 0000604 00000001076 15245560676 0015135 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class RecordNotLoaded extends BaseException { public function __construct( $message = "", $code = 404, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_COULDNOTLOAD'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeInvalidLftRgtSibling.php 0000604 00000001131 15245560676 0016756 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeInvalidLftRgtSibling extends TreeInvalidLftRgt { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_SIBLING'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeUnexpectedPrimaryKey.php 0000604 00000001130 15245560676 0017055 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeUnexpectedPrimaryKey extends \UnexpectedValueException { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_TREE_UNEXPECTEDPK'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/InvalidSearchMethod.php 0000604 00000000447 15245560676 0016003 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; class InvalidSearchMethod extends BaseException { } Model/DataModel/Exception/TreeRootNotFound.php 0000604 00000001076 15245560676 0015345 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeRootNotFound extends \RuntimeException { public function __construct($tableName, $lft, $code = 500, Exception $previous = null) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_ROOTNOTFOUND', $tableName, $lft); parent::__construct($message, $code, $previous); } } Model/DataModel/Exception/NoItemsFound.php 0000604 00000001052 15245560676 0014471 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class NoItemsFound extends BaseException { public function __construct( $className, $code = 404, Exception $previous = null ) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_NOITEMSFOUND', $className); parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/CannotLockNotLoadedRecord.php 0000604 00000001125 15245560676 0017104 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class CannotLockNotLoadedRecord extends BaseException { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_CANNOTLOCKNOTLOADEDRECORD'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeInvalidLftRgtOther.php 0000604 00000001125 15245560676 0016453 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeInvalidLftRgtOther extends TreeInvalidLftRgt { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_OTHER'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/NoTableColumns.php 0000604 00000000442 15245560676 0015006 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; class NoTableColumns extends BaseException { } Model/DataModel/Exception/NoAssetKey.php 0000604 00000001103 15245560676 0014141 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class NoAssetKey extends \UnexpectedValueException { public function __construct( $message = '', $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_NOASSETKEY'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/BaseException.php 0000604 00000000445 15245560676 0014655 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; class BaseException extends \RuntimeException { } Model/DataModel/Exception/SpecialColumnMissing.php 0000604 00000000450 15245560676 0016210 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; class SpecialColumnMissing extends BaseException { } Model/DataModel/Exception/NoContentType.php 0000604 00000001070 15245560676 0014670 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class NoContentType extends \UnexpectedValueException { public function __construct( $className, $code = 500, Exception $previous = null ) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_NOCONTENTTYPE', $className); parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeUnsupportedMethod.php 0000604 00000001076 15245560676 0016436 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeUnsupportedMethod extends \LogicException { public function __construct( $method = '', $code = 500, Exception $previous = null ) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_UNSUPPORTEDMETHOD', $method); parent::__construct( $message, $code, $previous ); } } Model/DataModel/Exception/TreeMethodOnlyAllowedInRoot.php 0000604 00000001077 15245560676 0017473 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class TreeMethodOnlyAllowedInRoot extends \RuntimeException { public function __construct( $method = '', $code = 500, Exception $previous = null ) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_ONLYINROOT', $method); parent::__construct( $message, $code, $previous ); } } Model/DataModel/Filter/Boolean.php 0000604 00000000760 15245560676 0012772 0 ustar 00 <?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\Filter; defined('_JEXEC') || die; class Boolean extends Number { /** * Is it a null or otherwise empty value? * * @param mixed $value The value to test for emptiness * * @return bool */ public function isEmpty($value) { return is_null($value) || ($value === ''); } } Model/DataModel/Filter/AbstractFilter.php 0000604 00000023074 15245560676 0014327 0 ustar 00 <?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\Filter; defined('_JEXEC') || die; use FOF40\Model\DataModel\Filter\Exception\InvalidFieldObject; use FOF40\Model\DataModel\Filter\Exception\NoDatabaseObject; abstract class AbstractFilter { /** * The null value for this type * * @var mixed */ public $null_value; protected $db; /** * The column name of the table field * * @var string */ protected $name = ''; /** * The column type of the table field * * @var string */ protected $type = ''; /** * Should I allow filtering against the number 0? * * @var bool */ protected $filterZero = true; /** * Prefix each table name with this table alias. For example, field bar normally creates a WHERE clause: * `bar` = '1' * If tableAlias is set to "foo" then the WHERE clause it generates becomes * `foo`.`bar` = '1' * * @var null */ protected $tableAlias; /** * Constructor * * @param \JDatabaseDriver $db The database object * @param object $field The field information as taken from the db */ public function __construct($db, $field) { $this->db = $db; if (!is_object($field) || !isset($field->name) || !isset($field->type)) { throw new InvalidFieldObject; } $this->name = $field->name; $this->type = $field->type; if (isset ($field->filterZero)) { $this->filterZero = $field->filterZero; } if (isset ($field->tableAlias)) { $this->tableAlias = $field->tableAlias; } } /** * Creates a field Object based on the field column type * * @param object $field The field information * @param array $config The field configuration (like the db object to use) * * @return AbstractFilter The Filter object * * @throws \InvalidArgumentException */ public static function getField($field, $config = []) { if (!is_object($field) || !isset($field->name) || !isset($field->type)) { throw new InvalidFieldObject; } $type = $field->type; $classType = self::getFieldType($type); $className = '\\FOF40\\Model\\DataModel\\Filter\\' . ucfirst($classType); if (($classType !== false) && class_exists($className, true)) { if (!isset($config['dbo'])) { throw new NoDatabaseObject($className); } $db = $config['dbo']; return new $className($db, $field); } return null; } /** * Get the class name based on the field Type * * @param string $type The type of the field * * @return string the class name suffix */ public static function getFieldType($type) { // Remove parentheses, indicating field options / size (they don't matter in type detection) if (!empty($type)) { [$type,] = explode('(', $type); } $detectedType = null; switch (trim($type)) { case 'varchar': case 'text': case 'smalltext': case 'longtext': case 'char': case 'mediumtext': case 'character varying': case 'nvarchar': case 'nchar': $detectedType = 'Text'; break; case 'date': case 'datetime': case 'time': case 'year': case 'timestamp': case 'timestamp without time zone': case 'timestamp with time zone': $detectedType = 'Date'; break; case 'tinyint': case 'smallint': $detectedType = 'Boolean'; break; } // Sometimes we have character types followed by a space and some cruft. Let's handle them. if (is_null($detectedType) && !empty($type)) { [$type,] = explode(' ', $type); switch (trim($type)) { case 'varchar': case 'text': case 'smalltext': case 'longtext': case 'char': case 'mediumtext': case 'nvarchar': case 'nchar': $detectedType = 'Text'; break; case 'date': case 'datetime': case 'time': case 'year': case 'timestamp': $detectedType = 'Date'; break; case 'tinyint': case 'smallint': $detectedType = 'Boolean'; break; default: $detectedType = 'Number'; break; } } // If all else fails assume it's a Number and hope for the best if (empty($detectedType)) { $detectedType = 'Number'; } return $detectedType; } /** * Is it a null or otherwise empty value? * * @param mixed $value The value to test for emptiness * * @return boolean */ public function isEmpty($value) { return (($value === $this->null_value) || empty($value)) && !($this->filterZero && ($value === "0")); } /** * Returns the default search method for a field. This always returns 'exact' * and you are supposed to override it in specialised classes. The possible * values are exact, partial, between and outside, unless something * different is returned by getSearchMethods(). * * @return string * @see self::getSearchMethods() * */ public function getDefaultSearchMethod() { return 'exact'; } /** * Return the search methods available for this field class, * * @return array */ public function getSearchMethods() { $ignore = [ 'isEmpty', 'getField', 'getFieldType', '__construct', 'getDefaultSearchMethod', 'getSearchMethods', 'getFieldName', ]; $class = new \ReflectionClass(__CLASS__); $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); $tmp = []; foreach ($methods as $method) { $tmp[] = $method->name; } $methods = $tmp; if ($methods = array_diff($methods, $ignore)) { return $methods; } return []; } /** * Perform an exact match (equality matching) * * @param mixed $value The value to compare to * * @return string The SQL where clause for this search */ public function exact($value) { if ($this->isEmpty($value)) { return ''; } if (is_array($value)) { $db = $this->db; $value = array_map([$db, 'quote'], $value); return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))'; } else { return $this->search($value); } } /** * Perform a partial match (usually: search in string) * * @param mixed $value The value to compare to * * @return string The SQL where clause for this search */ abstract public function partial($value); /** * Perform a between limits match (usually: search for a value between * two numbers or a date between two preset dates). When $include is true * the condition tested is: * $from <= VALUE <= $to * When $include is false the condition tested is: * $from < VALUE < $to * * @param mixed $from The lowest value to compare to * @param mixed $to The highest value to compare to * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ abstract public function between($from, $to, $include = true); /** * Perform an outside limits match (usually: search for a value outside an * area or a date outside a preset period). When $include is true * the condition tested is: * (VALUE <= $from) || (VALUE >= $to) * When $include is false the condition tested is: * (VALUE < $from) || (VALUE > $to) * * @param mixed $from The lowest value of the excluded range * @param mixed $to The highest value of the excluded range * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ abstract public function outside($from, $to, $include = false); /** * Perform an interval search (usually: a date interval check) * * @param string $from The value to search * @param string|array|object $interval The interval * * @return string The SQL where clause for this search */ abstract public function interval($from, $interval); /** * Perform a between limits match (usually: search for a value between * two numbers or a date between two preset dates). When $include is true * the condition tested is: * $from <= VALUE <= $to * When $include is false the condition tested is: * $from < VALUE < $to * * @param mixed $from The lowest value to compare to * @param mixed $to The higherst value to compare to * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ abstract public function range($from, $to, $include = true); /** * Perform an modulo search * * @param integer|float $from The starting value of the search space * @param integer|float $interval The interval period of the search space * @param boolean $include Should I include the boundaries in the search? * * @return string The SQL where clause */ abstract public function modulo($from, $interval, $include = true); /** * Return the SQL where clause for a search * * @param mixed $value The value to search for * @param string $operator The operator to use * * @return string The SQL where clause for this search */ public function search($value, $operator = '=') { if ($this->isEmpty($value)) { return ''; } $prefix = ''; if (substr($operator, 0, 1) == '!') { $prefix = 'NOT '; $operator = substr($operator, 1); } return $prefix . '(' . $this->getFieldName() . ' ' . $operator . ' ' . $this->db->quote($value) . ')'; } /** * Get the field name * * @return string The field name */ public function getFieldName() { $name = $this->db->qn($this->name); if (!empty($this->tableAlias)) { $name = $this->db->qn($this->tableAlias) . '.' . $name; } return $name; } } Model/DataModel/Filter/Date.php 0000604 00000011677 15245560676 0012301 0 ustar 00 <?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\Filter; defined('_JEXEC') || die; class Date extends Text { /** * Returns the default search method for this field. * * @return string */ public function getDefaultSearchMethod() { return 'exact'; } /** * Perform a between limits match. When $include is true * the condition tested is: * $from <= VALUE <= $to * When $include is false the condition tested is: * $from < VALUE < $to * * @param mixed $from The lowest value to compare to * @param mixed $to The highest value to compare to * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ public function between($from, $to, $include = true) { if ($this->isEmpty($from) || $this->isEmpty($to)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ') AND '; return $sql . ('(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . '))'); } /** * Perform an outside limits match. When $include is true * the condition tested is: * (VALUE <= $from) || (VALUE >= $to) * When $include is false the condition tested is: * (VALUE < $from) || (VALUE > $to) * * @param mixed $from The lowest value of the excluded range * @param mixed $to The highest value of the excluded range * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ public function outside($from, $to, $include = false) { if ($this->isEmpty($from) || $this->isEmpty($to)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($from) . ') AND '; return $sql . ('(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($to) . '))'); } /** * Interval date search * * @param string $value The value to search * @param string|array|object $interval The interval. Can be (+1 MONTH or array('value' => 1, 'unit' => * 'MONTH', 'sign' => '+')) * @param boolean $include If the borders should be included * * @return string the sql string */ public function interval($value, $interval, $include = true) { if ($this->isEmpty($value) || $this->isEmpty($interval)) { return ''; } $interval = $this->getInterval($interval); // Sanity check on $interval array if (!isset($interval['sign']) || !isset($interval['value']) || !isset($interval['unit'])) { return ''; } $function = $interval['sign'] == '+' ? 'DATE_ADD' : 'DATE_SUB'; $extra = ''; if ($include) { $extra = '='; } $sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $function; return $sql . ('(' . $this->getFieldName() . ', INTERVAL ' . $interval['value'] . ' ' . $interval['unit'] . '))'); } /** * Perform a between limits match. When $include is true * the condition tested is: * $from <= VALUE <= $to * When $include is false the condition tested is: * $from < VALUE < $to * * @param mixed $from The lowest value to compare to * @param mixed $to The highest value to compare to * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ public function range($from, $to, $include = true) { if ($this->isEmpty($from) && $this->isEmpty($to)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $sql = []; if ($from) { $sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ')'; } if ($to) { $sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . ')'; } return '(' . implode(' AND ', $sql) . ')'; } /** * Parses an interval –which may be given as a string, array or object– into * a standardised hash array that can then be used bu the interval() method. * * @param string|array|object $interval The interval expression to parse * * @return array The parsed, hash array form of the interval */ protected function getInterval($interval) { if (is_string($interval)) { if (strlen($interval) > 2) { $interval = explode(" ", $interval); $sign = ($interval[0] == '-') ? '-' : '+'; $value = (int) substr($interval[0], 1); $interval = [ 'unit' => $interval[1], 'value' => $value, 'sign' => $sign, ]; } else { $interval = [ 'unit' => 'MONTH', 'value' => 1, 'sign' => '+', ]; } } else { $interval = (array) $interval; } return $interval; } } Model/DataModel/Filter/Text.php 0000604 00000006211 15245560676 0012334 0 ustar 00 <?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\Filter; defined('_JEXEC') || die; class Text extends AbstractFilter { /** * Constructor * * @param \JDatabaseDriver $db The database object * @param object $field The field information as taken from the db */ public function __construct($db, $field) { parent::__construct($db, $field); $this->null_value = ''; } /** * Returns the default search method for this field. * * @return string */ public function getDefaultSearchMethod() { return 'partial'; } /** * Perform a partial match (search in string) * * @param mixed $value The value to compare to * * @return string The SQL where clause for this search */ public function partial($value) { if ($this->isEmpty($value)) { return ''; } return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote('%' . $value . '%') . ')'; } /** * Perform an exact match (match string) * * @param mixed $value The value to compare to * * @return string The SQL where clause for this search */ public function exact($value) { if ($this->isEmpty($value)) { return ''; } if (is_array($value) || is_object($value)) { $value = (array) $value; $db = $this->db; $value = array_map([$db, 'quote'], $value); return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))'; } return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote($value) . ')'; } /** * Dummy method; this search makes no sense for text fields * * @param mixed $from Ignored * @param mixed $to Ignored * @param boolean $include Ignored * * @return string Empty string */ public function between($from, $to, $include = true) { return ''; } /** * Dummy method; this search makes no sense for text fields * * @param mixed $from Ignored * @param mixed $to Ignored * @param boolean $include Ignored * * @return string Empty string */ public function outside($from, $to, $include = false) { return ''; } /** * Dummy method; this search makes no sense for text fields * * @param mixed $value Ignored * @param mixed $interval Ignored * @param boolean $include Ignored * * @return string Empty string */ public function interval($value, $interval, $include = true) { return ''; } /** * Dummy method; this search makes no sense for text fields * * @param mixed $from Ignored * @param mixed $to Ignored * @param boolean $include Ignored * * @return string Empty string */ public function range($from, $to, $include = false) { return ''; } /** * Dummy method; this search makes no sense for text fields * * @param mixed $from Ignored * @param mixed $interval Ignored * @param boolean $include Ignored * * @return string Empty string */ public function modulo($from, $interval, $include = false) { return ''; } } Model/DataModel/Filter/Exception/InvalidFieldObject.php 0000604 00000001133 15245560676 0017025 0 ustar 00 <?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\Filter\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class InvalidFieldObject extends \InvalidArgumentException { public function __construct( $message = "", $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_FILTER_INVALIDFIELD'); } parent::__construct( $message, $code, $previous ); } } Model/DataModel/Filter/Exception/NoDatabaseObject.php 0000604 00000001106 15245560676 0016474 0 ustar 00 <?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\Filter\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; class NoDatabaseObject extends \InvalidArgumentException { public function __construct( $fieldType, $code = 500, Exception $previous = null ) { $message = Text::sprintf('LIB_FOF40_MODEL_ERR_FILTER_NODBOBJECT', $fieldType); parent::__construct( $message, $code, $previous ); } } Model/DataModel/Filter/Relation.php 0000604 00000001350 15245560676 0013164 0 ustar 00 <?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\Filter; defined('_JEXEC') || die; class Relation extends Number { /** @var \JDatabaseQuery The COUNT subquery to filter by */ protected $subQuery; public function __construct($db, $relationName, $subQuery) { $field = (object)array( 'name' => $relationName, 'type' => 'relation', ); parent::__construct($db, $field); $this->subQuery = $subQuery; } public function callback($value) { return call_user_func($value, $this->subQuery); } public function getFieldName() { return '(' . $this->subQuery . ')'; } } Model/DataModel/Filter/Number.php 0000604 00000015721 15245560676 0012646 0 ustar 00 <?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\Filter; defined('_JEXEC') || die; class Number extends AbstractFilter { /** * The partial match is mapped to an exact match * * @param mixed $value The value to compare to * * @return string The SQL where clause for this search */ public function partial($value) { return $this->exact($value); } /** * Perform a between limits match. When $include is true * the condition tested is: * $from <= VALUE <= $to * When $include is false the condition tested is: * $from < VALUE < $to * * @param mixed $from The lowest value to compare to * @param mixed $to The highest value to compare to * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ public function between($from, $to, $include = true) { $from = (float) $from; $to = (float) $to; if ($this->isEmpty($from) || $this->isEmpty($to)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $from = $this->sanitiseValue($from); $to = $this->sanitiseValue($to); $sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND '; return $sql . ('(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))'); } /** * Perform an outside limits match. When $include is true * the condition tested is: * (VALUE <= $from) || (VALUE >= $to) * When $include is false the condition tested is: * (VALUE < $from) || (VALUE > $to) * * @param mixed $from The lowest value of the excluded range * @param mixed $to The highest value of the excluded range * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ public function outside($from, $to, $include = false) { $from = (float) $from; $to = (float) $to; if ($this->isEmpty($from) || $this->isEmpty($to)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $from = $this->sanitiseValue($from); $to = $this->sanitiseValue($to); $sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $from . ') OR '; return $sql . ('(' . $this->getFieldName() . ' >' . $extra . ' ' . $to . '))'); } /** * Perform an interval match. It's similar to a 'between' match, but the * from and to values are calculated based on $value and $interval: * $value - $interval < VALUE < $value + $interval * * @param integer|float $value The center value of the search space * @param integer|float $interval The width of the search space * @param boolean $include Should I include the boundaries in the search? * * @return string The SQL where clause */ public function interval($value, $interval, $include = true) { if ($this->isEmpty($value)) { return ''; } // Convert them to float, just to be sure $value = (float) $value; $interval = (float) $interval; $from = $value - $interval; $to = $value + $interval; $extra = ''; if ($include) { $extra = '='; } $from = $this->sanitiseValue($from); $to = $this->sanitiseValue($to); $sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND '; return $sql . ('(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))'); } /** * Perform a range limits match. When $include is true * the condition tested is: * $from <= VALUE <= $to * When $include is false the condition tested is: * $from < VALUE < $to * * @param mixed $from The lowest value to compare to * @param mixed $to The highest value to compare to * @param boolean $include Should we include the boundaries in the search? * * @return string The SQL where clause for this search */ public function range($from, $to, $include = true) { if ($this->isEmpty($from) && $this->isEmpty($to)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $sql = []; if ($from) { $sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ')'; } if ($to) { $sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . ')'; } return '(' . implode(' AND ', $sql) . ')'; } /** * Perform an interval match. It's similar to a 'between' match, but the * from and to values are calculated based on $value and $interval: * $value - $interval < VALUE < $value + $interval * * @param integer|float $value The starting value of the search space * @param integer|float $interval The interval period of the search space * @param boolean $include Should I include the boundaries in the search? * * @return string The SQL where clause */ public function modulo($value, $interval, $include = true) { if ($this->isEmpty($value) || $this->isEmpty($interval)) { return ''; } $extra = ''; if ($include) { $extra = '='; } $sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $value . ' AND '; return $sql . ('(' . $this->getFieldName() . ' - ' . $value . ') % ' . $interval . ' = 0)'); } /** * Overrides the parent to handle floats in locales where the decimal separator is a comma instead of a dot * * @param mixed $value * @param string $operator * * @return string */ public function search($value, $operator = '=') { $value = $this->sanitiseValue($value); return parent::search($value, $operator); } /** * Sanitises float values. Really ugly and desperate workaround. Read below. * * Some locales, such as el-GR, use a comma as the decimal separator. This means that $x = 1.23; echo (string) $x; * will yield 1,23 (with a comma!) instead of 1.23 (with a dot!). This affects the way the SQL WHERE clauses are * generated. All database servers expect a dot as the decimal separator. If they see a decimal with a comma as the * separator they throw a SQL error. * * This method will try to replace commas with dots. I tried working around this with locale switching and the %F * (capital F) format option in sprintf to no avail. I'm pretty sure I was doing something wrong, but I ran out of * time trying to find an academically correct solution. The current implementation of sanitiseValue is a silly * hack around the problem. If you have a proper –and better performing– solution please send in a PR and I'll put * it to the test. * * @param mixed $value A string representing a number, integer, float or array of them. * * @return mixed The sanitised value, or null if the input wasn't numeric. */ public function sanitiseValue($value) { if (!is_numeric($value) && !is_string($value) && !is_array($value)) { $value = null; } if (!is_array($value)) { return str_replace(',', '.', (string) $value); } return array_map([$this, 'sanitiseValue'], $value); } } Model/DataModel/Collection.php 0000604 00000015506 15245560676 0012265 0 ustar 00 <?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; defined('_JEXEC') || die; use FOF40\Model\DataModel; use FOF40\Utils\Collection as BaseCollection; /** * A collection of data models. You can enumerate it like an array, use it everywhere a collection is expected (e.g. a * foreach loop) and even implements a countable interface. You can also batch-apply DataModel methods on it thanks to * its magic __call() method, hence the type-hinting below. * * @method void setFieldValue(string $name, mixed $value = '') * @method void archive() * @method void save(mixed $data, string $orderingFilter = '', bool $ignore = null) * @method void push(mixed $data, string $orderingFilter = '', bool $ignore = null, array $relations = null) * @method void bind(mixed $data, array $ignore = []) * @method void check() * @method void reorder(string $where = '') * @method void delete(mixed $id = null) * @method void trash(mixed $id) * @method void forceDelete(mixed $id = null) * @method void lock(int $userId = null) * @method void move(int $delta, string $where = '') * @method void publish() * @method void restore(mixed $id) * @method void touch(int $userId = null) * @method void unlock() * @method void unpublish() */ class Collection extends BaseCollection { /** * Find a model in the collection by key. * * @param mixed $key * @param mixed $default * * @return DataModel */ public function find($key, $default = null) { if ($key instanceof DataModel) { $key = $key->getId(); } return array_first($this->items, function ($itemKey, $model) use ($key) { /** @var DataModel $model */ return $model->getId() == $key; }, $default); } /** * Remove an item in the collection by key * * @param mixed $key * * @return void */ public function removeById($key) { if ($key instanceof DataModel) { $key = $key->getId(); } $index = array_search($key, $this->modelKeys()); if ($index !== false) { unset($this->items[$index]); } } /** * Add an item to the collection. * * @param mixed $item * * @return Collection */ public function add($item) { $this->items[] = $item; return $this; } /** * Determine if a key exists in the collection. * * @param mixed $key * * @return bool */ public function contains($key) { return !is_null($this->find($key)); } /** * Fetch a nested element of the collection. * * @param string $key * * @return Collection */ public function fetch(string $key): BaseCollection { return new static(array_fetch($this->toArray(), $key)); } /** * Get the max value of a given key. * * @param string $key * * @return mixed */ public function max($key) { return $this->reduce(function ($result, $item) use ($key) { return (is_null($result) || $item->{$key} > $result) ? $item->{$key} : $result; }); } /** * Get the min value of a given key. * * @param string $key * * @return mixed */ public function min($key) { return $this->reduce(function ($result, $item) use ($key) { return (is_null($result) || $item->{$key} < $result) ? $item->{$key} : $result; }); } /** * Get the array of primary keys * * @return array */ public function modelKeys() { return array_map( function ($m) { /** @var DataModel $m */ return $m->getId(); }, $this->items); } /** * Merge the collection with the given items. * * @param BaseCollection|array $collection * * @return BaseCollection */ public function merge($collection): BaseCollection { $dictionary = $this->getDictionary($this); foreach ($collection as $item) { $dictionary[$item->getId()] = $item; } return new static(array_values($dictionary)); } /** * Diff the collection with the given items. * * @param BaseCollection|array $collection * * @return BaseCollection */ public function diff($collection): BaseCollection { $diff = new static; $dictionary = $this->getDictionary($collection); foreach ($this->items as $item) { /** @var DataModel $item */ if (!isset($dictionary[$item->getId()])) { $diff->add($item); } } return $diff; } /** * Intersect the collection with the given items. * * @param BaseCollection|array $collection * * @return Collection */ public function intersect($collection): BaseCollection { $intersect = new static; $dictionary = $this->getDictionary($collection); foreach ($this->items as $item) { /** @var DataModel $item */ if (isset($dictionary[$item->getId()])) { $intersect->add($item); } } return $intersect; } /** * Return only unique items from the collection. * * @return BaseCollection */ public function unique(): BaseCollection { $dictionary = $this->getDictionary($this); return new static(array_values($dictionary)); } /** * Get a base Support collection instance from this collection. * * @return BaseCollection */ public function toBase() { return new BaseCollection($this->items); } /** * Magic method which allows you to run a DataModel method to all items in the collection. * * For example, you can do $collection->save('foobar' => 1) to update the 'foobar' column to 1 across all items in * the collection. * * IMPORTANT: The return value of the method call is not returned back to you! * * @param string $name The method to call * @param array $arguments The arguments to the method */ public function __call($name, $arguments) { if (count($this) === 0) { return; } $class = get_class($this->first()); if (method_exists($class, $name)) { foreach ($this as $item) { switch (count($arguments)) { case 0: $item->$name(); break; case 1: $item->$name($arguments[0]); break; case 2: $item->$name($arguments[0], $arguments[1]); break; case 3: $item->$name($arguments[0], $arguments[1], $arguments[2]); break; case 4: $item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]); break; case 5: $item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]); break; case 6: $item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]); break; default: call_user_func_array([$item, $name], $arguments); break; } } } } /** * Get a dictionary keyed by primary keys. * * @param BaseCollection $collection * * @return array */ protected function getDictionary($collection) { $dictionary = []; foreach ($collection as $value) { $dictionary[$value->getId()] = $value; } return $dictionary; } } Model/DataModel/Behaviour/ContentHistory.php 0000604 00000004244 15245560676 0015107 0 ustar 00 <?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(); } } Model/DataModel/Behaviour/Modified.php 0000604 00000003623 15245560676 0013633 0 ustar 00 <?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'); } } } Model/DataModel/Behaviour/EmptyNonZero.php 0000604 00000001636 15245560676 0014526 0 ustar 00 <?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); } } Model/DataModel/Behaviour/Assets.php 0000604 00000011362 15245560676 0013354 0 ustar 00 <?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; } } Model/DataModel/Behaviour/RelationFilters.php 0000604 00000004360 15245560676 0015220 0 ustar 00 <?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 RelationFilters 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 based on model relations. * * @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) { $relationFilters = $model->getRelationFilters(); foreach ($relationFilters as $filterState) { $relationName = $filterState['relation']; $tableAlias = $model->getBehaviorParam('tableAlias', null); $subQuery = $model->getRelations()->getCountSubquery($relationName, $tableAlias); // Callback method needs different handling if (isset($filterState['method']) && ($filterState['method'] == 'callback')) { call_user_func_array($filterState['value'], array(&$subQuery)); $filterState['method'] = 'search'; $filterState['operator'] = '>='; $filterState['value'] = '1'; } $options = new Registry($filterState); $filter = new DataModel\Filter\Relation($model->getDbo(), $relationName, $subQuery); $methods = $filter->getSearchMethods(); $method = $options->get('method', $filter->getDefaultSearchMethod()); if (!in_array($method, $methods)) { $method = 'exact'; } switch ($method) { case 'between': case 'outside': $sql = $filter->$method($options->get('from', null), $options->get('to')); break; case 'interval': $sql = $filter->$method($options->get('value', null), $options->get('interval')); break; case 'search': $sql = $filter->$method($options->get('value', null), $options->get('operator', '=')); break; default: $sql = $filter->$method($options->get('value', null)); break; } if ($sql) { $query->where($sql); } } } } Model/DataModel/Behaviour/Own.php 0000604 00000004012 15245560676 0012647 0 ustar 00 <?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 owned by the currently logged in user only * * @since 2.1 */ class Own 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('created_by')) { return; } // Get the current user's id $user_id = $model->getContainer()->platform->getUser()->id; // And filter the query output by the user id $db = $model->getContainer()->platform->getDbo(); $query->where($db->qn($model->getFieldAlias('created_by')) . ' = ' . $db->q($user_id)); } /** * 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('created_by')) { return; } // Get the user $user_id = $model->getContainer()->platform->getUser()->id; $recordUser = $model->getFieldValue('created_by', null); // Filter by authorised access levels if ($recordUser != $user_id) { $model->reset(true); } } } Model/DataModel/Behaviour/Created.php 0000604 00000004050 15245560676 0013455 0 ustar 00 <?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'); } } } } Model/DataModel/Behaviour/Language.php 0000604 00000011131 15245560676 0013627 0 ustar 00 <?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(); } } } Model/DataModel/Behaviour/Filters.php 0000604 00000006621 15245560676 0013524 0 ustar 00 <?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); } } } } Model/DataModel/Behaviour/Access.php 0000604 00000003447 15245560676 0013320 0 ustar 00 <?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); } } } Model/DataModel/Behaviour/Enabled.php 0000604 00000003342 15245560676 0013443 0 ustar 00 <?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); } } } Model/DataModel/Behaviour/PageParametersToState.php 0000604 00000003324 15245560676 0016315 0 ustar 00 <?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\Application\SiteApplication; use Joomla\CMS\Factory as JoomlaFactory; use Joomla\Registry\Registry; /** * FOF model behavior class to populate the state with the front-end page parameters * * @since 2.1 */ class PageParametersToState extends Observer { public function onAfterConstruct(DataModel &$model) { // This only applies to the front-end if (!$model->getContainer()->platform->isFrontend()) { return; } // Get the page parameters /** @var SiteApplication $app */ $app = JoomlaFactory::getApplication(); /** @var Registry $params */ $params = $app->getParams(); // Extract the page parameter keys $asArray = []; if (is_object($params) && method_exists($params, 'toArray')) { $asArray = $params->toArray(); } if (empty($asArray)) { // There are no keys; no point in going on. return; } $keys = array_keys($asArray); unset($asArray); // Loop all page parameter keys foreach ($keys as $key) { // This is the current model state $currentState = $model->getState($key); // This is the explicitly requested state in the input $explicitInput = $model->input->get($key, null, 'raw'); // If the current state is empty and there's no explicit input we'll use the page parameters instead if (!is_null($currentState)) { return; } if (!is_null($explicitInput)) { return; } $model->setState($key, $params->get($key)); } } } Model/DataModel/Behaviour/Tags.php 0000604 00000010100 15245560676 0012775 0 ustar 00 <?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\Observable; use FOF40\Event\Observer; use FOF40\Model\DataModel; use Joomla\CMS\Helper\TagsHelper; /** * FOF model behavior class to add Joomla! Tags support * * @since 2.1 */ class Tags extends Observer { /** @var TagsHelper */ protected $tagsHelper; public function __construct(Observable &$subject) { parent::__construct($subject); $this->tagsHelper = new TagsHelper(); } /** * This event runs after unpublishing a record in a model * * @param DataModel &$model The model which calls this event * @param \stdClass &$dataObject The data to bind to the form * * @return void */ public function onBeforeCreate(DataModel &$model, &$dataObject) { $tagField = $model->getBehaviorParam('tagFieldName', 'tags'); unset($dataObject->$tagField); } /** * This event runs after unpublishing a record in a model * * @param DataModel &$model The model which calls this event * @param \stdClass &$dataObject The data to bind to the form * * @return void */ public function onBeforeUpdate(DataModel &$model, &$dataObject) { $tagField = $model->getBehaviorParam('tagFieldName', 'tags'); unset($dataObject->$tagField); } /** * The event which runs after binding data to the table * * @param DataModel &$model The model which calls this event * * @return void * * @throws \Exception Error message if failed to store tags */ public function onAfterSave(DataModel &$model) { $tagField = $model->getBehaviorParam('tagFieldName', 'tags'); // Avoid to update on other method (e.g. publish, ...) if (!in_array($model->getContainer()->input->getCmd('task'), ['apply', 'save', 'savenew'])) { return; } $oldTags = $this->tagsHelper->getTagIds($model->getId(), $model->getContentType()); $newTags = $model->$tagField ? implode(',', $model->$tagField) : null; // If no changes, we stop here if ($oldTags == $newTags) { return; } // Check if the content type exists, and create it if it does not $model->checkContentType(); $this->tagsHelper->typeAlias = $model->getContentType(); if (!$this->tagsHelper->postStoreProcess($model, $model->$tagField)) { throw new \Exception('Error storing tags'); } } /** * The event which runs after deleting a record * * @param DataModel &$model The model which calls this event * @param integer $oid The PK value of the record which was deleted * * @return void * * @throws \Exception Error message if failed to detele tags */ public function onAfterDelete(DataModel &$model, $oid) { $this->tagsHelper->typeAlias = $model->getContentType(); if (!$this->tagsHelper->deleteTagData($model, $oid)) { throw new \Exception('Error deleting Tags'); } } /** * This event runs after unpublishing a record in a model * * @param DataModel &$model The model which calls this event * @param mixed $data An associative array or object to bind to the DataModel instance. * * @return void * @noinspection PhpUnusedParameterInspection */ public function onAfterBind(DataModel &$model, &$data) { $tagField = $model->getBehaviorParam('tagFieldName', 'tags'); if ($model->$tagField) { return; } $type = $model->getContentType(); $model->addKnownField($tagField); $model->$tagField = $this->tagsHelper->getTagIds($model->getId(), $type); } /** * 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(); } } Model/DataModel/Relation.php 0000604 00000020125 15245560676 0011740 0 ustar 00 <?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; defined('_JEXEC') || die; use FOF40\Container\Container; use FOF40\Model\DataModel; abstract class Relation { /** @var DataModel The data model we are attached to */ protected $parentModel; /** @var string The class name of the foreign key's model */ protected $foreignModelClass; /** @var string The application name of the foreign model */ protected $foreignModelComponent; /** @var string The bade name of the foreign model */ protected $foreignModelName; /** @var string The local table key for this relation */ protected $localKey; /** @var string The foreign table key for this relation */ protected $foreignKey; /** @var null For many-to-many relations, the pivot (glue) table */ protected $pivotTable; /** @var null For many-to-many relations, the pivot table's column storing the local key */ protected $pivotLocalKey; /** @var null For many-to-many relations, the pivot table's column storing the foreign key */ protected $pivotForeignKey; /** @var Collection The data loaded by this relation */ protected $data; /** @var array Maps each local table key to an array of foreign table keys, used in many-to-many relations */ protected $foreignKeyMap = []; /** @var Container The component container for this relation */ protected $container; /** * Public constructor. Initialises the relation. * * @param DataModel $parentModel The data model we are attached to * @param string $foreignModelName The name of the foreign key's model in the format * "modelName@com_something" * @param string $localKey The local table key for this relation * @param string $foreignKey The foreign key for this relation * @param string $pivotTable For many-to-many relations, the pivot (glue) table * @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local * key * @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign * key */ public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null) { $this->parentModel = $parentModel; $this->foreignModelClass = $foreignModelName; $this->localKey = $localKey; $this->foreignKey = $foreignKey; $this->pivotTable = $pivotTable; $this->pivotLocalKey = $pivotLocalKey; $this->pivotForeignKey = $pivotForeignKey; $this->container = $parentModel->getContainer(); $class = $foreignModelName; if (strpos($class, '@') === false) { $this->foreignModelComponent = null; $this->foreignModelName = $class; } else { $foreignParts = explode('@', $class, 2); $this->foreignModelComponent = $foreignParts[1]; $this->foreignModelName = $foreignParts[0]; } } /** * Reset the relation data * * @return $this For chaining */ public function reset() { $this->data = null; $this->foreignKeyMap = []; return $this; } /** * Rebase the relation to a different model * * @param DataModel $model * * @return $this For chaining */ public function rebase(DataModel $model) { $this->parentModel = $model; return $this->reset(); } /** * Get the relation data. * * If you want to apply additional filtering to the foreign model, use the $callback. It can be any function, * static method, public method or closure with an interface of function(DataModel $foreignModel). You are not * supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do: * $foreignModel->setState('foo', 'bar') * * @param callable $callback The callback to run on the remote model. * @param Collection $dataCollection * * @return Collection|DataModel */ public function getData($callback = null, Collection $dataCollection = null) { if (is_null($this->data)) { // Initialise $this->data = new Collection(); // Get a model instance $foreignModel = $this->getForeignModel(); $foreignModel->setIgnoreRequest(true); $filtered = $this->filterForeignModel($foreignModel, $dataCollection); if (!$filtered) { return $this->data; } // Apply the callback, if applicable if (!is_null($callback) && is_callable($callback)) { call_user_func($callback, $foreignModel); } // Get the list of items from the foreign model and cache in $this->data $this->data = $foreignModel->get(true); } return $this->data; } /** * Populates the internal $this->data collection from the contents of the provided collection. This is used by * DataModel to push the eager loaded data into each item's relation. * * @param Collection $data The relation data to push into this relation * @param mixed $keyMap Used by many-to-many relations to pass around the local to foreign key map * * @return void */ public function setDataFromCollection(Collection &$data, $keyMap = null) { $this->data = new Collection(); if (!empty($data)) { $localKeyValue = $this->parentModel->getFieldValue($this->localKey); /** @var DataModel $item */ foreach ($data as $item) { if ($item->getFieldValue($this->foreignKey) == $localKeyValue) { $this->data->add($item); } } } } /** * Returns the count subquery for DataModel's has() and whereHas() methods. * * @return \JDatabaseQuery */ abstract public function getCountSubquery(); /** * Returns a new item of the foreignModel type, pre-initialised to fulfil this relation * * @return DataModel * * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported */ abstract public function getNew(); /** * Saves all related items. You can use it to touch items as well: every item being saved causes the modified_by and * modified_on fields to be changed automatically, thanks to the DataModel's magic. */ public function saveAll() { if ($this->data instanceof Collection) { foreach ($this->data as $item) { if ($item instanceof DataModel) { $item->save(); } } } } /** * Returns the foreign key map of a many-to-many relation, used for eager loading many-to-many relations * * @return array */ public function &getForeignKeyMap() { return $this->foreignKeyMap; } /** * Gets an object instance of the foreign model * * @param array $config Optional configuration information for the Model * * @return DataModel */ public function &getForeignModel(array $config = []) { // If the model comes from this component go through our Factory if (is_null($this->foreignModelComponent)) { /** @var DataModel $model */ $model = $this->container->factory->model($this->foreignModelName, $config)->tmpInstance(); return $model; } // The model comes from another component. Create a container and go through its factory. $foreignContainer = Container::getInstance($this->foreignModelComponent, ['tempInstance' => true]); /** @var DataModel $model */ $model = $foreignContainer->factory->model($this->foreignModelName, $config)->tmpInstance(); return $model; } /** * Returns the name of the local key of the relation * * @return string */ public function getLocalKey() { return $this->localKey; } /** * Applies the relation filters to the foreign model when getData is called * * @param DataModel $foreignModel The foreign model you're operating on * @param Collection $dataCollection If it's an eager loaded relation, the collection of loaded parent records * * @return boolean Return false to force an empty data collection */ abstract protected function filterForeignModel(DataModel $foreignModel, Collection $dataCollection = null); } Model/DataModel/RelationManager.php 0000604 00000032762 15245560676 0013245 0 ustar 00 <?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; defined('_JEXEC') || die; use FOF40\Model\DataModel; class RelationManager { /** @var array The known relation types */ protected static $relationTypes = []; /** @var DataModel The data model we are attached to */ protected $parentModel; /** @var Relation[] The relations known to us */ protected $relations = []; /** @var array A list of the names of eager loaded relations */ protected $eager = []; /** * Creates a new relation manager for the defined parent model * * @param DataModel $parentModel The model we are attached to */ public function __construct(DataModel $parentModel) { // Set the parent model $this->parentModel = $parentModel; // Make sure the relation types are initialised static::getRelationTypes(); // @todo Maybe set up a few relations automatically? } /** * Populates the static map of relation type methods and relation handling classes * * @return array Key = method name, Value = relation handling class */ public static function getRelationTypes() { if (empty(static::$relationTypes)) { $relationTypeDirectory = __DIR__ . '/Relation'; $fs = new \DirectoryIterator($relationTypeDirectory); /** @var $file \DirectoryIterator */ foreach ($fs as $file) { if ($file->isDir()) { continue; } if ($file->getExtension() != 'php') { continue; } $baseName = ucfirst($file->getBasename('.php')); $methodName = strtolower($baseName[0]) . substr($baseName, 1); $className = '\\FOF40\\Model\\DataModel\\Relation\\' . $baseName; if (!class_exists($className, true)) { continue; } static::$relationTypes[$methodName] = $className; } } return static::$relationTypes; } /** * Implements deep cloning of the relation object */ function __clone() { $relations = []; /** @var Relation[] $relations */ foreach ($this->relations as $key => $relation) { $relations[$key] = clone($relation); $relations[$key]->reset(); } $this->relations = $relations; } /** * Rebase a relation manager * * @param DataModel $parentModel */ public function rebase(DataModel $parentModel) { $this->parentModel = $parentModel; if (count($this->relations) > 0) { foreach ($this->relations as $relation) { /** @var Relation $relation */ $relation->rebase($parentModel); } } } /** * Populates the internal $this->data collection of a relation from the contents of the provided collection. This is * used by DataModel to push the eager loaded data into each item's relation. * * @param string $name Relation name * @param Collection $data The relation data to push into this relation * @param mixed $keyMap Used by many-to-many relations to pass around the local to foreign key map * * @return void * * @throws Relation\Exception\RelationNotFound */ public function setDataFromCollection($name, Collection &$data, $keyMap = null) { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } $this->relations[$name]->setDataFromCollection($data, $keyMap); } /** * Adds a relation to the relation manager * * @param string $name The name of the relation as known to this relation manager, e.g. 'phone' * @param string $type The relation type, e.g. 'hasOne' * @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something" * @param string $localKey The local table key for this relation * @param string $foreignKey The foreign key for this relation * @param string $pivotTable For many-to-many relations, the pivot (glue) table * @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local key * @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign key * * @return DataModel The parent model, for chaining * * @throws Relation\Exception\RelationTypeNotFound when $type is not known * @throws Relation\Exception\ForeignModelNotFound when $foreignModelClass doesn't exist */ public function addRelation($name, $type, $foreignModelName = null, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null) { if (!isset(static::$relationTypes[$type])) { throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$type' not found"); } // Guess the foreign model class if necessary if (empty($foreignModelName)) { $foreignModelName = ucfirst($name); } $className = static::$relationTypes[$type]; /** @var Relation $relation */ $relation = new $className($this->parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey); $this->relations[$name] = $relation; return $this->parentModel; } /** * Removes a known relation * * @param string $name The name of the relation to remove * * @return DataModel The parent model, for chaining */ public function removeRelation($name) { if (isset($this->relations[$name])) { unset ($this->relations[$name]); } return $this->parentModel; } /** * Removes all known relations */ public function resetRelations() { $this->relations = []; } /** * Resets the data of all relations in this manager. This doesn't remove relations, just their data so that they * get loaded again. * * @param array $relationsToReset The names of the relations to reset. Pass an empty array (default) to reset * all relations. */ public function resetRelationData(array $relationsToReset = []) { /** @var Relation $relation */ foreach ($this->relations as $name => $relation) { if (!empty($relationsToReset) && !in_array($name, $relationsToReset)) { continue; } $relation->reset(); } } /** * Returns a list of all known relations' names * * @return array */ public function getRelationNames() { return array_keys($this->relations); } /** * Gets the related items of a relation * * @param string $name The name of the relation to return data for * * @return Relation * * @throws Relation\Exception\RelationNotFound */ public function &getRelation($name) { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } return $this->relations[$name]; } /** * Get a new related item which satisfies relation $name and adds it to this relation's data list. * * @param string $name The relation based on which a new item is returned * * @return DataModel * * @throws Relation\Exception\RelationNotFound */ public function getNew($name) { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } return $this->relations[$name]->getNew(); } /** * Saves all related items belonging to the specified relation or, if $name is null, all known relations which * support saving. * * @param null|string $name The relation to save, or null to save all known relations * * @return DataModel The parent model, for chaining * * @throws Relation\Exception\RelationNotFound */ public function save($name = null) { if (is_null($name)) { foreach ($this->relations as $relation) { try { $relation->saveAll(); } catch (DataModel\Relation\Exception\SaveNotSupported $e) { // We don't care if a relation doesn't support saving } } } else { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } $this->relations[$name]->saveAll(); } return $this->parentModel; } /** * Gets the related items of a relation * * @param string $name The name of the relation to return data for * @param callable $callback A callback to customise the returned data * @param \FOF40\Utils\Collection $dataCollection Used when fetching the data of an eager loaded relation * * @return Collection|DataModel * * @throws Relation\Exception\RelationNotFound * @see Relation::getData() * */ public function getData($name, $callback = null, \FOF40\Utils\Collection $dataCollection = null) { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } return $this->relations[$name]->getData($callback, $dataCollection); } /** * Gets the foreign key map of a many-to-many relation * * @param string $name The name of the relation to return data for * * @return array * * @throws Relation\Exception\RelationNotFound */ public function &getForeignKeyMap($name) { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } return $this->relations[$name]->getForeignKeyMap(); } /** * Returns the count sub-query for a relation, used for relation filters (whereHas in the DataModel). * * @param string $name The relation to get the sub-query for * @param string $tableAlias The alias to use for the local table * * @return \JDatabaseQuery * @throws Relation\Exception\RelationNotFound */ public function getCountSubquery($name, $tableAlias = null) { if (!isset($this->relations[$name])) { throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found"); } return $this->relations[$name]->getCountSubquery($tableAlias); } /** * A magic method which allows us to define relations using shorthand notation, e.g. $manager->hasOne('phone') * instead of $manager->addRelation('phone', 'hasOne') * * You can also use it to get data of a relation using shorthand notation, e.g. $manager->getPhone($callback) * instead of $manager->getData('phone', $callback); * * @param string $name The magic method to call * @param array $arguments The arguments to the magic method * * @return DataModel The parent model, for chaining * * @throws \InvalidArgumentException * @throws DataModel\Relation\Exception\RelationTypeNotFound */ function __call($name, $arguments) { $numberOfArguments = count($arguments); if (isset(static::$relationTypes[$name])) { if ($numberOfArguments == 1) { return $this->addRelation($arguments[0], $name); } elseif ($numberOfArguments == 2) { return $this->addRelation($arguments[0], $name, $arguments[1]); } elseif ($numberOfArguments == 3) { return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2]); } elseif ($numberOfArguments == 4) { return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3]); } elseif ($numberOfArguments == 5) { return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4]); } elseif ($numberOfArguments == 6) { return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]); } elseif ($numberOfArguments >= 7) { return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]); } else { throw new \InvalidArgumentException("You can not create an unnamed '$name' relation"); } } elseif (substr($name, 0, 3) == 'get') { $relationName = substr($name, 3); $relationName = strtolower($relationName[0]) . substr($relationName, 1); if ($numberOfArguments == 0) { return $this->getData($relationName); } elseif ($numberOfArguments == 1) { return $this->getData($relationName, $arguments[0]); } elseif ($numberOfArguments == 2) { return $this->getData($relationName, $arguments[0], $arguments[1]); } else { throw new \InvalidArgumentException("Invalid number of arguments getting data for the '$relationName' relation"); } } // Throw an exception otherwise throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$name' not known to relation manager"); } /** * Is $name a magic-callable method? * * @param string $name The name of a potential magic-callable method * * @return bool */ public function isMagicMethod($name) { if (isset(static::$relationTypes[$name])) { return true; } elseif (substr($name, 0, 3) == 'get') { $relationName = substr($name, 3); $relationName = strtolower($relationName[0]) . substr($relationName, 1); if (isset($this->relations[$relationName])) { return true; } } return false; } /** * Is $name a magic property? Corollary: returns true if a relation of this name is known to the relation manager. * * @param string $name The name of a potential magic property * * @return bool */ public function isMagicProperty($name) { return isset($this->relations[$name]); } /** * Magic method to get the data of a relation using shorthand notation, e.g. $manager->phone instead of * $manager->getData('phone') * * @param $name * * @return Collection */ function __get($name) { return $this->getData($name); } } Model/Exception/CannotGetName.php 0000604 00000001164 15245560676 0012754 0 ustar 00 <?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\Exception; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Language\Text; /** * Exception thrown when we can't get a Controller's name */ class CannotGetName extends \RuntimeException { public function __construct( $message = "", $code = 500, Exception $previous = null ) { if (empty($message)) { $message = Text::_('LIB_FOF40_MODEL_ERR_GET_NAME'); } parent::__construct( $message, $code, $previous ); } } Model/TreeModel.php 0000604 00000161252 15245560676 0010220 0 ustar 00 <?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; defined('_JEXEC') || die; use FOF40\Container\Container; use FOF40\Model\DataModel\Exception\TreeIncompatibleTable; use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtCurrent; use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtOther; use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtParent; use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtSibling; use FOF40\Model\DataModel\Exception\TreeMethodOnlyAllowedInRoot; use FOF40\Model\DataModel\Exception\TreeRootNotFound; use FOF40\Model\DataModel\Exception\TreeUnexpectedPrimaryKey; use FOF40\Model\DataModel\Exception\TreeUnsupportedMethod; use Joomla\CMS\Application\ApplicationHelper; /** * A DataModel which implements nested trees * * @property int $lft Left value (for nested set implementation) * @property int $rgt Right value (for nested set implementation) * @property string $hash Slug hash (for faster searching) */ class TreeModel extends DataModel { /** @var int The level (depth) of this node in the tree */ protected $treeDepth; /** @var TreeModel The root node in the tree */ protected $treeRoot; /** @var TreeModel The parent node of ourselves */ protected $treeParent; /** @var bool Should I perform a nested get (used to query ascendants/descendants) */ protected $treeNestedGet = false; /** * Public constructor. Overrides the parent constructor, making sure there are lft/rgt columns which make it * compatible with nested sets. * * @param Container $container The configuration variables to this model * @param array $config Configuration values for this model * * @throws \RuntimeException When lft/rgt columns are not found * @see \FOF40\Model\DataModel::__construct() * */ public function __construct(Container $container = null, array $config = []) { parent::__construct($container, $config); if (!$this->hasField('lft') || !$this->hasField('rgt')) { throw new TreeIncompatibleTable($this->tableName); } } /** * Overrides the automated table checks to handle the 'hash' column for faster searching * * @return $this|DataModel */ public function check() { // Create a slug if there is a title and an empty slug if ($this->hasField('title') && $this->hasField('slug') && !$this->slug) { $this->slug = ApplicationHelper::stringURLSafe($this->title); } // Create the SHA-1 hash of the slug for faster searching (make sure the hash column is CHAR(64) to take // advantage of MySQL's optimised searching for fixed size CHAR columns) if ($this->hasField('hash') && $this->hasField('slug')) { $this->hash = sha1($this->slug); } // Reset cached values $this->resetTreeCache(); // Run the parent checks parent::check(); return $this; } /** * Delete a node, either the currently loaded one or the one specified in $id. If an $id is specified that node * is loaded before trying to delete it. In the end the data model is reset. If the node has any children nodes * they will be removed before the node itself is deleted. * * @param mixed $id Primary key (id field) value * * @return $this for chaining * @throws \UnexpectedValueException * */ public function forceDelete($id = null) { // Load the specified record (if necessary) if (!empty($id)) { $this->findOrFail($id); } $k = $this->getIdFieldName(); $pk = (!$id) ? $this->$k : $id; // If no primary key is given, return false. if (!$pk) { throw new TreeUnexpectedPrimaryKey; } // Execute the logic only if I have a primary key, otherwise I could have weird results // Perform the checks on the current node *BEFORE* starting to delete the children try { $this->triggerEvent('onBeforeDelete', [&$pk]); } catch (\Exception $e) { return false; } $result = true; // Recursively delete all children nodes as long as we are not a leaf node if (!$this->isLeaf()) { // Get all sub-nodes $table = $this->getClone(); $table->bind($this->getData()); $subNodes = $table->getDescendants(); // Delete all sub-nodes (goes through the model to trigger the observers) if (!empty($subNodes)) { /** @var TreeModel $item */ foreach ($subNodes as $item) { // We have to pass the id, so we are getting it again from the database. // We have to do in this way, since a previous child could have changed our lft and rgt values if (!$item->forceDelete($item->$k)) { // A sub-node failed or prevents the delete, continue deleting other nodes, // but preserve the current node (ie the parent) $result = false; } }; // Load it again, since while deleting a children we could have updated ourselves, too $this->find($pk); } } if ($result) { $db = $this->getDbo(); // Delete the row by primary key. $query = $db->getQuery(true); $query->delete(); $query->from($this->getTableName()); $query->where($db->qn($this->getIdFieldName()) . ' = ' . $db->q($pk)); $db->setQuery($query)->execute(); $this->triggerEvent('onAfterDelete', [&$pk]); } return $this; } /** * Not supported in nested sets * * @param string $where Ignored * * @return static Self, for chaining * * @throws \RuntimeException */ public function reorder($where = '') { throw new TreeUnsupportedMethod(__METHOD__); } /** * Not supported in nested sets * * @param integer $delta Ignored * @param string $where Ignored * * @return static Self, for chaining * * @throws \RuntimeException */ public function move($delta, $where = '') { throw new TreeUnsupportedMethod(__METHOD__); } /** * Create a new record with the provided data. It is inserted as the last child of the current node's parent * * @param array $data The data to use in the new record * * @return static The new node */ public function create($data) { $newNode = $this->getClone(); $newNode->reset(); $newNode->bind($data); if ($this->isRoot()) { return $newNode->insertAsChildOf($this); } else { $parentNode = $this->getParent(); return $newNode->insertAsChildOf($parentNode); } } /** * Makes a copy of the record, inserting it as the last child of the current node's parent. * * @return static * * @codeCoverageIgnore */ public function copy($data = null) { $selfData = $this->toArray(); if (!is_array($data)) { $data = []; } $data = array_merge($data, $selfData); return $this->create($data); } /** * Reset the record data and the tree cache * * @param boolean $useDefaults Should I use the default values? Default: yes * @param boolean $resetRelations Should I reset the relations too? Default: no * * @return static Self, for chaining * * @codeCoverageIgnore */ public function reset($useDefaults = true, $resetRelations = false) { $this->resetTreeCache(); return parent::reset($useDefaults, $resetRelations); } /** * Insert the current node as a tree root. It is a good idea to never use this method, instead providing a root node * in your schema installation and then sticking to only one root. * * @return static * * @throws \RuntimeException */ public function insertAsRoot() { // You can't insert a node that is already saved i.e. the table has an id if ($this->getId()) { throw new TreeMethodOnlyAllowedInRoot(__METHOD__); } // First we need to find the right value of the last parent, a.k.a. the max(rgt) of the table $db = $this->getDbo(); // Get the lft/rgt names $fldRgt = $db->qn($this->getFieldAlias('rgt')); $query = $db->getQuery(true) ->select('MAX(' . $fldRgt . ')') ->from($db->qn($this->tableName)); $maxRgt = $db->setQuery($query, 0, 1)->loadResult(); if (empty($maxRgt)) { $maxRgt = 0; } $this->lft = ++$maxRgt; $this->rgt = ++$maxRgt; return $this->save(); } /** * Insert the current node as the first (leftmost) child of a parent node. * * WARNING: If it's an existing node it will be COPIED, not moved. * * @param TreeModel $parentNode The node which will become our parent * * @return $this for chaining * @throws \Exception * @throws \RuntimeException */ public function insertAsFirstChildOf(TreeModel &$parentNode) { if ($parentNode->lft >= $parentNode->rgt) { throw new TreeInvalidLftRgtParent; } // Get a reference to the database $db = $this->getDbo(); // Get the field names $fldRgt = $db->qn($this->getFieldAlias('rgt')); $fldLft = $db->qn($this->getFieldAlias('lft')); // Nullify the PK, so a new record will be created $this->{$this->idFieldName} = null; // Get the value of the parent node's rgt $myLeft = $parentNode->lft; // Update my lft/rgt values $this->lft = $myLeft + 1; $this->rgt = $myLeft + 2; // Update parent node's right (we added two elements in there, remember?) $parentNode->rgt += 2; // Wrap everything in a transaction $db->transactionStart(); try { // Make a hole (2 queries) $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldLft . ' = ' . $fldLft . '+2') ->where($fldLft . ' > ' . $db->q($myLeft)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldRgt . ' = ' . $fldRgt . '+ 2') ->where($fldRgt . '>' . $db->q($myLeft)); $db->setQuery($query)->execute(); // Insert the new node $this->save(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { // Roll back the transaction on error $db->transactionRollback(); throw $e; } return $this; } /** * Insert the current node as the last (rightmost) child of a parent node. * * WARNING: If it's an existing node it will be COPIED, not moved. * * @param TreeModel $parentNode The node which will become our parent * * @return $this for chaining * @throws \Exception * @throws \RuntimeException */ public function insertAsLastChildOf(TreeModel &$parentNode) { if ($parentNode->lft >= $parentNode->rgt) { throw new TreeInvalidLftRgtParent; } // Get a reference to the database $db = $this->getDbo(); // Get the field names $fldRgt = $db->qn($this->getFieldAlias('rgt')); $fldLft = $db->qn($this->getFieldAlias('lft')); // Nullify the PK, so a new record will be created $this->{$this->idFieldName} = null; // Get the value of the parent node's lft $myRight = $parentNode->rgt; // Update my lft/rgt values $this->lft = $myRight; $this->rgt = $myRight + 1; // Update parent node's right (we added two elements in there, remember?) $parentNode->rgt += 2; // Wrap everything in a transaction $db->transactionStart(); try { // Make a hole (2 queries) $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldRgt . ' = ' . $fldRgt . '+2') ->where($fldRgt . '>=' . $db->q($myRight)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldLft . ' = ' . $fldLft . '+2') ->where($fldLft . '>' . $db->q($myRight)); $db->setQuery($query)->execute(); // Insert the new node $this->save(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { // Roll back the transaction on error $db->transactionRollback(); throw $e; } return $this; } /** * Alias for insertAsLastchildOf * * @codeCoverageIgnore * * @param TreeModel $parentNode * * @return $this for chaining */ public function insertAsChildOf(TreeModel &$parentNode) { return $this->insertAsLastChildOf($parentNode); } /** * Insert the current node to the left of (before) a sibling node * * WARNING: If it's an existing node it will be COPIED, not moved. * * @param TreeModel $siblingNode We will be inserted before this node * * @return $this for chaining * @throws \Exception * @throws \RuntimeException */ public function insertLeftOf(TreeModel &$siblingNode) { if ($siblingNode->lft >= $siblingNode->rgt) { throw new TreeInvalidLftRgtSibling; } // Get a reference to the database $db = $this->getDbo(); // Get the field names $fldRgt = $db->qn($this->getFieldAlias('rgt')); $fldLft = $db->qn($this->getFieldAlias('lft')); // Nullify the PK, so a new record will be created $this->{$this->idFieldName} = null; // Get the value of the parent node's rgt $myLeft = $siblingNode->lft; // Update my lft/rgt values $this->lft = $myLeft; $this->rgt = $myLeft + 1; // Update sibling's lft/rgt values $siblingNode->lft += 2; $siblingNode->rgt += 2; $db->transactionStart(); try { $db->setQuery( $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldLft . ' = ' . $fldLft . '+2') ->where($fldLft . ' >= ' . $db->q($myLeft)) )->execute(); $db->setQuery( $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldRgt . ' = ' . $fldRgt . '+2') ->where($fldRgt . ' > ' . $db->q($myLeft)) )->execute(); $this->save(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { $db->transactionRollback(); throw $e; } return $this; } /** * Insert the current node to the right of (after) a sibling node * * WARNING: If it's an existing node it will be COPIED, not moved. * * @param TreeModel $siblingNode We will be inserted after this node * * @return $this for chaining * @throws \Exception * @throws \RuntimeException */ public function insertRightOf(TreeModel &$siblingNode) { if ($siblingNode->lft >= $siblingNode->rgt) { throw new TreeInvalidLftRgtSibling; } // Get a reference to the database $db = $this->getDbo(); // Get the field names $fldRgt = $db->qn($this->getFieldAlias('rgt')); $fldLft = $db->qn($this->getFieldAlias('lft')); // Nullify the PK, so a new record will be created $this->{$this->idFieldName} = null; // Get the value of the parent node's lft $myRight = $siblingNode->rgt; // Update my lft/rgt values $this->lft = $myRight + 1; $this->rgt = $myRight + 2; $db->transactionStart(); try { $db->setQuery( $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldRgt . ' = ' . $fldRgt . '+2') ->where($fldRgt . ' > ' . $db->q($myRight)) )->execute(); $db->setQuery( $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($fldLft . ' = ' . $fldLft . '+2') ->where($fldLft . ' > ' . $db->q($myRight)) )->execute(); $this->save(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { $db->transactionRollback(); throw $e; } return $this; } /** * Alias for insertRightOf * * @codeCoverageIgnore * * @param TreeModel $siblingNode * * @return $this for chaining */ public function insertAsSiblingOf(TreeModel &$siblingNode) { return $this->insertRightOf($siblingNode); } /** * Move the current node (and its subtree) one position to the left in the tree, i.e. before its left-hand sibling * * @return $this * @throws \RuntimeException * */ public function moveLeft() { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } // If it is a root node we will not move the node (roots don't participate in tree ordering) if ($this->isRoot()) { return $this; } // Are we already the leftmost node? $parentNode = $this->getParent(); if ($parentNode->lft === $this->lft - 1) { return $this; } // Get the sibling to the left $db = $this->getDbo(); $leftSibling = $this->getClone()->reset() ->whereRaw($db->qn($this->getFieldAlias('rgt')) . ' = ' . $db->q($this->lft - 1)) ->firstOrFail(); // Move the node return $this->moveToLeftOf($leftSibling); } /** * Move the current node (and its subtree) one position to the right in the tree, i.e. after its right-hand sibling * * @return $this * @throws \RuntimeException * */ public function moveRight() { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } // If it is a root node we will not move the node (roots don't participate in tree ordering) if ($this->isRoot()) { return $this; } // Are we already the rightmost node? $parentNode = $this->getParent(); if ($parentNode->rgt === $this->rgt + 1) { return $this; } // Get the sibling to the right $db = $this->getDbo(); $rightSibling = $this->getClone()->reset() ->whereRaw($db->qn($this->getFieldAlias('lft')) . ' = ' . $db->q($this->rgt + 1)) ->firstOrFail(); // Move the node return $this->moveToRightOf($rightSibling); } /** * Moves the current node (and its subtree) to the left of another node. The other node can be in a different * position in the tree or even under a different root. * * @param TreeModel $siblingNode * * @return $this for chaining * * @throws \Exception * @throws \RuntimeException */ public function moveToLeftOf(TreeModel $siblingNode) { // Sanity checks on current and sibling node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($siblingNode->lft >= $siblingNode->rgt) { throw new TreeInvalidLftRgtSibling; } $db = $this->getDbo(); $left = $db->qn($this->getFieldAlias('lft')); $right = $db->qn($this->getFieldAlias('rgt')); // Get node metrics $myLeft = $this->lft; $myRight = $this->rgt; $myWidth = $myRight - $myLeft + 1; // Get parent metrics $sibLeft = $siblingNode->lft; // Start the transaction $db->transactionStart(); try { // Temporary remove subtree being moved $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set("$left = " . $db->q(0) . " - $left") ->set("$right = " . $db->q(0) . " - $right") ->where($left . ' >= ' . $db->q($myLeft)) ->where($right . ' <= ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Close hole left behind $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' - ' . $db->q($myWidth)) ->where($left . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' - ' . $db->q($myWidth)) ->where($right . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Make a hole for the new items $newSibLeft = ($sibLeft > $myRight) ? $sibLeft - $myWidth : $sibLeft; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' + ' . $db->q($myWidth)) ->where($right . ' >= ' . $db->q($newSibLeft)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' + ' . $db->q($myWidth)) ->where($left . ' >= ' . $db->q($newSibLeft)); $db->setQuery($query)->execute(); // Move node and sub-nodes $moveRight = $newSibLeft - $myLeft; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight)) ->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight)) ->where($left . ' <= 0 - ' . $db->q($myLeft)) ->where($right . ' >= 0 - ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { $db->transactionRollback(); throw $e; } // Let's load the record again to fetch the new values for lft and rgt $this->findOrFail(); return $this; } /** * Moves the current node (and its subtree) to the right of another node. The other node can be in a different * position in the tree or even under a different root. * * @param TreeModel $siblingNode * * @return $this for chaining * * @throws \Exception * @throws \RuntimeException */ public function moveToRightOf(TreeModel $siblingNode) { // Sanity checks on current and sibling node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($siblingNode->lft >= $siblingNode->rgt) { throw new TreeInvalidLftRgtSibling; } $db = $this->getDbo(); $left = $db->qn($this->getFieldAlias('lft')); $right = $db->qn($this->getFieldAlias('rgt')); // Get node metrics $myLeft = $this->lft; $myRight = $this->rgt; $myWidth = $myRight - $myLeft + 1; // Get parent metrics $sibRight = $siblingNode->rgt; // Start the transaction $db->transactionStart(); try { // Temporary remove subtree being moved $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set("$left = " . $db->q(0) . " - $left") ->set("$right = " . $db->q(0) . " - $right") ->where($left . ' >= ' . $db->q($myLeft)) ->where($right . ' <= ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Close hole left behind $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' - ' . $db->q($myWidth)) ->where($left . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' - ' . $db->q($myWidth)) ->where($right . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Make a hole for the new items $newSibRight = ($sibRight > $myRight) ? $sibRight - $myWidth : $sibRight; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' + ' . $db->q($myWidth)) ->where($left . ' > ' . $db->q($newSibRight)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' + ' . $db->q($myWidth)) ->where($right . ' > ' . $db->q($newSibRight)); $db->setQuery($query)->execute(); // Move node and sub-nodes $moveRight = ($sibRight > $myRight) ? $sibRight - $myRight : $sibRight - $myRight + $myWidth; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight)) ->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight)) ->where($left . ' <= 0 - ' . $db->q($myLeft)) ->where($right . ' >= 0 - ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { $db->transactionRollback(); throw $e; } // Let's load the record again to fetch the new values for lft and rgt $this->findOrFail(); return $this; } /** * Alias for moveToRightOf * * @param TreeModel $siblingNode * * @return $this for chaining * * @codeCoverageIgnore */ public function makeNextSiblingOf(TreeModel $siblingNode) { return $this->moveToRightOf($siblingNode); } /** * Alias for makeNextSiblingOf * * @param TreeModel $siblingNode * * @return $this for chaining * * @codeCoverageIgnore */ public function makeSiblingOf(TreeModel $siblingNode) { return $this->makeNextSiblingOf($siblingNode); } /** * Alias for moveToLeftOf * * @param TreeModel $siblingNode * * @return $this for chaining * * @codeCoverageIgnore */ public function makePreviousSiblingOf(TreeModel $siblingNode) { return $this->moveToLeftOf($siblingNode); } /** * Moves a node and its subtree as a the first (leftmost) child of $parentNode * * @param TreeModel $parentNode * * @return $this for chaining * * @throws \Exception * @throws \RuntimeException */ public function makeFirstChildOf(TreeModel $parentNode) { // Sanity checks on current and sibling node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($parentNode->lft >= $parentNode->rgt) { throw new TreeInvalidLftRgtParent; } $db = $this->getDbo(); $left = $db->qn($this->getFieldAlias('lft')); $right = $db->qn($this->getFieldAlias('rgt')); // Get node metrics $myLeft = $this->lft; $myRight = $this->rgt; $myWidth = $myRight - $myLeft + 1; // Get parent metrics $parentRight = $parentNode->rgt; $parentLeft = $parentNode->lft; // Start the transaction $db->transactionStart(); try { // Temporary remove subtree being moved $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set("$left = " . $db->q(0) . " - $left") ->set("$right = " . $db->q(0) . " - $right") ->where($left . ' >= ' . $db->q($myLeft)) ->where($right . ' <= ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Close hole left behind $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' - ' . $db->q($myWidth)) ->where($left . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' - ' . $db->q($myWidth)) ->where($right . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Make a hole for the new items $newParentLeft = ($parentLeft > $myRight) ? $parentLeft - $myWidth : $parentLeft; $newParentRight = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' + ' . $db->q($myWidth)) ->where($right . ' >= ' . $db->q($newParentLeft)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' + ' . $db->q($myWidth)) ->where($left . ' > ' . $db->q($newParentLeft)); $db->setQuery($query)->execute(); // Move node and sub-nodes $moveRight = $newParentLeft - $myLeft + 1; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight)) ->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight)) ->where($left . ' <= 0 - ' . $db->q($myLeft)) ->where($right . ' >= 0 - ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { $db->transactionRollback(); throw $e; } // Let's load the record again to fetch the new values for lft and rgt $this->findOrFail(); return $this; } /** * Moves a node and its subtree as a the last (rightmost) child of $parentNode * * @param TreeModel $parentNode * * @return $this for chaining * * @throws \Exception * @throws \RuntimeException */ public function makeLastChildOf(TreeModel $parentNode) { // Sanity checks on current and sibling node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($parentNode->lft >= $parentNode->rgt) { throw new TreeInvalidLftRgtParent; } $db = $this->getDbo(); $left = $db->qn($this->getFieldAlias('lft')); $right = $db->qn($this->getFieldAlias('rgt')); // Get node metrics $myLeft = $this->lft; $myRight = $this->rgt; $myWidth = $myRight - $myLeft + 1; // Get parent metrics $parentRight = $parentNode->rgt; // Start the transaction $db->transactionStart(); try { // Temporary remove subtree being moved $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set("$left = " . $db->q(0) . " - $left") ->set("$right = " . $db->q(0) . " - $right") ->where($left . ' >= ' . $db->q($myLeft)) ->where($right . ' <= ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Close hole left behind $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' - ' . $db->q($myWidth)) ->where($left . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' - ' . $db->q($myWidth)) ->where($right . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Make a hole for the new items $newLeft = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $left . ' + ' . $db->q($myWidth)) ->where($left . ' >= ' . $db->q($newLeft)); $db->setQuery($query)->execute(); $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($right . ' = ' . $right . ' + ' . $db->q($myWidth)) ->where($right . ' >= ' . $db->q($newLeft)); $db->setQuery($query)->execute(); // Move node and sub-nodes $moveRight = ($parentRight > $myRight) ? $parentRight - $myRight - 1 : $parentRight - $myRight - 1 + $myWidth; $query = $db->getQuery(true) ->update($db->qn($this->tableName)) ->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight)) ->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight)) ->where($left . ' <= 0 - ' . $db->q($myLeft)) ->where($right . ' >= 0 - ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { $db->transactionRollback(); throw $e; } // Let's load the record again to fetch the new values for lft and rgt $this->findOrFail(); return $this; } /** * Alias for makeLastChildOf * * @param TreeModel $parentNode * * @return $this for chaining * * @codeCoverageIgnore */ public function makeChildOf(TreeModel $parentNode) { return $this->makeLastChildOf($parentNode); } /** * Makes the current node a root (and moving its entire subtree along the way). This is achieved by moving the node * to the right of its root node * * @return $this for chaining */ public function makeRoot() { // Make sure we are not a root if ($this->isRoot()) { return $this; } // Get a reference to my root $myRoot = $this->getRoot(); // Double check I am not a root if ($this->equals($myRoot)) { return $this; } // Move myself to the right of my root $this->moveToRightOf($myRoot); $this->treeDepth = 0; return $this; } /** * Gets the level (depth) of this node in the tree. The result is cached in $this->treeDepth for faster fetch. * * @return int|mixed * @throws \RuntimeException * */ public function getLevel() { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if (is_null($this->treeDepth)) { $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $query = $db->getQuery(true) ->select('(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth')) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt) ->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft)) ->group($db->qn('node') . '.' . $fldLft) ->order($db->qn('node') . '.' . $fldLft . ' ASC'); $this->treeDepth = $db->setQuery($query, 0, 1)->loadResult(); } return $this->treeDepth; } /** * Returns the immediate parent of the current node * * @return static * @throws \RuntimeException * */ public function getParent() { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($this->isRoot()) { return $this; } if (empty($this->treeParent) || !is_object($this->treeParent) || !($this->treeParent instanceof TreeModel)) { $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $query = $db->getQuery(true) ->select($db->qn('parent') . '.' . $fldLft) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt) ->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft)) ->order($db->qn('parent') . '.' . $fldLft . ' DESC'); $targetLft = $db->setQuery($query, 1, 1)->loadResult(); $this->treeParent = $this->getClone()->reset() ->whereRaw($fldLft . ' = ' . $db->q($targetLft)) ->firstOrFail(); } return $this->treeParent; } /** * Is this a top-level root node? * * @return bool */ public function isRoot() { // If lft=1 it is necessarily a root node if ($this->lft == 1) { return true; } // Otherwise make sure its level is 0 return $this->getLevel() == 0; } /** * Is this a leaf node (a node without children)? * * @return bool * @throws \RuntimeException * */ public function isLeaf() { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } return $this->rgt - 1 === $this->lft; } /** * Is this a child node (not root)? * * @codeCoverageIgnore * * @return bool */ public function isChild() { return !$this->isRoot(); } /** * Returns true if we are a descendant of $otherNode * * @param TreeModel $otherNode * * @return bool * @throws \RuntimeException * */ public function isDescendantOf(TreeModel $otherNode) { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($otherNode->lft >= $otherNode->rgt) { throw new TreeInvalidLftRgtOther; } return ($otherNode->lft < $this->lft) && ($otherNode->rgt > $this->rgt); } /** * Returns true if $otherNode is ourselves or if we are a descendant of $otherNode * * @param TreeModel $otherNode * * @return bool */ public function isSelfOrDescendantOf(TreeModel $otherNode) { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($otherNode->lft >= $otherNode->rgt) { throw new TreeInvalidLftRgtOther; } return ($otherNode->lft <= $this->lft) && ($otherNode->rgt >= $this->rgt); } /** * Returns true if we are an ancestor of $otherNode * * @codeCoverageIgnore * * @param TreeModel $otherNode * * @return bool */ public function isAncestorOf(TreeModel $otherNode) { return $otherNode->isDescendantOf($this); } /** * Returns true if $otherNode is ourselves or we are an ancestor of $otherNode * * @codeCoverageIgnore * * @param TreeModel $otherNode * * @return bool */ public function isSelfOrAncestorOf(TreeModel $otherNode) { return $otherNode->isSelfOrDescendantOf($this); } /** * Is $node this very node? * * @param TreeModel $node * * @return bool * @throws \RuntimeException * */ public function equals(TreeModel &$node) { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($node->lft >= $node->rgt) { throw new TreeInvalidLftRgtOther; } return ( ($this->getId() == $node->getId()) && ($this->lft === $node->lft) && ($this->rgt === $node->rgt) ); } /** * Checks if our node is inside the subtree of $otherNode. This is a fast check as only lft and rgt values have to * be compared. * * @param TreeModel $otherNode * * @return bool * @throws \RuntimeException * */ public function insideSubtree(TreeModel $otherNode) { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } if ($otherNode->lft >= $otherNode->rgt) { throw new TreeInvalidLftRgtOther; } return ($this->lft > $otherNode->lft) && ($this->rgt < $otherNode->rgt); } /** * Returns true if both this node and $otherNode are root, leaf or child (same tree scope) * * @param TreeModel $otherNode * * @return bool */ public function inSameScope(TreeModel $otherNode) { if ($this->isLeaf()) { return $otherNode->isLeaf(); } elseif ($this->isRoot()) { return $otherNode->isRoot(); } elseif ($this->isChild()) { return $otherNode->isChild(); } else { return false; } } /** * get() will not return the selected node if it's part of the query results * * @param TreeModel $node The node to exclude from the results * * @return void */ public function withoutNode(TreeModel $node) { $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $this->whereRaw('NOT(' . $db->qn('node') . '.' . $fldLft . ' = ' . $db->q($node->lft) . ')'); } /** * Returns the root node of the tree this node belongs to * * @return static * * @throws \RuntimeException */ public function getRoot() { // Empty node, let's try to get the first available root, ie lft=1 if (!$this->getId()) { $this->load(['lft' => 1]); } // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } // If this is a root node return itself (there is no such thing as the root of a root node) if ($this->isRoot()) { return $this; } if (empty($this->treeRoot) || !is_object($this->treeRoot) || !($this->treeRoot instanceof TreeModel)) { $this->treeRoot = null; // First try to get the record with the minimum ID $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $subQuery = $db->getQuery(true) ->select('MIN(' . $fldLft . ')') ->from($db->qn($this->tableName)); try { $root = $this->getClone()->reset() ->whereRaw($fldLft . ' = (' . $subQuery . ')') ->firstOrFail(); if ($this->isDescendantOf($root)) { $this->treeRoot = $root; } } catch (\RuntimeException $e) { // If there is no root found throw an exception. Basically: your table is FUBAR. throw new TreeRootNotFound($this->tableName, $this->lft, 500, $e); } // If the above method didn't work, get all roots and select the one with the appropriate lft/rgt values if (is_null($this->treeRoot)) { // Find the node with depth = 0, lft < our lft and rgt > our right. That's our root node. $query = $db->getQuery(true) ->select([ $db->qn('node') . '.' . $fldLft, '(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'), ]) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt) ->where($db->qn('node') . '.' . $fldLft . ' < ' . $db->q($this->lft)) ->where($db->qn('node') . '.' . $fldRgt . ' > ' . $db->q($this->rgt)) ->having($db->qn('depth') . ' = ' . $db->q(0)) ->group($db->qn('node') . '.' . $fldLft); // Get the lft value $targetLeft = $db->setQuery($query)->loadResult(); if (empty($targetLeft)) { // If there is no root found throw an exception. Basically: your table is FUBAR. throw new TreeRootNotFound($this->tableName, $this->lft); } try { $this->treeRoot = $this->getClone()->reset() ->whereRaw($fldLft . ' = ' . $db->q($targetLeft)) ->firstOrFail(); } catch (\RuntimeException $e) { // If there is no root found throw an exception. Basically: your table is FUBAR. throw new TreeRootNotFound($this->tableName, $this->lft, 500, $e); } } } return $this->treeRoot; } /** * Get all ancestors to this node and the node itself. In other words it gets the full path to the node and the node * itself. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getAncestorsAndSelf() { $this->scopeAncestorsAndSelf(); return $this->get(true); } /** * Get all ancestors to this node and the node itself, but not the root node. If you want to * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getAncestorsAndSelfWithoutRoot() { $this->scopeAncestorsAndSelf(); $this->scopeWithoutRoot(); return $this->get(true); } /** * Get all ancestors to this node but not the node itself. In other words it gets the path to the node, without the * node itself. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getAncestors() { $this->scopeAncestorsAndSelf(); $this->scopeWithoutSelf(); return $this->get(true); } /** * Get all ancestors to this node but not the node itself and its root. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getAncestorsWithoutRoot() { $this->scopeAncestors(); $this->scopeWithoutRoot(); return $this->get(true); } /** * Get all sibling nodes, including ourselves * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getSiblingsAndSelf() { $this->scopeSiblingsAndSelf(); return $this->get(true); } /** * Get all sibling nodes, except ourselves * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getSiblings() { $this->scopeSiblings(); return $this->get(true); } /** * Get all leaf nodes in the tree. You may want to use the scopes to narrow down the search in a specific subtree or * path. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getLeaves() { $this->scopeLeaves(); return $this->get(true); } /** * Get all descendant (children) nodes and ourselves. * * Note: all descendant nodes, even descendants of our immediate descendants, will be returned. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getDescendantsAndSelf() { $this->scopeDescendantsAndSelf(); return $this->get(true); } /** * Get only our descendant (children) nodes, not ourselves. * * Note: all descendant nodes, even descendants of our immediate descendants, will be returned. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getDescendants() { $this->scopeDescendants(); return $this->get(true); } /** * Get the immediate descendants (children). Unlike getDescendants it only goes one level deep into the tree * structure. Descendants of descendant nodes will not be returned. * * @codeCoverageIgnore * * @return DataModel\Collection */ public function getImmediateDescendants() { $this->scopeImmediateDescendants(); return $this->get(true); } /** * Returns a hashed array where each element's key is the value of the $key column (default: the ID column of the * table) and its value is the value of the $column column (default: title). Each nesting level will have the value * of the $column column prefixed by a number of $separator strings, as many as its nesting level (depth). * * This is useful for creating HTML select elements showing the hierarchy in a human readable format. * * @param string $column * @param null $key * @param string $separator * * @return array */ public function getNestedList($column = 'title', $key = null, $separator = ' ') { $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); if (empty($key) || !$this->hasField($key)) { $key = $this->getIdFieldName(); } if (empty($column)) { $column = 'title'; } $fldKey = $db->qn($this->getFieldAlias($key)); $fldColumn = $db->qn($this->getFieldAlias($column)); $query = $db->getQuery(true) ->select([ $db->qn('node') . '.' . $fldKey, $db->qn('node') . '.' . $fldColumn, '(COUNT(' . $db->qn('parent') . '.' . $fldKey . ') - 1) AS ' . $db->qn('depth'), ]) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt) ->group($db->qn('node') . '.' . $fldLft) ->order($db->qn('node') . '.' . $fldLft . ' ASC'); $tempResults = $db->setQuery($query)->loadAssocList(); $ret = []; if (!empty($tempResults)) { foreach ($tempResults as $row) { $ret[$row[$key]] = str_repeat($separator, $row['depth']) . $row[$column]; } } return $ret; } /** * Locate a node from a given path, e.g. "/some/other/leaf" * * Notes: * - This will only work when you have a "slug" and a "hash" field in your table. * - If the path starts with "/" we will use the root with lft=1. Otherwise the first component of the path is * supposed to be the slug of the root node. * - If the root node is not found you'll get null as the return value * - You will also get null if any component of the path is not found * * @param string $path The path to locate * * @return TreeModel|null The found node or null if nothing is found */ public function findByPath($path) { // No path? No node. if (empty($path)) { return null; } // Extract the path parts $pathParts = explode('/', $path); $firstElement = array_shift($pathParts); if (!empty($firstElement)) { array_unshift($pathParts, $firstElement); } // Just a slash? Return the root if (empty($pathParts[0])) { return $this->getRoot(); } // Get the quoted field names $db = $this->getDbo(); $fldLeft = $db->qn($this->getFieldAlias('lft')); $fldRight = $db->qn($this->getFieldAlias('rgt')); $fldHash = $db->qn($this->getFieldAlias('hash')); // Get the quoted hashes of the slugs $pathHashesQuoted = []; foreach ($pathParts as $part) { $pathHashesQuoted[] = $db->q(sha1($part)); } // Get all nodes with slugs matching our path $query = $db->getQuery(true) ->select([ $db->qn('node') . '.*', '(COUNT(' . $db->qn('parent') . '.' . $db->qn($this->getFieldAlias('lft')) . ') - 1) AS ' . $db->qn('depth'), ])->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->where($db->qn('node') . '.' . $fldLeft . ' >= ' . $db->qn('parent') . '.' . $fldLeft) ->where($db->qn('node') . '.' . $fldLeft . ' <= ' . $db->qn('parent') . '.' . $fldRight) ->where($db->qn('node') . '.' . $fldHash . ' IN (' . implode(',', $pathHashesQuoted) . ')') ->group($db->qn('node') . '.' . $fldLeft) ->order([ $db->qn('depth') . ' ASC', $db->qn('node') . '.' . $fldLeft . ' ASC', ]); $queryResults = $db->setQuery($query)->loadAssocList(); $pathComponents = []; // Handle paths with (no root slug provided) and without (root slug provided) a leading slash $currentLevel = (substr($path, 0, 1) == '/') ? 0 : -1; $maxLevel = count($pathParts) + $currentLevel; // Initialise the path results array $i = $currentLevel; foreach ($pathParts as $part) { $i++; $pathComponents[$i] = [ 'slug' => $part, 'id' => null, 'lft' => null, 'rgt' => null, ]; } // Search for the best matching nodes $colSlug = $this->getFieldAlias('slug'); $colLft = $this->getFieldAlias('lft'); $colRgt = $this->getFieldAlias('rgt'); $colId = $this->getIdFieldName(); foreach ($queryResults as $row) { if ($row['depth'] == $currentLevel + 1) { if ($row[$colSlug] != $pathComponents[$currentLevel + 1]['slug']) { continue; } if ($currentLevel > 0) { if ($row[$colLft] < $pathComponents[$currentLevel]['lft']) { continue; } if ($row[$colRgt] > $pathComponents[$currentLevel]['rgt']) { continue; } } $currentLevel++; $pathComponents[$currentLevel]['id'] = $row[$colId]; $pathComponents[$currentLevel]['lft'] = $row[$colLft]; $pathComponents[$currentLevel]['rgt'] = $row[$colRgt]; } if ($currentLevel === $maxLevel) { break; } } // Get the last found node $lastNode = array_pop($pathComponents); // If the node exists, return it... if (!empty($lastNode['lft'])) { return $this->getClone()->reset()->where($colLft, '=', $lastNode['lft'])->firstOrFail(); } // ...otherwise return null return null; } /** * Overrides the DataModel's buildQuery to allow nested set searches using the provided scopes * * @param bool $overrideLimits * * @return \JDatabaseQuery */ public function buildQuery($overrideLimits = false) { $db = $this->getDbo(); $query = parent::buildQuery($overrideLimits); // Wipe out select and from sections $query->clear('select'); $query->clear('from'); $query ->select($db->qn('node') . '.*') ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')); if ($this->treeNestedGet) { $query ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')); } return $query; } protected function onAfterDelete($oid) { $db = $this->getDbo(); $myLeft = $this->lft; $myRight = $this->rgt; $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); // Move all siblings to the left $width = $this->rgt - $this->lft + 1; // Wrap everything in a transaction $db->transactionStart(); try { // Shrink lft values $query = $db->getQuery(true) ->update($db->qn($this->getTableName())) ->set($fldLft . ' = ' . $fldLft . ' - ' . $width) ->where($fldLft . ' > ' . $db->q($myLeft)); $db->setQuery($query)->execute(); // Shrink rgt values $query = $db->getQuery(true) ->update($db->qn($this->getTableName())) ->set($fldRgt . ' = ' . $fldRgt . ' - ' . $width) ->where($fldRgt . ' > ' . $db->q($myRight)); $db->setQuery($query)->execute(); // Commit the transaction $db->transactionCommit(); } catch (\Exception $e) { // Roll back the transaction on error $db->transactionRollback(); throw $e; } return $this; } /** * get() will return all ancestor nodes and ourselves * * @return void */ protected function scopeAncestorsAndSelf() { $this->treeNestedGet = true; $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' >= ' . $db->qn('node') . '.' . $fldLft); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' <= ' . $db->qn('node') . '.' . $fldRgt); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft)); } /** * get() will return all ancestor nodes but not ourselves * * @return void */ protected function scopeAncestors() { $this->treeNestedGet = true; $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' > ' . $db->qn('node') . '.' . $fldLft); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' < ' . $db->qn('node') . '.' . $fldRgt); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft)); } /** * get() will return all sibling nodes and ourselves * * @return void */ protected function scopeSiblingsAndSelf() { $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $parent = $this->getParent(); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->q($parent->lft)); $this->whereRaw($db->qn('node') . '.' . $fldRgt . ' < ' . $db->q($parent->rgt)); } /** * get() will return all sibling nodes but not ourselves * * @codeCoverageIgnore * * @return void */ protected function scopeSiblings() { $this->scopeSiblingsAndSelf(); $this->scopeWithoutSelf(); } /** * get() will return only leaf nodes * * @return void */ protected function scopeLeaves() { $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' = ' . $db->qn('node') . '.' . $fldRgt . ' - ' . $db->q(1)); } /** * get() will return all descendants (even subtrees of subtrees!) and ourselves * * @return void */ protected function scopeDescendantsAndSelf() { $this->treeNestedGet = true; $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft)); } /** * get() will return all descendants (even subtrees of subtrees!) but not ourselves * * @return void */ protected function scopeDescendants() { $this->treeNestedGet = true; $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->qn('parent') . '.' . $fldLft); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' < ' . $db->qn('parent') . '.' . $fldRgt); $this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft)); } /** * get() will only return immediate descendants (first level children) of the current node * * @return void * @throws \RuntimeException * */ protected function scopeImmediateDescendants() { // Sanity checks on current node position if ($this->lft >= $this->rgt) { throw new TreeInvalidLftRgtCurrent; } $db = $this->getDbo(); $fldLft = $db->qn($this->getFieldAlias('lft')); $fldRgt = $db->qn($this->getFieldAlias('rgt')); $subQuery = $db->getQuery(true) ->select([ $db->qn('node') . '.' . $fldLft, '(COUNT(*) - 1) AS ' . $db->qn('depth'), ]) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt) ->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft)) ->group($db->qn('node') . '.' . $fldLft) ->order($db->qn('node') . '.' . $fldLft . ' ASC'); $query = $db->getQuery(true) ->select([ $db->qn('node') . '.' . $fldLft, '(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - (' . $db->qn('sub_tree') . '.' . $db->qn('depth') . ' + 1)) AS ' . $db->qn('depth'), ]) ->from($db->qn($this->tableName) . ' AS ' . $db->qn('node')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent')) ->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('sub_parent')) ->join('CROSS', '(' . $subQuery . ') AS ' . $db->qn('sub_tree')) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt) ->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('sub_parent') . '.' . $fldLft) ->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('sub_parent') . '.' . $fldRgt) ->where($db->qn('sub_parent') . '.' . $fldLft . ' = ' . $db->qn('sub_tree') . '.' . $fldLft) ->group($db->qn('node') . '.' . $fldLft) ->having([ $db->qn('depth') . ' > ' . $db->q(0), $db->qn('depth') . ' <= ' . $db->q(1), ]) ->order($db->qn('node') . '.' . $fldLft . ' ASC'); $leftValues = $db->setQuery($query)->loadColumn(); if (empty($leftValues)) { $leftValues = [0]; } array_walk($leftValues, function (&$item, $key) use (&$db) { $item = $db->q($item); }); $this->whereRaw($db->qn('node') . '.' . $fldLft . ' IN (' . implode(',', $leftValues) . ')'); } /** * get() will not return ourselves if it's part of the query results * * @codeCoverageIgnore * * @return void */ protected function scopeWithoutSelf() { $this->withoutNode($this); } /** * get() will not return our root if it's part of the query results * * @codeCoverageIgnore * * @return void */ protected function scopeWithoutRoot() { $rootNode = $this->getRoot(); $this->withoutNode($rootNode); } /** * Resets cached values used to speed up querying the tree * * @return static for chaining */ protected function resetTreeCache() { $this->treeDepth = null; $this->treeRoot = null; $this->treeParent = null; $this->treeNestedGet = false; return $this; } } Model/Mixin/ImplodedArrays.php 0000604 00000002200 15245560676 0012326 0 ustar 00 <?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\Mixin; defined('_JEXEC') || die; /** * Trait for dealing with imploded arrays, stored as comma-separated values */ trait ImplodedArrays { /** * Converts the loaded comma-separated list into an array * * @param string $value The comma-separated list * * @return array The exploded array */ protected function getAttributeForImplodedArray($value) { if (is_array($value)) { return $value; } if (empty($value)) { return []; } $value = explode(',', $value); return array_map('trim', $value); } /** * Converts an array of values into a comma separated list * * @param array|string $value The array of values (or the already imploded array as a string) * * @return string The imploded comma-separated list */ protected function setAttributeForImplodedArray($value) { if (!is_array($value)) { return $value; } $value = array_map('trim', $value); return implode(',', $value); } } Model/Mixin/JsonData.php 0000604 00000001764 15245560676 0011130 0 ustar 00 <?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\Mixin; defined('_JEXEC') || die; /** * Trait for dealing with data stored as JSON-encoded strings */ trait JsonData { /** * Converts the loaded JSON string into an array * * @param string $value The JSON string * * @return array The data */ protected function getAttributeForJson($value) { if (is_array($value)) { return $value; } if (empty($value)) { return []; } $value = json_decode($value, true); if (empty($value)) { return []; } return $value; } /** * Converts and array into a JSON string * * @param array|string $value The data (or its JSON-encoded form) * * @return string The JSON string */ protected function setAttributeForJson($value) { if (!is_array($value)) { return $value; } return json_encode($value); } } Model/Mixin/Generators.php 0000604 00000003667 15245560676 0011542 0 ustar 00 <?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\Mixin; defined('_JEXEC') || die; /** * Trait for PHP 5.5 Generators */ trait Generators { /** * Returns a PHP Generator of DataModel instances based on your currently set Model state. You can foreach() the * returned generator to walk through each item of the data set. * * WARNING! This only works on PHP 5.5 and later. * * When the generator is done you might get a PHP warning. This is normal. Joomla! doesn't support multiple db * cursors being open at once. What we do instead is clone the database object. Of course it cannot close the db * connection when we dispose of it (since it's already in use by Joomla), hence the warning. Pay no attention. * * @param integer $limitstart How many items from the start to skip (0 = do not skip) * @param integer $limit How many items to return (0 = all) * @param bool $overrideLimits Set to true to override limitstart, limit and ordering * * @return \Generator A PHP generator of DataModel objects * @since 3.3.2 * @throws \Exception */ public function &getGenerator($limitstart = 0, $limit = 0, $overrideLimits = false) { $limitstart = max($limitstart, 0); $limit = max($limit, 0); $query = $this->buildQuery($overrideLimits); $db = clone $this->getDbo(); $db->setQuery($query, $limitstart, $limit); $cursor = $db->execute(); $reflectDB = new \ReflectionObject($db); $refFetchAssoc = $reflectDB->getMethod('fetchAssoc'); $refFetchAssoc->setAccessible(true); while ($data = $refFetchAssoc->invoke($db, $cursor)) { $item = clone $this; $item->clearState()->reset(true); $item->bind($data); $item->relationManager = clone $this->relationManager; $item->relationManager->rebase($item); yield $item; } } } Model/Mixin/Assertions.php 0000604 00000004172 15245560676 0011553 0 ustar 00 <?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\Mixin; defined('_JEXEC') || die; use Joomla\CMS\Language\Text; use RuntimeException; /** * Trait for check() method assertions */ trait Assertions { /** * Make sure $condition is true or throw a RuntimeException with the $message language string * * @param bool $condition The condition which must be true * @param string $message The language key for the message to throw * * @throws RuntimeException */ protected function assert($condition, $message) { if (!$condition) { throw new RuntimeException(Text::_($message)); } } /** * Assert that $value is not empty or throw a RuntimeException with the $message language string * * @param mixed $value The value to check * @param string $message The language key for the message to throw * * @throws RuntimeException */ protected function assertNotEmpty($value, $message) { $this->assert(!empty($value), $message); } /** * Assert that $value is set to one of $validValues or throw a RuntimeException with the $message language string * * @param mixed $value The value to check * @param array $validValues An array of valid values for $value * @param string $message The language key for the message to throw * * @throws RuntimeException */ protected function assertInArray($value, array $validValues, $message) { $this->assert(in_array($value, $validValues), $message); } /** * Assert that $value is set to none of $validValues. Otherwise throw a RuntimeException with the $message language * string. * * @param mixed $value The value to check * @param array $validValues An array of invalid values for $value * @param string $message The language key for the message to throw * * @throws \RuntimeException */ protected function assertNotInArray($value, array $validValues, $message) { $this->assert(!in_array($value, $validValues, true), $message); } } Model/Mixin/DateManipulation.php 0000604 00000006315 15245560676 0012660 0 ustar 00 <?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\Mixin; defined('_JEXEC') || die; use FOF40\Date\Date; use FOF40\Model\DataModel; /** * Trait for date manipulations commonly used in models */ trait DateManipulation { /** * Normalise a date into SQL format * * @param string $value The date to normalise * @param string $default The default date to use if the normalised date is invalid or empty (use 'now' for * current date/time) * * @return string */ protected function normaliseDate($value, $default = '2001-01-01') { /** @var DataModel $this */ $db = $this->container->platform->getDbo(); if (empty($value) || ($value == $db->getNullDate())) { $value = $default; } if (empty($value) || ($value == $db->getNullDate())) { return $value; } $regex = '/^\d{1,4}(\/|-)\d{1,2}(\/|-)\d{2,4}[[:space:]]{0,}(\d{1,2}:\d{1,2}(:\d{1,2}){0,1}){0,1}$/'; if (!preg_match($regex, $value)) { $value = $default; } if (empty($value) || ($value == $db->getNullDate())) { return $value; } $date = new Date($value); return $date->toSql(); } /** * Sort the published up/down times in case they are give out of order. If publish_up equals publish_down the * foreverDate will be used for publish_down. * * @param string $publish_up Publish Up date * @param string $publish_down Publish Down date * @param string $foreverDate See above * * @return array (publish_up, publish_down) */ protected function sortPublishDates($publish_up, $publish_down, $foreverDate = '2038-01-18 00:00:00') { $jUp = new Date($publish_up); $jDown = new Date($publish_down); if ($jDown->toUnix() < $jUp->toUnix()) { $temp = $publish_up; $publish_up = $publish_down; $publish_down = $temp; } elseif ($jDown->toUnix() == $jUp->toUnix()) { $jDown = new Date($foreverDate); $publish_down = $jDown->toSql(); } return [$publish_up, $publish_down]; } /** * Publish or unpublish a DataModel item based on its publish_up / publish_down fields * * @param DataModel $row The DataModel to publish/unpublish * * @return void */ protected function publishByDate(DataModel $row) { static $uNow = null; if (is_null($uNow)) { $jNow = new Date(); $uNow = $jNow->toUnix(); } /** @var \JDatabaseDriver $db */ $db = $this->container->platform->getDbo(); $triggered = false; $publishDown = $row->getFieldValue('publish_down'); if (!empty($publishDown) && ($publishDown != $db->getNullDate())) { $publish_down = $this->normaliseDate($publishDown, '2038-01-18 00:00:00'); $publish_up = $this->normaliseDate($row->publish_up, '2001-01-01 00:00:00'); $jDown = new Date($publish_down); $jUp = new Date($publish_up); if (($uNow >= $jDown->toUnix()) && $row->enabled) { $row->enabled = 0; $triggered = true; } elseif (($uNow >= $jUp->toUnix()) && !$row->enabled && ($uNow < $jDown->toUnix())) { $row->enabled = 1; $triggered = true; } } if ($triggered) { $row->save(); } } } Model/Model.php 0000604 00000032651 15245560676 0007400 0 ustar 00 <?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; defined('_JEXEC') || die; use FOF40\Container\Container; use FOF40\Input\Input; use FOF40\Model\Exception\CannotGetName; use Joomla\CMS\Filter\InputFilter; /** * Class Model * * A generic MVC model implementation * * @property-read \FOF40\Input\Input $input The input object (magic __get returns the Input from the Container) */ class Model { /** * Should I save the model's state in the session? * * @var boolean */ protected $_savestate = true; /** * Should we ignore request data when trying to get state data not already set in the Model? * * @var bool */ protected $_ignoreRequest = false; /** * The model (base) name * * @var string */ protected $name; /** * A state object * * @var string */ protected $state; /** * Are the state variables already set? * * @var boolean */ protected $_state_set = false; /** * The container attached to the model * * @var Container */ protected $container; /** * The state key hash returned by getHash(). This is typically something like "com_foobar.example." (note the dot * at the end). Always use getHash to get it and setHash to set it. * * @var null|string */ private $stateHash; /** * Public class constructor * * You can use the $config array to pass some configuration values to the object: * * state stdClass|array. The state variables of the Model. * use_populate Boolean. When true the model will set its state from populateState() instead of the request. * ignore_request Boolean. When true getState will not automatically load state data from the request. * * @param Container $container The configuration variables to this model * @param array $config Configuration values for this model */ public function __construct(Container $container, array $config = []) { $this->container = $container; // Set the model's name from $config if (isset($config['name'])) { $this->name = $config['name']; } // If $config['name'] is not set, auto-detect the model's name $this->name = $this->getName(); // Do we have a configured state hash? Since 3.1.2. if (isset($config['hash']) && !empty($config['hash'])) { $this->setHash($config['hash']); } elseif (isset($config['hash_view']) && !empty($config['hash_view'])) { $this->getHash($config['hash_view']); } // Set the model state if (array_key_exists('state', $config)) { if (is_object($config['state'])) { $this->state = $config['state']; } elseif (is_array($config['state'])) { $this->state = (object) $config['state']; } // Protect vs malformed state else { $this->state = new \stdClass(); } } else { $this->state = new \stdClass(); } // Set the internal state marker if (!empty($config['use_populate'])) { $this->_state_set = true; } // Set the internal state marker if (!empty($config['ignore_request'])) { $this->_ignoreRequest = true; } } /** * Method to get the model name * * The model name. By default parsed using the classname or it can be set * by passing a $config['name'] in the class constructor * * @return string The name of the model * * @throws \RuntimeException If it's impossible to get the name */ public function getName() { if (empty($this->name)) { $r = null; if (!preg_match('/(.*)\\\\Model\\\\(.*)/i', get_class($this), $r)) { throw new CannotGetName; } $this->name = $r[2]; } return $this->name; } /** * Get a filtered state variable * * @param string $key The state variable's name * @param mixed $default The default value to return if it's not already set * @param string $filter_type The filter type to use * * @return mixed The state variable's contents */ public function getState($key = null, $default = null, $filter_type = 'raw') { if (empty($key)) { return $this->internal_getState(); } // Get the savestate status $value = $this->internal_getState($key); // Value is not found in the internal state if (is_null($value)) { // Can I fetch it from the request? if (!$this->_ignoreRequest) { $value = $this->container->platform->getUserStateFromRequest($this->getHash() . $key, $key, $this->input, $value, 'none', $this->_savestate); // Did I get any useful value from the request? if (is_null($value)) { return $default; } } // Nope! Let's return the default value else { return $default; } } if (strtoupper($filter_type) == 'RAW') { return $value; } else { $filter = new InputFilter(); return $filter->clean($value, $filter_type); } } /** * Method to set model state variables * * @param string $property The name of the property. * @param mixed $value The value of the property to set or null. * * @return mixed The previous value of the property or null if not set. */ public function setState($property, $value = null) { if (is_null($this->state)) { $this->state = new \stdClass(); } return $this->state->$property = $value; } /** * Returns a unique hash for each view, used to prefix the state variables to allow us to retrieve them from the * state later on. If it's not already set (with setHash) it will be set in the form com_something.myModel. If you * pass a non-empty $viewName then if it's not already set it will be instead set in the form of * com_something.viewName.myModel which is useful when you are reusing models in multiple views and want to avoid * state bleedover among views. * * Also see the hash and hash_view parameters in the constructor's options. * * @return string */ public function getHash($viewName = null) { if (is_null($this->stateHash)) { $this->stateHash = ucfirst($this->container->componentName) . '.'; if (!empty($viewName)) { $this->stateHash .= $viewName . '.'; } $this->stateHash .= $this->getName() . '.'; } return $this->stateHash; } /** * Sets the unique hash to prefix the state variables. The hash is cleaned according to the 'CMD' input filtering, * must end in a dot (if not a dot is added automatically) and cannot be empty. * * @param string $hash * * @return void * * @see self::getHash() */ public function setHash($hash) { // Clean the hash, it has to conform to 'CMD' filtering $tempInput = new Input(['hash' => $hash]); $hash = $tempInput->getCmd('hash', null); if (empty($hash)) { return; } if (substr($hash, -1) == '_') { $hash = substr($hash, 0, -1); } if (substr($hash, -1) != '.') { $hash .= '.'; } $this->stateHash = $hash; } /** * Clears the model state, but doesn't touch the internal lists of records, * record tables or record id variables. To clear these values, please use * reset(). * * @return static */ public function clearState() { $this->state = new \stdClass(); return $this; } /** * Clones the model object and returns the clone * * @return $this for chaining */ public function getClone() { return clone($this); } /** * Returns a reference to the model's container * * @return \FOF40\Container\Container */ public function getContainer() { return $this->container; } /** * Magic getter; allows to use the name of model state keys as properties. Also handles magic properties: * $this->input mapped to $this->container->input * * @param string $name The state variable key * * @return mixed */ public function __get($name) { // Handle $this->input if ($name == 'input') { return $this->container->input; } return $this->getState($name); } /** * Magic setter; allows to use the name of model state keys as properties * * @param string $name The state variable key * @param mixed $value The state variable value * * @return static */ public function __set($name, $value) { return $this->setState($name, $value); } /** * Magic caller; allows to use the name of model state keys as methods to * set their values. * * @param string $name The state variable key * @param mixed $arguments The state variable contents * * @return static */ public function __call($name, $arguments) { $arg1 = array_shift($arguments); $this->setState($name, $arg1); return $this; } /** * Sets the model state auto-save status. By default the model is set up to * save its state to the session. * * @param boolean $newState True to save the state, false to not save it. * * @return static */ public function savestate($newState) { $this->_savestate = (bool) $newState; return $this; } /** * Public setter for the _savestate variable. Set it to true to save the state * of the Model in the session. * * @return static */ public function populateSavestate() { if (is_null($this->_savestate)) { $savestate = $this->input->getInt('savestate', -999); if ($savestate == -999) { $savestate = true; } $this->savestate($savestate); } } /** * Gets the ignore request flag. When false, getState() will try to populate state variables not already set from * same-named state variables in the request. * * @return boolean */ public function getIgnoreRequest() { return $this->_ignoreRequest; } /** * Sets the ignore request flag. When false, getState() will try to populate state variables not already set from * same-named state variables in the request. * * @param boolean $ignoreRequest * * @return $this for chaining */ public function setIgnoreRequest($ignoreRequest) { $this->_ignoreRequest = $ignoreRequest; return $this; } /** * Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the * original object. The new object is set up to not save its stats, ignore the request when getting state variables * and comes with an empty state. * * @return $this */ public function tmpInstance() { return $this->getClone()->savestate(false)->setIgnoreRequest(true)->clearState(); } /** * Method to auto-populate the model state. * * This method should only be called once per instantiation and is designed * to be called on the first call to the getState() method unless the model * configuration flag to ignore the request is set. * * @return void * * @note Calling getState in this method will result in recursion. */ protected function populateState() { } /** * Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the * object's behaviours dispatcher and Joomla! plugin system. Neither handler is expected to return anything (return * values are ignored). If you want to mark an error and cancel the event you have to raise an exception. * * EXAMPLE * Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456) * The event calls: * 1. $this->onBeforeSomething(123, 456) * 2. $his->behavioursDispatcher->trigger('onBeforeSomething', array(&$this, 123, 456)) * 3. Joomla! plugin event onComFoobarModelItemBeforeSomething($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 void */ protected function triggerEvent($event, array $arguments = []) { // If there is an object method for this event, call it if (method_exists($this, $event)) { $this->{$event}(...$arguments); } // 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); // Trigger the object's behaviours dispatcher, if such a thing exists if (property_exists($this, 'behavioursDispatcher') && method_exists($this->behavioursDispatcher, 'trigger')) { $this->behavioursDispatcher->trigger($event, $arguments); } // Prepare to run the Joomla! plugins now. // 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) . 'Model'; $prefix .= ucfirst($this->getName()); // The event name will be something like onComFoobarItemsBeforeSomething $event = $prefix . $event; // Call the Joomla! plugins $this->container->platform->runPlugins($event, $arguments); } /** * Method to get model state variables * * @param string $property Optional parameter name * @param mixed $default Optional default value * * @return object The property where specified, the state object where omitted */ private function internal_getState($property = null, $default = null) { if (!$this->_state_set) { // Protected method to auto-populate the model state. $this->populateState(); // Set the model state set flag to true. $this->_state_set = true; } if (is_null($property)) { return $this->state; } if (property_exists($this->state, $property)) { return $this->state->$property; } return $default; } } Pimple/ServiceProviderInterface.php 0000604 00000003164 15245560676 0013457 0 ustar 00 <?php /* * This file is part of Pimple. * * Copyright (c) 2009 Fabien Potencier * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is furnished * to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ namespace FOF40\Pimple; defined('_JEXEC') || die; /** * Pimple service provider interface. * * @author Fabien Potencier * @author Dominik Zogg */ interface ServiceProviderInterface { /** * Registers services on the given container. * * This method should only be used to configure services and parameters. * It should not get services. * * @param Container $pimple An Container instance */ public function register(Container $pimple); } Pimple/Container.php 0000604 00000021575 15245560676 0010453 0 ustar 00 <?php /* * This file is part of Pimple. * * Copyright (c) 2009 Fabien Potencier * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is furnished * to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ namespace FOF40\Pimple; defined('_JEXEC') || die; /** * Container main class. * * @author Fabien Potencier */ class Container implements \ArrayAccess { private $values = array(); private $factories; private $protected; private $frozen = array(); private $raw = array(); private $keys = array(); /** * Instantiate the container. * * Objects and parameters can be passed as argument to the constructor. * * @param array $values The parameters or objects. */ public function __construct(array $values = array()) { $this->factories = new \SplObjectStorage(); $this->protected = new \SplObjectStorage(); foreach ($values as $key => $value) { $this->offsetSet($key, $value); } } /** * Sets a parameter or an object. * * Objects must be defined as Closures. * * Allowing any PHP callable leads to difficult to debug problems * as function names (strings) are callable (creating a function with * the same name as an existing parameter would break your container). * * @param string $id The unique identifier for the parameter or object * @param mixed $value The value of the parameter or a closure to define an object * @throws \RuntimeException Prevent override of a frozen service */ #[\ReturnTypeWillChange] public function offsetSet($id, $value) { if (isset($this->frozen[$id])) { throw new \RuntimeException(sprintf('Cannot override frozen service "%s".', $id)); } $this->values[$id] = $value; $this->keys[$id] = true; } /** * Gets a parameter or an object. * * @param string $id The unique identifier for the parameter or object * * @return mixed The value of the parameter or an object * * @throws \InvalidArgumentException if the identifier is not defined */ #[\ReturnTypeWillChange] public function offsetGet($id) { if (!isset($this->keys[$id])) { throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); } if ( isset($this->raw[$id]) || !is_object($this->values[$id]) || isset($this->protected[$this->values[$id]]) || !method_exists($this->values[$id], '__invoke') ) { return $this->values[$id]; } if (isset($this->factories[$this->values[$id]])) { return $this->values[$id]($this); } $raw = $this->values[$id]; $val = $this->values[$id] = $raw($this); $this->raw[$id] = $raw; $this->frozen[$id] = true; return $val; } /** * Checks if a parameter or an object is set. * * @param string $id The unique identifier for the parameter or object * * @return bool */ #[\ReturnTypeWillChange] public function offsetExists($id) { return isset($this->keys[$id]); } /** * Unsets a parameter or an object. * * @param string $id The unique identifier for the parameter or object */ #[\ReturnTypeWillChange] public function offsetUnset($id) { if (isset($this->keys[$id])) { if (is_object($this->values[$id])) { unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); } unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); } } /** * Marks a callable as being a factory service. * * @param callable $callable A service definition to be used as a factory * * @return callable The passed callable * * @throws \InvalidArgumentException Service definition has to be a closure of an invokable object */ public function factory($callable) { if (!is_object($callable) || !method_exists($callable, '__invoke')) { throw new \InvalidArgumentException('Service definition is not a Closure or invokable object.'); } $this->factories->attach($callable); return $callable; } /** * Protects a callable from being interpreted as a service. * * This is useful when you want to store a callable as a parameter. * * @param callable $callable A callable to protect from being ΕνΑLυΑΤΕD (I have to use a stupid mix Greek and leetspeak because low quality hosts blacklist files due to their file scanners being utterly broken) * * @return callable The passed callable * * @throws \InvalidArgumentException Service definition has to be a closure of an invokable object */ public function protect($callable) { if (!is_object($callable) || !method_exists($callable, '__invoke')) { throw new \InvalidArgumentException('Callable is not a Closure or invokable object.'); } $this->protected->attach($callable); return $callable; } /** * Gets a parameter or the closure defining an object. * * @param string $id The unique identifier for the parameter or object * * @return mixed The value of the parameter or the closure defining an object * * @throws \InvalidArgumentException if the identifier is not defined */ public function raw($id) { if (!isset($this->keys[$id])) { throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); } if (isset($this->raw[$id])) { return $this->raw[$id]; } return $this->values[$id]; } /** * Extends an object definition. * * Useful when you want to extend an existing object definition, * without necessarily loading that object. * * @param string $id The unique identifier for the object * @param callable $callable A service definition to extend the original * * @return callable The wrapped callable * * @throws \InvalidArgumentException if the identifier is not defined or not a service definition */ public function extend($id, $callable) { if (!isset($this->keys[$id])) { throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); } if (!is_object($this->values[$id]) || !method_exists($this->values[$id], '__invoke')) { throw new \InvalidArgumentException(sprintf('Identifier "%s" does not contain an object definition.', $id)); } if (!is_object($callable) || !method_exists($callable, '__invoke')) { throw new \InvalidArgumentException('Extension service definition is not a Closure or invokable object.'); } $factory = $this->values[$id]; $extended = function ($c) use ($callable, $factory) { return $callable($factory($c), $c); }; if (isset($this->factories[$factory])) { $this->factories->detach($factory); $this->factories->attach($extended); } return $this[$id] = $extended; } /** * Returns all defined value names. * * @return array An array of value names */ public function keys() { return array_keys($this->values); } /** * Registers a service provider. * * @param ServiceProviderInterface $provider A ServiceProviderInterface instance * @param array $values An array of values that customizes the provider * * @return static */ public function register(ServiceProviderInterface $provider, array $values = array()) { $provider->register($this); foreach ($values as $key => $value) { $this[$key] = $value; } return $this; } } file_fof40.xml 0000604 00000011432 15245560676 0007220 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <!--~ ~ @package FOF ~ @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd ~ @license GNU General Public License version 3, or later --> <!-- A legitimate question among developers reading this file may be why we are using a "files" extension type instead of the "library" type which, on the face of it, seems more appropriate. We have not lost our mind. We are working around the adverse effects of the very different way Joomla treats "library" packages than any other package type. When applying an update to a library package Joomla! will uninstall it BEFORE it executes the installation script's preflight event. This means that any checks made there to prevent the installation of the library in an incompatible environment (e.g. wrong PHP or Joomla! version, or even preventing an accidental downgrade) results in the library files being UNINSTALLED. This is really bad for anyone who tries to install a library package on an unsupported environment. If the library package runs no checks the installed library version causes the extensions that depend on it to crash, taking down the site. If the library package runs checks in the earliest available point in time (preflight) you end up with the old library files having been uninstalled which again causes the extensions that depend on it to crash, taking down the site. No matter what you do, the very action of TRYING to install an unsupported library version KILLS THE SITE. This is madness. Worse than that, this is a known issue in Joomla since ~2017 but nobody will fix it until a new major version. Since this doesn't look likely in Joomla 4.0 we are talking about Joomla 5 which could be anywhere from two to ten years into the future. Clearly this doesn't cut it for us: we don't want trying to install our software causing sites to stop working! The only thing we can do to prevent your sites from crashing to the ground if you try to install a version of our software which does not support your PHP and/or Joomla! versions is to deliver our library as a *files* package. This is nonsensical, it is 100% architecturally wrong BUT it is also the only way we can apply pre-installation checks which fail gracefully instead of causing your site to crash and burn. --> <extension type="file" version="3.9" method="upgrade"> <name>file_fof40</name> <description> <![CDATA[ Framework-on-Framework (FOF) 4.x - The rapid application development framework for Joomla!.<br/> <b>WARNING</b>: This is NOT a duplicate of the FOF library already installed with Joomla! 3. It is a different version used by other extensions on your site. Do NOT uninstall either FOF package. If you do you will break your site. ]]> </description> <creationDate>2022-01-04</creationDate> <author>Nicholas K. Dionysopoulos / Akeeba Ltd</author> <authorEmail>nicholas@akeeba.com</authorEmail> <authorUrl>https://www.akeeba.com</authorUrl> <copyright>Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd</copyright> <license>GNU GPL v3 or later</license> <version>4.1.0</version> <packager>Akeeba Ltd</packager> <packagerurl>https://www.akeeba.com/download.html</packagerurl> <fileset> <files folder="fof" target="libraries/fof40"> <folder>Database</folder> <folder>Configuration</folder> <folder>Update</folder> <folder>InstallScript</folder> <folder>Input</folder> <folder>Layout</folder> <folder>Platform</folder> <folder>Date</folder> <folder>Timer</folder> <folder>Dispatcher</folder> <folder>ViewTemplates</folder> <folder>Toolbar</folder> <folder>Template</folder> <folder>Inflector</folder> <folder>Render</folder> <folder>Utils</folder> <folder>Html</folder> <folder>language</folder> <folder>Cli</folder> <folder>Controller</folder> <folder>Container</folder> <folder>Pimple</folder> <folder>Download</folder> <folder>Params</folder> <folder>Model</folder> <folder>View</folder> <folder>JoomlaAbstraction</folder> <folder>TransparentAuthentication</folder> <folder>IP</folder> <folder>Encrypt</folder> <folder>Event</folder> <folder>Factory</folder> <folder>Autoloader</folder> <file>LICENSE.txt</file> <file>include.php</file> <file>version.txt</file> <file>.htaccess</file> <file>web.config</file> </files> <files folder="fof/language/en-GB" target="language/en-GB"> <file>en-GB.lib_fof40.ini</file> </files> <files folder="fof/language/en-GB" target="administrator/language/en-GB"> <file>en-GB.lib_fof40.ini</file> </files> </fileset> <!-- Installation / uninstallation script file --> <scriptfile>script.fof.php</scriptfile> <updateservers> <server type="extension" priority="1" name="FOF 4.x">http://cdn.akeeba.com/updates/fof4_file.xml</server> </updateservers> </extension>