primo commit

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

View File

@ -0,0 +1,565 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use ArrayAccess;
use InvalidArgumentException;
use Traversable;
/**
* 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 mixed $default A default value (int|array) to assign if $array is not an array
*
* @return array
*
* @since 1.0
*/
public static function toInteger($array, $default = null)
{
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, $class = 'stdClass', $recursive = true)
{
$obj = new $class;
foreach ($array as $k => $v)
{
if ($recursive && is_array($v))
{
$obj->$k = static::toObject($v, $class);
}
else
{
$obj->$k = $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, $inner_glue = '=', $outer_glue = ' ', $keepOuterKey = false)
{
$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 $regex An optional regular expression to match on field names
*
* @return array
*
* @since 1.0
*/
public static function fromObject($p_obj, $recurse = true, $regex = null)
{
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 $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, $valueCol, $keyCol = null)
{
$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, $name, $default = null, $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('/-?[0-9]+/', $result, $matches);
$result = @(int) $matches[0];
break;
case 'FLOAT':
case 'DOUBLE':
// Only use the first floating point value
@preg_match('/-?[0-9]+(\.[0-9]+)?/', $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)
{
$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)
{
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 $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, $key = null)
{
$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_map('serialize', $array);
$array = array_unique($array);
$array = array_map('unserialize', $array);
return $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($needle, array $haystack, $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, $separator = '.', $prefix = '')
{
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, $recurse, $regex)
{
if (is_object($item))
{
$result = [];
foreach (get_object_vars($item) as $k => $v)
{
if (!$regex || preg_match($regex, $k))
{
if ($recurse)
{
$result[$k] = self::arrayFromObject($v, $recurse, $regex);
}
else
{
$result[$k] = $v;
}
}
}
return $result;
}
if (is_array($item))
{
$result = [];
foreach ($item as $k => $v)
{
$result[$k] = self::arrayFromObject($v, $recurse, $regex);
}
return $result;
}
return $item;
}
}

View File

@ -0,0 +1,311 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
/**
* Registers a fof:// stream wrapper
*/
class Buffer
{
/**
* Buffer hash
*
* @var array
*/
public static $buffers = [];
public static $canRegisterWrapper = null;
/**
* Stream position
*
* @var integer
*/
public $position = 0;
/**
* Buffer name
*
* @var string
*/
public $name = null;
/**
* Should I register the fof:// stream wrapper
*
* @return bool True if the stream wrapper can be registered
*/
public static function canRegisterWrapper()
{
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)
{
if (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($path, $mode, $options, &$opened_path)
{
$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($path)
{
$url = parse_url($path);
$name = $url['host'];
if (isset(static::$buffers[$name]))
{
unset (static::$buffers[$name]);
}
}
public function stream_stat()
{
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($count)
{
$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($data)
{
$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()
{
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()
{
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($offset, $whence)
{
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', 'FOF30\\Utils\\Buffer');
}

View File

@ -0,0 +1,215 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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\Application\CMSApplication;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Cache\Controller\CallbackController;
use Joomla\CMS\Cache\Exception\CacheExceptionInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
/**
* A utility class to help you quickly clean the Joomla! cache
*/
class CacheCleaner
{
/**
* Clears the com_modules and com_plugins cache. You need to call this whenever you alter the publish state or
* parameters of a module or plugin from your code.
*
* @return void
*/
public static function clearPluginsAndModulesCache()
{
self::clearPluginsCache();
self::clearModulesCache();
}
/**
* Clears the com_plugins cache. You need to call this whenever you alter the publish state or parameters of a
* plugin from your code.
*
* @return void
*/
public static function clearPluginsCache()
{
self::clearCacheGroups(['com_plugins'], [0, 1]);
}
/**
* Clears the com_modules cache. You need to call this whenever you alter the publish state or parameters of a
* module from your code.
*
* @return void
*/
public static function clearModulesCache()
{
self::clearCacheGroups(['com_modules'], [0, 1]);
}
/**
* Clears the specified cache groups.
*
* @param array $clearGroups Which cache groups to clear. Usually this is com_yourcomponent to clear
* your component's cache.
* @param array $cacheClients Which cache clients to clear. 0 is the back-end, 1 is the front-end. If you
* do not specify anything, both cache clients will be cleared.
* @param string|null $event An event to run upon trying to clear the cache. Empty string to disable. If
* NULL and the group is "com_content" I will trigger onContentCleanCache.
*
* @return void
* @throws Exception
*/
public static function clearCacheGroups(array $clearGroups, array $cacheClients = [
0, 1,
], ?string $event = null): void
{
// Early return on nonsensical input
if (empty($clearGroups) || empty($cacheClients))
{
return;
}
// Make sure I have a valid CMS application
try
{
$app = Factory::getApplication();
}
catch (Exception $e)
{
return;
}
$isJoomla4 = version_compare(JVERSION, '3.9999.9999', 'gt');
// Loop all groups to clean
foreach ($clearGroups as $group)
{
// Groups must be non-empty strings
if (empty($group) || !is_string($group))
{
continue;
}
// Loop all clients (applications)
foreach ($cacheClients as $client_id)
{
$client_id = (int) ($client_id ?? 0);
$options = $isJoomla4
? self::clearCacheGroupJoomla4($group, $client_id, $app)
: self::clearCacheGroupJoomla3($group, $client_id, $app);
// Do not call any events if I failed to clean the cache using the core Joomla API
if (!($options['result'] ?? false))
{
return;
}
/**
* If you're cleaning com_content and you have passed no event name I will use onContentCleanCache.
*/
if ($group === 'com_content')
{
$cacheCleaningEvent = $event ?: 'onContentCleanCache';
}
/**
* Call Joomla's cache cleaning plugin event (e.g. onContentCleanCache) as well.
*
* @see BaseDatabaseModel::cleanCache()
*/
if (empty($cacheCleaningEvent))
{
continue;
}
$app->triggerEvent($cacheCleaningEvent, $options);
}
}
}
/**
* Clean a cache group on Joomla 3
*
* @param string $group The cache to clean, e.g. com_content
* @param int $client_id The application ID for which the cache will be cleaned
* @param CMSApplication $app The current CMS application
*
* @return array Cache controller options, including cleaning result
* @throws Exception
*/
private static function clearCacheGroupJoomla3(string $group, int $client_id, CMSApplication $app): array
{
$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;
}
return $options;
}
/**
* Clean a cache group on Joomla 4
*
* @param string $group The cache to clean, e.g. com_content
* @param int $client_id The application ID for which the cache will be cleaned
* @param CMSApplication $app The current CMS application
*
* @return array Cache controller options, including cleaning result
* @throws Exception
*/
private static function clearCacheGroupJoomla4(string $group, int $client_id, CMSApplication $app): array
{
// Get the default cache folder. Start by using the JPATH_CACHE constant.
$cacheBaseDefault = JPATH_CACHE;
// -- If we are asked to clean cache on the other side of the application we need to find a new cache base
if ($client_id != $app->getClientId())
{
$cacheBaseDefault = (($client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/cache';
}
// Get the cache controller's options
$options = [
'defaultgroup' => $group,
'cachebase' => $app->get('cache_path', $cacheBaseDefault),
'result' => true,
];
try
{
/** @var CallbackController $cache */
$cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', $options);
$cache->clean();
}
catch (CacheExceptionInterface $exception)
{
$options['result'] = false;
}
return $options;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\Encrypt\Randval;
use JSessionHandlerInterface;
use RuntimeException;
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()
{
$phpfunc = new Phpfunc();
$rand = new Randval($phpfunc);
$this->id = md5($rand->generate(32));
}
}

View File

@ -0,0 +1,763 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.
*
* @param mixed $items
*
* @return static
*/
public static function make($items)
{
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()
{
return $this->items;
}
/**
* Collapse the collection items into a single array.
*
* @return static
*/
public function collapse()
{
$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 static
*/
public function diff($items)
{
return new static(array_diff($this->items, $this->getArrayableItems($items)));
}
/**
* Execute a callback over each item.
*
* @param Closure $callback
*
* @return static
*/
public function each(Closure $callback)
{
array_map($callback, $this->items);
return $this;
}
/**
* Fetch a nested element of the collection.
*
* @param string $key
*
* @return static
*/
public function fetch($key)
{
return new static(array_fetch($this->items, $key));
}
/**
* Run a filter over each of the items.
*
* @param Closure $callback
*
* @return static
*/
public function filter(Closure $callback)
{
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 array
*/
public function flatten()
{
return new static(array_flatten($this->items));
}
/**
* Remove an item from the collection by key.
*
* @param mixed $key
*
* @return void
*/
public function forget($key)
{
unset($this->items[$key]);
}
/**
* Get an item from the collection by key.
*
* @param mixed $key
* @param mixed $default
*
* @return mixed
*/
public function get($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 static
*/
public function groupBy($groupBy)
{
$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 mixed $key
*
* @return bool
*/
public function has($key)
{
return array_key_exists($key, $this->items);
}
/**
* Concatenate values of a given key as a string.
*
* @param string $value
* @param string $glue
*
* @return string
*/
public function implode($value, $glue = null)
{
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 static
*/
public function intersect($items)
{
return new static(array_intersect($this->items, $this->getArrayableItems($items)));
}
/**
* Determine if the collection is empty or not.
*
* @return bool
*/
public function isEmpty()
{
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 $key
*
* @return array
*/
public function lists($value, $key = null)
{
return array_pluck($this->items, $value, $key);
}
/**
* Run a map over each of the items.
*
* @param Closure $callback
*
* @return static
*/
public function map(Closure $callback)
{
return new static(array_map($callback, $this->items, array_keys($this->items)));
}
/**
* Merge the collection with the given items.
*
* @param Collection|array $items
*
* @return static
*/
public function merge($items)
{
return new static(array_merge($this->items, $this->getArrayableItems($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)
{
array_unshift($this->items, $value);
}
/**
* Push an item onto the end of the collection.
*
* @param mixed $value
*
* @return void
*/
public function push($value)
{
$this->items[] = $value;
}
/**
* Put an item in the collection by key.
*
* @param mixed $key
* @param mixed $value
*
* @return void
*/
public function put($key, $value)
{
$this->items[$key] = $value;
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
*
* @return mixed
*/
public function reduce($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($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 static
*/
public function reverse()
{
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 $length
* @param bool $preserveKeys
*
* @return static
*/
public function slice($offset, $length = null, $preserveKeys = false)
{
return new static(array_slice($this->items, $offset, $length, $preserveKeys));
}
/**
* Sort through each item with a callback.
*
* @param Closure $callback
*
* @return static
*/
public function sort(Closure $callback)
{
uasort($this->items, $callback);
return $this;
}
/**
* Sort the collection using the given Closure.
*
* @param Closure|string $callback
* @param int $options
* @param bool $descending
*
* @return static
*/
public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
{
$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 static
*/
public function sortByDesc($callback, $options = SORT_REGULAR)
{
return $this->sortBy($callback, $options, true);
}
/**
* Splice portion of the underlying collection array.
*
* @param int $offset
* @param int $length
* @param mixed $replacement
*
* @return static
*/
public function splice($offset, $length = 0, $replacement = [])
{
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 $limit
*
* @return static
*/
public function take($limit = null)
{
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()
{
$this->items = [];
return $this;
}
/**
* Transform each item in the collection using a callback.
*
* @param callable $callback
*
* @return static
*/
public function transform($callback)
{
$this->items = array_map($callback, $this->items);
return $this;
}
/**
* Return only unique items from the collection array.
*
* @return static
*/
public function unique()
{
return new static(array_unique($this->items));
}
/**
* Reset the keys on the underlying array.
*
* @return static
*/
public function values()
{
$this->items = array_values($this->items);
return $this;
}
/**
* Get the collection of items as a plain array.
*
* @return array
*/
public function toArray()
{
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
*/
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Get the collection of items as JSON.
*
* @param int $options
*
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->toArray(), $options);
}
/**
* Get an iterator for the items.
*
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->items);
}
/**
* Get a CachingIterator instance.
*
* @param integer $flags Caching iterator flags
*
* @return CachingIterator
*/
public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING)
{
return new CachingIterator($this->getIterator(), $flags);
}
/**
* Count the number of items in the collection.
*
* @return int
*/
public function count()
{
return count($this->items);
}
/**
* Determine if an item exists at an offset.
*
* @param mixed $key
*
* @return bool
*/
public function offsetExists($key)
{
return array_key_exists($key, $this->items);
}
/**
* Get an item at a given offset.
*
* @param mixed $key
*
* @return mixed
*/
public function offsetGet($key)
{
return $this->items[$key];
}
/**
* Set the item at a given offset.
*
* @param mixed $key
* @param mixed $value
*
* @return void
*/
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
*/
public function offsetUnset($key)
{
unset($this->items[$key]);
}
/**
* Convert the collection to its string representation.
*
* @return string
*/
public function __toString()
{
return $this->toJson();
}
/**
* Get a value retrieving callback.
*
* @param string $value
*
* @return Closure
*/
protected function valueRetriever($value)
{
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 getArrayableItems($items)
{
if ($items instanceof Collection)
{
$items = $items->all();
}
elseif (is_object($items) && method_exists($items, 'toArray'))
{
$items = $items->toArray();
}
return $items;
}
}

View File

@ -0,0 +1,158 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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]);
}
}

View File

@ -0,0 +1,199 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,915 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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' => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
'value_field' => $modelTitleField,
'fof.autosubmit' => true,
], $params);
return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
}
/**
* Display a text filter (search box)
*
* @param string $localField The name of the model field. Used when getting the filter state.
* @param string $searchField The INPUT element's name. Default: "filter_$localField".
* @param string $placeholder The 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' => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
'fof.autosubmit' => true,
], $params);
return self::genericSelect($localField, $options, $model->getState($localField), $params);
}
/**
* View access dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function accessFilter($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'] = '&mdash; ' . $placeholder . ' &mdash;';
}
}
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.");
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,831 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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>&ensp;';
$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>&ensp;';
$html[] = '</a>';
}
return implode($html);
}
/**
* Returns a state change button on the browse view's table
*
* @param array $states array of value/state. Each state is an array of the form
* (task, text, active title, inactive title, tip (boolean), HTML active class,
* HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active
* title,
* 'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active
* class,
* 'inactive_class'=>html inactive class)
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function state($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;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,887 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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 a searchable HTML selection list (Chosen on J3, Choices.js on J4).
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see JHtml::$formatOptions}.
* Selection options, see {@see JHtmlSelect::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
* @since 3.7.2
*/
public static function smartlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false, $translate = false)
{
$innerList = self::genericlist($data, $name, $attribs, $optKey, $optText, $selected, $idtag, $translate);
// Joomla 3: Use Chosen
if (version_compare(JVERSION, '3.999.999', 'le'))
{
HTMLHelper::_('formbehavior.chosen');
return $innerList;
}
// Joomla 4: Use the joomla-field-fancy-select using choices.js
try
{
\Joomla\CMS\Factory::getApplication()->getDocument()->getWebAssetManager()
->usePreset('choicesjs')
->useScript('webcomponent.field-fancy-select');
}
catch (Exception $e)
{
return $innerList;
}
$j4Attr = array_filter([
'class' => $attribs['class'] ?? null,
'placeholder' => $attribs['placeholder'] ?? null,
], function ($x) {
return !empty($x);
});
$dataAttribute = '';
if (isset($attribs['dataAttribute']))
{
$dataAttribute = is_string($attribs['dataAttribute']) ? $attribs['dataAttribute'] : '';
}
if ((bool) ($attribs['allowCustom'] ?? false))
{
$dataAttribute .= ' allow-custom new-item-prefix="#new#"';
}
$remoteSearchUrl = $attribs['remoteSearchURL'] ?? null;
$remoteSearch = ((bool) ($attribs['remoteSearch'] ?? false)) && !empty($remoteSearchUrl);
$termKey = $attribs['termKey'] ?? 'like';
$minTermLength = $attribs['minTermLength'] ?? 3;
if ($remoteSearch)
{
$dataAttribute .= ' remote-search';
$j4Attr['url'] = $remoteSearchUrl;
$j4Attr['term-key'] = $termKey;
$j4Attr['min-term-length'] = $minTermLength;
}
if (isset($attribs['required']))
{
$j4Attr['class'] = ($j4Attr['class'] ?? '') . ' required';
$dataAttribute .= ' required';
}
if (isset($attribs['readonly']))
{
return $innerList;
}
return sprintf("<joomla-field-fancy-select %s %s>%s</joomla-field-fancy-select>", ArrayHelper::toString($j4Attr), $dataAttribute, $innerList);
}
/**
* Generates an HTML selection list.
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see 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
* "&lt;optgroup>" and synthesizes groups from them. Deprecated. Defaults
* true for backwards compatibility.
* -list.select: either the value of one selected option or an array
* of selected options. Default: none.
* -list.translate: Boolean. If set, text and labels are translated via
* 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;
}
}

View File

@ -0,0 +1,285 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\Timer\Timer;
use Joomla\CMS\Factory;
/**
* 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 = null;
/** @var string Current component release date */
protected $date = null;
/** @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 $option 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($option, $version, $date)
{
// Initialise from parameters
$this->option = $option;
$this->version = $version;
$this->date = $date;
// Retrieve the date and version from the #__extensions table
$db = Factory::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()
{
return $this->wrongComponentVersion;
}
/**
* Is the fileslist.php reporting a version different than the reported component version?
*
* @return boolean
*/
public function isWrongFilesVersion()
{
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()
{
// 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($idx = 0)
{
$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] . ')';
}
else
{
if (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;
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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
{
}

View File

@ -0,0 +1,758 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,231 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,238 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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;
}
}

View File

@ -0,0 +1,628 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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;
}
}

View File

@ -0,0 +1,215 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use Exception;
use FOF30\Container\Container;
use JDatabaseDriver;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;
/**
* Class MediaVersion
* @package FOF30\Utils
*
* @since 3.5.3
*/
class MediaVersion
{
/**
* Cached the version and date of FOF-powered components
*
* @var array
* @since 3.5.3
*/
protected static $componentVersionCache = [];
/**
* 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($component, $db)
{
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();
if (class_exists('JRegistry'))
{
$params = new Registry($json);
}
else
{
$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()
{
$this->componentName = $this->container->componentName;
return [
'mediaVersion',
'componentName',
];
}
/**
* Unserialization helper
*
* @return void
* @see __sleep
*/
public function __wakeup()
{
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()
{
if (empty($this->mediaVersion))
{
$this->mediaVersion = $this->getDefaultMediaVersion();
}
return $this->mediaVersion;
}
/**
* Sets the media query version string
*
* @param mixed $mediaVersion
*
* @since 3.5.3
*/
public function setMediaVersion($mediaVersion)
{
$this->mediaVersion = $mediaVersion;
}
/**
* Returns the default media query version string if none is already defined
*
* @return string
* @since 3.5.3
*/
protected function getDefaultMediaVersion()
{
// 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);
}
}

View File

@ -0,0 +1,315 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Relation\Exception\RelationNotFound;
/**
* Generate phpDoc type hints for the magic fields of your DataModels
*
* @package FOF30\Utils
*/
class ModelTypeHints
{
/**
* The model for which to create type hints
*
* @var DataModel
*/
protected $model = null;
/**
* Name of the class. If empty will be inferred from the current object
*
* @var string
*/
protected $className = null;
/**
* Public constructor
*
* @param 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($type)
{
// Remove parentheses, indicating field options / size (they don't matter in type detection)
if (!empty($type))
{
[$type, ] = explode('(', $type);
}
$detectedType = null;
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'character varying':
case 'nvarchar':
case 'nchar':
$detectedType = '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($className)
{
$this->className = $className;
}
/**
* Return the raw hints array
*
* @return array
*
* @throws RelationNotFound
*/
public function getRawHints()
{
$model = $this->model;
$hints = [
'property' => [],
'method' => [],
'property-read' => [],
];
$hasFilters = $model->getBehavioursDispatcher()->hasObserverClass('FOF30\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 = self::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()
{
$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";
}
$text .= "**/\n";
return $text;
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,440 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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;
}
}

View File

@ -0,0 +1,351 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 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);
}
}

View File

@ -0,0 +1,585 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
defined('_JEXEC') || die;
/**
* 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
*/
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, $key, $value)
{
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, Closure $callback)
{
$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)
{
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, $prepend = '')
{
$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, $keys)
{
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, $key)
{
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, $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, $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)
{
$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, $key)
{
$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, $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, $keys)
{
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, $value, $key = null)
{
$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, $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, $key, $value)
{
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, Closure $callback)
{
return FOF30\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, Closure $callback)
{
$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($haystack, $needles)
{
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)
{
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, $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($pattern, &$replacements, $subject)
{
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($haystack, $needles)
{
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;
}
}