Файловый менеджер - Редактировать - /home/wuectly/www/03cbe/Utils.tar
Назад
StringHelper.php 0000604 00000004066 15245522752 0007676 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use JLoader; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Log\Log; abstract class StringHelper { /** * Convert a string into a slug (alias), suitable for use in URLs. Please * note that transliteration support is rudimentary at this stage. * * @param string $value A string to convert to slug * * @return string The slug * * @deprecated 3.0 Use \JApplicationHelper::stringURLSafe instead * * @codeCoverageIgnore */ public static function toSlug($value) { if (class_exists('\JLog')) { Log::add('FOF30\\Utils\\StringHelper::toSlug is deprecated. Use \\JApplicationHelper::stringURLSafe instead', Log::WARNING, 'deprecated'); } return ApplicationHelper::stringURLSafe($value); } /** * Convert common northern European languages' letters into plain ASCII. This * is a rudimentary transliteration. * * @param string $value The value to convert to ASCII * * @return string The converted string * * @deprecated 3.0 Use JFactory::getLanguage()->transliterate instead * * @codeCoverageIgnore */ public static function toASCII($value) { if (class_exists('\JLog')) { Log::add('FOF30\\Utils\\StringHelper::toASCII is deprecated. Use JFactory::getLanguage()->transliterate instead', Log::WARNING, 'deprecated'); } $lang = Factory::getLanguage(); return $lang->transliterate($value); } /** * Convert a string to a boolean. * * @param string $string The string. * * @return boolean The converted string */ public static function toBool($string) { $string = trim((string) $string); $string = strtolower($string); if (in_array($string, [1, 'true', 'yes', 'on', 'enabled'], true)) { return true; } if (in_array($string, [0, 'false', 'no', 'off', 'disabled'], true)) { return false; } return (bool) $string; } } ComponentVersion.php 0000604 00000006523 15245522752 0010600 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Factory; 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 = []; /** * 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($component) { 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 name * * @return string The component version or null if none is defined * * @since 3.1.5 */ private static function getVersionFromDatabase($component) { $db = Factory::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 name * * @return string The component version or null if none is defined * * @since 1.2.0 */ private static function getVersionFromManifest($component) { $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]); } } Phpfunc.php 0000604 00000001424 15245522752 0006666 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; /** * Intercept calls to PHP functions. * * Based on the Session package of Aura for PHP – https://github.com/auraphp/Aura.Session * * @method function_exists(string $function) * @method hash_algos() */ class Phpfunc { /** * * Magic call to intercept any function pass to it. * * @param string $func The function to call. * * @param array $args Arguments passed to the function. * * @return mixed The result of the function call. * */ public function __call($func, $args) { return call_user_func_array($func, $args); } } InstallScript/Plugin.php 0000604 00000017263 15245522752 0011324 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils\InstallScript; defined('_JEXEC') || die; use Exception; use FOF30\Database\Installer; use JLoader; use Joomla\CMS\Factory; use Joomla\CMS\Installer\Adapter\ComponentAdapter; use Joomla\CMS\Log\Log; // In case FOF's autoloader is not present yet, e.g. new installation if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true)) { require_once __DIR__ . '/BaseInstaller.php'; } /** * A helper class which you can use to create plugin installation scripts. * * Example usage: class PlgSystemExampleInstallerScript extends FOF30\Utils\InstallScript\Module * * NB: The class name is always Plg<Plugin Folder><Plugin Name>InstallerScript per Joomla's conventions. * * This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well. * Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3. */ class Plugin extends BaseInstaller { /** * The plugins's name, e.g. foobar (for plg_system_foobar). Auto-filled from the class name. * * @var string */ protected $pluginName = ''; /** * The plugins's folder, e.g. system (for plg_system_foobar). Auto-filled from the class name. * * @var string */ protected $pluginFolder = ''; /** * The path where the schema XML files are stored. The path is relative to the folder which contains the extension's * files. * * @var string */ protected $schemaXmlPath = 'sql/xml'; /** * Plugin installer script constructor. */ public function __construct() { // Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary. if (empty($this->pluginFolder) || empty($this->pluginName)) { $class = get_class($this); $words = preg_replace('/(\s)+/', '_', $class); $words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words)); $classParts = explode('_', $words); if (empty($this->pluginFolder)) { $this->pluginFolder = $classParts[1]; } if (empty($this->pluginName)) { $this->pluginName = $classParts[2]; } } } /** * Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to * tell Joomla! if it should abort the installation. * * @param string $type Installation type (install, update, * discover_install) * @param ComponentAdapter $parent Parent object * * @return boolean True to let the installation proceed, false to halt the installation */ public function preflight($type, $parent) { // Check the minimum PHP version if (!$this->checkPHPVersion()) { return false; } // Check the minimum Joomla! version if (!$this->checkJoomlaVersion()) { return false; } // Clear op-code caches to prevent any cached code issues $this->clearOpcodeCaches(); return true; } /** * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing * or updating your component. This is the last chance you've got to perform any additional installations, clean-up, * database updates and similar housekeeping functions. * * @param string $type install, update or discover_update * @param ComponentAdapter $parent Parent object * * @return void * @throws Exception * */ public function postflight($type, $parent) { /** * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to * uninstall). */ // Add ourselves to the list of extensions depending on FOF30 // $this->addDependency('fof30', $this->getDependencyName()); // Install or update database $schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath; if (@is_dir($schemaPath)) { $dbInstaller = new Installer(Factory::getDbo(), $schemaPath); $dbInstaller->updateSchema(); } // Make sure everything is copied properly $this->bugfixFilesNotCopiedOnUpdate($parent); // Add post-installation messages on Joomla! 3.2 and later $this->_applyPostInstallationMessages(); // Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded. $this->clearOpcodeCaches(); } /** * Runs on uninstallation * * @param ComponentAdapter $parent The parent object */ public function uninstall($parent) { // Uninstall database $schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath; // Uninstall database if (@is_dir($schemaPath)) { $dbInstaller = new Installer(Factory::getDbo(), $schemaPath); $dbInstaller->removeSchema(); } // Uninstall post-installation messages on Joomla! 3.2 and later $this->uninstallPostInstallationMessages(); /** * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to * uninstall). */ // Remove ourselves from the list of extensions depending on FOF30 // $this->removeDependency('fof30', $this->getDependencyName()); } /** * Fix for Joomla bug: sometimes files are not copied on update. * * We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files / * folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more * added / modified files and folders you have. We are trying to work around it by retrying the copy operation * ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's * extensions. * * @param ComponentAdapter $parent */ protected function bugfixFilesNotCopiedOnUpdate($parent) { Log::add("Joomla! extension update workaround for $this->pluginFolder plugin $this->pluginName", Log::INFO, 'fof3_extension_installation'); $temporarySource = $parent->getParent()->getPath('source'); $copyMap = [ // Plugin files $temporarySource => JPATH_ROOT . '/plugins/' . $this->pluginFolder . '/' . $this->pluginName, // Language (always stored in administrator for plugins) $temporarySource . '/language' => JPATH_ADMINISTRATOR . '/language', // Media files, e.g. /media/plg_system_foobar $temporarySource . '/media' => JPATH_ROOT . '/media/' . $this->getDependencyName(), ]; foreach ($copyMap as $source => $target) { Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation'); $ignored = []; if ($source == $temporarySource) { $ignored = [ 'index.html', 'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md', 'script.php', 'language', 'media', ]; } $this->recursiveConditionalCopy($source, $target, $ignored); } } /** * Get the extension name for FOF dependency tracking, e.g. plg_system_foobar * * @return string */ protected function getDependencyName() { return 'plg_' . strtolower($this->pluginFolder) . '_' . $this->pluginName; } } InstallScript/BaseInstaller.php 0000604 00000050102 15245522752 0012603 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils\InstallScript; defined('_JEXEC') || die; use DirectoryIterator; use Exception; use FOFTemplateUtils; use JLoader; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Log\Log; class BaseInstaller { /** * The minimum PHP version required to install this extension * * @var string */ protected $minimumPHPVersion = '7.2.0'; /** * The minimum Joomla! version required to install this extension * * @var string */ protected $minimumJoomlaVersion = '3.3.0'; /** * The maximum Joomla! version this extension can be installed on * * @var string */ protected $maximumJoomlaVersion = '4.0.99'; /** * Post-installation message definitions for Joomla! 3.2 or later. * * This array contains the message definitions for the Post-installation Messages component added in Joomla! 3.2 and * later versions. Each element is also a hashed array. For the keys used in these message definitions please see * addPostInstallationMessage * * @var array */ protected $postInstallationMessages = []; /** * Recursively copy a bunch of files, but only if the source and target file have a different size. * * @param string $source Path to copy FROM * @param string $dest Path to copy TO * @param array $ignored List of entries to ignore (first level entries are taken into account) * * @return void */ protected function recursiveConditionalCopy($source, $dest, $ignored = []) { // Make sure source and destination exist if (!@is_dir($source)) { return; } if (!@is_dir($dest)) { if (!@mkdir($dest, 0755)) { Folder::create($dest, 0755); } } if (!@is_dir($dest)) { $this->log(__CLASS__ . ": Cannot create folder $dest"); return; } // List the contents of the source folder try { $di = new DirectoryIterator($source); } catch (Exception $e) { return; } // Process each entry foreach ($di as $entry) { // Ignore dot dirs (. and ..) if ($entry->isDot()) { continue; } $sourcePath = $entry->getPathname(); $fileName = $entry->getFilename(); // Do not copy ignored files if (!empty($ignored) && in_array($fileName, $ignored)) { continue; } // If it's a directory do a recursive copy if ($entry->isDir()) { $this->recursiveConditionalCopy($sourcePath, $dest . DIRECTORY_SEPARATOR . $fileName); continue; } // If it's a file check if it's missing or identical $mustCopy = false; $targetPath = $dest . DIRECTORY_SEPARATOR . $fileName; if (!@is_file($targetPath)) { $mustCopy = true; } else { $sourceSize = @filesize($sourcePath); $targetSize = @filesize($targetPath); $mustCopy = $sourceSize != $targetSize; } if (!$mustCopy) { continue; } if (!@copy($sourcePath, $targetPath)) { if (!File::copy($sourcePath, $targetPath)) { $this->log(__CLASS__ . ": Cannot copy $sourcePath to $targetPath"); } } } } /** * Try to log a warning / error with Joomla * * @param string $message The message to write to the log * @param bool $error Is this an error? If not, it's a warning. (default: false) * @param string $category Log category, default jerror * * @return void */ protected function log($message, $error = false, $category = 'jerror') { // Just in case... if (!class_exists('JLog', true)) { return; } $priority = $error ? Log::ERROR : Log::WARNING; try { Log::add($message, $priority, $category); } catch (Exception $e) { // Swallow the exception. } } /** * Check that the server meets the minimum PHP version requirements. * * @return bool */ protected function checkPHPVersion() { if (!empty($this->minimumPHPVersion)) { if (defined('PHP_VERSION')) { $version = PHP_VERSION; } elseif (function_exists('phpversion')) { $version = phpversion(); } else { $version = '5.0.0'; // all bets are off! } if (!version_compare($version, $this->minimumPHPVersion, 'ge')) { $msg = "<p>You need PHP $this->minimumPHPVersion or later to install this extension</p>"; $this->log($msg); return false; } } return true; } /** * Check the minimum and maximum Joomla! versions for this extension * * @return bool */ protected function checkJoomlaVersion() { if (!empty($this->minimumJoomlaVersion) && !version_compare(JVERSION, $this->minimumJoomlaVersion, 'ge')) { $msg = "<p>You need Joomla! $this->minimumJoomlaVersion or later to install this extension</p>"; $this->log($msg); return false; } // Check the maximum Joomla! version if (!empty($this->maximumJoomlaVersion) && !version_compare(JVERSION, $this->maximumJoomlaVersion, 'le')) { $msg = "<p>You need Joomla! $this->maximumJoomlaVersion or earlier to install this extension</p>"; $this->log($msg); return false; } return true; } /** * Clear PHP opcode caches * * @return void */ protected function clearOpcodeCaches() { // Always reset the OPcache if it's enabled. Otherwise there's a good chance the server will not know we are // replacing .php scripts. This is a major concern since PHP 5.5 included and enabled OPcache by default. if (function_exists('opcache_reset')) { opcache_reset(); } // Also do that for APC cache elseif (function_exists('apc_clear_cache')) { @apc_clear_cache(); } } /** * Get the dependencies for a package from the #__akeeba_common table * * @param string $package The package * * @return array The dependencies */ protected function getDependencies($package) { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select($db->qn('value')) ->from($db->qn('#__akeeba_common')) ->where($db->qn('key') . ' = ' . $db->q($package)); try { $dependencies = $db->setQuery($query)->loadResult(); $dependencies = json_decode($dependencies, true); if (empty($dependencies)) { $dependencies = []; } } catch (Exception $e) { $dependencies = []; } return $dependencies; } /** * Sets the dependencies for a package into the #__akeeba_common table * * @param string $package The package * @param array $dependencies The dependencies list */ protected function setDependencies($package, array $dependencies) { $db = Factory::getDbo(); $query = $db->getQuery(true) ->delete('#__akeeba_common') ->where($db->qn('key') . ' = ' . $db->q($package)); try { $db->setQuery($query)->execute(); } catch (Exception $e) { // Do nothing if the old key wasn't found } $object = (object) [ 'key' => $package, 'value' => json_encode($dependencies), ]; try { $db->insertObject('#__akeeba_common', $object, 'key'); } catch (Exception $e) { // Do nothing if the old key wasn't found } } /** * Adds a package dependency to #__akeeba_common * * @param string $package The package * @param string $dependency The dependency to add */ protected function addDependency($package, $dependency) { $dependencies = $this->getDependencies($package); if (!in_array($dependency, $dependencies)) { $dependencies[] = $dependency; $this->setDependencies($package, $dependencies); } } /** * Removes a package dependency from #__akeeba_common * * @param string $package The package * @param string $dependency The dependency to remove */ protected function removeDependency($package, $dependency) { $dependencies = $this->getDependencies($package); if (in_array($dependency, $dependencies)) { $index = array_search($dependency, $dependencies); unset($dependencies[$index]); $this->setDependencies($package, $dependencies); } } /** * Do I have a dependency for a package in #__akeeba_common * * @param string $package The package * @param string $dependency The dependency to check for * * @return bool */ protected function hasDependency($package, $dependency) { $dependencies = $this->getDependencies($package); return in_array($dependency, $dependencies); } /** * Adds or updates a post-installation message (PIM) definition for Joomla! 3.2 or later. You can use this in your * post-installation script using this code: * * The $options array contains the following mandatory keys: * * extension_id The numeric ID of the extension this message is for (see the #__extensions table) * * type One of message, link or action. Their meaning is: * message Informative message. The user can dismiss it. * link The action button links to a URL. The URL is defined in the action parameter. * action A PHP action takes place when the action button is clicked. You need to specify the * action_file (RAD path to the PHP file) and action (PHP function name) keys. See * below for more information. * * title_key The JText language key for the title of this PIM * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE * * description_key The JText language key for the main body (description) of this PIM * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION * * action_key The JText language key for the action button. Ignored and not required when type=message * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION * * language_extension The extension name which holds the language keys used above. For example, com_foobar, * mod_something, plg_system_whatever, tpl_mytemplate * * language_client_id Should we load the front-end (0) or back-end (1) language keys? * * version_introduced Which was the version of your extension where this message appeared for the first time? * Example: 3.2.1 * * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. * * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message * should be shown to the user. @param array $options See description * * @return void * * @throws Exception * @see Template::parsePath() for RAD path format. Joomla! will include this file * before calling the function defined in the action key below. * Example: admin://components/com_foobar/helpers/postinstall.php * * action The name of a PHP function which will be used to run the action of this PIM. This must be * a * simple PHP user function (not a class method, static method etc) which returns no result. * Example: com_foobar_postinstall_messageone_action * * @see Template::parsePath() for RAD path format. Joomla! * will include this file before calling the condition_method. * Example: admin://components/com_foobar/helpers/postinstall.php * * condition_method The name of a PHP function which will be used to determine whether to show this message to * the user. This must be a simple PHP user function (not a class method, static method etc) * which returns true to show the message and false to hide it. This function is defined in * the condition_file. Example: com_foobar_postinstall_messageone_condition * * When type=message no additional keys are required. * * When type=link the following additional keys are required: * * action The URL which will open when the user clicks on the PIM's action button * Example: index.php?option=com_foobar&view=tools&task=installSampleData * * Then type=action the following additional keys are required: * * action_file The RAD path to a PHP file containing a PHP function which performs the action of this * PIM. * */ protected function addPostInstallationMessage(array $options) { // Make sure there are options set if (!is_array($options)) { throw new Exception('Post-installation message definitions must be of type array', 500); } // Initialise array keys $defaultOptions = [ 'extension_id' => '', 'type' => '', 'title_key' => '', 'description_key' => '', 'action_key' => '', 'language_extension' => '', 'language_client_id' => '', 'action_file' => '', 'action' => '', 'condition_file' => '', 'condition_method' => '', 'version_introduced' => '', 'enabled' => '1', ]; $options = array_merge($defaultOptions, $options); // Array normalisation. Removes array keys not belonging to a definition. $defaultKeys = array_keys($defaultOptions); $allKeys = array_keys($options); $extraKeys = array_diff($allKeys, $defaultKeys); if (!empty($extraKeys)) { foreach ($extraKeys as $key) { unset($options[$key]); } } // Normalisation of integer values $options['extension_id'] = (int) $options['extension_id']; $options['language_client_id'] = (int) $options['language_client_id']; $options['enabled'] = (int) $options['enabled']; // Normalisation of 0/1 values foreach (['language_client_id', 'enabled'] as $key) { $options[$key] = $options[$key] ? 1 : 0; } // Make sure there's an extension_id if (!(int) $options['extension_id']) { throw new Exception('Post-installation message definitions need an extension_id', 500); } // Make sure there's a valid type if (!in_array($options['type'], ['message', 'link', 'action'])) { throw new Exception('Post-installation message definitions need to declare a type of message, link or action', 500); } // Make sure there's a title key if (empty($options['title_key'])) { throw new Exception('Post-installation message definitions need a title key', 500); } // Make sure there's a description key if (empty($options['description_key'])) { throw new Exception('Post-installation message definitions need a description key', 500); } // If the type is anything other than message you need an action key if (($options['type'] != 'message') && empty($options['action_key'])) { throw new Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); } // You must specify the language extension if (empty($options['language_extension'])) { throw new Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); } // The action file and method are only required for the "action" type if ($options['type'] == 'action') { if (empty($options['action_file'])) { throw new Exception('Post-installation message definitions need an action file when they are of type "action"', 500); } $file_path = FOFTemplateUtils::parsePath($options['action_file'], true); if (!@is_file($file_path)) { throw new Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); } if (empty($options['action'])) { throw new Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); } } if ($options['type'] == 'link') { if (empty($options['link'])) { throw new Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); } } // The condition file and method are only required when the type is not "message" if ($options['type'] != 'message') { if (empty($options['condition_file'])) { throw new Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); } $file_path = FOFTemplateUtils::parsePath($options['condition_file'], true); if (!@is_file($file_path)) { throw new Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); } if (empty($options['condition_method'])) { throw new Exception('Post-installation message definitions need a condition method (function name) when they are of type "' . $options['type'] . '"', 500); } } // Check if the definition exists $tableName = '#__postinstall_messages'; $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('*') ->from($db->qn($tableName)) ->where($db->qn('extension_id') . ' = ' . $db->q($options['extension_id'])) ->where($db->qn('type') . ' = ' . $db->q($options['type'])) ->where($db->qn('title_key') . ' = ' . $db->q($options['title_key'])); $existingRow = $db->setQuery($query)->loadAssoc(); // Is the existing definition the same as the one we're trying to save (ignore the enabled flag)? if (!empty($existingRow)) { $same = true; foreach ($options as $k => $v) { if ($k == 'enabled') { continue; } if ($existingRow[$k] != $v) { $same = false; break; } } // Trying to add the same row as the existing one; quit if ($same) { return; } // Otherwise it's not the same row. Remove the old row before insert a new one. $query = $db->getQuery(true) ->delete($db->qn($tableName)) ->where($db->q('extension_id') . ' = ' . $db->q($options['extension_id'])) ->where($db->q('type') . ' = ' . $db->q($options['type'])) ->where($db->q('title_key') . ' = ' . $db->q($options['title_key'])); $db->setQuery($query)->execute(); } // Insert the new row $options = (object) $options; $db->insertObject($tableName, $options); } /** * Applies the post-installation messages for Joomla! 3.2 or later * * @return void */ protected function _applyPostInstallationMessages() { // Make sure it's Joomla! 3.2.0 or later if (!version_compare(JVERSION, '3.2.0', 'ge')) { return; } // Make sure there are post-installation messages if (empty($this->postInstallationMessages)) { return; } // Get the extension ID for our component $db = Factory::getDbo(); $query = $db->getQuery(true); $query->select('extension_id') ->from('#__extensions') ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('element') . ' = ' . $db->q($this->componentName)); $db->setQuery($query); try { $ids = $db->loadColumn(); } catch (Exception $exc) { return; } if (empty($ids)) { return; } $extension_id = array_shift($ids); foreach ($this->postInstallationMessages as $message) { $message['extension_id'] = $extension_id; $this->addPostInstallationMessage($message); } } /** * Uninstalls the post-installation messages for Joomla! 3.2 or later * * @return void */ protected function uninstallPostInstallationMessages() { // Make sure it's Joomla! 3.2.0 or later if (!version_compare(JVERSION, '3.2.0', 'ge')) { return; } // Make sure there are post-installation messages if (empty($this->postInstallationMessages)) { return; } // Get the extension ID for our component $db = Factory::getDbo(); $query = $db->getQuery(true); $query->select('extension_id') ->from('#__extensions') ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('element') . ' = ' . $db->q($this->componentName)); $db->setQuery($query); try { $ids = $db->loadColumn(); } catch (Exception $exc) { return; } if (empty($ids)) { return; } $extension_id = array_shift($ids); $query = $db->getQuery(true) ->delete($db->qn('#__postinstall_messages')) ->where($db->qn('extension_id') . ' = ' . $db->q($extension_id)); try { $db->setQuery($query)->execute(); } catch (Exception $e) { return; } } } InstallScript/Module.php 0000604 00000016574 15245522752 0011317 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils\InstallScript; defined('_JEXEC') || die; use Exception; use FOF30\Database\Installer; use JLoader; use Joomla\CMS\Factory; use Joomla\CMS\Installer\Adapter\ComponentAdapter; use Joomla\CMS\Log\Log; // In case FOF's autoloader is not present yet, e.g. new installation if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true)) { require_once __DIR__ . '/BaseInstaller.php'; } /** * A helper class which you can use to create module installation scripts. * * Example usage: class Mod_ExampleInstallerScript extends FOF30\Utils\InstallScript\Module * * This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well. * Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3. */ class Module extends BaseInstaller { /** * Which side of the site is this module installed in? Use 'site' or 'administrator'. * * @var string */ protected $moduleClient = 'site'; /** * The modules's name, e.g. mod_foobar. Auto-filled from the class name. * * @var string */ protected $moduleName = ''; /** * The path where the schema XML files are stored. The path is relative to the folder which contains the extension's * files. * * @var string */ protected $schemaXmlPath = 'sql/xml'; /** * Module installer script constructor. */ public function __construct() { // Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary. if (empty($this->moduleName)) { $class = get_class($this); $words = preg_replace('/(\s)+/', '_', $class); $words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words)); $classParts = explode('_', $words); $this->moduleName = 'mod_' . $classParts[2]; } } /** * Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to * tell Joomla! if it should abort the installation. * * @param string $type Installation type (install, update, * discover_install) * @param ComponentAdapter $parent Parent object * * @return boolean True to let the installation proceed, false to halt the installation */ public function preflight($type, $parent) { // Check the minimum PHP version if (!$this->checkPHPVersion()) { return false; } // Check the minimum Joomla! version if (!$this->checkJoomlaVersion()) { return false; } // Clear op-code caches to prevent any cached code issues $this->clearOpcodeCaches(); return true; } /** * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing * or updating your component. This is the last chance you've got to perform any additional installations, clean-up, * database updates and similar housekeeping functions. * * @param string $type install, update or discover_update * @param ComponentAdapter $parent Parent object * * @return void * @throws Exception * */ public function postflight($type, $parent) { /** * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to * uninstall). */ // Add ourselves to the list of extensions depending on FOF30 // $this->addDependency('fof30', $this->getDependencyName()); // Install or update database $schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath; if (@is_dir($schemaPath)) { $dbInstaller = new Installer(Factory::getDbo(), $schemaPath); $dbInstaller->updateSchema(); } // Make sure everything is copied properly $this->bugfixFilesNotCopiedOnUpdate($parent); // Add post-installation messages on Joomla! 3.2 and later $this->_applyPostInstallationMessages(); // Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded. $this->clearOpcodeCaches(); } /** * Runs on uninstallation * * @param ComponentAdapter $parent The parent object */ public function uninstall($parent) { // Uninstall database $schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath; // Uninstall database if (@is_dir($schemaPath)) { $dbInstaller = new Installer(Factory::getDbo(), $schemaPath); $dbInstaller->removeSchema(); } // Uninstall post-installation messages on Joomla! 3.2 and later $this->uninstallPostInstallationMessages(); /** * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to * uninstall). */ // Remove ourselves from the list of extensions depending on FOF30 // $this->removeDependency('fof30', $this->getDependencyName()); } /** * Fix for Joomla bug: sometimes files are not copied on update. * * We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files / * folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more * added / modified files and folders you have. We are trying to work around it by retrying the copy operation * ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's * extensions. * * @param ComponentAdapter $parent */ protected function bugfixFilesNotCopiedOnUpdate($parent) { Log::add("Joomla! extension update workaround for $this->moduleClient module $this->moduleName", Log::INFO, 'fof3_extension_installation'); $temporarySource = $parent->getParent()->getPath('source'); $rootFolder = ($this->moduleClient == 'site') ? JPATH_SITE : JPATH_ADMINISTRATOR; $copyMap = [ // Module files $temporarySource => $rootFolder . '/modules/' . $this->moduleName, // Language $temporarySource . '/language' => $rootFolder . '/language', // Media files $temporarySource . '/media' => JPATH_ROOT . '/media/' . $this->moduleName, ]; foreach ($copyMap as $source => $target) { Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation'); $ignored = []; if ($source == $temporarySource) { $ignored = [ 'index.html', 'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md', 'script.php', 'language', 'media', ]; } $this->recursiveConditionalCopy($source, $target, $ignored); } } /** * Get the extension name for FOF dependency tracking, e.g. mod_site_foobar * * @return string */ protected function getDependencyName() { return 'mod_' . strtolower($this->moduleClient) . '_' . substr($this->moduleName, 4); } } InstallScript/Component.php 0000604 00000104610 15245522752 0012021 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils\InstallScript; defined('_JEXEC') || die; use Exception; use FOF30\Database\Installer as DatabaseInstaller; use InvalidArgumentException; use JDatabaseDriver; use JError; use JLoader; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Installer\Adapter\ComponentAdapter; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Menu; use Joomla\CMS\Table\Table; use SimpleXMLElement; use stdClass; // In case FOF's autoloader is not present yet, e.g. new installation if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true)) { require_once __DIR__ . '/BaseInstaller.php'; } /** * A helper class which you can use to create component installation scripts. * * Example usage: class Com_ExampleInstallerScript extends FOF30\Utils\InstallScript\Component * * This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well. * Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3. */ class Component extends BaseInstaller { /** * The component's name. Auto-filled from the class name. * * @var string */ protected $componentName = ''; /** * The title of the component (printed on installation and uninstallation messages) * * @var string */ protected $componentTitle = 'Foobar Component'; /** * The list of obsolete extra modules and plugins to uninstall on component upgrade / installation. * * @var array */ protected $uninstallation_queue = [ // modules => { (folder) => { (module) }* }* 'modules' => [ 'admin' => [], 'site' => [], ], // plugins => { (folder) => { (element) }* }* 'plugins' => [ 'system' => [], ], ]; /** * Obsolete files and folders to remove from the free version only. This is used when you move a feature from the * free version of your extension to its paid version. If you don't have such a distinction you can ignore this. * * @var array */ protected $removeFilesFree = [ 'files' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/helpers/whatever.php' ], 'folders' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/baz' ], ]; /** * Obsolete files and folders to remove from both paid and free releases. This is used when you refactor code and * some files inevitably become obsolete and need to be removed. * * @var array */ protected $removeFilesAllVersions = [ 'files' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/helpers/whatever.php' ], 'folders' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/baz' ], ]; /** * A list of scripts to be copied to the "cli" directory of the site * * @var array */ protected $cliScriptFiles = [ // Use just the filename, e.g. // 'my-cron-script.php' ]; /** * The path inside your package where cli scripts are stored * * @var string */ protected $cliSourcePath = 'cli'; /** * Is the schemaXmlPath class variable a relative path? If set to true the schemaXmlPath variable contains a path * relative to the component's back-end directory. If set to false the schemaXmlPath variable contains an absolute * filesystem path. * * @var boolean */ protected $schemaXmlPathRelative = true; /** * The path where the schema XML files are stored. Its contents depend on the schemaXmlPathRelative variable above * true => schemaXmlPath contains a path relative to the component's back-end directory * false => schemaXmlPath contains an absolute filesystem path * * @var string */ protected $schemaXmlPath = 'sql/xml'; /** * Is this the paid version of the extension? This only determines which files / extensions will be removed. * * @var boolean */ protected $isPaid = false; /** * Module installer script constructor. */ public function __construct() { // Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary. if (empty($this->componentName)) { $class = get_class($this); $words = preg_replace('/(\s)+/', '_', $class); $words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words)); $classParts = explode('_', $words); $this->componentName = 'com_' . $classParts[2]; } } /** * Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to * tell Joomla! if it should abort the installation. * * @param string $type Installation type (install, update, * discover_install) * @param ComponentAdapter $parent Parent object * * @return boolean True to let the installation proceed, false to halt the installation */ public function preflight($type, $parent) { // Check the minimum PHP version if (!$this->checkPHPVersion()) { return false; } // Check the minimum Joomla! version if (!$this->checkJoomlaVersion()) { return false; } // Clear op-code caches to prevent any cached code issues $this->clearOpcodeCaches(); // Workarounds for JInstaller issues. if (in_array($type, ['install', 'discover_install'])) { // Bug fix for "Database function returned no error" $this->bugfixDBFunctionReturnedNoError(); } else { // Bug fix for "Can not build admin menus" $this->bugfixCantBuildAdminMenus(); } return true; } /** * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing * or updating your component. This is the last chance you've got to perform any additional installations, clean-up, * database updates and similar housekeeping functions. * * @param string $type install, update or discover_update * @param ComponentAdapter $parent Parent object * * @return void * @throws Exception * */ public function postflight($type, $parent) { // Add ourselves to the list of extensions depending on FOF30 $this->addDependency('fof30', $this->componentName); // Install or update database $dbInstaller = new DatabaseInstaller(Factory::getDbo(), ($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' . $this->schemaXmlPath ); $dbInstaller->updateSchema(); // These workarounds are only needed, and only work, on Joomla! 3.x if (strpos(JVERSION, '3.') === 0) { // Make sure menu items are installed $this->_createAdminMenus($parent); // Make sure menu items are published $this->_reallyPublishAdminMenuItems($parent); } // Which files should I remove? if ($this->isPaid) { // This is the paid version, only remove the removeFilesAllVersions files $removeFiles = $this->removeFilesAllVersions; } else { // This is the free version, remove the removeFilesAllVersions and removeFilesFree files $removeFiles = ['files' => [], 'folders' => []]; if (isset($this->removeFilesAllVersions['files'])) { if (isset($this->removeFilesFree['files'])) { $removeFiles['files'] = array_merge($this->removeFilesAllVersions['files'], $this->removeFilesFree['files']); } else { $removeFiles['files'] = $this->removeFilesAllVersions['files']; } } elseif (isset($this->removeFilesFree['files'])) { $removeFiles['files'] = $this->removeFilesFree['files']; } if (isset($this->removeFilesAllVersions['folders'])) { if (isset($this->removeFilesFree['folders'])) { $removeFiles['folders'] = array_merge($this->removeFilesAllVersions['folders'], $this->removeFilesFree['folders']); } else { $removeFiles['folders'] = $this->removeFilesAllVersions['folders']; } } elseif (isset($this->removeFilesFree['folders'])) { $removeFiles['folders'] = $this->removeFilesFree['folders']; } } // Remove obsolete files and folders $this->removeFilesAndFolders($removeFiles); // Make sure everything is copied properly $this->bugfixFilesNotCopiedOnUpdate($parent); // Copy the CLI files (if any) $this->copyCliFiles($parent); // Show the post-installation page $this->renderPostInstallation($parent); // Uninstall obsolete sub-extensions $this->uninstallObsoleteSubextensions($parent); // Clear the FOF cache $false = false; $cache = Factory::getCache('fof', ''); $cache->store($false, 'cache', 'fof'); // Make sure the Joomla! menu structure is correct $this->_rebuildMenu(); // Add post-installation messages on Joomla! 3.2 and later $this->_applyPostInstallationMessages(); // Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded. $this->clearOpcodeCaches(); } /** * Runs on uninstallation * * @param ComponentAdapter $parent The parent object */ public function uninstall($parent) { // Uninstall database $dbInstaller = new DatabaseInstaller(Factory::getDbo(), ($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' . $this->schemaXmlPath ); $dbInstaller->removeSchema(); // Uninstall post-installation messages on Joomla! 3.2 and later $this->uninstallPostInstallationMessages(); // Remove ourselves from the list of extensions depending on FOF30 $this->removeDependency('fof30', $this->componentName); // Show the post-uninstallation page $this->renderPostUninstallation($parent); } /** * Copies the CLI scripts into Joomla!'s cli directory * * @param ComponentAdapter $parent */ protected function copyCliFiles($parent) { $src = $parent->getParent()->getPath('source'); foreach ($this->cliScriptFiles as $script) { if (is_file(JPATH_ROOT . '/cli/' . $script)) { File::delete(JPATH_ROOT . '/cli/' . $script); } if (is_file($src . '/' . $this->cliSourcePath . '/' . $script)) { File::copy($src . '/' . $this->cliSourcePath . '/' . $script, JPATH_ROOT . '/cli/' . $script); } } } /** * Fix for Joomla bug: sometimes files are not copied on update. * * We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files / * folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more * added / modified files and folders you have. We are trying to work around it by retrying the copy operation * ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's * extensions. * * @param ComponentAdapter $parent */ protected function bugfixFilesNotCopiedOnUpdate($parent) { Log::add("Joomla! extension update workaround for component $this->componentName", Log::INFO, 'fof3_extension_installation'); $temporarySource = $parent->getParent()->getPath('source'); $copyMap = [ // Backend component files 'backend' => JPATH_ADMINISTRATOR . '/components/' . $this->componentName, 'admin' => JPATH_ADMINISTRATOR . '/components/' . $this->componentName, // Frontend component files 'frontend' => JPATH_SITE . '/components/' . $this->componentName, 'site' => JPATH_SITE . '/components/' . $this->componentName, // Backend language 'language/backend' => JPATH_ADMINISTRATOR . '/language', 'language/admin' => JPATH_ADMINISTRATOR . '/language', // Frontend language 'language/frontend' => JPATH_SITE . '/language', 'language/site' => JPATH_SITE . '/language', // Media files 'media' => JPATH_ROOT . '/media/' . $this->componentName, ]; foreach ($copyMap as $partialSource => $target) { $source = $temporarySource . '/' . $partialSource; Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation'); $this->recursiveConditionalCopy($source, $target); } } /** * Override this method to display a custom component installation message if you so wish * * @param ComponentAdapter $parent Parent class calling us */ protected function renderPostInstallation($parent) { echo "<h3>$this->componentName has been installed</h3>"; } /** * Override this method to display a custom component uninstallation message if you so wish * * @param ComponentAdapter $parent Parent class calling us */ protected function renderPostUninstallation($parent) { echo "<h3>$this->componentName has been uninstalled</h3>"; } /** * Bug fix for "DB function returned no error" */ protected function bugfixDBFunctionReturnedNoError() { $db = Factory::getDbo(); try { // Fix broken #__assets records $this->deleteComponentAssetRecords($db); // Fix broken #__extensions records $this->deleteComponentExtensionRecord($db); /** * Fix broken #__menu records * * Only run on Joomla! versions lower than 3.7. Joomla! 3.7 introduced a backend menu manager which * lets the user create missing menu items. Moreover, it lets them create custom links to the component * which means that our menu deleting code would break them! So we don't run this code in newer Joomla! * versions any more. */ if (version_compare(JVERSION, '3.6.9999', 'le')) { $this->deleteComponentMenuRecord($db); } } catch (Exception $exc) { return; } } /** * Joomla! 1.6+ bug fix for "Can not build admin menus" */ protected function bugfixCantBuildAdminMenus() { $db = Factory::getDbo(); // If there are multiple #__extensions record, keep one of them $query = $db->getQuery(true); $query->select('extension_id') ->from('#__extensions') ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('element') . ' = ' . $db->q($this->componentName)); $db->setQuery($query); try { $ids = $db->loadColumn(); } catch (Exception $exc) { return; } if ((is_array($ids) || $ids instanceof \Countable ? count($ids) : 0) > 1) { asort($ids); $extension_id = array_shift($ids); // Keep the oldest id foreach ($ids as $id) { $query = $db->getQuery(true); $query->delete('#__extensions') ->where($db->qn('extension_id') . ' = ' . $db->q($id)); $db->setQuery($query); try { $db->execute(); } catch (Exception $exc) { // Nothing } } } // If there are multiple assets records, delete all except the oldest one $query = $db->getQuery(true); $query->select('id') ->from('#__assets') ->where($db->qn('name') . ' = ' . $db->q($this->componentName)); $db->setQuery($query); $ids = $db->loadObjectList(); if ((is_array($ids) || $ids instanceof \Countable ? count($ids) : 0) > 1) { asort($ids); $asset_id = array_shift($ids); // Keep the oldest id foreach ($ids as $id) { $query = $db->getQuery(true); $query->delete('#__assets') ->where($db->qn('id') . ' = ' . $db->q($id)); $db->setQuery($query); try { $db->execute(); } catch (Exception $exc) { // Nothing } } } // Remove #__menu records for good measure! –– I think this is not necessary and causes the menu item to // disappear on extension update. /** * $query = $db->getQuery(true); * $query->select('id') * ->from('#__menu') * ->where($db->qn('type') . ' = ' . $db->q('component')) * ->where($db->qn('menutype') . ' = ' . $db->q('main')) * ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName)); * $db->setQuery($query); * * try * { * $ids1 = $db->loadColumn(); * } * catch (Exception $exc) * { * $ids1 = array(); * } * * if (empty($ids1)) * { * $ids1 = array(); * } * * $query = $db->getQuery(true); * $query->select('id') * ->from('#__menu') * ->where($db->qn('type') . ' = ' . $db->q('component')) * ->where($db->qn('menutype') . ' = ' . $db->q('main')) * ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName . '&%')); * $db->setQuery($query); * * try * { * $ids2 = $db->loadColumn(); * } * catch (Exception $exc) * { * $ids2 = array(); * } * * if (empty($ids2)) * { * $ids2 = array(); * } * * $ids = array_merge($ids1, $ids2); * * if (!empty($ids)) * { * foreach ($ids as $id) * { * $query = $db->getQuery(true); * $query->delete('#__menu') * ->where($db->qn('id') . ' = ' . $db->q($id)); * $db->setQuery($query); * * try * { * $db->execute(); * } * catch (Exception $exc) * { * // Nothing * } * } * } * /**/ } /** * Removes obsolete files and folders * * @param array $removeList The files and directories to remove */ protected function removeFilesAndFolders($removeList) { // Remove files if (isset($removeList['files']) && !empty($removeList['files'])) { foreach ($removeList['files'] as $file) { $f = JPATH_ROOT . '/' . $file; if (!is_file($f)) { continue; } File::delete($f); } } // Remove folders if (isset($removeList['folders']) && !empty($removeList['folders'])) { foreach ($removeList['folders'] as $folder) { $f = JPATH_ROOT . '/' . $folder; if (!is_dir($f)) { continue; } Folder::delete($f); } } } /** * Uninstalls obsolete sub-extensions (modules, plugins) bundled with the main extension * * @param ComponentAdapter $parent The parent object * * @return stdClass The sub-extension uninstallation status */ protected function uninstallObsoleteSubextensions($parent) { $db = Factory::getDBO(); $status = new stdClass(); $status->modules = []; $status->plugins = []; // Modules uninstallation if (isset($this->uninstallation_queue['modules']) && count($this->uninstallation_queue['modules'])) { foreach ($this->uninstallation_queue['modules'] as $folder => $modules) { if (is_array($modules) || $modules instanceof \Countable ? count($modules) : 0) { foreach ($modules as $module) { // Find the module ID $sql = $db->getQuery(true) ->select($db->qn('extension_id')) ->from($db->qn('#__extensions')) ->where($db->qn('element') . ' = ' . $db->q('mod_' . $module)) ->where($db->qn('type') . ' = ' . $db->q('module')); $db->setQuery($sql); $id = $db->loadResult(); // Uninstall the module if ($id) { $installer = new Installer; $result = $installer->uninstall('module', $id, 1); $status->modules[] = [ 'name' => 'mod_' . $module, 'client' => $folder, 'result' => $result, ]; } } } } } // Plugins uninstallation if (isset($this->uninstallation_queue['plugins']) && count($this->uninstallation_queue['plugins'])) { foreach ($this->uninstallation_queue['plugins'] as $folder => $plugins) { if (is_array($plugins) || $plugins instanceof \Countable ? count($plugins) : 0) { foreach ($plugins as $plugin) { $sql = $db->getQuery(true) ->select($db->qn('extension_id')) ->from($db->qn('#__extensions')) ->where($db->qn('type') . ' = ' . $db->q('plugin')) ->where($db->qn('element') . ' = ' . $db->q($plugin)) ->where($db->qn('folder') . ' = ' . $db->q($folder)); $db->setQuery($sql); $id = $db->loadResult(); if ($id) { $installer = new Installer; $result = $installer->uninstall('plugin', $id, 1); $status->plugins[] = [ 'name' => 'plg_' . $plugin, 'group' => $folder, 'result' => $result, ]; } } } } } return $status; } /** * @param ComponentAdapter $parent * * @return bool * * @throws Exception When the Joomla! menu is FUBAR */ private function _createAdminMenus($parent) { $db = $parent->getParent()->getDbo(); /** @var Menu $table */ $table = Table::getInstance('menu'); $option = $parent->get('element'); // If a component exists with this option in the table then we don't need to add menus $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->qn('#__menu') . ' AS ' . $db->qn('m')) ->leftJoin($db->qn('#__extensions', 'e') . ' ON ' . $db->qn('m.component_id') . ' = ' . $db->qn('e.extension_id')) ->where($db->qn('m.parent_id') . ' = ' . $db->q(1)) ->where($db->qn('m.client_id') . ' = ' . $db->q(1)) ->where($db->qn('e.type') . ' = ' . $db->q('component')) ->where($db->qn('e.element') . ' = ' . $db->q($option)); $db->setQuery($query); $existingMenus = $db->loadResult(); if ($existingMenus) { return true; } // Let's find the extension id $query->clear() ->select($db->qn('e.extension_id')) ->from($db->qn('#__extensions', 'e')) ->where($db->qn('e.type') . ' = ' . $db->q('component')) ->where($db->qn('e.element') . ' = ' . $db->q($option)); $db->setQuery($query); $componentId = $db->loadResult(); // Ok, now its time to handle the menus. Start with the component root menu, then handle submenus. if (method_exists($parent, 'getManifest')) { $menuElement = $parent->getManifest()->administration->menu; } else { $menuElement = $parent->get('manifest')->administration->menu; } // We need to insert the menu item as the last child of Joomla!'s menu root node. First let's make sure that // it exists. Normally it should be the menu item with ID = 1. $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('id') . ' = ' . $db->q(1)); $rootItemId = $db->setQuery($query)->loadResult(); // If we didn't find the item with ID=1 something has screwed up the menu table, e.g. a bad upgrade script. In // this case we can try to find the root node by title. if (is_null($rootItemId)) { $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('title') . ' = ' . $db->q('Menu_Item_Root')); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } // So, someone changed the title of the menu item too?! Let's find it by alias. if (is_null($rootItemId)) { $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('alias') . ' = ' . $db->q('root')); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } // For crying out loud, they changed the alias too? Fine! Find it by component ID. if (is_null($rootItemId)) { $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('component_id') . ' = ' . $db->q('0')); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } // Um, OK. Still no go. Let's try with minimum lft value. if (is_null($rootItemId)) { $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->order($db->qn('lft') . ' ASC'); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } // I quit. Your site's menu structure is broken. I'll just throw an error. if (is_null($rootItemId)) { throw new Exception("Your site is broken. There is no root menu item. As a result it is impossible to create menu items. The installation of this component has failed. Please fix your database and retry!", 500); } /** @var SimpleXMLElement $menuElement */ if ($menuElement) { $data = []; $data['menutype'] = 'main'; $data['client_id'] = 1; $data['title'] = (string) trim($menuElement); $data['alias'] = (string) $menuElement; $data['link'] = 'index.php?option=' . $option; $data['type'] = 'component'; $data['published'] = 0; $data['parent_id'] = 1; $data['component_id'] = $componentId; $data['img'] = (string) $menuElement->attributes()->img ?: 'class:component'; $data['home'] = 0; $data['path'] = ''; $data['params'] = ''; } // No menu element was specified, Let's make a generic menu item else { $data = []; $data['menutype'] = 'main'; $data['client_id'] = 1; $data['title'] = $option; $data['alias'] = $option; $data['link'] = 'index.php?option=' . $option; $data['type'] = 'component'; $data['published'] = 0; $data['parent_id'] = 1; $data['component_id'] = $componentId; $data['img'] = 'class:component'; $data['home'] = 0; $data['path'] = ''; $data['params'] = ''; } try { $table->setLocation($rootItemId, 'last-child'); } catch (InvalidArgumentException $e) { $this->log($e->getMessage()); return false; } if (!$table->bind($data) || !$table->check() || !$table->store()) { // The menu item already exists. Delete it and retry instead of throwing an error. $query->clear() ->select('id') ->from('#__menu') ->where('menutype = ' . $db->quote('main')) ->where('client_id = 1') ->where('link = ' . $db->quote('index.php?option=' . $option)) ->where('type = ' . $db->quote('component')) ->where('parent_id = 1') ->where('home = 0'); $db->setQuery($query); $menu_ids_level1 = $db->loadColumn(); if (empty($menu_ids_level1)) { // Oops! Could not get the menu ID. Go back and rollback changes. JError::raiseWarning(1, $table->getError()); return false; } else { $ids = implode(',', $menu_ids_level1); $query->clear() ->select('id') ->from('#__menu') ->where('menutype = ' . $db->quote('main')) ->where('client_id = 1') ->where('type = ' . $db->quote('component')) ->where('parent_id in (' . $ids . ')') ->where('level = 2') ->where('home = 0'); $db->setQuery($query); $menu_ids_level2 = $db->loadColumn(); $ids = implode(',', array_merge($menu_ids_level1, $menu_ids_level2)); // Remove the old menu item $query->clear() ->delete('#__menu') ->where('id in (' . $ids . ')'); $db->setQuery($query); $db->execute(); // Retry creating the menu item $table->setLocation($rootItemId, 'last-child'); if (!$table->bind($data) || !$table->check() || !$table->store()) { // Install failed, warn user and rollback changes JError::raiseWarning(1, $table->getError()); return false; } } } /* * Since we have created a menu item, we add it to the installation step stack * so that if we have to rollback the changes we can undo it. */ $parent->getParent()->pushStep(['type' => 'menu', 'id' => $componentId]); /* * Process SubMenus */ if (method_exists($parent, 'getManifest')) { $submenu = $parent->getManifest()->administration->submenu; } else { $submenu = $parent->get('manifest')->administration->submenu; } if (!$submenu) { return true; } $parent_id = $table->id; /** @var SimpleXMLElement $child */ foreach ($submenu->menu as $child) { $data = []; $data['menutype'] = 'main'; $data['client_id'] = 1; $data['title'] = (string) trim($child); $data['alias'] = (string) $child; $data['type'] = 'component'; $data['published'] = 0; $data['parent_id'] = $parent_id; $data['component_id'] = $componentId; $data['img'] = (string) $child->attributes()->img ?: 'class:component'; $data['home'] = 0; // Set the sub menu link if ((string) $child->attributes()->link) { $data['link'] = 'index.php?' . $child->attributes()->link; } else { $request = []; if ((string) $child->attributes()->act) { $request[] = 'act=' . $child->attributes()->act; } if ((string) $child->attributes()->task) { $request[] = 'task=' . $child->attributes()->task; } if ((string) $child->attributes()->controller) { $request[] = 'controller=' . $child->attributes()->controller; } if ((string) $child->attributes()->view) { $request[] = 'view=' . $child->attributes()->view; } if ((string) $child->attributes()->layout) { $request[] = 'layout=' . $child->attributes()->layout; } if ((string) $child->attributes()->sub) { $request[] = 'sub=' . $child->attributes()->sub; } $qstring = (count($request)) ? '&' . implode('&', $request) : ''; $data['link'] = 'index.php?option=' . $option . $qstring; } $table = Table::getInstance('menu'); try { $table->setLocation($parent_id, 'last-child'); } catch (InvalidArgumentException $e) { return false; } if (!$table->bind($data) || !$table->check() || !$table->store()) { // Install failed, rollback changes return false; } /* * Since we have created a menu item, we add it to the installation step stack * so that if we have to rollback the changes we can undo it. */ $parent->getParent()->pushStep(['type' => 'menu', 'id' => $componentId]); } return true; } /** * Make sure the Component menu items are really published! * * @param ComponentAdapter $parent * * @return bool */ private function _reallyPublishAdminMenuItems($parent) { $db = $parent->getParent()->getDbo(); $option = $parent->get('element'); $query = $db->getQuery(true) ->update('#__menu AS m') ->join('LEFT', '#__extensions AS e ON m.component_id = e.extension_id') ->set($db->qn('published') . ' = ' . $db->q(1)) ->where('m.parent_id = 1') ->where('m.client_id = 1') ->where('e.type = ' . $db->quote('component')) ->where('e.element = ' . $db->quote($option)); $db->setQuery($query); try { $db->execute(); } catch (Exception $e) { // If it fails, it fails. Who cares. } } /** * Tells Joomla! to rebuild its menu structure to make triple-sure that the Components menu items really do exist * in the correct place and can really be rendered. */ private function _rebuildMenu() { /** @var Menu $table */ $table = Table::getInstance('menu'); $db = $table->getDbo(); // We need to rebuild the menu based on its root item. By default this is the menu item with ID=1. However, some // crappy upgrade scripts enjoy screwing it up. Hey, ho, the workaround way I go. $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('id') . ' = ' . $db->q(1)); $rootItemId = $db->setQuery($query)->loadResult(); if (is_null($rootItemId)) { // Guess what? The Problem has happened. Let's find the root node by title. $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('title') . ' = ' . $db->q('Menu_Item_Root')); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } if (is_null($rootItemId)) { // Did they change the title too?! Let's find it by alias. $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('alias') . ' = ' . $db->q('root')); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } if (is_null($rootItemId)) { // Woah! The alias is borked, too?! Find it by component ID. $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->where($db->qn('component_id') . ' = ' . $db->q('0')); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } if (is_null($rootItemId)) { // Your site is more of a "shite" than a "site". Let's try with minimum lft value. $rootItemId = null; $query = $db->getQuery(true) ->select($db->qn('id')) ->from($db->qn('#__menu')) ->order($db->qn('lft') . ' ASC'); $rootItemId = $db->setQuery($query, 0, 1)->loadResult(); } if (is_null($rootItemId)) { // I quit. Your site is broken. return false; } $table->rebuild($rootItemId); } /** * Deletes the assets table records for the component * * @param JDatabaseDriver $db * * @return void * * @since 3.0.18 */ private function deleteComponentAssetRecords($db) { $query = $db->getQuery(true); $query->select('id') ->from('#__assets') ->where($db->qn('name') . ' = ' . $db->q($this->componentName)); $db->setQuery($query); $ids = $db->loadColumn(); if (empty($ids)) { return; } foreach ($ids as $id) { $query = $db->getQuery(true); $query->delete('#__assets') ->where($db->qn('id') . ' = ' . $db->q($id)); $db->setQuery($query); try { $db->execute(); } catch (Exception $exc) { // Nothing } } } /** * Deletes the extensions table records for the component * * @param JDatabaseDriver $db * * @return void * * @since 3.0.18 */ private function deleteComponentExtensionRecord($db) { $query = $db->getQuery(true); $query->select('extension_id') ->from('#__extensions') ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('element') . ' = ' . $db->q($this->componentName)); $db->setQuery($query); $ids = $db->loadColumn(); if (empty($ids)) { return; } foreach ($ids as $id) { $query = $db->getQuery(true); $query->delete('#__extensions') ->where($db->qn('extension_id') . ' = ' . $db->q($id)); $db->setQuery($query); try { $db->execute(); } catch (Exception $exc) { // Nothing } } } /** * Deletes the menu table records for the component * * @param JDatabaseDriver $db * * @return void * * @since 3.0.18 */ private function deleteComponentMenuRecord($db) { $query = $db->getQuery(true); $query->select('id') ->from('#__menu') ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('menutype') . ' = ' . $db->q('main')) ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName)); $db->setQuery($query); $ids = $db->loadColumn(); if (empty($ids)) { return; } foreach ($ids as $id) { $query = $db->getQuery(true); $query->delete('#__menu') ->where($db->qn('id') . ' = ' . $db->q($id)); $db->setQuery($query); try { $db->execute(); } catch (Exception $exc) { // Nothing } } } } helpers.php 0000604 00000026413 15245522752 0006732 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 */ /** * This is a modified copy of Laravel 4's "helpers.php" * * Laravel 4 is distributed under the MIT license, see https://github.com/laravel/framework/blob/master/LICENSE.txt */ defined('_JEXEC') || die; if (!function_exists('array_add')) { /** * Add an element to an array if it doesn't exist. * * @param array $array * @param string $key * @param mixed $value * * @return array */ function array_add(array $array, string $key, $value): array { if (!isset($array[$key])) { $array[$key] = $value; } return $array; } } if (!function_exists('array_build')) { /** * Build a new array using a callback. * * @param array $array * @param \Closure $callback * * @return array */ function array_build(array $array, Closure $callback): array { $results = []; foreach ($array as $key => $value) { [$innerKey, $innerValue] = call_user_func($callback, $key, $value); $results[$innerKey] = $innerValue; } return $results; } } if (!function_exists('array_divide')) { /** * Divide an array into two arrays. One with keys and the other with values. * * @param array $array * * @return array */ function array_divide(array $array) { return [array_keys($array), array_values($array)]; } } if (!function_exists('array_dot')) { /** * Flatten a multi-dimensional associative array with dots. * * @param array $array * @param string $prepend * * @return array */ function array_dot(array $array, string $prepend = ''): array { $results = []; foreach ($array as $key => $value) { if (is_array($value)) { $results = array_merge($results, array_dot($value, $prepend . $key . '.')); } else { $results[$prepend . $key] = $value; } } return $results; } } if (!function_exists('array_except')) { /** * Get all of the given array except for a specified array of items. * * @param array $array * @param array $keys * * @return array */ function array_except(array $array, array $keys): array { return array_diff_key($array, array_flip((array) $keys)); } } if (!function_exists('array_fetch')) { /** * Fetch a flattened array of a nested array element. * * @param array $array * @param string $key * * @return array */ function array_fetch(array $array, string $key): array { foreach (explode('.', $key) as $segment) { $results = []; foreach ($array as $value) { $value = (array) $value; $results[] = $value[$segment]; } $array = array_values($results); } return array_values($results); } } if (!function_exists('array_first')) { /** * Return the first element in an array passing a given truth test. * * @param array $array * @param Closure $callback * @param mixed $default * * @return mixed */ function array_first(array $array, callable $callback, $default = null) { foreach ($array as $key => $value) { if (call_user_func($callback, $key, $value)) { return $value; } } return value($default); } } if (!function_exists('array_last')) { /** * Return the last element in an array passing a given truth test. * * @param array $array * @param Closure $callback * @param mixed $default * * @return mixed */ function array_last(array $array, callable $callback, $default = null) { return array_first(array_reverse($array), $callback, $default); } } if (!function_exists('array_flatten')) { /** * Flatten a multi-dimensional array into a single level. * * @param array $array * * @return array */ function array_flatten(array $array): array { $return = []; array_walk_recursive($array, function ($x) use (&$return) { $return[] = $x; }); return $return; } } if (!function_exists('array_forget')) { /** * Remove an array item from a given array using "dot" notation. * * @param array $array * @param string $key * * @return void */ function array_forget(array &$array, string $key): void { $keys = explode('.', $key); while (count($keys) > 1) { $key = array_shift($keys); if (!isset($array[$key]) || !is_array($array[$key])) { return; } $array =& $array[$key]; } unset($array[array_shift($keys)]); } } if (!function_exists('array_get')) { /** * Get an item from an array using "dot" notation. * * @param array $array * @param string $key * @param mixed $default * * @return mixed */ function array_get(array $array, string $key, $default = null) { if (is_null($key)) { return $array; } if (isset($array[$key])) { return $array[$key]; } foreach (explode('.', $key) as $segment) { if (!is_array($array) || !array_key_exists($segment, $array)) { return value($default); } $array = $array[$segment]; } return $array; } } if (!function_exists('array_only')) { /** * Get a subset of the items from the given array. * * @param array $array * @param array $keys * * @return array */ function array_only(array $array, array $keys): array { return array_intersect_key($array, array_flip((array) $keys)); } } if (!function_exists('array_pluck')) { /** * Pluck an array of values from an array. * * @param array $array * @param string $value * @param string $key * * @return array */ function array_pluck(array $array, string $value, ?string $key = null): array { $results = []; foreach ($array as $item) { $itemValue = is_object($item) ? $item->{$value} : $item[$value]; // If the key is "null", we will just append the value to the array and keep // looping. Otherwise we will key the array using the value of the key we // received from the developer. Then we'll return the final array form. if (is_null($key)) { $results[] = $itemValue; } else { $itemKey = is_object($item) ? $item->{$key} : $item[$key]; $results[$itemKey] = $itemValue; } } return $results; } } if (!function_exists('array_pull')) { /** * Get a value from the array, and remove it. * * @param array $array * @param string $key * * @return mixed */ function array_pull(array &$array, string $key) { $value = array_get($array, $key); array_forget($array, $key); return $value; } } if (!function_exists('array_set')) { /** * Set an array item to a given value using "dot" notation. * * If no key is given to the method, the entire array will be replaced. * * @param array $array * @param string $key * @param mixed $value * * @return array */ function array_set(array &$array, string $key, $value): array { if (is_null($key)) { return $array = $value; } $keys = explode('.', $key); while (count($keys) > 1) { $key = array_shift($keys); // If the key doesn't exist at this depth, we will just create an empty array // to hold the next value, allowing us to create the arrays to hold final // values at the correct depth. Then we'll keep digging into the array. if (!isset($array[$key]) || !is_array($array[$key])) { $array[$key] = []; } $array =& $array[$key]; } $array[array_shift($keys)] = $value; return $array; } } if (!function_exists('array_sort')) { /** * Sort the array using the given Closure. * * @param array $array * @param \Closure $callback * * @return array */ function array_sort(array $array, callable $callback): array { return FOF40\Utils\Collection::make($array)->sortBy($callback)->all(); } } if (!function_exists('array_where')) { /** * Filter the array using the given Closure. * * @param array $array * @param \Closure $callback * * @return array */ function array_where(array $array, callable $callback): array { $filtered = []; foreach ($array as $key => $value) { if (call_user_func($callback, $key, $value)) { $filtered[$key] = $value; } } return $filtered; } } if (!function_exists('ends_with')) { /** * Determine if a given string ends with a given substring. * * @param string $haystack * @param string|array $needles * * @return bool */ function ends_with(string $haystack, $needles): bool { foreach ((array) $needles as $needle) { if ((string) $needle === substr($haystack, -strlen($needle))) { return true; } } return false; } } if (!function_exists('last')) { /** * Get the last element from an array. * * @param array $array * * @return mixed */ function last(array $array) { return end($array); } } if (!function_exists('object_get')) { /** * Get an item from an object using "dot" notation. * * @param object $object * @param string $key * @param mixed $default * * @return mixed */ function object_get($object, string $key, $default = null) { if (is_null($key) || trim($key) == '') { return $object; } foreach (explode('.', $key) as $segment) { if (!is_object($object) || !isset($object->{$segment})) { return value($default); } $object = $object->{$segment}; } return $object; } } if (!function_exists('preg_replace_sub')) { /** * Replace a given pattern with each value in the array in sequentially. * * @param string $pattern * @param array $replacements * @param string $subject * * @return string */ function preg_replace_sub(string $pattern, array &$replacements, string $subject): string { return preg_replace_callback($pattern, function ($match) use (&$replacements) { return array_shift($replacements); }, $subject); } } if (!function_exists('starts_with')) { /** * Determine if a given string starts with a given substring. * * @param string $haystack * @param string|array $needles * * @return bool */ function starts_with(string $haystack, $needles): bool { foreach ((array) $needles as $needle) { if ($needle != '' && strpos($haystack, $needle) === 0) { return true; } } return false; } } if (!function_exists('value')) { /** * Return the default value of the given value. * * @param mixed $value * * @return mixed */ function value($value) { return $value instanceof Closure ? $value() : $value; } } if (!function_exists('with')) { /** * Return the given object. Useful for chaining. * * @param mixed $object * * @return mixed */ function with($object) { return $object; } } if (!function_exists('fofStringToBool')) { /** * Convert a string to a boolean. It understands the following human-readable boolean notations 0, 1, true, false, * yes, no, on, off, enabled, disabled. If the value is anything else it will delegate it to PHP's `(bool)` type * casting operator. * * @param string $string The string with the human-readable boolean value. * * @return boolean The converted string */ function fofStringToBool(string $string): bool { $string = trim((string) $string); $string = strtolower($string); if (in_array($string, ['1', 'true', 'yes', 'on', 'enabled'], true)) { return true; } if (in_array($string, ['0', 'false', 'no', 'off', 'disabled'], true)) { return false; } return (bool) $string; } } FilesCheck.php 0000604 00000016521 15245522752 0007267 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\Utils; defined('_JEXEC') || die; use FOF40\Timer\Timer; use Joomla\CMS\Factory as JoomlaFactory; /** * A utility class to check that your extension's files are not missing and have not been tampered with. * * You need a file called fileslist.php in your component's administrator root directory with the following contents: * * $phpFileChecker = array( * 'version' => 'revCEE2DAB', * 'date' => '2014-10-16', * 'directories' => array( * 'administrator/components/com_foobar', * .... * ), * 'files' => array( * 'administrator/components/com_foobar/access.xml' => array('705', '09aa0351a316bf011ecc8c1145134761', * 'b95f00c7b49a07a60570dc674f2497c45c4e7152'), * .... * ) * ); * * All directory and file paths are relative to the site's root * * The directories array is a list of directories which must exist. The files array has the file paths as keys. The * value is a simple array containing the following elements in this order: file size in bytes, MD5 checksum, SHA1 * checksum. */ class FilesCheck { /** @var string The name of the component */ protected $option = ''; /** @var string Current component version */ protected $version; /** @var string Current component release date */ protected $date; /** @var array List of files to check as filepath => (filesize, md5, sha1) */ protected $fileList = []; /** @var array List of directories to check that exist */ protected $dirList = []; /** @var bool Is the reported component version different than the version of the #__extensions table? */ protected $wrongComponentVersion = false; /** @var bool Is the fileslist.php reporting a version different than the reported component version? */ protected $wrongFilesVersion = false; /** * Create and initialise the object * * @param string $componentName Component name, e.g. com_foobar * @param string $version The current component version, as reported by the component * @param string $date The current component release date, as reported by the component */ public function __construct(string $componentName, string $version, string $date) { // Initialise from parameters $this->option = $componentName; $this->version = $version; $this->date = $date; // Retrieve the date and version from the #__extensions table $db = JoomlaFactory::getDbo(); $query = $db->getQuery(true)->select('*')->from($db->qn('#__extensions')) ->where($db->qn('element') . ' = ' . $db->q($this->option)) ->where($db->qn('type') . ' = ' . $db->q('component')); $extension = $db->setQuery($query)->loadObject(); // Check the version and date against those from #__extensions. I hate heavily nested IFs as much as the next // guy, but what can you do... if (!is_null($extension)) { $manifestCache = $extension->manifest_cache; if (!empty($manifestCache)) { $manifestCache = json_decode($manifestCache, true); if (is_array($manifestCache) && isset($manifestCache['creationDate']) && isset($manifestCache['version'])) { // Make sure the fileslist.php version and date match the component's version if ($this->version != $manifestCache['version']) { $this->wrongComponentVersion = true; } if ($this->date != $manifestCache['creationDate']) { $this->wrongComponentVersion = true; } } } } // Try to load the fileslist.php file from the component's back-end root $filePath = JPATH_ADMINISTRATOR . '/components/' . $this->option . '/fileslist.php'; if (!file_exists($filePath)) { return; } $couldInclude = @include($filePath); // If we couldn't include the file with the array OR if it didn't define the array we have to quit. if (!$couldInclude || !isset($phpFileChecker)) { return; } // Make sure the fileslist.php version and date match the component's version if ($this->version != $phpFileChecker['version']) { $this->wrongFilesVersion = true; } if ($this->date != $phpFileChecker['date']) { $this->wrongFilesVersion = true; } // Initialise the files and directories lists $this->fileList = $phpFileChecker['files']; $this->dirList = $phpFileChecker['directories']; } /** * Is the reported component version different than the version of the #__extensions table? * * @return boolean */ public function isWrongComponentVersion(): bool { return $this->wrongComponentVersion; } /** * Is the fileslist.php reporting a version different than the reported component version? * * @return boolean */ public function isWrongFilesVersion(): bool { return $this->wrongFilesVersion; } /** * Performs a fast check of file and folders. If even one of the files/folders doesn't exist, or even one file has * the wrong file size it will return false. * * @return bool False when there are mismatched files and directories */ public function fastCheck(): bool { // Check that all directories exist foreach ($this->dirList as $directory) { $directory = JPATH_ROOT . '/' . $directory; if (!@is_dir($directory)) { return false; } } // Check that all files exist and have the right size foreach ($this->fileList as $filePath => $fileData) { $filePath = JPATH_ROOT . '/' . $filePath; if (!@file_exists($filePath)) { return false; } $fileSize = @filesize($filePath); if ($fileSize != $fileData[0]) { return false; } } return true; } /** * Performs a slow, thorough check of all files and folders (including MD5/SHA1 sum checks) * * @param int $idx The index from where to start * * @return array Progress report */ public function slowCheck(int $idx = 0): array { $ret = [ 'done' => false, 'files' => [], 'folders' => [], 'idx' => $idx, ]; $totalFiles = count($this->fileList); $totalFolders = count($this->dirList); $fileKeys = array_keys($this->fileList); $timer = new Timer(3.0, 75.0); while ($timer->getTimeLeft() && (($idx < $totalFiles) || ($idx < $totalFolders))) { if ($idx < $totalFolders) { $directory = JPATH_ROOT . '/' . $this->dirList[$idx]; if (!@is_dir($directory)) { $ret['folders'][] = $directory; } } if ($idx < $totalFiles) { $fileKey = $fileKeys[$idx]; $filePath = JPATH_ROOT . '/' . $fileKey; $fileData = $this->fileList[$fileKey]; if (!@file_exists($filePath)) { $ret['files'][] = $fileKey . ' (missing)'; } elseif (@filesize($filePath) != $fileData[0]) { $ret['files'][] = $fileKey . ' (size ' . @filesize($filePath) . ' ≠ ' . $fileData[0] . ')'; } elseif (function_exists('sha1_file')) { $fileSha1 = @sha1_file($filePath); if ($fileSha1 != $fileData[2]) { $ret['files'][] = $fileKey . ' (SHA1 ' . $fileSha1 . ' ≠ ' . $fileData[2] . ')'; } } elseif (function_exists('md5_file')) { $fileMd5 = @md5_file($filePath); if ($fileMd5 != $fileData[1]) { $ret['files'][] = $fileKey . ' (MD5 ' . $fileMd5 . ' ≠ ' . $fileData[1] . ')'; } } } $idx++; } if (($idx >= $totalFiles) && ($idx >= $totalFolders)) { $ret['done'] = true; } $ret['idx'] = $idx; return $ret; } } CliSessionHandler.php 0000604 00000006307 15245522752 0010641 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\Utils; defined('_JEXEC') || die; use FOF40\Encrypt\Randval; use JSessionHandlerInterface; use RuntimeException; /** * CLI session handler for Joomla 3.x */ class CliSessionHandler implements JSessionHandlerInterface { private $id; private $name = 'clisession'; public function __construct() { $this->makeId(); } /** * Starts the session. * * @return boolean True if started. * * @throws RuntimeException If something goes wrong starting the session. * @since 3.4.8 */ public function start() { return true; } /** * Checks if the session is started. * * @return boolean True if started, false otherwise. * * @since 3.4.8 */ public function isStarted() { return true; } /** * Returns the session ID * * @return string The session ID * * @since 3.4.8 */ public function getId() { return $this->id; } /** * Sets the session ID * * @param string $id The session ID * * @return void * * @since 3.4.8 */ public function setId($id) { $this->id = $id; } /** * Returns the session name * * @return mixed The session name. * * @since 3.4.8 */ public function getName() { return $this->name; } /** * Sets the session name * * @param string $name The name of the session * * @return void * * @since 3.4.8 */ public function setName($name) { $this->name = $name; } /** * Regenerates ID that represents this storage. * * Note regenerate+destroy should not clear the session data in memory only delete the session data from persistent * storage. * * @param boolean $destroy Destroy session when regenerating? * @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value will leave the system * settings unchanged, * 0 sets the cookie to expire with browser session. Time is in seconds, and is not a * Unix timestamp. * * @return boolean True if session regenerated, false if error * * @since 3.4.8 */ public function regenerate($destroy = false, $lifetime = null) { $this->makeId(); return true; } /** * Force the session to be saved and closed. * * This method must invoke session_write_close() unless this interface is used for a storage object design for unit * or functional testing where a real PHP session would interfere with testing, in which case it should actually * persist the session data if required. * * @return void * * @throws RuntimeException If the session is saved without being started, or if the session is already closed. * @since 3.4.8 * @see session_write_close() */ public function save() { // No operation. This is a CLI session, we save nothing. } /** * Clear all session data in memory. * * @return void * * @since 3.4.8 */ public function clear() { $this->makeId(); } private function makeId() { $rand = new Randval(); $this->id = md5($rand->generate(32)); } } FEFHelper/Html.php 0000604 00000005145 15245522752 0007733 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils\FEFHelper; defined('_JEXEC') || die; use FOF30\View\DataView\DataViewInterface; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Pagination\Pagination; /** * Interim FEF helper which was used in FOF 3.2. This is deprecated. Please use the FEFHelper.browse JHtml helper * instead. The implementation of this class should be a good hint on how you can do that. * * @deprecated 4.0 */ abstract class Html { /** * Helper function to create Javascript code required for table ordering * * @param string $order Current order * * @return string Javascript to add to the page */ public static function jsOrderingBackend($order) { return HTMLHelper::_('FEFHelper.browse.orderjs', $order, true); } /** * Creates the required HTML code for backend pagination and sorting * * @param Pagination $pagination Pagination object * @param array $sortFields Fields allowed to be sorted * @param string $order Ordering field * @param string $order_Dir Ordering direction (ASC, DESC) * * @return string */ public static function selectOrderingBackend($pagination, $sortFields, $order, $order_Dir) { if (is_null($sortFields)) { $sortFields = []; } if (is_string($sortFields)) { $sortFields = [$sortFields]; } if (!is_array($sortFields)) { $sortFields = []; } return '<div class="akeeba-filter-bar akeeba-filter-bar--right">' . HTMLHelper::_('FEFHelper.browse.orderheader', null, $sortFields, $pagination, $order, $order_Dir) . '</div>'; } /** * Returns the drag'n'drop reordering field for Browse views * * @param DataViewInterface $view The DataView you're rendering against * @param string $orderingField The name of the field you're ordering by * @param string $order The order value of the current row * @param string $class CSS class for the ordering value INPUT field * @param string $icon CSS class for the d'n'd handle icon * @param string $inactiveIcon CSS class for the d'n'd disabled icon * * @return string */ public static function dragDropReordering(DataViewInterface $view, $orderingField, $order, $class = 'input-sm', $icon = 'akion-drag', $inactiveIcon = 'akion-android-more-vertical') { return HTMLHelper::_('FEFHelper.browse.order', $orderingField, $order, $class, $icon, $inactiveIcon, $view); } } FEFHelper/BrowseView.php 0000604 00000067172 15245522752 0011133 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils\FEFHelper; defined('_JEXEC') || die; use Exception; use FOF30\Container\Container; use FOF30\Model\DataModel; use FOF30\Utils\ArrayHelper; use FOF30\Utils\SelectOptions; use FOF30\View\DataView\DataViewInterface; use FOF30\View\View; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use RuntimeException; /** * An HTML helper for Browse views. * * It reintroduces a FEF-friendly of some of the functionality found in FOF 3's Header and Field classes. These * helpers are also accessible through Blade, making the transition from XML forms to Blade templates easier. * * @since 3.3.0 */ abstract class BrowseView { /** * Caches the results of getOptionsFromModel keyed by a hash. The hash is computed by the model * name, the model state and the options passed to getOptionsFromModel. * * @var array */ private static $cacheModelOptions = []; /** * Get the translation key for a field's label * * @param string $fieldName The field name * * @return string * * @since 3.3.0 */ public static function fieldLabelKey($fieldName) { $view = self::getViewFromBacktrace(); try { $inflector = $view->getContainer()->inflector; $viewName = $inflector->singularize($view->getName()); $altViewName = $inflector->pluralize($view->getName()); $componentName = $view->getContainer()->componentName; $keys = [ strtoupper($componentName . '_' . $viewName . '_FIELD_' . $fieldName), strtoupper($componentName . '_' . $altViewName . '_FIELD_' . $fieldName), strtoupper($componentName . '_' . $viewName . '_' . $fieldName), strtoupper($componentName . '_' . $altViewName . '_' . $fieldName), ]; foreach ($keys as $key) { if (Text::_($key) != $key) { return $key; } } return $keys[0]; } catch (Exception $e) { return ucfirst($fieldName); } } /** * Returns the label for a field (translated) * * @param string $fieldName The field name * * @return string */ public static function fieldLabel($fieldName) { return Text::_(self::fieldLabelKey($fieldName)); } /** * Return a table field header which sorts the table by that field upon clicking * * @param string $field The name of the field * @param string|null $langKey (optional) The language key for the header to be displayed * * @return mixed */ public static function sortgrid($field, $langKey = null) { /** @var DataViewInterface $view */ $view = self::getViewFromBacktrace(); if (is_null($langKey)) { $langKey = self::fieldLabelKey($field); } return HTMLHelper::_('FEFHelper.browse.sort', $langKey, $field, $view->getLists()->order_Dir, $view->getLists()->order, $view->getTask()); } /** * Create a browse view filter from values returned by a model * * @param string $localField Field name * @param string $modelTitleField Foreign model field for drop-down display values * @param null $modelName Foreign model name * @param string $placeholder Placeholder for no selection * @param array $params Generic select display parameters * * @return string * * @since 3.3.0 */ public static function modelFilter($localField, $modelTitleField = 'title', $modelName = null, $placeholder = null, array $params = []) { /** @var DataModel $model */ $model = self::getViewFromBacktrace()->getModel(); if (empty($modelName)) { $modelName = $model->getForeignModelNameFor($localField); } if (is_null($placeholder)) { $placeholder = self::fieldLabelKey($localField); } $params = array_merge([ 'list.none' => '— ' . Text::_($placeholder) . ' —', 'value_field' => $modelTitleField, 'fof.autosubmit' => true, ], $params); return self::modelSelect($localField, $modelName, $model->getState($localField), $params); } /** * Display a text filter (search box) * * @param string $localField The name of the model field. Used when getting the filter state. * @param string $searchField The INPUT element's name. Default: "filter_$localField". * @param string $placeholder The JText language key for the placeholder. Default: extrapolate from $localField. * @param array $attributes HTML attributes for the INPUT element. * * @return string * * @since 3.3.0 */ public static function searchFilter($localField, $searchField = null, $placeholder = null, array $attributes = []) { /** @var DataModel $model */ $view = self::getViewFromBacktrace(); $model = $view->getModel(); $searchField = empty($searchField) ? $localField : $searchField; $placeholder = empty($placeholder) ? self::fieldLabelKey($localField) : $placeholder; $attributes['type'] = $attributes['type'] ?? 'text'; $attributes['name'] = $searchField; $attributes['id'] = !isset($attributes['id']) ? "filter_$localField" : $attributes['id']; $attributes['onchange'] = !isset($attributes['onchange']) ? 'document.adminForm.submit()' : null; $attributes['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder']; $attributes['title'] = $attributes['title'] ?? $attributes['placeholder']; $attributes['value'] = $view->escape($model->getState($localField)); // Remove null attributes and collapse into a string $attributes = array_filter($attributes, function ($v) { return !is_null($v); }); $attributes = ArrayHelper::toString($attributes); return "<input $attributes />"; } /** * Create a browse view filter with dropdown values * * @param string $localField Field name * @param array $options The JHtml options list to use * @param string $placeholder Placeholder for no selection * @param array $params Generic select display parameters * * @return string * * @since 3.3.0 */ public static function selectFilter($localField, array $options, $placeholder = null, array $params = []) { /** @var DataModel $model */ $model = self::getViewFromBacktrace()->getModel(); if (is_null($placeholder)) { $placeholder = self::fieldLabelKey($localField); } $params = array_merge([ 'list.none' => '— ' . Text::_($placeholder) . ' —', 'fof.autosubmit' => true, ], $params); return self::genericSelect($localField, $options, $model->getState($localField), $params); } /** * View access dropdown filter * * @param string $localField Field name * @param string $placeholder Placeholder for no selection * @param array $params Generic select display parameters * * @return string * * @since 3.3.0 */ public static function accessFilter($localField, $placeholder = null, array $params = []) { return self::selectFilter($localField, SelectOptions::getOptions('access', $params), $placeholder, $params); } /** * Published state dropdown filter * * @param string $localField Field name * @param string $placeholder Placeholder for no selection * @param array $params Generic select display parameters * * @return string * * @since 3.3.0 */ public static function publishedFilter($localField, $placeholder = null, array $params = []) { return self::selectFilter($localField, SelectOptions::getOptions('published', $params), $placeholder, $params); } /** * Create a select box from the values returned by a model * * @param string $name Field name * @param string $modelName The name of the model, e.g. "items" or "com_foobar.items" * @param string $currentValue The currently selected value * @param array $params Passed to optionsFromModel and genericSelect * @param array $modelState Optional state variables to pass to the model * @param array $options Any JHtml select options you want to add in front of the model's returned values * * @return string * * @see self::getOptionsFromModel * @see self::getOptionsFromSource * @see self::genericSelect * * @since 3.3.0 */ public static function modelSelect($name, $modelName, $currentValue, array $params = [], array $modelState = [], array $options = []) { $params = array_merge([ 'fof.autosubmit' => true, ], $params); $options = self::getOptionsFromModel($modelName, $params, $modelState, $options); return self::genericSelect($name, $options, $currentValue, $params); } /** * Get a (human readable) title from a (typically numeric, foreign key) key value using the data * returned by a DataModel. * * @param string $value The key value * @param string $modelName The name of the model, e.g. "items" or "com_foobar.items" * @param array $params Passed to getOptionsFromModel * @param array $modelState Optional state variables to pass to the model * @param array $options Any JHtml select options you want to add in front of the model's returned values * * @return string * * @see self::getOptionsFromModel * @see self::getOptionsFromSource * @see self::genericSelect * * @since 3.3.0 */ public static function modelOptionName($value, $modelName = null, array $params = [], array $modelState = [], array $options = []) { if (!isset($params['cache'])) { $params['cache'] = true; } if (!isset($params['none_as_zero'])) { $params['none_as_zero'] = true; } $options = self::getOptionsFromModel($modelName, $params, $modelState, $options); return self::getOptionName($value, $options); } /** * Gets the active option's label given an array of JHtml options * * @param mixed $selected The currently selected value * @param array $data The JHtml options to parse * @param string $optKey Key name, default: value * @param string $optText Value name, default: text * @param bool $selectFirst Should I automatically select the first option? Default: true * * @return mixed The label of the currently selected option */ public static function getOptionName($selected, $data, $optKey = 'value', $optText = 'text', $selectFirst = true) { $ret = null; foreach ($data as $elementKey => &$element) { if (is_array($element)) { $key = $optKey === null ? $elementKey : $element[$optKey]; $text = $element[$optText]; } elseif (is_object($element)) { $key = $optKey === null ? $elementKey : $element->$optKey; $text = $element->$optText; } else { // This is a simple associative array $key = $elementKey; $text = $element; } if (is_null($ret) && $selectFirst && ($selected == $key)) { $ret = $text; } elseif ($selected == $key) { $ret = $text; } } return $ret; } /** * Create a generic select list based on a bunch of options. Option sources will be merged into the provided * options automatically. * * Parameters: * - format.depth The current indent depth. * - format.eol The end of line string, default is linefeed. * - format.indent The string to use for indentation, default is tab. * - groups If set, looks for keys with the value "<optgroup>" and synthesizes groups from them. Deprecated. * Default: true. * - list.select Either the value of one selected option or an array of selected options. Default: $currentValue. * - list.translate If true, text and labels are translated via JText::_(). Default is false. * - list.attr HTML element attributes (key/value array or string) * - list.none Placeholder for no selection (creates an option with an empty string key) * - option.id The property in each option array to use as the selection id attribute. Defaults: null. * - option.key The property in each option array to use as the Default: "value". If set to null, the index of the * option array is used. * - option.label The property in each option array to use as the selection label attribute. Default: null * - option.text The property in each option array to use as the displayed text. Default: "text". If set to null, * the option array is assumed to be a list of displayable scalars. * - option.attr The property in each option array to use for additional selection attributes. Defaults: null. * - option.disable: The property that will hold the disabled state. Defaults to "disable". * - fof.autosubmit Should I auto-submit the form on change? Default: true * - fof.formname Form to auto-submit. Default: adminForm * - class CSS class to apply * - size Size attribute for the input * - multiple Is this a multiple select? Default: false. * - required Is this a required field? Default: false. * - autofocus Should I focus this field automatically? Default: false * - disabled Is this a disabled field? Default: false * - readonly Render as a readonly field with hidden inputs? Overrides 'disabled'. Default: false * - onchange Custom onchange handler. Overrides fof.autosubmit. Default: NULL (use fof.autosubmit). * * @param $name * @param array $options * @param $currentValue * @param array $params * * @return string * * @since 3.3.0 */ public static function genericSelect($name, array $options, $currentValue, array $params = []) { $params = array_merge([ 'format.depth' => 0, 'format.eol' => "\n", 'format.indent' => "\t", 'groups' => true, 'list.select' => $currentValue, 'list.translate' => false, 'option.id' => null, 'option.key' => 'value', 'option.label' => null, 'option.text' => 'text', 'option.attr' => null, 'option.disable' => 'disable', 'list.attr' => '', 'list.none' => '', 'id' => null, 'fof.autosubmit' => true, 'fof.formname' => 'adminForm', 'class' => '', 'size' => '', 'multiple' => false, 'required' => false, 'autofocus' => false, 'disabled' => false, 'onchange' => null, 'readonly' => false, ], $params); $currentValue = $params['list.select']; // If fof.autosubmit is enabled and onchange is not set we will add our own handler if ($params['fof.autosubmit'] && is_null($params['onchange'])) { $formName = $params['fof.formname'] ?: 'adminForm'; $params['onchange'] = "document.{$formName}.submit()"; } // Construct SELECT element's attributes $attr = ''; $attr .= $params['class'] ? ' class="' . $params['class'] . '"' : ''; $attr .= !empty($params['size']) ? ' size="' . $params['size'] . '"' : ''; $attr .= $params['multiple'] ? ' multiple' : ''; $attr .= $params['required'] ? ' required aria-required="true"' : ''; $attr .= $params['autofocus'] ? ' autofocus' : ''; $attr .= ($params['disabled'] || $params['readonly']) ? ' disabled="disabled"' : ''; $attr .= $params['onchange'] ? ' onchange="' . $params['onchange'] . '"' : ''; // We use the constructed SELECT element's attributes only if no 'attr' key was provided if (empty($params['list.attr'])) { $params['list.attr'] = $attr; } // Merge the options with those fetched from a source (e.g. another Helper object) $options = array_merge($options, self::getOptionsFromSource($params)); if (!empty($params['list.none'])) { array_unshift($options, HTMLHelper::_('FEFHelper.select.option', '', Text::_($params['list.none']))); } $html = []; // Create a read-only list (no name) with hidden input(s) to store the value(s). if ($params['readonly']) { $html[] = HTMLHelper::_('FEFHelper.select.genericlist', $options, $name, $params); // E.g. form field type tag sends $this->value as array if ($params['multiple'] && is_array($currentValue)) { if (!count($currentValue)) { $currentValue[] = ''; } foreach ($currentValue as $value) { $html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>'; } } else { $html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>'; } } else // Create a regular list. { $html[] = HTMLHelper::_('FEFHelper.select.genericlist', $options, $name, $params); } return implode($html); } /** * Replace tags that reference fields with their values * * @param string $text Text to process * @param DataModel $item The DataModel instance to get values from * * @return string Text with tags replace * * @since 3.3.0 */ public static function parseFieldTags($text, DataModel $item) { $ret = $text; if (empty($item)) { return $ret; } /** * Replace [ITEM:ID] in the URL with the item's key value (usually: the auto-incrementing numeric ID) */ $replace = $item->getId(); $ret = str_replace('[ITEM:ID]', $replace, $ret); // Replace the [ITEMID] in the URL with the current Itemid parameter $ret = str_replace('[ITEMID]', $item->getContainer()->input->getInt('Itemid', 0), $ret); // Replace the [TOKEN] in the URL with the Joomla! form token $ret = str_replace('[TOKEN]', $item->getContainer()->platform->getToken(true), $ret); // Replace other field variables in the URL $data = $item->getData(); foreach ($data as $field => $value) { // Skip non-processable values if (is_array($value) || is_object($value)) { continue; } $search = '[ITEM:' . strtoupper($field) . ']'; $ret = str_replace($search, $value, $ret); } return $ret; } /** * Get the FOF View from the backtrace of the static call. MAGIC! * * @return View * * @since 3.3.0 */ public static function getViewFromBacktrace() { // In case we are on a braindead host if (!function_exists('debug_backtrace')) { throw new RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software."); } /** * For performance reasons I look into the last 4 call stack entries. If I don't find a container I * will expand my search by another 2 entries and so on until I either find a container or I stop * finding new call stack entries. */ $lastNumberOfEntries = 0; $limit = 4; $skip = 0; $container = null; while (true) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit); if (count($backtrace) == $lastNumberOfEntries) { throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken."); } $lastNumberOfEntries = count($backtrace); if ($skip) { $backtrace = array_slice($backtrace, $skip); } foreach ($backtrace as $bt) { if (!isset($bt['object'])) { continue; } if ($bt['object'] instanceof View) { return $bt['object']; } } $skip = $limit; $limit += 2; } throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken."); } /** * Get JHtml options from an alternate source, e.g. a helper. This is useful for adding arbitrary options * which are either dynamic or you do not want to inline to your view, e.g. reusable options across * different views. * * The attribs can be: * source_file The file to load. You can use FOF's URIs such as 'admin:com_foobar/foo/bar' * source_class The class to use * source_method The static method to use on source_class * source_key Use * if you're returning a key/value array. Otherwise the array key for the key (ID) * value. * source_value Use * if you're returning a key/value array. Otherwise the array key for the displayed * value. source_translate Should I pass the value field through JText? Default: true source_format Set * to "optionsobject" if you're returning an array of JHtml options. Ignored otherwise. * * @param array $attribs * * @return array * * @since 3.3.0 */ private static function getOptionsFromSource(array $attribs = []) { $options = []; $container = self::getContainerFromBacktrace(); $attribs = array_merge([ 'source_file' => '', 'source_class' => '', 'source_method' => '', 'source_key' => '*', 'source_value' => '*', 'source_translate' => true, 'source_format' => '', ], $attribs); $source_file = $attribs['source_file']; $source_class = $attribs['source_class']; $source_method = $attribs['source_method']; $source_key = $attribs['source_key']; $source_value = $attribs['source_value']; $source_translate = $attribs['source_translate']; $source_format = $attribs['source_format']; if ($source_class && $source_method) { // Maybe we have to load a file? if (!empty($source_file)) { $source_file = $container->template->parsePath($source_file, true); if ($container->filesystem->fileExists($source_file)) { include $source_file; } } // Make sure the class exists if (class_exists($source_class, true)) { // ...and so does the option if (in_array($source_method, get_class_methods($source_class))) { // Get the data from the class if ($source_format == 'optionsobject') { $options = array_merge($options, $source_class::$source_method()); } else { $source_data = $source_class::$source_method(); // Loop through the data and prime the $options array foreach ($source_data as $k => $v) { $key = (empty($source_key) || ($source_key == '*')) ? $k : @$v[$source_key]; $value = (empty($source_value) || ($source_value == '*')) ? $v : @$v[$source_value]; if ($source_translate) { $value = Text::_($value); } $options[] = HTMLHelper::_('FEFHelper.select.option', $key, $value, 'value', 'text'); } } } } } reset($options); return $options; } /** * Get JHtml options from the values returned by a model. * * The params can be: * key_field The model field used for the OPTION's key. Default: the model's ID field. * value_field The model field used for the OPTION's displayed value. You must provide it. * apply_access Should I apply Joomla ACLs to the model? Default: FALSE. * none Placeholder for no selection. Default: NULL (no placeholder). * none_as_zero When true, the 'none' placeholder applies to values '' **AND** '0' (empty string and zero) * translate Should I pass the values through JText? Default: TRUE. * with Array of relation names for eager loading. * cache Cache the results for faster reuse * * @param string $modelName The name of the model, e.g. "items" or "com_foobar.items" * @param array $params Parameters which define which options to get from the model * @param array $modelState Optional state variables to pass to the model * @param array $options Any JHtml select options you want to add in front of the model's returned values * * @return mixed * * @since 3.3.0 */ private static function getOptionsFromModel($modelName, array $params = [], array $modelState = [], array $options = []) { // Let's find the FOF DI container from the call stack $container = self::getContainerFromBacktrace(); // Explode model name into component name and prefix $componentName = $container->componentName; $mName = $modelName; if (strpos($modelName, '.') !== false) { [$componentName, $mName] = explode('.', $mName, 2); } if ($componentName != $container->componentName) { $container = Container::getInstance($componentName); } /** @var DataModel $model */ $model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false); $defaultParams = [ 'key_field' => $model->getKeyName(), 'value_field' => 'title', 'apply_access' => false, 'none' => null, 'none_as_zero' => false, 'translate' => true, 'with' => [], ]; $params = array_merge($defaultParams, $params); $cache = isset($params['cache']) && $params['cache']; $cacheKey = null; if ($cache) { $cacheKey = sha1(print_r([ $model->getContainer()->componentName, $model->getName(), $params['key_field'], $params['value_field'], $params['apply_access'], $params['none'], $params['translate'], $params['with'], $modelState, ], true)); } if ($cache && isset(self::$cacheModelOptions[$cacheKey])) { return self::$cacheModelOptions[$cacheKey]; } if (empty($params['none']) && !is_null($params['none'])) { $langKey = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName()); $placeholder = Text::_($langKey); if ($langKey != $placeholder) { $params['none'] = '— ' . $placeholder . ' —'; } } if (!empty($params['none'])) { $options[] = HTMLHelper::_('FEFHelper.select.option', null, Text::_($params['none'])); if ($params['none_as_zero']) { $options[] = HTMLHelper::_('FEFHelper.select.option', 0, Text::_($params['none'])); } } if ($params['apply_access']) { $model->applyAccessFiltering(); } if (!is_null($params['with'])) { $model->with($params['with']); } // Set the model's state, if applicable foreach ($modelState as $stateKey => $stateValue) { $model->setState($stateKey, $stateValue); } // Set the query and get the result list. $items = $model->get(true); // Build the field options. if (!empty($items)) { foreach ($items as $item) { $value = $item->{$params['value_field']}; if ($params['translate']) { $value = Text::_($value); } $options[] = HTMLHelper::_('FEFHelper.select.option', $item->{$params['key_field']}, $value); } } if ($cache) { self::$cacheModelOptions[$cacheKey] = $options; } return $options; } /** * Get the FOF DI container from the backtrace of the static call. MAGIC! * * @return Container * * @since 3.3.0 */ private static function getContainerFromBacktrace() { // In case we are on a braindead host if (!function_exists('debug_backtrace')) { throw new RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software."); } /** * For performance reasons I look into the last 4 call stack entries. If I don't find a container I * will expand my search by another 2 entries and so on until I either find a container or I stop * finding new call stack entries. */ $lastNumberOfEntries = 0; $limit = 4; $skip = 0; $container = null; while (true) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit); if (count($backtrace) == $lastNumberOfEntries) { throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken."); } $lastNumberOfEntries = count($backtrace); if ($skip) { $backtrace = array_slice($backtrace, $skip); } foreach ($backtrace as $bt) { if (!isset($bt['object'])) { continue; } if (!method_exists($bt['object'], 'getContainer')) { continue; } return $bt['object']->getContainer(); } $skip = $limit; $limit += 2; } throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken."); } } FEFHelper/edit.php 0000604 00000002671 15245522752 0007755 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ defined('_JEXEC') || die; use Joomla\CMS\Editor\Editor; use Joomla\CMS\Factory; /** * Custom JHtml (HTMLHelper) class. Offers edit (form) view controls compatible with Akeeba Frontend * Framework (FEF). * * Call these methods as JHtml::_('FEFHelper.edit.methodName', $parameter1, $parameter2, ...) */ abstract class FEFHelperEdit { public static function editor($fieldName, $value, array $params = []) { $params = array_merge([ 'id' => null, 'editor' => null, 'width' => '100%', 'height' => 500, 'columns' => 50, 'rows' => 20, 'created_by' => null, 'asset_id' => null, 'buttons' => true, 'hide' => false, ], $params); $editorType = $params['editor']; if (is_null($editorType)) { $editorType = Factory::getConfig()->get('editor'); $user = Factory::getUser(); if (!$user->guest) { $editorType = $user->getParam('editor', $editorType); } } if (is_null($params['id'])) { $params['id'] = $fieldName; } $editor = Editor::getInstance($editorType); return $editor->display($fieldName, $value, $params['width'], $params['height'], $params['columns'], $params['rows'], $params['buttons'], $params['id'], $params['asset_id'], $params['created_by'], $params); } } FEFHelper/select.php 0000604 00000065565 15245522752 0010322 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ defined('_JEXEC') || die; use FOF30\Utils\ArrayHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; /** * Custom JHtml (HTMLHelper) class. Offers selects compatible with Akeeba Frontend Framework (FEF) * * Call these methods as JHtml::_('FEFHelper.select.methodName', $parameter1, $parameter2, ...) */ abstract class FEFHelperSelect { /** * Default values for options. Organized by option group. * * @var array */ protected static $optionDefaults = [ 'option' => [ 'option.attr' => null, 'option.disable' => 'disable', 'option.id' => null, 'option.key' => 'value', 'option.key.toHtml' => true, 'option.label' => null, 'option.label.toHtml' => true, 'option.text' => 'text', 'option.text.toHtml' => true, 'option.class' => 'class', 'option.onclick' => 'onclick', ], ]; /** * Generates a yes/no radio list. * * @param string $name The value of the HTML name attribute * @param array $attribs Additional HTML attributes for the `<select>` tag * @param string $selected The key that is selected * @param string $yes Language key for Yes * @param string $no Language key for no * @param mixed $id The id for the field or false for no id * * @return string HTML for the radio list * * @since 1.5 * @see JFormFieldRadio */ public static function booleanlist($name, $attribs = [], $selected = null, $yes = 'JYES', $no = 'JNO', $id = false) { $options = [ HTMLHelper::_('FEFHelper.select.option', '0', Text::_($no)), HTMLHelper::_('FEFHelper.select.option', '1', Text::_($yes)), ]; $attribs = array_merge(['forSelect' => 1], $attribs); return HTMLHelper::_('FEFHelper.select.radiolist', $options, $name, $attribs, 'value', 'text', (int) $selected, $id); } /** * Generates an HTML selection list. * * @param array $data An array of objects, arrays, or scalars. * @param string $name The value of the HTML name attribute. * @param mixed $attribs Additional HTML attributes for the `<select>` tag. This * can be an array of attributes, or an array of options. Treated as options * if it is the last argument passed. Valid options are: * Format options, see {@see JHtml::$formatOptions}. * Selection options, see {@see JHtmlSelect::options()}. * list.attr, string|array: Additional attributes for the select * element. * id, string: Value to use as the select element id attribute. * Defaults to the same as the name. * list.select, string|array: Identifies one or more option elements * to be selected, based on the option key values. * @param string $optKey The name of the object variable for the option value. If * set to null, the index of the value array is used. * @param string $optText The name of the object variable for the option text. * @param mixed $selected The key that is selected (accepts an array or a string). * @param mixed $idtag Value of the field id or null by default * @param boolean $translate True to translate * * @return string HTML for the select list. * * @since 1.5 */ public static function genericlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false, $translate = false) { // Set default options $options = array_merge(HTMLHelper::$formatOptions, ['format.depth' => 0, 'id' => false]); if (is_array($attribs) && func_num_args() === 3) { // Assume we have an options array $options = array_merge($options, $attribs); } else { // Get options from the parameters $options['id'] = $idtag; $options['list.attr'] = $attribs; $options['list.translate'] = $translate; $options['option.key'] = $optKey; $options['option.text'] = $optText; $options['list.select'] = $selected; } $attribs = ''; if (isset($options['list.attr'])) { if (is_array($options['list.attr'])) { $attribs = ArrayHelper::toString($options['list.attr']); } else { $attribs = $options['list.attr']; } if ($attribs !== '') { $attribs = ' ' . $attribs; } } $id = $options['id'] !== false ? $options['id'] : $name; $id = str_replace(['[', ']', ' '], '', $id); $baseIndent = str_repeat($options['format.indent'], $options['format.depth']++); $html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol'] . static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol']; return $html; } /** * Generates a grouped HTML selection list from nested arrays. * * @param array $data An array of groups, each of which is an array of options. * @param string $name The value of the HTML name attribute * @param array $options Options, an array of key/value pairs. Valid options are: * Format options, {@see JHtml::$formatOptions}. * Selection options. See {@see JHtmlSelect::options()}. * group.id: The property in each group to use as the group id * attribute. Defaults to none. * group.label: The property in each group to use as the group * label. Defaults to "text". If set to null, the data array index key is * used. * group.items: The property in each group to use as the array of * items in the group. Defaults to "items". If set to null, group.id and * group. label are forced to null and the data element is assumed to be a * list of selections. * id: Value to use as the select element id attribute. Defaults to * the same as the name. * list.attr: Attributes for the select element. Can be a string or * an array of key/value pairs. Defaults to none. * list.select: either the value of one selected option or an array * of selected options. Default: none. * list.translate: Boolean. If set, text and labels are translated via * JText::_(). * * @return string HTML for the select list * * @throws RuntimeException If a group has contents that cannot be processed. */ public static function groupedlist($data, $name, $options = []) { // Set default options and overwrite with anything passed in $options = array_merge( HTMLHelper::$formatOptions, [ 'format.depth' => 0, 'group.items' => 'items', 'group.label' => 'text', 'group.label.toHtml' => true, 'id' => false, ], $options ); // Apply option rules if ($options['group.items'] === null) { $options['group.label'] = null; } $attribs = ''; if (isset($options['list.attr'])) { if (is_array($options['list.attr'])) { $attribs = ArrayHelper::toString($options['list.attr']); } else { $attribs = $options['list.attr']; } if ($attribs !== '') { $attribs = ' ' . $attribs; } } $id = $options['id'] !== false ? $options['id'] : $name; $id = str_replace(['[', ']', ' '], '', $id); // Disable groups in the options. $options['groups'] = false; $baseIndent = str_repeat($options['format.indent'], $options['format.depth']++); $html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol']; $groupIndent = str_repeat($options['format.indent'], $options['format.depth']++); foreach ($data as $dataKey => $group) { $label = $dataKey; $id = ''; $noGroup = is_int($dataKey); if ($options['group.items'] == null) { // Sub-list is an associative array $subList = $group; } elseif (is_array($group)) { // Sub-list is in an element of an array. $subList = $group[$options['group.items']]; if (isset($group[$options['group.label']])) { $label = $group[$options['group.label']]; $noGroup = false; } if (isset($options['group.id']) && isset($group[$options['group.id']])) { $id = $group[$options['group.id']]; $noGroup = false; } } elseif (is_object($group)) { // Sub-list is in a property of an object $subList = $group->{$options['group.items']}; if (isset($group->{$options['group.label']})) { $label = $group->{$options['group.label']}; $noGroup = false; } if (isset($options['group.id']) && isset($group->{$options['group.id']})) { $id = $group->{$options['group.id']}; $noGroup = false; } } else { throw new RuntimeException('Invalid group contents.', 1); } if ($noGroup) { $html .= static::options($subList, $options); } else { $html .= $groupIndent . '<optgroup' . (empty($id) ? '' : ' id="' . $id . '"') . ' label="' . ($options['group.label.toHtml'] ? htmlspecialchars($label, ENT_COMPAT, 'UTF-8') : $label) . '">' . $options['format.eol'] . static::options($subList, $options) . $groupIndent . '</optgroup>' . $options['format.eol']; } } $html .= $baseIndent . '</select>' . $options['format.eol']; return $html; } /** * Generates a selection list of integers. * * @param integer $start The start integer * @param integer $end The end integer * @param integer $inc The increment * @param string $name The value of the HTML name attribute * @param mixed $attribs Additional HTML attributes for the `<select>` tag, an array of * attributes, or an array of options. Treated as options if it is the last * argument passed. * @param mixed $selected The key that is selected * @param string $format The printf format to be applied to the number * * @return string HTML for the select list */ public static function integerlist($start, $end, $inc, $name, $attribs = null, $selected = null, $format = '') { // Set default options $options = array_merge(HTMLHelper::$formatOptions, [ 'format.depth' => 0, 'option.format' => '', 'id' => null, ]); if (is_array($attribs) && func_num_args() === 5) { // Assume we have an options array $options = array_merge($options, $attribs); // Extract the format and remove it from downstream options $format = $options['option.format']; unset($options['option.format']); } else { // Get options from the parameters $options['list.attr'] = $attribs; $options['list.select'] = $selected; } $start = (int) $start; $end = (int) $end; $inc = (int) $inc; $data = []; for ($i = $start; $i <= $end; $i += $inc) { $data[$i] = $format ? sprintf($format, $i) : $i; } // Tell genericlist() to use array keys $options['option.key'] = null; return HTMLHelper::_('FEFHelper.select.genericlist', $data, $name, $options); } /** * Create an object that represents an option in an option list. * * @param string $value The value of the option * @param string $text The text for the option * @param mixed $optKey If a string, the returned object property name for * the value. If an array, options. Valid options are: * attr: String|array. Additional attributes for this option. * Defaults to none. * disable: Boolean. If set, this option is disabled. * label: String. The value for the option label. * option.attr: The property in each option array to use for * additional selection attributes. Defaults to none. * option.disable: The property that will hold the disabled state. * Defaults to "disable". * option.key: The property that will hold the selection value. * Defaults to "value". * option.label: The property in each option array to use as the * selection label attribute. If a "label" option is provided, defaults to * "label", if no label is given, defaults to null (none). * option.text: The property that will hold the the displayed text. * Defaults to "text". If set to null, the option array is assumed to be a * list of displayable scalars. * @param string $optText The property that will hold the the displayed text. This * parameter is ignored if an options array is passed. * @param boolean $disable Not used. * * @return stdClass */ public static function option($value, $text = '', $optKey = 'value', $optText = 'text', $disable = false) { $options = [ 'attr' => null, 'disable' => false, 'option.attr' => null, 'option.disable' => 'disable', 'option.key' => 'value', 'option.label' => null, 'option.text' => 'text', ]; if (is_array($optKey)) { // Merge in caller's options $options = array_merge($options, $optKey); } else { // Get options from the parameters $options['option.key'] = $optKey; $options['option.text'] = $optText; $options['disable'] = $disable; } $obj = new stdClass; $obj->{$options['option.key']} = $value; $obj->{$options['option.text']} = trim($text) ? $text : $value; /* * If a label is provided, save it. If no label is provided and there is * a label name, initialise to an empty string. */ $hasProperty = $options['option.label'] !== null; if (isset($options['label'])) { $labelProperty = $hasProperty ? $options['option.label'] : 'label'; $obj->$labelProperty = $options['label']; } elseif ($hasProperty) { $obj->{$options['option.label']} = ''; } // Set attributes only if there is a property and a value if ($options['attr'] !== null) { $obj->{$options['option.attr']} = $options['attr']; } // Set disable only if it has a property and a value if ($options['disable'] !== null) { $obj->{$options['option.disable']} = $options['disable']; } return $obj; } /** * Generates the option tags for an HTML select list (with no select tag * surrounding the options). * * @param array $arr An array of objects, arrays, or values. * @param mixed $optKey If a string, this is the name of the object variable for * the option value. If null, the index of the array of objects is used. If * an array, this is a set of options, as key/value pairs. Valid options are: * -Format options, {@see JHtml::$formatOptions}. * -groups: Boolean. If set, looks for keys with the value * "<optgroup>" and synthesizes groups from them. Deprecated. Defaults * true for backwards compatibility. * -list.select: either the value of one selected option or an array * of selected options. Default: none. * -list.translate: Boolean. If set, text and labels are translated via * JText::_(). Default is false. * -option.id: The property in each option array to use as the * selection id attribute. Defaults to none. * -option.key: The property in each option array to use as the * selection value. Defaults to "value". If set to null, the index of the * option array is used. * -option.label: The property in each option array to use as the * selection label attribute. Defaults to null (none). * -option.text: The property in each option array to use as the * displayed text. Defaults to "text". If set to null, the option array is * assumed to be a list of displayable scalars. * -option.attr: The property in each option array to use for * additional selection attributes. Defaults to none. * -option.disable: The property that will hold the disabled state. * Defaults to "disable". * -option.key: The property that will hold the selection value. * Defaults to "value". * -option.text: The property that will hold the the displayed text. * Defaults to "text". If set to null, the option array is assumed to be a * list of displayable scalars. * @param string $optText The name of the object variable for the option text. * @param mixed $selected The key that is selected (accepts an array or a string) * @param boolean $translate Translate the option values. * * @return string HTML for the select list */ public static function options($arr, $optKey = 'value', $optText = 'text', $selected = null, $translate = false) { $options = array_merge( HTMLHelper::$formatOptions, static::$optionDefaults['option'], ['format.depth' => 0, 'groups' => true, 'list.select' => null, 'list.translate' => false] ); if (is_array($optKey)) { // Set default options and overwrite with anything passed in $options = array_merge($options, $optKey); } else { // Get options from the parameters $options['option.key'] = $optKey; $options['option.text'] = $optText; $options['list.select'] = $selected; $options['list.translate'] = $translate; } $html = ''; $baseIndent = str_repeat($options['format.indent'], $options['format.depth']); foreach ($arr as $elementKey => &$element) { $attr = ''; $extra = ''; $label = ''; $id = ''; if (is_array($element)) { $key = $options['option.key'] === null ? $elementKey : $element[$options['option.key']]; $text = $element[$options['option.text']]; if (isset($element[$options['option.attr']])) { $attr = $element[$options['option.attr']]; } if (isset($element[$options['option.id']])) { $id = $element[$options['option.id']]; } if (isset($element[$options['option.label']])) { $label = $element[$options['option.label']]; } if (isset($element[$options['option.disable']]) && $element[$options['option.disable']]) { $extra .= ' disabled="disabled"'; } } elseif (is_object($element)) { $key = $options['option.key'] === null ? $elementKey : $element->{$options['option.key']}; $text = $element->{$options['option.text']}; if (isset($element->{$options['option.attr']})) { $attr = $element->{$options['option.attr']}; } if (isset($element->{$options['option.id']})) { $id = $element->{$options['option.id']}; } if (isset($element->{$options['option.label']})) { $label = $element->{$options['option.label']}; } if (isset($element->{$options['option.disable']}) && $element->{$options['option.disable']}) { $extra .= ' disabled="disabled"'; } if (isset($element->{$options['option.class']}) && $element->{$options['option.class']}) { $extra .= ' class="' . $element->{$options['option.class']} . '"'; } if (isset($element->{$options['option.onclick']}) && $element->{$options['option.onclick']}) { $extra .= ' onclick="' . $element->{$options['option.onclick']} . '"'; } } else { // This is a simple associative array $key = $elementKey; $text = $element; } /* * The use of options that contain optgroup HTML elements was * somewhat hacked for J1.5. J1.6 introduces the grouplist() method * to handle this better. The old solution is retained through the * "groups" option, which defaults true in J1.6, but should be * deprecated at some point in the future. */ $key = (string) $key; if ($key === '<OPTGROUP>' && $options['groups']) { $html .= $baseIndent . '<optgroup label="' . ($options['list.translate'] ? Text::_($text) : $text) . '">' . $options['format.eol']; $baseIndent = str_repeat($options['format.indent'], ++$options['format.depth']); } elseif ($key === '</OPTGROUP>' && $options['groups']) { $baseIndent = str_repeat($options['format.indent'], --$options['format.depth']); $html .= $baseIndent . '</optgroup>' . $options['format.eol']; } else { // If no string after hyphen - take hyphen out $splitText = explode(' - ', $text, 2); $text = $splitText[0]; if (isset($splitText[1]) && $splitText[1] !== '' && !preg_match('/^[\s]+$/', $splitText[1])) { $text .= ' - ' . $splitText[1]; } if (!empty($label) && $options['list.translate']) { $label = Text::_($label); } if ($options['option.label.toHtml']) { $label = htmlentities($label); } if (is_array($attr)) { $attr = ArrayHelper::toString($attr); } else { $attr = trim($attr); } $extra = ($id ? ' id="' . $id . '"' : '') . ($label ? ' label="' . $label . '"' : '') . ($attr ? ' ' . $attr : '') . $extra; if (is_array($options['list.select'])) { foreach ($options['list.select'] as $val) { $key2 = is_object($val) ? $val->{$options['option.key']} : $val; if ($key == $key2) { $extra .= ' selected="selected"'; break; } } } elseif ((string) $key === (string) $options['list.select']) { $extra .= ' selected="selected"'; } if ($options['list.translate']) { $text = Text::_($text); } // Generate the option, encoding as required $html .= $baseIndent . '<option value="' . ($options['option.key.toHtml'] ? htmlspecialchars($key, ENT_COMPAT, 'UTF-8') : $key) . '"' . $extra . '>'; $html .= $options['option.text.toHtml'] ? htmlentities(html_entity_decode($text, ENT_COMPAT, 'UTF-8'), ENT_COMPAT, 'UTF-8') : $text; $html .= '</option>' . $options['format.eol']; } } return $html; } /** * Generates an HTML radio list. * * @param array $data An array of objects * @param string $name The value of the HTML name attribute * @param string $attribs Additional HTML attributes for the `<select>` tag * @param mixed $optKey The key that is selected * @param string $optText The name of the object variable for the option value * @param string $selected The name of the object variable for the option text * @param boolean $idtag Value of the field id or null by default * @param boolean $translate True if options will be translated * * @return string HTML for the select list */ public static function radiolist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false, $translate = false) { $forSelect = false; if (isset($attribs['forSelect'])) { $forSelect = (bool) ($attribs['forSelect']); unset($attribs['forSelect']); } if (is_array($attribs)) { $attribs = ArrayHelper::toString($attribs); } $id_text = empty($idtag) ? $name : $idtag; $html = ''; foreach ($data as $optionObject) { $optionValue = $optionObject->$optKey; $labelText = $translate ? Text::_($optionObject->$optText) : $optionObject->$optText; $id = ($optionObject->id ?? null); $extra = ''; $id = $id ? $optionObject->id : $id_text . $optionValue; if (is_array($selected)) { foreach ($selected as $val) { $k2 = is_object($val) ? $val->$optKey : $val; if ($optionValue == $k2) { $extra .= ' selected="selected" '; break; } } } else { $extra .= ((string) $optionValue === (string) $selected ? ' checked="checked" ' : ''); } if ($forSelect) { $html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra . $attribs . ' />'; $html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">' . $labelText . '</label>'; } else { $html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">'; $html .= "\n\t\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra . $attribs . ' />' . $labelText; $html .= "\n\t" . '</label>'; } } $html .= "\n"; return $html; } /** * Creates two radio buttons styled with FEF to appear as a YES/NO switch * * @param string $name Name of the field * @param string $selected Selected field * @param array $attribs Additional attributes to add to the switch * * @return string The HTML for the switch */ public static function booleanswitch($name, $selected, array $attribs = []) { if (empty($attribs)) { $attribs = ['class' => 'akeeba-toggle']; } else { if (isset($attribs['class'])) { $attribs['class'] .= ' akeeba-toggle'; } else { $attribs['class'] = 'akeeba-toggle'; } } $temp = ''; foreach ($attribs as $key => $value) { $temp .= $key . ' = "' . $value . '"'; } $attribs = $temp; $checked_1 = $selected ? '' : 'checked '; $checked_2 = $selected ? 'checked ' : ''; $html = '<div ' . $attribs . '>'; $html .= '<input type="radio" class="radio-yes" name="' . $name . '" ' . $checked_2 . 'id="' . $name . '-2" value="1">'; $html .= '<label for="' . $name . '-2" class="green">' . Text::_('JYES') . '</label>'; $html .= '<input type="radio" class="radio-no" name="' . $name . '" ' . $checked_1 . 'id="' . $name . '-1" value="0">'; $html .= '<label for="' . $name . '-1" class="red">' . Text::_('JNO') . '</label>'; $html .= '</div>'; return $html; } } FEFHelper/browse.php 0000604 00000072505 15245522752 0010334 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ defined('_JEXEC') || die; use FOF30\Model\DataModel; use FOF30\Utils\ArrayHelper; use FOF30\Utils\FEFHelper\BrowseView; use FOF30\View\DataView\DataViewInterface; use FOF30\View\DataView\Html; use FOF30\View\DataView\Raw as DataViewRaw; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Pagination\Pagination; /** * Custom JHtml (HTMLHelper) class. Offers browse view controls compatible with Akeeba Frontend * Framework (FEF). * * Call these methods as JHtml::_('FEFHelper.browse.methodName', $parameter1, $parameter2, ...) */ abstract class FEFHelperBrowse { /** * Returns an action button on the browse view's table * * @param integer $i The row index * @param string $task The task to fire when the button is clicked * @param string|array $prefix An optional task prefix or an array of options * @param string $active_title An optional active tooltip to display if $enable is true * @param string $inactive_title An optional inactive tooltip to display if $enable is true * @param boolean $tip An optional setting for tooltip * @param string $active_class An optional active HTML class * @param string $inactive_class An optional inactive HTML class * @param boolean $enabled An optional setting for access control on the action. * @param boolean $translate An optional setting for translation. * @param string $checkbox An optional prefix for checkboxes. * * @return string The HTML markup * * @since 3.3.0 */ public static function action($i, $task, $prefix = '', $active_title = '', $inactive_title = '', $tip = false, $active_class = '', $inactive_class = '', $enabled = true, $translate = true, $checkbox = 'cb') { if (is_array($prefix)) { $options = $prefix; $active_title = array_key_exists('active_title', $options) ? $options['active_title'] : $active_title; $inactive_title = array_key_exists('inactive_title', $options) ? $options['inactive_title'] : $inactive_title; $tip = array_key_exists('tip', $options) ? $options['tip'] : $tip; $active_class = array_key_exists('active_class', $options) ? $options['active_class'] : $active_class; $inactive_class = array_key_exists('inactive_class', $options) ? $options['inactive_class'] : $inactive_class; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $translate = array_key_exists('translate', $options) ? $options['translate'] : $translate; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } if ($tip) { $title = $enabled ? $active_title : $inactive_title; $title = $translate ? Text::_($title) : $title; $title = HTMLHelper::_('tooltipText', $title, '', 0); } $html = []; if ($enabled) { $btnColor = 'grey'; if (substr($active_class, 0, 2) == '--') { [$btnColor, $active_class] = explode(' ', $active_class, 2); $btnColor = ltrim($btnColor, '-'); } $html[] = '<a class="akeeba-btn--' . $btnColor . '--mini ' . ($active_class === 'publish' ? ' active' : '') . ($tip ? ' hasTooltip' : '') . '"'; $html[] = ' href="javascript:void(0);" onclick="return Joomla.listItemTask(\'' . $checkbox . $i . '\',\'' . $prefix . $task . '\')"'; $html[] = $tip ? ' title="' . $title . '"' : ''; $html[] = '>'; $html[] = '<span class="akion-' . $active_class . '" aria-hidden="true"></span> '; $html[] = '</a>'; } else { $btnColor = 'grey'; if (substr($inactive_class, 0, 2) == '--') { [$btnColor, $inactive_class] = explode(' ', $inactive_class, 2); $btnColor = ltrim($btnColor, '-'); } $html[] = '<a class="akeeba-btn--' . $btnColor . '--mini disabled akeebagrid' . ($tip ? ' hasTooltip' : '') . '"'; $html[] = $tip ? ' title="' . $title . '"' : ''; $html[] = '>'; if ($active_class === 'protected') { $inactive_class = 'locked'; } $html[] = '<span class="akion-' . $inactive_class . '"></span> '; $html[] = '</a>'; } return implode($html); } /** * Returns a state change button on the browse view's table * * @param array $states array of value/state. Each state is an array of the form * (task, text, active title, inactive title, tip (boolean), HTML active class, * HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active * title, * 'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active * class, * 'inactive_class'=>html inactive class) * @param integer $value The state value. * @param integer $i The row index * @param string|array $prefix An optional task prefix or an array of options * @param boolean $enabled An optional setting for access control on the action. * @param boolean $translate An optional setting for translation. * @param string $checkbox An optional prefix for checkboxes. * * @return string The HTML markup * * @since 3.3.0 */ public static function state($states, $value, $i, $prefix = '', $enabled = true, $translate = true, $checkbox = 'cb') { if (is_array($prefix)) { $options = $prefix; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $translate = array_key_exists('translate', $options) ? $options['translate'] : $translate; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } $state = ArrayHelper::getValue($states, (int) $value, $states[0]); $task = array_key_exists('task', $state) ? $state['task'] : $state[0]; $text = array_key_exists('text', $state) ? $state['text'] : (array_key_exists(1, $state) ? $state[1] : ''); $active_title = array_key_exists('active_title', $state) ? $state['active_title'] : (array_key_exists(2, $state) ? $state[2] : ''); $inactive_title = array_key_exists('inactive_title', $state) ? $state['inactive_title'] : (array_key_exists(3, $state) ? $state[3] : ''); $tip = array_key_exists('tip', $state) ? $state['tip'] : (array_key_exists(4, $state) ? $state[4] : false); $active_class = array_key_exists('active_class', $state) ? $state['active_class'] : (array_key_exists(5, $state) ? $state[5] : ''); $inactive_class = array_key_exists('inactive_class', $state) ? $state['inactive_class'] : (array_key_exists(6, $state) ? $state[6] : ''); return static::action( $i, $task, $prefix, $active_title, $inactive_title, $tip, $active_class, $inactive_class, $enabled, $translate, $checkbox ); } /** * Returns a published state on the browse view's table * * @param integer $value The state value. * @param integer $i The row index * @param string|array $prefix An optional task prefix or an array of options * @param boolean $enabled An optional setting for access control on the action. * @param string $checkbox An optional prefix for checkboxes. * @param string $publish_up An optional start publishing date. * @param string $publish_down An optional finish publishing date. * * @return string The HTML markup * * @see self::state() * * @since 3.3.0 */ public static function published($value, $i, $prefix = '', $enabled = true, $checkbox = 'cb', $publish_up = null, $publish_down = null) { if (is_array($prefix)) { $options = $prefix; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } /** * Format: * * (task, text, active title, inactive title, tip (boolean), active icon class (without akion-), inactive icon class (without akion-)) */ $states = [ 1 => [ 'unpublish', 'JPUBLISHED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JPUBLISHED', true, '--green checkmark', '--green checkmark', ], 0 => [ 'publish', 'JUNPUBLISHED', 'JLIB_HTML_PUBLISH_ITEM', 'JUNPUBLISHED', true, '--red close', '--red close', ], 2 => [ 'unpublish', 'JARCHIVED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JARCHIVED', true, '--orange ion-ios-box', '--orange ion-ios-box', ], -2 => [ 'publish', 'JTRASHED', 'JLIB_HTML_PUBLISH_ITEM', 'JTRASHED', true, '--dark trash-a', '--dark trash-a', ], ]; // Special state for dates if ($publish_up || $publish_down) { $nullDate = Factory::getDbo()->getNullDate(); $nowDate = Factory::getDate()->toUnix(); $tz = Factory::getUser()->getTimezone(); $publish_up = (!empty($publish_up) && ($publish_up != $nullDate)) ? Factory::getDate($publish_up, 'UTC')->setTimeZone($tz) : false; $publish_down = (!empty($publish_down) && ($publish_down != $nullDate)) ? Factory::getDate($publish_down, 'UTC')->setTimeZone($tz) : false; // Create tip text, only we have publish up or down settings $tips = []; if ($publish_up) { $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publish_up, Text::_('DATE_FORMAT_LC5'), 'UTC')); } if ($publish_down) { $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publish_down, Text::_('DATE_FORMAT_LC5'), 'UTC')); } $tip = empty($tips) ? false : implode('<br />', $tips); // Add tips and special titles foreach ($states as $key => $state) { // Create special titles for published items if ($key == 1) { $states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_ITEM'; if (!empty($publish_up) && ($publish_up != $nullDate) && $nowDate < $publish_up->toUnix()) { $states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_PENDING_ITEM'; $states[$key][5] = $states[$key][6] = 'android-time'; } if (!empty($publish_down) && ($publish_down != $nullDate) && $nowDate > $publish_down->toUnix()) { $states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_EXPIRED_ITEM'; $states[$key][5] = $states[$key][6] = 'alert'; } } // Add tips to titles if ($tip) { $states[$key][1] = Text::_($states[$key][1]); $states[$key][2] = Text::_($states[$key][2]) . '<br />' . $tip; $states[$key][3] = Text::_($states[$key][3]) . '<br />' . $tip; $states[$key][4] = true; } } return static::state($states, $value, $i, [ 'prefix' => $prefix, 'translate' => !$tip, ], $enabled, true, $checkbox); } return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox); } /** * Returns an isDefault state on the browse view's table * * @param integer $value The state value. * @param integer $i The row index * @param string|array $prefix An optional task prefix or an array of options * @param boolean $enabled An optional setting for access control on the action. * @param string $checkbox An optional prefix for checkboxes. * * @return string The HTML markup * * @see self::state() * @since 3.3.0 */ public static function isdefault($value, $i, $prefix = '', $enabled = true, $checkbox = 'cb') { if (is_array($prefix)) { $options = $prefix; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } $states = [ 0 => ['setDefault', '', 'JLIB_HTML_SETDEFAULT_ITEM', '', 1, 'android-star-outline', 'android-star-outline'], 1 => [ 'unsetDefault', 'JDEFAULT', 'JLIB_HTML_UNSETDEFAULT_ITEM', 'JDEFAULT', 1, 'android-star', 'android-star', ], ]; return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox); } /** * Returns a checked-out icon * * @param integer $i The row index. * @param string $editorName The name of the editor. * @param string $time The time that the object was checked out. * @param string|array $prefix An optional task prefix or an array of options * @param boolean $enabled True to enable the action. * @param string $checkbox An optional prefix for checkboxes. * * @return string The HTML markup * * @since 3.3.0 */ public static function checkedout($i, $editorName, $time, $prefix = '', $enabled = false, $checkbox = 'cb') { HTMLHelper::_('bootstrap.tooltip'); if (is_array($prefix)) { $options = $prefix; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } $text = $editorName . '<br />' . HTMLHelper::_('date', $time, Text::_('DATE_FORMAT_LC')) . '<br />' . HTMLHelper::_('date', $time, 'H:i'); $active_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKIN'), $text, 0); $inactive_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKED_OUT'), $text, 0); return static::action( $i, 'checkin', $prefix, html_entity_decode($active_title, ENT_QUOTES, 'UTF-8'), html_entity_decode($inactive_title, ENT_QUOTES, 'UTF-8'), true, 'locked', 'locked', $enabled, false, $checkbox ); } /** * Returns the drag'n'drop reordering field for Browse views * * @param string $orderingField The name of the field you're ordering by * @param string $order The order value of the current row * @param string $class CSS class for the ordering value INPUT field * @param string $icon CSS class for the d'n'd handle icon * @param string $inactiveIcon CSS class for the d'n'd disabled icon * @param DataViewInterface $view The view you're rendering against. Leave null for auto-detection. * * @return string */ public static function order($orderingField, $order, $class = 'input-sm', $icon = 'akion-android-more-vertical', $inactiveIcon = 'akion-android-more-vertical', DataViewInterface $view = null) { /** @var Html $view */ if (is_null($view)) { $view = BrowseView::getViewFromBacktrace(); } $dndOrderingActive = $view->getLists()->order == $orderingField; // Default inactive ordering $html = '<span class="sortable-handler inactive" >'; $html .= '<span class="' . $icon . '"></span>'; $html .= '</span>'; // The modern drag'n'drop method if ($view->getPerms()->editstate) { $disableClassName = ''; $disabledLabel = ''; // DO NOT REMOVE! It will initialize Joomla libraries and javascript functions $hasAjaxOrderingSupport = $view->hasAjaxOrderingSupport(); if (!is_array($hasAjaxOrderingSupport) || !$hasAjaxOrderingSupport['saveOrder']) { $disabledLabel = Text::_('JORDERINGDISABLED'); $disableClassName = 'inactive tip-top hasTooltip'; } $orderClass = $dndOrderingActive ? 'order-enabled' : 'order-disabled'; $html = '<div class="' . $orderClass . '">'; $html .= '<span class="sortable-handler ' . $disableClassName . '" title="' . $disabledLabel . '">'; $html .= '<span class="' . ($disableClassName ? $inactiveIcon : $icon) . '"></span>'; $html .= '</span>'; if ($dndOrderingActive) { $joomla35IsBroken = version_compare(JVERSION, '3.5.0', 'ge') ? 'style="display: none"' : ''; $html .= '<input type="text" name="order[]" ' . $joomla35IsBroken . ' size="5" class="' . $class . ' text-area-order" value="' . $order . '" />'; } $html .= '</div>'; } return $html; } /** * Returns the drag'n'drop reordering table header for Browse views * * @param string $orderingField The name of the field you're ordering by * @param string $icon CSS class for the d'n'd handle icon * * @return string */ public static function orderfield($orderingField = 'ordering', $icon = 'akion-stats-bars') { $title = Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN'); $orderingLabel = Text::_('JFIELD_ORDERING_LABEL'); return <<< HTML <a href="#" onclick="Joomla.tableOrdering('{$orderingField}','asc','');return false;" class="hasPopover" title="{$orderingLabel}" data-content="{$title}" data-placement="top" > <span class="{$icon}"></span> </a> HTML; } /** * Creates an order-up action icon. * * @param integer $i The row index. * @param string $task An optional task to fire. * @param string|array $prefix An optional task prefix or an array of options * @param string $text An optional text to display * @param boolean $enabled An optional setting for access control on the action. * @param string $checkbox An optional prefix for checkboxes. * * @return string The HTML markup * * @since 3.3.0 */ public static function orderUp($i, $task = 'orderup', $prefix = '', $text = 'JLIB_HTML_MOVE_UP', $enabled = true, $checkbox = 'cb') { if (is_array($prefix)) { $options = $prefix; $text = array_key_exists('text', $options) ? $options['text'] : $text; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } return static::action($i, $task, $prefix, $text, $text, false, 'arrow-up-b', 'arrow-up-b', $enabled, true, $checkbox); } /** * Creates an order-down action icon. * * @param integer $i The row index. * @param string $task An optional task to fire. * @param string|array $prefix An optional task prefix or an array of options * @param string $text An optional text to display * @param boolean $enabled An optional setting for access control on the action. * @param string $checkbox An optional prefix for checkboxes. * * @return string The HTML markup * * @since 3.3.0 */ public static function orderDown($i, $task = 'orderdown', $prefix = '', $text = 'JLIB_HTML_MOVE_DOWN', $enabled = true, $checkbox = 'cb') { if (is_array($prefix)) { $options = $prefix; $text = array_key_exists('text', $options) ? $options['text'] : $text; $enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled; $checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox; $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; } return static::action($i, $task, $prefix, $text, $text, false, 'arrow-down-b', 'arrow-down-b', $enabled, true, $checkbox); } /** * Table header for a field which changes the sort order when clicked * * @param string $title The link title * @param string $order The order field for the column * @param string $direction The current direction * @param string $selected The selected ordering * @param string $task An optional task override * @param string $new_direction An optional direction for the new column * @param string $tip An optional text shown as tooltip title instead of $title * @param string $form An optional form selector * * @return string * * @since 3.3.0 */ public static function sort($title, $order, $direction = 'asc', $selected = '', $task = null, $new_direction = 'asc', $tip = '', $form = null) { HTMLHelper::_('behavior.core'); HTMLHelper::_('bootstrap.popover'); $direction = strtolower($direction); $icon = ['akion-android-arrow-dropup', 'akion-android-arrow-dropdown']; $index = (int) ($direction === 'desc'); if ($order != $selected) { $direction = $new_direction; } else { $direction = $direction === 'desc' ? 'asc' : 'desc'; } if ($form) { $form = ', document.getElementById(\'' . $form . '\')'; } $html = '<a href="#" onclick="Joomla.tableOrdering(\'' . $order . '\',\'' . $direction . '\',\'' . $task . '\'' . $form . ');return false;"' . ' class="hasPopover" title="' . htmlspecialchars(Text::_($tip ?: $title)) . '"' . ' data-content="' . htmlspecialchars(Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN')) . '" data-placement="top">'; if (isset($title['0']) && $title['0'] === '<') { $html .= $title; } else { $html .= Text::_($title); } if ($order == $selected) { $html .= '<span class="' . $icon[$index] . '"></span>'; } $html .= '</a>'; return $html; } /** * Method to check all checkboxes on the browse view's table * * @param string $name The name of the form element * @param string $tip The text shown as tooltip title instead of $tip * @param string $action The action to perform on clicking the checkbox * * @return string * * @since 3.3.0 */ public static function checkall($name = 'checkall-toggle', $tip = 'JGLOBAL_CHECK_ALL', $action = 'Joomla.checkAll(this)') { HTMLHelper::_('behavior.core'); HTMLHelper::_('bootstrap.tooltip'); return '<input type="checkbox" name="' . $name . '" value="" class="hasTooltip" title="' . HTMLHelper::_('tooltipText', $tip) . '" onclick="' . $action . '" />'; } /** * Method to create a checkbox for a grid row. * * @param integer $rowNum The row index * @param integer $recId The record id * @param boolean $checkedOut True if item is checked out * @param string $name The name of the form element * @param string $stub The name of stub identifier * * @return mixed String of html with a checkbox if item is not checked out, null if checked out. * * @since 3.3.0 */ public static function id($rowNum, $recId, $checkedOut = false, $name = 'cid', $stub = 'cb') { return $checkedOut ? '' : '<input type="checkbox" id="' . $stub . $rowNum . '" name="' . $name . '[]" value="' . $recId . '" onclick="Joomla.isChecked(this.checked);" />'; } /** * Include the necessary JavaScript for the browse view's table order feature * * @param string $orderBy Filed by which we are currently sorting the table. * @param bool $return Should I return the JS? Default: false (= add to the page's head) * * @return string */ public static function orderjs($orderBy, $return = false) { $js = <<< JS Joomla.orderTable = function() { var table = document.getElementById("sortTable"); var direction = document.getElementById("directionTable"); var order = table.options[table.selectedIndex].value; var dirn = 'asc'; if (order !== '$orderBy') { dirn = 'asc'; } else { dirn = direction.options[direction.selectedIndex].value; } Joomla.tableOrdering(order, dirn); }; JS; if ($return) { return $js; } try { Factory::getApplication()->getDocument()->addScriptDeclaration($js); } catch (Exception $e) { // If we have no application, well, not having table sorting JS is the least of your worries... } } /** * Returns the table ordering / pagination header for a browse view: number of records to display, order direction, * order by field. * * @param DataViewRaw $view The view you're rendering against. If not provided we * will guess it using MAGIC. * @param array $sortFields Array of field name => description for the ordering * fields in the dropdown. If not provided we will use all * the fields available in the model. * @param Pagination $pagination The Joomla pagination object. If not provided we fetch * it from the view. * @param string $sortBy Order by field name. If not provided we fetch it from * the view. * @param string $order_Dir Order direction. If not provided we fetch it from the * view. * * @return string * * @since 3.3.0 */ public static function orderheader(DataViewRaw $view = null, array $sortFields = [], Pagination $pagination = null, $sortBy = null, $order_Dir = null) { if (is_null($view)) { $view = BrowseView::getViewFromBacktrace(); } if (empty($sortFields)) { /** @var DataModel $model */ $model = $view->getModel(); $sortFields = $view->getLists()->sortFields ?? []; $sortFields = empty($sortFields) ? self::getSortFields($model) : $sortFields; } if (empty($pagination)) { $pagination = $view->getPagination(); } if (empty($sortBy)) { $sortBy = $view->getLists()->order; } if (empty($order_Dir)) { $order_Dir = $view->getLists()->order_Dir; if (empty($order_Dir)) { $order_Dir = 'asc'; } } // Static hidden text labels $limitLabel = Text::_('JFIELD_PLG_SEARCH_SEARCHLIMIT_DESC'); $orderingDescr = Text::_('JFIELD_ORDERING_DESC'); $sortByLabel = Text::_('JGLOBAL_SORT_BY'); // Order direction dropdown $directionSelect = HTMLHelper::_('FEFHelper.select.genericlist', [ '' => $orderingDescr, 'asc' => Text::_('JGLOBAL_ORDER_ASCENDING'), 'desc' => Text::_('JGLOBAL_ORDER_DESCENDING'), ], 'directionTable', [ 'id' => 'directionTable', 'list.select' => $order_Dir, 'list.attr' => [ 'class' => 'input-medium custom-select', 'onchange' => 'Joomla.orderTable()', ], ]); // Sort by field dropdown $sortTable = HTMLHelper::_('FEFHelper.select.genericlist', array_merge([ '' => Text::_('JGLOBAL_SORT_BY'), ], $sortFields), 'sortTable', [ 'id' => 'sortTable', 'list.select' => $sortBy, 'list.attr' => [ 'class' => 'input-medium custom-select', 'onchange' => 'Joomla.orderTable()', ], ]); $html = <<<HTML <div class="akeeba-filter-element akeeba-form-group"> <label for="limit" class="element-invisible"> $limitLabel </label> {$pagination->getLimitBox()} </div> <div class="akeeba-filter-element akeeba-form-group"> <label for="directionTable" class="element-invisible"> $orderingDescr </label> $directionSelect </div> <div class="akeeba-filter-element akeeba-form-group"> <label for="sortTable" class="element-invisible"> {$sortByLabel} </label> $sortTable </div> HTML; return $html; } /** * Get the default sort fields from a model. It creates a hash array where the keys are the model's field names and * the values are the translation keys for their names, following FOF's naming conventions. * * @param DataModel $model The model for which we get the sort fields * * @return array * * @since 3.3.0 */ private static function getSortFields(DataModel $model) { $sortFields = []; $idField = $model->getIdFieldName() ?: 'id'; $defaultFieldLabels = [ 'publish_up' => 'JGLOBAL_FIELD_PUBLISH_UP_LABEL', 'publish_down' => 'JGLOBAL_FIELD_PUBLISH_DOWN_LABEL', 'created_by' => 'JGLOBAL_FIELD_CREATED_BY_LABEL', 'created_on' => 'JGLOBAL_FIELD_CREATED_LABEL', 'modified_by' => 'JGLOBAL_FIELD_MODIFIED_BY_LABEL', 'modified_on' => 'JGLOBAL_FIELD_MODIFIED_LABEL', 'ordering' => 'JGLOBAL_FIELD_FIELD_ORDERING_LABEL', 'id' => 'JGLOBAL_FIELD_ID_LABEL', 'hits' => 'JGLOBAL_HITS', 'title' => 'JGLOBAL_TITLE', 'user_id' => 'JGLOBAL_USERNAME', 'username' => 'JGLOBAL_USERNAME', ]; $componentName = $model->getContainer()->componentName; $viewNameSingular = $model->getContainer()->inflector->singularize($model->getName()); $viewNamePlural = $model->getContainer()->inflector->pluralize($model->getName()); foreach ($model->getFields() as $field => $fieldDescriptor) { $possibleKeys = [ $componentName . '_' . $viewNamePlural . '_FIELD_' . $field, $componentName . '_' . $viewNamePlural . '_' . $field, $componentName . '_' . $viewNameSingular . '_FIELD_' . $field, $componentName . '_' . $viewNameSingular . '_' . $field, ]; if (array_key_exists($field, $defaultFieldLabels)) { $possibleKeys[] = $defaultFieldLabels[$field]; } if ($field === $idField) { $possibleKeys[] = $defaultFieldLabels['id']; } $fieldLabel = ''; foreach ($possibleKeys as $langKey) { $langKey = strtoupper($langKey); $fieldLabel = Text::_($langKey); if ($fieldLabel !== $langKey) { break; } $fieldLabel = ''; } if (!empty($fieldLabel)) { $sortFields[$field] = (new Joomla\Filter\InputFilter())->clean($fieldLabel); } } return $sortFields; } } Collection.php 0000604 00000035075 15245522752 0007367 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\Utils; defined('_JEXEC') || die; use ArrayAccess; use ArrayIterator; use CachingIterator; use Closure; use Countable; use IteratorAggregate; use JsonSerializable; class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable { /** * The items contained in the collection. * * @var array */ protected $items = []; /** * Create a new collection. * * @param array $items */ public function __construct(array $items = []) { $this->items = $items; } /** * Create a new collection instance if the value isn't one already. If given an array then the array is wrapped in * a Collection. Otherwise we return a Collection with only one item, whatever was passed in $items. If you pass * null you get an empty Collection. * * @param array|mixed $items * * @return static */ public static function make($items): Collection { if (is_null($items)) { return new static; } if ($items instanceof Collection) { return $items; } return new static(is_array($items) ? $items : [$items]); } /** * Get all of the items in the collection. * * @return array */ public function all(): array { return $this->items; } /** * Collapse the collection items into a single array. This assumes that the Collection is composed of arrays. We * are essentially merging all of these arrays and creating a new Collection out of them. * * If $this->items = [ ['a','b'], ['c','d'], ['e'] ] after collapse it becomes [ 'a','b','c','d','e' ] * * @return Collection */ public function collapse(): Collection { $results = []; foreach ($this->items as $values) { $results = array_merge($results, $values); } return new static($results); } /** * Diff the collection with the given items. * * @param Collection|array $items * * @return Collection */ public function diff($items): Collection { return new static(array_diff($this->items, $this->getIterableItemsAsArray($items))); } /** * Execute a callback over each item. * * @param Closure $callback * * @return Collection */ public function each(Closure $callback): Collection { array_map($callback, $this->items); return $this; } /** * Fetch a nested element of the collection. * * @param string $key * * @return Collection */ public function fetch(string $key): Collection { return new static(array_fetch($this->items, $key)); } /** * Run a filter over each of the items. * * @param Closure $callback * * @return Collection */ public function filter(Closure $callback): Collection { return new static(array_filter($this->items, $callback)); } /** * Get the first item from the collection. * * @param \Closure $callback * @param mixed $default * * @return mixed|null */ public function first(Closure $callback = null, $default = null) { if (is_null($callback)) { return count($this->items) > 0 ? reset($this->items) : null; } else { return array_first($this->items, $callback, $default); } } /** * Get a flattened array of the items in the collection. * * @return Collection */ public function flatten(): Collection { return new static(array_flatten($this->items)); } /** * Remove an item from the collection by key. * * @param mixed $key * * @return void */ public function forget(string $key): void { unset($this->items[$key]); } /** * Get an item from the collection by key. * * @param mixed $key * @param mixed $default * * @return mixed */ public function get(string $key, $default = null) { if (array_key_exists($key, $this->items)) { return $this->items[$key]; } return value($default); } /** * Group an associative array by a field or Closure value. * * @param callable|string $groupBy * * @return Collection */ public function groupBy($groupBy): Collection { $results = []; foreach ($this->items as $key => $value) { $key = is_callable($groupBy) ? $groupBy($value, $key) : array_get($value, $groupBy); $results[$key][] = $value; } return new static($results); } /** * Determine if an item exists in the collection by key. * * @param string $key * * @return bool */ public function has(string $key): bool { return array_key_exists($key, $this->items); } /** * Concatenate values of a given key as a string. * * @param string $value * @param string|null $glue * * @return string */ public function implode(string $value, ?string $glue = null): string { if (is_null($glue)) { return implode($this->lists($value)); } return implode($glue, $this->lists($value)); } /** * Intersect the collection with the given items. * * @param Collection|array $items * * @return Collection */ public function intersect($items): Collection { return new static(array_intersect($this->items, $this->getIterableItemsAsArray($items))); } /** * Determine if the collection is empty or not. * * @return bool */ public function isEmpty(): bool { return empty($this->items); } /** * Get the last item from the collection. * * @return mixed|null */ public function last() { return count($this->items) > 0 ? end($this->items) : null; } /** * Get an array with the values of a given key. * * @param string $value * @param string|null $key * * @return array */ public function lists(string $value, ?string $key = null) { return array_pluck($this->items, $value, $key); } /** * Run a map over each of the items. * * @param Closure $callback * * @return Collection */ public function map(Closure $callback): Collection { return new static(array_map($callback, $this->items, array_keys($this->items))); } /** * Merge the collection with the given items. * * @param Collection|array $items * * @return Collection */ public function merge($items): Collection { return new static(array_merge($this->items, $this->getIterableItemsAsArray($items))); } /** * Get and remove the last item from the collection. * * @return mixed|null */ public function pop() { return array_pop($this->items); } /** * Push an item onto the beginning of the collection. * * @param mixed $value * * @return void */ public function prepend($value): void { array_unshift($this->items, $value); } /** * Push an item onto the end of the collection. * * @param mixed $value * * @return void */ public function push($value): void { $this->items[] = $value; } /** * Put an item in the collection by key. * * @param string $key * @param mixed $value * * @return void */ public function put(string $key, $value): void { $this->items[$key] = $value; } /** * Reduce the collection to a single value. * * @param callable $callback * @param mixed $initial * * @return mixed */ public function reduce(callable $callback, $initial = null) { return array_reduce($this->items, $callback, $initial); } /** * Get one or more items randomly from the collection. * * @param int $amount * * @return mixed */ public function random(int $amount = 1) { $keys = array_rand($this->items, $amount); return is_array($keys) ? array_intersect_key($this->items, array_flip($keys)) : $this->items[$keys]; } /** * Reverse items order. * * @return Collection */ public function reverse(): Collection { return new static(array_reverse($this->items)); } /** * Get and remove the first item from the collection. * * @return mixed|null */ public function shift() { return array_shift($this->items); } /** * Slice the underlying collection array. * * @param int $offset * @param int|null $length * @param bool $preserveKeys * * @return Collection */ public function slice(int $offset, ?int $length = null, bool $preserveKeys = false): Collection { return new static(array_slice($this->items, $offset, $length, $preserveKeys)); } /** * Sort through each item with a callback. * * @param Closure $callback * * @return Collection */ public function sort(Closure $callback): Collection { uasort($this->items, $callback); return $this; } /** * Sort the collection using the given Closure. * * @param \Closure|string $callback * @param int $options * @param bool $descending * * @return Collection */ public function sortBy($callback, int $options = SORT_REGULAR, bool $descending = false): Collection { $results = []; if (is_string($callback)) { $callback = $this->valueRetriever($callback); } // First we will loop through the items and get the comparator from a callback // function which we were given. Then, we will sort the returned values and // and grab the corresponding values for the sorted keys from this array. foreach ($this->items as $key => $value) { $results[$key] = $callback($value); } $descending ? arsort($results, $options) : asort($results, $options); // Once we have sorted all of the keys in the array, we will loop through them // and grab the corresponding model so we can set the underlying items list // to the sorted version. Then we'll just return the collection instance. foreach (array_keys($results) as $key) { $results[$key] = $this->items[$key]; } $this->items = $results; return $this; } /** * Sort the collection in descending order using the given Closure. * * @param \Closure|string $callback * @param int $options * * @return Collection */ public function sortByDesc($callback, int $options = SORT_REGULAR): Collection { return $this->sortBy($callback, $options, true); } /** * Splice portion of the underlying collection array. * * @param int $offset * @param int $length * @param mixed $replacement * * @return Collection */ public function splice(int $offset, int $length = 0, $replacement = []): Collection { return new static(array_splice($this->items, $offset, $length, $replacement)); } /** * Get the sum of the given values. * * @param \Closure|string $callback * * @return mixed */ public function sum($callback) { if (is_string($callback)) { $callback = $this->valueRetriever($callback); } return $this->reduce(function ($result, $item) use ($callback) { return $result += $callback($item); }, 0); } /** * Take the first or last {$limit} items. * * @param int|null $limit * * @return Collection */ public function take(?int $limit = null): Collection { if ($limit < 0) { return $this->slice($limit, abs($limit)); } return $this->slice(0, $limit); } /** * Resets the Collection (removes all items) * * @return Collection */ public function reset(): Collection { $this->items = []; return $this; } /** * Transform each item in the collection using a callback. * * @param callable $callback * * @return Collection */ public function transform(callable $callback): Collection { $this->items = array_map($callback, $this->items); return $this; } /** * Return only unique items from the collection array. * * @return Collection */ public function unique(): Collection { return new static(array_unique($this->items)); } /** * Reset the keys on the underlying array. * * @return Collection */ public function values(): Collection { $this->items = array_values($this->items); return $this; } /** * Get the collection of items as a plain array. * * @return array */ public function toArray(): array { return array_map(function ($value) { return (is_object($value) && method_exists($value, 'toArray')) ? $value->toArray() : $value; }, $this->items); } /** * Convert the object into something JSON serializable. * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize(): array { return $this->toArray(); } /** * Get the collection of items as JSON. * * @param int $options * * @return string */ public function toJson(int $options = 0): string { return json_encode($this->toArray(), $options); } /** * Get an iterator for the items. * * @return ArrayIterator */ public function getIterator(): ArrayIterator { return new ArrayIterator($this->items); } /** * Get a CachingIterator instance. * * @param integer $flags Caching iterator flags * * @return \CachingIterator */ public function getCachingIterator(int $flags = CachingIterator::CALL_TOSTRING): CachingIterator { return new \CachingIterator($this->getIterator(), $flags); } /** * Count the number of items in the collection. * * @return int */ public function count(): int { return count($this->items); } /** * Determine if an item exists at an offset. * * @param mixed $key * * @return bool */ public function offsetExists($key): bool { return array_key_exists($key, $this->items); } /** * Get an item at a given offset. * * @param mixed $key * * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->items[$key]; } /** * Set the item at a given offset. * * @param mixed $key * @param mixed $value * * @return void */ #[\ReturnTypeWillChange] public function offsetSet($key, $value) { if (is_null($key)) { $this->items[] = $value; } else { $this->items[$key] = $value; } } /** * Unset the item at a given offset. * * @param string $key * * @return void */ #[\ReturnTypeWillChange] public function offsetUnset($key) { unset($this->items[$key]); } /** * Convert the collection to its string representation. * * @return string */ public function __toString(): string { return $this->toJson(); } /** * Get a value retrieving callback. * * @param string $value * * @return \Closure */ protected function valueRetriever(string $value): callable { return function ($item) use ($value) { return is_object($item) ? $item->{$value} : array_get($item, $value); }; } /** * Results array of items from Collection. * * @param Collection|array $items * * @return array */ private function getIterableItemsAsArray($items): array { if ($items instanceof Collection) { $items = $items->all(); } elseif (is_object($items) && method_exists($items, 'toArray')) { $items = $items->toArray(); } return $items; } } CacheCleaner.php 0000604 00000006260 15245522752 0007563 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use Exception; use Joomla\CMS\Cache\Cache; use Joomla\CMS\Factory; use Joomla\CMS\MVC\Model\BaseDatabaseModel; /** * 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 */ public static function clearCacheGroups(array $clearGroups, array $cacheClients = [0, 1], $event = null) { try { $app = Factory::getApplication(); } catch (Exception $e) { return; } foreach ($clearGroups as $group) { foreach ($cacheClients as $client_id) { $options = [ 'defaultgroup' => $group, 'cachebase' => ($client_id) ? JPATH_ADMINISTRATOR . '/cache' : $app->get('cache_path', JPATH_SITE . '/cache'), 'result' => true, ]; try { $cache = Cache::getInstance('callback', $options); /** @noinspection PhpUndefinedMethodInspection Available via __call(), not tagged in Joomla core */ $cache->clean(); } catch (Exception $e) { $options['result'] = false; } /** * Call Joomla's cache cleaning plugin event (e.g. onContentCleanCache) as well. * * @see BaseDatabaseModel::cleanCache() */ if (!empty($event)) { $app->triggerEvent($event, $options); } /** * If you're cleaning com_content and you have passed no event name I will use onContentCleanCache. */ elseif (($group == 'com_content') && ($event !== '')) { $app->triggerEvent('onContentCleanCache', $options); } } } } } DynamicGroups.php 0000604 00000013433 15245522752 0010052 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use FOF30\Container\Container; use ReflectionClass; use ReflectionException; use ReflectionObject; /** * 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($groupID) { 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($groupID) { 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($groupID, $add) { // 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 = class_exists('Joomla\\CMS\\Access\\Access') ? 'Joomla\\CMS\\Access\\Access' : 'JAccess'; try { $reflectedAccess = new ReflectionClass($className); } catch (ReflectionException $e) { // This should never happen! $container->platform->logDebug('Cannot locate the Joomla\\CMS\\Access\\Access or JAccess 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 (JAccess::$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 = [$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() { // 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, []); $refProperty->setAccessible(false); // Clear the view access level cache $refProperty = $reflectedUser->getProperty('_authLevels'); $refProperty->setAccessible(true); $refProperty->setValue($user, []); $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, []); $refProperty->setAccessible(false); } } Ip.php 0000604 00000033306 15245522752 0005637 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; /** * IP address helper * * Makes sure that we get the real IP of the user */ class Ip { /** * The IP address of the current visitor * * @var string */ protected static $ip = null; /** * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? * * @var bool */ protected static $allowIpOverrides = true; /** * See self::detectAndCleanIP and setUseFirstIpInChain * * If this is enabled (default) self::detectAndCleanIP will return the FIRST IP in case there is an IP chain coming * for example from an X-Forwarded-For HTTP header. When set to false it will simulate the old behavior in FOF up to * and including 3.1.1 which returned the LAST IP in the list. * * @var bool */ protected static $useFirstIpInChain = true; /** * List of headers we should check to know if the user is behind a proxy. PLEASE NOTE: ORDER MATTERS! * * @var array */ protected static $proxyHeaders = [ 'HTTP_CF_CONNECTING_IP', // CloudFlare 'HTTP_X_FORWARDED_FOR', // Standard for transparent proxy (e.g. NginX) 'HTTP_X_SUCURI_CLIENTIP', // Sucuri firewall uses its own header ]; /** * Set the $useFirstIpInChain flag. See above. * * @param bool $value */ public static function setUseFirstIpInChain($value = true) { self::$useFirstIpInChain = $value; } /** * Getter for the list of proxy headers we can check * * @return array */ public static function getProxyHeaders() { return static::$proxyHeaders; } /** * Get the current visitor's IP address * * @return string */ public static function getIp() { if (is_null(static::$ip)) { $ip = self::detectAndCleanIP(); if (!empty($ip) && ($ip != '0.0.0.0') && function_exists('inet_pton') && function_exists('inet_ntop')) { $myIP = @inet_pton($ip); if ($myIP !== false) { $ip = inet_ntop($myIP); } } static::setIp($ip); } return static::$ip; } /** * Set the IP address of the current visitor * * @param string $ip * * @return void */ public static function setIp($ip) { static::$ip = $ip; } /** * Checks if an IP is contained in a list of IPs or IP expressions * * @param string $ip The IPv4/IPv6 address to check * @param array|string $ipTable An IP expression (or a comma-separated or array list of IP expressions) to * check against * * @return null|boolean True if it's in the list */ public static function IPinList($ip, $ipTable = '') { // No point proceeding with an empty IP list if (empty($ipTable)) { return false; } // If the IP list is not an array, convert it to an array if (!is_array($ipTable)) { if (strpos($ipTable, ',') !== false) { $ipTable = explode(',', $ipTable); $ipTable = array_map(function ($x) { return trim($x); }, $ipTable); } else { $ipTable = trim($ipTable); $ipTable = [$ipTable]; } } // If no IP address is found, return false if ($ip == '0.0.0.0') { return false; } // If no IP is given, return false if (empty($ip)) { return false; } // Sanity check if (!function_exists('inet_pton')) { return false; } // Get the IP's in_adds representation $myIP = @inet_pton($ip); // If the IP is in an unrecognisable format, quite if ($myIP === false) { return false; } $ipv6 = self::isIPv6($ip); foreach ($ipTable as $ipExpression) { $ipExpression = trim($ipExpression); // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 if (strstr($ipExpression, '-')) { [$from, $to] = explode('-', $ipExpression, 2); if ($ipv6 && (!self::isIPv6($from) || !self::isIPv6($to))) { // Do not apply IPv4 filtering on an IPv6 address continue; } elseif (!$ipv6 && (self::isIPv6($from) || self::isIPv6($to))) { // Do not apply IPv6 filtering on an IPv4 address continue; } $from = @inet_pton(trim($from)); $to = @inet_pton(trim($to)); // Sanity check if (($from === false) || ($to === false)) { continue; } // Swap from/to if they're in the wrong order if ($from > $to) { [$from, $to] = [$to, $from]; } if (($myIP >= $from) && ($myIP <= $to)) { return true; } } // Netmask or CIDR provided elseif (strstr($ipExpression, '/')) { $binaryip = self::inet_to_bits($myIP); [$net, $maskbits] = explode('/', $ipExpression, 2); if ($ipv6 && !self::isIPv6($net)) { // Do not apply IPv4 filtering on an IPv6 address continue; } elseif (!$ipv6 && self::isIPv6($net)) { // Do not apply IPv6 filtering on an IPv4 address continue; } elseif ($ipv6 && strstr($maskbits, ':')) { // Perform an IPv6 CIDR check if (self::checkIPv6CIDR($myIP, $ipExpression)) { return true; } // If we didn't match it proceed to the next expression continue; } elseif (!$ipv6 && strstr($maskbits, '.')) { // Convert IPv4 netmask to CIDR $long = ip2long($maskbits); $base = ip2long('255.255.255.255'); $maskbits = 32 - log(($long ^ $base) + 1, 2); } // Convert network IP to in_addr representation $net = @inet_pton($net); // Sanity check if ($net === false) { continue; } // Get the network's binary representation $binarynet = self::inet_to_bits($net); $expectedNumberOfBits = $ipv6 ? 128 : 24; $binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT); // Check the corresponding bits of the IP and the network $ip_net_bits = substr($binaryip, 0, $maskbits); $net_bits = substr($binarynet, 0, $maskbits); if ($ip_net_bits == $net_bits) { return true; } } else { // IPv6: Only single IPs are supported if ($ipv6) { $ipExpression = trim($ipExpression); if (!self::isIPv6($ipExpression)) { continue; } $ipCheck = @inet_pton($ipExpression); if ($ipCheck === false) { continue; } if ($ipCheck == $myIP) { return true; } } else { // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] $dots = 0; if (substr($ipExpression, -1) == '.') { // Partial IP address. Convert to CIDR and re-match foreach (count_chars($ipExpression, 1) as $i => $val) { if ($i == 46) { $dots = $val; } } $netmask = '255.255.255.255'; switch ($dots) { case 1: $netmask = '255.0.0.0'; $ipExpression .= '0.0.0'; break; case 2: $netmask = '255.255.0.0'; $ipExpression .= '0.0'; break; case 3: $netmask = '255.255.255.0'; $ipExpression .= '0'; break; default: $dots = 0; } if ($dots) { $binaryip = self::inet_to_bits($myIP); // Convert netmask to CIDR $long = ip2long($netmask); $base = ip2long('255.255.255.255'); $maskbits = 32 - log(($long ^ $base) + 1, 2); $net = @inet_pton($ipExpression); // Sanity check if ($net === false) { continue; } // Get the network's binary representation $binarynet = self::inet_to_bits($net); $expectedNumberOfBits = $ipv6 ? 128 : 24; $binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT); // Check the corresponding bits of the IP and the network $ip_net_bits = substr($binaryip, 0, $maskbits); $net_bits = substr($binarynet, 0, $maskbits); if ($ip_net_bits == $net_bits) { return true; } } } if (!$dots) { $ip = @inet_pton(trim($ipExpression)); if ($ip == $myIP) { return true; } } } } } return false; } /** * Works around the REMOTE_ADDR not containing the user's IP */ public static function workaroundIPIssues() { $ip = self::getIp(); if (array_key_exists('REMOTE_ADDR', $_SERVER) && ($_SERVER['REMOTE_ADDR'] == $ip)) { return; } if (array_key_exists('REMOTE_ADDR', $_SERVER)) { $_SERVER['FOF_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; } elseif (function_exists('getenv')) { if (getenv('REMOTE_ADDR')) { $_SERVER['FOF_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); } } $_SERVER['REMOTE_ADDR'] = $ip; } /** * Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header? * * @param bool $newState True to allow the override * * @return void */ public static function setAllowIpOverrides($newState) { self::$allowIpOverrides = $newState ? true : false; } /** * Is it an IPv6 IP address? * * @param string $ip An IPv4 or IPv6 address * * @return boolean True if it's IPv6 */ protected static function isIPv6($ip) { if (strstr($ip, ':')) { return true; } return false; } /** * Gets the visitor's IP address. Automatically handles reverse proxies * reporting the IPs of intermediate devices, like load balancers. Examples: * https://www.akeeba.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html * http://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips * The solution used is assuming that the first IP address is the external one (unless $useFirstIpInChain is set to * false) * * @return string */ protected static function detectAndCleanIP() { $ip = static::detectIP(); if ((strstr($ip, ',') !== false) || (strstr($ip, ' ') !== false)) { $ip = str_replace(' ', ',', $ip); $ip = str_replace(',,', ',', $ip); $ips = explode(',', $ip); $ip = ''; // Loop until we're running out of parts or we have a hit while ($ips) { $ip = array_shift($ips); $ip = trim($ip); if (self::$useFirstIpInChain) { return self::cleanIP($ip); } } } return self::cleanIP($ip); } protected static function cleanIP($ip) { $ip = trim($ip); $ip = strtoupper($ip); /** * Work around IPv4-mapped addresses. * * IPv4 addresses may be embedded in an IPv6 address. This is always 80 zeroes, 16 ones and the IPv4 address. * In all possible IPv6 notations this is: * 0:0:0:0:0:FFFF:192.168.1.1 * ::FFFF:192.168.1.1 * ::FFFF:C0A8:0101 * * @see http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding-2.htm */ if ((strpos($ip, '::FFFF:') === 0) || (strpos($ip, '0:0:0:0:0:FFFF:') === 0)) { // Fast path: the embedded IPv4 is in decimal notation. if (strstr($ip, '.') !== false) { return substr($ip, strrpos($ip, ':') + 1); } // Get the embedded IPv4 (in hex notation) $ip = substr($ip, strpos($ip, ':FFFF:') + 6); // Convert each 16-bit WORD to decimal [$word1, $word2] = explode(':', $ip); $word1 = hexdec($word1); $word2 = hexdec($word2); $longIp = $word1 * 65536 + $word2; return long2ip($longIp); } return $ip; } /** * Gets the visitor's IP address * * @return string */ protected static function detectIP() { // Normally the $_SERVER superglobal is set if (isset($_SERVER)) { // Do we have IP overrides enabled? if (static::$allowIpOverrides) { // If so, check for every proxy header foreach (static::$proxyHeaders as $header) { if (array_key_exists($header, $_SERVER)) { return $_SERVER[$header]; } } } // CLI applications if (!array_key_exists('REMOTE_ADDR', $_SERVER)) { return ''; } // Normal, non-proxied server or server behind a transparent proxy return $_SERVER['REMOTE_ADDR']; } // This part is executed on PHP running as CGI, or on SAPIs which do // not set the $_SERVER superglobal // If getenv() is disabled, you're screwed if (!function_exists('getenv')) { return ''; } // Do we have IP overrides enabled? if (static::$allowIpOverrides) { // If so, check for every proxy header foreach (static::$proxyHeaders as $header) { if (getenv($header)) { return getenv($header); } } } // Normal, non-proxied server or server behind a transparent proxy if (getenv('REMOTE_ADDR')) { return getenv('REMOTE_ADDR'); } // Catch-all case for broken servers and CLI applications return ''; } /** * Converts inet_pton output to bits string * * @param string $inet The in_addr representation of an IPv4 or IPv6 address * * @return string */ protected static function inet_to_bits($inet) { if (strlen($inet) == 4) { $unpacked = unpack('C4', $inet); } else { $unpacked = unpack('C16', $inet); } $binaryip = ''; foreach ($unpacked as $byte) { $binaryip .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT); } return $binaryip; } /** * Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet * * @param string $ip The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A * @param string $cidrnet The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64 * * @return bool */ protected static function checkIPv6CIDR($ip, $cidrnet) { $ip = inet_pton($ip); $binaryip = self::inet_to_bits($ip); [$net, $maskbits] = explode('/', $cidrnet); $net = inet_pton($net); $binarynet = self::inet_to_bits($net); $ip_net_bits = substr($binaryip, 0, $maskbits); $net_bits = substr($binarynet, 0, $maskbits); return $ip_net_bits === $net_bits; } } SelectOptions.php 0000604 00000024746 15245522752 0010072 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use InvalidArgumentException; use Joomla\CMS\Cache\Cache; use Joomla\CMS\Factory; use Joomla\CMS\Helper\UserGroupsHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\LanguageHelper; use Joomla\CMS\Language\Text; use stdClass; /** * Returns arrays of JHtml select options for Joomla-specific information such as access levels. */ class SelectOptions { private static $cache = []; /** * Magic method to handle static calls * * @param string $name The name of the static method being called * @param string $arguments Ignored. * * @return mixed * @since 3.3.0 * */ public static function __callStatic($name, $arguments) { return self::getOptions($name, $arguments); } /** * Get a list of Joomla options of the type you specify. Supported types * - access View access levels * - usergroups User groups * - cachehandlers Cache handlers * - components Installed components accessible by the current user * - languages Site or administrator languages * - published Published status * * Global params: * - cache Should I returned cached data? Default: true. * * See the private static methods of this class for more information on params. * * @param string $type The options type to get * @param array $params Optional arguments, if they are supported by the options type. * * @return stdClass[] * @since 3.3.0 * */ public static function getOptions($type, array $params = []) { if ((substr($type, 0, 1) == '_') || !method_exists(__CLASS__, $type)) { throw new InvalidArgumentException(__CLASS__ . "does not support option type '$type'."); } $useCache = true; if (isset($params['cache'])) { $useCache = isset($params['cache']); unset($params['cache']); } $cacheKey = sha1($type . '--' . print_r($params, true)); $fetchNew = !$useCache || ($useCache && !isset(self::$cache[$cacheKey])); if ($fetchNew) { $ret = forward_static_call_array([__CLASS__, $type], [$params]); } if (!$useCache) { return $ret; } if ($fetchNew) { self::$cache[$cacheKey] = $ret; } return self::$cache[$cacheKey]; } /** * Joomla! Access Levels (previously: view access levels) * * Available params: * - allLevels: Show an option for all levels (default: false) * * @param array $params Parameters * * @return stdClass[] * * @since 3.3.0 * * @see \JHtmlAccess::level() */ private static function access(array $params = []) { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select($db->quoteName('a.id', 'value') . ', ' . $db->quoteName('a.title', 'text')) ->from($db->quoteName('#__viewlevels', 'a')) ->group($db->quoteName(['a.id', 'a.title', 'a.ordering'])) ->order($db->quoteName('a.ordering') . ' ASC') ->order($db->quoteName('title') . ' ASC'); // Get the options. $db->setQuery($query); $options = $db->loadObjectList(); if (isset($params['allLevels']) && $params['allLevels']) { array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_LEVELS'))); } return $options; } /** * Joomla! User Groups * * Available params: * - allGroups: Show an option for all groups (default: false) * * @param array $params Parameters * * @return stdClass[] * * @since 3.3.0 * * @see \JHtmlAccess::usergroup() */ private static function usergroups(array $params = []) { $options = array_values(UserGroupsHelper::getInstance()->getAll()); for ($i = 0, $n = count($options); $i < $n; $i++) { $options[$i]->value = $options[$i]->id; $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; } // If all usergroups is allowed, push it into the array. if (isset($params['allGroups']) && $params['allGroups']) { array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_GROUPS'))); } return $options; } /** * Joomla cache handlers * * @return stdClass[] * @since 3.3.0 * */ private static function cachehandlers() { $options = []; // Convert to name => name array. foreach (Cache::getStores() as $store) { $options[] = HTMLHelper::_('select.option', $store, Text::_('JLIB_FORM_VALUE_CACHE_' . $store), 'value', 'text'); } return $options; } /** * Get a list of all installed components and also translates them. * * Available params: * - client_ids Array of Joomla application client IDs * * @param array $params * * @return stdClass[] * @since 3.3.0 * */ private static function components(array $params) { $db = Factory::getDbo(); // Check for client_ids override $client_ids = $params['client_ids'] ?? [0, 1]; if (is_string($client_ids)) { $client_ids = explode(',', $client_ids); } // Calculate client_ids where clause $client_ids = array_map(function ($client_id) use ($db) { return $db->q((int) trim($client_id)); }, $client_ids); $query = $db->getQuery(true) ->select( [ $db->qn('name'), $db->qn('element'), $db->qn('client_id'), $db->qn('manifest_cache'), ] ) ->from($db->qn('#__extensions')) ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('client_id') . ' IN (' . implode(',', $client_ids) . ')'); $components = $db->setQuery($query)->loadObjectList('element'); // Convert to array of objects, so we can use sortObjects() // Also translate component names with JText::_() $aComponents = []; $user = Factory::getUser(); foreach ($components as $component) { // Don't show components in the list where the user doesn't have access for // TODO: perhaps add an option for this if (!$user->authorise('core.manage', $component->element)) { continue; } $aComponents[$component->element] = (object) [ 'value' => $component->element, 'text' => self::_translateComponentName($component), ]; } // Reorder the components array, because the alphabetical // ordering changed due to the JText::_() translation uasort( $aComponents, function ($a, $b) { return strcasecmp($a->text, $b->text); } ); return $aComponents; } /** * Method to get the field options. * * Available params: * - client 'site' (default) or 'administrator' * - none Text to show for "all languages" option, use empty string to remove it * * @return array Languages for the specified client * @since 3.3.0 * */ private static function languages($params) { $db = Factory::getDbo(); $client = $params['client'] ?? 'site'; if (!in_array($client, ['site', 'administrator'])) { $client = 'site'; } // Make sure the languages are sorted base on locale instead of random sorting $options = LanguageHelper::createLanguageList(null, constant('JPATH_' . strtoupper($client)), true, true); if (count($options) > 1) { usort( $options, function ($a, $b) { return strcmp($a['value'], $b['value']); } ); } $none = $params['none'] ?? '*'; if (!empty($none)) { array_unshift($options, HTMLHelper::_('select.option', '*', Text::_($none))); } return $options; } /** * Options for a Published field * * Params: * - none Placeholder for no selection (empty key). Default: null. * - published Show "Published"? Default: true * - unpublished Show "Unpublished"? Default: true * - archived Show "Archived"? Default: false * - trash Show "Trashed"? Default: false * - all Show "All" option? This is different than none, the key is '*'. Default: false * * @param $params * * @return array */ private static function published(array $params = []) { $config = array_merge([ 'none' => '', 'published' => true, 'unpublished' => true, 'archived' => false, 'trash' => false, 'all' => false, ], $params); $options = []; if (!empty($config['none'])) { $options[] = HTMLHelper::_('select.option', '', Text::_($config['none'])); } if ($config['published']) { $options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED')); } if ($config['unpublished']) { $options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED')); } if ($config['archived']) { $options[] = HTMLHelper::_('select.option', '2', Text::_('JARCHIVED')); } if ($config['trash']) { $options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED')); } if ($config['all']) { $options[] = HTMLHelper::_('select.option', '*', Text::_('JALL')); } return $options; } /** * Options for a Published field * * Params: * - none Placeholder for no selection (empty key). Default: null. * * @param $params * * @return array */ private static function boolean(array $params = []) { $config = array_merge([ 'none' => '', ], $params); $options = []; if (!empty($config['none'])) { $options[] = HTMLHelper::_('select.option', '', Text::_($config['none'])); } $options[] = HTMLHelper::_('select.option', '1', Text::_('JYES')); $options[] = HTMLHelper::_('select.option', '0', Text::_('JNO')); return $options; } /** * Translate a component name * * @param stdClass $item The component object * * @return string $text The translated name of the extension * * @since 3.3.0 * * @see administrator/com_installer/models/extension.php */ private static function _translateComponentName($item) { // Map the manifest cache to $item. This is needed to get the name from the // manifest_cache and NOT from the name column, else some JText::_() translations fails. $mData = json_decode($item->manifest_cache); if ($mData) { foreach ($mData as $key => $value) { if ($key == 'type') { // Ignore the type field continue; } $item->$key = $value; } } $lang = Factory::getLanguage(); $source = JPATH_ADMINISTRATOR . '/components/' . $item->element; $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, null, false, false) || $lang->load("$item->element.sys", $source, null, false, false) || $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) || $lang->load("$item->element.sys", $source, $lang->getDefault(), false, false); return Text::_($item->name); } } InstallScript.php 0000604 00000001360 15245522752 0010055 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use FOF30\Utils\InstallScript\Component; // Make sure the new class can be loaded if (!class_exists('FOF30\\Utils\\InstallScript\\Component', true)) { require_once __DIR__ . '/InstallScript/Component.php'; } /** * A helper class which you can use to create component installation scripts. * * This is the old location of the installation script class, maintained for backwards compatibility with FOF 3.0. Please * use the new class FOF30\Utils\InstallScript\Component instead. */ class InstallScript extends Component { } ArrayHelper.php 0000604 00000031500 15245522752 0007477 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\Utils; defined('_JEXEC') || die; /** * ArrayHelper is an array utility class for doing all sorts of odds and ends with arrays. * * Copied from Joomla Framework to avoid class name issues between Joomla! versions 3 and 4. sortObjects is not included * because it needs the UTF-8 package. If you need to use that then you should be using the Joomla! Framework's helper * anyway. */ final class ArrayHelper { /** * Private constructor to prevent instantiation of this class * * @since 1.0 */ private function __construct() { } /** * Function to convert array to integer values * * @param array $array The source array to convert * @param int|array|null $default A default value (int|array) to assign if $array is not an array * * @return int[] * * @since 1.0 */ public static function toInteger(array $array, $default = null): array { if (is_array($array)) { return array_map('intval', $array); } if ($default === null) { return []; } if (is_array($default)) { return static::toInteger($default, null); } return [(int) $default]; } /** * Utility function to map an array to a stdClass object. * * @param array $array The array to map. * @param string $class Name of the class to create * @param boolean $recursive Convert also any array inside the main array * * @return object * * @since 1.0 */ public static function toObject(array $array, string $class = 'stdClass', bool $recursive = true) { $obj = new $class; foreach ($array as $k => $v) { $obj->$k = ($recursive && is_array($v)) ? static::toObject($v, $class) : $v; } return $obj; } /** * Utility function to map an array to a string. * * @param array $array The array to map. * @param string $inner_glue The glue (optional, defaults to '=') between the key and the value. * @param string $outer_glue The glue (optional, defaults to ' ') between array elements. * @param boolean $keepOuterKey True if final key should be kept. * * @return string * * @since 1.0 */ public static function toString(array $array, string $inner_glue = '=', string $outer_glue = ' ', bool $keepOuterKey = false): string { $output = []; foreach ($array as $key => $item) { if (is_array($item)) { if ($keepOuterKey) { $output[] = $key; } // This is value is an array, go and do it again! $output[] = static::toString($item, $inner_glue, $outer_glue, $keepOuterKey); } else { $output[] = $key . $inner_glue . '"' . $item . '"'; } } return implode($outer_glue, $output); } /** * Utility function to map an object to an array * * @param object $p_obj The source object * @param boolean $recurse True to recurse through multi-level objects * @param string|null $regex An optional regular expression to match on field names * * @return array * * @since 1.0 */ public static function fromObject($p_obj, bool $recurse = true, ?string $regex = null): array { if (is_object($p_obj) || is_array($p_obj)) { return self::arrayFromObject($p_obj, $recurse, $regex); } return []; } /** * Extracts a column from an array of arrays or objects * * @param array $array The source array * @param string $valueCol The index of the column or name of object property to be used as value * It may also be NULL to return complete arrays or objects (this is * useful together with <var>$keyCol</var> to reindex the array). * @param string|null $keyCol The index of the column or name of object property to be used as key * * @return array Column of values from the source array * * @since 1.0 * @see http://php.net/manual/en/language.types.array.php * @see http://php.net/manual/en/function.array-column.php */ public static function getColumn(array $array, string $valueCol, ?string $keyCol = null): array { $result = []; foreach ($array as $item) { // Convert object to array $subject = is_object($item) ? static::fromObject($item) : $item; /* * We process arrays (and objects already converted to array) * Only if the value column (if required) exists in this item */ if (is_array($subject) && (!isset($valueCol) || isset($subject[$valueCol]))) { // Use whole $item if valueCol is null, else use the value column. $value = isset($valueCol) ? $subject[$valueCol] : $item; // Array keys can only be integer or string. Casting will occur as per the PHP Manual. if (isset($keyCol) && isset($subject[$keyCol]) && is_scalar($subject[$keyCol])) { $key = $subject[$keyCol]; $result[$key] = $value; } else { $result[] = $value; } } } return $result; } /** * Utility function to return a value from a named array or a specified default * * @param array|\ArrayAccess $array A named array or object that implements ArrayAccess * @param string $name The key to search for * @param mixed $default The default value to give if no key found * @param string $type Return type for the variable (INT, FLOAT, STRING, WORD, BOOLEAN, ARRAY) * * @return mixed * * @throws \InvalidArgumentException * @since 1.0 */ public static function getValue(array $array, string $name, $default = null, string $type = '') { if (!is_array($array) && !($array instanceof \ArrayAccess)) { throw new \InvalidArgumentException('The object must be an array or an object that implements ArrayAccess'); } $result = null; if (isset($array[$name])) { $result = $array[$name]; } // Handle the default case if (is_null($result)) { $result = $default; } // Handle the type constraint switch (strtoupper($type)) { case 'INT': case 'INTEGER': // Only use the first integer value @preg_match('/-?\d+/', $result, $matches); $result = @(int) $matches[0]; break; case 'FLOAT': case 'DOUBLE': // Only use the first floating point value @preg_match('/-?\d+(\.\d+)?/', $result, $matches); $result = @(float) $matches[0]; break; case 'BOOL': case 'BOOLEAN': $result = (bool) $result; break; case 'ARRAY': if (!is_array($result)) { $result = [$result]; } break; case 'STRING': $result = (string) $result; break; case 'WORD': $result = (string) preg_replace('#\W#', '', $result); break; case 'NONE': default: // No casting necessary break; } return $result; } /** * Takes an associative array of arrays and inverts the array keys to values using the array values as keys. * * Example: * $input = array( * 'New' => array('1000', '1500', '1750'), * 'Used' => array('3000', '4000', '5000', '6000') * ); * $output = ArrayHelper::invert($input); * * Output would be equal to: * $output = array( * '1000' => 'New', * '1500' => 'New', * '1750' => 'New', * '3000' => 'Used', * '4000' => 'Used', * '5000' => 'Used', * '6000' => 'Used' * ); * * @param array $array The source array. * * @return array * * @since 1.0 */ public static function invert(array $array): array { $return = []; foreach ($array as $base => $values) { if (!is_array($values)) { continue; } foreach ($values as $key) { // If the key isn't scalar then ignore it. if (is_scalar($key)) { $return[$key] = $base; } } } return $return; } /** * Method to determine if an array is an associative array. * * @param array $array An array to test. * * @return boolean * * @since 1.0 */ public static function isAssociative(array $array): bool { if (is_array($array)) { foreach (array_keys($array) as $k => $v) { if ($k !== $v) { return true; } } } return false; } /** * Pivots an array to create a reverse lookup of an array of scalars, arrays or objects. * * @param array $source The source array. * @param string|null $key Where the elements of the source array are objects or arrays, the key to pivot * on. * * @return array An array of arrays pivoted either on the value of the keys, or an individual key of an object or * array. * * @since 1.0 */ public static function pivot(array $source, ?string $key = null): array { $result = []; $counter = []; foreach ($source as $index => $value) { // Determine the name of the pivot key, and its value. if (is_array($value)) { // If the key does not exist, ignore it. if (!isset($value[$key])) { continue; } $resultKey = $value[$key]; $resultValue = $source[$index]; } elseif (is_object($value)) { // If the key does not exist, ignore it. if (!isset($value->$key)) { continue; } $resultKey = $value->$key; $resultValue = $source[$index]; } else { // Just a scalar value. $resultKey = $value; $resultValue = $index; } // The counter tracks how many times a key has been used. if (empty($counter[$resultKey])) { // The first time around we just assign the value to the key. $result[$resultKey] = $resultValue; $counter[$resultKey] = 1; } elseif ($counter[$resultKey] == 1) { // If there is a second time, we convert the value into an array. $result[$resultKey] = [ $result[$resultKey], $resultValue, ]; $counter[$resultKey]++; } else { // After the second time, no need to track any more. Just append to the existing array. $result[$resultKey][] = $resultValue; } } unset($counter); return $result; } /** * Multidimensional-array-safe unique test * * @param array $array The array to make unique. * * @return array * * @see http://php.net/manual/en/function.array-unique.php * @since 1.0 */ public static function arrayUnique(array $array): array { $array = array_map('serialize', $array); $array = array_unique($array); return array_map('unserialize', $array); } /** * An improved array_search that allows for partial matching of strings values in associative arrays. * * @param string $needle The text to search for within the array. * @param array $haystack Associative array to search in to find $needle. * @param boolean $caseSensitive True to search case sensitive, false otherwise. * * @return mixed Returns the matching array $key if found, otherwise false. * * @since 1.0 */ public static function arraySearch(string $needle, array $haystack, bool $caseSensitive = true) { foreach ($haystack as $key => $value) { $searchFunc = ($caseSensitive) ? 'strpos' : 'stripos'; if ($searchFunc($value, $needle) === 0) { return $key; } } return false; } /** * Method to recursively convert data to a one dimension array. * * @param array|object $array The array or object to convert. * @param string $separator The key separator. * @param string $prefix Last level key prefix. * * @return array * * @since 1.3.0 */ public static function flatten($array, string $separator = '.', string $prefix = ''): array { if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (is_object($array)) { $array = get_object_vars($array); } foreach ($array as $k => $v) { $key = $prefix ? ($prefix . $separator . $k) : $k; if (is_object($v) || is_array($v)) { $array = array_merge($array, static::flatten($v, $separator, $key)); } else { $array[$key] = $v; } } return $array; } /** * Utility function to map an object or array to an array * * @param mixed $item The source object or array * @param boolean $recurse True to recurse through multi-level objects * @param string $regex An optional regular expression to match on field names * * @return array * * @since 1.0 */ private static function arrayFromObject($item, bool $recurse, ?string $regex): array { if (is_object($item)) { $result = []; foreach (get_object_vars($item) as $k => $v) { if (!$regex || preg_match($regex, $k)) { $result[$k] = $recurse ? self::arrayFromObject($v, $recurse, $regex) : $v; } } return $result; } if (is_array($item)) { $result = []; foreach ($item as $k => $v) { $result[$k] = self::arrayFromObject($v, $recurse, $regex); } return $result; } return $item; } } Buffer.php 0000604 00000015013 15245522752 0006473 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\Utils; defined('_JEXEC') || die; /** * Registers a fof:// stream wrapper */ class Buffer { /** * Buffer hash * * @var array */ public static $buffers = []; public static $canRegisterWrapper; /** * Stream position * * @var integer */ public $position = 0; /** * Buffer name * * @var string */ public $name; /** * Should I register the fof:// stream wrapper * * @return bool True if the stream wrapper can be registered */ public static function canRegisterWrapper(): bool { if (is_null(static::$canRegisterWrapper)) { static::$canRegisterWrapper = false; // Maybe the host has disabled registering stream wrappers altogether? if (!function_exists('stream_wrapper_register')) { return false; } // Check for Suhosin if (function_exists('extension_loaded')) { $hasSuhosin = extension_loaded('suhosin'); } else { $hasSuhosin = -1; // Can't detect } if ($hasSuhosin !== true) { $hasSuhosin = defined('SUHOSIN_PATCH') ? true : -1; } if (($hasSuhosin === -1) && function_exists('ini_get')) { $hasSuhosin = false; $maxIdLength = ini_get('suhosin.session.max_id_length'); if ($maxIdLength !== false) { $hasSuhosin = ini_get('suhosin.session.max_id_length') !== ''; } } // If we can't detect whether Suhosin is installed we won't proceed to prevent a White Screen of Death if ($hasSuhosin === -1) { return false; } // If Suhosin is installed but ini_get is not available we won't proceed to prevent a WSoD if ($hasSuhosin && !function_exists('ini_get')) { return false; } // If Suhosin is installed check if fof:// is whitelisted if ($hasSuhosin) { $whiteList = ini_get('suhosin.executor.include.whitelist'); // Nothing in the whitelist? I can't go on, sorry. if (empty($whiteList)) { return false; } $whiteList = explode(',', $whiteList); $whiteList = array_map(function ($x) { return trim($x); }, $whiteList); if (!in_array('fof://', $whiteList)) { return false; } } static::$canRegisterWrapper = true; } return static::$canRegisterWrapper; } /** * Function to open file or url * * @param string $path The URL that was passed * @param string $mode Mode used to open the file @see fopen * @param integer $options Flags used by the API, may be STREAM_USE_PATH and * STREAM_REPORT_ERRORS * @param string &$opened_path Full path of the resource. Used with STREAM_USE_PATH option * * @return boolean * * @see streamWrapper::stream_open */ public function stream_open(string $path, string $mode, ?int $options, ?string &$opened_path): bool { $url = parse_url($path); $this->name = $url['host'] . $url['path']; $this->position = 0; if (!isset(static::$buffers[$this->name])) { static::$buffers[$this->name] = null; } return true; } public function unlink(string $path): void { $url = parse_url($path); $name = $url['host']; if (isset(static::$buffers[$name])) { unset (static::$buffers[$name]); } } public function stream_stat(): array { return [ 'dev' => 0, 'ino' => 0, 'mode' => 0644, 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => strlen(static::$buffers[$this->name]), 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => -1, 'blocks' => -1, ]; } /** * Read stream * * @param integer $count How many bytes of data from the current position should be returned. * * @return mixed The data from the stream up to the specified number of bytes (all data if * the total number of bytes in the stream is less than $count. Null if * the stream is empty. * * @see streamWrapper::stream_read * @since 11.1 */ public function stream_read(int $count): ?string { $ret = substr(static::$buffers[$this->name], $this->position, $count); $this->position += strlen($ret); return $ret; } /** * Write stream * * @param string $data The data to write to the stream. * * @return integer * * @see streamWrapper::stream_write * @since 11.1 */ public function stream_write(string $data): int { $left = substr(static::$buffers[$this->name], 0, $this->position); $right = substr(static::$buffers[$this->name], $this->position + strlen($data)); static::$buffers[$this->name] = $left . $data . $right; $this->position += strlen($data); return strlen($data); } /** * Function to get the current position of the stream * * @return integer * * @see streamWrapper::stream_tell * @since 11.1 */ public function stream_tell(): int { return $this->position; } /** * Function to test for end of file pointer * * @return boolean True if the pointer is at the end of the stream * * @see streamWrapper::stream_eof * @since 11.1 */ public function stream_eof(): bool { return $this->position >= strlen(static::$buffers[$this->name]); } /** * The read write position updates in response to $offset and $whence * * @param integer $offset The offset in bytes * @param integer $whence Position the offset is added to * Options are SEEK_SET, SEEK_CUR, and SEEK_END * * @return boolean True if updated * * @see streamWrapper::stream_seek * @since 11.1 */ public function stream_seek(int $offset, int $whence): bool { switch ($whence) { case SEEK_SET: if ($offset < strlen(static::$buffers[$this->name]) && $offset >= 0) { $this->position = $offset; return true; } else { return false; } break; case SEEK_CUR: if ($offset >= 0) { $this->position += $offset; return true; } else { return false; } break; case SEEK_END: if (strlen(static::$buffers[$this->name]) + $offset >= 0) { $this->position = strlen(static::$buffers[$this->name]) + $offset; return true; } else { return false; } break; default: return false; } } } if (Buffer::canRegisterWrapper()) { stream_wrapper_register('fof', 'FOF40\\Utils\\Buffer'); } ModelTypeHints.php 0000604 00000014645 15245522752 0010204 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\Utils; defined('_JEXEC') || die; use FOF40\Model\DataModel; /** * Generate phpDoc type hints for the magic properties and methods of your DataModels. * * Usage: * $typeHinter = new ModelTypeHints($instanceOfYourFOFDataModel); * var_dump($typeHinter->getHints()); * * This will dump the type hints you should add to the DocBlock of your DataModel class to allow IDEs such as phpStorm * to provide smart type hinting for magic property and method access. * * @package FOF40\Utils */ class ModelTypeHints { /** * The model for which to create type hints * * @var DataModel */ protected $model; /** * Name of the class. If empty will be inferred from the current object * * @var string */ protected $className; /** * Public constructor * * @param \FOF40\Model\DataModel $model The model to create hints for */ public function __construct(DataModel $model) { $this->model = $model; $this->className = get_class($model); } /** * Translates the database field type into a PHP base type * * @param string $type The type of the field * * @return string The PHP base type */ public static function getFieldType(string $type): string { // 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 = 'string'; break; case 'date': case 'datetime': case 'time': case 'year': case 'timestamp': case 'timestamp without time zone': case 'timestamp with time zone': $detectedType = 'string'; break; case 'tinyint': case 'smallint': $detectedType = 'bool'; break; case 'float': case 'currency': case 'single': case 'double': $detectedType = 'float'; 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 = 'string'; break; case 'date': case 'datetime': case 'time': case 'year': case 'timestamp': case 'enum': $detectedType = 'string'; break; case 'tinyint': case 'smallint': $detectedType = 'bool'; break; case 'float': case 'currency': case 'single': case 'double': $detectedType = 'float'; break; default: $detectedType = 'int'; break; } } // If all else fails assume it's an int and hope for the best if (empty($detectedType)) { $detectedType = 'int'; } return $detectedType; } /** * @param string $className */ public function setClassName(string $className): void { $this->className = $className; } /** * Return the raw hints array * * @return array * * @throws \FOF40\Model\DataModel\Relation\Exception\RelationNotFound */ public function getRawHints(): array { $model = $this->model; $hints = [ 'property' => [], 'method' => [], 'property-read' => [], ]; $hasFilters = $model->getBehavioursDispatcher()->hasObserverClass('FOF40\Model\DataModel\Behaviour\Filters'); $magicFields = [ 'enabled', 'ordering', 'created_on', 'created_by', 'modified_on', 'modified_by', 'locked_on', 'locked_by', ]; foreach ($model->getTableFields() as $fieldName => $fieldMeta) { $fieldType = static::getFieldType($fieldMeta->Type); if (!in_array($fieldName, $magicFields)) { $hints['property'][] = [$fieldType, '$' . $fieldName]; } if ($hasFilters) { $hints['method'][] = [ '$this', $fieldName . '()', $fieldName . '(' . $fieldType . ' $v)', ]; } } $relations = $model->getRelations()->getRelationNames(); $modelType = get_class($model); $modelTypeParts = explode('\\', $modelType); array_pop($modelTypeParts); $modelType = implode('\\', $modelTypeParts) . '\\'; if ($relations !== []) { foreach ($relations as $relationName) { $relationObject = $model->getRelations()->getRelation($relationName)->getForeignModel(); $relationType = get_class($relationObject); $relationType = str_replace($modelType, '', $relationType); $hints['property-read'][] = [ $relationType, '$' . $relationName, ]; } } return $hints; } /** * Returns the docblock with the magic field hints for the model class * * @return string */ public function getHints(): string { $modelName = $this->className; $text = "/**\n * Model $modelName\n *\n"; $hints = $this->getRawHints(); if (!empty($hints['property'])) { $text .= " * Fields:\n *\n"; $colWidth = 0; foreach ($hints['property'] as $hintLine) { $colWidth = max($colWidth, strlen($hintLine[0])); } $colWidth += 2; foreach ($hints['property'] as $hintLine) { $text .= " * @property " . str_pad($hintLine[0], $colWidth, ' ') . $hintLine[1] . "\n"; } $text .= " *\n"; } if (!empty($hints['method'])) { $text .= " * Filters:\n *\n"; $colWidth = 0; $col2Width = 0; foreach ($hints['method'] as $hintLine) { $colWidth = max($colWidth, strlen($hintLine[0])); $col2Width = max($col2Width, strlen($hintLine[1])); } $colWidth += 2; $col2Width += 2; foreach ($hints['method'] as $hintLine) { $text .= " * @method " . str_pad($hintLine[0], $colWidth, ' ') . str_pad($hintLine[1], $col2Width, ' ') . $hintLine[2] . "\n"; } $text .= " *\n"; } if (!empty($hints['property-read'])) { $text .= " * Relations:\n *\n"; $colWidth = 0; foreach ($hints['property-read'] as $hintLine) { $colWidth = max($colWidth, strlen($hintLine[0])); } $colWidth += 2; foreach ($hints['property-read'] as $hintLine) { $text .= " * @property " . str_pad($hintLine[0], $colWidth, ' ') . $hintLine[1] . "\n"; } $text .= " *\n"; } return $text . "**/\n"; } } TimezoneWrangler.php 0000604 00000023103 15245522752 0010555 0 ustar 00 <?php /** * @package FOF * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 2, or later */ namespace FOF30\Utils; defined('_JEXEC') || die; use DateTime; use DateTimeZone; use Exception; use FOF30\Container\Container; use FOF30\Date\Date; use Joomla\CMS\User\User; /** * A helper class to wrangle timezones, as used by Joomla!. * * @package FOF30\Utils * * @since 3.1.3 */ class TimezoneWrangler { /** * The default timestamp format string to use when one is not provided * * @var string */ protected $defaultFormat = 'Y-m-d H:i:s T'; /** * When set, this timezone will be used instead of the Joomla! applicable timezone for the user. * * @var DateTimeZone */ protected $forcedTimezone = null; /** * Cache of user IDs to applicable timezones * * @var array */ protected $userToTimezone = []; /** * The component container for which we are created * * @var Container */ protected $container; public function __construct(Container $container) { $this->container = $container; } /** * Get the default timestamp format to use when one is not provided * * @return string */ public function getDefaultFormat() { return $this->defaultFormat; } /** * Set the default timestamp format to use when one is not provided * * @param string $defaultFormat * * @return void */ public function setDefaultFormat($defaultFormat) { $this->defaultFormat = $defaultFormat; } /** * Returns the forced timezone which is used instead of the applicable Joomla! timezone. * * @return DateTimeZone */ public function getForcedTimezone() { return $this->forcedTimezone; } /** * Sets the forced timezone which is used instead of the applicable Joomla! timezone. If the new timezone is * different than the existing one we will also reset the user to timezone cache. * * @param DateTimeZone|string $forcedTimezone * * @return void */ public function setForcedTimezone($forcedTimezone) { // Are we unsetting the forced TZ? if (empty($forcedTimezone)) { $this->forcedTimezone = null; $this->resetCache(); return; } // If the new TZ is a string we have to create an object if (is_string($forcedTimezone)) { $forcedTimezone = new DateTimeZone($forcedTimezone); } $oldTZ = ''; if (is_object($this->forcedTimezone) && ($this->forcedTimezone instanceof DateTimeZone)) { $oldTZ = $this->forcedTimezone->getName(); } if ($oldTZ == $forcedTimezone->getName()) { return; } $this->forcedTimezone = $forcedTimezone; $this->resetCache(); } /** * Reset the user to timezone cache. This is done automatically every time you change the forced timezone. */ public function resetCache() { $this->userToTimezone = []; } /** * Get the applicable timezone for a user. If the user is not a guest and they have a timezone set up in their * profile it will be used. Otherwise we fall back to the Server Timezone as set up in Global Configuration. If that * fails, we use GMT. However, if you have used a non-blank forced timezone that will be used instead, circumventing * this calculation. Therefore the returned timezone is one of the following, by descending order of priority: * - Forced timezone * - User's timezone (explicitly set in their user profile) * - Server Timezone (from Joomla's Global Configuration) * - GMT * * @param User|null $user * * @return DateTimeZone */ public function getApplicableTimezone($user = null) { // If we have a forced timezone use it instead of trying to figure anything out. if (is_object($this->forcedTimezone)) { return $this->forcedTimezone; } // No user? Get the current user. if (is_null($user)) { $user = $this->container->platform->getUser(); } // If there is a cached timezone return that instead. if (isset($this->userToTimezone[$user->id])) { return $this->userToTimezone[$user->id]; } // Prefer the user timezone if it's set. if (!$user->guest) { $tz = $user->getParam('timezone', null); if (!empty($tz)) { try { $this->userToTimezone[$user->id] = new DateTimeZone($tz); return $this->userToTimezone[$user->id]; } catch (Exception $e) { } } } // Get the Server Timezone from Global Configuration with a fallback to GMT $tz = $this->container->platform->getConfig()->get('offset', 'GMT'); try { $this->userToTimezone[$user->id] = new DateTimeZone($tz); } catch (Exception $e) { // If an invalid timezone was set we get to use GMT $this->userToTimezone[$user->id] = new DateTimeZone('GMT'); } return $this->userToTimezone[$user->id]; } /** * Returns a FOF Date object with its timezone set to the user's applicable timezone. * * If no user is specified the current user will be used. * * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no * timezone is specified in a date string we assume it's GMT. * * @param User $user Applicable user for timezone calculation. Null = current user. * @param mixed $time Source time. Leave blank for current date/time. * * @return Date */ public function getLocalDateTime($user, $time = null) { $time = empty($time) ? 'now' : $time; $date = new Date($time); $tz = $this->getApplicableTimezone($user); $date->setTimezone($tz); return $date; } /** * Returns a FOF Date object with its timezone set to GMT. * * If no user is specified the current user will be used. * * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no * timezone is specified in a date string we assume it's the user's applicable timezone. * * @param User $user * @param mixed $time * * @return Date */ public function getGMTDateTime($user, $time) { $time = empty($time) ? 'now' : $time; $tz = $this->getApplicableTimezone($user); $date = new Date($time, $tz); $gmtTimezone = new DateTimeZone('GMT'); $date->setTimezone($gmtTimezone); return $date; } /** * Returns a formatted date string in the user's applicable timezone. * * If no format is specified we will use $defaultFormat. * * If no user is specified the current user will be used. * * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no * timezone is specified in a date string we assume it's GMT. * * $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). JApplicationCms does that * for you automatically. If you're under CLI, a custom JApplicationWeb etc you will probably have to load this * file * manually. * * @param string|null $format Timestamp format. If empty $defaultFormat is used. * @param User|null $user Applicable user for timezone calculation. Null = current * user. * @param DateTime|Date|string|int|null $time Source time. Leave blank for current date/time. * @param bool $translate Translate day of week and month names? * * @return string */ public function getLocalTimeStamp($format = null, $user = null, $time = null, $translate = false) { $date = $this->getLocalDateTime($user, $time); $format = empty($format) ? $this->defaultFormat : $format; return $date->format($format, true, $translate); } /** * Returns a formatted date string in the GMT timezone. * * If no format is specified we will use $defaultFormat. * * If no user is specified the current user will be used. * * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no * timezone is specified in a date string we assume it's the user's applicable timezone. * * $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). JApplicationCms does that * for you automatically. If you're under CLI, a custom JApplicationWeb etc you will probably have to load this * file * manually. * * @param string|null $format Timestamp format. If empty $defaultFormat is used. * @param User|null $user Applicable user for timezone calculation. Null = current * user. * @param DateTime|Date|string|int|null $time Source time. Leave blank for current date/time. * @param bool $translate Translate day of week and month names? * * @return string */ public function getGMTTimeStamp($format = null, $user = null, $time = null, $translate = false) { $date = $this->localToGMT($user, $time); $format = empty($format) ? $this->defaultFormat : $format; return $date->format($format, true, $translate); } /** * Convert a local time back to GMT. Returns a Date object with its timezone set to GMT. * * This is an alias to getGMTDateTime * * @param string|Date $time * @param User|null $user * * @return Date */ public function localToGMT($time, $user = null) { return $this->getGMTDateTime($user, $time); } /** * Convert a GMT time to local timezone. Returns a Date object with its timezone set to the applicable user's TZ. * * This is an alias to getLocalDateTime * * @param string|Date $time * @param User|null $user * * @return Date */ public function GMTToLocal($time, $user = null) { return $this->getLocalDateTime($user, $time); } } MediaVersion.php 0000604 00000011555 15245522752 0007656 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\Utils; defined('_JEXEC') || die; use Exception; use FOF40\Container\Container; use JDatabaseDriver; use Joomla\CMS\Factory; use Joomla\Registry\Registry; /** * Class MediaVersion * * @since 3.5.3 */ class MediaVersion { /** * Cached the version and date of FOF-powered components * * @var array * @since 3.5.3 */ protected static $componentVersionCache = []; /** * @var string|mixed */ public $componentName; /** * The current component's container * * @var Container * @since 3.5.3 */ protected $container; /** * The configured media query version * * @var string|null; * @since 3.5.3 */ protected $mediaVersion; /** * MediaVersion constructor. * * @param Container $c The component container * * @since 3.5.3 */ public function __construct(Container $c) { $this->container = $c; } /** * Get a component's version and date * * @param string $component * @param JDatabaseDriver $db * * @return array * @since 3.5.3 */ protected static function getComponentVersionAndDate(string $component, JDatabaseDriver $db): array { if (array_key_exists($component, self::$componentVersionCache)) { return self::$componentVersionCache[$component]; } $version = '0.0.0'; $date = date('Y-m-d H:i:s'); try { $query = $db->getQuery(true) ->select([ $db->qn('manifest_cache'), ])->from($db->qn('#__extensions')) ->where($db->qn('type') . ' = ' . $db->q('component')) ->where($db->qn('name') . ' = ' . $db->q($component)); $db->setQuery($query); $json = $db->loadResult() ?? '{}'; $params = new Registry($json); $version = $params->get('version', $version); $date = $params->get('creationDate', $date); } catch (Exception $e) { } self::$componentVersionCache[$component] = [$version, $date]; return self::$componentVersionCache[$component]; } /** * Serialization helper * * This is for the benefit of legacy components which might use Joomla's JS/CSS inclusion directly passing * $container->mediaVersion as the version argument. In FOF 3.5.2 and lower that was always string or null, making * it a safe bet. In FOF 3.5.3 and later it's an object. It's not converted to a string until Joomla builds its * template header. However, Joomla's cache system will try to serialize all CSS and JS definitions, including their * parameters of which version is one. Therefore, for those legacy applications, Joomla would be trying to serialize * the MediaVersion object which would try to serialize the container. That would cause an immediate failure since * we protect the Container from being serialized. * * Our Template service knows about this and stringifies the MediaVersion before passing it to Joomla. Legacy apps * may not do that. Using the __sleep and __wakeup methods in this class we make sure that we are essentially * storing nothing but strings in the serialized representation and we reconstruct the container upon * unseralization. That said, it's a good idea to use the Template service instead of $container->mediaVersion * directly or, at the very least, use (string) $container->mediaVersion when using the Template service is not a * viable option. * * @return string[] */ public function __sleep(): array { $this->componentName = $this->container->componentName; return [ 'mediaVersion', 'componentName', ]; } /** * Unserialization helper * * @return void * @see __sleep */ public function __wakeup(): void { if (isset($this->componentName)) { $this->container = Container::getInstance($this->componentName); } } /** * Returns the media query version string * * @return string * @since 3.5.3 */ public function __toString(): string { if (empty($this->mediaVersion)) { $this->mediaVersion = $this->getDefaultMediaVersion(); } return $this->mediaVersion; } /** * Sets the media query version string * * @param string|null $mediaVersion * * @since 3.5.3 */ public function setMediaVersion(?string $mediaVersion): void { $this->mediaVersion = $mediaVersion; } /** * Returns the default media query version string if none is already defined * * @return string * @since 3.5.3 */ protected function getDefaultMediaVersion(): string { // Initialise [$version, $date] = self::getComponentVersionAndDate($this->container->componentName, $this->container->db); // Get the site's secret try { $app = Factory::getApplication(); if (method_exists($app, 'get')) { $secret = $app->get('secret'); } } catch (Exception $e) { } // Generate the version string return md5($version . $date . $secret); } } ViewManifestMigration.php 0000604 00000010557 15245571257 0011551 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\Utils; use FOF40\Container\Container; use Joomla\CMS\Filesystem\File; use Joomla\Filesystem\Folder; class ViewManifestMigration { /** * Migrates Joomla 4 view XML manifests into their Joomla 3 locations * * @param Container $container The FOF 4 container of the component we'll be migrating. * * @return void */ public static function migrateJoomla4MenuXMLFiles(Container $container): void { self::migrateJoomla4MenuXMLFiles_real($container->frontEndPath, $container->backEndPath); } /** * Migrates Joomla 4 view XML manifests into their Joomla 3 locations * * @param string $frontendPath Component's frontend path * @param string $backendPath Component's backend path * * @return void * @noinspection PhpUnused */ public static function migrateJoomla4MenuXMLFiles_real($frontendPath, $backendPath): void { // This only applies to Joomla 3 if (version_compare(JVERSION, '3.999.999', 'gt')) { return; } // Map modern to legacy locations $maps = [ $frontendPath . '/tmpl' => $frontendPath . '/views', $frontendPath . '/ViewTemplates' => $frontendPath . '/views', $backendPath . '/tmpl' => $backendPath . '/views', $backendPath . '/ViewTemplates' => $backendPath . '/views', ]; foreach ($maps as $source => $dest) { try { self::migrateViewXMLManifests($source, $dest); } catch (\UnexpectedValueException $e) { // This means the source folder doesn't exist. No problem! } } } /** * Removes the legacy `views` paths from the front- and backend of the component on Joomla 4 and later versions. * * @param Container $container * * @return void */ public static function removeJoomla3LegacyViews(Container $container): void { self::removeJoomla3LegacyViews_real($container->frontEndPath, $container->backEndPath); } /** * Removes the legacy `views` paths from the front- and backend of the component on Joomla 4 and later versions. * * @param string $frontendPath Component's frontend path * @param string $backendPath Component's backend path * * @return void * @noinspection PhpUnused */ public static function removeJoomla3LegacyViews_real($frontendPath, $backendPath): void { // This only applies to Joomla 4 if (version_compare(JVERSION, '3.999.999', 'le')) { return; } $legacyLocations = [ $frontendPath . '/views', $backendPath . '/views', ]; foreach ($legacyLocations as $path) { if (!is_dir($path)) { continue; } Folder::delete($path); } } /** * Migrates view manifest XML files from the source to the dest folder. * * @param string $source Source folder to scan, i.e. the `tmpl` or `ViewTemplates` folder. * @param string $dest Target folder to copy the files to, i.e. the legacy `views` folder. */ private static function migrateViewXMLManifests(string $source, string $dest): void { $di = new \DirectoryIterator($source); /** @var \DirectoryIterator $folderItem */ foreach ($di as $folderItem) { if ($folderItem->isDot() || !$folderItem->isDir()) { continue; } // Delete the metadata.xml and tmpl/*.xml files in the corresponding `views` subfolder $killLegacyFile = $dest . '/' . $folderItem->getFilename() . '/metadata.xml'; $killLegacyFolder = $dest . '/' . $folderItem->getFilename() . '/tmpl'; if (!@is_file($killLegacyFile)) { File::delete($killLegacyFile); } if (@file_exists($killLegacyFolder) && @is_dir($killLegacyFolder)) { $files = Folder::files($killLegacyFolder, '\.xml$', false, true); if (!empty($files)) { File::delete($files); } } $filesIterator = new \DirectoryIterator($folderItem->getPathname()); /** @var \DirectoryIterator $fileItem */ foreach ($filesIterator as $fileItem) { if ($fileItem->isDir()) { continue; } if ($fileItem->getExtension() != 'xml') { continue; } $destPath = $dest . '/' . $folderItem->getFilename() . (($fileItem->getFilename() == 'metadata.xml') ? '' : '/tmpl'); $destPathName = $destPath . '/' . $fileItem->getFilename(); Folder::create($destPath); File::copy($fileItem->getPathname(), $destPathName); } } } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка