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,547 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
/**
* ArrayHelper is an array utility class for doing all sorts of odds and ends with arrays.
*
* Copied from Joomla Framework to avoid class name issues between Joomla! versions 3 and 4. sortObjects is not included
* because it needs the UTF-8 package. If you need to use that then you should be using the Joomla! Framework's helper
* anyway.
*/
final class ArrayHelper
{
/**
* Private constructor to prevent instantiation of this class
*
* @since 1.0
*/
private function __construct()
{
}
/**
* Function to convert array to integer values
*
* @param array $array The source array to convert
* @param int|array|null $default A default value (int|array) to assign if $array is not an array
*
* @return int[]
*
* @since 1.0
*/
public static function toInteger(array $array, $default = null): array
{
if (is_array($array))
{
return array_map('intval', $array);
}
if ($default === null)
{
return [];
}
if (is_array($default))
{
return static::toInteger($default, null);
}
return [(int) $default];
}
/**
* Utility function to map an array to a stdClass object.
*
* @param array $array The array to map.
* @param string $class Name of the class to create
* @param boolean $recursive Convert also any array inside the main array
*
* @return object
*
* @since 1.0
*/
public static function toObject(array $array, string $class = 'stdClass', bool $recursive = true)
{
$obj = new $class;
foreach ($array as $k => $v)
{
$obj->$k = ($recursive && is_array($v)) ? static::toObject($v, $class) : $v;
}
return $obj;
}
/**
* Utility function to map an array to a string.
*
* @param array $array The array to map.
* @param string $inner_glue The glue (optional, defaults to '=') between the key and the value.
* @param string $outer_glue The glue (optional, defaults to ' ') between array elements.
* @param boolean $keepOuterKey True if final key should be kept.
*
* @return string
*
* @since 1.0
*/
public static function toString(array $array, string $inner_glue = '=', string $outer_glue = ' ', bool $keepOuterKey = false): string
{
$output = [];
foreach ($array as $key => $item)
{
if (is_array($item))
{
if ($keepOuterKey)
{
$output[] = $key;
}
// This is value is an array, go and do it again!
$output[] = static::toString($item, $inner_glue, $outer_glue, $keepOuterKey);
}
else
{
$output[] = $key . $inner_glue . '"' . $item . '"';
}
}
return implode($outer_glue, $output);
}
/**
* Utility function to map an object to an array
*
* @param object $p_obj The source object
* @param boolean $recurse True to recurse through multi-level objects
* @param string|null $regex An optional regular expression to match on field names
*
* @return array
*
* @since 1.0
*/
public static function fromObject($p_obj, bool $recurse = true, ?string $regex = null): array
{
if (is_object($p_obj) || is_array($p_obj))
{
return self::arrayFromObject($p_obj, $recurse, $regex);
}
return [];
}
/**
* Extracts a column from an array of arrays or objects
*
* @param array $array The source array
* @param string $valueCol The index of the column or name of object property to be used as value
* It may also be NULL to return complete arrays or objects (this is
* useful together with <var>$keyCol</var> to reindex the array).
* @param string|null $keyCol The index of the column or name of object property to be used as key
*
* @return array Column of values from the source array
*
* @since 1.0
* @see http://php.net/manual/en/language.types.array.php
* @see http://php.net/manual/en/function.array-column.php
*/
public static function getColumn(array $array, string $valueCol, ?string $keyCol = null): array
{
$result = [];
foreach ($array as $item)
{
// Convert object to array
$subject = is_object($item) ? static::fromObject($item) : $item;
/*
* We process arrays (and objects already converted to array)
* Only if the value column (if required) exists in this item
*/
if (is_array($subject) && (!isset($valueCol) || isset($subject[$valueCol])))
{
// Use whole $item if valueCol is null, else use the value column.
$value = isset($valueCol) ? $subject[$valueCol] : $item;
// Array keys can only be integer or string. Casting will occur as per the PHP Manual.
if (isset($keyCol) && isset($subject[$keyCol]) && is_scalar($subject[$keyCol]))
{
$key = $subject[$keyCol];
$result[$key] = $value;
}
else
{
$result[] = $value;
}
}
}
return $result;
}
/**
* Utility function to return a value from a named array or a specified default
*
* @param array|\ArrayAccess $array A named array or object that implements ArrayAccess
* @param string $name The key to search for
* @param mixed $default The default value to give if no key found
* @param string $type Return type for the variable (INT, FLOAT, STRING, WORD, BOOLEAN, ARRAY)
*
* @return mixed
*
* @throws \InvalidArgumentException
* @since 1.0
*/
public static function getValue(array $array, string $name, $default = null, string $type = '')
{
if (!is_array($array) && !($array instanceof \ArrayAccess))
{
throw new \InvalidArgumentException('The object must be an array or an object that implements ArrayAccess');
}
$result = null;
if (isset($array[$name]))
{
$result = $array[$name];
}
// Handle the default case
if (is_null($result))
{
$result = $default;
}
// Handle the type constraint
switch (strtoupper($type))
{
case 'INT':
case 'INTEGER':
// Only use the first integer value
@preg_match('/-?\d+/', $result, $matches);
$result = @(int) $matches[0];
break;
case 'FLOAT':
case 'DOUBLE':
// Only use the first floating point value
@preg_match('/-?\d+(\.\d+)?/', $result, $matches);
$result = @(float) $matches[0];
break;
case 'BOOL':
case 'BOOLEAN':
$result = (bool) $result;
break;
case 'ARRAY':
if (!is_array($result))
{
$result = [$result];
}
break;
case 'STRING':
$result = (string) $result;
break;
case 'WORD':
$result = (string) preg_replace('#\W#', '', $result);
break;
case 'NONE':
default:
// No casting necessary
break;
}
return $result;
}
/**
* Takes an associative array of arrays and inverts the array keys to values using the array values as keys.
*
* Example:
* $input = array(
* 'New' => array('1000', '1500', '1750'),
* 'Used' => array('3000', '4000', '5000', '6000')
* );
* $output = ArrayHelper::invert($input);
*
* Output would be equal to:
* $output = array(
* '1000' => 'New',
* '1500' => 'New',
* '1750' => 'New',
* '3000' => 'Used',
* '4000' => 'Used',
* '5000' => 'Used',
* '6000' => 'Used'
* );
*
* @param array $array The source array.
*
* @return array
*
* @since 1.0
*/
public static function invert(array $array): array
{
$return = [];
foreach ($array as $base => $values)
{
if (!is_array($values))
{
continue;
}
foreach ($values as $key)
{
// If the key isn't scalar then ignore it.
if (is_scalar($key))
{
$return[$key] = $base;
}
}
}
return $return;
}
/**
* Method to determine if an array is an associative array.
*
* @param array $array An array to test.
*
* @return boolean
*
* @since 1.0
*/
public static function isAssociative(array $array): bool
{
if (is_array($array))
{
foreach (array_keys($array) as $k => $v)
{
if ($k !== $v)
{
return true;
}
}
}
return false;
}
/**
* Pivots an array to create a reverse lookup of an array of scalars, arrays or objects.
*
* @param array $source The source array.
* @param string|null $key Where the elements of the source array are objects or arrays, the key to pivot
* on.
*
* @return array An array of arrays pivoted either on the value of the keys, or an individual key of an object or
* array.
*
* @since 1.0
*/
public static function pivot(array $source, ?string $key = null): array
{
$result = [];
$counter = [];
foreach ($source as $index => $value)
{
// Determine the name of the pivot key, and its value.
if (is_array($value))
{
// If the key does not exist, ignore it.
if (!isset($value[$key]))
{
continue;
}
$resultKey = $value[$key];
$resultValue = $source[$index];
}
elseif (is_object($value))
{
// If the key does not exist, ignore it.
if (!isset($value->$key))
{
continue;
}
$resultKey = $value->$key;
$resultValue = $source[$index];
}
else
{
// Just a scalar value.
$resultKey = $value;
$resultValue = $index;
}
// The counter tracks how many times a key has been used.
if (empty($counter[$resultKey]))
{
// The first time around we just assign the value to the key.
$result[$resultKey] = $resultValue;
$counter[$resultKey] = 1;
}
elseif ($counter[$resultKey] == 1)
{
// If there is a second time, we convert the value into an array.
$result[$resultKey] = [
$result[$resultKey],
$resultValue,
];
$counter[$resultKey]++;
}
else
{
// After the second time, no need to track any more. Just append to the existing array.
$result[$resultKey][] = $resultValue;
}
}
unset($counter);
return $result;
}
/**
* Multidimensional-array-safe unique test
*
* @param array $array The array to make unique.
*
* @return array
*
* @see http://php.net/manual/en/function.array-unique.php
* @since 1.0
*/
public static function arrayUnique(array $array): array
{
$array = array_map('serialize', $array);
$array = array_unique($array);
return array_map('unserialize', $array);
}
/**
* An improved array_search that allows for partial matching of strings values in associative arrays.
*
* @param string $needle The text to search for within the array.
* @param array $haystack Associative array to search in to find $needle.
* @param boolean $caseSensitive True to search case sensitive, false otherwise.
*
* @return mixed Returns the matching array $key if found, otherwise false.
*
* @since 1.0
*/
public static function arraySearch(string $needle, array $haystack, bool $caseSensitive = true)
{
foreach ($haystack as $key => $value)
{
$searchFunc = ($caseSensitive) ? 'strpos' : 'stripos';
if ($searchFunc($value, $needle) === 0)
{
return $key;
}
}
return false;
}
/**
* Method to recursively convert data to a one dimension array.
*
* @param array|object $array The array or object to convert.
* @param string $separator The key separator.
* @param string $prefix Last level key prefix.
*
* @return array
*
* @since 1.3.0
*/
public static function flatten($array, string $separator = '.', string $prefix = ''): array
{
if ($array instanceof \Traversable)
{
$array = iterator_to_array($array);
}
elseif (is_object($array))
{
$array = get_object_vars($array);
}
foreach ($array as $k => $v)
{
$key = $prefix ? ($prefix . $separator . $k) : $k;
if (is_object($v) || is_array($v))
{
$array = array_merge($array, static::flatten($v, $separator, $key));
}
else
{
$array[$key] = $v;
}
}
return $array;
}
/**
* Utility function to map an object or array to an array
*
* @param mixed $item The source object or array
* @param boolean $recurse True to recurse through multi-level objects
* @param string $regex An optional regular expression to match on field names
*
* @return array
*
* @since 1.0
*/
private static function arrayFromObject($item, bool $recurse, ?string $regex): array
{
if (is_object($item))
{
$result = [];
foreach (get_object_vars($item) as $k => $v)
{
if (!$regex || preg_match($regex, $k))
{
$result[$k] = $recurse ? self::arrayFromObject($v, $recurse, $regex) : $v;
}
}
return $result;
}
if (is_array($item))
{
$result = [];
foreach ($item as $k => $v)
{
$result[$k] = self::arrayFromObject($v, $recurse, $regex);
}
return $result;
}
return $item;
}
}

