Your IP : 216.73.216.50


Current Path : /proc/1908984/root/proc/self/root/proc/self/root/proc/2411249/cwd/
Upload File :
Current File : //proc/1908984/root/proc/self/root/proc/self/root/proc/2411249/cwd/Utils.tar

ArrayHelper.php000064400000031260152345757660007521 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or 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   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 array();
		}

		if (is_array($default))
		{
			return static::toInteger($default, null);
		}

		return array((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 = array();

		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 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 = array();

			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 = array();

			foreach ($item as $k => $v)
			{
				$result[$k] = self::arrayFromObject($v, $recurse, $regex);
			}

			return $result;
		}

		return $item;
	}

	/**
	 * 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 = array();

		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
	 *
	 * @since   1.0
	 * @throws  \InvalidArgumentException
	 */
	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 = array($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 = array();

		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  = array();
		$counter = array();

		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] = array(
					$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;
	}
}
Buffer.php000064400000014671152345757660006523 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or die;

/**
 * Registers a fof:// stream wrapper
 */
class Buffer
{
	/**
	 * Stream position
	 *
	 * @var    integer
	 */
	public $position = 0;

	/**
	 * Buffer name
	 *
	 * @var    string
	 */
	public $name = null;

	/**
	 * Buffer hash
	 *
	 * @var    array
	 */
	public static $buffers = array();

	public static $canRegisterWrapper = 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 array(
			'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');
}
CliSessionHandler.php000064400000006307152345757660010660 0ustar00<?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));
	}
}Collection.php000064400000032531152345757660007400 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use ArrayAccess;
use ArrayIterator;
use CachingIterator;
use Closure;
use Countable;
use IteratorAggregate;
use JsonSerializable;

defined('_JEXEC') or die;

class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
{
	/**
	 * The items contained in the collection.
	 *
	 * @var array
	 */
	protected $items = array();