View File

@ -0,0 +1,309 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
/**
* Registers a fof:// stream wrapper
*/
class Buffer
{
/**
* Buffer hash
*
* @var array
*/
public static $buffers = [];
public static $canRegisterWrapper;
/**
* Stream position
*
* @var integer
*/
public $position = 0;
/**
* Buffer name
*
* @var string
*/
public $name;
/**
* Should I register the fof:// stream wrapper
*
* @return bool True if the stream wrapper can be registered
*/
public static function canRegisterWrapper(): bool
{
if (is_null(static::$canRegisterWrapper))
{
static::$canRegisterWrapper = false;
// Maybe the host has disabled registering stream wrappers altogether?
if (!function_exists('stream_wrapper_register'))
{
return false;
}
// Check for Suhosin
if (function_exists('extension_loaded'))
{
$hasSuhosin = extension_loaded('suhosin');
}
else
{
$hasSuhosin = -1; // Can't detect
}
if ($hasSuhosin !== true)
{
$hasSuhosin = defined('SUHOSIN_PATCH') ? true : -1;
}
if (($hasSuhosin === -1) && function_exists('ini_get'))
{
$hasSuhosin = false;
$maxIdLength = ini_get('suhosin.session.max_id_length');
if ($maxIdLength !== false)
{
$hasSuhosin = ini_get('suhosin.session.max_id_length') !== '';
}
}
// If we can't detect whether Suhosin is installed we won't proceed to prevent a White Screen of Death
if ($hasSuhosin === -1)
{
return false;
}
// If Suhosin is installed but ini_get is not available we won't proceed to prevent a WSoD
if ($hasSuhosin && !function_exists('ini_get'))
{
return false;
}
// If Suhosin is installed check if fof:// is whitelisted
if ($hasSuhosin)
{
$whiteList = ini_get('suhosin.executor.include.whitelist');
// Nothing in the whitelist? I can't go on, sorry.
if (empty($whiteList))
{
return false;
}
$whiteList = explode(',', $whiteList);
$whiteList = array_map(function ($x) {
return trim($x);
}, $whiteList);
if (!in_array('fof://', $whiteList))
{
return false;
}
}
static::$canRegisterWrapper = true;
}
return static::$canRegisterWrapper;
}
/**
* Function to open file or url
*
* @param string $path The URL that was passed
* @param string $mode Mode used to open the file @see fopen
* @param integer $options Flags used by the API, may be STREAM_USE_PATH and
* STREAM_REPORT_ERRORS
* @param string &$opened_path Full path of the resource. Used with STREAM_USE_PATH option
*
* @return boolean
*
* @see streamWrapper::stream_open
*/
public function stream_open(string $path, string $mode, ?int $options, ?string &$opened_path): bool
{
$url = parse_url($path);
$this->name = $url['host'] . $url['path'];
$this->position = 0;
if (!isset(static::$buffers[$this->name]))
{
static::$buffers[$this->name] = null;
}
return true;
}
public function unlink(string $path): void
{
$url = parse_url($path);
$name = $url['host'];
if (isset(static::$buffers[$name]))
{
unset (static::$buffers[$name]);
}
}
public function stream_stat(): array
{
return [
'dev' => 0,
'ino' => 0,
'mode' => 0644,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => strlen(static::$buffers[$this->name]),
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => -1,
'blocks' => -1,
];
}
/**
* Read stream
*
* @param integer $count How many bytes of data from the current position should be returned.
*
* @return mixed The data from the stream up to the specified number of bytes (all data if
* the total number of bytes in the stream is less than $count. Null if
* the stream is empty.
*
* @see streamWrapper::stream_read
* @since 11.1
*/
public function stream_read(int $count): ?string
{
$ret = substr(static::$buffers[$this->name], $this->position, $count);
$this->position += strlen($ret);
return $ret;
}
/**
* Write stream
*
* @param string $data The data to write to the stream.
*
* @return integer
*
* @see streamWrapper::stream_write
* @since 11.1
*/
public function stream_write(string $data): int
{
$left = substr(static::$buffers[$this->name], 0, $this->position);
$right = substr(static::$buffers[$this->name], $this->position + strlen($data));
static::$buffers[$this->name] = $left . $data . $right;
$this->position += strlen($data);
return strlen($data);
}
/**
* Function to get the current position of the stream
*
* @return integer
*
* @see streamWrapper::stream_tell
* @since 11.1
*/
public function stream_tell(): int
{
return $this->position;
}
/**
* Function to test for end of file pointer
*
* @return boolean True if the pointer is at the end of the stream
*
* @see streamWrapper::stream_eof
* @since 11.1
*/
public function stream_eof(): bool
{
return $this->position >= strlen(static::$buffers[$this->name]);
}
/**
* The read write position updates in response to $offset and $whence
*
* @param integer $offset The offset in bytes
* @param integer $whence Position the offset is added to
* Options are SEEK_SET, SEEK_CUR, and SEEK_END
*
* @return boolean True if updated
*
* @see streamWrapper::stream_seek
* @since 11.1
*/
public function stream_seek(int $offset, int $whence): bool
{
switch ($whence)
{
case SEEK_SET:
if ($offset < strlen(static::$buffers[$this->name]) && $offset >= 0)
{
$this->position = $offset;
return true;
}
else
{
return false;
}
break;
case SEEK_CUR:
if ($offset >= 0)
{
$this->position += $offset;
return true;
}
else
{
return false;
}
break;
case SEEK_END:
if (strlen(static::$buffers[$this->name]) + $offset >= 0)
{
$this->position = strlen(static::$buffers[$this->name]) + $offset;
return true;
}
else
{
return false;
}
break;
default:
return false;
}
}
}
if (Buffer::canRegisterWrapper())
{
stream_wrapper_register('fof', 'FOF40\\Utils\\Buffer');
}

View File

@ -0,0 +1,166 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
use FOF40\Encrypt\Randval;
use JSessionHandlerInterface;
use RuntimeException;
/**
* CLI session handler for Joomla 3.x
*/
class CliSessionHandler implements JSessionHandlerInterface
{
private $id;
private $name = 'clisession';
public function __construct()
{
$this->makeId();
}
/**
* Starts the session.
*
* @return boolean True if started.
*
* @throws RuntimeException If something goes wrong starting the session.
* @since 3.4.8
*/
public function start()
{
return true;
}
/**
* Checks if the session is started.
*
* @return boolean True if started, false otherwise.
*
* @since 3.4.8
*/
public function isStarted()
{
return true;
}
/**
* Returns the session ID
*
* @return string The session ID
*
* @since 3.4.8
*/
public function getId()
{
return $this->id;
}
/**
* Sets the session ID
*
* @param string $id The session ID
*
* @return void
*
* @since 3.4.8
*/
public function setId($id)
{
$this->id = $id;
}
/**
* Returns the session name
*
* @return mixed The session name.
*
* @since 3.4.8
*/
public function getName()
{
return $this->name;
}
/**
* Sets the session name
*
* @param string $name The name of the session
*
* @return void
*
* @since 3.4.8
*/
public function setName($name)
{
$this->name = $name;
}
/**
* Regenerates ID that represents this storage.
*
* Note regenerate+destroy should not clear the session data in memory only delete the session data from persistent
* storage.
*
* @param boolean $destroy Destroy session when regenerating?
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value will leave the system
* settings unchanged,
* 0 sets the cookie to expire with browser session. Time is in seconds, and is not a
* Unix timestamp.
*
* @return boolean True if session regenerated, false if error
*
* @since 3.4.8
*/
public function regenerate($destroy = false, $lifetime = null)
{
$this->makeId();
return true;
}
/**
* Force the session to be saved and closed.
*
* This method must invoke session_write_close() unless this interface is used for a storage object design for unit
* or functional testing where a real PHP session would interfere with testing, in which case it should actually
* persist the session data if required.
*
* @return void
*
* @throws RuntimeException If the session is saved without being started, or if the session is already closed.
* @since 3.4.8
* @see session_write_close()
*/
public function save()
{
// No operation. This is a CLI session, we save nothing.
}
/**
* Clear all session data in memory.
*
* @return void
*
* @since 3.4.8
*/
public function clear()
{
$this->makeId();
}
private function makeId()
{
$rand = new Randval();
$this->id = md5($rand->generate(32));
}
}

View File