	/**
	 * Create a new collection.
	 *
	 * @param  array $items
	 */
	public function __construct(array $items = array())
	{
		$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 : array($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 Collection
	 */
	public function collapse()
	{
		$results = array();

		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)
	{
		return new static(array_diff($this->items, $this->getArrayableItems($items)));
	}

	/**
	 * Execute a callback over each item.
	 *
	 * @param  Closure $callback
	 *
	 * @return Collection
	 */
	public function each(Closure $callback)
	{
		array_map($callback, $this->items);

		return $this;
	}

	/**
	 * Fetch a nested element of the collection.
	 *
	 * @param  string $key
	 *
	 * @return Collection
	 */
	public function fetch($key)
	{
		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)
	{
		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 Collection
	 */
	public function groupBy($groupBy)
	{
		$results = array();

		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 Collection
	 */
	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 Collection
	 */
	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 Collection
	 */
	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 Collection
	 */
	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 Collection
	 */
	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 Collection
	 */
	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 Collection
	 */
	public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
	{
		$results = array();

		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, $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 Collection
	 */
	public function splice($offset, $length = 0, $replacement = array())
	{
		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 Collection
	 */
	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 = array();

		return $this;
	}

	/**
	 * Transform each item in the collection using a callback.
	 *
	 * @param  callable $callback
	 *
	 * @return Collection
	 */
	public function transform($callback)
	{
		$this->items = array_map($callback, $this->items);

		return $this;
	}

	/**
	 * Return only unique items from the collection array.
	 *
	 * @return Collection
	 */
	public function unique()
	{
		return new static(array_unique($this->items));
	}

	/**
	 * Reset the keys on the underlying array.
	 *
	 * @return Collection
	 */
	public function values()
	{
		$this->items = array_values($this->items);

		return $this;
	}

	/**
	 * 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);
		};
	}

	/**
	 * 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();
	}

	/**
	 * 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;
	}

}
FilesCheck.php000064400000016320152345757660007303 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use FOF30\Timer\Timer;

defined('_JEXEC') or die;

/**
 * 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 = array();

	/** @var array List of directories to check that exist */
	protected $dirList = array();

	/** @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 = \JFactory::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 = array(
			'done'	=> false,
			'files'	=> array(),
			'folders'	=> array(),
			'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;
	}
}
MediaVersion.php000064400000011555152345757660007675 0ustar00<?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);
	}
}ModelTypeHints.php000064400000014204152345757660010212 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use FOF30\Model\DataModel;

defined('_JEXEC') or die;

/**
 * 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;

    /**
     * @param string $className
     */
    public function setClassName($className)
    {
        $this->className = $className;
    }

	/**
	 * Public constructor
	 *
	 * @param   \FOF30\Model\DataModel  $model  The model to create hints for
	 */
	public function __construct(DataModel $model)
	{
		$this->model     = $model;
        $this->className = get_class($model);
	}

	/**
	 * Return the raw hints array
	 *
	 * @return  array
	 *
	 * @throws  \FOF30\Model\DataModel\Relation\Exception\RelationNotFound
	 */
	public function getRawHints()
	{
		$model = $this->model;

		$hints = array(
			'property'      => array(),
			'method'        => array(),
			'property-read' => array()
		);

		$hasFilters = $model->getBehavioursDispatcher()->hasObserverClass('FOF30\Model\DataModel\Behaviour\Filters');

		$magicFields = array(
			'enabled', 'ordering', 'created_on', 'created_by', 'modified_on', 'modified_by',  'locked_on', 'locked_by',
		);

		foreach ($model->getTableFields() as $fieldName => $fieldMeta)
		{
			$fieldType = $this->getFieldType($fieldMeta->Type);

			if (!in_array($fieldName, $magicFields))
			{
				$hints['property'][] = array($fieldType, '$' . $fieldName);
			}

			if ($hasFilters)
			{
				$hints['method'][] = array(
					'$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'][] = array(
					$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;
	}


	/**
	 * 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))
		{
			list($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))
		{
			list ($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;
	}
}
ViewManifestMigration.php000064400000010557152345757660011564 0ustar00<?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);
			}
		}
	}
}helpers.php000064400000023742152345757660006753 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 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') or 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, $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 = array();

		foreach ($array as $key => $value)
		{
			list($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(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 = array();

		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 = array();

			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();

		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 = array();

		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 =& $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 = array();

		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) or 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;
	}
}
Phpfunc.php000064400000001443152354067330006672 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or 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  mcrypt_list_algorithms()
 * @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);
	}
}
Ip.php000064400000033706152354067330005646 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or 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 chaing 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;

	/**
	 * Set the $useFirstIpInChain flag. See above.
	 *
	 * @param   bool  $value
	 */
	public static function setUseFirstIpInChain($value = true)
	{
		self::$useFirstIpInChain = $value;
	}

	/**
	 * 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;
	}

	/**
	 * 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;
	}

	/**
	 * 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 = array($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, '-'))
			{
				list($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)
				{
					list($from, $to) = array($to, $from);
				}

				if (($myIP >= $from) && ($myIP <= $to))
				{
					return true;
				}
			}
			// Netmask or CIDR provided
			elseif (strstr($ipExpression, '/'))
			{
				$binaryip = self::inet_to_bits($myIP);

				list($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;
	}

	/**
	 * Gets the visitor's IP address. Automatically handles reverse proxies
	 * reporting the IPs of intermediate devices, like load balancers. Examples:
	 * https://www.akeebabackup.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
			list($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 an x-forwarded-for HTTP header (e.g. NginX)?
			if (self::$allowIpOverrides && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER))
			{
				return $_SERVER['HTTP_X_FORWARDED_FOR'];
			}

			// Are we under CloudFlare?
			if (self::$allowIpOverrides && array_key_exists('HTTP_CF_CONNECTING_IP', $_SERVER))
			{
				return $_SERVER['HTTP_CF_CONNECTING_IP'];
			}

			// Are we using Sucuri firewall? They use a custom HTTP header
			if (self::$allowIpOverrides && array_key_exists('HTTP_X_SUCURI_CLIENTIP', $_SERVER))
			{
				return $_SERVER['HTTP_X_SUCURI_CLIENTIP'];
			}

			// Do we have a client-ip header (e.g. non-transparent proxy)?
			if (self::$allowIpOverrides && array_key_exists('HTTP_CLIENT_IP', $_SERVER))
			{
				return $_SERVER['HTTP_CLIENT_IP'];
			}

			// 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 an x-forwarded-for HTTP header?
		if (self::$allowIpOverrides && getenv('HTTP_X_FORWARDED_FOR'))
		{
			return getenv('HTTP_X_FORWARDED_FOR');
		}

		// Are we under CloudFlare?
		if (self::$allowIpOverrides && getenv('HTTP_CF_CONNECTING_IP'))
		{
			return getenv('HTTP_CF_CONNECTING_IP');
		}

		// Are we using Sucuri firewall? They use a custom HTTP header
		if (self::$allowIpOverrides && getenv('HTTP_X_SUCURI_CLIENTIP'))
		{
			return getenv('HTTP_X_SUCURI_CLIENTIP');
		}

		// Do we have a client-ip header?
		if (self::$allowIpOverrides && getenv('HTTP_CLIENT_IP'))
		{
			return getenv('HTTP_CLIENT_IP');
		}

		// 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('A4', $inet);
		}
		else
		{
			$unpacked = unpack('A16', $inet);
		}
		$unpacked = str_split($unpacked[1]);
		$binaryip = '';

		foreach ($unpacked as $char)
		{
			$binaryip .= str_pad(decbin(ord($char)), 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);

		list($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;
	}
}
CacheCleaner.php000064400000004226152354067330007566 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or die;

/**
 * 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(array('com_plugins'), array(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(array('com_modules'), array(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.
	 *
	 * @return  void
	 */
	public static function clearCacheGroups(array $clearGroups, array $cacheClients = array(0, 1))
	{
		$conf = \JFactory::getConfig();

		foreach ($clearGroups as $group)
		{
			foreach ($cacheClients as $client_id)
			{
				try
				{
					$options = array(
						'defaultgroup' => $group,
						'cachebase' => ($client_id) ? JPATH_ADMINISTRATOR . '/cache' : $conf->get('cache_path', JPATH_SITE . '/cache')
					);

					$cache = \JCache::getInstance('callback', $options);
					$cache->clean();
				}
				catch (\Exception $e)
				{
					// suck it up
				}
			}
		}
	}
}
InstallScript.php000064400000001341152354067330010057 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or 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 compatiblity with FOF 3.0. Please
 * use the new class FOF30\Utils\InstallScript\Component instead.
 */
class InstallScript extends Component
{

}
ComponentVersion.php000064400000006475152354067330010611 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use Exception;
use JFactory;
use SimpleXMLElement;

defined('_JEXEC') or die;

/**
 * Retrieve the version of a component from the cached XML manifest or, if it's not present, the version recorded in the
 * database.
 */
abstract class ComponentVersion
{
	/**
	 * A cache with the version numbers of components
	 *
	 * @var   array
	 *
	 * @since 3.1.5
	 */
	private static $version = array();

	/**
	 * Get a component's version. The XML manifest on disk will be tried first. If it's not there or does not have a
	 * version string the manifest cache in the database is tried. If that fails a fake version number will be returned.
	 *
	 * @param   string  $component  The name of the component, e.g. com_foobar
	 *
	 * @return  string  The version string
	 *
	 * @since   3.1.5
	 */
	public static function getFor($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 bname
	 *
	 * @return  string  The component version or null if none is defined
	 *
	 * @since   3.1.5
	 */
	private static function getVersionFromDatabase($component)
	{
		$db      = JFactory::getDbo();
		$query   = $db->getQuery(true)
			->select($db->qn('manifest_cache'))
			->from($db->qn('#__extensions'))
			->where($db->qn('element') . ' = ' . $db->q($component))
			->where($db->qn('type') . ' = ' . $db->q('component'));

		try
		{
			$json = $db->setQuery($query)->loadResult();
		}
		catch (Exception $e)
		{
			return null;
		}

		if (empty($json))
		{
			return null;
		}

		$options = json_decode($json, true);

		if (empty($options))
		{
			return null;
		}

		if (!isset($options['version']))
		{
			return null;
		}

		return $options['version'];
	}

	/**
	 * Get a component's version from the manifest file on disk. IMPORTANT! The manifest for com_something must be named
	 * something.xml.
	 *
	 * @param   string  $component  The component's bname
	 *
	 * @return  string  The component version or null if none is defined
	 *
	 * @since   1.2.0
	 */
	private static function getVersionFromManifest($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]);
	}
}
TimezoneWrangler.php000064400000022652152354067330010570 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use DateTime;
use DateTimeZone;
use Exception;
use FOF30\Container\Container;
use FOF30\Date\Date;
use JUser;

defined('_JEXEC') or die;

/**
 * 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   JUser|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   JUser  $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   JUser  $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   JUser|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   JUser|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   JUser|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   JUser|null  $user
	 *
	 * @return  Date
	 */
	public function GMTToLocal($time, $user = null)
	{
		return $this->getLocalDateTime($user, $time);
	}
}
SelectOptions.php000064400000024524152354067330010067 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use JCache;
use JFactory;
use JHtml;
use JText;

defined('_JEXEC') or die;

/**
 * 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.
	 *
	 * @since   3.3.0
	 *
	 * @return mixed
	 */
	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.
	 *
	 * @since   3.3.0
	 *
	 * @return  \stdClass[]
	 */
	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
	 *
	 * @since   3.3.0
	 *
	 * @return  \stdClass[]
	 *
	 * @see \JHtmlAccess::level()
	 */
	private static function access(array $params = [])
	{
		$db = JFactory::getDbo();
		$query = $db->getQuery(true)
			->select($db->quoteName('a.id', 'value') . ', ' . $db->quoteName('a.title', 'text'))
			->from($db->quoteName('#__viewlevels', 'a'))
			->group($db->quoteName(array('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, JHtml::_('select.option', '', JText::_('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
	 *
	 * @since   3.3.0
	 *
	 * @return  \stdClass[]
	 *
	 * @see \JHtmlAccess::usergroup()
	 */
	private static function usergroups(array $params = [])
	{
		$options = array_values(\JHelperUsergroups::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, JHtml::_('select.option', '', JText::_('JOPTION_ACCESS_SHOW_ALL_GROUPS')));
		}

		return $options;
	}

	/**
	 * Joomla cache handlers
	 *
	 * @since   3.3.0
	 *
	 * @return  \stdClass[]
	 */
	private static function cachehandlers()
	{
		$options = array();

		// Convert to name => name array.
		foreach (JCache::getStores() as $store)
		{
			$options[] = JHtml::_('select.option', $store, JText::_('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
	 *
	 * @since   3.3.0
	 *
	 * @return 	\stdClass[]
	 */
	private static function components(array $params)
	{
		$db = JFactory::getDbo();

		// Check for client_ids override
		$client_ids = isset($params['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        = JFactory::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
	 *
	 * @since   3.3.0
	 *
	 * @return  \stdClass  Languages for the specifed client
	 */
	private static function languages($params)
	{
		$db = JFactory::getDbo();

		$client = isset($params['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 = \JLanguageHelper::createLanguageList(null, constant('JPATH_' . strtoupper($client)), true, true);

		if (count($options) > 1)
		{
			usort(
				$options,
				function ($a, $b)
				{
					return strcmp($a['value'], $b['value']);
				}
			);
		}

		$none = isset($params['none']) ? $params['none'] : '*';

		if (!empty($none))
		{
			array_unshift($options, JHtml::_('select.option', '*', JText::_($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 = array();

		if (!empty($config['none']))
		{
			$options[] = JHtml::_('select.option', '', JText::_($config['none']));
		}

		if ($config['published'])
		{
			$options[] = JHtml::_('select.option', '1', JText::_('JPUBLISHED'));
		}

		if ($config['unpublished'])
		{
			$options[] = JHtml::_('select.option', '0', JText::_('JUNPUBLISHED'));
		}

		if ($config['archived'])
		{
			$options[] = JHtml::_('select.option', '2', JText::_('JARCHIVED'));
		}

		if ($config['trash'])
		{
			$options[] = JHtml::_('select.option', '-2', JText::_('JTRASHED'));
		}

		if ($config['all'])
		{
			$options[] = JHtml::_('select.option', '*', JText::_('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 = array();

		if (!empty($config['none']))
		{
			$options[] = JHtml::_('select.option', '', JText::_($config['none']));
		}

		$options[] = JHtml::_('select.option', '1', JText::_('JYES'));
		$options[] = JHtml::_('select.option', '0', JText::_('JNO'));

		return $options;
	}


	/**
	 * Translate a component name
	 *
	 * @param   \stdClass  $item  The component object
	 *
	 * @since   3.3.0
	 *
	 * @return  string  $text  The translated name of the extension
	 *
	 * @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   = JFactory::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 JText::_($item->name);
	}
}
StringHelper.php000064400000004063152354067330007676 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

defined('_JEXEC') or die;

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'))
		{
			\JLog::add('FOF30\\Utils\\StringHelper::toSlug is deprecated. Use \\JApplicationHelper::stringURLSafe instead', \JLog::WARNING, 'deprecated');
		}

		if (!class_exists('\JApplicationHelper'))
		{
			\JLoader::import('cms.application.helper');
		}

		return \JApplicationHelper::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'))
		{
			\JLog::add('FOF30\\Utils\\StringHelper::toASCII is deprecated. Use JFactory::getLanguage()->transliterate instead', \JLog::WARNING, 'deprecated');
		}

		$lang = \JFactory::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, array(1, 'true', 'yes', 'on', 'enabled'), true))
		{
			return true;
		}

		if (in_array($string, array(0, 'false', 'no', 'off', 'disabled'), true))
		{
			return false;
		}

		return (bool)$string;
	}
}
DynamicGroups.php000064400000013345152354067330010057 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils;

use FOF30\Container\Container;

defined('_JEXEC') or die;

/**
 * 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 = array($user->id, 0);

		foreach ($keys as $key)
		{
			if (!array_key_exists($key, $identities))
			{
				continue;
			}

			unset($identities[$key]);
		}

		$refProperty->setValue(null, $identities);
		$refProperty->setAccessible(false);
	}

	/**
	 * Clean up the current user's authenticated groups cache.
	 *
	 * @return  void
	 */
	protected static function cleanUpUserObjectCache()
	{
		// Get a fake container (we need it for its platform interface)
		$container = Container::getInstance('com_FOOBAR');

		$user          = $container->platform->getUser();
		$reflectedUser = new \ReflectionObject($user);

		// Clear the user group cache
		$refProperty   = $reflectedUser->getProperty('_authGroups');
		$refProperty->setAccessible(true);
		$refProperty->setValue($user, array());
		$refProperty->setAccessible(false);

		// Clear the view access level cache
		$refProperty   = $reflectedUser->getProperty('_authLevels');
		$refProperty->setAccessible(true);
		$refProperty->setValue($user, array());
		$refProperty->setAccessible(false);

		// Clear the authenticated actions cache. I haven't seen it used anywhere but it's there, so...
		$refProperty   = $reflectedUser->getProperty('_authActions');
		$refProperty->setAccessible(true);
		$refProperty->setValue($user, array());
		$refProperty->setAccessible(false);
	}
}FEFHelper/Html.php000064400000004727152354067330007743 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils\FEFHelper;

defined('_JEXEC') or die;

use FOF30\View\DataView\DataViewInterface;
use JHtml;

/**
 * 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 JHtml::_('FEFHelper.browse.orderjs', $order, true);
	}

	/**
	 * Creates the required HTML code for backend pagination and sorting
	 *
	 * @param	\JPagination	$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">' .
			JHtml::_('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 JHtml::_('FEFHelper.browse.order', $orderingField, $order, $class, $icon, $inactiveIcon, $view);
	}
}
FEFHelper/edit.php000064400000002564152354067330007761 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

defined('_JEXEC') or die;

/**
 * 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 = JFactory::getConfig()->get('editor');
			$user   = JFactory::getUser();

			if (!$user->guest)
			{
				$editorType = $user->getParam('editor', $editorType);
			}
		}

		if (is_null($params['id']))
		{
			$params['id'] = $fieldName;
		}

		$editor = JEditor::getInstance($editorType);

		return $editor->display($fieldName, $value, $params['width'], $params['height'],
			$params['columns'],  $params['rows'], $params['buttons'], $params['id'],
			$params['asset_id'], $params['created_by'], $params);
	}
}
FEFHelper/select.php000064400000064010152354067330010305 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

use FOF30\Utils\ArrayHelper;

defined('_JEXEC') or die;

/**
 * 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 = array(
		'option' => array(
			'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 = array(), $selected = null, $yes = 'JYES', $no = 'JNO', $id = false)
	{
		$arr = array(JHtml::_('FEFHelper.select.option', '0', JText::_($no)), JHtml::_('FEFHelper.select.option', '1', JText::_($yes)));

		return JHtml::_('FEFHelper.select.radiolist', $arr, $name, $attribs, 'value', 'text', (int) $selected, $id);
	}

	/**
	 * Generates an HTML selection list.
	 *
	 * @param   array    $data       An array of objects, arrays, or scalars.
	 * @param   string   $name       The value of the HTML name attribute.
	 * @param   mixed    $attribs    Additional HTML attributes for the `<select>` tag. This
	 *                               can be an array of attributes, or an array of options. Treated as options
	 *                               if it is the last argument passed. Valid options are:
	 *                               Format options, see {@see JHtml::$formatOptions}.
	 *                               Selection options, see {@see JHtmlSelect::options()}.
	 *                               list.attr, string|array: Additional attributes for the select
	 *                               element.
	 *                               id, string: Value to use as the select element id attribute.
	 *                               Defaults to the same as the name.
	 *                               list.select, string|array: Identifies one or more option elements
	 *                               to be selected, based on the option key values.
	 * @param   string   $optKey     The name of the object variable for the option value. If
	 *                               set to null, the index of the value array is used.
	 * @param   string   $optText    The name of the object variable for the option text.
	 * @param   mixed    $selected   The key that is selected (accepts an array or a string).
	 * @param   mixed    $idtag      Value of the field id or null by default
	 * @param   boolean  $translate  True to translate
	 *
	 * @return  string  HTML for the select list.
	 *
	 * @since   1.5
	 */
	public static function genericlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
	                                   $translate = false)
	{
		// Set default options
		$options = array_merge(JHtml::$formatOptions, array('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(array('[', ']', ' '), '', $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 = array())
	{
		// Set default options and overwrite with anything passed in
		$options = array_merge(
			JHtml::$formatOptions,
			array('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(array('[', ']', ' '), '', $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(JHtml::$formatOptions, array('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 = array();

		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 JHtml::_('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 = array(
			'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(
			JHtml::$formatOptions,
			static::$optionDefaults['option'],
			array('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'] ? JText::_($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 = JText::_($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 = JText::_($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)
	{

		if (is_array($attribs))
		{
			$attribs = ArrayHelper::toString($attribs);
		}

		$id_text = empty($idtag) ? $name : $idtag;

		$html = '';

		foreach ($data as $optionObject)
		{
			$optionValue = $optionObject->$optKey;
			$labelText   = $translate ? JText::_($optionObject->$optText) : $optionObject->$optText;
			$id          = (isset($optionObject->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" ' : '');
			}

			$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 = array())
	{
		if (empty($attribs))
		{
			$attribs = array('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">'.JText::_('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">'.JText::_('JNO').'</label>';
		$html .= '</div>';

		return $html;
	}
}
FEFHelper/BrowseView.php000064400000066704152354067330011136 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils\FEFHelper;

defined('_JEXEC') or die;

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 JHtml;
use JText;

/**
 * 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 (JText::_($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 JText::_(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 JHtml::_('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; ' . JText::_($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']        = isset($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(JText::_($placeholder)) : $attributes['placeholder'];
		$attributes['title']       = isset($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; ' . JText::_($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
	 *
	 * @see   self::getOptionsFromModel
	 * @see   self::getOptionsFromSource
	 * @see   self::genericSelect
	 *
	 * @return string
	 *
	 * @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
	 *
	 * @see   self::getOptionsFromModel
	 * @see   self::getOptionsFromSource
	 * @see   self::genericSelect
	 *
	 * @return string
	 *
	 * @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 = null, $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'] ? $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, JHtml::_('FEFHelper.select.option', '', JText::_($params['list.none'])));
		}

		$html = [];

		// Create a read-only list (no name) with hidden input(s) to store the value(s).
		if ($params['readonly'])
		{
			$html[] = JHtml::_('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[] = JHtml::_('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 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 = JText::_($value);
							}

							$options[] = JHtml::_('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)
		{
			list ($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 = JText::_($langKey);

			if ($langKey != $placeholder)
			{
				$params['none'] = '&mdash; ' . $placeholder . ' &mdash;';
			}
		}

		if (!empty($params['none']))
		{
			$options[] = JHtml::_('FEFHelper.select.option', null, JText::_($params['none']));

			if ($params['none_as_zero'])
			{
				$options[] = JHtml::_('FEFHelper.select.option', 0, JText::_($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 = JText::_($value);
				}

				$options[] = JHtml::_('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.");
	}

	/**
	 * 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.");
	}

}
FEFHelper/browse.php000064400000070516152354067330010337 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

use FOF30\Model\DataModel;
use FOF30\Utils\ArrayHelper;
use FOF30\Utils\FEFHelper\BrowseView;
use FOF30\View\DataView\DataViewInterface;
use FOF30\View\DataView\Raw as DataViewRaw;

defined('_JEXEC') or die;

/**
 * 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 ? JText::_($title) : $title;
			$title = JHtml::_('tooltipText', $title, '', 0);
		}

		if ($enabled)
		{
			$btnColor = 'grey';

			if (substr($active_class, 0, 2) == '--')
			{
				list($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 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) == '--')
			{
				list($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 = JFactory::getDbo()->getNullDate();
			$nowDate  = JFactory::getDate()->toUnix();

			$tz = JFactory::getUser()->getTimezone();

			$publish_up   = ($publish_up != $nullDate) ? JFactory::getDate($publish_up, 'UTC')->setTimeZone($tz) : false;
			$publish_down = ($publish_down != $nullDate) ? JFactory::getDate($publish_down, 'UTC')->setTimeZone($tz) : false;

			// Create tip text, only we have publish up or down settings
			$tips = [];

			if ($publish_up)
			{
				$tips[] = JText::sprintf('JLIB_HTML_PUBLISHED_START', JHtml::_('date', $publish_up, JText::_('DATE_FORMAT_LC5'), 'UTC'));
			}

			if ($publish_down)
			{
				$tips[] = JText::sprintf('JLIB_HTML_PUBLISHED_FINISHED', JHtml::_('date', $publish_down, JText::_('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 ($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 ($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] = JText::_($states[$key][1]);
					$states[$key][2] = JText::_($states[$key][2]) . '<br />' . $tip;
					$states[$key][3] = JText::_($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')
	{
		JHtml::_('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 />' . JHtml::_('date', $time, JText::_('DATE_FORMAT_LC')) . '<br />' . JHtml::_('date', $time, 'H:i');
		$active_title   = JHtml::_('tooltipText', JText::_('JLIB_HTML_CHECKIN'), $text, 0);
		$inactive_title = JHtml::_('tooltipText', JText::_('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 \FOF30\View\DataView\Html $view */
		if (is_null($view))
		{
			$view = BrowseView::getViewFromBacktrace();
		}
		$dndOrderingActive = $view->getLists()->order == $orderingField;

		// Default inactive ordering
		$html = '<span class="sortable-handler inactive" >';
		$html .= '<span class="' . $icon . '"></span>';
		$html .= '</span>';

		// The modern drag'n'drop method
		if ($view->getPerms()->editstate)
		{
			$disableClassName = '';
			$disabledLabel    = '';

			// DO NOT REMOVE! It will initialize Joomla libraries and javascript functions
			$hasAjaxOrderingSupport = $view->hasAjaxOrderingSupport();

			if (!$hasAjaxOrderingSupport['saveOrder'])
			{
				$disabledLabel    = JText::_('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 = JText::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN');
		$orderingLabel = JText::_('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)
	{
		JHtml::_('behavior.core');
		JHtml::_('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(JText::_($tip ?: $title)) . '"'
			. ' data-content="' . htmlspecialchars(JText::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN')) . '" data-placement="top">';

		if (isset($title['0']) && $title['0'] === '<')
		{
			$html .= $title;
		}
		else
		{
			$html .= JText::_($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)')
	{
		JHtml::_('behavior.core');
		JHtml::_('bootstrap.tooltip');

		return '<input type="checkbox" name="' . $name . '" value="" class="hasTooltip" title="' . JHtml::_('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
		{
			JFactory::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   JPagination $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 = [], JPagination $pagination = null, $sortBy = null, $order_Dir = null)
	{
		if (is_null($view))
		{
			$view = BrowseView::getViewFromBacktrace();
		}

		if (empty($sortFields))
		{
			/** @var DataModel $model */
			$model      = $view->getModel();
			$sortFields = isset($view->getLists()->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    = JText::_('JFIELD_PLG_SEARCH_SEARCHLIMIT_DESC');
		$orderingDescr = JText::_('JFIELD_ORDERING_DESC');
		$sortByLabel   = JText::_('JGLOBAL_SORT_BY');

		// Order direction dropdown
		$directionSelect = JHtml::_('FEFHelper.select.genericlist', [
			''     => $orderingDescr,
			'asc'  => JText::_('JGLOBAL_ORDER_ASCENDING'),
			'desc' => JText::_('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 = JHtml::_('FEFHelper.select.genericlist', array_merge([
			'' => JText::_('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 = JText::_($langKey);

				if ($fieldLabel !== $langKey)
				{
					break;
				}

				$fieldLabel = '';
			}

			if (!empty($fieldLabel))
			{
				$sortFields[$field] = (new Joomla\Filter\InputFilter())->clean($fieldLabel);
			}

		}

		return $sortFields;
	}
}
InstallScript/Module.php000064400000016700152354067330011311 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils\InstallScript;

use Exception;
use FOF30\Database\Installer;
use JFactory;
use JLoader;

defined('_JEXEC') or die;

JLoader::import('joomla.filesystem.folder');
JLoader::import('joomla.filesystem.file');
JLoader::import('joomla.installer.installer');
JLoader::import('joomla.utilities.date');

// 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   \JInstallerAdapterComponent $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   \JInstallerAdapterComponent $parent Parent object
	 *
     * @throws Exception
	 *
	 * @return  void
	 */
	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(JFactory::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   \JInstallerAdapterComponent $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(JFactory::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   \JInstallerAdapterComponent $parent
	 */
	protected function bugfixFilesNotCopiedOnUpdate($parent)
	{
		\JLog::add("Joomla! extension update workaround for $this->moduleClient module $this->moduleName", \JLog::INFO, 'fof3_extension_installation');

		$temporarySource = $parent->getParent()->getPath('source');
		$rootFolder      = ($this->moduleClient == 'site') ? JPATH_SITE : JPATH_ADMINISTRATOR;

		$copyMap = array(
			// 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)
		{
			\JLog::add(__CLASS__ . ":: Conditional copy $source to $target", \JLog::DEBUG, 'fof3_extension_installation');

			$ignored = array();

			if ($source == $temporarySource)
			{
				$ignored = array(
					'index.html',  'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md',
					'script.php', 'language', 'media',
				);

			}

			$this->recursiveConditionalCopy($source, $target, $ignored);
		}
	}

	/**
	 * Get the extension name for FOF dependency tracking, e.g. mod_site_foobar
	 *
	 * @return  string
	 */
	protected function getDependencyName()
	{
		return 'mod_' . strtolower($this->moduleClient) . '_' . substr($this->moduleName, 4);
	}
}
InstallScript/BaseInstaller.php000064400000050247152354067330012620 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils\InstallScript;

use DirectoryIterator;
use Exception;
use JFactory;
use JFile;
use JFolder;
use JLoader;
use JLog;

defined('_JEXEC') or die;

JLoader::import('joomla.filesystem.folder');
JLoader::import('joomla.filesystem.file');
JLoader::import('joomla.installer.installer');
JLoader::import('joomla.utilities.date');

class BaseInstaller
{
	/**
	 * The minimum PHP version required to install this extension
	 *
	 * @var   string
	 */
	protected $minimumPHPVersion = '5.4.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 = array();

	/**
	 * 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 = array())
	{
		// Make sure source and destination exist
		if (!@is_dir($source))
		{
			return;
		}

		if (!@is_dir($dest))
		{
			if (!@mkdir($dest, 0755))
			{
				JFolder::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 (!JFile::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 ? JLog::ERROR : JLog::WARNING;

		try
		{
			JLog::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 = JFactory::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 = array();
			}
		}
		catch (Exception $e)
		{
			$dependencies = array();
		}

		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 = JFactory::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) array(
			'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. @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.
	 *
	 * @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
	 *
	 * @param array $options See description
	 *
	 * @return  void
	 *
	 * @throws Exception
	 */
	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 = array(
			'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 (array('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'], array('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          = JFactory::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    = JFactory::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    = JFactory::getDbo();
		$query = $db->getQuery(true);
		$query->select('extension_id')
			->from('#__extensions')
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('element') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);

		try
		{
			$ids = $db->loadColumn();
		}
		catch (Exception $exc)
		{
			return;
		}

		if (empty($ids))
		{
			return;
		}

		$extension_id = array_shift($ids);

		$query = $db->getQuery(true)
			->delete($db->qn('#__postinstall_messages'))
			->where($db->qn('extension_id') . ' = ' . $db->q($extension_id));

		try
		{
			$db->setQuery($query)->execute();
		}
		catch (Exception $e)
		{
			return;
		}
	}
}
InstallScript/Plugin.php000064400000017371152354067330011327 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils\InstallScript;

use Exception;
use FOF30\Database\Installer;
use JFactory;
use JLoader;

defined('_JEXEC') or die;

JLoader::import('joomla.filesystem.folder');
JLoader::import('joomla.filesystem.file');
JLoader::import('joomla.installer.installer');
JLoader::import('joomla.utilities.date');

// 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   \JInstallerAdapterComponent $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   \JInstallerAdapterComponent $parent Parent object
	 *
     * @throws Exception
	 *
	 * @return  void
	 */
	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(JFactory::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   \JInstallerAdapterComponent $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(JFactory::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   \JInstallerAdapterComponent $parent
	 */
	protected function bugfixFilesNotCopiedOnUpdate($parent)
	{
		\JLog::add("Joomla! extension update workaround for $this->pluginFolder plugin $this->pluginName", \JLog::INFO, 'fof3_extension_installation');

		$temporarySource = $parent->getParent()->getPath('source');

		$copyMap = array(
			// 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)
		{
			\JLog::add(__CLASS__ . ":: Conditional copy $source to $target", \JLog::DEBUG, 'fof3_extension_installation');

			$ignored = array();

			if ($source == $temporarySource)
			{
				$ignored = array(
					'index.html',  'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md',
					'script.php', 'language', 'media',
				);
			}

			$this->recursiveConditionalCopy($source, $target, $ignored);
		}
	}

	/**
	 * Get the extension name for FOF dependency tracking, e.g. plg_system_foobar
	 *
	 * @return  string
	 */
	protected function getDependencyName()
	{
		return 'plg_' . strtolower($this->pluginFolder) . '_' . $this->pluginName;
	}
}
InstallScript/Component.php000064400000104514152354067330012027 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Utils\InstallScript;

use FOF30\Database\Installer;
use Exception;
use JFactory;
use JFile;
use JFolder;
use JInstaller;
use JLoader;

defined('_JEXEC') or die;

JLoader::import('joomla.filesystem.folder');
JLoader::import('joomla.filesystem.file');
JLoader::import('joomla.installer.installer');
JLoader::import('joomla.utilities.date');

// In case FOF's autoloader is not present yet, e.g. new installation
if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true))
{
	require_once __DIR__ . '/BaseInstaller.php';
}

/**
 * A helper class which you can use to create component installation scripts.
 *
 * Example usage: class Com_ExampleInstallerScript extends FOF30\Utils\InstallScript\Component
 *
 * This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
 * Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
 */
class Component extends BaseInstaller
{
	/**
	 * The component's name. Auto-filled from the class name.
	 *
	 * @var   string
	 */
	protected $componentName = '';

	/**
	 * The title of the component (printed on installation and uninstallation messages)
	 *
	 * @var string
	 */
	protected $componentTitle = 'Foobar Component';

	/**
	 * The list of obsolete extra modules and plugins to uninstall on component upgrade / installation.
	 *
	 * @var array
	 */
	protected $uninstallation_queue = array(
		// modules => { (folder) => { (module) }* }*
		'modules' => array(
			'admin' => array(),
			'site'  => array(),
		),
		// plugins => { (folder) => { (element) }* }*
		'plugins' => array(
			'system' => array(),
		),
	);

	/**
	 * Obsolete files and folders to remove from the free version only. This is used when you move a feature from the
	 * free version of your extension to its paid version. If you don't have such a distinction you can ignore this.
	 *
	 * @var   array
	 */
	protected $removeFilesFree = array(
		'files'   => array(
			// Use pathnames relative to your site's root, e.g.
			// 'administrator/components/com_foobar/helpers/whatever.php'
		),
		'folders' => array(
			// Use pathnames relative to your site's root, e.g.
			// 'administrator/components/com_foobar/baz'
		),
	);

	/**
	 * Obsolete files and folders to remove from both paid and free releases. This is used when you refactor code and
	 * some files inevitably become obsolete and need to be removed.
	 *
	 * @var   array
	 */
	protected $removeFilesAllVersions = array(
		'files'   => array(
			// Use pathnames relative to your site's root, e.g.
			// 'administrator/components/com_foobar/helpers/whatever.php'
		),
		'folders' => array(
			// Use pathnames relative to your site's root, e.g.
			// 'administrator/components/com_foobar/baz'
		),
	);

	/**
	 * A list of scripts to be copied to the "cli" directory of the site
	 *
	 * @var   array
	 */
	protected $cliScriptFiles = array(
		// Use just the filename, e.g.
		// 'my-cron-script.php'
	);

	/**
	 * The path inside your package where cli scripts are stored
	 *
	 * @var   string
	 */
	protected $cliSourcePath = 'cli';

	/**
	 * Is the schemaXmlPath class variable a relative path? If set to true the schemaXmlPath variable contains a path
	 * relative to the component's back-end directory. If set to false the schemaXmlPath variable contains an absolute
	 * filesystem path.
	 *
	 * @var   boolean
	 */
	protected $schemaXmlPathRelative = true;

	/**
	 * The path where the schema XML files are stored. Its contents depend on the schemaXmlPathRelative variable above
	 * true        => schemaXmlPath contains a path relative to the component's back-end directory
	 * false    => schemaXmlPath contains an absolute filesystem path
	 *
	 * @var string
	 */
	protected $schemaXmlPath = 'sql/xml';

	/**
	 * Is this the paid version of the extension? This only determines which files / extensions will be removed.
	 *
	 * @var   boolean
	 */
	protected $isPaid = false;

	/**
	 * Module installer script constructor.
	 */
	public function __construct()
	{
		// Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary.
		if (empty($this->componentName))
		{
			$class              = get_class($this);
			$words              = preg_replace('/(\s)+/', '_', $class);
			$words              = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words));
			$classParts         = explode('_', $words);

			$this->componentName = 'com_' . $classParts[2];
		}
	}

	/**
	 * Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to
	 * tell Joomla! if it should abort the installation.
	 *
	 * @param   string                      $type   Installation type (install, update, discover_install)
	 * @param   \JInstallerAdapterComponent $parent Parent object
	 *
	 * @return  boolean  True to let the installation proceed, false to halt the installation
	 */
	public function preflight($type, $parent)
	{
		// Check the minimum PHP version
		if (!$this->checkPHPVersion())
		{
			return false;
		}

		// Check the minimum Joomla! version
		if (!$this->checkJoomlaVersion())
		{
			return false;
		}

		// Clear op-code caches to prevent any cached code issues
		$this->clearOpcodeCaches();

		// Workarounds for JInstaller issues.
		if (in_array($type, array('install', 'discover_install')))
		{
			// Bugfix for "Database function returned no error"
			$this->bugfixDBFunctionReturnedNoError();
		}
		else
		{
			// Bugfix for "Can not build admin menus"
			$this->bugfixCantBuildAdminMenus();
		}

		return true;
	}

	/**
	 * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
	 * or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
	 * database updates and similar housekeeping functions.
	 *
	 * @param   string                      $type   install, update or discover_update
	 * @param   \JInstallerAdapterComponent $parent Parent object
	 *
	 * @throws Exception
	 *
	 * @return  void
	 */
	public function postflight($type, $parent)
	{
		// Add ourselves to the list of extensions depending on FOF30
		$this->addDependency('fof30', $this->componentName);

		// Install or update database
		$dbInstaller = new Installer(JFactory::getDbo(),
			($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' .
			$this->schemaXmlPath
		);
		$dbInstaller->updateSchema();

		// These workarounds are only needed, and only work, on Joomla! 3.x
		if (strpos(JVERSION, '3.') === 0)
		{
			// Make sure menu items are installed
			$this->_createAdminMenus($parent);

			// Make sure menu items are published
			$this->_reallyPublishAdminMenuItems($parent);
		}

		// Which files should I remove?
		if ($this->isPaid)
		{
			// This is the paid version, only remove the removeFilesAllVersions files
			$removeFiles = $this->removeFilesAllVersions;
		}
		else
		{
			// This is the free version, remove the removeFilesAllVersions and removeFilesFree files
			$removeFiles = array('files' => array(), 'folders' => array());

			if (isset($this->removeFilesAllVersions['files']))
			{
				if (isset($this->removeFilesFree['files']))
				{
					$removeFiles['files'] = array_merge($this->removeFilesAllVersions['files'], $this->removeFilesFree['files']);
				}
				else
				{
					$removeFiles['files'] = $this->removeFilesAllVersions['files'];
				}
			}
			elseif (isset($this->removeFilesFree['files']))
			{
				$removeFiles['files'] = $this->removeFilesFree['files'];
			}

			if (isset($this->removeFilesAllVersions['folders']))
			{
				if (isset($this->removeFilesFree['folders']))
				{
					$removeFiles['folders'] = array_merge($this->removeFilesAllVersions['folders'], $this->removeFilesFree['folders']);
				}
				else
				{
					$removeFiles['folders'] = $this->removeFilesAllVersions['folders'];
				}
			}
			elseif (isset($this->removeFilesFree['folders']))
			{
				$removeFiles['folders'] = $this->removeFilesFree['folders'];
			}
		}

		// Remove obsolete files and folders
		$this->removeFilesAndFolders($removeFiles);

		// Make sure everything is copied properly
		$this->bugfixFilesNotCopiedOnUpdate($parent);

		// Copy the CLI files (if any)
		$this->copyCliFiles($parent);

		// Show the post-installation page
		$this->renderPostInstallation($parent);

		// Uninstall obsolete subextensions
		$this->uninstallObsoleteSubextensions($parent);

		// Clear the FOF cache
		$false = false;
		$cache = \JFactory::getCache('fof', '');
		$cache->store($false, 'cache', 'fof');

		// Make sure the Joomla! menu structure is correct
		$this->_rebuildMenu();

		// Add post-installation messages on Joomla! 3.2 and later
		$this->_applyPostInstallationMessages();

		// Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded.
		$this->clearOpcodeCaches();
	}

	/**
	 * Runs on uninstallation
	 *
	 * @param   \JInstallerAdapterComponent $parent The parent object
	 */
	public function uninstall($parent)
	{
		// Uninstall database
		$dbInstaller = new Installer(JFactory::getDbo(),
			($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' .
			$this->schemaXmlPath
		);

		$dbInstaller->removeSchema();

		// Uninstall post-installation messages on Joomla! 3.2 and later
		$this->uninstallPostInstallationMessages();

		// Remove ourselves from the list of extensions depending on FOF30
		$this->removeDependency('fof30', $this->componentName);

		// Show the post-uninstallation page
		$this->renderPostUninstallation($parent);
	}

	/**
	 * Copies the CLI scripts into Joomla!'s cli directory
	 *
	 * @param   \JInstallerAdapterComponent $parent
	 */
	protected function copyCliFiles($parent)
	{
		$src = $parent->getParent()->getPath('source');

		foreach ($this->cliScriptFiles as $script)
		{
			if (is_file(JPATH_ROOT . '/cli/' . $script))
			{
				JFile::delete(JPATH_ROOT . '/cli/' . $script);
			}

			if (is_file($src . '/' . $this->cliSourcePath . '/' . $script))
			{
				JFile::copy($src . '/' . $this->cliSourcePath . '/' . $script, JPATH_ROOT . '/cli/' . $script);
			}
		}
	}

	/**
	 * Fix for Joomla bug: sometimes files are not copied on update.
	 *
	 * We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files /
	 * folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more
	 * added / modified files and folders you have. We are trying to work around it by retrying the copy operation
	 * ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's
	 * extensions.
	 *
	 * @param   \JInstallerAdapterComponent $parent
	 */
	protected function bugfixFilesNotCopiedOnUpdate($parent)
	{
		\JLog::add("Joomla! extension update workaround for component $this->componentName", \JLog::INFO, 'fof3_extension_installation');

		$temporarySource = $parent->getParent()->getPath('source');

		$copyMap = array(
			// Backend component files
			'backend'           => JPATH_ADMINISTRATOR . '/components/' . $this->componentName,
			'admin'             => JPATH_ADMINISTRATOR . '/components/' . $this->componentName,
			// Frontend component files
			'frontend'          => JPATH_SITE . '/components/' . $this->componentName,
			'site'              => JPATH_SITE . '/components/' . $this->componentName,
			// Backend language
			'language/backend'  => JPATH_ADMINISTRATOR . '/language',
			'language/admin'    => JPATH_ADMINISTRATOR . '/language',
			// Frontend language
			'language/frontend' => JPATH_SITE . '/language',
			'language/site'     => JPATH_SITE . '/language',
			// Media files
			'media'             => JPATH_ROOT . '/media/' . $this->componentName,
		);

		foreach ($copyMap as $partialSource => $target)
		{
			$source = $temporarySource . '/' . $partialSource;

			\JLog::add(__CLASS__ . ":: Conditional copy $source to $target", \JLog::DEBUG, 'fof3_extension_installation');

			$this->recursiveConditionalCopy($source, $target);
		}
	}

	/**
	 * Override this method to display a custom component installation message if you so wish
	 *
	 * @param  \JInstallerAdapterComponent $parent Parent class calling us
	 */
	protected function renderPostInstallation($parent)
	{
		echo "<h3>$this->componentName has been installed</h3>";
	}

	/**
	 * Override this method to display a custom component uninstallation message if you so wish
	 *
	 * @param  \JInstallerAdapterComponent $parent Parent class calling us
	 */
	protected function renderPostUninstallation($parent)
	{
		echo "<h3>$this->componentName has been uninstalled</h3>";
	}

	/**
	 * Bugfix for "DB function returned no error"
	 */
	protected function bugfixDBFunctionReturnedNoError()
	{
		$db = JFactory::getDbo();

		try
		{
			// Fix broken #__assets records
			$this->deleteComponentAssetRecords($db);

			// Fix broken #__extensions records
			$this->deleteComponentExtensionRecord($db);

			/**
			 * Fix broken #__menu records
			 *
			 * Only run on Joomla! versions lower than 3.7. Joomla! 3.7 introduced a backend menu manager which
			 * lets the user create missing menu items. Moreover, it lets them create custom links to the component
			 * which means that our menu deleting code would break them! So we don't run this code in newer Joomla!
			 * versions any more.
			 */
			if (version_compare(JVERSION, '3.6.9999', 'le'))
			{
				$this->deleteComponentMenuRecord($db);
			}
		}
		catch (Exception $exc)
		{
			return;
		}
	}

	/**
	 * Joomla! 1.6+ bugfix for "Can not build admin menus"
	 */
	protected function bugfixCantBuildAdminMenus()
	{
		$db = JFactory::getDbo();

		// If there are multiple #__extensions record, keep one of them
		$query = $db->getQuery(true);
		$query->select('extension_id')
			->from('#__extensions')
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('element') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);

		try
		{
			$ids = $db->loadColumn();
		}
		catch (Exception $exc)
		{
			return;
		}


		if (count($ids) > 1)
		{
			asort($ids);
			$extension_id = array_shift($ids); // Keep the oldest id

			foreach ($ids as $id)
			{
				$query = $db->getQuery(true);
				$query->delete('#__extensions')
					->where($db->qn('extension_id') . ' = ' . $db->q($id));
				$db->setQuery($query);

				try
				{
					$db->execute();
				}
				catch (Exception $exc)
				{
					// Nothing
				}
			}
		}

		// If there are multiple assets records, delete all except the oldest one
		$query = $db->getQuery(true);
		$query->select('id')
			->from('#__assets')
			->where($db->qn('name') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);
		$ids = $db->loadObjectList();

		if (count($ids) > 1)
		{
			asort($ids);
			$asset_id = array_shift($ids); // Keep the oldest id

			foreach ($ids as $id)
			{
				$query = $db->getQuery(true);
				$query->delete('#__assets')
					->where($db->qn('id') . ' = ' . $db->q($id));
				$db->setQuery($query);

				try
				{
					$db->execute();
				}
				catch (Exception $exc)
				{
					// Nothing
				}
			}
		}

		// Remove #__menu records for good measure! –– I think this is not necessary and causes the menu item to
		// disappear on extension update.
		/**
		 * $query = $db->getQuery(true);
		 * $query->select('id')
		 * ->from('#__menu')
		 * ->where($db->qn('type') . ' = ' . $db->q('component'))
		 * ->where($db->qn('menutype') . ' = ' . $db->q('main'))
		 * ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName));
		 * $db->setQuery($query);
		 *
		 * try
		 * {
		 * $ids1 = $db->loadColumn();
		 * }
		 * catch (Exception $exc)
		 * {
		 * $ids1 = array();
		 * }
		 *
		 * if (empty($ids1))
		 * {
		 * $ids1 = array();
		 * }
		 *
		 * $query = $db->getQuery(true);
		 * $query->select('id')
		 * ->from('#__menu')
		 * ->where($db->qn('type') . ' = ' . $db->q('component'))
		 * ->where($db->qn('menutype') . ' = ' . $db->q('main'))
		 * ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName . '&%'));
		 * $db->setQuery($query);
		 *
		 * try
		 * {
		 * $ids2 = $db->loadColumn();
		 * }
		 * catch (Exception $exc)
		 * {
		 * $ids2 = array();
		 * }
		 *
		 * if (empty($ids2))
		 * {
		 * $ids2 = array();
		 * }
		 *
		 * $ids = array_merge($ids1, $ids2);
		 *
		 * if (!empty($ids))
		 * {
		 * foreach ($ids as $id)
		 * {
		 * $query = $db->getQuery(true);
		 * $query->delete('#__menu')
		 * ->where($db->qn('id') . ' = ' . $db->q($id));
		 * $db->setQuery($query);
		 *
		 * try
		 * {
		 * $db->execute();
		 * }
		 * catch (Exception $exc)
		 * {
		 * // Nothing
		 * }
		 * }
		 * }
		 * /**/
	}

	/**
	 * Removes obsolete files and folders
	 *
	 * @param   array $removeList The files and directories to remove
	 */
	protected function removeFilesAndFolders($removeList)
	{
		// Remove files
		if (isset($removeList['files']) && !empty($removeList['files']))
		{
			foreach ($removeList['files'] as $file)
			{
				$f = JPATH_ROOT . '/' . $file;

				if (!is_file($f))
				{
					continue;
				}

				JFile::delete($f);
			}
		}

		// Remove folders
		if (isset($removeList['folders']) && !empty($removeList['folders']))
		{
			foreach ($removeList['folders'] as $folder)
			{
				$f = JPATH_ROOT . '/' . $folder;

				if (!is_dir($f))
				{
					continue;
				}

				JFolder::delete($f);
			}
		}
	}

	/**
	 * Uninstalls obsolete subextensions (modules, plugins) bundled with the main extension
	 *
	 * @param   \JInstallerAdapterComponent $parent The parent object
	 *
	 * @return  \stdClass The subextension uninstallation status
	 */
	protected function uninstallObsoleteSubextensions($parent)
	{
		JLoader::import('joomla.installer.installer');

		$db = JFactory::getDBO();

		$status          = new \stdClass();
		$status->modules = array();
		$status->plugins = array();

		// Modules uninstallation
		if (isset($this->uninstallation_queue['modules']) && count($this->uninstallation_queue['modules']))
		{
			foreach ($this->uninstallation_queue['modules'] as $folder => $modules)
			{
				if (count($modules))
				{
					foreach ($modules as $module)
					{
						// Find the module ID
						$sql = $db->getQuery(true)
							->select($db->qn('extension_id'))
							->from($db->qn('#__extensions'))
							->where($db->qn('element') . ' = ' . $db->q('mod_' . $module))
							->where($db->qn('type') . ' = ' . $db->q('module'));
						$db->setQuery($sql);
						$id = $db->loadResult();
						// Uninstall the module
						if ($id)
						{
							$installer         = new JInstaller;
							$result            = $installer->uninstall('module', $id, 1);
							$status->modules[] = array(
								'name'   => 'mod_' . $module,
								'client' => $folder,
								'result' => $result,
							);
						}
					}
				}
			}
		}

		// Plugins uninstallation
		if (isset($this->uninstallation_queue['plugins']) && count($this->uninstallation_queue['plugins']))
		{
			foreach ($this->uninstallation_queue['plugins'] as $folder => $plugins)
			{
				if (count($plugins))
				{
					foreach ($plugins as $plugin)
					{
						$sql = $db->getQuery(true)
							->select($db->qn('extension_id'))
							->from($db->qn('#__extensions'))
							->where($db->qn('type') . ' = ' . $db->q('plugin'))
							->where($db->qn('element') . ' = ' . $db->q($plugin))
							->where($db->qn('folder') . ' = ' . $db->q($folder));
						$db->setQuery($sql);

						$id = $db->loadResult();
						if ($id)
						{
							$installer         = new JInstaller;
							$result            = $installer->uninstall('plugin', $id, 1);
							$status->plugins[] = array(
								'name'   => 'plg_' . $plugin,
								'group'  => $folder,
								'result' => $result,
							);
						}
					}
				}
			}
		}

		return $status;
	}

	/**
	 * @param \JInstallerAdapterComponent $parent
	 *
	 * @return bool
	 *
	 * @throws Exception When the Joomla! menu is FUBAR
	 */
	private function _createAdminMenus($parent)
	{
		$db = $parent->getParent()->getDbo();
		/** @var \JTableMenu $table */
		$table  = \JTable::getInstance('menu');
		$option = $parent->get('element');

		// If a component exists with this option in the table then we don't need to add menus
		$query = $db->getQuery(true)
			->select('COUNT(*)')
			->from($db->qn('#__menu') . ' AS ' . $db->qn('m'))
			->leftJoin($db->qn('#__extensions', 'e') . ' ON ' .
				$db->qn('m.component_id') . ' = ' . $db->qn('e.extension_id'))
			->where($db->qn('m.parent_id') . ' = ' . $db->q(1))
			->where($db->qn('m.client_id') . ' = ' . $db->q(1))
			->where($db->qn('e.type') . ' = ' . $db->q('component'))
			->where($db->qn('e.element') . ' = ' . $db->q($option));

		$db->setQuery($query);

		$existingMenus = $db->loadResult();

		if ($existingMenus)
		{
			return true;
		}

		// Let's find the extension id
		$query->clear()
			->select($db->qn('e.extension_id'))
			->from($db->qn('#__extensions', 'e'))
			->where($db->qn('e.type') . ' = ' . $db->q('component'))
			->where($db->qn('e.element') . ' = ' . $db->q($option));
		$db->setQuery($query);
		$componentId = $db->loadResult();

		// Ok, now its time to handle the menus.  Start with the component root menu, then handle submenus.
		if (method_exists($parent, 'getManifest'))
		{
			$menuElement = $parent->getManifest()->administration->menu;
		}
		else
		{
			$menuElement = $parent->get('manifest')->administration->menu;
		}

		// We need to insert the menu item as the last child of Joomla!'s menu root node. First let's make sure that
		// it exists. Normally it should be the menu item with ID = 1.
		$query      = $db->getQuery(true)
			->select($db->qn('id'))
			->from($db->qn('#__menu'))
			->where($db->qn('id') . ' = ' . $db->q(1));
		$rootItemId = $db->setQuery($query)->loadResult();

		// If we didn't find the item with ID=1 something has screwed up the menu table, e.g. a bad upgrade script. In
		// this case we can try to find the root node by title.
		if (is_null($rootItemId))
		{
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->where($db->qn('title') . ' = ' . $db->q('Menu_Item_Root'));
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		// So, someone changed the title of the menu item too?! Let's find it by alias.
		if (is_null($rootItemId))
		{
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->where($db->qn('alias') . ' = ' . $db->q('root'));
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		// For crying out loud, they changed the alias too? Fine! Find it by component ID.
		if (is_null($rootItemId))
		{
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->where($db->qn('component_id') . ' = ' . $db->q('0'));
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		// Um, OK. Still no go. Let's try with minimum lft value.
		if (is_null($rootItemId))
		{
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->order($db->qn('lft') . ' ASC');
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		// I quit. Your site's menu structure is broken. I'll just throw an error.
		if (is_null($rootItemId))
		{
			throw new Exception("Your site is broken. There is no root menu item. As a result it is impossible to create menu items. The installation of this component has failed. Please fix your database and retry!", 500);
		}

		/** @var \SimpleXMLElement $menuElement */
		if ($menuElement)
		{
			$data                 = array();
			$data['menutype']     = 'main';
			$data['client_id']    = 1;
			$data['title']        = (string) trim($menuElement);
			$data['alias']        = (string) $menuElement;
			$data['link']         = 'index.php?option=' . $option;
			$data['type']         = 'component';
			$data['published']    = 0;
			$data['parent_id']    = 1;
			$data['component_id'] = $componentId;
			$data['img']          = ((string) $menuElement->attributes()->img) ? (string) $menuElement->attributes()->img : 'class:component';
			$data['home']         = 0;
			$data['path']         = '';
			$data['params']       = '';
		}
		// No menu element was specified, Let's make a generic menu item
		else
		{
			$data                 = array();
			$data['menutype']     = 'main';
			$data['client_id']    = 1;
			$data['title']        = $option;
			$data['alias']        = $option;
			$data['link']         = 'index.php?option=' . $option;
			$data['type']         = 'component';
			$data['published']    = 0;
			$data['parent_id']    = 1;
			$data['component_id'] = $componentId;
			$data['img']          = 'class:component';
			$data['home']         = 0;
			$data['path']         = '';
			$data['params']       = '';
		}

		try
		{
			$table->setLocation($rootItemId, 'last-child');
		}
		catch (\InvalidArgumentException $e)
		{
			$this->log($e->getMessage());

			return false;
		}

		if (!$table->bind($data) || !$table->check() || !$table->store())
		{
			// The menu item already exists. Delete it and retry instead of throwing an error.
			$query->clear()
				->select('id')
				->from('#__menu')
				->where('menutype = ' . $db->quote('main'))
				->where('client_id = 1')
				->where('link = ' . $db->quote('index.php?option=' . $option))
				->where('type = ' . $db->quote('component'))
				->where('parent_id = 1')
				->where('home = 0');

			$db->setQuery($query);
			$menu_ids_level1 = $db->loadColumn();

			if (empty($menu_ids_level1))
			{
				// Oops! Could not get the menu ID. Go back and rollback changes.
				\JError::raiseWarning(1, $table->getError());

				return false;
			}
			else
			{
				$ids = implode(',', $menu_ids_level1);

				$query->clear()
					->select('id')
					->from('#__menu')
					->where('menutype = ' . $db->quote('main'))
					->where('client_id = 1')
					->where('type = ' . $db->quote('component'))
					->where('parent_id in (' . $ids . ')')
					->where('level = 2')
					->where('home = 0');

				$db->setQuery($query);
				$menu_ids_level2 = $db->loadColumn();

				$ids = implode(',', array_merge($menu_ids_level1, $menu_ids_level2));

				// Remove the old menu item
				$query->clear()
					->delete('#__menu')
					->where('id in (' . $ids . ')');

				$db->setQuery($query);
				$db->execute();

				// Retry creating the menu item
				$table->setLocation($rootItemId, 'last-child');

				if (!$table->bind($data) || !$table->check() || !$table->store())
				{
					// Install failed, warn user and rollback changes
					\JError::raiseWarning(1, $table->getError());

					return false;
				}
			}
		}

		/*
		 * Since we have created a menu item, we add it to the installation step stack
		 * so that if we have to rollback the changes we can undo it.
		 */
		$parent->getParent()->pushStep(array('type' => 'menu', 'id' => $componentId));

		/*
		 * Process SubMenus
		 */

		if (method_exists($parent, 'getManifest'))
		{
			$submenu = $parent->getManifest()->administration->submenu;
		}
		else
		{
			$submenu = $parent->get('manifest')->administration->submenu;
		}

		if (!$submenu)
		{
			return true;
		}

		$parent_id = $table->id;

		/** @var \SimpleXMLElement $child */
		foreach ($submenu->menu as $child)
		{
			$data                 = array();
			$data['menutype']     = 'main';
			$data['client_id']    = 1;
			$data['title']        = (string) trim($child);
			$data['alias']        = (string) $child;
			$data['type']         = 'component';
			$data['published']    = 0;
			$data['parent_id']    = $parent_id;
			$data['component_id'] = $componentId;
			$data['img']          = ((string) $child->attributes()->img) ? (string) $child->attributes()->img : 'class:component';
			$data['home']         = 0;

			// Set the sub menu link
			if ((string) $child->attributes()->link)
			{
				$data['link'] = 'index.php?' . $child->attributes()->link;
			}
			else
			{
				$request = array();

				if ((string) $child->attributes()->act)
				{
					$request[] = 'act=' . $child->attributes()->act;
				}

				if ((string) $child->attributes()->task)
				{
					$request[] = 'task=' . $child->attributes()->task;
				}

				if ((string) $child->attributes()->controller)
				{
					$request[] = 'controller=' . $child->attributes()->controller;
				}

				if ((string) $child->attributes()->view)
				{
					$request[] = 'view=' . $child->attributes()->view;
				}

				if ((string) $child->attributes()->layout)
				{
					$request[] = 'layout=' . $child->attributes()->layout;
				}

				if ((string) $child->attributes()->sub)
				{
					$request[] = 'sub=' . $child->attributes()->sub;
				}

				$qstring      = (count($request)) ? '&' . implode('&', $request) : '';
				$data['link'] = 'index.php?option=' . $option . $qstring;
			}

			$table = \JTable::getInstance('menu');

			try
			{
				$table->setLocation($parent_id, 'last-child');
			}
			catch (\InvalidArgumentException $e)
			{
				return false;
			}

			if (!$table->bind($data) || !$table->check() || !$table->store())
			{
				// Install failed, rollback changes
				return false;
			}

			/*
			 * Since we have created a menu item, we add it to the installation step stack
			 * so that if we have to rollback the changes we can undo it.
			 */
			$parent->getParent()->pushStep(array('type' => 'menu', 'id' => $componentId));
		}

		return true;
	}

	/**
	 * Make sure the Component menu items are really published!
	 *
	 * @param \JInstallerAdapterComponent $parent
	 *
	 * @return bool
	 */
	private function _reallyPublishAdminMenuItems($parent)
	{
		$db     = $parent->getParent()->getDbo();
		$option = $parent->get('element');

		$query = $db->getQuery(true)
			->update('#__menu AS m')
			->join('LEFT', '#__extensions AS e ON m.component_id = e.extension_id')
			->set($db->qn('published') . ' = ' . $db->q(1))
			->where('m.parent_id = 1')
			->where('m.client_id = 1')
			->where('e.type = ' . $db->quote('component'))
			->where('e.element = ' . $db->quote($option));

		$db->setQuery($query);

		try
		{
			$db->execute();
		}
		catch (Exception $e)
		{
			// If it fails, it fails. Who cares.
		}
	}

	/**
	 * Tells Joomla! to rebuild its menu structure to make triple-sure that the Components menu items really do exist
	 * in the correct place and can really be rendered.
	 */
	private function _rebuildMenu()
	{
		/** @var \JTableMenu $table */
		$table = \JTable::getInstance('menu');
		$db    = $table->getDbo();

		// We need to rebuild the menu based on its root item. By default this is the menu item with ID=1. However, some
		// crappy upgrade scripts enjoy screwing it up. Hey, ho, the workaround way I go.
		$query      = $db->getQuery(true)
			->select($db->qn('id'))
			->from($db->qn('#__menu'))
			->where($db->qn('id') . ' = ' . $db->q(1));
		$rootItemId = $db->setQuery($query)->loadResult();

		if (is_null($rootItemId))
		{
			// Guess what? The Problem has happened. Let's find the root node by title.
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->where($db->qn('title') . ' = ' . $db->q('Menu_Item_Root'));
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		if (is_null($rootItemId))
		{
			// Did they change the title too?! Let's find it by alias.
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->where($db->qn('alias') . ' = ' . $db->q('root'));
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		if (is_null($rootItemId))
		{
			// Dude. Dude! Duuuuuuude! The alias is borked, too?! Find it by component ID.
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->where($db->qn('component_id') . ' = ' . $db->q('0'));
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		if (is_null($rootItemId))
		{
			// Your site is more of a "shite" than a "site". Let's try with minimum lft value.
			$rootItemId = null;
			$query      = $db->getQuery(true)
				->select($db->qn('id'))
				->from($db->qn('#__menu'))
				->order($db->qn('lft') . ' ASC');
			$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
		}

		if (is_null($rootItemId))
		{
			// I quit. Your site is broken.
			return false;
		}

		$table->rebuild($rootItemId);
	}

	/**
	 * Deletes the assets table records for the component
	 *
	 * @param   \JDatabaseDriver $db
	 *
	 * @return  void
	 *
	 * @since   3.0.18
	 */
	private function deleteComponentAssetRecords($db)
	{
		$query = $db->getQuery(true);
		$query->select('id')
			->from('#__assets')
			->where($db->qn('name') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);

		$ids = $db->loadColumn();

		if (empty($ids))
		{
			return;
		}

		foreach ($ids as $id)
		{
			$query = $db->getQuery(true);
			$query->delete('#__assets')
				->where($db->qn('id') . ' = ' . $db->q($id));
			$db->setQuery($query);

			try
			{
				$db->execute();
			}
			catch (Exception $exc)
			{
				// Nothing
			}
		}
	}

	/**
	 * Deletes the extensions table records for the component
	 *
	 * @param   \JDatabaseDriver $db
	 *
	 * @return  void
	 *
	 * @since   3.0.18
	 */
	private function deleteComponentExtensionRecord($db)
	{
		$query = $db->getQuery(true);
		$query->select('extension_id')
			->from('#__extensions')
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('element') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);
		$ids = $db->loadColumn();

		if (empty($ids))
		{
			return;
		}

		foreach ($ids as $id)
		{
			$query = $db->getQuery(true);
			$query->delete('#__extensions')
				->where($db->qn('extension_id') . ' = ' . $db->q($id));
			$db->setQuery($query);

			try
			{
				$db->execute();
			}
			catch (Exception $exc)
			{
				// Nothing
			}
		}
	}

	/**
	 * Deletes the menu table records for the component
	 *
	 * @param   \JDatabaseDriver $db
	 *
	 * @return  void
	 *
	 * @since   3.0.18
	 */
	private function deleteComponentMenuRecord($db)
	{
		$query = $db->getQuery(true);
		$query->select('id')
			->from('#__menu')
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('menutype') . ' = ' . $db->q('main'))
			->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName));
		$db->setQuery($query);
		$ids = $db->loadColumn();

		if (empty($ids))
		{
			return;
		}

		foreach ($ids as $id)
		{
			$query = $db->getQuery(true);
			$query->delete('#__menu')
				->where($db->qn('id') . ' = ' . $db->q($id));
			$db->setQuery($query);

			try
			{
				$db->execute();
			}
			catch (Exception $exc)
			{
				// Nothing
			}
		}
	}
}