@ -0,0 +1,772 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
use ArrayAccess;
use ArrayIterator;
use CachingIterator;
use Closure;
use Countable;
use IteratorAggregate;
use JsonSerializable;
class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
{
/**
* The items contained in the collection.
*
* @var array
*/
protected $items = [];
/**
* Create a new collection.
*
* @param array $items
*/
public function __construct(array $items = [])
{
$this->items = $items;
}
/**
* Create a new collection instance if the value isn't one already. If given an array then the array is wrapped in
* a Collection. Otherwise we return a Collection with only one item, whatever was passed in $items. If you pass
* null you get an empty Collection.
*
* @param array|mixed $items
*
* @return static
*/
public static function make($items): Collection
{
if (is_null($items))
{
return new static;
}
if ($items instanceof Collection)
{
return $items;
}
return new static(is_array($items) ? $items : [$items]);
}
/**
* Get all of the items in the collection.
*
* @return array
*/
public function all(): array
{
return $this->items;
}
/**
* Collapse the collection items into a single array. This assumes that the Collection is composed of arrays. We
* are essentially merging all of these arrays and creating a new Collection out of them.
*
* If $this->items = [ ['a','b'], ['c','d'], ['e'] ] after collapse it becomes [ 'a','b','c','d','e' ]
*
* @return Collection
*/
public function collapse(): Collection
{
$results = [];
foreach ($this->items as $values)
{
$results = array_merge($results, $values);
}
return new static($results);
}
/**
* Diff the collection with the given items.
*
* @param Collection|array $items
*
* @return Collection
*/
public function diff($items): Collection
{
return new static(array_diff($this->items, $this->getIterableItemsAsArray($items)));
}
/**
* Execute a callback over each item.
*
* @param Closure $callback
*
* @return Collection
*/
public function each(Closure $callback): Collection
{
array_map($callback, $this->items);
return $this;
}
/**
* Fetch a nested element of the collection.
*
* @param string $key
*
* @return Collection
*/
public function fetch(string $key): Collection
{
return new static(array_fetch($this->items, $key));
}
/**
* Run a filter over each of the items.
*
* @param Closure $callback
*
* @return Collection
*/
public function filter(Closure $callback): Collection
{
return new static(array_filter($this->items, $callback));
}
/**
* Get the first item from the collection.
*
* @param \Closure $callback
* @param mixed $default
*
* @return mixed|null
*/
public function first(Closure $callback = null, $default = null)
{
if (is_null($callback))
{
return count($this->items) > 0 ? reset($this->items) : null;
}
else
{
return array_first($this->items, $callback, $default);
}
}
/**
* Get a flattened array of the items in the collection.
*
* @return Collection
*/
public function flatten(): Collection
{
return new static(array_flatten($this->items));
}
/**
* Remove an item from the collection by key.
*
* @param mixed $key
*
* @return void
*/
public function forget(string $key): void
{
unset($this->items[$key]);
}
/**
* Get an item from the collection by key.
*
* @param mixed $key
* @param mixed $default
*
* @return mixed
*/
public function get(string $key, $default = null)
{
if (array_key_exists($key, $this->items))
{
return $this->items[$key];
}
return value($default);
}
/**
* Group an associative array by a field or Closure value.
*
* @param callable|string $groupBy
*
* @return Collection
*/
public function groupBy($groupBy): Collection
{
$results = [];
foreach ($this->items as $key => $value)
{
$key = is_callable($groupBy) ? $groupBy($value, $key) : array_get($value, $groupBy);
$results[$key][] = $value;
}
return new static($results);
}
/**
* Determine if an item exists in the collection by key.
*
* @param string $key
*
* @return bool
*/
public function has(string $key): bool
{
return array_key_exists($key, $this->items);
}
/**
* Concatenate values of a given key as a string.
*
* @param string $value
* @param string|null $glue
*
* @return string
*/
public function implode(string $value, ?string $glue = null): string
{
if (is_null($glue))
{
return implode($this->lists($value));
}
return implode($glue, $this->lists($value));
}
/**
* Intersect the collection with the given items.
*
* @param Collection|array $items
*
* @return Collection
*/
public function intersect($items): Collection
{
return new static(array_intersect($this->items, $this->getIterableItemsAsArray($items)));
}
/**
* Determine if the collection is empty or not.
*
* @return bool
*/
public function isEmpty(): bool
{
return empty($this->items);
}
/**
* Get the last item from the collection.
*
* @return mixed|null
*/
public function last()
{
return count($this->items) > 0 ? end($this->items) : null;
}
/**
* Get an array with the values of a given key.
*
* @param string $value
* @param string|null $key
*
* @return array
*/
public function lists(string $value, ?string $key = null)
{
return array_pluck($this->items, $value, $key);
}
/**
* Run a map over each of the items.
*
* @param Closure $callback
*
* @return Collection
*/
public function map(Closure $callback): Collection
{
return new static(array_map($callback, $this->items, array_keys($this->items)));
}
/**
* Merge the collection with the given items.
*
* @param Collection|array $items
*
* @return Collection
*/
public function merge($items): Collection
{
return new static(array_merge($this->items, $this->getIterableItemsAsArray($items)));
}
/**
* Get and remove the last item from the collection.
*
* @return mixed|null
*/
public function pop()
{
return array_pop($this->items);
}
/**
* Push an item onto the beginning of the collection.
*
* @param mixed $value
*
* @return void
*/
public function prepend($value): void
{
array_unshift($this->items, $value);
}
/**
* Push an item onto the end of the collection.
*
* @param mixed $value
*
* @return void
*/
public function push($value): void
{
$this->items[] = $value;
}
/**
* Put an item in the collection by key.
*
* @param string $key
* @param mixed $value
*
* @return void
*/
public function put(string $key, $value): void
{
$this->items[$key] = $value;
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
*
* @return mixed
*/
public function reduce(callable $callback, $initial = null)
{
return array_reduce($this->items, $callback, $initial);
}
/**
* Get one or more items randomly from the collection.
*
* @param int $amount
*
* @return mixed
*/
public function random(int $amount = 1)
{
$keys = array_rand($this->items, $amount);
return is_array($keys) ? array_intersect_key($this->items, array_flip($keys)) : $this->items[$keys];
}
/**
* Reverse items order.
*
* @return Collection
*/
public function reverse(): Collection
{
return new static(array_reverse($this->items));
}
/**
* Get and remove the first item from the collection.
*
* @return mixed|null
*/
public function shift()
{
return array_shift($this->items);
}
/**
* Slice the underlying collection array.
*
* @param int $offset
* @param int|null $length
* @param bool $preserveKeys
*
* @return Collection
*/
public function slice(int $offset, ?int $length = null, bool $preserveKeys = false): Collection
{
return new static(array_slice($this->items, $offset, $length, $preserveKeys));
}
/**
* Sort through each item with a callback.
*
* @param Closure $callback
*
* @return Collection
*/
public function sort(Closure $callback): Collection
{
uasort($this->items, $callback);
return $this;
}
/**
* Sort the collection using the given Closure.
*
* @param \Closure|string $callback
* @param int $options
* @param bool $descending
*
* @return Collection
*/
public function sortBy($callback, int $options = SORT_REGULAR, bool $descending = false): Collection
{
$results = [];
if (is_string($callback))
{
$callback =
$this->valueRetriever($callback);
}
// First we will loop through the items and get the comparator from a callback
// function which we were given. Then, we will sort the returned values and
// and grab the corresponding values for the sorted keys from this array.
foreach ($this->items as $key => $value)
{
$results[$key] = $callback($value);
}
$descending ? arsort($results, $options)
: asort($results, $options);
// Once we have sorted all of the keys in the array, we will loop through them
// and grab the corresponding model so we can set the underlying items list
// to the sorted version. Then we'll just return the collection instance.
foreach (array_keys($results) as $key)
{
$results[$key] = $this->items[$key];
}
$this->items = $results;
return $this;
}
/**
* Sort the collection in descending order using the given Closure.
*
* @param \Closure|string $callback
* @param int $options
*
* @return Collection
*/
public function sortByDesc($callback, int $options = SORT_REGULAR): Collection
{
return $this->sortBy($callback, $options, true);
}
/**
* Splice portion of the underlying collection array.
*
* @param int $offset
* @param int $length
* @param mixed $replacement
*
* @return Collection
*/
public function splice(int $offset, int $length = 0, $replacement = []): Collection
{
return new static(array_splice($this->items, $offset, $length, $replacement));
}
/**
* Get the sum of the given values.
*
* @param \Closure|string $callback
*
* @return mixed
*/
public function sum($callback)
{
if (is_string($callback))
{
$callback = $this->valueRetriever($callback);
}
return $this->reduce(function ($result, $item) use ($callback) {
return $result += $callback($item);
}, 0);
}
/**
* Take the first or last {$limit} items.
*
* @param int|null $limit
*
* @return Collection
*/
public function take(?int $limit = null): Collection
{
if ($limit < 0)
{
return $this->slice($limit, abs($limit));
}
return $this->slice(0, $limit);
}
/**
* Resets the Collection (removes all items)
*
* @return Collection
*/
public function reset(): Collection
{
$this->items = [];
return $this;
}
/**
* Transform each item in the collection using a callback.
*
* @param callable $callback
*
* @return Collection
*/
public function transform(callable $callback): Collection
{
$this->items = array_map($callback, $this->items);
return $this;
}
/**
* Return only unique items from the collection array.
*
* @return Collection
*/
public function unique(): Collection
{
return new static(array_unique($this->items));
}
/**
* Reset the keys on the underlying array.
*
* @return Collection
*/
public function values(): Collection
{
$this->items = array_values($this->items);
return $this;
}
/**
* Get the collection of items as a plain array.
*
* @return array
*/
public function toArray(): array
{
return array_map(function ($value) {
return (is_object($value) && method_exists($value, 'toArray')) ? $value->toArray() : $value;
}, $this->items);
}
/**
* Convert the object into something JSON serializable.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* Get the collection of items as JSON.
*
* @param int $options
*
* @return string
*/
public function toJson(int $options = 0): string
{
return json_encode($this->toArray(), $options);
}
/**
* Get an iterator for the items.
*
* @return ArrayIterator
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->items);
}
/**
* Get a CachingIterator instance.
*
* @param integer $flags Caching iterator flags
*
* @return \CachingIterator
*/
public function getCachingIterator(int $flags = CachingIterator::CALL_TOSTRING): CachingIterator
{
return new \CachingIterator($this->getIterator(), $flags);
}
/**
* Count the number of items in the collection.
*
* @return int
*/
public function count(): int
{
return count($this->items);
}
/**
* Determine if an item exists at an offset.
*
* @param mixed $key
*
* @return bool
*/
public function offsetExists($key): bool
{
return array_key_exists($key, $this->items);
}
/**
* Get an item at a given offset.
*
* @param mixed $key
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->items[$key];
}
/**
* Set the item at a given offset.
*
* @param mixed $key
* @param mixed $value
*
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
if (is_null($key))
{
$this->items[] = $value;
}
else
{
$this->items[$key] = $value;
}
}
/**
* Unset the item at a given offset.
*
* @param string $key
*
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
unset($this->items[$key]);
}
/**
* Convert the collection to its string representation.
*
* @return string
*/
public function __toString(): string
{
return $this->toJson();
}
/**
* Get a value retrieving callback.
*
* @param string $value
*
* @return \Closure
*/
protected function valueRetriever(string $value): callable
{
return function ($item) use ($value) {
return is_object($item) ? $item->{$value} : array_get($item, $value);
};
}
/**
* Results array of items from Collection.
*
* @param Collection|array $items
*
* @return array
*/
private function getIterableItemsAsArray($items): array
{
if ($items instanceof Collection)
{
$items = $items->all();
}
elseif (is_object($items) && method_exists($items, 'toArray'))
{
$items = $items->toArray();
}
return $items;
}
}

View File

@ -0,0 +1,282 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
use FOF40\Timer\Timer;
use Joomla\CMS\Factory as JoomlaFactory;
/**
* A utility class to check that your extension's files are not missing and have not been tampered with.
*
* You need a file called fileslist.php in your component's administrator root directory with the following contents:
*
* $phpFileChecker = array(
* 'version' => 'revCEE2DAB',
* 'date' => '2014-10-16',
* 'directories' => array(
* 'administrator/components/com_foobar',
* ....
* ),
* 'files' => array(
* 'administrator/components/com_foobar/access.xml' => array('705', '09aa0351a316bf011ecc8c1145134761',
* 'b95f00c7b49a07a60570dc674f2497c45c4e7152'),
* ....
* )
* );
*
* All directory and file paths are relative to the site's root
*
* The directories array is a list of directories which must exist. The files array has the file paths as keys. The
* value is a simple array containing the following elements in this order: file size in bytes, MD5 checksum, SHA1
* checksum.
*/
class FilesCheck
{
/** @var string The name of the component */
protected $option = '';
/** @var string Current component version */
protected $version;
/** @var string Current component release date */
protected $date;
/** @var array List of files to check as filepath => (filesize, md5, sha1) */
protected $fileList = [];
/** @var array List of directories to check that exist */
protected $dirList = [];
/** @var bool Is the reported component version different than the version of the #__extensions table? */
protected $wrongComponentVersion = false;
/** @var bool Is the fileslist.php reporting a version different than the reported component version? */
protected $wrongFilesVersion = false;
/**
* Create and initialise the object
*
* @param string $componentName Component name, e.g. com_foobar
* @param string $version The current component version, as reported by the component
* @param string $date The current component release date, as reported by the component
*/
public function __construct(string $componentName, string $version, string $date)
{
// Initialise from parameters
$this->option = $componentName;
$this->version = $version;
$this->date = $date;
// Retrieve the date and version from the #__extensions table
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)->select('*')->from($db->qn('#__extensions'))
->where($db->qn('element') . ' = ' . $db->q($this->option))
->where($db->qn('type') . ' = ' . $db->q('component'));
$extension = $db->setQuery($query)->loadObject();
// Check the version and date against those from #__extensions. I hate heavily nested IFs as much as the next
// guy, but what can you do...
if (!is_null($extension))
{
$manifestCache = $extension->manifest_cache;
if (!empty($manifestCache))
{
$manifestCache = json_decode($manifestCache, true);
if (is_array($manifestCache) && isset($manifestCache['creationDate']) && isset($manifestCache['version']))
{
// Make sure the fileslist.php version and date match the component's version
if ($this->version != $manifestCache['version'])
{
$this->wrongComponentVersion = true;
}
if ($this->date != $manifestCache['creationDate'])
{
$this->wrongComponentVersion = true;
}
}
}
}
// Try to load the fileslist.php file from the component's back-end root
$filePath = JPATH_ADMINISTRATOR . '/components/' . $this->option . '/fileslist.php';
if (!file_exists($filePath))
{
return;
}
$couldInclude = @include($filePath);
// If we couldn't include the file with the array OR if it didn't define the array we have to quit.
if (!$couldInclude || !isset($phpFileChecker))
{
return;
}
// Make sure the fileslist.php version and date match the component's version
if ($this->version != $phpFileChecker['version'])
{
$this->wrongFilesVersion = true;
}
if ($this->date != $phpFileChecker['date'])
{
$this->wrongFilesVersion = true;
}
// Initialise the files and directories lists
$this->fileList = $phpFileChecker['files'];
$this->dirList = $phpFileChecker['directories'];
}
/**
* Is the reported component version different than the version of the #__extensions table?
*
* @return boolean
*/
public function isWrongComponentVersion(): bool
{
return $this->wrongComponentVersion;
}
/**
* Is the fileslist.php reporting a version different than the reported component version?
*
* @return boolean
*/
public function isWrongFilesVersion(): bool
{
return $this->wrongFilesVersion;
}
/**
* Performs a fast check of file and folders. If even one of the files/folders doesn't exist, or even one file has
* the wrong file size it will return false.
*
* @return bool False when there are mismatched files and directories
*/
public function fastCheck(): bool
{
// Check that all directories exist
foreach ($this->dirList as $directory)
{
$directory = JPATH_ROOT . '/' . $directory;
if (!@is_dir($directory))
{
return false;
}
}
// Check that all files exist and have the right size
foreach ($this->fileList as $filePath => $fileData)
{
$filePath = JPATH_ROOT . '/' . $filePath;
if (!@file_exists($filePath))
{
return false;
}
$fileSize = @filesize($filePath);
if ($fileSize != $fileData[0])
{
return false;
}
}
return true;
}
/**
* Performs a slow, thorough check of all files and folders (including MD5/SHA1 sum checks)
*
* @param int $idx The index from where to start
*
* @return array Progress report
*/
public function slowCheck(int $idx = 0): array
{
$ret = [
'done' => false,
'files' => [],
'folders' => [],
'idx' => $idx,
];
$totalFiles = count($this->fileList);
$totalFolders = count($this->dirList);
$fileKeys = array_keys($this->fileList);
$timer = new Timer(3.0, 75.0);
while ($timer->getTimeLeft() && (($idx < $totalFiles) || ($idx < $totalFolders)))
{
if ($idx < $totalFolders)
{
$directory = JPATH_ROOT . '/' . $this->dirList[$idx];
if (!@is_dir($directory))
{
$ret['folders'][] = $directory;
}
}
if ($idx < $totalFiles)
{
$fileKey = $fileKeys[$idx];
$filePath = JPATH_ROOT . '/' . $fileKey;
$fileData = $this->fileList[$fileKey];
if (!@file_exists($filePath))
{
$ret['files'][] = $fileKey . ' (missing)';
}
elseif (@filesize($filePath) != $fileData[0])
{
$ret['files'][] = $fileKey . ' (size ' . @filesize($filePath) . ' ≠ ' . $fileData[0] . ')';
}
elseif (function_exists('sha1_file'))
{
$fileSha1 = @sha1_file($filePath);
if ($fileSha1 != $fileData[2])
{
$ret['files'][] = $fileKey . ' (SHA1 ' . $fileSha1 . ' ≠ ' . $fileData[2] . ')';
}
}
elseif (function_exists('md5_file'))
{
$fileMd5 = @md5_file($filePath);
if ($fileMd5 != $fileData[1])
{
$ret['files'][] = $fileKey . ' (MD5 ' . $fileMd5 . ' ≠ ' . $fileData[1] . ')';
}
}
}
$idx++;
}
if (($idx >= $totalFiles) && ($idx >= $totalFolders))
{
$ret['done'] = true;
}
$ret['idx'] = $idx;
return $ret;
}
}

View File

@ -0,0 +1,211 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
use Exception;
use FOF40\Container\Container;
use JDatabaseDriver;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;
/**
* Class MediaVersion
*
* @since 3.5.3
*/
class MediaVersion
{
/**
* Cached the version and date of FOF-powered components
*
* @var array
* @since 3.5.3
*/
protected static $componentVersionCache = [];
/**
* @var string|mixed
*/
public $componentName;
/**
* The current component's container
*
* @var Container
* @since 3.5.3
*/
protected $container;
/**
* The configured media query version
*
* @var string|null;
* @since 3.5.3
*/
protected $mediaVersion;
/**
* MediaVersion constructor.
*
* @param Container $c The component container
*
* @since 3.5.3
*/
public function __construct(Container $c)
{
$this->container = $c;
}
/**
* Get a component's version and date
*
* @param string $component
* @param JDatabaseDriver $db
*
* @return array
* @since 3.5.3
*/
protected static function getComponentVersionAndDate(string $component, JDatabaseDriver $db): array
{
if (array_key_exists($component, self::$componentVersionCache))
{
return self::$componentVersionCache[$component];
}
$version = '0.0.0';
$date = date('Y-m-d H:i:s');
try
{
$query = $db->getQuery(true)
->select([
$db->qn('manifest_cache'),
])->from($db->qn('#__extensions'))
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('name') . ' = ' . $db->q($component));
$db->setQuery($query);
$json = $db->loadResult() ?? '{}';
$params = new Registry($json);
$version = $params->get('version', $version);
$date = $params->get('creationDate', $date);
}
catch (Exception $e)
{
}
self::$componentVersionCache[$component] = [$version, $date];
return self::$componentVersionCache[$component];
}
/**
* Serialization helper
*
* This is for the benefit of legacy components which might use Joomla's JS/CSS inclusion directly passing
* $container->mediaVersion as the version argument. In FOF 3.5.2 and lower that was always string or null, making
* it a safe bet. In FOF 3.5.3 and later it's an object. It's not converted to a string until Joomla builds its
* template header. However, Joomla's cache system will try to serialize all CSS and JS definitions, including their
* parameters of which version is one. Therefore, for those legacy applications, Joomla would be trying to serialize
* the MediaVersion object which would try to serialize the container. That would cause an immediate failure since
* we protect the Container from being serialized.
*
* Our Template service knows about this and stringifies the MediaVersion before passing it to Joomla. Legacy apps
* may not do that. Using the __sleep and __wakeup methods in this class we make sure that we are essentially
* storing nothing but strings in the serialized representation and we reconstruct the container upon
* unseralization. That said, it's a good idea to use the Template service instead of $container->mediaVersion
* directly or, at the very least, use (string) $container->mediaVersion when using the Template service is not a
* viable option.
*
* @return string[]
*/
public function __sleep(): array
{
$this->componentName = $this->container->componentName;
return [
'mediaVersion',
'componentName',
];
}
/**
* Unserialization helper
*
* @return void
* @see __sleep
*/
public function __wakeup(): void
{
if (isset($this->componentName))
{
$this->container = Container::getInstance($this->componentName);
}
}
/**
* Returns the media query version string
*
* @return string
* @since 3.5.3
*/
public function __toString(): string
{
if (empty($this->mediaVersion))
{
$this->mediaVersion = $this->getDefaultMediaVersion();
}
return $this->mediaVersion;
}
/**
* Sets the media query version string
*
* @param string|null $mediaVersion
*
* @since 3.5.3
*/
public function setMediaVersion(?string $mediaVersion): void
{
$this->mediaVersion = $mediaVersion;
}
/**
* Returns the default media query version string if none is already defined
*
* @return string
* @since 3.5.3
*/
protected function getDefaultMediaVersion(): string
{
// Initialise
[$version, $date] = self::getComponentVersionAndDate($this->container->componentName, $this->container->db);
// Get the site's secret
try
{
$app = Factory::getApplication();
if (method_exists($app, 'get'))
{
$secret = $app->get('secret');
}
}
catch (Exception $e)
{
}
// Generate the version string
return md5($version . $date . $secret);
}
}

View File

@ -0,0 +1,319 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
/**
* Generate phpDoc type hints for the magic properties and methods of your DataModels.
*
* Usage:
* $typeHinter = new ModelTypeHints($instanceOfYourFOFDataModel);
* var_dump($typeHinter->getHints());
*
* This will dump the type hints you should add to the DocBlock of your DataModel class to allow IDEs such as phpStorm
* to provide smart type hinting for magic property and method access.
*
* @package FOF40\Utils
*/
class ModelTypeHints
{
/**
* The model for which to create type hints
*
* @var DataModel
*/
protected $model;
/**
* Name of the class. If empty will be inferred from the current object
*
* @var string
*/
protected $className;
/**
* Public constructor
*
* @param \FOF40\Model\DataModel $model The model to create hints for
*/
public function __construct(DataModel $model)
{
$this->model = $model;
$this->className = get_class($model);
}
/**
* Translates the database field type into a PHP base type
*
* @param string $type The type of the field
*
* @return string The PHP base type
*/
public static function getFieldType(string $type): string
{
// Remove parentheses, indicating field options / size (they don't matter in type detection)
if (!empty($type))
{
[$type,] = explode('(', $type);
}
$detectedType = null;
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'character varying':
case 'nvarchar':
case 'nchar':
$detectedType = 'string';
break;
case 'date':
case 'datetime':
case 'time':
case 'year':
case 'timestamp':
case 'timestamp without time zone':
case 'timestamp with time zone':
$detectedType = 'string';
break;
case 'tinyint':
case 'smallint':
$detectedType = 'bool';
break;
case 'float':
case 'currency':
case 'single':
case 'double':
$detectedType = 'float';
break;
}
// Sometimes we have character types followed by a space and some cruft. Let's handle them.
if (is_null($detectedType) && !empty($type))
{
[$type,] = explode(' ', $type);
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'nvarchar':
case 'nchar':
$detectedType = 'string';
break;
case 'date':
case 'datetime':
case 'time':
case 'year':
case 'timestamp':
case 'enum':
$detectedType = 'string';
break;
case 'tinyint':
case 'smallint':
$detectedType = 'bool';
break;
case 'float':
case 'currency':
case 'single':
case 'double':
$detectedType = 'float';
break;
default:
$detectedType = 'int';
break;
}
}
// If all else fails assume it's an int and hope for the best
if (empty($detectedType))
{
$detectedType = 'int';
}
return $detectedType;
}
/**
* @param string $className
*/
public function setClassName(string $className): void
{
$this->className = $className;
}
/**
* Return the raw hints array
*
* @return array
*
* @throws \FOF40\Model\DataModel\Relation\Exception\RelationNotFound
*/
public function getRawHints(): array
{
$model = $this->model;
$hints = [
'property' => [],
'method' => [],
'property-read' => [],
];
$hasFilters = $model->getBehavioursDispatcher()->hasObserverClass('FOF40\Model\DataModel\Behaviour\Filters');
$magicFields = [
'enabled', 'ordering', 'created_on', 'created_by', 'modified_on', 'modified_by', 'locked_on', 'locked_by',
];
foreach ($model->getTableFields() as $fieldName => $fieldMeta)
{
$fieldType = static::getFieldType($fieldMeta->Type);
if (!in_array($fieldName, $magicFields))
{
$hints['property'][] = [$fieldType, '$' . $fieldName];
}
if ($hasFilters)
{
$hints['method'][] = [
'$this',
$fieldName . '()',
$fieldName . '(' . $fieldType . ' $v)',
];
}
}
$relations = $model->getRelations()->getRelationNames();
$modelType = get_class($model);
$modelTypeParts = explode('\\', $modelType);
array_pop($modelTypeParts);
$modelType = implode('\\', $modelTypeParts) . '\\';
if ($relations !== [])
{
foreach ($relations as $relationName)
{
$relationObject = $model->getRelations()->getRelation($relationName)->getForeignModel();
$relationType = get_class($relationObject);
$relationType = str_replace($modelType, '', $relationType);
$hints['property-read'][] = [
$relationType,
'$' . $relationName,
];
}
}
return $hints;
}
/**
* Returns the docblock with the magic field hints for the model class
*
* @return string
*/
public function getHints(): string
{
$modelName = $this->className;
$text = "/**\n * Model $modelName\n *\n";
$hints = $this->getRawHints();
if (!empty($hints['property']))
{
$text .= " * Fields:\n *\n";
$colWidth = 0;
foreach ($hints['property'] as $hintLine)
{
$colWidth = max($colWidth, strlen($hintLine[0]));
}
$colWidth += 2;
foreach ($hints['property'] as $hintLine)
{
$text .= " * @property " . str_pad($hintLine[0], $colWidth, ' ') . $hintLine[1] . "\n";
}
$text .= " *\n";
}
if (!empty($hints['method']))
{
$text .= " * Filters:\n *\n";
$colWidth = 0;
$col2Width = 0;
foreach ($hints['method'] as $hintLine)
{
$colWidth = max($colWidth, strlen($hintLine[0]));
$col2Width = max($col2Width, strlen($hintLine[1]));
}
$colWidth += 2;
$col2Width += 2;
foreach ($hints['method'] as $hintLine)
{
$text .= " * @method " . str_pad($hintLine[0], $colWidth, ' ')
. str_pad($hintLine[1], $col2Width, ' ')
. $hintLine[2] . "\n";
}
$text .= " *\n";
}
if (!empty($hints['property-read']))
{
$text .= " * Relations:\n *\n";
$colWidth = 0;
foreach ($hints['property-read'] as $hintLine)
{
$colWidth = max($colWidth, strlen($hintLine[0]));
}
$colWidth += 2;
foreach ($hints['property-read'] as $hintLine)
{
$text .= " * @property " . str_pad($hintLine[0], $colWidth, ' ') . $hintLine[1] . "\n";
}
$text .= " *\n";
}
return $text . "**/\n";
}
}

View File

@ -0,0 +1,173 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Utils;
use FOF40\Container\Container;
use Joomla\CMS\Filesystem\File;
use Joomla\Filesystem\Folder;
class ViewManifestMigration
{
/**
* Migrates Joomla 4 view XML manifests into their Joomla 3 locations
*
* @param Container $container The FOF 4 container of the component we'll be migrating.
*
* @return void
*/
public static function migrateJoomla4MenuXMLFiles(Container $container): void
{
self::migrateJoomla4MenuXMLFiles_real($container->frontEndPath, $container->backEndPath);
}
/**
* Migrates Joomla 4 view XML manifests into their Joomla 3 locations
*
* @param string $frontendPath Component's frontend path
* @param string $backendPath Component's backend path
*
* @return void
* @noinspection PhpUnused
*/
public static function migrateJoomla4MenuXMLFiles_real($frontendPath, $backendPath): void
{
// This only applies to Joomla 3
if (version_compare(JVERSION, '3.999.999', 'gt'))
{
return;
}
// Map modern to legacy locations
$maps = [
$frontendPath . '/tmpl' => $frontendPath . '/views',
$frontendPath . '/ViewTemplates' => $frontendPath . '/views',
$backendPath . '/tmpl' => $backendPath . '/views',
$backendPath . '/ViewTemplates' => $backendPath . '/views',
];
foreach ($maps as $source => $dest)
{
try
{
self::migrateViewXMLManifests($source, $dest);
}
catch (\UnexpectedValueException $e)
{
// This means the source folder doesn't exist. No problem!
}
}
}
/**
* Removes the legacy `views` paths from the front- and backend of the component on Joomla 4 and later versions.
*
* @param Container $container
*
* @return void
*/
public static function removeJoomla3LegacyViews(Container $container): void
{
self::removeJoomla3LegacyViews_real($container->frontEndPath, $container->backEndPath);
}
/**
* Removes the legacy `views` paths from the front- and backend of the component on Joomla 4 and later versions.
*
* @param string $frontendPath Component's frontend path
* @param string $backendPath Component's backend path
*
* @return void
* @noinspection PhpUnused
*/
public static function removeJoomla3LegacyViews_real($frontendPath, $backendPath): void
{
// This only applies to Joomla 4
if (version_compare(JVERSION, '3.999.999', 'le'))
{
return;
}
$legacyLocations = [
$frontendPath . '/views',
$backendPath . '/views',
];
foreach ($legacyLocations as $path)
{
if (!is_dir($path))
{
continue;
}
Folder::delete($path);
}
}
/**
* Migrates view manifest XML files from the source to the dest folder.
*
* @param string $source Source folder to scan, i.e. the `tmpl` or `ViewTemplates` folder.
* @param string $dest Target folder to copy the files to, i.e. the legacy `views` folder.
*/
private static function migrateViewXMLManifests(string $source, string $dest): void
{
$di = new \DirectoryIterator($source);
/** @var \DirectoryIterator $folderItem */
foreach ($di as $folderItem)
{
if ($folderItem->isDot() || !$folderItem->isDir())
{
continue;
}
// Delete the metadata.xml and tmpl/*.xml files in the corresponding `views` subfolder
$killLegacyFile = $dest . '/' . $folderItem->getFilename() . '/metadata.xml';
$killLegacyFolder = $dest . '/' . $folderItem->getFilename() . '/tmpl';
if (!@is_file($killLegacyFile))
{
File::delete($killLegacyFile);
}
if (@file_exists($killLegacyFolder) && @is_dir($killLegacyFolder))
{
$files = Folder::files($killLegacyFolder, '\.xml$', false, true);
if (!empty($files))
{
File::delete($files);
}
}
$filesIterator = new \DirectoryIterator($folderItem->getPathname());
/** @var \DirectoryIterator $fileItem */
foreach ($filesIterator as $fileItem)
{
if ($fileItem->isDir())
{
continue;
}
if ($fileItem->getExtension() != 'xml')
{
continue;
}
$destPath = $dest . '/' . $folderItem->getFilename() . (($fileItem->getFilename() == 'metadata.xml') ? '' : '/tmpl');
$destPathName = $destPath . '/' . $fileItem->getFilename();
Folder::create($destPath);
File::copy($fileItem->getPathname(), $destPathName);
}
}
}
}

View File

@ -0,0 +1,616 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
/**
* This is a modified copy of Laravel 4's "helpers.php"
*
* Laravel 4 is distributed under the MIT license, see https://github.com/laravel/framework/blob/master/LICENSE.txt
*/
defined('_JEXEC') || die;
if (!function_exists('array_add'))
{
/**
* Add an element to an array if it doesn't exist.
*
* @param array $array
* @param string $key
* @param mixed $value
*
* @return array
*/
function array_add(array $array, string $key, $value): array
{
if (!isset($array[$key]))
{
$array[$key] = $value;
}
return $array;
}
}
if (!function_exists('array_build'))
{
/**
* Build a new array using a callback.
*
* @param array $array
* @param \Closure $callback
*
* @return array
*/
function array_build(array $array, Closure $callback): array
{
$results = [];
foreach ($array as $key => $value)
{
[$innerKey, $innerValue] = call_user_func($callback, $key, $value);
$results[$innerKey] = $innerValue;
}
return $results;
}
}
if (!function_exists('array_divide'))
{
/**
* Divide an array into two arrays. One with keys and the other with values.
*
* @param array $array
*
* @return array
*/
function array_divide(array $array)
{
return [array_keys($array), array_values($array)];
}
}
if (!function_exists('array_dot'))
{
/**
* Flatten a multi-dimensional associative array with dots.
*
* @param array $array
* @param string $prepend
*
* @return array
*/
function array_dot(array $array, string $prepend = ''): array
{
$results = [];
foreach ($array as $key => $value)
{
if (is_array($value))
{
$results = array_merge($results, array_dot($value, $prepend . $key . '.'));
}
else
{
$results[$prepend . $key] = $value;
}
}
return $results;
}
}
if (!function_exists('array_except'))
{
/**
* Get all of the given array except for a specified array of items.
*
* @param array $array
* @param array $keys
*
* @return array
*/
function array_except(array $array, array $keys): array
{
return array_diff_key($array, array_flip((array) $keys));
}
}
if (!function_exists('array_fetch'))
{
/**
* Fetch a flattened array of a nested array element.
*
* @param array $array
* @param string $key
*
* @return array
*/
function array_fetch(array $array, string $key): array
{
foreach (explode('.', $key) as $segment)
{
$results = [];
foreach ($array as $value)
{
$value = (array) $value;
$results[] = $value[$segment];
}
$array = array_values($results);
}
return array_values($results);
}
}
if (!function_exists('array_first'))
{
/**
* Return the first element in an array passing a given truth test.
*
* @param array $array
* @param Closure $callback
* @param mixed $default
*
* @return mixed
*/
function array_first(array $array, callable $callback, $default = null)
{
foreach ($array as $key => $value)
{
if (call_user_func($callback, $key, $value))
{
return $value;
}
}
return value($default);
}
}
if (!function_exists('array_last'))
{
/**
* Return the last element in an array passing a given truth test.
*
* @param array $array
* @param Closure $callback
* @param mixed $default
*
* @return mixed
*/
function array_last(array $array, callable $callback, $default = null)
{
return array_first(array_reverse($array), $callback, $default);
}
}
if (!function_exists('array_flatten'))
{
/**
* Flatten a multi-dimensional array into a single level.
*
* @param array $array
*
* @return array
*/
function array_flatten(array $array): array
{
$return = [];
array_walk_recursive($array, function ($x) use (&$return) {
$return[] = $x;
});
return $return;
}
}
if (!function_exists('array_forget'))
{
/**
* Remove an array item from a given array using "dot" notation.
*
* @param array $array
* @param string $key
*
* @return void
*/
function array_forget(array &$array, string $key): void
{
$keys = explode('.', $key);
while (count($keys) > 1)
{
$key = array_shift($keys);
if (!isset($array[$key]) || !is_array($array[$key]))
{
return;
}
$array =& $array[$key];
}
unset($array[array_shift($keys)]);
}
}
if (!function_exists('array_get'))
{
/**
* Get an item from an array using "dot" notation.
*
* @param array $array
* @param string $key
* @param mixed $default
*
* @return mixed
*/
function array_get(array $array, string $key, $default = null)
{
if (is_null($key))
{
return $array;
}
if (isset($array[$key]))
{
return $array[$key];
}
foreach (explode('.', $key) as $segment)
{
if (!is_array($array) || !array_key_exists($segment, $array))
{
return value($default);
}
$array = $array[$segment];
}
return $array;
}
}
if (!function_exists('array_only'))
{
/**
* Get a subset of the items from the given array.
*
* @param array $array
* @param array $keys
*
* @return array
*/
function array_only(array $array, array $keys): array
{
return array_intersect_key($array, array_flip((array) $keys));
}
}
if (!function_exists('array_pluck'))
{
/**
* Pluck an array of values from an array.
*
* @param array $array
* @param string $value
* @param string $key
*
* @return array
*/
function array_pluck(array $array, string $value, ?string $key = null): array
{
$results = [];
foreach ($array as $item)
{
$itemValue = is_object($item) ? $item->{$value} : $item[$value];
// If the key is "null", we will just append the value to the array and keep
// looping. Otherwise we will key the array using the value of the key we
// received from the developer. Then we'll return the final array form.
if (is_null($key))
{
$results[] = $itemValue;
}
else
{
$itemKey = is_object($item) ? $item->{$key} : $item[$key];
$results[$itemKey] = $itemValue;
}
}
return $results;
}
}
if (!function_exists('array_pull'))
{
/**
* Get a value from the array, and remove it.
*
* @param array $array
* @param string $key
*
* @return mixed
*/
function array_pull(array &$array, string $key)
{
$value = array_get($array, $key);
array_forget($array, $key);
return $value;
}
}
if (!function_exists('array_set'))
{
/**
* Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string $key
* @param mixed $value
*
* @return array
*/
function array_set(array &$array, string $key, $value): array
{
if (is_null($key))
{
return $array = $value;
}
$keys = explode('.', $key);
while (count($keys) > 1)
{
$key = array_shift($keys);
// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (!isset($array[$key]) || !is_array($array[$key]))
{
$array[$key] = [];
}
$array =& $array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
}
if (!function_exists('array_sort'))
{
/**
* Sort the array using the given Closure.
*
* @param array $array
* @param \Closure $callback
*
* @return array
*/
function array_sort(array $array, callable $callback): array
{
return FOF40\Utils\Collection::make($array)->sortBy($callback)->all();
}
}
if (!function_exists('array_where'))
{
/**
* Filter the array using the given Closure.
*
* @param array $array
* @param \Closure $callback
*
* @return array
*/
function array_where(array $array, callable $callback): array
{
$filtered = [];
foreach ($array as $key => $value)
{
if (call_user_func($callback, $key, $value))
{
$filtered[$key] = $value;
}
}
return $filtered;
}
}
if (!function_exists('ends_with'))
{
/**
* Determine if a given string ends with a given substring.
*
* @param string $haystack
* @param string|array $needles
*
* @return bool
*/
function ends_with(string $haystack, $needles): bool
{
foreach ((array) $needles as $needle)
{
if ((string) $needle === substr($haystack, -strlen($needle)))
{
return true;
}
}
return false;
}
}
if (!function_exists('last'))
{
/**
* Get the last element from an array.
*
* @param array $array
*
* @return mixed
*/
function last(array $array)
{
return end($array);
}
}
if (!function_exists('object_get'))
{
/**
* Get an item from an object using "dot" notation.
*
* @param object $object
* @param string $key
* @param mixed $default
*
* @return mixed
*/
function object_get($object, string $key, $default = null)
{
if (is_null($key) || trim($key) == '')
{
return $object;
}
foreach (explode('.', $key) as $segment)
{
if (!is_object($object) || !isset($object->{$segment}))
{
return value($default);
}
$object = $object->{$segment};
}
return $object;
}
}
if (!function_exists('preg_replace_sub'))
{
/**
* Replace a given pattern with each value in the array in sequentially.
*
* @param string $pattern
* @param array $replacements
* @param string $subject
*
* @return string
*/
function preg_replace_sub(string $pattern, array &$replacements, string $subject): string
{
return preg_replace_callback($pattern, function ($match) use (&$replacements) {
return array_shift($replacements);
}, $subject);
}
}
if (!function_exists('starts_with'))
{
/**
* Determine if a given string starts with a given substring.
*
* @param string $haystack
* @param string|array $needles
*
* @return bool
*/
function starts_with(string $haystack, $needles): bool
{
foreach ((array) $needles as $needle)
{
if ($needle != '' && strpos($haystack, $needle) === 0)
{
return true;
}
}
return false;
}
}
if (!function_exists('value'))
{
/**
* Return the default value of the given value.
*
* @param mixed $value
*
* @return mixed
*/
function value($value)
{
return $value instanceof Closure ? $value() : $value;
}
}
if (!function_exists('with'))
{
/**
* Return the given object. Useful for chaining.
*
* @param mixed $object
*
* @return mixed
*/
function with($object)
{
return $object;
}
}
if (!function_exists('fofStringToBool'))
{
/**
* Convert a string to a boolean. It understands the following human-readable boolean notations 0, 1, true, false,
* yes, no, on, off, enabled, disabled. If the value is anything else it will delegate it to PHP's `(bool)` type
* casting operator.
*
* @param string $string The string with the human-readable boolean value.
*
* @return boolean The converted string
*/
function fofStringToBool(string $string): bool
{
$string = trim((string) $string);
$string = strtolower($string);
if (in_array($string, ['1', 'true', 'yes', 'on', 'enabled'], true))
{
return true;
}
if (in_array($string, ['0', 'false', 'no', 'off', 'disabled'], true))
{
return false;
}
return (bool) $string;
}
}