Your IP : 216.73.216.61


Current Path : /home/w/u/e/wuectly/www/03cbe/
Upload File :
Current File : /home/w/u/e/wuectly/www/03cbe/fof30.tar

Inflector/Inflector.php000060400000031164152455514240011137 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Inflector;

defined('_JEXEC') || die;

/**
 * An Inflector to pluralize and singularize English nouns.
 */
class Inflector
{
	/**
	 * Rules for pluralizing and singularizing of nouns.
	 *
	 * @var array
	 */
	protected $rules = [
		// Pluralization rules. The regex on the left transforms to the regex on the right.
		'pluralization'   => [
			'/move$/i'                      => 'moves',
			'/sex$/i'                       => 'sexes',
			'/child$/i'                     => 'children',
			'/children$/i'                  => 'children',
			'/man$/i'                       => 'men',
			'/men$/i'                       => 'men',
			'/foot$/i'                      => 'feet',
			'/feet$/i'                      => 'feet',
			'/person$/i'                    => 'people',
			'/people$/i'                    => 'people',
			'/taxon$/i'                     => 'taxa',
			'/taxa$/i'                      => 'taxa',
			'/(quiz)$/i'                    => '$1zes',
			'/^(ox)$/i'                     => '$1en',
			'/oxen$/i'                      => 'oxen',
			'/(m|l)ouse$/i'                 => '$1ice',
			'/(m|l)ice$/i'                  => '$1ice',
			'/(matr|vert|ind|suff)ix|ex$/i' => '$1ices',
			'/(x|ch|ss|sh)$/i'              => '$1es',
			'/([^aeiouy]|qu)y$/i'           => '$1ies',
			'/(?:([^f])fe|([lr])f)$/i'      => '$1$2ves',
			'/sis$/i'                       => 'ses',
			'/([ti]|addend)um$/i'           => '$1a',
			'/([ti]|addend)a$/i'            => '$1a',
			'/(alumn|formul)a$/i'           => '$1ae',
			'/(alumn|formul)ae$/i'          => '$1ae',
			'/(buffal|tomat|her)o$/i'       => '$1oes',
			'/(bu)s$/i'                     => '$1ses',
			'/(campu)s$/i'                  => '$1ses',
			'/(alias|status)$/i'            => '$1es',
			'/(octop|vir)us$/i'             => '$1i',
			'/(octop|vir)i$/i'              => '$1i',
			'/(gen)us$/i'                   => '$1era',
			'/(gen)era$/i'                  => '$1era',
			'/(ax|test)is$/i'               => '$1es',
			'/s$/i'                         => 's',
			'/$/'                           => 's',
		],
		// Singularization rules. The regex on the left transforms to the regex on the right.
		'singularization' => [
			'/cookies$/i'                                                      => 'cookie',
			'/moves$/i'                                                        => 'move',
			'/sexes$/i'                                                        => 'sex',
			'/children$/i'                                                     => 'child',
			'/men$/i'                                                          => 'man',
			'/feet$/i'                                                         => 'foot',
			'/people$/i'                                                       => 'person',
			'/taxa$/i'                                                         => 'taxon',
			'/databases$/i'                                                    => 'database',
			'/menus$/i'                                                        => 'menu',
			'/(quiz)zes$/i'                                                    => '\1',
			'/(matr|suff)ices$/i'                                              => '\1ix',
			'/(vert|ind|cod)ices$/i'                                           => '\1ex',
			'/^(ox)en/i'                                                       => '\1',
			'/(alias|status)es$/i'                                             => '\1',
			'/(tomato|hero|buffalo)es$/i'                                      => '\1',
			'/([octop|vir])i$/i'                                               => '\1us',
			'/(gen)era$/i'                                                     => '\1us',
			'/(cris|^ax|test)es$/i'                                            => '\1is',
			'/is$/i'                                                           => 'is',
			'/us$/i'                                                           => 'us',
			'/ias$/i'                                                          => 'ias',
			'/(shoe)s$/i'                                                      => '\1',
			'/(o)es$/i'                                                        => '\1e',
			'/(bus)es$/i'                                                      => '\1',
			'/(campus)es$/i'                                                   => '\1',
			'/([m|l])ice$/i'                                                   => '\1ouse',
			'/(x|ch|ss|sh)es$/i'                                               => '\1',
			'/(m)ovies$/i'                                                     => '\1ovie',
			'/(s)eries$/i'                                                     => '\1eries',
			'/(v)ies$/i'                                                       => '\1ie',
			'/([^aeiouy]|qu)ies$/i'                                            => '\1y',
			'/([lr])ves$/i'                                                    => '\1f',
			'/(tive)s$/i'                                                      => '\1',
			'/(hive)s$/i'                                                      => '\1',
			'/([^f])ves$/i'                                                    => '\1fe',
			'/(^analy)ses$/i'                                                  => '\1sis',
			'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
			'/([ti]|addend)a$/i'                                               => '\1um',
			'/(alumn|formul)ae$/i'                                             => '$1a',
			'/(n)ews$/i'                                                       => '\1ews',
			'/(.*)ss$/i'                                                       => '\1ss',
			'/(.*)s$/i'                                                        => '\1',
		],
		// Uncountable objects are always singular
		'uncountable'     => [
			'aircraft',
			'cannon',
			'deer',
			'equipment',
			'fish',
			'information',
			'money',
			'moose',
			'news',
			'rice',
			'series',
			'sheep',
			'species',
			'swine',
		],
	];

	/**
	 * Cache of pluralized and singularized nouns.
	 *
	 * @var array
	 */
	protected $cache = [
		'singularized' => [],
		'pluralized'   => [],
	];

	public function deleteCache()
	{
		$this->cache['pluralized']   = [];
		$this->cache['singularized'] = [];
	}

	/**
	 * Add a word to the cache, useful to make exceptions or to add words in other languages.
	 *
	 * @param   string  $singular  word.
	 * @param   string  $plural    word.
	 *
	 * @return  void
	 */
	public function addWord($singular, $plural)
	{
		$this->cache['pluralized'][$singular] = $plural;
		$this->cache['singularized'][$plural] = $singular;
	}

	/**
	 * Singular English word to plural.
	 *
	 * @param   string  $word  word to pluralize.
	 *
	 * @return  string Plural noun.
	 */
	public function pluralize($word)
	{
		// Get the cached noun of it exists
		if (isset($this->cache['pluralized'][$word]))
		{
			return $this->cache['pluralized'][$word];
		}

		// Check if the noun is already in plural form, i.e. in the singularized cache
		if (isset($this->cache['singularized'][$word]))
		{
			return $word;
		}

		// Create the plural noun
		if (in_array($word, $this->rules['uncountable']))
		{
			$_cache['pluralized'][$word] = $word;

			return $word;
		}

		foreach ($this->rules['pluralization'] as $regexp => $replacement)
		{
			$matches = null;
			$plural  = preg_replace($regexp, $replacement, $word, -1, $matches);

			if ($matches > 0)
			{
				$_cache['pluralized'][$word] = $plural;

				return $plural;
			}
		}

		return $word;
	}

	/**
	 * Plural English word to singular.
	 *
	 * @param   string  $word  Word to singularize.
	 *
	 * @return  string Singular noun.
	 */
	public function singularize($word)
	{
		// Get the cached noun of it exists
		if (isset($this->cache['singularized'][$word]))
		{
			return $this->cache['singularized'][$word];
		}

		// Check if the noun is already in singular form, i.e. in the pluralized cache
		if (isset($this->cache['pluralized'][$word]))
		{
			return $word;
		}

		// Create the singular noun
		if (in_array($word, $this->rules['uncountable']))
		{
			$_cache['singularized'][$word] = $word;

			return $word;
		}

		foreach ($this->rules['singularization'] as $regexp => $replacement)
		{
			$matches  = null;
			$singular = preg_replace($regexp, $replacement, $word, -1, $matches);

			if ($matches > 0)
			{
				$_cache['singularized'][$word] = $singular;

				return $singular;
			}
		}

		return $word;
	}

	/**
	 * Returns given word as CamelCased.
	 *
	 * Converts a word like "foo_bar" or "foo bar" to "FooBar". It
	 * will remove non alphanumeric characters from the word, so
	 * "who's online" will be converted to "WhoSOnline"
	 *
	 * @param   string  $word  Word to convert to camel case.
	 *
	 * @return  string  UpperCamelCasedWord
	 */
	public function camelize($word)
	{
		$word = preg_replace('/[^a-zA-Z0-9\s]/', ' ', $word);
		$word = str_replace(' ', '', ucwords(strtolower(str_replace('_', ' ', $word))));

		return $word;
	}

	/**
	 * Converts a word "into_it_s_underscored_version"
	 *
	 * Convert any "CamelCased" or "ordinary Word" into an "underscored_word".
	 *
	 * @param   string  $word  Word to underscore
	 *
	 * @return string Underscored word
	 */
	public function underscore($word)
	{
		$word = preg_replace('/(\s)+/', '_', $word);
		$word = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $word));

		return $word;
	}

	/**
	 * Convert any "CamelCased" word into an array of strings
	 *
	 * Returns an array of strings each of which is a substring of string formed
	 * by splitting it at the camelcased letters.
	 *
	 * @param   string  $word  Word to explode
	 *
	 * @return  array   Array of strings
	 */
	public function explode($word)
	{
		$result = explode('_', self::underscore($word));

		return $result;
	}

	/**
	 * Convert  an array of strings into a "CamelCased" word.
	 *
	 * @param   array  $words  Array to implode
	 *
	 * @return  string UpperCamelCasedWord
	 */
	public function implode($words)
	{
		$result = self::camelize(implode('_', $words));

		return $result;
	}

	/**
	 * Returns a human-readable string from $word.
	 *
	 * Returns a human-readable string from $word, by replacing
	 * underscores with a space, and by upper-casing the initial
	 * character by default.
	 *
	 * @param   string  $word  String to "humanize"
	 *
	 * @return string Human-readable word
	 */
	public function humanize($word)
	{
		$result = ucwords(strtolower(str_replace("_", " ", $word)));

		return $result;
	}

	/**
	 * Returns camelBacked version of a string. Same as camelize but first char is lowercased.
	 *
	 * @param   string  $string  String to be camelBacked.
	 *
	 * @return string
	 *
	 * @see camelize
	 */
	public function variablize($string)
	{
		$string   = self::camelize(self::underscore($string));
		$result   = strtolower(substr($string, 0, 1));
		$variable = preg_replace('/\\w/', $result, $string, 1);

		return $variable;
	}

	/**
	 * Check to see if an English word is singular
	 *
	 * @param   string  $string  The word to check
	 *
	 * @return boolean
	 */
	public function isSingular($string)
	{
		// Check cache assuming the string is plural.
		$singular = $this->cache['singularized'][$string] ?? null;
		$plural   = $singular && isset($this->cache['pluralized'][$singular]) ? $this->cache['pluralized'][$singular] : null;

		if ($singular && $plural)
		{
			return $plural != $string;
		}

		// If string is not in the cache, try to pluralize and singularize it.
		return self::singularize(self::pluralize($string)) == $string;
	}

	/**
	 * Check to see if an English word is plural.
	 *
	 * @param   string  $string  String to be checked.
	 *
	 * @return boolean
	 */
	public function isPlural($string)
	{
		// Uncountable objects are always singular (e.g. information)
		if (in_array($string, $this->rules['uncountable']))
		{
			return false;
		}

		// Check cache assuming the string is singular.
		$plural   = $this->cache['pluralized'][$string] ?? null;
		$singular = $plural && isset($this->cache['singularized'][$plural]) ? $this->cache['singularized'][$plural] : null;

		if ($plural && $singular)
		{
			return $singular != $string;
		}

		// If string is not in the cache, try to singularize and pluralize it.
		return self::pluralize(self::singularize($string)) == $string;
	}

	/**
	 * Gets a part of a CamelCased word by index.
	 *
	 * Use a negative index to start at the last part of the word (-1 is the
	 * last part)
	 *
	 * @param   string   $string   Word
	 * @param   integer  $index    Index of the part
	 * @param   string   $default  Default value
	 *
	 * @return string
	 */
	public function getPart($string, $index, $default = null)
	{
		$parts = self::explode($string);

		if ($index < 0)
		{
			$index = count($parts) + $index;
		}

		return $parts[$index] ?? $default;
	}
}
Encrypt/EncryptService.php000060400000016316152455514240011660 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Utils\Phpfunc;

/**
 * Data encryption service for FOF-based components.
 *
 * This service allows you to transparently encrypt and decrypt *text* plaintext data. Use it to provide encryption for
 * sensitive or personal data stored in your database. Please remember:
 *
 * - The default behavior is to create a file with a random key on your component's root. If the file cannot be created
 *   the encryption is turned off.
 * - The key file is only created when you access the service. If you never use this service nothing happens (for
 *   backwards compatibility).
 * - You have to manually encrypt and decrypt data. It won't happen magically.
 * - Encrypted data cannot be searched unless you implement your own, slow, search algorithm.
 * - Data encryption is meant to be used on top of, not instead of, any other security measures for your site.
 * - Data encryption only protects against exploits targeting the database. If the attacker *also* gains read access to
 *   your filesystem OR if the attacker gains read / write access to the filesystem the encryption won't protect you.
 *   This is a full compromise of your site. At this point you're pwned and nothing can protect you. If you don't
 *   understand this simple truth do NOT use encryption.
 * - This is meant as a simple and basic encryption layer. It has not been independently verified. Use at your own risk.
 *
 * This service has the following FOF application configuration parameters which can be declared under the "container"
 * key (e.g. the "name" attribute of the fof.xml elements under fof > common > container > option):
 *
 * - encrypt_key_file  The path to the key file, relative to the component's backend root and WITHOUT the .php extension
 * - encrypt_key_const The constant for the key. By default it is COMPONENTNAME_FOF_ENCRYPT_SERVICE_SECRETKEY where
 *                     COMPONENTNAME corresponds to the uppercase com_componentname without the com_ prefix.
 *
 * @package     FOF30\Encrypt
 *
 * @since       3.3.2
 */
class EncryptService
{
	/**
	 * The component's container
	 *
	 * @var    Container
	 * @since  3.3.2
	 */
	private $container;

	/**
	 * The encryption engine used by this service
	 *
	 * @var    Aes
	 * @since  3.3.2
	 */
	private $aes;

	/**
	 * EncryptService constructor.
	 *
	 * @param   Container  $c  The FOF component container
	 *
	 * @since   3.3.2
	 */
	public function __construct(Container $c)
	{
		$this->container = $c;
		$this->initialize();
	}

	/**
	 * Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128###
	 *
	 * @param   string  $data  The plaintext data
	 *
	 * @return  string  The ciphertext, prefixed by ###AES128###
	 *
	 * @since   3.3.2
	 */
	public function encrypt($data)
	{
		if (!is_object($this->aes))
		{
			return $data;
		}

		$encrypted = $this->aes->encryptString($data, true);

		return '###AES128###' . $encrypted;
	}

	/**
	 * Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext.
	 *
	 * @param   string  $data  The ciphertext, prefixed by ###AES128###
	 *
	 * @return  string  The plaintext data
	 *
	 * @since   3.3.2
	 */
	public function decrypt($data)
	{
		if (substr($data, 0, 12) != '###AES128###')
		{
			return $data;
		}

		$data = substr($data, 12);

		if (!is_object($this->aes))
		{
			return $data;
		}

		$decrypted = $this->aes->decryptString($data, true);

		// Decrypted data is null byte padded. We have to remove the padding before proceeding.
		return rtrim($decrypted, "\0");
	}

	/**
	 * Initialize the AES cryptography object
	 *
	 * @return void
	 * @since  3.3.2
	 *
	 */
	private function initialize()
	{
		if (is_object($this->aes))
		{
			return;
		}

		$password = $this->getPassword();

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

		$phpFunc   = new Phpfunc();
		$this->aes = new Aes($password, 128, 'cbc', $phpFunc);
	}

	/**
	 * Returns the path to the secret key file
	 *
	 * @return  string
	 *
	 * @since   3.3.2
	 */
	private function getPasswordFilePath()
	{
		$default  = 'encrypt_service_key';
		$baseName = $this->container->appConfig->get('container.encrypt_key_file', $default);
		$baseName = trim($baseName, '/\\');

		return $this->container->backEndPath . '/' . $baseName . '.php';
	}

	/**
	 * Get the name of the constant where the secret key is stored. Remember that this is searched first, before a new
	 * key file is created. You can define this constant anywhere in your code loaded before the encryption service is
	 * first used to prevent a key file being created.
	 *
	 * @return string
	 *
	 * @since  3.3.2
	 */
	private function getConstantName()
	{
		$default = strtoupper($this->container->bareComponentName) . '_FOF_ENCRYPT_SERVICE_SECRETKEY';

		return $this->container->appConfig->get('container.encrypt_key_const', $default);
	}

	/**
	 * Returns the password used to encrypt information
	 *
	 * @return  string
	 *
	 * @since   3.3.2
	 */
	private function getPassword()
	{
		$constantName = $this->getConstantName();

		// If we have already read the file just return the key
		if (defined($constantName))
		{
			return constant($constantName);
		}

		// Do I have a secret key file?
		$filePath = $this->getPasswordFilePath();

		// I can't get the path to the file. Cut our losses and assume we can get no key.
		if (empty($filePath))
		{
			define($constantName, '');

			return '';
		}

		// If not, try to create one.
		if (!file_exists($filePath))
		{
			$this->makePasswordFile();
		}

		// We failed to create a new file? Cut our losses and assume we can get no key.
		if (!file_exists($filePath) || !is_readable($filePath))
		{
			define($constantName, '');

			return '';
		}

		// Try to include the key file
		include_once $filePath;

		// The key file contains garbage. Treason! Cut our losses and assume we can get no key.
		if (!defined($constantName))
		{
			define($constantName, '');

			return '';
		}

		// Finally, return the key which was defined in the file (happy path).
		return constant($constantName);
	}

	/**
	 * Create a new secret key file using a long, randomly generated password. The password generator uses a crypto-safe
	 * pseudorandom number generator (PRNG) to ensure suitability of the password for encrypting data at rest.
	 *
	 * @return  void
	 *
	 * @since   3.3.2
	 */
	private function makePasswordFile()
	{
		// Get the path to the new secret key file.
		$filePath = $this->getPasswordFilePath();

		// I can't get the path to the file. Sorry.
		if (empty($filePath))
		{
			return;
		}

		$phpFunc      = new Phpfunc();
		$randval      = new Randval($phpFunc);
		$secretKey    = $randval->getRandomPassword(64);
		$constantName = $this->getConstantName();

		$fileContent = "<?" . 'ph' . "p\n\n";
		$fileContent .= <<< END
/**
 * This file is automatically generated. It contains a secret key used for encrypting data by the component. Please do
 * not remove, edit or manually replace this file. It will render your existing encrypted data unreadable forever.
 */
 
define('$constantName', '$secretKey');

END;

		$this->container->filesystem->fileWrite($filePath, $fileContent);
	}
}
Encrypt/Randval.php000060400000012237152455514240010300 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt;

defined('_JEXEC') || die;

use FOF30\Utils\Phpfunc;

/**
 * Generates cryptographically-secure random values.
 */
class Randval implements RandvalInterface
{
	/**
	 * @var Phpfunc
	 */
	protected $phpfunc;

	/**
	 *
	 * Constructor.
	 *
	 * @param   Phpfunc  $phpfunc  An object to intercept PHP function calls;
	 *                             this makes testing easier.
	 *
	 */
	public function __construct(Phpfunc $phpfunc = null)
	{
		if (!is_object($phpfunc) || !($phpfunc instanceof Phpfunc))
		{
			$phpfunc = new Phpfunc();
		}

		$this->phpfunc = $phpfunc;
	}

	/**
	 *
	 * Returns a cryptographically secure random value.
	 *
	 * @param   integer  $bytes  How many bytes to return
	 *
	 * @return  string
	 */
	public function generate($bytes = 32)
	{
		if ($this->phpfunc->extension_loaded('openssl') && (version_compare(PHP_VERSION, '5.3.4') >= 0 || IS_WIN))
		{
			$strong    = false;
			$randBytes = openssl_random_pseudo_bytes($bytes, $strong);

			if ($strong)
			{
				return $randBytes;
			}
		}

		return $this->genRandomBytes($bytes);
	}

	/**
	 * Generate random bytes. Adapted from Joomla! 3.2.
	 *
	 * @param   integer  $length  Length of the random data to generate
	 *
	 * @return  string  Random binary data
	 */
	public function genRandomBytes($length = 32)
	{
		$length = (int) $length;
		$sslStr = '';

		/*
		 * Collect any entropy available in the system along with a number
		 * of time measurements of operating system randomness.
		 */
		$bitsPerRound  = 2;
		$maxTimeMicro  = 400;
		$shaHashLength = 20;
		$randomStr     = '';
		$total         = $length;

		// Check if we can use /dev/urandom.
		$urandom = false;
		$handle  = null;

		// This is PHP 5.3.3 and up
		if ($this->phpfunc->function_exists('stream_set_read_buffer') && @is_readable('/dev/urandom'))
		{
			$handle = @fopen('/dev/urandom', 'rb');

			if ($handle)
			{
				$urandom = true;
			}
		}

		while ($length > strlen($randomStr))
		{
			$bytes = ($total > $shaHashLength) ? $shaHashLength : $total;
			$total -= $bytes;

			/*
			 * Collect any entropy available from the PHP system and filesystem.
			 * If we have ssl data that isn't strong, we use it once.
			 */
			$entropy = random_int(0, mt_getrandmax()) . uniqid(random_int(0, mt_getrandmax()), true) . $sslStr;

			$entropy .= implode('', @fstat(fopen(__FILE__, 'r')));
			$entropy .= memory_get_usage();
			$sslStr  = '';

			if ($urandom)
			{
				stream_set_read_buffer($handle, 0);
				$entropy .= @fread($handle, $bytes);
			}
			else
			{
				/*
				 * There is no external source of entropy so we repeat calls
				 * to mt_rand until we are assured there's real randomness in
				 * the result.
				 *
				 * Measure the time that the operations will take on average.
				 */
				$samples  = 3;
				$duration = 0;

				for ($pass = 0; $pass < $samples; ++$pass)
				{
					$microStart = microtime(true) * 1000000;
					$hash       = sha1(random_int(0, mt_getrandmax()), true);

					for ($count = 0; $count < 50; ++$count)
					{
						$hash = sha1($hash, true);
					}

					$microEnd = microtime(true) * 1000000;
					$entropy  .= $microStart . $microEnd;

					if ($microStart >= $microEnd)
					{
						$microEnd += 1000000;
					}

					$duration += $microEnd - $microStart;
				}

				$duration = $duration / $samples;

				/*
				 * Based on the average time, determine the total rounds so that
				 * the total running time is bounded to a reasonable number.
				 */
				$rounds = (int) (($maxTimeMicro / $duration) * 50);

				/*
				 * Take additional measurements. On average we can expect
				 * at least $bitsPerRound bits of entropy from each measurement.
				 */
				$iter = $bytes * (int) ceil(8 / $bitsPerRound);

				for ($pass = 0; $pass < $iter; ++$pass)
				{
					$microStart = microtime(true);
					$hash       = sha1(random_int(0, mt_getrandmax()), true);

					for ($count = 0; $count < $rounds; ++$count)
					{
						$hash = sha1($hash, true);
					}

					$entropy .= $microStart . microtime(true);
				}
			}

			$randomStr .= sha1($entropy, true);
		}

		if ($urandom)
		{
			@fclose($handle);
		}

		return substr($randomStr, 0, $length);
	}

	/**
	 * Return a randomly generated password using safe characters (a-z, A-Z, 0-9).
	 *
	 * @param   int  $length  How many characters long should the password be. Default is 64.
	 *
	 * @return  string
	 *
	 * @since   3.3.2
	 */
	public function getRandomPassword($length = 64)
	{
		$salt     = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
		$base     = strlen($salt);
		$makepass = '';

		/*
		 * Start with a cryptographic strength random string, then convert it to
		 * a string with the numeric base of the salt.
		 * Shift the base conversion on each character so the character
		 * distribution is even, and randomize the start shift so it's not
		 * predictable.
		 */
		$random = $this->generate($length + 1);
		$shift  = ord($random[0]);

		for ($i = 1; $i <= $length; ++$i)
		{
			$makepass .= $salt[($shift + ord($random[$i])) % $base];
			$shift    += ord($random[$i]);
		}

		return $makepass;
	}

}
Encrypt/RandvalInterface.php000060400000000571152455514240012117 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt;

defined('_JEXEC') || die;

interface RandvalInterface
{
	/**
	 *
	 * Returns a cryptographically secure random value.
	 *
	 * @return string
	 *
	 */
	public function generate();
}
Encrypt/Totp.php000060400000011452152455514240007635 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt;

defined('_JEXEC') || die;

class Totp
{
	/**
	 * @var int The length of the resulting passcode (default: 6 digits)
	 */
	private $passCodeLength = 6;

	/**
	 * @var number The PIN modulo. It is set automatically to log10(passCodeLength)
	 */
	private $pinModulo;

	/**
	 * The length of the secret key, in characters (default: 10)
	 *
	 * @var int
	 */
	private $secretLength = 10;

	/**
	 * The time step between successive TOTPs in seconds (default: 30 seconds)
	 *
	 * @var int
	 */
	private $timeStep = 30;

	/**
	 * The Base32 encoder class
	 *
	 * @var Base32|null
	 */
	private $base32 = null;

	/**
	 * Initialises an RFC6238-compatible TOTP generator. Please note that this
	 * class does not implement the constraint in the last paragraph of §5.2
	 * of RFC6238. It's up to you to ensure that the same user/device does not
	 * retry validation within the same Time Step.
	 *
	 * @param   int     $timeStep        The Time Step (in seconds). Use 30 to be compatible with Google Authenticator.
	 * @param   int     $passCodeLength  The generated passcode length. Default: 6 digits.
	 * @param   int     $secretLength    The length of the secret key. Default: 10 bytes (80 bits).
	 * @param   Base32  $base32          The base32 en/decrypter
	 */
	public function __construct($timeStep = 30, $passCodeLength = 6, $secretLength = 10, Base32 $base32 = null)
	{
		$this->timeStep       = $timeStep;
		$this->passCodeLength = $passCodeLength;
		$this->secretLength   = $secretLength;
		$this->pinModulo      = 10 ** $this->passCodeLength;

		if (is_null($base32))
		{
			$this->base32 = new Base32();
		}
		else
		{
			$this->base32 = $base32;
		}
	}

	/**
	 * Get the time period based on the $time timestamp and the Time Step
	 * defined. If $time is skipped or set to null the current timestamp will
	 * be used.
	 *
	 * @param   int|null  $time  Timestamp
	 *
	 * @return  int  The time period since the UNIX Epoch
	 */
	public function getPeriod($time = null)
	{
		if (is_null($time))
		{
			$time = time();
		}

		$period = floor($time / $this->timeStep);

		return $period;
	}

	/**
	 * Check is the given passcode $code is a valid TOTP generated using secret
	 * key $secret
	 *
	 * @param   string  $secret  The Base32-encoded secret key
	 * @param   string  $code    The passcode to check
	 * @param   int     $time    The time to check it against. Leave null to check for the current server time.
	 *
	 * @return boolean True if the code is valid
	 */
	public function checkCode($secret, $code, $time = null)
	{
		$time = $this->getPeriod($time);

		for ($i = -1; $i <= 1; $i++)
		{
			if ($this->getCode($secret, ($time + $i) * $this->timeStep) == $code)
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Gets the TOTP passcode for a given secret key $secret and a given UNIX
	 * timestamp $time
	 *
	 * @param   string  $secret  The Base32-encoded secret key
	 * @param   int     $time    UNIX timestamp
	 *
	 * @return string
	 */
	public function getCode($secret, $time = null)
	{
		$period = $this->getPeriod($time);
		$secret = $this->base32->decode($secret);

		$time = pack("N", $period);
		$time = str_pad($time, 8, chr(0), STR_PAD_LEFT);

		$hash   = hash_hmac('sha1', $time, $secret, true);
		$offset = ord(substr($hash, -1));
		$offset = $offset & 0xF;

		$truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
		$pinValue      = str_pad($truncatedHash % $this->pinModulo, $this->passCodeLength, "0", STR_PAD_LEFT);

		return $pinValue;
	}

	/**
	 * Returns a QR code URL for easy setup of TOTP apps like Google Authenticator
	 *
	 * @param   string  $user      User
	 * @param   string  $hostname  Hostname
	 * @param   string  $secret    Secret string
	 *
	 * @return  string
	 */
	public function getUrl($user, $hostname, $secret)
	{
		$url        = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret);
		$encoder    = "https://chart.googleapis.com/chart?chs=200x200&chld=Q|2&cht=qr&chl=";
		$encoderURL = $encoder . urlencode($url);

		return $encoderURL;
	}

	/**
	 * Generates a (semi-)random Secret Key for TOTP generation
	 *
	 * @return  string
	 */
	public function generateSecret()
	{
		$secret = "";

		for ($i = 1; $i <= $this->secretLength; $i++)
		{
			$c      = random_int(0, 255);
			$secret .= pack("c", $c);
		}

		return $this->base32->encode($secret);
	}

	/**
	 * Extracts a part of a hash as an integer
	 *
	 * @param   string  $bytes  The hash
	 * @param   string  $start  The char to start from (0 = first char)
	 *
	 * @return  string
	 */
	protected function hashToInt($bytes, $start)
	{
		$input = substr($bytes, $start, strlen($bytes) - $start);
		$val2  = unpack("N", substr($input, 0, 4));

		return $val2[1];
	}
}
Encrypt/Base32.php000060400000011017152455514240007723 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt;

defined('_JEXEC') || die;

use InvalidArgumentException;

/**
 * Base32 encoding class, used by the TOTP
 */
class Base32
{
	/**
	 * CSRFC3548
	 *
	 * The character set as defined by RFC3548
	 * @link http://www.ietf.org/rfc/rfc3548.txt
	 */
	public const CSRFC3548 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

	/**
	 * Convert any string to a base32 string
	 * This should be binary safe...
	 *
	 * @param   string  $str  The string to convert
	 *
	 * @return  string  The converted base32 string
	 */
	public function encode($str)
	{
		return $this->fromBin($this->str2bin($str));
	}

	/**
	 * Convert any base32 string to a normal sctring
	 * This should be binary safe...
	 *
	 * @param   string  $str  The base32 string to convert
	 *
	 * @return  string  The normal string
	 */
	public function decode($str)
	{
		$str = strtoupper($str);

		return $this->bin2str($this->tobin($str));
	}

	/**
	 * Converts any ascii string to a binary string
	 *
	 * @param   string  $str  The string you want to convert
	 *
	 * @return  string  String of 0's and 1's
	 */
	private function str2bin($str)
	{
		$chrs = unpack('C*', $str);

		return vsprintf(str_repeat('%08b', count($chrs)), $chrs);
	}

	/**
	 * Converts a binary string to an ascii string
	 *
	 * @param   string  $str  The string of 0's and 1's you want to convert
	 *
	 * @return  string  The ascii output
	 *
	 * @throws  InvalidArgumentException
	 */
	private function bin2str($str)
	{
		if (strlen($str) % 8 > 0)
		{
			throw new InvalidArgumentException('Length must be divisible by 8');
		}

		if (!preg_match('/^[01]+$/', $str))
		{
			throw new InvalidArgumentException('Only 0\'s and 1\'s are permitted');
		}

		preg_match_all('/.{8}/', $str, $chrs);
		$chrs = array_map('bindec', $chrs[0]);

		// I'm just being slack here
		array_unshift($chrs, 'C*');

		return call_user_func_array('pack', $chrs);
	}

	/**
	 * Converts a correct binary string to base32
	 *
	 * @param   string  $str  The string of 0's and 1's you want to convert
	 *
	 * @return  string  String encoded as base32
	 *
	 * @throws  InvalidArgumentException
	 */
	private function fromBin($str)
	{
		if (strlen($str) % 8 > 0)
		{
			throw new InvalidArgumentException('Length must be divisible by 8');
		}

		if (!preg_match('/^[01]+$/', $str))
		{
			throw new InvalidArgumentException('Only 0\'s and 1\'s are permitted');
		}

		// Base32 works on the first 5 bits of a byte, so we insert blanks to pad it out
		$str = preg_replace('/(.{5})/', '000$1', $str);

		// We need a string divisible by 5
		$length = strlen($str);
		$rbits  = $length & 7;

		if ($rbits > 0)
		{
			// Excessive bits need to be padded
			$ebits = substr($str, $length - $rbits);
			$str   = substr($str, 0, $length - $rbits);
			$str   .= "000$ebits" . str_repeat('0', 5 - strlen($ebits));
		}

		preg_match_all('/.{8}/', $str, $chrs);
		$chrs = array_map([$this, 'mapCharset'], $chrs[0]);

		return implode('', $chrs);
	}

	/**
	 * Accepts a base32 string and returns an ascii binary string
	 *
	 * @param   string  $str  The base32 string to convert
	 *
	 * @return  string  Ascii binary string
	 *
	 * @throws  InvalidArgumentException
	 */
	private function toBin($str)
	{
		if (!preg_match('/^[' . self::CSRFC3548 . ']+$/', $str))
		{
			throw new InvalidArgumentException('Base64 string must match character set');
		}

		// Convert the base32 string back to a binary string
		$str = implode('', array_map([$this, 'mapBin'], str_split($str)));

		// Remove the extra 0's we added
		$str = preg_replace('/000(.{5})/', '$1', $str);

		// Remove padding if necessary
		$length = strlen($str);
		$rbits  = $length & 7;

		if ($rbits > 0)
		{
			$str = substr($str, 0, $length - $rbits);
		}

		return $str;
	}

	/**
	 * Used with array_map to map the bits from a binary string
	 * directly into a base32 character set
	 *
	 * @param   string  $str  The string of 0's and 1's you want to convert
	 *
	 * @return  string  Resulting base32 character
	 *
	 * @access private
	 */
	private function mapCharset($str)
	{
		// Huh!
		$x = self::CSRFC3548;

		return $x[bindec($str)];
	}

	/**
	 * Used with array_map to map the characters from a base32
	 * character set directly into a binary string
	 *
	 * @param   string  $chr  The character to map
	 *
	 * @return  string  String of 0's and 1's
	 *
	 * @access private
	 */
	private function mapBin($chr)
	{
		return sprintf('%08b', strpos(self::CSRFC3548, $chr));
	}

}
Encrypt/Aes.php000060400000016270152455514240007422 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt;

defined('_JEXEC') || die;

use FOF30\Encrypt\AesAdapter\AdapterInterface;
use FOF30\Encrypt\AesAdapter\OpenSSL;
use FOF30\Utils\Phpfunc;

/**
 * A simple abstraction to AES encryption
 *
 * Usage:
 *
 * // Create a new instance. The key is ignored – only use it if you have legacy encrypted content you need to decrypt
 * $aes = new Aes('ignored');
 * // Set the password. Do not use uf you have legacy encrypted content you need to decrypt
 * $aes->setPassword('yourRealPassword');
 * // Encrypt something.
 * $cipherText = $aes->encryptString($sourcePlainText);
 * // Decrypt something
 * $plainText = $aes->decryptString($sourceCipherText);
 */
class Aes
{
	/**
	 * The cipher key.
	 *
	 * @var   string
	 */
	private $key = '';

	/**
	 * The AES encryption adapter in use.
	 *
	 * @var  AdapterInterface
	 */
	private $adapter;

	/**
	 * Initialise the AES encryption object.
	 *
	 * Note: If the key is not 16 bytes this class will do a stupid key expansion for legacy reasons (produce the
	 * SHA-256 of the key string and throw away half of it).
	 *
	 * @param   string   $key       The encryption key (password). It can be a raw key (16 bytes) or a passphrase.
	 * @param   int      $strength  Bit strength (128, 192 or 256) – ALWAYS USE 128 BITS. THIS PARAMETER IS DEPRECATED.
	 * @param   string   $mode      Encryption mode. Can be ebc or cbc. We recommend using cbc.
	 * @param   Phpfunc  $phpfunc   For testing
	 */
	public function __construct($key, $strength = 128, $mode = 'cbc', Phpfunc $phpfunc = null)
	{
		$this->adapter = new OpenSSL();

		if (!$this->adapter->isSupported($phpfunc))
		{
			throw new \RuntimeException('Your server does not have the PHP OpenSSL extension enabled. This is required for encryption handling.');
		}

		$this->adapter->setEncryptionMode($mode, $strength);
		$this->setPassword($key, true);
	}

	/**
	 * Is AES encryption supported by this PHP installation?
	 *
	 * @return boolean
	 */
	public static function isSupported(Phpfunc $phpfunc = null)
	{
		if (!is_object($phpfunc) || !($phpfunc instanceof $phpfunc))
		{
			$phpfunc = new Phpfunc();
		}

		$adapter = new OpenSSL();

		if (!$adapter->isSupported($phpfunc))
		{
			return false;
		}

		if (!$phpfunc->function_exists('base64_encode'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('base64_decode'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('hash_algos'))
		{
			return false;
		}

		$algorightms = $phpfunc->hash_algos();

		if (!in_array('sha256', $algorightms))
		{
			return false;
		}

		return true;
	}

	/**
	 * Sets the password for this instance.
	 *
	 * WARNING: Do not use the legacy mode, it's insecure
	 *
	 * @param   string  $password    The password (either user-provided password or binary encryption key) to use
	 * @param   bool    $legacyMode  True to use the legacy key expansion. We recommend against using it.
	 */
	public function setPassword($password, $legacyMode = false)
	{
		$this->key = $password;

		$passLength = strlen($password);

		if (function_exists('mb_strlen'))
		{
			$passLength = mb_strlen($password, 'ASCII');
		}

		// Legacy mode was doing something stupid, requiring a key of 32 bytes. DO NOT USE LEGACY MODE!
		if ($legacyMode && ($passLength != 32))
		{
			// Legacy mode: use the sha256 of the password
			$this->key = hash('sha256', $password, true);
			// We have to trim or zero pad the password (we end up throwing half of it away in Rijndael-128 / AES...)
			$this->key = $this->adapter->resizeKey($this->key, $this->adapter->getBlockSize());
		}
	}

	/**
	 * Encrypts a string using AES
	 *
	 * @param   string  $stringToEncrypt  The plaintext to encrypt
	 * @param   bool    $base64encoded    Should I Base64-encode the result?
	 *
	 * @return   string  The cryptotext. Please note that the first 16 bytes of
	 *                   the raw string is the IV (initialisation vector) which
	 *                   is necessary for decoding the string.
	 */
	public function encryptString($stringToEncrypt, $base64encoded = true)
	{
		$blockSize = $this->adapter->getBlockSize();
		$randVal   = new Randval();
		$iv        = $randVal->generate($blockSize);

		$key        = $this->getExpandedKey($blockSize, $iv);
		$cipherText = $this->adapter->encrypt($stringToEncrypt, $key, $iv);

		// Optionally pass the result through Base64 encoding
		if ($base64encoded)
		{
			$cipherText = base64_encode($cipherText);
		}

		// Return the result
		return $cipherText;
	}

	/**
	 * Decrypts a ciphertext into a plaintext string using AES
	 *
	 * @param   string  $stringToDecrypt  The ciphertext to decrypt. The first 16 bytes of the raw string must contain
	 *                                    the IV (initialisation vector).
	 * @param   bool    $base64encoded    Should I Base64-decode the data before decryption?
	 *
	 * @return   string  The plain text string
	 */
	public function decryptString($stringToDecrypt, $base64encoded = true)
	{
		if ($base64encoded)
		{
			$stringToDecrypt = base64_decode($stringToDecrypt);
		}

		// Extract IV
		$iv_size = $this->adapter->getBlockSize();
		$iv      = substr($stringToDecrypt, 0, $iv_size);
		$key     = $this->getExpandedKey($iv_size, $iv);

		// Decrypt the data
		$plainText = $this->adapter->decrypt($stringToDecrypt, $key);

		return $plainText;
	}

	/**
	 * @param $blockSize
	 * @param $iv
	 *
	 * @return string
	 */
	public function getExpandedKey($blockSize, $iv)
	{
		$key        = $this->key;
		$passLength = strlen($key);

		if (function_exists('mb_strlen'))
		{
			$passLength = mb_strlen($key, 'ASCII');
		}

		if ($passLength != $blockSize)
		{
			$iterations = 1000;
			$salt       = $this->adapter->resizeKey($iv, 16);
			$key        = hash_pbkdf2('sha256', $this->key, $salt, $iterations, $blockSize, true);
		}

		return $key;
	}
}

if (!function_exists('hash_pbkdf2'))
{
	function hash_pbkdf2($algo, $password, $salt, $count, $length = 0, $raw_output = false)
	{
		if (!in_array(strtolower($algo), hash_algos()))
		{
			trigger_error(__FUNCTION__ . '(): Unknown hashing algorithm: ' . $algo, E_USER_WARNING);
		}

		if (!is_numeric($count))
		{
			trigger_error(__FUNCTION__ . '(): expects parameter 4 to be long, ' . gettype($count) . ' given', E_USER_WARNING);
		}

		if (!is_numeric($length))
		{
			trigger_error(__FUNCTION__ . '(): expects parameter 5 to be long, ' . gettype($length) . ' given', E_USER_WARNING);
		}

		if ($count <= 0)
		{
			trigger_error(__FUNCTION__ . '(): Iterations must be a positive integer: ' . $count, E_USER_WARNING);
		}

		if ($length < 0)
		{
			trigger_error(__FUNCTION__ . '(): Length must be greater than or equal to 0: ' . $length, E_USER_WARNING);
		}

		$output      = '';
		$block_count = $length ? ceil($length / strlen(hash($algo, '', $raw_output))) : 1;

		for ($i = 1; $i <= $block_count; $i++)
		{
			$last = $xorsum = hash_hmac($algo, $salt . pack('N', $i), $password, true);

			for ($j = 1; $j < $count; $j++)
			{
				$xorsum ^= ($last = hash_hmac($algo, $last, $password, true));
			}

			$output .= $xorsum;
		}

		if (!$raw_output)
		{
			$output = bin2hex($output);
		}

		return $length ? substr($output, 0, $length) : $output;
	}
}
Encrypt/AesAdapter/AdapterInterface.php000060400000005015152455514240014117 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt\AesAdapter;

defined('_JEXEC') || die;

use FOF30\Utils\Phpfunc;

/**
 * Interface for AES encryption adapters
 */
interface AdapterInterface
{
	/**
	 * Sets the AES encryption mode.
	 *
	 * WARNING: The strength parameter is deprecated since FOF 3.1 and has no effect.
	 *
	 * @param   string  $mode      Choose between CBC (recommended) or ECB
	 * @param   int     $strength  DEPRECATED AND UNUSED.
	 *
	 * @return  mixed
	 */
	public function setEncryptionMode($mode = 'cbc', $strength = 128);

	/**
	 * Encrypts a string. Returns the raw binary ciphertext.
	 *
	 * WARNING: The plaintext is zero-padded to the algorithm's block size. You are advised to store the size of the
	 * plaintext and trim the string to that length upon decryption.
	 *
	 * @param   string       $plainText  The plaintext to encrypt
	 * @param   string       $key        The raw binary key (will be zero-padded or chopped if its size is different
	 *                                   than the block size)
	 * @param   null|string  $iv         The initialization vector (for CBC mode algorithms)
	 *
	 * @return  string  The raw encrypted binary string.
	 */
	public function encrypt($plainText, $key, $iv = null);

	/**
	 * Decrypts a string. Returns the raw binary plaintext.
	 *
	 * $ciphertext MUST start with the IV followed by the ciphertext, even for EBC data (the first block of data is
	 * dropped in EBC mode since there is no concept of IV in EBC).
	 *
	 * WARNING: The returned plaintext is zero-padded to the algorithm's block size during encryption. You are advised
	 * to trim the string to the original plaintext's length upon decryption. While rtrim($decrypted, "\0") sounds
	 * appealing it's NOT the correct approach for binary data (zero bytes may actually be part of your plaintext, not
	 * just padding!).
	 *
	 * @param   string  $cipherText  The ciphertext to encrypt
	 * @param   string  $key         The raw binary key (will be zero-padded or chopped if its size is different than
	 *                               the block size)
	 *
	 * @return  string  The raw unencrypted binary string.
	 */
	public function decrypt($cipherText, $key);

	/**
	 * Returns the encryption block size in bytes
	 *
	 * @return  int
	 */
	public function getBlockSize();

	/**
	 * Is this adapter supported?
	 *
	 * @return  bool
	 */
	public function isSupported(Phpfunc $phpfunc = null);
}
Encrypt/AesAdapter/OpenSSL.php000060400000010231152455514240012175 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt\AesAdapter;

defined('_JEXEC') || die;

use FOF30\Encrypt\Randval;
use FOF30\Utils\Phpfunc;

class OpenSSL extends AbstractAdapter implements AdapterInterface
{
	/**
	 * The OpenSSL options for encryption / decryption
	 *
	 * PHP 5.3 does not have the constants OPENSSL_RAW_DATA and OPENSSL_ZERO_PADDING. In fact, the parameter
	 * is called $raw_data and is a boolean. Since integer 1 is equivalent to boolean TRUE in PHP we can get
	 * away with initializing this parameter with the integer 1.
	 *
	 * @var  int
	 */
	protected $openSSLOptions = 1;

	/**
	 * The encryption method to use
	 *
	 * @var  string
	 */
	protected $method = 'aes-128-cbc';

	public function __construct()
	{
		/**
		 * PHP 5.4 and later replaced the $raw_data parameter with the $options parameter. Instead of a boolean we need
		 * to pass some flags. Here you go.
		 *
		 * Since PHP 5.3 does NOT have the relevant constants we must NOT run this bit of code under PHP 5.3.
		 *
		 * See http://stackoverflow.com/questions/24707007/using-openssl-raw-data-param-in-openssl-decrypt-with-php-5-3#24707117
		 */
		if (version_compare(PHP_VERSION, '5.4.0', 'ge'))
		{
			$this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
		}
	}

	public function setEncryptionMode($mode = 'cbc', $strength = 128)
	{
		static $availableAlgorithms = null;
		static $defaultAlgo = 'aes-128-cbc';

		if (!is_array($availableAlgorithms))
		{
			$availableAlgorithms = openssl_get_cipher_methods();

			foreach ([
				         'aes-256-cbc', 'aes-256-ecb', 'aes-192-cbc',
				         'aes-192-ecb', 'aes-128-cbc', 'aes-128-ecb',
			         ] as $algo)
			{
				if (in_array($algo, $availableAlgorithms))
				{
					$defaultAlgo = $algo;
					break;
				}
			}
		}

		$strength = (int) $strength;
		$mode     = strtolower($mode);

		if (!in_array($strength, [128, 192, 256]))
		{
			$strength = 256;
		}

		if (!in_array($mode, ['cbc', 'ebc']))
		{
			$mode = 'cbc';
		}

		$algo = 'aes-' . $strength . '-' . $mode;

		if (!in_array($algo, $availableAlgorithms))
		{
			$algo = $defaultAlgo;
		}

		$this->method = $algo;
	}

	public function encrypt($plainText, $key, $iv = null)
	{
		$iv_size = $this->getBlockSize();
		$key     = $this->resizeKey($key, $iv_size);
		$iv      = $this->resizeKey($iv, $iv_size);

		if (empty($iv))
		{
			$randVal = new Randval();
			$iv      = $randVal->generate($iv_size);
		}

		$plainText  .= $this->getZeroPadding($plainText, $iv_size);
		$cipherText = openssl_encrypt($plainText, $this->method, $key, $this->openSSLOptions, $iv);
		$cipherText = $iv . $cipherText;

		return $cipherText;
	}

	public function decrypt($cipherText, $key)
	{
		$iv_size    = $this->getBlockSize();
		$key        = $this->resizeKey($key, $iv_size);
		$iv         = substr($cipherText, 0, $iv_size);
		$cipherText = substr($cipherText, $iv_size);
		$plainText  = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv);

		return $plainText;
	}

	public function isSupported(Phpfunc $phpfunc = null)
	{
		if (!is_object($phpfunc) || !($phpfunc instanceof $phpfunc))
		{
			$phpfunc = new Phpfunc();
		}

		if (!$phpfunc->function_exists('openssl_get_cipher_methods'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('openssl_random_pseudo_bytes'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('openssl_cipher_iv_length'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('openssl_encrypt'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('openssl_decrypt'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('hash'))
		{
			return false;
		}

		if (!$phpfunc->function_exists('hash_algos'))
		{
			return false;
		}

		$algorightms = $phpfunc->openssl_get_cipher_methods();

		if (!in_array('aes-128-cbc', $algorightms))
		{
			return false;
		}

		$algorightms = $phpfunc->hash_algos();

		if (!in_array('sha256', $algorightms))
		{
			return false;
		}

		return true;
	}

	/**
	 * @return int
	 */
	public function getBlockSize()
	{
		return openssl_cipher_iv_length($this->method);
	}
}
Encrypt/AesAdapter/AbstractAdapter.php000060400000003450152455514240013763 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Encrypt\AesAdapter;

defined('_JEXEC') || die;

/**
 * Abstract AES encryption class
 */
abstract class AbstractAdapter
{
	/**
	 * Trims or zero-pads a key / IV
	 *
	 * @param   string  $key   The key or IV to treat
	 * @param   int     $size  The block size of the currently used algorithm
	 *
	 * @return  null|string  Null if $key is null, treated string of $size byte length otherwise
	 */
	public function resizeKey($key, $size)
	{
		if (empty($key))
		{
			return null;
		}

		$keyLength = strlen($key);

		if (function_exists('mb_strlen'))
		{
			$keyLength = mb_strlen($key, 'ASCII');
		}

		if ($keyLength == $size)
		{
			return $key;
		}

		if ($keyLength > $size)
		{
			if (function_exists('mb_substr'))
			{
				return mb_substr($key, 0, $size, 'ASCII');
			}

			return substr($key, 0, $size);
		}

		return $key . str_repeat("\0", ($size - $keyLength));
	}

	/**
	 * Returns null bytes to append to the string so that it's zero padded to the specified block size
	 *
	 * @param   string  $string     The binary string which will be zero padded
	 * @param   int     $blockSize  The block size
	 *
	 * @return  string  The zero bytes to append to the string to zero pad it to $blockSize
	 */
	protected function getZeroPadding($string, $blockSize)
	{
		$stringSize = strlen($string);

		if (function_exists('mb_strlen'))
		{
			$stringSize = mb_strlen($string, 'ASCII');
		}

		if ($stringSize == $blockSize)
		{
			return '';
		}

		if ($stringSize < $blockSize)
		{
			return str_repeat("\0", $blockSize - $stringSize);
		}

		$paddingBytes = $stringSize % $blockSize;

		return str_repeat("\0", $blockSize - $paddingBytes);
	}
}
Input/Input.php000060400000012507152455514240007463 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Input;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Input\Input as JInput;
use ReflectionObject;

class Input extends JInput
{
	/**
	 * Public constructor. Overridden to allow specifying the global input array
	 * to use as a string and instantiate from an object holding variables.
	 *
	 * @param   array|string|object|null  $source   Source data; set null to use the default Joomla input source
	 * @param   array                     $options  Filter options
	 */
	public function __construct($source = null, array $options = [])
	{
		$hash = null;

		if (is_string($source))
		{
			$hash = strtoupper($source);

			if (!in_array($hash, ['GET', 'POST', 'FILES', 'COOKIE', 'ENV', 'SERVER', 'REQUEST']))
			{
				$hash = 'REQUEST';
			}

			$source = $this->extractJoomlaSource($hash);
		}
		elseif (is_object($source) && ($source instanceof Input))
		{
			$source = $source->getData();
		}
		elseif (is_object($source) && ($source instanceof JInput))
		{
			$serialised = $source->serialize();
			[$xOptions, $xData, $xInput] = unserialize($serialised);
			unset ($xOptions);
			unset ($xInput);
			unset ($source);
			$source = $xData;
			unset ($xData);
		}
		elseif (is_object($source))
		{
			try
			{
				$source = (array) $source;
			}
			catch (Exception $exc)
			{
				$source = null;
			}
		}
		elseif (is_array($source))
		{
			// Nothing, it's already an array
		}
		else
		{
			// Any other case
			$source = null;
		}

		// TODO Joomla 4 -- get the data from the application input

		// If we are not sure use the REQUEST array
		if (empty($source))
		{
			$source = $this->extractJoomlaSource('REQUEST');
		}

		parent::__construct($source, $options);
	}

	/**
	 * Gets a value from the input data. Overridden to allow specifying a filter
	 * mask.
	 *
	 * @param   string  $name     Name of the value to get.
	 * @param   mixed   $default  Default value to return if variable does not exist.
	 * @param   string  $filter   Filter to apply to the value.
	 * @param   int     $mask     The filter mask
	 *
	 * @return  mixed  The filtered input value.
	 */
	public function get($name, $default = null, $filter = 'cmd', $mask = 0)
	{
		if (isset($this->data[$name]))
		{
			return $this->_cleanVar($this->data[$name], $mask, $filter);
		}

		return $default;
	}

	/**
	 * Remove a key from the input data.
	 *
	 * @param   string  $name  The key name to remove from the input data.
	 *
	 * @return  void
	 * @since   3.6.3
	 */
	public function remove($name)
	{
		if (!isset($this->data[$name]))
		{
			return;
		}

		unset($this->data[$name]);
	}

	/**
	 * Returns a copy of the raw data stored in the class
	 *
	 * @return  array
	 */
	public function getData()
	{
		return $this->data;
	}

	public function setData(array $data)
	{
		$this->data = $data;
	}

	/**
	 * Magic method to get filtered input data.
	 *
	 * @param   mixed   $name       Name of the value to get.
	 * @param   string  $arguments  [0] The name of the variable [1] The default value [2] Mask
	 *
	 * @return  boolean  The filtered boolean input value.
	 */
	public function __call($name, $arguments)
	{
		if (substr($name, 0, 3) == 'get')
		{
			$filter = substr($name, 3);

			$default = null;
			$mask    = 0;

			if (isset($arguments[1]))
			{
				$default = $arguments[1];
			}

			if (isset($arguments[2]))
			{
				$mask = $arguments[2];
			}

			return $this->get($arguments[0], $default, $filter, $mask);
		}
	}

	/**
	 * Custom filter implementation. Works better with arrays and allows the use
	 * of a filter mask.
	 *
	 * @param   mixed    $var   The variable (value) to clean
	 * @param   integer  $mask  The clean mask
	 * @param   string   $type  The variable type
	 *
	 * @return   mixed
	 */
	protected function _cleanVar($var, $mask = 0, $type = null)
	{
		if (is_array($var))
		{
			$temp = [];

			foreach ($var as $k => $v)
			{
				$temp[$k] = self::_cleanVar($v, $mask);
			}

			return $temp;
		}

		// If the no trim flag is not set, trim the variable
		if (!($mask & 1) && is_string($var))
		{
			$var = trim($var);
		}

		// Now we handle input filtering
		if ($mask & 2)
		{
			// If the allow raw flag is set, do not modify the variable
		}
		elseif ($mask & 4)
		{
			// If the allow HTML flag is set, apply a safe HTML filter to the variable
			if (version_compare(JVERSION, '3.999.999', 'le'))
			{
				$safeHtmlFilter = InputFilter::getInstance(null, null, 1, 1);
			}
			else
			{
				$safeHtmlFilter = InputFilter::getInstance([], [], 1, 1);
			}
			$var = $safeHtmlFilter->clean($var, $type);
		}
		else
		{
			$var = $this->filter->clean($var, $type);
		}

		return $var;
	}

	protected function extractJoomlaSource($hash = 'REQUEST')
	{
		if (!in_array(strtoupper($hash), ['GET', 'POST', 'FILES', 'COOKIE', 'ENV', 'SERVER', 'REQUEST']))
		{
			$hash = 'REQUEST';
		}

		$hash = strtolower($hash);

		try
		{
			$input = Factory::getApplication()->input;
		}
		catch (Exception $e)
		{
			$input = new \Joomla\Input\Input();
		}

		if ($hash !== 'request')
		{
			$input = $input->{$hash};
		}

		$refObject = new ReflectionObject($input);
		$refProp   = $refObject->getProperty('data');
		$refProp->setAccessible(true);

		return $refProp->getValue($input);
	}
}
Render/RenderBase.php000060400000012232152455514240010511 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Render;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use Joomla\Registry\Registry;
use LogicException;
use stdClass;

/**
 * Base class for other render classes
 *
 * @package FOF30\Render
 * @since   3.0.0
 */
abstract class RenderBase implements RenderInterface
{
	/** @var   Container|null  The container we are attached to */
	protected $container = null;

	/** @var   bool  Is this renderer available under this execution environment? */
	protected $enabled = false;

	/** @var   int  The priority of this renderer in case we have multiple available ones */
	protected $priority = 0;

	/** @var   Registry  A registry object holding renderer options */
	protected $optionsRegistry = null;

	/**
	 * Public constructor. Determines the priority of this class and if it should be enabled
	 */
	public function __construct(Container $container)
	{
		$this->container = $container;

		$this->optionsRegistry = new Registry();
	}

	/**
	 * Set a renderer option (depends on the renderer)
	 *
	 * @param   string  $key    The name of the option to set
	 * @param   mixed   $value  The value of the option
	 *
	 * @return  void
	 */
	function setOption(string $key, $value = null): void
	{
		$this->optionsRegistry->set($key, $value);
	}

	/**
	 * Set multiple renderer options at once (depends on the renderer)
	 *
	 * @param   array  $options  The options to set as key => value pairs
	 *
	 * @return  void
	 */
	function setOptions(array $options): void
	{
		foreach ($options as $key => $value)
		{
			$this->setOption($key, $value);
		}
	}

	/**
	 * Get the value of a renderer option
	 *
	 * @param   string  $key      The name of the parameter
	 * @param   mixed   $default  The default value to return if the parameter is not set
	 *
	 * @return  mixed  The parameter value
	 */
	function getOption(string $key, $default = null)
	{
		return $this->optionsRegistry->get($key, $default);
	}

	/**
	 * Returns the information about this renderer
	 *
	 * @return  stdClass
	 */
	function getInformation(): stdClass
	{
		$classParts = explode('\\', get_class($this));

		return (object) [
			'enabled'  => $this->enabled,
			'priority' => $this->priority,
			'name'     => strtolower(array_pop($classParts)),
		];
	}

	/**
	 * Echoes any HTML to show before the view template
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function preRender(string $view, string $task): void
	{
		$this->loadCustomCss();
	}

	/**
	 * Echoes any HTML to show after the view template
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function postRender(string $view, string $task): void
	{
	}

	/**
	 * Renders the submenu (link bar) for a category view when it is used in a
	 * extension
	 *
	 * Note: this function has to be called from the addSubmenu function in
	 *         the ExtensionNameHelper class located in
	 *         administrator/components/com_ExtensionName/helpers/Extensionname.php
	 *
	 * @return  void
	 */
	function renderCategoryLinkbar(): void
	{
		throw new LogicException(sprintf('Renderer class %s must implement the %s method', get_class($this), __METHOD__));
	}

	/**
	 * Opens a wrapper DIV. Our component's output will be inside this wrapper.
	 *
	 * @param   array  $classes  An array of additional CSS classes to add to the outer page wrapper element.
	 *
	 * @return  void
	 */
	protected function openPageWrapper(array $classes): void
	{
		$removeClasses = $this->getOption('remove_wrapper_classes', []);

		if (!is_array($removeClasses))
		{
			$removeClasses = explode(',', $removeClasses);
		}

		$removeClasses = array_map('trim', $removeClasses);

		foreach ($removeClasses as $class)
		{
			$x = array_search($class, $classes);

			if ($x !== false)
			{
				unset($classes[$x]);
			}
		}

		// Add the following classes to the wrapper div
		$addClasses = $this->getOption('add_wrapper_classes', '');

		if (!is_array($addClasses))
		{
			$addClasses = explode(',', $addClasses);
		}

		$addClasses    = array_map('trim', $addClasses);
		$customClasses = implode(' ', array_unique(array_merge($classes, $addClasses)));

		$id = $this->getOption('wrapper_id', null);
		$id = empty($id) ? "" : sprintf(' id="%s"', $id);

		echo <<< HTML
<div class="$customClasses"$id>

HTML;
	}

	/**
	 * Outputs HTML which closes the page wrappers opened with openPageWrapper.
	 *
	 * @return  void
	 */
	protected function closePageWrapper(): void
	{
		echo "</div>\n";
	}

	/**
	 * Loads the custom CSS files defined in the custom_css renderer option.
	 */
	protected function loadCustomCss()
	{
		$custom_css_raw = $this->getOption('custom_css', '');
		$custom_css_raw = trim($custom_css_raw);

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

		$files        = explode(',', $custom_css_raw);
		$mediaVersion = $this->container->mediaVersion;

		foreach ($files as $file)
		{
			$file = trim($file);

			if (empty($file))
			{
				continue;
			}

			$this->container->template->addCSS($file, $mediaVersion);
		}
	}
}
Render/Joomla.php000060400000025571152455514240007732 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Render;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Toolbar\Toolbar;
use JHtmlSidebar;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Toolbar\Toolbar as JoomlaToolbar;

/**
 * Renderer class for use with Joomla! 3.x and 4.x
 *
 * Renderer options
 *
 * wrapper_id              The ID of the wrapper DIV. Default: akeeba-renderjoomla
 * linkbar_style           Style for linkbars: joomla3|classic. Default: joomla3
 * remove_wrapper_classes  Comma-separated list of classes to REMOVE from the container
 * add_wrapper_classes     Comma-separated list of classes to ADD to the container
 *
 * @package FOF30\Render
 * @since   3.6.0
 */
class Joomla extends RenderBase implements RenderInterface
{
	/** @inheritDoc */
	public function __construct(Container $container)
	{
		$this->priority = 30;
		$this->enabled  = true;

		parent::__construct($container);
	}

	/**
	 * Echoes any HTML to show before the view template
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function preRender(string $view, string $task): void
	{
		$input    = $this->container->input;
		$platform = $this->container->platform;

		$format = $input->getCmd('format', 'html');

		if (empty($format))
		{
			$format = 'html';
		}

		if ($format != 'html')
		{
			return;
		}

		if ($platform->isCli())
		{
			return;
		}

		HTMLHelper::_('behavior.core');
		HTMLHelper::_('jquery.framework', true);

		// Wrap output in various classes
		$versionParts = explode('.', JVERSION);
		$minorVersion = $versionParts[0] . $versionParts[1];
		$majorVersion = $versionParts[0];

		$classes = [];

		if ($platform->isBackend())
		{
			$area            = $platform->isBackend() ? 'admin' : 'site';
			$option          = $input->getCmd('option', '');
			$viewForCssClass = $input->getCmd('view', '');
			$layout          = $input->getCmd('layout', '');
			$taskForCssClass = $input->getCmd('task', '');

			$classes = [
				'joomla-version-' . $majorVersion,
				'joomla-version-' . $minorVersion,
				$area,
				$option,
				'view-' . $view,
				'view-' . $viewForCssClass,
				'layout-' . $layout,
				'task-' . $task,
				'task-' . $taskForCssClass,
				// We have a floating sidebar, they said. It looks great, they said. They must've been blind, I say!
				'j-toggle-main',
				'j-toggle-transition',
				'row-fluid',
			];

			$classes = array_unique($classes);
		}

		$this->openPageWrapper($classes);

		// Render the submenu and toolbar
		if ($input->getBool('render_toolbar', true))
		{
			$this->renderButtons($view, $task);
			$this->renderLinkbar($view, $task);
		}

		parent::preRender($view, $task);
	}

	/**
	 * Echoes any HTML to show after the view template
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function postRender(string $view, string $task): void
	{
		$input    = $this->container->input;
		$platform = $this->container->platform;

		$format = $input->getCmd('format', 'html');

		if (empty($format))
		{
			$format = 'html';
		}

		if ($format != 'html')
		{
			return;
		}

		// Closing tag only if we're not in CLI
		if ($platform->isCli())
		{
			return;
		}

		// Closes akeeba-renderjoomla div
		$this->closePageWrapper();
	}

	/**
	 * Renders the submenu (link bar)
	 *
	 * @param   string  $view  The active view name
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	protected function renderLinkbar(string $view, string $task): void
	{
		$style = $this->getOption('linkbar_style', 'joomla');

		switch ($style)
		{
			case 'joomla':
				$this->renderLinkbar_joomla($view, $task);
				break;

			case 'classic':
			default:
				$this->renderLinkbar_classic($view, $task);
				break;
		}
	}

	/**
	 * Renders the submenu (link bar) in F0F's classic style, using a Bootstrapped
	 * tab bar.
	 *
	 * @param   string  $view  The active view name
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	protected function renderLinkbar_classic(string $view, string $task): void
	{
		$platform = $this->container->platform;

		if ($platform->isCli())
		{
			return;
		}

		$isJoomla4 = version_compare(JVERSION, '3.99999.99999', 'gt');
		$isJoomla3 = !$isJoomla4 && version_compare(JVERSION, '3.0.0', 'ge');

		// Do not render a submenu unless we are in the the admin area
		$toolbar               = $this->container->toolbar;
		$renderFrontendSubmenu = $toolbar->getRenderFrontendSubmenu();

		if (!$platform->isBackend() && !$renderFrontendSubmenu)
		{
			return;
		}

		$links = $toolbar->getLinks();

		if (!empty($links))
		{
			echo "<ul class=\"nav nav-tabs\">\n";

			foreach ($links as $link)
			{
				$dropdown = false;

				if (array_key_exists('dropdown', $link))
				{
					$dropdown = $link['dropdown'];
				}

				if ($dropdown)
				{
					echo "<li";
					$class = 'nav-item dropdown';

					if ($link['active'])
					{
						$class .= ' active';
					}

					echo ' class="' . $class . '">';

					echo '<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#">';

					if ($link['icon'])
					{
						echo "<i class=\"icon icon-" . $link['icon'] . "\"></i>";
					}

					echo $link['name'];
					echo '<b class="caret"></b>';
					echo '</a>';

					echo "\n<ul class=\"dropdown-menu\">";

					foreach ($link['items'] as $item)
					{
						echo "<li class=\"dropdown-item";

						if ($item['active'])
						{
							echo ' active';
						}

						echo "\">";

						if ($item['icon'])
						{
							echo "<i class=\"icon icon-" . $item['icon'] . "\"></i>";
						}

						if ($item['link'])
						{
							echo "<a href=\"" . $item['link'] . "\">" . $item['name'] . "</a>";
						}
						else
						{
							echo $item['name'];
						}

						echo "</li>";
					}

					echo "</ul>\n";
				}
				else
				{
					echo "<li class=\"nav-item";

					if ($link['active'] && $isJoomla3)
					{
						echo ' active"';
					}

					echo "\">";

					if ($link['icon'])
					{
						echo "<span class=\"icon icon-" . $link['icon'] . "\"></span>";
					}

					if ($isJoomla3)
					{
						if ($link['link'])
						{
							echo "<a href=\"" . $link['link'] . "\">" . $link['name'] . "</a>";
						}
						else
						{
							echo $link['name'];
						}
					}
					else
					{
						$class = $link['active'] ? 'active' : '';

						$href = $link['link'] ?: '#';

						echo "<a href=\"$href\" class=\"nav-link $class\">{$link['name']}</a>";
					}
				}

				echo "</li>\n";
			}

			echo "</ul>\n";
		}
	}

	/**
	 * Renders the submenu (link bar) using Joomla!'s style. On Joomla! 2.5 this
	 * is a list of bar separated links, on Joomla! 3 it's a sidebar at the
	 * left-hand side of the page.
	 *
	 * @param   string  $view  The active view name
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	protected function renderLinkbar_joomla(string $view, string $task): void
	{
		$platform = $this->container->platform;

		// On command line don't do anything
		if ($platform->isCli())
		{
			return;
		}

		// Do not render a submenu unless we are in the the admin area
		$toolbar               = $this->container->toolbar;
		$renderFrontendSubmenu = $toolbar->getRenderFrontendSubmenu();

		if (!$platform->isBackend() && !$renderFrontendSubmenu)
		{
			return;
		}

		$this->renderLinkbarItems($toolbar);
	}

	/**
	 * Render the linkbar
	 *
	 * @param   Toolbar  $toolbar  An FOF toolbar object
	 *
	 * @return  void
	 */
	protected function renderLinkbarItems(Toolbar $toolbar): void
	{
		$links = $toolbar->getLinks();

		if (!empty($links))
		{
			foreach ($links as $link)
			{
				JHtmlSidebar::addEntry($link['name'], $link['link'], $link['active']);

				$dropdown = false;

				if (array_key_exists('dropdown', $link))
				{
					$dropdown = $link['dropdown'];
				}

				if ($dropdown)
				{
					foreach ($link['items'] as $item)
					{
						JHtmlSidebar::addEntry('– ' . $item['name'], $item['link'], $item['active']);
					}
				}
			}
		}
	}

	/**
	 * Renders the toolbar buttons
	 *
	 * @param   string  $view  The active view name
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	protected function renderButtons(string $view, string $task): void
	{
		$platform = $this->container->platform;

		if ($platform->isCli())
		{
			return;
		}

		// Do not render buttons unless we are in the the frontend area and we are asked to do so
		$toolbar               = $this->container->toolbar;
		$renderFrontendButtons = $toolbar->getRenderFrontendButtons();

		// Load main backend language, in order to display toolbar strings
		// (JTOOLBAR_BACK, JTOOLBAR_PUBLISH etc etc)
		$platform->loadTranslations('joomla');

		if ($platform->isBackend() || !$renderFrontendButtons)
		{
			return;
		}

		$bar   = JoomlaToolbar::getInstance('toolbar');
		$items = $bar->getItems();

		$substitutions = [
			'icon-32-new'       => 'icon-plus',
			'icon-32-publish'   => 'icon-eye-open',
			'icon-32-unpublish' => 'icon-eye-close',
			'icon-32-delete'    => 'icon-trash',
			'icon-32-edit'      => 'icon-edit',
			'icon-32-copy'      => 'icon-th-large',
			'icon-32-cancel'    => 'icon-remove',
			'icon-32-back'      => 'icon-circle-arrow-left',
			'icon-32-apply'     => 'icon-ok',
			'icon-32-save'      => 'icon-hdd',
			'icon-32-save-new'  => 'icon-repeat',
		];

		if (isset(JoomlaFactory::getApplication()->JComponentTitle))
		{
			$title = JoomlaFactory::getApplication()->JComponentTitle;
		}
		else
		{
			$title = '';
		}

		$html    = [];
		$actions = [];

		// We have to use the same id we're using inside other renderers
		$html[] = '<div class="well" id="FOFHeaderContainer">';
		$html[] = '<div class="titleContainer">' . $title . '</div>';
		$html[] = '<div class="buttonsContainer">';

		foreach ($items as $node)
		{
			$type   = $node[0];
			$button = $bar->loadButtonType($type);

			if ($button !== false)
			{
				$action    = call_user_func_array([&$button, 'fetchButton'], $node);
				$action    = str_replace('class="toolbar"', 'class="toolbar btn"', $action);
				$action    = str_replace('<span ', '<i ', $action);
				$action    = str_replace('</span>', '</i>', $action);
				$action    = str_replace(array_keys($substitutions), array_values($substitutions), $action);
				$actions[] = $action;
			}
		}

		$html   = array_merge($html, $actions);
		$html[] = '</div>';
		$html[] = '</div>';

		echo implode("\n", $html);
	}

	/**
	 * Opens the wrapper DIV element. Our component's output will be inside this wrapper.
	 *
	 * @param   array  $classes  An array of additional CSS classes to add to the outer page wrapper element.
	 *
	 * @return  void
	 */
	protected function openPageWrapper(array $classes): void
	{
		$this->setOption('wrapper_id', $this->getOption('wrapper_id', 'akeeba-renderjoomla'));

		$classes[] = 'akeeba-renderer-joomla';

		parent::openPageWrapper($classes);
	}

}
Render/FEF.php000060400000010520152455514240007075 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Render;

defined('_JEXEC') || die;

use AkeebaFEFHelper;
use FOF30\Container\Container;

/**
 * Renderer class for use with Akeeba FEF
 *
 * Renderer options
 *
 * wrapper_id           The ID of the wrapper DIV. Default: akeeba-rendered-fef
 * linkbar_style        Style for linkbars: joomla3|classic. Default: joomla3
 * load_fef             Load FEF CSS and JS? Set to false if you are loading it outside the renderer. Default: true
 * fef_reset            Should I reset the CSS styling for basic HTML elements inside the FEF container? Default: true
 * fef_dark             Should I load the FEF Dark Mode CSS and supporting JS? Default: 0 (no). Options: 1 (yes and
 *                      activate immediately), -1 (include dark.css but not enable by default, also enables auto mode
 *                      for Safari)
 * custom_css           Comma-separated list of custom CSS files to load _after_ the main FEF CSS file, e.g.
 *                      media://com_foo/css/bar.min.css,media://com_foo/css/baz.min.css
 * remove_wrapper_classes  Comma-separated list of classes to REMOVE from the container
 * add_wrapper_classes     Comma-separated list of classes to ADD to the container
 *
 * Note: when Dark Mode is enabled the class akeeba-renderer-fef--dark is applied to the container DIV. You can use
 * remove_wrapper_classes to remove it e.g. when you want it to be enabled only through a JavaScript-powered toggle.
 *
 * @package FOF30\Render
 */
class FEF extends Joomla
{
	public function __construct(Container $container)
	{
		parent::__construct($container);

		$helperFile = JPATH_SITE . '/media/fef/fef.php';

		if (!class_exists('AkeebaFEFHelper') && is_file($helperFile))
		{
			include_once $helperFile;
		}

		$this->priority = 20;
		$this->enabled  = class_exists('AkeebaFEFHelper');
	}

	/**
	 * Echoes any HTML to show before the view template. We override it to load the CSS files required for FEF.
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function preRender(string $view, string $task): void
	{
		$useReset    = $this->getOption('fef_reset', true);
		$useFEF      = $this->getOption('load_fef', true);
		$useDarkMode = $this->getOption('fef_dark', 0);

		if ($useFEF && class_exists('AkeebaFEFHelper'))
		{
			AkeebaFEFHelper::load($useReset);

			if ($useDarkMode != 0)
			{
				$this->container->template->addCSS('media://fef/css/dark.min.css');
			}
		}

		parent::preRender($view, $task);
	}


	/**
	 * Opens the FEF styling wrapper element. Our component's output will be inside this wrapper.
	 *
	 * @param   array  $classes  An array of additional CSS classes to add to the outer page wrapper element.
	 *
	 * @return  void
	 */
	protected function openPageWrapper(array $classes): void
	{
		$useDarkMode = $this->getOption('fef_dark', false);

		if (($useDarkMode == 1) && !in_array('akeeba-renderer-fef--dark', $classes))
		{
			$classes[] = 'akeeba-renderer-fef--dark';
		}

		/**
		 * Remove wrapper classes. By default these are classes for the Joomla 3 sidebar which is not used in FEF
		 * components anymore.
		 */
		$removeClasses = $this->getOption('remove_wrapper_classes', [
			'j-toggle-main',
			'j-toggle-transition',
			'row-fluid',
		]);

		if (!is_array($removeClasses))
		{
			$removeClasses = explode(',', $removeClasses);
		}

		$removeClasses = array_map('trim', $removeClasses);

		foreach ($removeClasses as $class)
		{
			$x = array_search($class, $classes);

			if ($x !== false)
			{
				unset($classes[$x]);
			}
		}

		// Add the following classes to the wrapper div
		$addClasses = $this->getOption('add_wrapper_classes', '');

		if (!is_array($addClasses))
		{
			$addClasses = explode(',', $addClasses);
		}

		$addClasses    = array_map('trim', $addClasses);
		$customClasses = implode(' ', array_unique(array_merge($classes, $addClasses)));

		$id = $this->getOption('wrapper_id', 'akeeba-renderer-fef');
		$id = empty($id) ? "" : sprintf(' id="%s"', $id);

		echo <<< HTML
<div class="akeeba-renderer-fef $customClasses"$id>

HTML;
	}

	/**
	 * Close the FEF styling wrapper element.
	 *
	 * @return  void
	 */
	protected function closePageWrapper(): void
	{
		echo <<< HTML
</div>

HTML;

	}
}
Render/Joomla4.php000060400000002413152455514240010004 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Render;

defined('_JEXEC') || die;

use FOF30\Container\Container;

/**
 * Renderer class for use with Joomla! 4.x
 *
 * Renderer options
 *
 * wrapper_id              The ID of the wrapper DIV. Default: akeeba-renderjoomla
 * linkbar_style           Style for linkbars: joomla3|classic. Default: joomla3
 * remove_wrapper_classes  Comma-separated list of classes to REMOVE from the container
 * add_wrapper_classes     Comma-separated list of classes to ADD to the container
 *
 * @package FOF30\Render
 */
class Joomla4 extends Joomla
{
	public function __construct(Container $container)
	{
		$this->priority = 40;
		$this->enabled  = version_compare(JVERSION, '3.9.999', 'gt');

		parent::__construct($container);
	}

	/**
	 * Opens the FEF styling wrapper element. Our component's output will be inside this wrapper.
	 *
	 * @param   array  $classes  An array of additional CSS classes to add to the outer page wrapper element.
	 *
	 * @return  void
	 */
	protected function openPageWrapper(array $classes): void
	{
		$classes[] = 'akeeba-renderer-joomla4';

		parent::openPageWrapper($classes);
	}
}
Render/Joomla3.php000060400000002521152455514240010003 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Render;

defined('_JEXEC') || die;

use FOF30\Container\Container;

/**
 * Renderer class for use with Joomla! 3.x
 *
 * Renderer options
 *
 * wrapper_id              The ID of the wrapper DIV. Default: akeeba-renderjoomla
 * linkbar_style           Style for linkbars: joomla3|classic. Default: joomla3
 * remove_wrapper_classes  Comma-separated list of classes to REMOVE from the container
 * add_wrapper_classes     Comma-separated list of classes to ADD to the container
 *
 * @package FOF30\Render
 * @since   3.0.0
 */
class Joomla3 extends Joomla
{
	public function __construct(Container $container)
	{
		$this->priority = 55;
		$this->enabled  = version_compare(JVERSION, '3.0', 'ge') &&
			version_compare(JVERSION, '3.9.999', 'le');

		parent::__construct($container);
	}

	/**
	 * Opens the FEF styling wrapper element. Our component's output will be inside this wrapper.
	 *
	 * @param   array        $classes  An array of additional CSS classes to add to the outer page wrapper element.
	 *
	 * @return  void
	 */
	protected function openPageWrapper(array $classes): void
	{
		$classes[] = 'akeeba-renderer-joomla3';

		parent::openPageWrapper($classes);
	}

}
Render/RenderInterface.php000060400000004341152455514240011541 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Render;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use stdClass;

/**
 * Interface for FOF view renderers
 *
 * @package FOF30\Render
 * @since   3.0.0
 */
interface RenderInterface
{
	/**
	 * Public constructor
	 *
	 * @param   Container  $container  The container we are attached to
	 */
	function __construct(Container $container);

	/**
	 * Returns the information about this renderer
	 *
	 * @return object
	 */
	function getInformation(): stdClass;

	/**
	 * Echoes any HTML to show before the view template
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function preRender(string $view, string $task): void;

	/**
	 * Echoes any HTML to show after the view template
	 *
	 * @param   string  $view  The current view
	 * @param   string  $task  The current task
	 *
	 * @return  void
	 */
	function postRender(string $view, string $task): void;

	/**
	 * Renders the submenu (link bar) for a category view when it is used in a
	 * extension
	 *
	 * Note: this function has to be called from the addSubmenu function in
	 *         the ExtensionNameHelper class located in
	 *         administrator/components/com_ExtensionName/helpers/Extensionname.php
	 *
	 * @return  void
	 */
	function renderCategoryLinkbar(): void;

	/**
	 * Set a renderer option (depends on the renderer)
	 *
	 * @param   string  $key    The name of the option to set
	 * @param   mixed   $value  The value of the option
	 *
	 * @return  void
	 */
	function setOption(string $key, $value = null): void;

	/**
	 * Set multiple renderer options at once (depends on the renderer)
	 *
	 * @param   array  $options  The options to set as key => value pairs
	 *
	 * @return  void
	 */
	function setOptions(array $options): void;

	/**
	 * Get the value of a renderer option
	 *
	 * @param   string  $key      The name of the parameter
	 * @param   mixed   $default  The default value to return if the parameter is not set
	 *
	 * @return  mixed  The parameter value
	 */
	function getOption(string $key, $default = null);
}
script.fof.php000060400000032254152455514240007343 0ustar00<?php
/**
 * @package     FOF
 * @copyright Copyright (c)2010-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

defined('_JEXEC') or die();

// Do not declare the class if it's already defined. We have to put this check otherwise while updating
// multiple components at once will result in a fatal error since the class lib_fof30InstallerScript
// is already declared
if (class_exists('lib_fof30InstallerScript', false))
{
	return;
}

class lib_fof30InstallerScript
{
	/**
	 * 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.999';

	/**
	 * The name of the subdirectory under JPATH_LIBRARIES where this version of FOF is installed.
	 *
	 * @var   string
	 */
	protected $libraryFolder = 'fof30';

	/**
	 * 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   JInstaller $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 (!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 package but you are currently using PHP  $version</p>";

				JLog::add($msg, JLog::WARNING, 'jerror');

				return false;
			}
		}

		// Check the minimum Joomla! version
		if (!empty($this->minimumJoomlaVersion) && !version_compare(JVERSION, $this->minimumJoomlaVersion, 'ge'))
		{
			$jVersion = JVERSION;
			$msg      = "<p>You need Joomla! $this->minimumJoomlaVersion or later to install this package but you only have $jVersion installed.</p>";

			JLog::add($msg, JLog::WARNING, 'jerror');

			return false;
		}

		// Check the maximum Joomla! version
		if (!empty($this->maximumJoomlaVersion) && !version_compare(JVERSION, $this->maximumJoomlaVersion, 'le'))
		{
			$jVersion = JVERSION;
			$msg      = "<p>You need Joomla! $this->maximumJoomlaVersion or earlier to install this package but you have $jVersion installed</p>";

			JLog::add($msg, JLog::WARNING, 'jerror');

			return false;
		}

		// In case of an update, discovery etc I need to check if I am an update
		if (($type != 'install') && !$this->amIAnUpdate($parent))
		{
			$msg = "<p>You have a newer version of FOF installed. If you want to downgrade please uninstall FOF and install the older version.</p>";

			JLog::add($msg, JLog::WARNING, 'jerror');

			return false;
		}

		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   JInstallerAdapterLibrary $parent Parent object
	 */
	public function postflight($type, JInstallerAdapterLibrary $parent)
	{
		if ($type == 'update')
		{
			$this->bugfixFilesNotCopiedOnUpdate($parent);
		}

		$this->loadFOF30();

		if (!defined('FOF30_INCLUDED'))
		{
			return;
		}

		// Install or update database
		$db = JFactory::getDbo();

		/** @var JInstaller $grandpa */
		$grandpa   = $parent->getParent();
		$src       = $grandpa->getPath('source');
		$sqlSource = $src . '/fof/sql';

		// If we have an uppercase db prefix we can expect the database update to fail because we cannot detect reliably
		// the existence of database tables. See https://github.com/joomla/joomla-cms/issues/10928#issuecomment-228549658
		$prefix  = $db->getPrefix();
		$canFail = preg_match('/[A-Z]/', $prefix);

		try
		{
			$dbInstaller = new FOF30\Database\Installer($db, $sqlSource);
			$dbInstaller->updateSchema();
		}
		catch (\Exception $e)
		{
			if (!$canFail)
			{
				throw $e;
			}
		}

		// Since we're adding common table, I have to nuke the installer cache, otherwise checks on their existence would fail
		$dbInstaller->nukeCache();

		// Clear the FOF cache
		$fakeController = \FOF30\Container\Container::getInstance('com_FOOBAR');
		$fakeController->platform->clearCache();
	}

	/**
	 * Runs on uninstallation
	 *
	 * @param   JInstallerAdapterLibrary $parent The parent object
	 *
	 * @throws  RuntimeException  If the uninstallation is not allowed
	 */
	public function uninstall(JInstallerAdapterLibrary $parent)
	{
		// Check dependencies on FOF
		$dependencyCount = count($this->getDependencies('fof30'));

		if ($dependencyCount)
		{
			$msg = "<p>You have $dependencyCount extension(s) depending on this version of FOF. The package cannot be uninstalled unless these extensions are uninstalled first.</p>";

			JLog::add($msg, JLog::WARNING, 'jerror');

			throw new RuntimeException($msg, 500);
		}
	}

	/**
	 * Is this package an update to the currently installed FOF? If not (we're a downgrade) we will return false
	 * and prevent the installation from going on.
	 *
	 * @param   JInstallerAdapterLibrary $parent The parent object
	 *
	 * @return  array  The installation status
	 */
	protected function amIAnUpdate(JInstallerAdapterLibrary $parent)
	{
		/** @var JInstaller $grandpa */
		$grandpa = $parent->getParent();

		$source = $grandpa->getPath('source');

		$target = JPATH_LIBRARIES . '/fof30';

		// If FOF is not really installed (someone removed the directory instead of uninstalling?) I have to install it.
		if (!JFolder::exists($target))
		{
			return true;
		}

		$fofVersion = array();

		if (JFile::exists($target . '/version.txt'))
		{
			$rawData                 = @file_get_contents($target . '/version.txt');
			$rawData                 = ($rawData === false) ? "0.0.0\n2011-01-01\n" : $rawData;
			$info                    = explode("\n", $rawData);
			$fofVersion['installed'] = array(
				'version' => trim($info[0]),
				'date'    => new JDate(trim($info[1])),
			);
		}
		else
		{
			$fofVersion['installed'] = array(
				'version' => '0.0',
				'date'    => new JDate('2011-01-01'),
			);
		}

		$rawData               = @file_get_contents($source . '/version.txt');
		$rawData               = ($rawData === false) ? "0.0.0\n2011-01-01\n" : $rawData;
		$info                  = explode("\n", $rawData);
		$fofVersion['package'] = array(
			'version' => trim($info[0]),
			'date'    => new JDate(trim($info[1])),
		);

		$haveToInstallFOF = $fofVersion['package']['date']->toUNIX() >= $fofVersion['installed']['date']->toUNIX();

		return $haveToInstallFOF;
	}

	/**
	 * Loads FOF 3.0 if it's not already loaded
	 */
	protected function loadFOF30()
	{
		// Load FOF if not already loaded
		if (!defined('FOF30_INCLUDED'))
		{
			$filePath = JPATH_LIBRARIES . '/fof30/include.php';

			if (!defined('FOF30_INCLUDED') && file_exists($filePath))
			{
				@include_once $filePath;
			}
		}
	}

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

	/**
	 * 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
	 *
	 * @return  void
	 */
	protected function recursiveConditionalCopy($source, $dest)
	{
		// 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();

			// 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.
		}
	}

	/**
	 * 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.
	 *
	 * @param   \JInstallerAdapterComponent $parent
	 */
	protected function bugfixFilesNotCopiedOnUpdate($parent)
	{
		$source = $parent->getParent()->getPath('source') . '/fof';
		$target = JPATH_LIBRARIES . '/' . $this->libraryFolder;

		$this->recursiveConditionalCopy($source, $target);
	}
}
Download/Download.php000060400000026027152455514240010605 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Download;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Download\Exception\DownloadError;
use FOF30\Timer\Timer;
use Joomla\CMS\Language\Text;

class Download
{
	/**
	 * The component container object
	 *
	 * @var  Container
	 */
	protected $container = null;

	/**
	 * Parameters passed from the GUI when importing from URL
	 *
	 * @var  array
	 */
	private $params = [];

	/**
	 * The download adapter which will be used by this class
	 *
	 * @var  DownloadInterface
	 */
	private $adapter = null;

	/**
	 * Additional params that will be passed to the adapter while performing the download
	 *
	 * @var  array
	 */
	private $adapterOptions = [];

	/**
	 * Public constructor
	 *
	 * @param   Container  $c  The component container
	 */
	public function __construct(Container $c)
	{
		$this->container = $c;

		// Find the best fitting adapter
		$allAdapters = self::getFiles(__DIR__ . '/Adapter', [], ['AbstractAdapter.php']);
		$priority    = 0;

		foreach ($allAdapters as $adapterInfo)
		{
			/** @var Adapter\AbstractAdapter $adapter */
			$adapter = new $adapterInfo['classname'];

			if (!$adapter->isSupported())
			{
				continue;
			}

			if ($adapter->priority > $priority)
			{
				$this->adapter = $adapter;
				$priority      = $adapter->priority;
			}
		}

		// Load the language strings
		$c->platform->loadTranslations('lib_fof30');
	}

	/**
	 * This method will crawl a starting directory and get all the valid files
	 * that will be analyzed by __construct. Then it organizes them into an
	 * associative array.
	 *
	 * @param   string  $path           Folder where we should start looking
	 * @param   array   $ignoreFolders  Folder ignore list
	 * @param   array   $ignoreFiles    File ignore list
	 *
	 * @return  array   Associative array, where the `fullpath` key contains the path to the file,
	 *                  and the `classname` key contains the name of the class
	 */
	protected static function getFiles($path, array $ignoreFolders = [], array $ignoreFiles = [])
	{
		$return = [];

		$files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles);

		// Ok, I got the files, now I have to organize them
		foreach ($files as $file)
		{
			$clean = str_replace($path, '', $file);
			$clean = trim(str_replace('\\', '/', $clean), '/');

			$parts = explode('/', $clean);

			$return[] = [
				'fullpath'  => $file,
				'classname' => '\\FOF30\\Download\\Adapter\\' . ucfirst(basename($parts[0], '.php')),
			];
		}

		return $return;
	}

	/**
	 * Recursive function that will scan every directory unless it's in the
	 * ignore list. Files that aren't in the ignore list are returned.
	 *
	 * @param   string  $path           Folder where we should start looking
	 * @param   array   $ignoreFolders  Folder ignore list
	 * @param   array   $ignoreFiles    File ignore list
	 *
	 * @return  array   List of all the files
	 */
	protected static function scanDirectory($path, array $ignoreFolders = [], array $ignoreFiles = [])
	{
		$return = [];

		$handle = @opendir($path);

		if (!$handle)
		{
			return $return;
		}

		while (($file = readdir($handle)) !== false)
		{
			if ($file == '.' || $file == '..')
			{
				continue;
			}

			$fullpath = $path . '/' . $file;

			if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles)))
			{
				continue;
			}

			if (is_dir($fullpath))
			{
				$return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return);
			}
			else
			{
				$return[] = $path . '/' . $file;
			}
		}

		return $return;
	}

	/**
	 * Forces the use of a specific adapter
	 *
	 * @param   string  $className  The name of the class or the name of the adapter
	 */
	public function setAdapter($className)
	{
		$adapter = null;

		if (class_exists($className, true))
		{
			$adapter = new $className;
		}
		elseif (class_exists('\\FOF30\\Download\\Adapter\\' . ucfirst($className)))
		{
			$className = '\\FOF30\\Download\\Adapter\\' . ucfirst($className);
			$adapter   = new $className;
		}

		if (is_object($adapter) && ($adapter instanceof DownloadInterface))
		{
			$this->adapter = $adapter;
		}
	}

	/**
	 * Returns the name of the current adapter
	 *
	 * @return string
	 */
	public function getAdapterName()
	{
		if (is_object($this->adapter))
		{
			$class = get_class($this->adapter);

			return strtolower(str_ireplace('FOF30\\Download\\Adapter\\', '', $class));
		}

		return '';
	}

	/**
	 * Returns the additional options for the adapter
	 *
	 * @return array
	 *
	 * @codeCoverageIgnore
	 */
	public function getAdapterOptions()
	{
		return $this->adapterOptions;
	}

	/**
	 * Sets the additional options for the adapter
	 *
	 * @param   array  $options
	 *
	 * @codeCoverageIgnore
	 */
	public function setAdapterOptions(array $options)
	{
		$this->adapterOptions = $options;
	}

	/**
	 * Download data from a URL and return it.
	 *
	 * Important note about ranges: byte ranges start at 0. This means that the first 500 bytes of a file are from 0
	 * to 499, NOT from 1 to 500. If you ask more bytes than there are in the file or a range which is invalid or does
	 * not exist this method will return false.
	 *
	 * @param   string  $url   The URL to download from
	 * @param   int     $from  Byte range to start downloading from. Use null (default) for start of file.
	 * @param   int     $to    Byte range to stop downloading. Use null to download the entire file ($from will be
	 *                         ignored!)
	 *
	 * @return  bool|string  The downloaded data or false on failure
	 */
	public function getFromURL($url, $from = null, $to = null)
	{
		try
		{
			return $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
		}
		catch (DownloadError $e)
		{
			return false;
		}
	}

	/**
	 * Performs the staggered download of file.
	 *
	 * @param   array  $params  A parameters array, as sent by the user interface
	 *
	 * @return  array  A return status array
	 */
	public function importFromURL($params)
	{
		$this->params = $params;

		// Fetch data
		$url           = $this->getParam('url');
		$localFilename = $this->getParam('localFilename');
		$frag          = $this->getParam('frag', -1);
		$totalSize     = $this->getParam('totalSize', -1);
		$doneSize      = $this->getParam('doneSize', -1);
		$maxExecTime   = $this->getParam('maxExecTime', 5);
		$runTimeBias   = $this->getParam('runTimeBias', 75);
		$length        = $this->getParam('length', 1048576);

		if (empty($localFilename))
		{
			$localFilename = basename($url);

			if (strpos($localFilename, '?') !== false)
			{
				$paramsPos     = strpos($localFilename, '?');
				$localFilename = substr($localFilename, 0, $paramsPos - 1);

				$platformBaseDirectories = $this->container->platform->getPlatformBaseDirs();
				$tmpDir                  = $platformBaseDirectories['tmp'];
				$tmpDir                  = rtrim($tmpDir, '/\\');

				$localFilename = $tmpDir . '/' . $localFilename;
			}
		}

		// Init retArray
		$retArray = [
			"status"    => true,
			"error"     => '',
			"frag"      => $frag,
			"totalSize" => $totalSize,
			"doneSize"  => $doneSize,
			"percent"   => 0,
			"localfile" => $localFilename,
		];

		try
		{
			$timer = new Timer($maxExecTime, $runTimeBias);
			$start = $timer->getRunningTime(); // Mark the start of this download
			$break = false; // Don't break the step

			do
			{
				// Do we have to initialize the file?
				if ($frag == -1)
				{
					// Currently downloaded size
					$doneSize = 0;

					if (@file_exists($localFilename))
					{
						@unlink($localFilename);
					}

					// Delete and touch the output file
					$fp = @fopen($localFilename, 'wb');

					if ($fp !== false)
					{
						@fclose($fp);
					}

					// Init
					$frag = 0;

					$retArray['totalSize'] = $this->adapter->getFileSize($url);

					if ($retArray['totalSize'] <= 0)
					{
						$retArray['totalSize'] = 0;
					}

					$totalSize = $retArray['totalSize'];
				}

				// Calculate from and length
				$from = $frag * $length;
				$to   = $length + $from - 1;

				// Try to download the first frag
				$required_time = 1.0;

				$error = '';

				try
				{
					$result = $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
				}
				catch (DownloadError $e)
				{
					$result = false;
					$error  = $e->getMessage();
				}

				if ($result === false)
				{
					// Failed download
					if ($frag == 0)
					{
						// Failure to download first frag = failure to download. Period.
						$retArray['status'] = false;
						$retArray['error']  = $error;

						return $retArray;
					}
					else
					{
						// Since this is a staggered download, consider this normal and finish
						$frag      = -1;
						$totalSize = $doneSize;
						$break     = true;
					}
				}

				// Add the currently downloaded frag to the total size of downloaded files
				if ($result !== false)
				{
					$fileSize = strlen($result);
					$doneSize += $fileSize;

					// Append the file
					$fp = @fopen($localFilename, 'ab');

					if ($fp === false)
					{
						// Can't open the file for writing
						$retArray['status'] = false;
						$retArray['error']  = Text::sprintf('LIB_FOF_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE', $localFilename);

						return $retArray;
					}

					fwrite($fp, $result);
					fclose($fp);

					$frag++;

					if (($fileSize < $length) || ($fileSize > $length)
						|| (($totalSize == $doneSize) && ($totalSize > 0))
					)
					{
						// A partial download or a download larger than the frag size means we are done
						$frag = -1;
						//debugMsg("-- Import complete (partial download of last frag)");
						$totalSize = $doneSize;
						$break     = true;
					}
				}

				// Advance the frag pointer and mark the end
				$end = $timer->getRunningTime();

				// Do we predict that we have enough time?
				$required_time = max(1.1 * ($end - $start), $required_time);

				if ($required_time > (10 - $end + $start))
				{
					$break = true;
				}

				$start = $end;

			} while (($timer->getTimeLeft() > 0) && !$break);

			if ($frag == -1)
			{
				$percent = 100;
			}
			elseif ($doneSize <= 0)
			{
				$percent = 0;
			}
			else
			{
				if ($totalSize > 0)
				{
					$percent = 100 * ($doneSize / $totalSize);
				}
				else
				{
					$percent = 0;
				}
			}

			// Update $retArray
			$retArray = [
				"status"    => true,
				"error"     => '',
				"frag"      => $frag,
				"totalSize" => $totalSize,
				"doneSize"  => $doneSize,
				"percent"   => $percent,
			];
		}
		catch (DownloadError $e)
		{
			$retArray['status'] = false;
			$retArray['error']  = $e->getMessage();
		}

		return $retArray;
	}

	/**
	 * Used to decode the $params array
	 *
	 * @param   string  $key      The parameter key you want to retrieve the value for
	 * @param   mixed   $default  The default value, if none is specified
	 *
	 * @return  mixed  The value for this parameter key
	 */
	private function getParam($key, $default = null)
	{
		if (array_key_exists($key, $this->params))
		{
			return $this->params[$key];
		}

		return $default;
	}
}
Download/Exception/DownloadError.php000060400000000463152455514240013551 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Download\Exception;

defined('_JEXEC') || die;

use RuntimeException;

class DownloadError extends RuntimeException
{

}
Download/Adapter/Curl.php000060400000015542152455514240011323 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Download\Adapter;

defined('_JEXEC') || die;

use FOF30\Download\DownloadInterface;
use FOF30\Download\Exception\DownloadError;
use Joomla\CMS\Language\Text;

/**
 * A download adapter using the cURL PHP integration
 */
class Curl extends AbstractAdapter implements DownloadInterface
{
	protected $headers = [];

	public function __construct()
	{
		$this->priority              = 110;
		$this->supportsFileSize      = true;
		$this->supportsChunkDownload = true;
		$this->name                  = 'curl';
		$this->isSupported           = function_exists('curl_init') && function_exists('curl_exec') && function_exists('curl_close');
	}

	/**
	 * Download a part (or the whole) of a remote URL and return the downloaded
	 * data. You are supposed to check the size of the returned data. If it's
	 * smaller than what you expected you've reached end of file. If it's empty
	 * you have tried reading past EOF. If it's larger than what you expected
	 * the server doesn't support chunk downloads.
	 *
	 * If this class' supportsChunkDownload returns false you should assume
	 * that the $from and $to parameters will be ignored.
	 *
	 * @param   string   $url     The remote file's URL
	 * @param   integer  $from    Byte range to start downloading from. Use null for start of file.
	 * @param   integer  $to      Byte range to stop downloading. Use null to download the entire file ($from is
	 *                            ignored)
	 * @param   array    $params  Additional params that will be added before performing the download
	 *
	 * @return  string  The raw file data retrieved from the remote URL.
	 *
	 * @throws  DownloadError  A generic exception is thrown on error
	 */
	public function downloadAndReturn($url, $from = null, $to = null, array $params = [])
	{
		$ch = curl_init();

		if (empty($from))
		{
			$from = 0;
		}

		if (empty($to))
		{
			$to = 0;
		}

		if ($to < $from)
		{
			$temp = $to;
			$to   = $from;
			$from = $temp;
			unset($temp);
		}

		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
		curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_SSLVERSION, 0);
		curl_setopt($ch, CURLOPT_CAINFO, JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem');
		curl_setopt($ch, CURLOPT_HEADERFUNCTION, [$this, 'reponseHeaderCallback']);

		if (!(empty($from) && empty($to)))
		{
			curl_setopt($ch, CURLOPT_RANGE, "$from-$to");
		}

		if (!is_array($params))
		{
			$params = [];
		}

		$patched_accept_encoding = false;

		// Work around LiteSpeed sending compressed output under HTTP/2 when no encoding was requested
		// See https://github.com/joomla/joomla-cms/issues/21423#issuecomment-410941000
		if (defined('CURLOPT_ACCEPT_ENCODING'))
		{
			if (!array_key_exists(CURLOPT_ACCEPT_ENCODING, $params))
			{
				$params[CURLOPT_ACCEPT_ENCODING] = 'identity';
			}

			$patched_accept_encoding = true;
		}

		if (!empty($params))
		{
			foreach ($params as $k => $v)
			{
				// I couldn't patch the accept encoding header (missing constant), so I'll check if we manually set it
				if (!$patched_accept_encoding && $k == CURLOPT_HTTPHEADER)
				{
					foreach ($v as $custom_header)
					{
						// Ok, we explicitly set the Accept-Encoding header, so we consider it patched
						if (stripos($custom_header, 'Accept-Encoding') !== false)
						{
							$patched_accept_encoding = true;
						}
					}
				}

				@curl_setopt($ch, $k, $v);
			}
		}

		// Accept encoding wasn't patched, let's manually do that
		if (!$patched_accept_encoding)
		{
			@curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept-Encoding: identity']);

			$patched_accept_encoding = true;
		}

		$result = curl_exec($ch);

		$errno       = curl_errno($ch);
		$errmsg      = curl_error($ch);
		$error       = '';
		$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

		if ($result === false)
		{
			$error = Text::sprintf('LIB_FOF_DOWNLOAD_ERR_CURL_ERROR', $errno, $errmsg);
		}
		elseif (($http_status >= 300) && ($http_status <= 399) && isset($this->headers['location']) && !empty($this->headers['location']))
		{
			return $this->downloadAndReturn($this->headers['location'], $from, $to, $params);
		}
		elseif ($http_status > 399)
		{
			$result = false;
			$errno  = $http_status;
			$error  = Text::sprintf('LIB_FOF_DOWNLOAD_ERR_HTTPERROR', $http_status);
		}

		curl_close($ch);

		if ($result === false)
		{
			throw new DownloadError($error, $errno);
		}
		else
		{
			return $result;
		}
	}

	/**
	 * Get the size of a remote file in bytes
	 *
	 * @param   string  $url  The remote file's URL
	 *
	 * @return  integer  The file size, or -1 if the remote server doesn't support this feature
	 */
	public function getFileSize($url)
	{
		$result = -1;

		$ch = curl_init();

		curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_SSLVERSION, 0);

		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_NOBODY, true);
		curl_setopt($ch, CURLOPT_HEADER, true);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($ch, CURLOPT_CAINFO, JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem');

		$data = curl_exec($ch);
		curl_close($ch);

		if ($data)
		{
			$content_length = "unknown";
			$status         = "unknown";
			$redirection    = null;

			if (preg_match("/^HTTP\/1\.[01] (\d\d\d)/i", $data, $matches))
			{
				$status = (int) $matches[1];
			}

			if (preg_match("/Content-Length: (\d+)/i", $data, $matches))
			{
				$content_length = (int) $matches[1];
			}

			if (preg_match("/Location: (.*)/i", $data, $matches))
			{
				$redirection = (int) $matches[1];
			}

			if ($status == 200 || ($status > 300 && $status <= 308))
			{
				$result = $content_length;
			}

			if (($status > 300) && ($status <= 308))
			{
				if (!empty($redirection))
				{
					return $this->getFileSize($redirection);
				}

				return -1;
			}
		}

		return $result;
	}

	/**
	 * Handles the HTTP headers returned by cURL
	 *
	 * @param   resource  $ch    cURL resource handle (unused)
	 * @param   string    $data  Each header line, as returned by the server
	 *
	 * @return  int  The length of the $data string
	 */
	protected function reponseHeaderCallback(&$ch, &$data)
	{
		$strlen = strlen($data);

		if (($strlen) <= 2)
		{
			return $strlen;
		}

		if (substr($data, 0, 4) == 'HTTP')
		{
			return $strlen;
		}

		if (strpos($data, ':') === false)
		{
			return $strlen;
		}

		[$header, $value] = explode(': ', trim($data), 2);

		$this->headers[strtolower($header)] = $value;

		return $strlen;
	}
}
Download/Adapter/AbstractAdapter.php000060400000006547152455514240013467 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Download\Adapter;

defined('_JEXEC') || die;

use FOF30\Download\DownloadInterface;
use FOF30\Download\Exception\DownloadError;

abstract class AbstractAdapter implements DownloadInterface
{
	/**
	 * Load order priority
	 *
	 * @var  int
	 */
	public $priority = 100;

	/**
	 * Name of the adapter (identical to filename)
	 *
	 * @var  string
	 */
	public $name = '';

	/**
	 * Is this adapter supported in the current execution environment?
	 *
	 * @var  bool
	 */
	public $isSupported = false;

	/**
	 * Does this adapter support chunked downloads?
	 *
	 * @var  bool
	 */
	public $supportsChunkDownload = false;

	/**
	 * Does this adapter support querying the remote file's size?
	 *
	 * @var  bool
	 */
	public $supportsFileSize = false;

	/**
	 * Does this download adapter support downloading files in chunks?
	 *
	 * @return  boolean  True if chunk download is supported
	 */
	public function supportsChunkDownload()
	{
		return $this->supportsChunkDownload;
	}

	/**
	 * Does this download adapter support reading the size of a remote file?
	 *
	 * @return  boolean  True if remote file size determination is supported
	 */
	public function supportsFileSize()
	{
		return $this->supportsFileSize;
	}

	/**
	 * Is this download class supported in the current server environment?
	 *
	 * @return  boolean  True if this server environment supports this download class
	 */
	public function isSupported()
	{
		return $this->isSupported;
	}

	/**
	 * Get the priority of this adapter. If multiple download adapters are
	 * supported on a site, the one with the highest priority will be
	 * used.
	 *
	 * @return  boolean
	 */
	public function getPriority()
	{
		return $this->priority;
	}

	/**
	 * Returns the name of this download adapter in use
	 *
	 * @return  string
	 */
	public function getName()
	{
		return $this->name;
	}

	/**
	 * Download a part (or the whole) of a remote URL and return the downloaded
	 * data. You are supposed to check the size of the returned data. If it's
	 * smaller than what you expected you've reached end of file. If it's empty
	 * you have tried reading past EOF. If it's larger than what you expected
	 * the server doesn't support chunk downloads.
	 *
	 * If this class' supportsChunkDownload returns false you should assume
	 * that the $from and $to parameters will be ignored.
	 *
	 * @param   string   $url     The remote file's URL
	 * @param   integer  $from    Byte range to start downloading from. Use null for start of file.
	 * @param   integer  $to      Byte range to stop downloading. Use null to download the entire file ($from is
	 *                            ignored)
	 * @param   array    $params  Additional params that will be added before performing the download
	 *
	 * @return  string  The raw file data retrieved from the remote URL.
	 *
	 * @throws  DownloadError  A generic exception is thrown on error
	 */
	public function downloadAndReturn($url, $from = null, $to = null, array $params = [])
	{
		return '';
	}

	/**
	 * Get the size of a remote file in bytes
	 *
	 * @param   string  $url  The remote file's URL
	 *
	 * @return  integer  The file size, or -1 if the remote server doesn't support this feature
	 */
	public function getFileSize($url)
	{
		return -1;
	}
}
Download/Adapter/Fopen.php000060400000010010152455514240011446 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Download\Adapter;

defined('_JEXEC') || die;

use FOF30\Download\DownloadInterface;
use FOF30\Download\Exception\DownloadError;
use Joomla\CMS\Language\Text;

/**
 * A download adapter using URL fopen() wrappers
 */
class Fopen extends AbstractAdapter implements DownloadInterface
{
	public function __construct()
	{
		$this->priority              = 100;
		$this->supportsFileSize      = false;
		$this->supportsChunkDownload = true;
		$this->name                  = 'fopen';

		// If we are not allowed to use ini_get, we assume that URL fopen is disabled
		if (!function_exists('ini_get'))
		{
			$this->isSupported = false;
		}
		else
		{
			$this->isSupported = ini_get('allow_url_fopen');
		}
	}

	/**
	 * Download a part (or the whole) of a remote URL and return the downloaded
	 * data. You are supposed to check the size of the returned data. If it's
	 * smaller than what you expected you've reached end of file. If it's empty
	 * you have tried reading past EOF. If it's larger than what you expected
	 * the server doesn't support chunk downloads.
	 *
	 * If this class' supportsChunkDownload returns false you should assume
	 * that the $from and $to parameters will be ignored.
	 *
	 * @param   string   $url     The remote file's URL
	 * @param   integer  $from    Byte range to start downloading from. Use null for start of file.
	 * @param   integer  $to      Byte range to stop downloading. Use null to download the entire file ($from is
	 *                            ignored)
	 * @param   array    $params  Additional params that will be added before performing the download
	 *
	 * @return  string  The raw file data retrieved from the remote URL.
	 *
	 * @throws  DownloadError  A generic exception is thrown on error
	 */
	public function downloadAndReturn($url, $from = null, $to = null, array $params = [])
	{
		if (empty($from))
		{
			$from = 0;
		}

		if (empty($to))
		{
			$to = 0;
		}

		if ($to < $from)
		{
			$temp = $to;
			$to   = $from;
			$from = $temp;
			unset($temp);
		}


		if (!(empty($from) && empty($to)))
		{
			$options = [
				'http' => [
					'method' => 'GET',
					'header' => "Range: bytes=$from-$to\r\n",
				],
				'ssl'  => [
					'verify_peer'  => true,
					'cafile'       => JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem',
					'verify_depth' => 5,
				],
			];

			$options = array_merge($options, $params);

			$context = stream_context_create($options);
			$result  = @file_get_contents($url, false, $context, $from - $to + 1);
		}
		else
		{
			$options = [
				'http' => [
					'method' => 'GET',
				],
				'ssl'  => [
					'verify_peer'  => true,
					'cafile'       => JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem',
					'verify_depth' => 5,
				],
			];

			$options = array_merge($options, $params);

			$context = stream_context_create($options);
			$result  = @file_get_contents($url, false, $context);
		}

		global $http_response_header_test;

		if (!isset($http_response_header) && empty($http_response_header_test))
		{
			$error = Text::_('LIB_FOF_DOWNLOAD_ERR_FOPEN_ERROR');
			throw new DownloadError($error, 404);
		}
		else
		{
			// Used for testing
			if (!isset($http_response_header) && !empty($http_response_header_test))
			{
				$http_response_header = $http_response_header_test;
			}

			$http_code = 200;
			$nLines    = count($http_response_header);

			for ($i = $nLines - 1; $i >= 0; $i--)
			{
				$line = $http_response_header[$i];
				if (strncasecmp("HTTP", $line, 4) == 0)
				{
					$response  = explode(' ', $line);
					$http_code = $response[1];
					break;
				}
			}

			if ($http_code >= 299)
			{
				$error = Text::sprintf('LIB_FOF_DOWNLOAD_ERR_HTTPERROR', $http_code);
				throw new DownloadError($error, $http_code);
			}
		}

		if ($result === false)
		{
			$error = Text::sprintf('LIB_FOF_DOWNLOAD_ERR_FOPEN_ERROR');
			throw new DownloadError($error, 1);
		}
		else
		{
			return $result;
		}
	}
}
Download/DownloadInterface.php000060400000005136152455514240012424 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Download;

defined('_JEXEC') || die;

use FOF30\Download\Exception\DownloadError;

/**
 * Interface DownloadInterface
 *
 * @codeCoverageIgnore
 */
interface DownloadInterface
{
	/**
	 * Does this download adapter support downloading files in chunks?
	 *
	 * @return  boolean  True if chunk download is supported
	 */
	public function supportsChunkDownload();

	/**
	 * Does this download adapter support reading the size of a remote file?
	 *
	 * @return  boolean  True if remote file size determination is supported
	 */
	public function supportsFileSize();

	/**
	 * Is this download class supported in the current server environment?
	 *
	 * @return  boolean  True if this server environment supports this download class
	 */
	public function isSupported();

	/**
	 * Get the priority of this adapter. If multiple download adapters are
	 * supported on a site, the one with the highest priority will be
	 * used.
	 *
	 * @return  boolean
	 */
	public function getPriority();

	/**
	 * Returns the name of this download adapter in use
	 *
	 * @return  string
	 */
	public function getName();

	/**
	 * Download a part (or the whole) of a remote URL and return the downloaded
	 * data. You are supposed to check the size of the returned data. If it's
	 * smaller than what you expected you've reached end of file. If it's empty
	 * you have tried reading past EOF. If it's larger than what you expected
	 * the server doesn't support chunk downloads.
	 *
	 * If this class' supportsChunkDownload returns false you should assume
	 * that the $from and $to parameters will be ignored.
	 *
	 * @param   string   $url     The remote file's URL
	 * @param   integer  $from    Byte range to start downloading from. Use null for start of file.
	 * @param   integer  $to      Byte range to stop downloading. Use null to download the entire file ($from is
	 *                            ignored)
	 * @param   array    $params  Additional params that will be added before performing the download
	 *
	 * @return  string  The raw file data retrieved from the remote URL.
	 *
	 * @throws  DownloadError  A generic exception is thrown on error
	 */
	public function downloadAndReturn($url, $from = null, $to = null, array $params = []);

	/**
	 * Get the size of a remote file in bytes
	 *
	 * @param   string  $url  The remote file's URL
	 *
	 * @return  integer  The file size, or -1 if the remote server doesn't support this feature
	 */
	public function getFileSize($url);
}
Utils/StringHelper.php000060400000004066152455514240010774 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use JLoader;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;

abstract class StringHelper
{
	/**
	 * Convert a string into a slug (alias), suitable for use in URLs. Please
	 * note that transliteration support is rudimentary at this stage.
	 *
	 * @param   string  $value  A string to convert to slug
	 *
	 * @return  string  The slug
	 *
	 * @deprecated  3.0  Use \JApplicationHelper::stringURLSafe instead
	 *
	 * @codeCoverageIgnore
	 */
	public static function toSlug($value)
	{
		if (class_exists('\JLog'))
		{
			Log::add('FOF30\\Utils\\StringHelper::toSlug is deprecated. Use \\JApplicationHelper::stringURLSafe instead', Log::WARNING, 'deprecated');
		}

		return ApplicationHelper::stringURLSafe($value);
	}

	/**
	 * Convert common northern European languages' letters into plain ASCII. This
	 * is a rudimentary transliteration.
	 *
	 * @param   string  $value  The value to convert to ASCII
	 *
	 * @return  string  The converted string
	 *
	 * @deprecated   3.0  Use JFactory::getLanguage()->transliterate instead
	 *
	 * @codeCoverageIgnore
	 */
	public static function toASCII($value)
	{
		if (class_exists('\JLog'))
		{
			Log::add('FOF30\\Utils\\StringHelper::toASCII is deprecated. Use JFactory::getLanguage()->transliterate instead', Log::WARNING, 'deprecated');
		}

		$lang = Factory::getLanguage();

		return $lang->transliterate($value);
	}

	/**
	 * Convert a string to a boolean.
	 *
	 * @param   string  $string  The string.
	 *
	 * @return  boolean  The converted string
	 */
	public static function toBool($string)
	{
		$string = trim((string) $string);
		$string = strtolower($string);

		if (in_array($string, [1, 'true', 'yes', 'on', 'enabled'], true))
		{
			return true;
		}

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

		return (bool) $string;
	}
}
Utils/ComponentVersion.php000060400000006523152455514240011676 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Factory;
use SimpleXMLElement;

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

	/**
	 * Get a component's version. The XML manifest on disk will be tried first. If it's not there or does not have a
	 * version string the manifest cache in the database is tried. If that fails a fake version number will be returned.
	 *
	 * @param   string  $component  The name of the component, e.g. com_foobar
	 *
	 * @return  string  The version string
	 *
	 * @since   3.1.5
	 */
	public static function getFor($component)
	{
		if (!isset(self::$version[$component]))
		{
			self::$version[$component] = null;
		}

		if (is_null(self::$version[$component]))
		{
			self::$version[$component] = self::getVersionFromManifest($component);
		}

		if (is_null(self::$version[$component]))
		{
			self::$version[$component] = self::getVersionFromDatabase($component);
		}

		if (is_null(self::$version[$component]))
		{
			self::$version[$component] = 'dev-' . str_replace(' ', '_', microtime(false));
		}

		return self::$version[$component];
	}

	/**
	 * Get a component's version from the manifest cache in the database
	 *
	 * @param   string  $component  The component's name
	 *
	 * @return  string  The component version or null if none is defined
	 *
	 * @since   3.1.5
	 */
	private static function getVersionFromDatabase($component)
	{
		$db    = Factory::getDbo();
		$query = $db->getQuery(true)
			->select($db->qn('manifest_cache'))
			->from($db->qn('#__extensions'))
			->where($db->qn('element') . ' = ' . $db->q($component))
			->where($db->qn('type') . ' = ' . $db->q('component'));

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

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

		$options = json_decode($json, true);

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

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

		return $options['version'];
	}

	/**
	 * Get a component's version from the manifest file on disk. IMPORTANT! The manifest for com_something must be named
	 * something.xml.
	 *
	 * @param   string  $component  The component's name
	 *
	 * @return  string  The component version or null if none is defined
	 *
	 * @since   1.2.0
	 */
	private static function getVersionFromManifest($component)
	{
		$bareComponent = str_replace('com_', '', $component);
		$file          = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . $bareComponent . '.xml';

		if (!is_file($file) || !is_readable($file))
		{
			return null;
		}

		$data = @file_get_contents($file);

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

		try
		{
			$xml = new SimpleXMLElement($data, LIBXML_COMPACT | LIBXML_NONET | LIBXML_ERR_NONE);
		}
		catch (Exception $e)
		{
			return null;
		}

		$versionNode = $xml->xpath('/extension/version');

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

		return (string) ($versionNode[0]);
	}
}
Utils/Phpfunc.php000060400000001424152455514240007764 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

/**
 * Intercept calls to PHP functions.
 *
 * Based on the Session package of Aura for PHP – https://github.com/auraphp/Aura.Session
 *
 * @method  function_exists(string $function)
 * @method  hash_algos()
 */
class Phpfunc
{
	/**
	 *
	 * Magic call to intercept any function pass to it.
	 *
	 * @param   string  $func  The function to call.
	 *
	 * @param   array   $args  Arguments passed to the function.
	 *
	 * @return mixed The result of the function call.
	 *
	 */
	public function __call($func, $args)
	{
		return call_user_func_array($func, $args);
	}
}
Utils/InstallScript/Plugin.php000060400000017263152455514240012422 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils\InstallScript;

defined('_JEXEC') || die;

use Exception;
use FOF30\Database\Installer;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Log\Log;

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

/**
 * A helper class which you can use to create plugin installation scripts.
 *
 * Example usage: class PlgSystemExampleInstallerScript extends FOF30\Utils\InstallScript\Module
 *
 * NB: The class name is always Plg<Plugin Folder><Plugin Name>InstallerScript per Joomla's conventions.
 *
 * This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
 * Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
 */
class Plugin extends BaseInstaller
{
	/**
	 * The plugins's name, e.g. foobar (for plg_system_foobar). Auto-filled from the class name.
	 *
	 * @var   string
	 */
	protected $pluginName = '';

	/**
	 * The plugins's folder, e.g. system (for plg_system_foobar). Auto-filled from the class name.
	 *
	 * @var   string
	 */
	protected $pluginFolder = '';

	/**
	 * The path where the schema XML files are stored. The path is relative to the folder which contains the extension's
	 * files.
	 *
	 * @var string
	 */
	protected $schemaXmlPath = 'sql/xml';

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

			if (empty($this->pluginFolder))
			{
				$this->pluginFolder = $classParts[1];
			}

			if (empty($this->pluginName))
			{
				$this->pluginName = $classParts[2];
			}
		}
	}

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

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

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

		return true;
	}

	/**
	 * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
	 * or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
	 * database updates and similar housekeeping functions.
	 *
	 * @param   string            $type    install, update or discover_update
	 * @param   ComponentAdapter  $parent  Parent object
	 *
	 * @return  void
	 * @throws Exception
	 *
	 */
	public function postflight($type, $parent)
	{
		/**
		 * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
		 * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
		 * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
		 * uninstall).
		 */
		// Add ourselves to the list of extensions depending on FOF30
		// $this->addDependency('fof30', $this->getDependencyName());
		// Install or update database
		$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;

		if (@is_dir($schemaPath))
		{
			$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
			$dbInstaller->updateSchema();
		}

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

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

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

	/**
	 * Runs on uninstallation
	 *
	 * @param   ComponentAdapter  $parent  The parent object
	 */
	public function uninstall($parent)
	{
		// Uninstall database
		$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;

		// Uninstall database
		if (@is_dir($schemaPath))
		{
			$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
			$dbInstaller->removeSchema();
		}

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

		/**
		 * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
		 * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
		 * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
		 * uninstall).
		 */
		// Remove ourselves from the list of extensions depending on FOF30
		// $this->removeDependency('fof30', $this->getDependencyName());
	}

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

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

		$copyMap = [
			// Plugin files
			$temporarySource               => JPATH_ROOT . '/plugins/' . $this->pluginFolder . '/' . $this->pluginName,
			// Language (always stored in administrator for plugins)
			$temporarySource . '/language' => JPATH_ADMINISTRATOR . '/language',
			// Media files, e.g. /media/plg_system_foobar
			$temporarySource . '/media'    => JPATH_ROOT . '/media/' . $this->getDependencyName(),
		];

		foreach ($copyMap as $source => $target)
		{
			Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation');

			$ignored = [];

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

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

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

namespace FOF30\Utils\InstallScript;

defined('_JEXEC') || die;

use DirectoryIterator;
use Exception;
use FOFTemplateUtils;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;

class BaseInstaller
{
	/**
	 * The minimum PHP version required to install this extension
	 *
	 * @var   string
	 */
	protected $minimumPHPVersion = '7.2.0';

	/**
	 * The minimum Joomla! version required to install this extension
	 *
	 * @var   string
	 */
	protected $minimumJoomlaVersion = '3.3.0';

	/**
	 * The maximum Joomla! version this extension can be installed on
	 *
	 * @var   string
	 */
	protected $maximumJoomlaVersion = '4.0.99';

	/**
	 * Post-installation message definitions for Joomla! 3.2 or later.
	 *
	 * This array contains the message definitions for the Post-installation Messages component added in Joomla! 3.2 and
	 * later versions. Each element is also a hashed array. For the keys used in these message definitions please see
	 * addPostInstallationMessage
	 *
	 * @var   array
	 */
	protected $postInstallationMessages = [];

	/**
	 * Recursively copy a bunch of files, but only if the source and target file have a different size.
	 *
	 * @param   string  $source   Path to copy FROM
	 * @param   string  $dest     Path to copy TO
	 * @param   array   $ignored  List of entries to ignore (first level entries are taken into account)
	 *
	 * @return  void
	 */
	protected function recursiveConditionalCopy($source, $dest, $ignored = [])
	{
		// Make sure source and destination exist
		if (!@is_dir($source))
		{
			return;
		}

		if (!@is_dir($dest))
		{
			if (!@mkdir($dest, 0755))
			{
				Folder::create($dest, 0755);
			}
		}

		if (!@is_dir($dest))
		{
			$this->log(__CLASS__ . ": Cannot create folder $dest");

			return;
		}

		// List the contents of the source folder
		try
		{
			$di = new DirectoryIterator($source);
		}
		catch (Exception $e)
		{
			return;
		}

		// Process each entry
		foreach ($di as $entry)
		{
			// Ignore dot dirs (. and ..)
			if ($entry->isDot())
			{
				continue;
			}

			$sourcePath = $entry->getPathname();
			$fileName   = $entry->getFilename();

			// Do not copy ignored files
			if (!empty($ignored) && in_array($fileName, $ignored))
			{
				continue;
			}

			// If it's a directory do a recursive copy
			if ($entry->isDir())
			{
				$this->recursiveConditionalCopy($sourcePath, $dest . DIRECTORY_SEPARATOR . $fileName);

				continue;
			}

			// If it's a file check if it's missing or identical
			$mustCopy   = false;
			$targetPath = $dest . DIRECTORY_SEPARATOR . $fileName;

			if (!@is_file($targetPath))
			{
				$mustCopy = true;
			}
			else
			{
				$sourceSize = @filesize($sourcePath);
				$targetSize = @filesize($targetPath);

				$mustCopy = $sourceSize != $targetSize;
			}

			if (!$mustCopy)
			{
				continue;
			}

			if (!@copy($sourcePath, $targetPath))
			{
				if (!File::copy($sourcePath, $targetPath))
				{
					$this->log(__CLASS__ . ": Cannot copy $sourcePath to $targetPath");
				}
			}
		}
	}

	/**
	 * Try to log a warning / error with Joomla
	 *
	 * @param   string  $message   The message to write to the log
	 * @param   bool    $error     Is this an error? If not, it's a warning. (default: false)
	 * @param   string  $category  Log category, default jerror
	 *
	 * @return  void
	 */
	protected function log($message, $error = false, $category = 'jerror')
	{
		// Just in case...
		if (!class_exists('JLog', true))
		{
			return;
		}

		$priority = $error ? Log::ERROR : Log::WARNING;

		try
		{
			Log::add($message, $priority, $category);
		}
		catch (Exception $e)
		{
			// Swallow the exception.
		}
	}

	/**
	 * Check that the server meets the minimum PHP version requirements.
	 *
	 * @return  bool
	 */
	protected function checkPHPVersion()
	{
		if (!empty($this->minimumPHPVersion))
		{
			if (defined('PHP_VERSION'))
			{
				$version = PHP_VERSION;
			}
			elseif (function_exists('phpversion'))
			{
				$version = phpversion();
			}
			else
			{
				$version = '5.0.0'; // all bets are off!
			}

			if (!version_compare($version, $this->minimumPHPVersion, 'ge'))
			{
				$msg = "<p>You need PHP $this->minimumPHPVersion or later to install this extension</p>";

				$this->log($msg);

				return false;
			}
		}

		return true;
	}

	/**
	 * Check the minimum and maximum Joomla! versions for this extension
	 *
	 * @return  bool
	 */
	protected function checkJoomlaVersion()
	{
		if (!empty($this->minimumJoomlaVersion) && !version_compare(JVERSION, $this->minimumJoomlaVersion, 'ge'))
		{
			$msg = "<p>You need Joomla! $this->minimumJoomlaVersion or later to install this extension</p>";

			$this->log($msg);

			return false;
		}

		// Check the maximum Joomla! version
		if (!empty($this->maximumJoomlaVersion) && !version_compare(JVERSION, $this->maximumJoomlaVersion, 'le'))
		{
			$msg = "<p>You need Joomla! $this->maximumJoomlaVersion or earlier to install this extension</p>";

			$this->log($msg);

			return false;
		}

		return true;
	}

	/**
	 * Clear PHP opcode caches
	 *
	 * @return  void
	 */
	protected function clearOpcodeCaches()
	{
		// Always reset the OPcache if it's enabled. Otherwise there's a good chance the server will not know we are
		// replacing .php scripts. This is a major concern since PHP 5.5 included and enabled OPcache by default.
		if (function_exists('opcache_reset'))
		{
			opcache_reset();
		}
		// Also do that for APC cache
		elseif (function_exists('apc_clear_cache'))
		{
			@apc_clear_cache();
		}
	}

	/**
	 * Get the dependencies for a package from the #__akeeba_common table
	 *
	 * @param   string  $package  The package
	 *
	 * @return  array  The dependencies
	 */
	protected function getDependencies($package)
	{
		$db = Factory::getDbo();

		$query = $db->getQuery(true)
			->select($db->qn('value'))
			->from($db->qn('#__akeeba_common'))
			->where($db->qn('key') . ' = ' . $db->q($package));

		try
		{
			$dependencies = $db->setQuery($query)->loadResult();
			$dependencies = json_decode($dependencies, true);

			if (empty($dependencies))
			{
				$dependencies = [];
			}
		}
		catch (Exception $e)
		{
			$dependencies = [];
		}

		return $dependencies;
	}

	/**
	 * Sets the dependencies for a package into the #__akeeba_common table
	 *
	 * @param   string  $package       The package
	 * @param   array   $dependencies  The dependencies list
	 */
	protected function setDependencies($package, array $dependencies)
	{
		$db = Factory::getDbo();

		$query = $db->getQuery(true)
			->delete('#__akeeba_common')
			->where($db->qn('key') . ' = ' . $db->q($package));

		try
		{
			$db->setQuery($query)->execute();
		}
		catch (Exception $e)
		{
			// Do nothing if the old key wasn't found
		}

		$object = (object) [
			'key'   => $package,
			'value' => json_encode($dependencies),
		];

		try
		{
			$db->insertObject('#__akeeba_common', $object, 'key');
		}
		catch (Exception $e)
		{
			// Do nothing if the old key wasn't found
		}
	}

	/**
	 * Adds a package dependency to #__akeeba_common
	 *
	 * @param   string  $package     The package
	 * @param   string  $dependency  The dependency to add
	 */
	protected function addDependency($package, $dependency)
	{
		$dependencies = $this->getDependencies($package);

		if (!in_array($dependency, $dependencies))
		{
			$dependencies[] = $dependency;

			$this->setDependencies($package, $dependencies);
		}
	}

	/**
	 * Removes a package dependency from #__akeeba_common
	 *
	 * @param   string  $package     The package
	 * @param   string  $dependency  The dependency to remove
	 */
	protected function removeDependency($package, $dependency)
	{
		$dependencies = $this->getDependencies($package);

		if (in_array($dependency, $dependencies))
		{
			$index = array_search($dependency, $dependencies);
			unset($dependencies[$index]);

			$this->setDependencies($package, $dependencies);
		}
	}

	/**
	 * Do I have a dependency for a package in #__akeeba_common
	 *
	 * @param   string  $package     The package
	 * @param   string  $dependency  The dependency to check for
	 *
	 * @return bool
	 */
	protected function hasDependency($package, $dependency)
	{
		$dependencies = $this->getDependencies($package);

		return in_array($dependency, $dependencies);
	}

	/**
	 * Adds or updates a post-installation message (PIM) definition for Joomla! 3.2 or later. You can use this in your
	 * post-installation script using this code:
	 *
	 * The $options array contains the following mandatory keys:
	 *
	 * extension_id        The numeric ID of the extension this message is for (see the #__extensions table)
	 *
	 * type                One of message, link or action. Their meaning is:
	 *                    message        Informative message. The user can dismiss it.
	 *                    link        The action button links to a URL. The URL is defined in the action parameter.
	 *                  action      A PHP action takes place when the action button is clicked. You need to specify the
	 *                              action_file (RAD path to the PHP file) and action (PHP function name) keys. See
	 *                              below for more information.
	 *
	 * title_key        The JText language key for the title of this PIM
	 *                    Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE
	 *
	 * description_key    The JText language key for the main body (description) of this PIM
	 *                    Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION
	 *
	 * action_key        The JText language key for the action button. Ignored and not required when type=message
	 *                    Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION
	 *
	 * language_extension    The extension name which holds the language keys used above. For example, com_foobar,
	 *                    mod_something, plg_system_whatever, tpl_mytemplate
	 *
	 * language_client_id   Should we load the front-end (0) or back-end (1) language keys?
	 *
	 * version_introduced   Which was the version of your extension where this message appeared for the first time?
	 *                        Example: 3.2.1
	 *
	 * enabled              Must be 1 for this message to be enabled. If you omit it, it defaults to 1.
	 *
	 * condition_file        The RAD path to a PHP file containing a PHP function which determines whether this message
	 *                        should be shown to the user. @param   array  $options  See description
	 *
	 * @return  void
	 *
	 * @throws Exception
	 * @see                   Template::parsePath() for RAD path format. Joomla! will include this file
	 *                        before calling the function defined in the action key below.
	 *                        Example:   admin://components/com_foobar/helpers/postinstall.php
	 *
	 * action                The name of a PHP function which will be used to run the action of this PIM. This must be
	 * a
	 *                      simple PHP user function (not a class method, static method etc) which returns no result.
	 *                        Example: com_foobar_postinstall_messageone_action
	 *
	 * @see                   Template::parsePath() for RAD path format. Joomla!
	 *                        will include this file before calling the condition_method.
	 *                      Example:   admin://components/com_foobar/helpers/postinstall.php
	 *
	 * condition_method     The name of a PHP function which will be used to determine whether to show this message to
	 *                      the user. This must be a simple PHP user function (not a class method, static method etc)
	 *                        which returns true to show the message and false to hide it. This function is defined in
	 *                        the condition_file. Example: com_foobar_postinstall_messageone_condition
	 *
	 * When type=message no additional keys are required.
	 *
	 * When type=link the following additional keys are required:
	 *
	 * action                The URL which will open when the user clicks on the PIM's action button
	 *                        Example:    index.php?option=com_foobar&view=tools&task=installSampleData
	 *
	 * Then type=action the following additional keys are required:
	 *
	 * action_file            The RAD path to a PHP file containing a PHP function which performs the action of this
	 * PIM.
	 *
	 */
	protected function addPostInstallationMessage(array $options)
	{
		// Make sure there are options set
		if (!is_array($options))
		{
			throw new Exception('Post-installation message definitions must be of type array', 500);
		}

		// Initialise array keys
		$defaultOptions = [
			'extension_id'       => '',
			'type'               => '',
			'title_key'          => '',
			'description_key'    => '',
			'action_key'         => '',
			'language_extension' => '',
			'language_client_id' => '',
			'action_file'        => '',
			'action'             => '',
			'condition_file'     => '',
			'condition_method'   => '',
			'version_introduced' => '',
			'enabled'            => '1',
		];

		$options = array_merge($defaultOptions, $options);

		// Array normalisation. Removes array keys not belonging to a definition.
		$defaultKeys = array_keys($defaultOptions);
		$allKeys     = array_keys($options);
		$extraKeys   = array_diff($allKeys, $defaultKeys);

		if (!empty($extraKeys))
		{
			foreach ($extraKeys as $key)
			{
				unset($options[$key]);
			}
		}

		// Normalisation of integer values
		$options['extension_id']       = (int) $options['extension_id'];
		$options['language_client_id'] = (int) $options['language_client_id'];
		$options['enabled']            = (int) $options['enabled'];

		// Normalisation of 0/1 values
		foreach (['language_client_id', 'enabled'] as $key)
		{
			$options[$key] = $options[$key] ? 1 : 0;
		}

		// Make sure there's an extension_id
		if (!(int) $options['extension_id'])
		{
			throw new Exception('Post-installation message definitions need an extension_id', 500);
		}

		// Make sure there's a valid type
		if (!in_array($options['type'], ['message', 'link', 'action']))
		{
			throw new Exception('Post-installation message definitions need to declare a type of message, link or action', 500);
		}

		// Make sure there's a title key
		if (empty($options['title_key']))
		{
			throw new Exception('Post-installation message definitions need a title key', 500);
		}

		// Make sure there's a description key
		if (empty($options['description_key']))
		{
			throw new Exception('Post-installation message definitions need a description key', 500);
		}

		// If the type is anything other than message you need an action key
		if (($options['type'] != 'message') && empty($options['action_key']))
		{
			throw new Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500);
		}

		// You must specify the language extension
		if (empty($options['language_extension']))
		{
			throw new Exception('Post-installation message definitions need to specify which extension contains their language keys', 500);
		}

		// The action file and method are only required for the "action" type
		if ($options['type'] == 'action')
		{
			if (empty($options['action_file']))
			{
				throw new Exception('Post-installation message definitions need an action file when they are of type "action"', 500);
			}

			$file_path = FOFTemplateUtils::parsePath($options['action_file'], true);

			if (!@is_file($file_path))
			{
				throw new Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500);
			}

			if (empty($options['action']))
			{
				throw new Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500);
			}
		}

		if ($options['type'] == 'link')
		{
			if (empty($options['link']))
			{
				throw new Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500);
			}
		}

		// The condition file and method are only required when the type is not "message"
		if ($options['type'] != 'message')
		{
			if (empty($options['condition_file']))
			{
				throw new Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500);
			}

			$file_path = FOFTemplateUtils::parsePath($options['condition_file'], true);

			if (!@is_file($file_path))
			{
				throw new Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500);
			}

			if (empty($options['condition_method']))
			{
				throw new Exception('Post-installation message definitions need a condition method (function name) when they are of type "' . $options['type'] . '"', 500);
			}
		}

		// Check if the definition exists
		$tableName = '#__postinstall_messages';

		$db          = Factory::getDbo();
		$query       = $db->getQuery(true)
			->select('*')
			->from($db->qn($tableName))
			->where($db->qn('extension_id') . ' = ' . $db->q($options['extension_id']))
			->where($db->qn('type') . ' = ' . $db->q($options['type']))
			->where($db->qn('title_key') . ' = ' . $db->q($options['title_key']));
		$existingRow = $db->setQuery($query)->loadAssoc();

		// Is the existing definition the same as the one we're trying to save (ignore the enabled flag)?
		if (!empty($existingRow))
		{
			$same = true;

			foreach ($options as $k => $v)
			{
				if ($k == 'enabled')
				{
					continue;
				}

				if ($existingRow[$k] != $v)
				{
					$same = false;
					break;
				}
			}

			// Trying to add the same row as the existing one; quit
			if ($same)
			{
				return;
			}

			// Otherwise it's not the same row. Remove the old row before insert a new one.
			$query = $db->getQuery(true)
				->delete($db->qn($tableName))
				->where($db->q('extension_id') . ' = ' . $db->q($options['extension_id']))
				->where($db->q('type') . ' = ' . $db->q($options['type']))
				->where($db->q('title_key') . ' = ' . $db->q($options['title_key']));
			$db->setQuery($query)->execute();
		}

		// Insert the new row
		$options = (object) $options;
		$db->insertObject($tableName, $options);
	}

	/**
	 * Applies the post-installation messages for Joomla! 3.2 or later
	 *
	 * @return  void
	 */
	protected function _applyPostInstallationMessages()
	{
		// Make sure it's Joomla! 3.2.0 or later
		if (!version_compare(JVERSION, '3.2.0', 'ge'))
		{
			return;
		}

		// Make sure there are post-installation messages
		if (empty($this->postInstallationMessages))
		{
			return;
		}

		// Get the extension ID for our component
		$db    = Factory::getDbo();
		$query = $db->getQuery(true);
		$query->select('extension_id')
			->from('#__extensions')
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('element') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);

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

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

		$extension_id = array_shift($ids);

		foreach ($this->postInstallationMessages as $message)
		{
			$message['extension_id'] = $extension_id;
			$this->addPostInstallationMessage($message);
		}
	}

	/**
	 * Uninstalls the post-installation messages for Joomla! 3.2 or later
	 *
	 * @return  void
	 */
	protected function uninstallPostInstallationMessages()
	{
		// Make sure it's Joomla! 3.2.0 or later
		if (!version_compare(JVERSION, '3.2.0', 'ge'))
		{
			return;
		}

		// Make sure there are post-installation messages
		if (empty($this->postInstallationMessages))
		{
			return;
		}

		// Get the extension ID for our component
		$db    = Factory::getDbo();
		$query = $db->getQuery(true);
		$query->select('extension_id')
			->from('#__extensions')
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('element') . ' = ' . $db->q($this->componentName));
		$db->setQuery($query);

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

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

		$extension_id = array_shift($ids);

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

		try
		{
			$db->setQuery($query)->execute();
		}
		catch (Exception $e)
		{
			return;
		}
	}
}
Utils/InstallScript/Module.php000060400000016574152455514240012415 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils\InstallScript;

defined('_JEXEC') || die;

use Exception;
use FOF30\Database\Installer;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Log\Log;

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

/**
 * A helper class which you can use to create module installation scripts.
 *
 * Example usage: class Mod_ExampleInstallerScript extends FOF30\Utils\InstallScript\Module
 *
 * This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
 * Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
 */
class Module extends BaseInstaller
{
	/**
	 * Which side of the site is this module installed in? Use 'site' or 'administrator'.
	 *
	 * @var   string
	 */
	protected $moduleClient = 'site';

	/**
	 * The modules's name, e.g. mod_foobar. Auto-filled from the class name.
	 *
	 * @var   string
	 */
	protected $moduleName = '';

	/**
	 * The path where the schema XML files are stored. The path is relative to the folder which contains the extension's
	 * files.
	 *
	 * @var string
	 */
	protected $schemaXmlPath = 'sql/xml';


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

			$this->moduleName = 'mod_' . $classParts[2];
		}
	}

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

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

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

		return true;
	}

	/**
	 * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
	 * or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
	 * database updates and similar housekeeping functions.
	 *
	 * @param   string            $type    install, update or discover_update
	 * @param   ComponentAdapter  $parent  Parent object
	 *
	 * @return  void
	 * @throws Exception
	 *
	 */
	public function postflight($type, $parent)
	{
		/**
		 * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
		 * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
		 * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
		 * uninstall).
		 */
		// Add ourselves to the list of extensions depending on FOF30
		// $this->addDependency('fof30', $this->getDependencyName());
		// Install or update database
		$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;

		if (@is_dir($schemaPath))
		{
			$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
			$dbInstaller->updateSchema();
		}

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

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

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

	/**
	 * Runs on uninstallation
	 *
	 * @param   ComponentAdapter  $parent  The parent object
	 */
	public function uninstall($parent)
	{
		// Uninstall database
		$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;

		// Uninstall database
		if (@is_dir($schemaPath))
		{
			$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
			$dbInstaller->removeSchema();
		}

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

		/**
		 * We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
		 * works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
		 * uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
		 * uninstall).
		 */
		// Remove ourselves from the list of extensions depending on FOF30
		// $this->removeDependency('fof30', $this->getDependencyName());
	}

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

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

		$copyMap = [
			// Module files
			$temporarySource               => $rootFolder . '/modules/' . $this->moduleName,
			// Language
			$temporarySource . '/language' => $rootFolder . '/language',
			// Media files
			$temporarySource . '/media'    => JPATH_ROOT . '/media/' . $this->moduleName,
		];

		foreach ($copyMap as $source => $target)
		{
			Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation');

			$ignored = [];

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

			}

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

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

namespace FOF30\Utils\InstallScript;

defined('_JEXEC') || die;

use Exception;
use FOF30\Database\Installer as DatabaseInstaller;
use InvalidArgumentException;
use JDatabaseDriver;
use JError;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Table\Menu;
use Joomla\CMS\Table\Table;
use SimpleXMLElement;
use stdClass;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return true;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		$dbInstaller->removeSchema();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


		if ((is_array($ids) || $ids instanceof \Countable ? count($ids) : 0) > 1)
		{
			asort($ids);
			$extension_id = array_shift($ids); // Keep the oldest id

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

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

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

		if ((is_array($ids) || $ids instanceof \Countable ? count($ids) : 0) > 1)
		{
			asort($ids);
			$asset_id = array_shift($ids); // Keep the oldest id

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

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

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

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

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

				File::delete($f);
			}
		}

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

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

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

	/**
	 * Uninstalls obsolete sub-extensions (modules, plugins) bundled with the main extension
	 *
	 * @param   ComponentAdapter  $parent  The parent object
	 *
	 * @return  stdClass The sub-extension uninstallation status
	 */
	protected function uninstallObsoleteSubextensions($parent)
	{
		$db = Factory::getDBO();

		$status          = new stdClass();
		$status->modules = [];
		$status->plugins = [];

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

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

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

		return $status;
	}

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

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

		$db->setQuery($query);

		$existingMenus = $db->loadResult();

		if ($existingMenus)
		{
			return true;
		}

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

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

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

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

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

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

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

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

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

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

			return false;
		}

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

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

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

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

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

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

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

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

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

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

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

					return false;
				}
			}
		}

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

		/*
		 * Process SubMenus
		 */

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

		if (!$submenu)
		{
			return true;
		}

		$parent_id = $table->id;

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

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

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

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

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

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

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

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

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

			$table = Table::getInstance('menu');

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

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

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

		return true;
	}

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

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

		$db->setQuery($query);

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

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

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

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

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

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

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

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

		$table->rebuild($rootItemId);
	}

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

		$ids = $db->loadColumn();

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

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

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

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

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

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

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

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

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

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

			try
			{
				$db->execute();
			}
			catch (Exception $exc)
			{
				// Nothing
			}
		}
	}
}
Utils/helpers.php000060400000024233152455514240010026 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

defined('_JEXEC') || die;

/**
 * This is a modified copy of Laravel 4's "helpers.php"
 *
 * Laravel 4 is distributed under the MIT license, see https://github.com/laravel/framework/blob/master/LICENSE.txt
 */
if (!function_exists('array_add'))
{
	/**
	 * Add an element to an array if it doesn't exist.
	 *
	 * @param   array   $array
	 * @param   string  $key
	 * @param   mixed   $value
	 *
	 * @return array
	 */
	function array_add($array, $key, $value)
	{
		if (!isset($array[$key]))
		{
			$array[$key] = $value;
		}

		return $array;
	}
}

if (!function_exists('array_build'))
{
	/**
	 * Build a new array using a callback.
	 *
	 * @param   array    $array
	 * @param   Closure  $callback
	 *
	 * @return array
	 */
	function array_build($array, Closure $callback)
	{
		$results = [];

		foreach ($array as $key => $value)
		{
			[$innerKey, $innerValue] = call_user_func($callback, $key, $value);

			$results[$innerKey] = $innerValue;
		}

		return $results;
	}
}

if (!function_exists('array_divide'))
{
	/**
	 * Divide an array into two arrays. One with keys and the other with values.
	 *
	 * @param   array  $array
	 *
	 * @return array
	 */
	function array_divide($array)
	{
		return [array_keys($array), array_values($array)];
	}
}

if (!function_exists('array_dot'))
{
	/**
	 * Flatten a multi-dimensional associative array with dots.
	 *
	 * @param   array   $array
	 * @param   string  $prepend
	 *
	 * @return array
	 */
	function array_dot($array, $prepend = '')
	{
		$results = [];

		foreach ($array as $key => $value)
		{
			if (is_array($value))
			{
				$results = array_merge($results, array_dot($value, $prepend . $key . '.'));
			}
			else
			{
				$results[$prepend . $key] = $value;
			}
		}

		return $results;
	}
}

if (!function_exists('array_except'))
{
	/**
	 * Get all of the given array except for a specified array of items.
	 *
	 * @param   array  $array
	 * @param   array  $keys
	 *
	 * @return array
	 */
	function array_except($array, $keys)
	{
		return array_diff_key($array, array_flip((array) $keys));
	}
}

if (!function_exists('array_fetch'))
{
	/**
	 * Fetch a flattened array of a nested array element.
	 *
	 * @param   array   $array
	 * @param   string  $key
	 *
	 * @return array
	 */
	function array_fetch($array, $key)
	{
		foreach (explode('.', $key) as $segment)
		{
			$results = [];

			foreach ($array as $value)
			{
				$value = (array) $value;

				$results[] = $value[$segment];
			}

			$array = array_values($results);
		}

		return array_values($results);
	}
}

if (!function_exists('array_first'))
{
	/**
	 * Return the first element in an array passing a given truth test.
	 *
	 * @param   array    $array
	 * @param   Closure  $callback
	 * @param   mixed    $default
	 *
	 * @return mixed
	 */
	function array_first($array, $callback, $default = null)
	{
		foreach ($array as $key => $value)
		{
			if (call_user_func($callback, $key, $value))
			{
				return $value;
			}
		}

		return value($default);
	}
}

if (!function_exists('array_last'))
{
	/**
	 * Return the last element in an array passing a given truth test.
	 *
	 * @param   array    $array
	 * @param   Closure  $callback
	 * @param   mixed    $default
	 *
	 * @return mixed
	 */
	function array_last($array, $callback, $default = null)
	{
		return array_first(array_reverse($array), $callback, $default);
	}
}

if (!function_exists('array_flatten'))
{
	/**
	 * Flatten a multi-dimensional array into a single level.
	 *
	 * @param   array  $array
	 *
	 * @return array
	 */
	function array_flatten($array)
	{
		$return = [];

		array_walk_recursive($array, function ($x) use (&$return) {
			$return[] = $x;
		});

		return $return;
	}
}

if (!function_exists('array_forget'))
{
	/**
	 * Remove an array item from a given array using "dot" notation.
	 *
	 * @param   array   $array
	 * @param   string  $key
	 *
	 * @return void
	 */
	function array_forget(&$array, $key)
	{
		$keys = explode('.', $key);

		while (count($keys) > 1)
		{
			$key = array_shift($keys);

			if (!isset($array[$key]) || !is_array($array[$key]))
			{
				return;
			}

			$array =& $array[$key];
		}

		unset($array[array_shift($keys)]);
	}
}

if (!function_exists('array_get'))
{
	/**
	 * Get an item from an array using "dot" notation.
	 *
	 * @param   array   $array
	 * @param   string  $key
	 * @param   mixed   $default
	 *
	 * @return mixed
	 */
	function array_get($array, $key, $default = null)
	{
		if (is_null($key))
		{
			return $array;
		}

		if (isset($array[$key]))
		{
			return $array[$key];
		}

		foreach (explode('.', $key) as $segment)
		{
			if (!is_array($array) || !array_key_exists($segment, $array))
			{
				return value($default);
			}

			$array = $array[$segment];
		}

		return $array;
	}
}

if (!function_exists('array_only'))
{
	/**
	 * Get a subset of the items from the given array.
	 *
	 * @param   array  $array
	 * @param   array  $keys
	 *
	 * @return array
	 */
	function array_only($array, $keys)
	{
		return array_intersect_key($array, array_flip((array) $keys));
	}
}

if (!function_exists('array_pluck'))
{
	/**
	 * Pluck an array of values from an array.
	 *
	 * @param   array   $array
	 * @param   string  $value
	 * @param   string  $key
	 *
	 * @return array
	 */
	function array_pluck($array, $value, $key = null)
	{
		$results = [];

		foreach ($array as $item)
		{
			$itemValue = is_object($item) ? $item->{$value} : $item[$value];

			// If the key is "null", we will just append the value to the array and keep
			// looping. Otherwise we will key the array using the value of the key we
			// received from the developer. Then we'll return the final array form.
			if (is_null($key))
			{
				$results[] = $itemValue;
			}
			else
			{
				$itemKey = is_object($item) ? $item->{$key} : $item[$key];

				$results[$itemKey] = $itemValue;
			}
		}

		return $results;
	}
}

if (!function_exists('array_pull'))
{
	/**
	 * Get a value from the array, and remove it.
	 *
	 * @param   array   $array
	 * @param   string  $key
	 *
	 * @return mixed
	 */
	function array_pull(&$array, $key)
	{
		$value = array_get($array, $key);

		array_forget($array, $key);

		return $value;
	}
}

if (!function_exists('array_set'))
{
	/**
	 * Set an array item to a given value using "dot" notation.
	 *
	 * If no key is given to the method, the entire array will be replaced.
	 *
	 * @param   array   $array
	 * @param   string  $key
	 * @param   mixed   $value
	 *
	 * @return array
	 */
	function array_set(&$array, $key, $value)
	{
		if (is_null($key))
		{
			return $array = $value;
		}

		$keys = explode('.', $key);

		while (count($keys) > 1)
		{
			$key = array_shift($keys);

			// If the key doesn't exist at this depth, we will just create an empty array
			// to hold the next value, allowing us to create the arrays to hold final
			// values at the correct depth. Then we'll keep digging into the array.
			if (!isset($array[$key]) || !is_array($array[$key]))
			{
				$array[$key] = [];
			}

			$array =& $array[$key];
		}

		$array[array_shift($keys)] = $value;

		return $array;
	}
}

if (!function_exists('array_sort'))
{
	/**
	 * Sort the array using the given Closure.
	 *
	 * @param   array    $array
	 * @param   Closure  $callback
	 *
	 * @return array
	 */
	function array_sort($array, Closure $callback)
	{
		return FOF30\Utils\Collection::make($array)->sortBy($callback)->all();
	}
}

if (!function_exists('array_where'))
{
	/**
	 * Filter the array using the given Closure.
	 *
	 * @param   array    $array
	 * @param   Closure  $callback
	 *
	 * @return array
	 */
	function array_where($array, Closure $callback)
	{
		$filtered = [];

		foreach ($array as $key => $value)
		{
			if (call_user_func($callback, $key, $value))
			{
				$filtered[$key] = $value;
			}
		}

		return $filtered;
	}
}

if (!function_exists('ends_with'))
{
	/**
	 * Determine if a given string ends with a given substring.
	 *
	 * @param   string        $haystack
	 * @param   string|array  $needles
	 *
	 * @return bool
	 */
	function ends_with($haystack, $needles)
	{
		foreach ((array) $needles as $needle)
		{
			if ((string) $needle === substr($haystack, -strlen($needle)))
			{
				return true;
			}
		}

		return false;
	}
}

if (!function_exists('last'))
{
	/**
	 * Get the last element from an array.
	 *
	 * @param   array  $array
	 *
	 * @return mixed
	 */
	function last($array)
	{
		return end($array);
	}
}

if (!function_exists('object_get'))
{
	/**
	 * Get an item from an object using "dot" notation.
	 *
	 * @param   object  $object
	 * @param   string  $key
	 * @param   mixed   $default
	 *
	 * @return mixed
	 */
	function object_get($object, $key, $default = null)
	{
		if (is_null($key) || trim($key) == '')
		{
			return $object;
		}

		foreach (explode('.', $key) as $segment)
		{
			if (!is_object($object) || !isset($object->{$segment}))
			{
				return value($default);
			}

			$object = $object->{$segment};
		}

		return $object;
	}
}

if (!function_exists('preg_replace_sub'))
{
	/**
	 * Replace a given pattern with each value in the array in sequentially.
	 *
	 * @param   string  $pattern
	 * @param   array   $replacements
	 * @param   string  $subject
	 *
	 * @return string
	 */
	function preg_replace_sub($pattern, &$replacements, $subject)
	{
		return preg_replace_callback($pattern, function ($match) use (&$replacements) {
			return array_shift($replacements);

		}, $subject);
	}
}

if (!function_exists('starts_with'))
{
	/**
	 * Determine if a given string starts with a given substring.
	 *
	 * @param   string        $haystack
	 * @param   string|array  $needles
	 *
	 * @return bool
	 */
	function starts_with($haystack, $needles)
	{
		foreach ((array) $needles as $needle)
		{
			if ($needle != '' && strpos($haystack, $needle) === 0)
			{
				return true;
			}
		}

		return false;
	}
}

if (!function_exists('value'))
{
	/**
	 * Return the default value of the given value.
	 *
	 * @param   mixed  $value
	 *
	 * @return mixed
	 */
	function value($value)
	{
		return $value instanceof Closure ? $value() : $value;
	}
}

if (!function_exists('with'))
{
	/**
	 * Return the given object. Useful for chaining.
	 *
	 * @param   mixed  $object
	 *
	 * @return mixed
	 */
	function with($object)
	{
		return $object;
	}
}
Utils/FilesCheck.php000060400000016427152455514240010372 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use FOF30\Timer\Timer;
use Joomla\CMS\Factory;

/**
 * A utility class to check that your extension's files are not missing and have not been tampered with.
 *
 * You need a file called fileslist.php in your component's administrator root directory with the following contents:
 *
 * $phpFileChecker = array(
 *   'version' => 'revCEE2DAB',
 *   'date' => '2014-10-16',
 *   'directories' => array(
 *     'administrator/components/com_foobar',
 *     ....
 *   ),
 *   'files' => array(
 *     'administrator/components/com_foobar/access.xml' => array('705', '09aa0351a316bf011ecc8c1145134761',
 *     'b95f00c7b49a07a60570dc674f2497c45c4e7152'),
 *     ....
 *   )
 * );
 *
 * All directory and file paths are relative to the site's root
 *
 * The directories array is a list of directories which must exist. The files array has the file paths as keys. The
 * value is a simple array containing the following elements in this order: file size in bytes, MD5 checksum, SHA1
 * checksum.
 */
class FilesCheck
{
	/** @var string The name of the component */
	protected $option = '';

	/** @var string Current component version */
	protected $version = null;

	/** @var string Current component release date */
	protected $date = null;

	/** @var array List of files to check as filepath => (filesize, md5, sha1) */
	protected $fileList = [];

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

	/** @var bool Is the reported component version different than the version of the #__extensions table? */
	protected $wrongComponentVersion = false;

	/** @var bool Is the fileslist.php reporting a version different than the reported component version? */
	protected $wrongFilesVersion = false;

	/**
	 * Create and initialise the object
	 *
	 * @param   string  $option   Component name, e.g. com_foobar
	 * @param   string  $version  The current component version, as reported by the component
	 * @param   string  $date     The current component release date, as reported by the component
	 */
	public function __construct($option, $version, $date)
	{
		// Initialise from parameters
		$this->option  = $option;
		$this->version = $version;
		$this->date    = $date;

		// Retrieve the date and version from the #__extensions table
		$db        = Factory::getDbo();
		$query     = $db->getQuery(true)->select('*')->from($db->qn('#__extensions'))
			->where($db->qn('element') . ' = ' . $db->q($this->option))
			->where($db->qn('type') . ' = ' . $db->q('component'));
		$extension = $db->setQuery($query)->loadObject();

		// Check the version and date against those from #__extensions. I hate heavily nested IFs as much as the next
		// guy, but what can you do...
		if (!is_null($extension))
		{
			$manifestCache = $extension->manifest_cache;

			if (!empty($manifestCache))
			{
				$manifestCache = json_decode($manifestCache, true);

				if (is_array($manifestCache) && isset($manifestCache['creationDate']) && isset($manifestCache['version']))
				{
					// Make sure the fileslist.php version and date match the component's version
					if ($this->version != $manifestCache['version'])
					{
						$this->wrongComponentVersion = true;
					}

					if ($this->date != $manifestCache['creationDate'])
					{
						$this->wrongComponentVersion = true;
					}
				}
			}
		}

		// Try to load the fileslist.php file from the component's back-end root
		$filePath = JPATH_ADMINISTRATOR . '/components/' . $this->option . '/fileslist.php';

		if (!file_exists($filePath))
		{
			return;
		}

		$couldInclude = @include($filePath);

		// If we couldn't include the file with the array OR if it didn't define the array we have to quit.
		if (!$couldInclude || !isset($phpFileChecker))
		{
			return;
		}

		// Make sure the fileslist.php version and date match the component's version
		if ($this->version != $phpFileChecker['version'])
		{
			$this->wrongFilesVersion = true;
		}

		if ($this->date != $phpFileChecker['date'])
		{
			$this->wrongFilesVersion = true;
		}

		// Initialise the files and directories lists
		$this->fileList = $phpFileChecker['files'];
		$this->dirList  = $phpFileChecker['directories'];
	}

	/**
	 * Is the reported component version different than the version of the #__extensions table?
	 *
	 * @return boolean
	 */
	public function isWrongComponentVersion()
	{
		return $this->wrongComponentVersion;
	}

	/**
	 * Is the fileslist.php reporting a version different than the reported component version?
	 *
	 * @return boolean
	 */
	public function isWrongFilesVersion()
	{
		return $this->wrongFilesVersion;
	}

	/**
	 * Performs a fast check of file and folders. If even one of the files/folders doesn't exist, or even one file has
	 * the wrong file size it will return false.
	 *
	 * @return bool False when there are mismatched files and directories
	 */
	public function fastCheck()
	{
		// Check that all directories exist
		foreach ($this->dirList as $directory)
		{
			$directory = JPATH_ROOT . '/' . $directory;

			if (!@is_dir($directory))
			{
				return false;
			}
		}

		// Check that all files exist and have the right size
		foreach ($this->fileList as $filePath => $fileData)
		{
			$filePath = JPATH_ROOT . '/' . $filePath;

			if (!@file_exists($filePath))
			{
				return false;
			}

			$fileSize = @filesize($filePath);

			if ($fileSize != $fileData[0])
			{
				return false;
			}
		}

		return true;
	}

	/**
	 * Performs a slow, thorough check of all files and folders (including MD5/SHA1 sum checks)
	 *
	 * @param   int  $idx  The index from where to start
	 *
	 * @return array Progress report
	 */
	public function slowCheck($idx = 0)
	{
		$ret = [
			'done'    => false,
			'files'   => [],
			'folders' => [],
			'idx'     => $idx,
		];

		$totalFiles   = count($this->fileList);
		$totalFolders = count($this->dirList);
		$fileKeys     = array_keys($this->fileList);

		$timer = new Timer(3.0, 75.0);

		while ($timer->getTimeLeft() && (($idx < $totalFiles) || ($idx < $totalFolders)))
		{
			if ($idx < $totalFolders)
			{
				$directory = JPATH_ROOT . '/' . $this->dirList[$idx];

				if (!@is_dir($directory))
				{
					$ret['folders'][] = $directory;
				}
			}

			if ($idx < $totalFiles)
			{
				$fileKey  = $fileKeys[$idx];
				$filePath = JPATH_ROOT . '/' . $fileKey;
				$fileData = $this->fileList[$fileKey];

				if (!@file_exists($filePath))
				{
					$ret['files'][] = $fileKey . ' (missing)';
				}
				elseif (@filesize($filePath) != $fileData[0])
				{
					$ret['files'][] = $fileKey . ' (size ' . @filesize($filePath) . ' ≠ ' . $fileData[0] . ')';
				}
				else
				{
					if (function_exists('sha1_file'))
					{
						$fileSha1 = @sha1_file($filePath);

						if ($fileSha1 != $fileData[2])
						{
							$ret['files'][] = $fileKey . ' (SHA1 ' . $fileSha1 . ' ≠ ' . $fileData[2] . ')';
						}
					}
					elseif (function_exists('md5_file'))
					{
						$fileMd5 = @md5_file($filePath);

						if ($fileMd5 != $fileData[1])
						{
							$ret['files'][] = $fileKey . ' (MD5 ' . $fileMd5 . ' ≠ ' . $fileData[1] . ')';
						}
					}
				}
			}

			$idx++;
		}

		if (($idx >= $totalFiles) && ($idx >= $totalFolders))
		{
			$ret['done'] = true;
		}

		$ret['idx'] = $idx;

		return $ret;
	}
}
Utils/CliSessionHandler.php000060400000006276152455514240011744 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use FOF30\Encrypt\Randval;
use JSessionHandlerInterface;
use RuntimeException;

class CliSessionHandler implements JSessionHandlerInterface
{
	private $id;

	private $name = 'clisession';

	public function __construct()
	{
		$this->makeId();
	}

	/**
	 * Starts the session.
	 *
	 * @return  boolean  True if started.
	 *
	 * @throws  RuntimeException If something goes wrong starting the session.
	 * @since   3.4.8
	 */
	public function start()
	{
		return true;
	}

	/**
	 * Checks if the session is started.
	 *
	 * @return  boolean  True if started, false otherwise.
	 *
	 * @since   3.4.8
	 */
	public function isStarted()
	{
		return true;
	}

	/**
	 * Returns the session ID
	 *
	 * @return  string  The session ID
	 *
	 * @since   3.4.8
	 */
	public function getId()
	{
		return $this->id;
	}

	/**
	 * Sets the session ID
	 *
	 * @param   string  $id  The session ID
	 *
	 * @return  void
	 *
	 * @since   3.4.8
	 */
	public function setId($id)
	{
		$this->id = $id;
	}

	/**
	 * Returns the session name
	 *
	 * @return  mixed  The session name.
	 *
	 * @since   3.4.8
	 */
	public function getName()
	{
		return $this->name;
	}

	/**
	 * Sets the session name
	 *
	 * @param   string  $name  The name of the session
	 *
	 * @return  void
	 *
	 * @since   3.4.8
	 */
	public function setName($name)
	{
		$this->name = $name;
	}

	/**
	 * Regenerates ID that represents this storage.
	 *
	 * Note regenerate+destroy should not clear the session data in memory only delete the session data from persistent
	 * storage.
	 *
	 * @param   boolean  $destroy   Destroy session when regenerating?
	 * @param   integer  $lifetime  Sets the cookie lifetime for the session cookie. A null value will leave the system
	 *                              settings unchanged,
	 *                              0 sets the cookie to expire with browser session. Time is in seconds, and is not a
	 *                              Unix timestamp.
	 *
	 * @return  boolean  True if session regenerated, false if error
	 *
	 * @since   3.4.8
	 */
	public function regenerate($destroy = false, $lifetime = null)
	{
		$this->makeId();

		return true;
	}

	/**
	 * Force the session to be saved and closed.
	 *
	 * This method must invoke session_write_close() unless this interface is used for a storage object design for unit
	 * or functional testing where a real PHP session would interfere with testing, in which case it should actually
	 * persist the session data if required.
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException  If the session is saved without being started, or if the session is already closed.
	 * @since   3.4.8
	 * @see     session_write_close()
	 */
	public function save()
	{
		// No operation. This is a CLI session, we save nothing.
	}

	/**
	 * Clear all session data in memory.
	 *
	 * @return  void
	 *
	 * @since   3.4.8
	 */
	public function clear()
	{
		$this->makeId();
	}

	private function makeId()
	{
		$phpfunc = new Phpfunc();
		$rand    = new Randval($phpfunc);

		$this->id = md5($rand->generate(32));
	}
}
Utils/FEFHelper/Html.php000060400000005145152455514240011031 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils\FEFHelper;

defined('_JEXEC') || die;

use FOF30\View\DataView\DataViewInterface;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Pagination\Pagination;

/**
 * Interim FEF helper which was used in FOF 3.2. This is deprecated. Please use the FEFHelper.browse JHtml helper
 * instead. The implementation of this class should be a good hint on how you can do that.
 *
 * @deprecated 4.0
 */
abstract class Html
{
	/**
	 * Helper function to create Javascript code required for table ordering
	 *
	 * @param   string  $order  Current order
	 *
	 * @return string    Javascript to add to the page
	 */
	public static function jsOrderingBackend($order)
	{
		return HTMLHelper::_('FEFHelper.browse.orderjs', $order, true);
	}

	/**
	 * Creates the required HTML code for backend pagination and sorting
	 *
	 * @param   Pagination  $pagination  Pagination object
	 * @param   array       $sortFields  Fields allowed to be sorted
	 * @param   string      $order       Ordering field
	 * @param   string      $order_Dir   Ordering direction (ASC, DESC)
	 *
	 * @return string
	 */
	public static function selectOrderingBackend($pagination, $sortFields, $order, $order_Dir)
	{
		if (is_null($sortFields))
		{
			$sortFields = [];
		}

		if (is_string($sortFields))
		{
			$sortFields = [$sortFields];
		}

		if (!is_array($sortFields))
		{
			$sortFields = [];
		}

		return
			'<div class="akeeba-filter-bar akeeba-filter-bar--right">' .
			HTMLHelper::_('FEFHelper.browse.orderheader', null, $sortFields, $pagination, $order, $order_Dir) .
			'</div>';
	}

	/**
	 * Returns the drag'n'drop reordering field for Browse views
	 *
	 * @param   DataViewInterface  $view           The DataView you're rendering against
	 * @param   string             $orderingField  The name of the field you're ordering by
	 * @param   string             $order          The order value of the current row
	 * @param   string             $class          CSS class for the ordering value INPUT field
	 * @param   string             $icon           CSS class for the d'n'd handle icon
	 * @param   string             $inactiveIcon   CSS class for the d'n'd disabled icon
	 *
	 * @return string
	 */
	public static function dragDropReordering(DataViewInterface $view, $orderingField, $order, $class = 'input-sm', $icon = 'akion-drag', $inactiveIcon = 'akion-android-more-vertical')
	{
		return HTMLHelper::_('FEFHelper.browse.order', $orderingField, $order, $class, $icon, $inactiveIcon, $view);
	}
}
Utils/FEFHelper/BrowseView.php000060400000067172152455514240012231 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils\FEFHelper;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Model\DataModel;
use FOF30\Utils\ArrayHelper;
use FOF30\Utils\SelectOptions;
use FOF30\View\DataView\DataViewInterface;
use FOF30\View\View;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * An HTML helper for Browse views.
 *
 * It reintroduces a FEF-friendly of some of the functionality found in FOF 3's Header and Field classes. These
 * helpers are also accessible through Blade, making the transition from XML forms to Blade templates easier.
 *
 * @since 3.3.0
 */
abstract class BrowseView
{
	/**
	 * Caches the results of getOptionsFromModel keyed by a hash. The hash is computed by the model
	 * name, the model state and the options passed to getOptionsFromModel.
	 *
	 * @var array
	 */
	private static $cacheModelOptions = [];

	/**
	 * Get the translation key for a field's label
	 *
	 * @param   string  $fieldName  The field name
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function fieldLabelKey($fieldName)
	{
		$view = self::getViewFromBacktrace();

		try
		{
			$inflector     = $view->getContainer()->inflector;
			$viewName      = $inflector->singularize($view->getName());
			$altViewName   = $inflector->pluralize($view->getName());
			$componentName = $view->getContainer()->componentName;

			$keys = [
				strtoupper($componentName . '_' . $viewName . '_FIELD_' . $fieldName),
				strtoupper($componentName . '_' . $altViewName . '_FIELD_' . $fieldName),
				strtoupper($componentName . '_' . $viewName . '_' . $fieldName),
				strtoupper($componentName . '_' . $altViewName . '_' . $fieldName),
			];

			foreach ($keys as $key)
			{
				if (Text::_($key) != $key)
				{
					return $key;
				}
			}

			return $keys[0];
		}
		catch (Exception $e)
		{
			return ucfirst($fieldName);
		}
	}

	/**
	 * Returns the label for a field (translated)
	 *
	 * @param   string  $fieldName  The field name
	 *
	 * @return string
	 */
	public static function fieldLabel($fieldName)
	{
		return Text::_(self::fieldLabelKey($fieldName));
	}

	/**
	 * Return a table field header which sorts the table by that field upon clicking
	 *
	 * @param   string       $field    The name of the field
	 * @param   string|null  $langKey  (optional) The language key for the header to be displayed
	 *
	 * @return  mixed
	 */
	public static function sortgrid($field, $langKey = null)
	{
		/** @var DataViewInterface $view */
		$view = self::getViewFromBacktrace();

		if (is_null($langKey))
		{
			$langKey = self::fieldLabelKey($field);
		}

		return HTMLHelper::_('FEFHelper.browse.sort', $langKey, $field, $view->getLists()->order_Dir, $view->getLists()->order, $view->getTask());
	}

	/**
	 * Create a browse view filter from values returned by a model
	 *
	 * @param   string  $localField       Field name
	 * @param   string  $modelTitleField  Foreign model field for drop-down display values
	 * @param   null    $modelName        Foreign model name
	 * @param   string  $placeholder      Placeholder for no selection
	 * @param   array   $params           Generic select display parameters
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function modelFilter($localField, $modelTitleField = 'title', $modelName = null, $placeholder = null, array $params = [])
	{
		/** @var DataModel $model */
		$model = self::getViewFromBacktrace()->getModel();

		if (empty($modelName))
		{
			$modelName = $model->getForeignModelNameFor($localField);
		}

		if (is_null($placeholder))
		{
			$placeholder = self::fieldLabelKey($localField);
		}

		$params = array_merge([
			'list.none'      => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
			'value_field'    => $modelTitleField,
			'fof.autosubmit' => true,
		], $params);

		return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
	}

	/**
	 * Display a text filter (search box)
	 *
	 * @param   string  $localField   The name of the model field. Used when getting the filter state.
	 * @param   string  $searchField  The INPUT element's name. Default: "filter_$localField".
	 * @param   string  $placeholder  The JText language key for the placeholder. Default: extrapolate from $localField.
	 * @param   array   $attributes   HTML attributes for the INPUT element.
	 *
	 * @return  string
	 *
	 * @since   3.3.0
	 */
	public static function searchFilter($localField, $searchField = null, $placeholder = null, array $attributes = [])
	{
		/** @var DataModel $model */
		$view                      = self::getViewFromBacktrace();
		$model                     = $view->getModel();
		$searchField               = empty($searchField) ? $localField : $searchField;
		$placeholder               = empty($placeholder) ? self::fieldLabelKey($localField) : $placeholder;
		$attributes['type']        = $attributes['type'] ?? 'text';
		$attributes['name']        = $searchField;
		$attributes['id']          = !isset($attributes['id']) ? "filter_$localField" : $attributes['id'];
		$attributes['onchange']    = !isset($attributes['onchange']) ? 'document.adminForm.submit()' : null;
		$attributes['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder'];
		$attributes['title']       = $attributes['title'] ?? $attributes['placeholder'];
		$attributes['value']       = $view->escape($model->getState($localField));

		// Remove null attributes and collapse into a string
		$attributes = array_filter($attributes, function ($v) {
			return !is_null($v);
		});
		$attributes = ArrayHelper::toString($attributes);

		return "<input $attributes />";
	}

	/**
	 * Create a browse view filter with dropdown values
	 *
	 * @param   string  $localField   Field name
	 * @param   array   $options      The JHtml options list to use
	 * @param   string  $placeholder  Placeholder for no selection
	 * @param   array   $params       Generic select display parameters
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function selectFilter($localField, array $options, $placeholder = null, array $params = [])
	{
		/** @var DataModel $model */
		$model = self::getViewFromBacktrace()->getModel();

		if (is_null($placeholder))
		{
			$placeholder = self::fieldLabelKey($localField);
		}

		$params = array_merge([
			'list.none'      => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
			'fof.autosubmit' => true,
		], $params);

		return self::genericSelect($localField, $options, $model->getState($localField), $params);
	}

	/**
	 * View access dropdown filter
	 *
	 * @param   string  $localField   Field name
	 * @param   string  $placeholder  Placeholder for no selection
	 * @param   array   $params       Generic select display parameters
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function accessFilter($localField, $placeholder = null, array $params = [])
	{
		return self::selectFilter($localField, SelectOptions::getOptions('access', $params), $placeholder, $params);
	}

	/**
	 * Published state dropdown filter
	 *
	 * @param   string  $localField   Field name
	 * @param   string  $placeholder  Placeholder for no selection
	 * @param   array   $params       Generic select display parameters
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function publishedFilter($localField, $placeholder = null, array $params = [])
	{
		return self::selectFilter($localField, SelectOptions::getOptions('published', $params), $placeholder, $params);
	}

	/**
	 * Create a select box from the values returned by a model
	 *
	 * @param   string  $name          Field name
	 * @param   string  $modelName     The name of the model, e.g. "items" or "com_foobar.items"
	 * @param   string  $currentValue  The currently selected value
	 * @param   array   $params        Passed to optionsFromModel and genericSelect
	 * @param   array   $modelState    Optional state variables to pass to the model
	 * @param   array   $options       Any JHtml select options you want to add in front of the model's returned values
	 *
	 * @return string
	 *
	 * @see   self::getOptionsFromModel
	 * @see   self::getOptionsFromSource
	 * @see   self::genericSelect
	 *
	 * @since 3.3.0
	 */
	public static function modelSelect($name, $modelName, $currentValue, array $params = [], array $modelState = [], array $options = [])
	{
		$params = array_merge([
			'fof.autosubmit' => true,
		], $params);

		$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);

		return self::genericSelect($name, $options, $currentValue, $params);
	}

	/**
	 * Get a (human readable) title from a (typically numeric, foreign key) key value using the data
	 * returned by a DataModel.
	 *
	 * @param   string  $value       The key value
	 * @param   string  $modelName   The name of the model, e.g. "items" or "com_foobar.items"
	 * @param   array   $params      Passed to getOptionsFromModel
	 * @param   array   $modelState  Optional state variables to pass to the model
	 * @param   array   $options     Any JHtml select options you want to add in front of the model's returned values
	 *
	 * @return string
	 *
	 * @see   self::getOptionsFromModel
	 * @see   self::getOptionsFromSource
	 * @see   self::genericSelect
	 *
	 * @since 3.3.0
	 */
	public static function modelOptionName($value, $modelName = null, array $params = [], array $modelState = [], array $options = [])
	{
		if (!isset($params['cache']))
		{
			$params['cache'] = true;
		}

		if (!isset($params['none_as_zero']))
		{
			$params['none_as_zero'] = true;
		}

		$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);

		return self::getOptionName($value, $options);
	}

	/**
	 * Gets the active option's label given an array of JHtml options
	 *
	 * @param   mixed   $selected     The currently selected value
	 * @param   array   $data         The JHtml options to parse
	 * @param   string  $optKey       Key name, default: value
	 * @param   string  $optText      Value name, default: text
	 * @param   bool    $selectFirst  Should I automatically select the first option? Default: true
	 *
	 * @return  mixed   The label of the currently selected option
	 */
	public static function getOptionName($selected, $data, $optKey = 'value', $optText = 'text', $selectFirst = true)
	{
		$ret = null;

		foreach ($data as $elementKey => &$element)
		{
			if (is_array($element))
			{
				$key  = $optKey === null ? $elementKey : $element[$optKey];
				$text = $element[$optText];
			}
			elseif (is_object($element))
			{
				$key  = $optKey === null ? $elementKey : $element->$optKey;
				$text = $element->$optText;
			}
			else
			{
				// This is a simple associative array
				$key  = $elementKey;
				$text = $element;
			}

			if (is_null($ret) && $selectFirst && ($selected == $key))
			{
				$ret = $text;
			}
			elseif ($selected == $key)
			{
				$ret = $text;
			}
		}

		return $ret;
	}

	/**
	 * Create a generic select list based on a bunch of options. Option sources will be merged into the provided
	 * options automatically.
	 *
	 * Parameters:
	 * - format.depth The current indent depth.
	 * - format.eol The end of line string, default is linefeed.
	 * - format.indent The string to use for indentation, default is tab.
	 * - groups If set, looks for keys with the value "<optgroup>" and synthesizes groups from them. Deprecated.
	 * Default: true.
	 * - list.select Either the value of one selected option or an array of selected options. Default: $currentValue.
	 * - list.translate If true, text and labels are translated via JText::_(). Default is false.
	 * - list.attr HTML element attributes (key/value array or string)
	 * - list.none Placeholder for no selection (creates an option with an empty string key)
	 * - option.id The property in each option array to use as the selection id attribute. Defaults: null.
	 * - option.key The property in each option array to use as the Default: "value". If set to null, the index of the
	 * option array is used.
	 * - option.label The property in each option array to use as the selection label attribute. Default: null
	 * - option.text The property in each option array to use as the displayed text. Default: "text". If set to null,
	 * the option array is assumed to be a list of displayable scalars.
	 * - option.attr The property in each option array to use for additional selection attributes. Defaults: null.
	 * - option.disable: The property that will hold the disabled state. Defaults to "disable".
	 * - fof.autosubmit Should I auto-submit the form on change? Default: true
	 * - fof.formname Form to auto-submit. Default: adminForm
	 * - class CSS class to apply
	 * - size Size attribute for the input
	 * - multiple Is this a multiple select? Default: false.
	 * - required Is this a required field? Default: false.
	 * - autofocus Should I focus this field automatically? Default: false
	 * - disabled Is this a disabled field? Default: false
	 * - readonly Render as a readonly field with hidden inputs? Overrides 'disabled'. Default: false
	 * - onchange Custom onchange handler. Overrides fof.autosubmit. Default: NULL (use fof.autosubmit).
	 *
	 * @param          $name
	 * @param   array  $options
	 * @param          $currentValue
	 * @param   array  $params
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function genericSelect($name, array $options, $currentValue, array $params = [])
	{
		$params = array_merge([
			'format.depth'   => 0,
			'format.eol'     => "\n",
			'format.indent'  => "\t",
			'groups'         => true,
			'list.select'    => $currentValue,
			'list.translate' => false,
			'option.id'      => null,
			'option.key'     => 'value',
			'option.label'   => null,
			'option.text'    => 'text',
			'option.attr'    => null,
			'option.disable' => 'disable',
			'list.attr'      => '',
			'list.none'      => '',
			'id'             => null,
			'fof.autosubmit' => true,
			'fof.formname'   => 'adminForm',
			'class'          => '',
			'size'           => '',
			'multiple'       => false,
			'required'       => false,
			'autofocus'      => false,
			'disabled'       => false,
			'onchange'       => null,
			'readonly'       => false,
		], $params);

		$currentValue = $params['list.select'];

		// If fof.autosubmit is enabled and onchange is not set we will add our own handler
		if ($params['fof.autosubmit'] && is_null($params['onchange']))
		{
			$formName           = $params['fof.formname'] ?: 'adminForm';
			$params['onchange'] = "document.{$formName}.submit()";
		}

		// Construct SELECT element's attributes
		$attr = '';
		$attr .= $params['class'] ? ' class="' . $params['class'] . '"' : '';
		$attr .= !empty($params['size']) ? ' size="' . $params['size'] . '"' : '';
		$attr .= $params['multiple'] ? ' multiple' : '';
		$attr .= $params['required'] ? ' required aria-required="true"' : '';
		$attr .= $params['autofocus'] ? ' autofocus' : '';
		$attr .= ($params['disabled'] || $params['readonly']) ? ' disabled="disabled"' : '';
		$attr .= $params['onchange'] ? ' onchange="' . $params['onchange'] . '"' : '';

		// We use the constructed SELECT element's attributes only if no 'attr' key was provided
		if (empty($params['list.attr']))
		{
			$params['list.attr'] = $attr;
		}

		// Merge the options with those fetched from a source (e.g. another Helper object)
		$options = array_merge($options, self::getOptionsFromSource($params));

		if (!empty($params['list.none']))
		{
			array_unshift($options, HTMLHelper::_('FEFHelper.select.option', '', Text::_($params['list.none'])));
		}

		$html = [];

		// Create a read-only list (no name) with hidden input(s) to store the value(s).
		if ($params['readonly'])
		{
			$html[] = HTMLHelper::_('FEFHelper.select.genericlist', $options, $name, $params);

			// E.g. form field type tag sends $this->value as array
			if ($params['multiple'] && is_array($currentValue))
			{
				if (!count($currentValue))
				{
					$currentValue[] = '';
				}

				foreach ($currentValue as $value)
				{
					$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
				}
			}
			else
			{
				$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
			}
		}
		else
			// Create a regular list.
		{
			$html[] = HTMLHelper::_('FEFHelper.select.genericlist', $options, $name, $params);
		}

		return implode($html);
	}

	/**
	 * Replace tags that reference fields with their values
	 *
	 * @param   string     $text  Text to process
	 * @param   DataModel  $item  The DataModel instance to get values from
	 *
	 * @return  string         Text with tags replace
	 *
	 * @since 3.3.0
	 */
	public static function parseFieldTags($text, DataModel $item)
	{
		$ret = $text;

		if (empty($item))
		{
			return $ret;
		}

		/**
		 * Replace [ITEM:ID] in the URL with the item's key value (usually: the auto-incrementing numeric ID)
		 */
		$replace = $item->getId();
		$ret     = str_replace('[ITEM:ID]', $replace, $ret);

		// Replace the [ITEMID] in the URL with the current Itemid parameter
		$ret = str_replace('[ITEMID]', $item->getContainer()->input->getInt('Itemid', 0), $ret);

		// Replace the [TOKEN] in the URL with the Joomla! form token
		$ret = str_replace('[TOKEN]', $item->getContainer()->platform->getToken(true), $ret);

		// Replace other field variables in the URL
		$data = $item->getData();

		foreach ($data as $field => $value)
		{
			// Skip non-processable values
			if (is_array($value) || is_object($value))
			{
				continue;
			}

			$search = '[ITEM:' . strtoupper($field) . ']';
			$ret    = str_replace($search, $value, $ret);
		}

		return $ret;
	}

	/**
	 * Get the FOF View from the backtrace of the static call. MAGIC!
	 *
	 * @return  View
	 *
	 * @since 3.3.0
	 */
	public static function getViewFromBacktrace()
	{
		// In case we are on a braindead host
		if (!function_exists('debug_backtrace'))
		{
			throw new RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
		}

		/**
		 * For performance reasons I look into the last 4 call stack entries. If I don't find a container I
		 * will expand my search by another 2 entries and so on until I either find a container or I stop
		 * finding new call stack entries.
		 */
		$lastNumberOfEntries = 0;
		$limit               = 4;
		$skip                = 0;
		$container           = null;

		while (true)
		{
			$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);

			if (count($backtrace) == $lastNumberOfEntries)
			{
				throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
			}

			$lastNumberOfEntries = count($backtrace);

			if ($skip)
			{
				$backtrace = array_slice($backtrace, $skip);
			}

			foreach ($backtrace as $bt)
			{
				if (!isset($bt['object']))
				{
					continue;
				}

				if ($bt['object'] instanceof View)
				{
					return $bt['object'];
				}
			}

			$skip  = $limit;
			$limit += 2;
		}

		throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
	}

	/**
	 * Get JHtml options from an alternate source, e.g. a helper. This is useful for adding arbitrary options
	 * which are either dynamic or you do not want to inline to your view, e.g. reusable options across
	 * different views.
	 *
	 * The attribs can be:
	 * source_file          The file to load. You can use FOF's URIs such as 'admin:com_foobar/foo/bar'
	 * source_class         The class to use
	 * source_method        The static method to use on source_class
	 * source_key           Use * if you're returning a key/value array. Otherwise the array key for the key (ID)
	 * value.
	 * source_value         Use * if you're returning a key/value array. Otherwise the array key for the displayed
	 * value. source_translate     Should I pass the value field through JText? Default: true source_format        Set
	 * to "optionsobject" if you're returning an array of JHtml options. Ignored otherwise.
	 *
	 * @param   array  $attribs
	 *
	 * @return array
	 *
	 * @since 3.3.0
	 */
	private static function getOptionsFromSource(array $attribs = [])
	{
		$options = [];

		$container = self::getContainerFromBacktrace();

		$attribs = array_merge([
			'source_file'      => '',
			'source_class'     => '',
			'source_method'    => '',
			'source_key'       => '*',
			'source_value'     => '*',
			'source_translate' => true,
			'source_format'    => '',
		], $attribs);

		$source_file      = $attribs['source_file'];
		$source_class     = $attribs['source_class'];
		$source_method    = $attribs['source_method'];
		$source_key       = $attribs['source_key'];
		$source_value     = $attribs['source_value'];
		$source_translate = $attribs['source_translate'];
		$source_format    = $attribs['source_format'];

		if ($source_class && $source_method)
		{
			// Maybe we have to load a file?
			if (!empty($source_file))
			{
				$source_file = $container->template->parsePath($source_file, true);

				if ($container->filesystem->fileExists($source_file))
				{
					include $source_file;
				}
			}

			// Make sure the class exists
			if (class_exists($source_class, true))
			{
				// ...and so does the option
				if (in_array($source_method, get_class_methods($source_class)))
				{
					// Get the data from the class
					if ($source_format == 'optionsobject')
					{
						$options = array_merge($options, $source_class::$source_method());
					}
					else
					{
						$source_data = $source_class::$source_method();

						// Loop through the data and prime the $options array
						foreach ($source_data as $k => $v)
						{
							$key   = (empty($source_key) || ($source_key == '*')) ? $k : @$v[$source_key];
							$value = (empty($source_value) || ($source_value == '*')) ? $v : @$v[$source_value];

							if ($source_translate)
							{
								$value = Text::_($value);
							}

							$options[] = HTMLHelper::_('FEFHelper.select.option', $key, $value, 'value', 'text');
						}
					}
				}
			}
		}

		reset($options);

		return $options;
	}

	/**
	 * Get JHtml options from the values returned by a model.
	 *
	 * The params can be:
	 * key_field        The model field used for the OPTION's key. Default: the model's ID field.
	 * value_field      The model field used for the OPTION's displayed value. You must provide it.
	 * apply_access     Should I apply Joomla ACLs to the model? Default: FALSE.
	 * none             Placeholder for no selection. Default: NULL (no placeholder).
	 * none_as_zero     When true, the 'none' placeholder applies to values '' **AND** '0' (empty string and zero)
	 * translate        Should I pass the values through JText? Default: TRUE.
	 * with             Array of relation names for eager loading.
	 * cache            Cache the results for faster reuse
	 *
	 * @param   string  $modelName   The name of the model, e.g. "items" or "com_foobar.items"
	 * @param   array   $params      Parameters which define which options to get from the model
	 * @param   array   $modelState  Optional state variables to pass to the model
	 * @param   array   $options     Any JHtml select options you want to add in front of the model's returned values
	 *
	 * @return mixed
	 *
	 * @since 3.3.0
	 */
	private static function getOptionsFromModel($modelName, array $params = [], array $modelState = [], array $options = [])
	{
		// Let's find the FOF DI container from the call stack
		$container = self::getContainerFromBacktrace();

		// Explode model name into component name and prefix
		$componentName = $container->componentName;
		$mName         = $modelName;

		if (strpos($modelName, '.') !== false)
		{
			[$componentName, $mName] = explode('.', $mName, 2);
		}

		if ($componentName != $container->componentName)
		{
			$container = Container::getInstance($componentName);
		}

		/** @var DataModel $model */
		$model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false);

		$defaultParams = [
			'key_field'    => $model->getKeyName(),
			'value_field'  => 'title',
			'apply_access' => false,
			'none'         => null,
			'none_as_zero' => false,
			'translate'    => true,
			'with'         => [],
		];

		$params = array_merge($defaultParams, $params);

		$cache    = isset($params['cache']) && $params['cache'];
		$cacheKey = null;

		if ($cache)
		{
			$cacheKey = sha1(print_r([
				$model->getContainer()->componentName,
				$model->getName(),
				$params['key_field'],
				$params['value_field'],
				$params['apply_access'],
				$params['none'],
				$params['translate'],
				$params['with'],
				$modelState,
			], true));
		}

		if ($cache && isset(self::$cacheModelOptions[$cacheKey]))
		{
			return self::$cacheModelOptions[$cacheKey];
		}

		if (empty($params['none']) && !is_null($params['none']))
		{
			$langKey     = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName());
			$placeholder = Text::_($langKey);

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

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

			if ($params['none_as_zero'])
			{
				$options[] = HTMLHelper::_('FEFHelper.select.option', 0, Text::_($params['none']));
			}
		}


		if ($params['apply_access'])
		{
			$model->applyAccessFiltering();
		}

		if (!is_null($params['with']))
		{
			$model->with($params['with']);
		}

		// Set the model's state, if applicable
		foreach ($modelState as $stateKey => $stateValue)
		{
			$model->setState($stateKey, $stateValue);
		}

		// Set the query and get the result list.
		$items = $model->get(true);

		// Build the field options.
		if (!empty($items))
		{
			foreach ($items as $item)
			{
				$value = $item->{$params['value_field']};

				if ($params['translate'])
				{
					$value = Text::_($value);
				}

				$options[] = HTMLHelper::_('FEFHelper.select.option', $item->{$params['key_field']}, $value);
			}
		}

		if ($cache)
		{
			self::$cacheModelOptions[$cacheKey] = $options;
		}

		return $options;
	}

	/**
	 * Get the FOF DI container from the backtrace of the static call. MAGIC!
	 *
	 * @return  Container
	 *
	 * @since 3.3.0
	 */
	private static function getContainerFromBacktrace()
	{
		// In case we are on a braindead host
		if (!function_exists('debug_backtrace'))
		{
			throw new RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
		}

		/**
		 * For performance reasons I look into the last 4 call stack entries. If I don't find a container I
		 * will expand my search by another 2 entries and so on until I either find a container or I stop
		 * finding new call stack entries.
		 */
		$lastNumberOfEntries = 0;
		$limit               = 4;
		$skip                = 0;
		$container           = null;

		while (true)
		{
			$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);

			if (count($backtrace) == $lastNumberOfEntries)
			{
				throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
			}

			$lastNumberOfEntries = count($backtrace);

			if ($skip)
			{
				$backtrace = array_slice($backtrace, $skip);
			}

			foreach ($backtrace as $bt)
			{
				if (!isset($bt['object']))
				{
					continue;
				}

				if (!method_exists($bt['object'], 'getContainer'))
				{
					continue;
				}

				return $bt['object']->getContainer();
			}

			$skip  = $limit;
			$limit += 2;
		}

		throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
	}

}
Utils/FEFHelper/edit.php000060400000002671152455514240011053 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

defined('_JEXEC') || die;

use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory;

/**
 * Custom JHtml (HTMLHelper) class. Offers edit (form) view controls compatible with Akeeba Frontend
 * Framework (FEF).
 *
 * Call these methods as JHtml::_('FEFHelper.edit.methodName', $parameter1, $parameter2, ...)
 */
abstract class FEFHelperEdit
{
	public static function editor($fieldName, $value, array $params = [])
	{
		$params = array_merge([
			'id'         => null,
			'editor'     => null,
			'width'      => '100%',
			'height'     => 500,
			'columns'    => 50,
			'rows'       => 20,
			'created_by' => null,
			'asset_id'   => null,
			'buttons'    => true,
			'hide'       => false,
		], $params);

		$editorType = $params['editor'];

		if (is_null($editorType))
		{
			$editorType = Factory::getConfig()->get('editor');
			$user       = Factory::getUser();

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

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

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

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

defined('_JEXEC') || die;

use FOF30\Utils\ArrayHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;

/**
 * Custom JHtml (HTMLHelper) class. Offers selects compatible with Akeeba Frontend Framework (FEF)
 *
 * Call these methods as JHtml::_('FEFHelper.select.methodName', $parameter1, $parameter2, ...)
 */
abstract class FEFHelperSelect
{
	/**
	 * Default values for options. Organized by option group.
	 *
	 * @var     array
	 */
	protected static $optionDefaults = [
		'option' => [
			'option.attr'         => null,
			'option.disable'      => 'disable',
			'option.id'           => null,
			'option.key'          => 'value',
			'option.key.toHtml'   => true,
			'option.label'        => null,
			'option.label.toHtml' => true,
			'option.text'         => 'text',
			'option.text.toHtml'  => true,
			'option.class'        => 'class',
			'option.onclick'      => 'onclick',
		],
	];

	/**
	 * Generates a yes/no radio list.
	 *
	 * @param   string  $name      The value of the HTML name attribute
	 * @param   array   $attribs   Additional HTML attributes for the `<select>` tag
	 * @param   string  $selected  The key that is selected
	 * @param   string  $yes       Language key for Yes
	 * @param   string  $no        Language key for no
	 * @param   mixed   $id        The id for the field or false for no id
	 *
	 * @return  string  HTML for the radio list
	 *
	 * @since   1.5
	 * @see     JFormFieldRadio
	 */
	public static function booleanlist($name, $attribs = [], $selected = null, $yes = 'JYES', $no = 'JNO', $id = false)
	{
		$options = [
			HTMLHelper::_('FEFHelper.select.option', '0', Text::_($no)),
			HTMLHelper::_('FEFHelper.select.option', '1', Text::_($yes)),
		];
		$attribs = array_merge(['forSelect' => 1], $attribs);

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

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

		if (is_array($attribs) && func_num_args() === 3)
		{
			// Assume we have an options array
			$options = array_merge($options, $attribs);
		}
		else
		{
			// Get options from the parameters
			$options['id']             = $idtag;
			$options['list.attr']      = $attribs;
			$options['list.translate'] = $translate;
			$options['option.key']     = $optKey;
			$options['option.text']    = $optText;
			$options['list.select']    = $selected;
		}

		$attribs = '';

		if (isset($options['list.attr']))
		{
			if (is_array($options['list.attr']))
			{
				$attribs = ArrayHelper::toString($options['list.attr']);
			}
			else
			{
				$attribs = $options['list.attr'];
			}

			if ($attribs !== '')
			{
				$attribs = ' ' . $attribs;
			}
		}

		$id = $options['id'] !== false ? $options['id'] : $name;
		$id = str_replace(['[', ']', ' '], '', $id);

		$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
		$html       = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol']
			. static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol'];

		return $html;
	}

	/**
	 * Generates a grouped HTML selection list from nested arrays.
	 *
	 * @param   array   $data     An array of groups, each of which is an array of options.
	 * @param   string  $name     The value of the HTML name attribute
	 * @param   array   $options  Options, an array of key/value pairs. Valid options are:
	 *                            Format options, {@see JHtml::$formatOptions}.
	 *                            Selection options. See {@see JHtmlSelect::options()}.
	 *                            group.id: The property in each group to use as the group id
	 *                            attribute. Defaults to none.
	 *                            group.label: The property in each group to use as the group
	 *                            label. Defaults to "text". If set to null, the data array index key is
	 *                            used.
	 *                            group.items: The property in each group to use as the array of
	 *                            items in the group. Defaults to "items". If set to null, group.id and
	 *                            group. label are forced to null and the data element is assumed to be a
	 *                            list of selections.
	 *                            id: Value to use as the select element id attribute. Defaults to
	 *                            the same as the name.
	 *                            list.attr: Attributes for the select element. Can be a string or
	 *                            an array of key/value pairs. Defaults to none.
	 *                            list.select: either the value of one selected option or an array
	 *                            of selected options. Default: none.
	 *                            list.translate: Boolean. If set, text and labels are translated via
	 *                            JText::_().
	 *
	 * @return  string  HTML for the select list
	 *
	 * @throws  RuntimeException If a group has contents that cannot be processed.
	 */
	public static function groupedlist($data, $name, $options = [])
	{
		// Set default options and overwrite with anything passed in
		$options = array_merge(
			HTMLHelper::$formatOptions,
			[
				'format.depth' => 0, 'group.items' => 'items', 'group.label' => 'text', 'group.label.toHtml' => true,
				'id'           => false,
			],
			$options
		);

		// Apply option rules
		if ($options['group.items'] === null)
		{
			$options['group.label'] = null;
		}

		$attribs = '';

		if (isset($options['list.attr']))
		{
			if (is_array($options['list.attr']))
			{
				$attribs = ArrayHelper::toString($options['list.attr']);
			}
			else
			{
				$attribs = $options['list.attr'];
			}

			if ($attribs !== '')
			{
				$attribs = ' ' . $attribs;
			}
		}

		$id = $options['id'] !== false ? $options['id'] : $name;
		$id = str_replace(['[', ']', ' '], '', $id);

		// Disable groups in the options.
		$options['groups'] = false;

		$baseIndent  = str_repeat($options['format.indent'], $options['format.depth']++);
		$html        = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol'];
		$groupIndent = str_repeat($options['format.indent'], $options['format.depth']++);

		foreach ($data as $dataKey => $group)
		{
			$label   = $dataKey;
			$id      = '';
			$noGroup = is_int($dataKey);

			if ($options['group.items'] == null)
			{
				// Sub-list is an associative array
				$subList = $group;
			}
			elseif (is_array($group))
			{
				// Sub-list is in an element of an array.
				$subList = $group[$options['group.items']];

				if (isset($group[$options['group.label']]))
				{
					$label   = $group[$options['group.label']];
					$noGroup = false;
				}

				if (isset($options['group.id']) && isset($group[$options['group.id']]))
				{
					$id      = $group[$options['group.id']];
					$noGroup = false;
				}
			}
			elseif (is_object($group))
			{
				// Sub-list is in a property of an object
				$subList = $group->{$options['group.items']};

				if (isset($group->{$options['group.label']}))
				{
					$label   = $group->{$options['group.label']};
					$noGroup = false;
				}

				if (isset($options['group.id']) && isset($group->{$options['group.id']}))
				{
					$id      = $group->{$options['group.id']};
					$noGroup = false;
				}
			}
			else
			{
				throw new RuntimeException('Invalid group contents.', 1);
			}

			if ($noGroup)
			{
				$html .= static::options($subList, $options);
			}
			else
			{
				$html .= $groupIndent . '<optgroup' . (empty($id) ? '' : ' id="' . $id . '"') . ' label="'
					. ($options['group.label.toHtml'] ? htmlspecialchars($label, ENT_COMPAT, 'UTF-8') : $label) . '">' . $options['format.eol']
					. static::options($subList, $options) . $groupIndent . '</optgroup>' . $options['format.eol'];
			}
		}

		$html .= $baseIndent . '</select>' . $options['format.eol'];

		return $html;
	}

	/**
	 * Generates a selection list of integers.
	 *
	 * @param   integer  $start     The start integer
	 * @param   integer  $end       The end integer
	 * @param   integer  $inc       The increment
	 * @param   string   $name      The value of the HTML name attribute
	 * @param   mixed    $attribs   Additional HTML attributes for the `<select>` tag, an array of
	 *                              attributes, or an array of options. Treated as options if it is the last
	 *                              argument passed.
	 * @param   mixed    $selected  The key that is selected
	 * @param   string   $format    The printf format to be applied to the number
	 *
	 * @return  string   HTML for the select list
	 */
	public static function integerlist($start, $end, $inc, $name, $attribs = null, $selected = null, $format = '')
	{
		// Set default options
		$options = array_merge(HTMLHelper::$formatOptions, [
			'format.depth' => 0, 'option.format' => '', 'id' => null,
		]);

		if (is_array($attribs) && func_num_args() === 5)
		{
			// Assume we have an options array
			$options = array_merge($options, $attribs);

			// Extract the format and remove it from downstream options
			$format = $options['option.format'];
			unset($options['option.format']);
		}
		else
		{
			// Get options from the parameters
			$options['list.attr']   = $attribs;
			$options['list.select'] = $selected;
		}

		$start = (int) $start;
		$end   = (int) $end;
		$inc   = (int) $inc;

		$data = [];

		for ($i = $start; $i <= $end; $i += $inc)
		{
			$data[$i] = $format ? sprintf($format, $i) : $i;
		}

		// Tell genericlist() to use array keys
		$options['option.key'] = null;

		return HTMLHelper::_('FEFHelper.select.genericlist', $data, $name, $options);
	}

	/**
	 * Create an object that represents an option in an option list.
	 *
	 * @param   string   $value    The value of the option
	 * @param   string   $text     The text for the option
	 * @param   mixed    $optKey   If a string, the returned object property name for
	 *                             the value. If an array, options. Valid options are:
	 *                             attr: String|array. Additional attributes for this option.
	 *                             Defaults to none.
	 *                             disable: Boolean. If set, this option is disabled.
	 *                             label: String. The value for the option label.
	 *                             option.attr: The property in each option array to use for
	 *                             additional selection attributes. Defaults to none.
	 *                             option.disable: The property that will hold the disabled state.
	 *                             Defaults to "disable".
	 *                             option.key: The property that will hold the selection value.
	 *                             Defaults to "value".
	 *                             option.label: The property in each option array to use as the
	 *                             selection label attribute. If a "label" option is provided, defaults to
	 *                             "label", if no label is given, defaults to null (none).
	 *                             option.text: The property that will hold the the displayed text.
	 *                             Defaults to "text". If set to null, the option array is assumed to be a
	 *                             list of displayable scalars.
	 * @param   string   $optText  The property that will hold the the displayed text. This
	 *                             parameter is ignored if an options array is passed.
	 * @param   boolean  $disable  Not used.
	 *
	 * @return  stdClass
	 */
	public static function option($value, $text = '', $optKey = 'value', $optText = 'text', $disable = false)
	{
		$options = [
			'attr'           => null,
			'disable'        => false,
			'option.attr'    => null,
			'option.disable' => 'disable',
			'option.key'     => 'value',
			'option.label'   => null,
			'option.text'    => 'text',
		];

		if (is_array($optKey))
		{
			// Merge in caller's options
			$options = array_merge($options, $optKey);
		}
		else
		{
			// Get options from the parameters
			$options['option.key']  = $optKey;
			$options['option.text'] = $optText;
			$options['disable']     = $disable;
		}

		$obj                            = new stdClass;
		$obj->{$options['option.key']}  = $value;
		$obj->{$options['option.text']} = trim($text) ? $text : $value;

		/*
		 * If a label is provided, save it. If no label is provided and there is
		 * a label name, initialise to an empty string.
		 */
		$hasProperty = $options['option.label'] !== null;

		if (isset($options['label']))
		{
			$labelProperty       = $hasProperty ? $options['option.label'] : 'label';
			$obj->$labelProperty = $options['label'];
		}
		elseif ($hasProperty)
		{
			$obj->{$options['option.label']} = '';
		}

		// Set attributes only if there is a property and a value
		if ($options['attr'] !== null)
		{
			$obj->{$options['option.attr']} = $options['attr'];
		}

		// Set disable only if it has a property and a value
		if ($options['disable'] !== null)
		{
			$obj->{$options['option.disable']} = $options['disable'];
		}

		return $obj;
	}

	/**
	 * Generates the option tags for an HTML select list (with no select tag
	 * surrounding the options).
	 *
	 * @param   array    $arr         An array of objects, arrays, or values.
	 * @param   mixed    $optKey      If a string, this is the name of the object variable for
	 *                                the option value. If null, the index of the array of objects is used. If
	 *                                an array, this is a set of options, as key/value pairs. Valid options are:
	 *                                -Format options, {@see JHtml::$formatOptions}.
	 *                                -groups: Boolean. If set, looks for keys with the value
	 *                                "&lt;optgroup>" and synthesizes groups from them. Deprecated. Defaults
	 *                                true for backwards compatibility.
	 *                                -list.select: either the value of one selected option or an array
	 *                                of selected options. Default: none.
	 *                                -list.translate: Boolean. If set, text and labels are translated via
	 *                                JText::_(). Default is false.
	 *                                -option.id: The property in each option array to use as the
	 *                                selection id attribute. Defaults to none.
	 *                                -option.key: The property in each option array to use as the
	 *                                selection value. Defaults to "value". If set to null, the index of the
	 *                                option array is used.
	 *                                -option.label: The property in each option array to use as the
	 *                                selection label attribute. Defaults to null (none).
	 *                                -option.text: The property in each option array to use as the
	 *                                displayed text. Defaults to "text". If set to null, the option array is
	 *                                assumed to be a list of displayable scalars.
	 *                                -option.attr: The property in each option array to use for
	 *                                additional selection attributes. Defaults to none.
	 *                                -option.disable: The property that will hold the disabled state.
	 *                                Defaults to "disable".
	 *                                -option.key: The property that will hold the selection value.
	 *                                Defaults to "value".
	 *                                -option.text: The property that will hold the the displayed text.
	 *                                Defaults to "text". If set to null, the option array is assumed to be a
	 *                                list of displayable scalars.
	 * @param   string   $optText     The name of the object variable for the option text.
	 * @param   mixed    $selected    The key that is selected (accepts an array or a string)
	 * @param   boolean  $translate   Translate the option values.
	 *
	 * @return  string  HTML for the select list
	 */
	public static function options($arr, $optKey = 'value', $optText = 'text', $selected = null, $translate = false)
	{
		$options = array_merge(
			HTMLHelper::$formatOptions,
			static::$optionDefaults['option'],
			['format.depth' => 0, 'groups' => true, 'list.select' => null, 'list.translate' => false]
		);

		if (is_array($optKey))
		{
			// Set default options and overwrite with anything passed in
			$options = array_merge($options, $optKey);
		}
		else
		{
			// Get options from the parameters
			$options['option.key']     = $optKey;
			$options['option.text']    = $optText;
			$options['list.select']    = $selected;
			$options['list.translate'] = $translate;
		}

		$html       = '';
		$baseIndent = str_repeat($options['format.indent'], $options['format.depth']);

		foreach ($arr as $elementKey => &$element)
		{
			$attr  = '';
			$extra = '';
			$label = '';
			$id    = '';

			if (is_array($element))
			{
				$key  = $options['option.key'] === null ? $elementKey : $element[$options['option.key']];
				$text = $element[$options['option.text']];

				if (isset($element[$options['option.attr']]))
				{
					$attr = $element[$options['option.attr']];
				}

				if (isset($element[$options['option.id']]))
				{
					$id = $element[$options['option.id']];
				}

				if (isset($element[$options['option.label']]))
				{
					$label = $element[$options['option.label']];
				}

				if (isset($element[$options['option.disable']]) && $element[$options['option.disable']])
				{
					$extra .= ' disabled="disabled"';
				}
			}
			elseif (is_object($element))
			{
				$key  = $options['option.key'] === null ? $elementKey : $element->{$options['option.key']};
				$text = $element->{$options['option.text']};

				if (isset($element->{$options['option.attr']}))
				{
					$attr = $element->{$options['option.attr']};
				}

				if (isset($element->{$options['option.id']}))
				{
					$id = $element->{$options['option.id']};
				}

				if (isset($element->{$options['option.label']}))
				{
					$label = $element->{$options['option.label']};
				}

				if (isset($element->{$options['option.disable']}) && $element->{$options['option.disable']})
				{
					$extra .= ' disabled="disabled"';
				}

				if (isset($element->{$options['option.class']}) && $element->{$options['option.class']})
				{
					$extra .= ' class="' . $element->{$options['option.class']} . '"';
				}

				if (isset($element->{$options['option.onclick']}) && $element->{$options['option.onclick']})
				{
					$extra .= ' onclick="' . $element->{$options['option.onclick']} . '"';
				}
			}
			else
			{
				// This is a simple associative array
				$key  = $elementKey;
				$text = $element;
			}

			/*
			 * The use of options that contain optgroup HTML elements was
			 * somewhat hacked for J1.5. J1.6 introduces the grouplist() method
			 * to handle this better. The old solution is retained through the
			 * "groups" option, which defaults true in J1.6, but should be
			 * deprecated at some point in the future.
			 */

			$key = (string) $key;

			if ($key === '<OPTGROUP>' && $options['groups'])
			{
				$html       .= $baseIndent . '<optgroup label="' . ($options['list.translate'] ? Text::_($text) : $text) . '">' . $options['format.eol'];
				$baseIndent = str_repeat($options['format.indent'], ++$options['format.depth']);
			}
			elseif ($key === '</OPTGROUP>' && $options['groups'])
			{
				$baseIndent = str_repeat($options['format.indent'], --$options['format.depth']);
				$html       .= $baseIndent . '</optgroup>' . $options['format.eol'];
			}
			else
			{
				// If no string after hyphen - take hyphen out
				$splitText = explode(' - ', $text, 2);
				$text      = $splitText[0];

				if (isset($splitText[1]) && $splitText[1] !== '' && !preg_match('/^[\s]+$/', $splitText[1]))
				{
					$text .= ' - ' . $splitText[1];
				}

				if (!empty($label) && $options['list.translate'])
				{
					$label = Text::_($label);
				}

				if ($options['option.label.toHtml'])
				{
					$label = htmlentities($label);
				}

				if (is_array($attr))
				{
					$attr = ArrayHelper::toString($attr);
				}
				else
				{
					$attr = trim($attr);
				}

				$extra = ($id ? ' id="' . $id . '"' : '') . ($label ? ' label="' . $label . '"' : '') . ($attr ? ' ' . $attr : '') . $extra;

				if (is_array($options['list.select']))
				{
					foreach ($options['list.select'] as $val)
					{
						$key2 = is_object($val) ? $val->{$options['option.key']} : $val;

						if ($key == $key2)
						{
							$extra .= ' selected="selected"';
							break;
						}
					}
				}
				elseif ((string) $key === (string) $options['list.select'])
				{
					$extra .= ' selected="selected"';
				}

				if ($options['list.translate'])
				{
					$text = Text::_($text);
				}

				// Generate the option, encoding as required
				$html .= $baseIndent . '<option value="' . ($options['option.key.toHtml'] ? htmlspecialchars($key, ENT_COMPAT, 'UTF-8') : $key) . '"'
					. $extra . '>';
				$html .= $options['option.text.toHtml'] ? htmlentities(html_entity_decode($text, ENT_COMPAT, 'UTF-8'), ENT_COMPAT, 'UTF-8') : $text;
				$html .= '</option>' . $options['format.eol'];
			}
		}

		return $html;
	}

	/**
	 * Generates an HTML radio list.
	 *
	 * @param   array    $data       An array of objects
	 * @param   string   $name       The value of the HTML name attribute
	 * @param   string   $attribs    Additional HTML attributes for the `<select>` tag
	 * @param   mixed    $optKey     The key that is selected
	 * @param   string   $optText    The name of the object variable for the option value
	 * @param   string   $selected   The name of the object variable for the option text
	 * @param   boolean  $idtag      Value of the field id or null by default
	 * @param   boolean  $translate  True if options will be translated
	 *
	 * @return  string  HTML for the select list
	 */
	public static function radiolist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
	                                 $translate = false)
	{

		$forSelect = false;

		if (isset($attribs['forSelect']))
		{
			$forSelect = (bool) ($attribs['forSelect']);
			unset($attribs['forSelect']);
		}

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

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

		$html = '';

		foreach ($data as $optionObject)
		{
			$optionValue = $optionObject->$optKey;
			$labelText   = $translate ? Text::_($optionObject->$optText) : $optionObject->$optText;
			$id          = ($optionObject->id ?? null);

			$extra = '';
			$id    = $id ? $optionObject->id : $id_text . $optionValue;

			if (is_array($selected))
			{
				foreach ($selected as $val)
				{
					$k2 = is_object($val) ? $val->$optKey : $val;

					if ($optionValue == $k2)
					{
						$extra .= ' selected="selected" ';
						break;
					}
				}
			}
			else
			{
				$extra .= ((string) $optionValue === (string) $selected ? ' checked="checked" ' : '');
			}

			if ($forSelect)
			{
				$html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
					. $attribs . ' />';
				$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">' . $labelText . '</label>';
			}
			else
			{
				$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">';
				$html .= "\n\t\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
					. $attribs . ' />' . $labelText;
				$html .= "\n\t" . '</label>';

			}
		}

		$html .= "\n";

		return $html;
	}

	/**
	 * Creates two radio buttons styled with FEF to appear as a YES/NO switch
	 *
	 * @param   string  $name      Name of the field
	 * @param   string  $selected  Selected field
	 * @param   array   $attribs   Additional attributes to add to the switch
	 *
	 * @return    string    The HTML for the switch
	 */
	public static function booleanswitch($name, $selected, array $attribs = [])
	{
		if (empty($attribs))
		{
			$attribs = ['class' => 'akeeba-toggle'];
		}
		else
		{
			if (isset($attribs['class']))
			{
				$attribs['class'] .= ' akeeba-toggle';
			}
			else
			{
				$attribs['class'] = 'akeeba-toggle';
			}
		}

		$temp = '';

		foreach ($attribs as $key => $value)
		{
			$temp .= $key . ' = "' . $value . '"';
		}

		$attribs = $temp;

		$checked_1 = $selected ? '' : 'checked ';
		$checked_2 = $selected ? 'checked ' : '';

		$html = '<div ' . $attribs . '>';
		$html .= '<input type="radio" class="radio-yes" name="' . $name . '" ' . $checked_2 . 'id="' . $name . '-2" value="1">';
		$html .= '<label for="' . $name . '-2" class="green">' . Text::_('JYES') . '</label>';
		$html .= '<input type="radio" class="radio-no" name="' . $name . '" ' . $checked_1 . 'id="' . $name . '-1" value="0">';
		$html .= '<label for="' . $name . '-1" class="red">' . Text::_('JNO') . '</label>';
		$html .= '</div>';

		return $html;
	}
}
Utils/FEFHelper/browse.php000060400000072505152455514240011432 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

defined('_JEXEC') || die;

use FOF30\Model\DataModel;
use FOF30\Utils\ArrayHelper;
use FOF30\Utils\FEFHelper\BrowseView;
use FOF30\View\DataView\DataViewInterface;
use FOF30\View\DataView\Html;
use FOF30\View\DataView\Raw as DataViewRaw;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Pagination\Pagination;

/**
 * Custom JHtml (HTMLHelper) class. Offers browse view controls compatible with Akeeba Frontend
 * Framework (FEF).
 *
 * Call these methods as JHtml::_('FEFHelper.browse.methodName', $parameter1, $parameter2, ...)
 */
abstract class FEFHelperBrowse
{
	/**
	 * Returns an action button on the browse view's table
	 *
	 * @param   integer       $i               The row index
	 * @param   string        $task            The task to fire when the button is clicked
	 * @param   string|array  $prefix          An optional task prefix or an array of options
	 * @param   string        $active_title    An optional active tooltip to display if $enable is true
	 * @param   string        $inactive_title  An optional inactive tooltip to display if $enable is true
	 * @param   boolean       $tip             An optional setting for tooltip
	 * @param   string        $active_class    An optional active HTML class
	 * @param   string        $inactive_class  An optional inactive HTML class
	 * @param   boolean       $enabled         An optional setting for access control on the action.
	 * @param   boolean       $translate       An optional setting for translation.
	 * @param   string        $checkbox        An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @since   3.3.0
	 */
	public static function action($i, $task, $prefix = '', $active_title = '', $inactive_title = '', $tip = false, $active_class = '',
	                              $inactive_class = '', $enabled = true, $translate = true, $checkbox = 'cb')
	{
		if (is_array($prefix))
		{
			$options        = $prefix;
			$active_title   = array_key_exists('active_title', $options) ? $options['active_title'] : $active_title;
			$inactive_title = array_key_exists('inactive_title', $options) ? $options['inactive_title'] : $inactive_title;
			$tip            = array_key_exists('tip', $options) ? $options['tip'] : $tip;
			$active_class   = array_key_exists('active_class', $options) ? $options['active_class'] : $active_class;
			$inactive_class = array_key_exists('inactive_class', $options) ? $options['inactive_class'] : $inactive_class;
			$enabled        = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$translate      = array_key_exists('translate', $options) ? $options['translate'] : $translate;
			$checkbox       = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix         = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		if ($tip)
		{
			$title = $enabled ? $active_title : $inactive_title;
			$title = $translate ? Text::_($title) : $title;
			$title = HTMLHelper::_('tooltipText', $title, '', 0);
		}

		$html   = [];

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

			if (substr($active_class, 0, 2) == '--')
			{
				[$btnColor, $active_class] = explode(' ', $active_class, 2);
				$btnColor = ltrim($btnColor, '-');
			}

			$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini ' . ($active_class === 'publish' ? ' active' : '') . ($tip ? ' hasTooltip' : '') . '"';
			$html[] = ' href="javascript:void(0);" onclick="return Joomla.listItemTask(\'' . $checkbox . $i . '\',\'' . $prefix . $task . '\')"';
			$html[] = $tip ? ' title="' . $title . '"' : '';
			$html[] = '>';
			$html[] = '<span class="akion-' . $active_class . '" aria-hidden="true"></span>&ensp;';
			$html[] = '</a>';
		}
		else
		{
			$btnColor = 'grey';

			if (substr($inactive_class, 0, 2) == '--')
			{
				[$btnColor, $inactive_class] = explode(' ', $inactive_class, 2);
				$btnColor = ltrim($btnColor, '-');
			}

			$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini disabled akeebagrid' . ($tip ? ' hasTooltip' : '') . '"';
			$html[] = $tip ? ' title="' . $title . '"' : '';
			$html[] = '>';

			if ($active_class === 'protected')
			{
				$inactive_class = 'locked';
			}

			$html[] = '<span class="akion-' . $inactive_class . '"></span>&ensp;';
			$html[] = '</a>';
		}

		return implode($html);
	}

	/**
	 * Returns a state change button on the browse view's table
	 *
	 * @param   array         $states     array of value/state. Each state is an array of the form
	 *                                    (task, text, active title, inactive title, tip (boolean), HTML active class,
	 *                                    HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active
	 *                                    title,
	 *                                    'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active
	 *                                    class,
	 *                                    'inactive_class'=>html inactive class)
	 * @param   integer       $value      The state value.
	 * @param   integer       $i          The row index
	 * @param   string|array  $prefix     An optional task prefix or an array of options
	 * @param   boolean       $enabled    An optional setting for access control on the action.
	 * @param   boolean       $translate  An optional setting for translation.
	 * @param   string        $checkbox   An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @since   3.3.0
	 */
	public static function state($states, $value, $i, $prefix = '', $enabled = true, $translate = true, $checkbox = 'cb')
	{
		if (is_array($prefix))
		{
			$options   = $prefix;
			$enabled   = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
			$checkbox  = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix    = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		$state          = ArrayHelper::getValue($states, (int) $value, $states[0]);
		$task           = array_key_exists('task', $state) ? $state['task'] : $state[0];
		$text           = array_key_exists('text', $state) ? $state['text'] : (array_key_exists(1, $state) ? $state[1] : '');
		$active_title   = array_key_exists('active_title', $state) ? $state['active_title'] : (array_key_exists(2, $state) ? $state[2] : '');
		$inactive_title = array_key_exists('inactive_title', $state) ? $state['inactive_title'] : (array_key_exists(3, $state) ? $state[3] : '');
		$tip            = array_key_exists('tip', $state) ? $state['tip'] : (array_key_exists(4, $state) ? $state[4] : false);
		$active_class   = array_key_exists('active_class', $state) ? $state['active_class'] : (array_key_exists(5, $state) ? $state[5] : '');
		$inactive_class = array_key_exists('inactive_class', $state) ? $state['inactive_class'] : (array_key_exists(6, $state) ? $state[6] : '');

		return static::action(
			$i, $task, $prefix, $active_title, $inactive_title, $tip,
			$active_class, $inactive_class, $enabled, $translate, $checkbox
		);
	}

	/**
	 * Returns a published state on the browse view's table
	 *
	 * @param   integer       $value         The state value.
	 * @param   integer       $i             The row index
	 * @param   string|array  $prefix        An optional task prefix or an array of options
	 * @param   boolean       $enabled       An optional setting for access control on the action.
	 * @param   string        $checkbox      An optional prefix for checkboxes.
	 * @param   string        $publish_up    An optional start publishing date.
	 * @param   string        $publish_down  An optional finish publishing date.
	 *
	 * @return  string  The HTML markup
	 *
	 * @see     self::state()
	 *
	 * @since   3.3.0
	 */
	public static function published($value, $i, $prefix = '', $enabled = true, $checkbox = 'cb', $publish_up = null, $publish_down = null)
	{
		if (is_array($prefix))
		{
			$options  = $prefix;
			$enabled  = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix   = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		/**
		 * Format:
		 *
		 * (task, text, active title, inactive title, tip (boolean), active icon class (without akion-), inactive icon class (without akion-))
		 */
		$states = [
			1  => [
				'unpublish', 'JPUBLISHED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JPUBLISHED', true, '--green checkmark',
				'--green checkmark',
			],
			0  => [
				'publish', 'JUNPUBLISHED', 'JLIB_HTML_PUBLISH_ITEM', 'JUNPUBLISHED', true, '--red close', '--red close',
			],
			2  => [
				'unpublish', 'JARCHIVED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JARCHIVED', true, '--orange ion-ios-box',
				'--orange ion-ios-box',
			],
			-2 => [
				'publish', 'JTRASHED', 'JLIB_HTML_PUBLISH_ITEM', 'JTRASHED', true, '--dark trash-a', '--dark trash-a',
			],
		];

		// Special state for dates
		if ($publish_up || $publish_down)
		{
			$nullDate = Factory::getDbo()->getNullDate();
			$nowDate  = Factory::getDate()->toUnix();

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

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

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

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

			if ($publish_down)
			{
				$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publish_down, Text::_('DATE_FORMAT_LC5'), 'UTC'));
			}

			$tip = empty($tips) ? false : implode('<br />', $tips);

			// Add tips and special titles
			foreach ($states as $key => $state)
			{
				// Create special titles for published items
				if ($key == 1)
				{
					$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_ITEM';

					if (!empty($publish_up) && ($publish_up != $nullDate) && $nowDate < $publish_up->toUnix())
					{
						$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_PENDING_ITEM';
						$states[$key][5] = $states[$key][6] = 'android-time';
					}

					if (!empty($publish_down) && ($publish_down != $nullDate) && $nowDate > $publish_down->toUnix())
					{
						$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_EXPIRED_ITEM';
						$states[$key][5] = $states[$key][6] = 'alert';
					}
				}

				// Add tips to titles
				if ($tip)
				{
					$states[$key][1] = Text::_($states[$key][1]);
					$states[$key][2] = Text::_($states[$key][2]) . '<br />' . $tip;
					$states[$key][3] = Text::_($states[$key][3]) . '<br />' . $tip;
					$states[$key][4] = true;
				}
			}

			return static::state($states, $value, $i, [
				'prefix' => $prefix, 'translate' => !$tip,
			], $enabled, true, $checkbox);
		}

		return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
	}

	/**
	 * Returns an isDefault state on the browse view's table
	 *
	 * @param   integer       $value     The state value.
	 * @param   integer       $i         The row index
	 * @param   string|array  $prefix    An optional task prefix or an array of options
	 * @param   boolean       $enabled   An optional setting for access control on the action.
	 * @param   string        $checkbox  An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @see     self::state()
	 * @since   3.3.0
	 */
	public static function isdefault($value, $i, $prefix = '', $enabled = true, $checkbox = 'cb')
	{
		if (is_array($prefix))
		{
			$options  = $prefix;
			$enabled  = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix   = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		$states = [
			0 => ['setDefault', '', 'JLIB_HTML_SETDEFAULT_ITEM', '', 1, 'android-star-outline', 'android-star-outline'],
			1 => [
				'unsetDefault', 'JDEFAULT', 'JLIB_HTML_UNSETDEFAULT_ITEM', 'JDEFAULT', 1, 'android-star',
				'android-star',
			],
		];

		return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
	}

	/**
	 * Returns a checked-out icon
	 *
	 * @param   integer       $i           The row index.
	 * @param   string        $editorName  The name of the editor.
	 * @param   string        $time        The time that the object was checked out.
	 * @param   string|array  $prefix      An optional task prefix or an array of options
	 * @param   boolean       $enabled     True to enable the action.
	 * @param   string        $checkbox    An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @since   3.3.0
	 */
	public static function checkedout($i, $editorName, $time, $prefix = '', $enabled = false, $checkbox = 'cb')
	{
		HTMLHelper::_('bootstrap.tooltip');

		if (is_array($prefix))
		{
			$options  = $prefix;
			$enabled  = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix   = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		$text           = $editorName . '<br />' . HTMLHelper::_('date', $time, Text::_('DATE_FORMAT_LC')) . '<br />' . HTMLHelper::_('date', $time, 'H:i');
		$active_title   = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKIN'), $text, 0);
		$inactive_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKED_OUT'), $text, 0);

		return static::action(
			$i, 'checkin', $prefix, html_entity_decode($active_title, ENT_QUOTES, 'UTF-8'),
			html_entity_decode($inactive_title, ENT_QUOTES, 'UTF-8'), true, 'locked', 'locked', $enabled, false, $checkbox
		);
	}

	/**
	 * Returns the drag'n'drop reordering field for Browse views
	 *
	 * @param   string             $orderingField  The name of the field you're ordering by
	 * @param   string             $order          The order value of the current row
	 * @param   string             $class          CSS class for the ordering value INPUT field
	 * @param   string             $icon           CSS class for the d'n'd handle icon
	 * @param   string             $inactiveIcon   CSS class for the d'n'd disabled icon
	 * @param   DataViewInterface  $view           The view you're rendering against. Leave null for auto-detection.
	 *
	 * @return string
	 */
	public static function order($orderingField, $order, $class = 'input-sm', $icon = 'akion-android-more-vertical', $inactiveIcon = 'akion-android-more-vertical', DataViewInterface $view = null)
	{
		/** @var Html $view */
		if (is_null($view))
		{
			$view = BrowseView::getViewFromBacktrace();
		}
		$dndOrderingActive = $view->getLists()->order == $orderingField;

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

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

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

			if (!is_array($hasAjaxOrderingSupport) || !$hasAjaxOrderingSupport['saveOrder'])
			{
				$disabledLabel    = Text::_('JORDERINGDISABLED');
				$disableClassName = 'inactive tip-top hasTooltip';
			}

			$orderClass = $dndOrderingActive ? 'order-enabled' : 'order-disabled';

			$html = '<div class="' . $orderClass . '">';
			$html .= '<span class="sortable-handler ' . $disableClassName . '" title="' . $disabledLabel . '">';
			$html .= '<span class="' . ($disableClassName ? $inactiveIcon : $icon) . '"></span>';
			$html .= '</span>';

			if ($dndOrderingActive)
			{
				$joomla35IsBroken = version_compare(JVERSION, '3.5.0', 'ge') ? 'style="display: none"' : '';

				$html .= '<input type="text" name="order[]" ' . $joomla35IsBroken . ' size="5" class="' . $class . ' text-area-order" value="' . $order . '" />';
			}

			$html .= '</div>';
		}

		return $html;
	}

	/**
	 * Returns the drag'n'drop reordering table header for Browse views
	 *
	 * @param   string  $orderingField  The name of the field you're ordering by
	 * @param   string  $icon           CSS class for the d'n'd handle icon
	 *
	 * @return string
	 */
	public static function orderfield($orderingField = 'ordering', $icon = 'akion-stats-bars')
	{
		$title         = Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN');
		$orderingLabel = Text::_('JFIELD_ORDERING_LABEL');

		return <<< HTML
<a href="#"
   onclick="Joomla.tableOrdering('{$orderingField}','asc','');return false;"
   class="hasPopover"
   title="{$orderingLabel}"
   data-content="{$title}"
   data-placement="top"
>
    <span class="{$icon}"></span>
</a>

HTML;
	}


	/**
	 * Creates an order-up action icon.
	 *
	 * @param   integer       $i         The row index.
	 * @param   string        $task      An optional task to fire.
	 * @param   string|array  $prefix    An optional task prefix or an array of options
	 * @param   string        $text      An optional text to display
	 * @param   boolean       $enabled   An optional setting for access control on the action.
	 * @param   string        $checkbox  An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @since   3.3.0
	 */
	public static function orderUp($i, $task = 'orderup', $prefix = '', $text = 'JLIB_HTML_MOVE_UP', $enabled = true, $checkbox = 'cb')
	{
		if (is_array($prefix))
		{
			$options  = $prefix;
			$text     = array_key_exists('text', $options) ? $options['text'] : $text;
			$enabled  = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix   = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		return static::action($i, $task, $prefix, $text, $text, false, 'arrow-up-b', 'arrow-up-b', $enabled, true, $checkbox);
	}

	/**
	 * Creates an order-down action icon.
	 *
	 * @param   integer       $i         The row index.
	 * @param   string        $task      An optional task to fire.
	 * @param   string|array  $prefix    An optional task prefix or an array of options
	 * @param   string        $text      An optional text to display
	 * @param   boolean       $enabled   An optional setting for access control on the action.
	 * @param   string        $checkbox  An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @since   3.3.0
	 */
	public static function orderDown($i, $task = 'orderdown', $prefix = '', $text = 'JLIB_HTML_MOVE_DOWN', $enabled = true, $checkbox = 'cb')
	{
		if (is_array($prefix))
		{
			$options  = $prefix;
			$text     = array_key_exists('text', $options) ? $options['text'] : $text;
			$enabled  = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
			$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
			$prefix   = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		}

		return static::action($i, $task, $prefix, $text, $text, false, 'arrow-down-b', 'arrow-down-b', $enabled, true, $checkbox);
	}

	/**
	 * Table header for a field which changes the sort order when clicked
	 *
	 * @param   string  $title          The link title
	 * @param   string  $order          The order field for the column
	 * @param   string  $direction      The current direction
	 * @param   string  $selected       The selected ordering
	 * @param   string  $task           An optional task override
	 * @param   string  $new_direction  An optional direction for the new column
	 * @param   string  $tip            An optional text shown as tooltip title instead of $title
	 * @param   string  $form           An optional form selector
	 *
	 * @return  string
	 *
	 * @since   3.3.0
	 */
	public static function sort($title, $order, $direction = 'asc', $selected = '', $task = null, $new_direction = 'asc', $tip = '', $form = null)
	{
		HTMLHelper::_('behavior.core');
		HTMLHelper::_('bootstrap.popover');

		$direction = strtolower($direction);
		$icon      = ['akion-android-arrow-dropup', 'akion-android-arrow-dropdown'];
		$index     = (int) ($direction === 'desc');

		if ($order != $selected)
		{
			$direction = $new_direction;
		}
		else
		{
			$direction = $direction === 'desc' ? 'asc' : 'desc';
		}

		if ($form)
		{
			$form = ', document.getElementById(\'' . $form . '\')';
		}

		$html = '<a href="#" onclick="Joomla.tableOrdering(\'' . $order . '\',\'' . $direction . '\',\'' . $task . '\'' . $form . ');return false;"'
			. ' class="hasPopover" title="' . htmlspecialchars(Text::_($tip ?: $title)) . '"'
			. ' data-content="' . htmlspecialchars(Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN')) . '" data-placement="top">';

		if (isset($title['0']) && $title['0'] === '<')
		{
			$html .= $title;
		}
		else
		{
			$html .= Text::_($title);
		}

		if ($order == $selected)
		{
			$html .= '<span class="' . $icon[$index] . '"></span>';
		}

		$html .= '</a>';

		return $html;
	}

	/**
	 * Method to check all checkboxes on the browse view's table
	 *
	 * @param   string  $name    The name of the form element
	 * @param   string  $tip     The text shown as tooltip title instead of $tip
	 * @param   string  $action  The action to perform on clicking the checkbox
	 *
	 * @return  string
	 *
	 * @since   3.3.0
	 */
	public static function checkall($name = 'checkall-toggle', $tip = 'JGLOBAL_CHECK_ALL', $action = 'Joomla.checkAll(this)')
	{
		HTMLHelper::_('behavior.core');
		HTMLHelper::_('bootstrap.tooltip');

		return '<input type="checkbox" name="' . $name . '" value="" class="hasTooltip" title="' . HTMLHelper::_('tooltipText', $tip)
			. '" onclick="' . $action . '" />';
	}

	/**
	 * Method to create a checkbox for a grid row.
	 *
	 * @param   integer  $rowNum      The row index
	 * @param   integer  $recId       The record id
	 * @param   boolean  $checkedOut  True if item is checked out
	 * @param   string   $name        The name of the form element
	 * @param   string   $stub        The name of stub identifier
	 *
	 * @return  mixed    String of html with a checkbox if item is not checked out, null if checked out.
	 *
	 * @since   3.3.0
	 */
	public static function id($rowNum, $recId, $checkedOut = false, $name = 'cid', $stub = 'cb')
	{
		return $checkedOut ? '' : '<input type="checkbox" id="' . $stub . $rowNum . '" name="' . $name . '[]" value="' . $recId
			. '" onclick="Joomla.isChecked(this.checked);" />';
	}

	/**
	 * Include the necessary JavaScript for the browse view's table order feature
	 *
	 * @param   string  $orderBy  Filed by which we are currently sorting the table.
	 * @param   bool    $return   Should I return the JS? Default: false (= add to the page's head)
	 *
	 * @return string
	 */
	public static function orderjs($orderBy, $return = false)
	{
		$js = <<< JS

Joomla.orderTable = function()
{
		var table = document.getElementById("sortTable");
		var direction = document.getElementById("directionTable");
		var order = table.options[table.selectedIndex].value;
		var dirn = 'asc';

		if (order !== '$orderBy')
		{
			dirn = 'asc';
		}
		else {
			dirn = direction.options[direction.selectedIndex].value;
		}

		Joomla.tableOrdering(order, dirn);
	};
JS;

		if ($return)
		{
			return $js;
		}

		try
		{
			Factory::getApplication()->getDocument()->addScriptDeclaration($js);
		}
		catch (Exception $e)
		{
			// If we have no application, well, not having table sorting JS is the least of your worries...
		}
	}

	/**
	 * Returns the table ordering / pagination header for a browse view: number of records to display, order direction,
	 * order by field.
	 *
	 * @param   DataViewRaw  $view                              The view you're rendering against. If not provided we
	 *                                                          will guess it using MAGIC.
	 * @param   array        $sortFields                        Array of field name => description for the ordering
	 *                                                          fields in the dropdown. If not provided we will use all
	 *                                                          the fields available in the model.
	 * @param   Pagination   $pagination                        The Joomla pagination object. If not provided we fetch
	 *                                                          it from the view.
	 * @param   string       $sortBy                            Order by field name. If not provided we fetch it from
	 *                                                          the view.
	 * @param   string       $order_Dir                         Order direction. If not provided we fetch it from the
	 *                                                          view.
	 *
	 * @return  string
	 *
	 * @since   3.3.0
	 */
	public static function orderheader(DataViewRaw $view = null, array $sortFields = [], Pagination $pagination = null, $sortBy = null, $order_Dir = null)
	{
		if (is_null($view))
		{
			$view = BrowseView::getViewFromBacktrace();
		}

		if (empty($sortFields))
		{
			/** @var DataModel $model */
			$model      = $view->getModel();
			$sortFields = $view->getLists()->sortFields ?? [];
			$sortFields = empty($sortFields) ? self::getSortFields($model) : $sortFields;
		}

		if (empty($pagination))
		{
			$pagination = $view->getPagination();
		}

		if (empty($sortBy))
		{
			$sortBy = $view->getLists()->order;
		}

		if (empty($order_Dir))
		{
			$order_Dir = $view->getLists()->order_Dir;

			if (empty($order_Dir))
			{
				$order_Dir = 'asc';
			}
		}

		// Static hidden text labels
		$limitLabel    = Text::_('JFIELD_PLG_SEARCH_SEARCHLIMIT_DESC');
		$orderingDescr = Text::_('JFIELD_ORDERING_DESC');
		$sortByLabel   = Text::_('JGLOBAL_SORT_BY');

		// Order direction dropdown
		$directionSelect = HTMLHelper::_('FEFHelper.select.genericlist', [
			''     => $orderingDescr,
			'asc'  => Text::_('JGLOBAL_ORDER_ASCENDING'),
			'desc' => Text::_('JGLOBAL_ORDER_DESCENDING'),
		], 'directionTable', [
			'id'          => 'directionTable',
			'list.select' => $order_Dir,
			'list.attr'   => [
				'class'    => 'input-medium custom-select',
				'onchange' => 'Joomla.orderTable()',
			],
		]);

		// Sort by field dropdown

		$sortTable = HTMLHelper::_('FEFHelper.select.genericlist', array_merge([
			'' => Text::_('JGLOBAL_SORT_BY'),
		], $sortFields), 'sortTable', [
			'id'          => 'sortTable',
			'list.select' => $sortBy,
			'list.attr'   => [
				'class'    => 'input-medium custom-select',
				'onchange' => 'Joomla.orderTable()',
			],
		]);


		$html = <<<HTML
		<div class="akeeba-filter-element akeeba-form-group">
			<label for="limit" class="element-invisible">
				$limitLabel
			</label>
			{$pagination->getLimitBox()}
		</div>

		<div class="akeeba-filter-element akeeba-form-group">
			<label for="directionTable" class="element-invisible">
				$orderingDescr
			</label>
			$directionSelect
		</div>

		<div class="akeeba-filter-element akeeba-form-group">
			<label for="sortTable" class="element-invisible">
				{$sortByLabel}
			</label>
			$sortTable
		</div>

HTML;

		return $html;
	}

	/**
	 * Get the default sort fields from a model. It creates a hash array where the keys are the model's field names and
	 * the values are the translation keys for their names, following FOF's naming conventions.
	 *
	 * @param   DataModel  $model  The model for which we get the sort fields
	 *
	 * @return  array
	 *
	 * @since   3.3.0
	 */
	private static function getSortFields(DataModel $model)
	{
		$sortFields         = [];
		$idField            = $model->getIdFieldName() ?: 'id';
		$defaultFieldLabels = [
			'publish_up'   => 'JGLOBAL_FIELD_PUBLISH_UP_LABEL',
			'publish_down' => 'JGLOBAL_FIELD_PUBLISH_DOWN_LABEL',
			'created_by'   => 'JGLOBAL_FIELD_CREATED_BY_LABEL',
			'created_on'   => 'JGLOBAL_FIELD_CREATED_LABEL',
			'modified_by'  => 'JGLOBAL_FIELD_MODIFIED_BY_LABEL',
			'modified_on'  => 'JGLOBAL_FIELD_MODIFIED_LABEL',
			'ordering'     => 'JGLOBAL_FIELD_FIELD_ORDERING_LABEL',
			'id'           => 'JGLOBAL_FIELD_ID_LABEL',
			'hits'         => 'JGLOBAL_HITS',
			'title'        => 'JGLOBAL_TITLE',
			'user_id'      => 'JGLOBAL_USERNAME',
			'username'     => 'JGLOBAL_USERNAME',
		];
		$componentName      = $model->getContainer()->componentName;
		$viewNameSingular   = $model->getContainer()->inflector->singularize($model->getName());
		$viewNamePlural     = $model->getContainer()->inflector->pluralize($model->getName());

		foreach ($model->getFields() as $field => $fieldDescriptor)
		{
			$possibleKeys = [
				$componentName . '_' . $viewNamePlural . '_FIELD_' . $field,
				$componentName . '_' . $viewNamePlural . '_' . $field,
				$componentName . '_' . $viewNameSingular . '_FIELD_' . $field,
				$componentName . '_' . $viewNameSingular . '_' . $field,
			];

			if (array_key_exists($field, $defaultFieldLabels))
			{
				$possibleKeys[] = $defaultFieldLabels[$field];
			}

			if ($field === $idField)
			{
				$possibleKeys[] = $defaultFieldLabels['id'];
			}

			$fieldLabel = '';

			foreach ($possibleKeys as $langKey)
			{
				$langKey    = strtoupper($langKey);
				$fieldLabel = Text::_($langKey);

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

				$fieldLabel = '';
			}

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

		}

		return $sortFields;
	}
}
Utils/Collection.php000060400000032507152455514240010462 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

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

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

	/**
	 * Create a new collection.
	 *
	 * @param   array  $items
	 */
	public function __construct(array $items = [])
	{
		$this->items = $items;
	}

	/**
	 * Create a new collection instance if the value isn't one already.
	 *
	 * @param   mixed  $items
	 *
	 * @return static
	 */
	public static function make($items)
	{
		if (is_null($items))
		{
			return new static;
		}

		if ($items instanceof Collection)
		{
			return $items;
		}

		return new static(is_array($items) ? $items : [$items]);
	}

	/**
	 * Get all of the items in the collection.
	 *
	 * @return array
	 */
	public function all()
	{
		return $this->items;
	}

	/**
	 * Collapse the collection items into a single array.
	 *
	 * @return static
	 */
	public function collapse()
	{
		$results = [];

		foreach ($this->items as $values)
		{
			$results = array_merge($results, $values);
		}

		return new static($results);
	}

	/**
	 * Diff the collection with the given items.
	 *
	 * @param   Collection|array  $items
	 *
	 * @return static
	 */
	public function diff($items)
	{
		return new static(array_diff($this->items, $this->getArrayableItems($items)));
	}

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

		return $this;
	}

	/**
	 * Fetch a nested element of the collection.
	 *
	 * @param   string  $key
	 *
	 * @return static
	 */
	public function fetch($key)
	{
		return new static(array_fetch($this->items, $key));
	}

	/**
	 * Run a filter over each of the items.
	 *
	 * @param   Closure  $callback
	 *
	 * @return static
	 */
	public function filter(Closure $callback)
	{
		return new static(array_filter($this->items, $callback));
	}

	/**
	 * Get the first item from the collection.
	 *
	 * @param   Closure  $callback
	 * @param   mixed    $default
	 *
	 * @return mixed|null
	 */
	public function first(Closure $callback = null, $default = null)
	{
		if (is_null($callback))
		{
			return count($this->items) > 0 ? reset($this->items) : null;
		}
		else
		{
			return array_first($this->items, $callback, $default);
		}
	}

	/**
	 * Get a flattened array of the items in the collection.
	 *
	 * @return array
	 */
	public function flatten()
	{
		return new static(array_flatten($this->items));
	}

	/**
	 * Remove an item from the collection by key.
	 *
	 * @param   mixed  $key
	 *
	 * @return void
	 */
	public function forget($key)
	{
		unset($this->items[$key]);
	}

	/**
	 * Get an item from the collection by key.
	 *
	 * @param   mixed  $key
	 * @param   mixed  $default
	 *
	 * @return mixed
	 */
	public function get($key, $default = null)
	{
		if (array_key_exists($key, $this->items))
		{
			return $this->items[$key];
		}

		return value($default);
	}

	/**
	 * Group an associative array by a field or Closure value.
	 *
	 * @param   callable|string  $groupBy
	 *
	 * @return static
	 */
	public function groupBy($groupBy)
	{
		$results = [];

		foreach ($this->items as $key => $value)
		{
			$key = is_callable($groupBy) ? $groupBy($value, $key) : array_get($value, $groupBy);

			$results[$key][] = $value;
		}

		return new static($results);
	}

	/**
	 * Determine if an item exists in the collection by key.
	 *
	 * @param   mixed  $key
	 *
	 * @return bool
	 */
	public function has($key)
	{
		return array_key_exists($key, $this->items);
	}

	/**
	 * Concatenate values of a given key as a string.
	 *
	 * @param   string  $value
	 * @param   string  $glue
	 *
	 * @return string
	 */
	public function implode($value, $glue = null)
	{
		if (is_null($glue))
		{
			return implode($this->lists($value));
		}

		return implode($glue, $this->lists($value));
	}

	/**
	 * Intersect the collection with the given items.
	 *
	 * @param   Collection|array  $items
	 *
	 * @return static
	 */
	public function intersect($items)
	{
		return new static(array_intersect($this->items, $this->getArrayableItems($items)));
	}

	/**
	 * Determine if the collection is empty or not.
	 *
	 * @return bool
	 */
	public function isEmpty()
	{
		return empty($this->items);
	}

	/**
	 * Get the last item from the collection.
	 *
	 * @return mixed|null
	 */
	public function last()
	{
		return count($this->items) > 0 ? end($this->items) : null;
	}

	/**
	 * Get an array with the values of a given key.
	 *
	 * @param   string  $value
	 * @param   string  $key
	 *
	 * @return array
	 */
	public function lists($value, $key = null)
	{
		return array_pluck($this->items, $value, $key);
	}

	/**
	 * Run a map over each of the items.
	 *
	 * @param   Closure  $callback
	 *
	 * @return static
	 */
	public function map(Closure $callback)
	{
		return new static(array_map($callback, $this->items, array_keys($this->items)));
	}

	/**
	 * Merge the collection with the given items.
	 *
	 * @param   Collection|array  $items
	 *
	 * @return static
	 */
	public function merge($items)
	{
		return new static(array_merge($this->items, $this->getArrayableItems($items)));
	}

	/**
	 * Get and remove the last item from the collection.
	 *
	 * @return mixed|null
	 */
	public function pop()
	{
		return array_pop($this->items);
	}

	/**
	 * Push an item onto the beginning of the collection.
	 *
	 * @param   mixed  $value
	 *
	 * @return void
	 */
	public function prepend($value)
	{
		array_unshift($this->items, $value);
	}

	/**
	 * Push an item onto the end of the collection.
	 *
	 * @param   mixed  $value
	 *
	 * @return void
	 */
	public function push($value)
	{
		$this->items[] = $value;
	}

	/**
	 * Put an item in the collection by key.
	 *
	 * @param   mixed  $key
	 * @param   mixed  $value
	 *
	 * @return void
	 */
	public function put($key, $value)
	{
		$this->items[$key] = $value;
	}

	/**
	 * Reduce the collection to a single value.
	 *
	 * @param   callable  $callback
	 * @param   mixed     $initial
	 *
	 * @return mixed
	 */
	public function reduce($callback, $initial = null)
	{
		return array_reduce($this->items, $callback, $initial);
	}

	/**
	 * Get one or more items randomly from the collection.
	 *
	 * @param   int  $amount
	 *
	 * @return mixed
	 */
	public function random($amount = 1)
	{
		$keys = array_rand($this->items, $amount);

		return is_array($keys) ? array_intersect_key($this->items, array_flip($keys)) : $this->items[$keys];
	}

	/**
	 * Reverse items order.
	 *
	 * @return static
	 */
	public function reverse()
	{
		return new static(array_reverse($this->items));
	}

	/**
	 * Get and remove the first item from the collection.
	 *
	 * @return mixed|null
	 */
	public function shift()
	{
		return array_shift($this->items);
	}

	/**
	 * Slice the underlying collection array.
	 *
	 * @param   int   $offset
	 * @param   int   $length
	 * @param   bool  $preserveKeys
	 *
	 * @return static
	 */
	public function slice($offset, $length = null, $preserveKeys = false)
	{
		return new static(array_slice($this->items, $offset, $length, $preserveKeys));
	}

	/**
	 * Sort through each item with a callback.
	 *
	 * @param   Closure  $callback
	 *
	 * @return static
	 */
	public function sort(Closure $callback)
	{
		uasort($this->items, $callback);

		return $this;
	}

	/**
	 * Sort the collection using the given Closure.
	 *
	 * @param   Closure|string  $callback
	 * @param   int             $options
	 * @param   bool            $descending
	 *
	 * @return static
	 */
	public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
	{
		$results = [];

		if (is_string($callback))
		{
			$callback =
				$this->valueRetriever($callback);
		}

		// First we will loop through the items and get the comparator from a callback
		// function which we were given. Then, we will sort the returned values and
		// and grab the corresponding values for the sorted keys from this array.
		foreach ($this->items as $key => $value)
		{
			$results[$key] = $callback($value);
		}

		$descending ? arsort($results, $options)
			: asort($results, $options);

		// Once we have sorted all of the keys in the array, we will loop through them
		// and grab the corresponding model so we can set the underlying items list
		// to the sorted version. Then we'll just return the collection instance.
		foreach (array_keys($results) as $key)
		{
			$results[$key] = $this->items[$key];
		}

		$this->items = $results;

		return $this;
	}

	/**
	 * Sort the collection in descending order using the given Closure.
	 *
	 * @param   Closure|string  $callback
	 * @param   int             $options
	 *
	 * @return static
	 */
	public function sortByDesc($callback, $options = SORT_REGULAR)
	{
		return $this->sortBy($callback, $options, true);
	}

	/**
	 * Splice portion of the underlying collection array.
	 *
	 * @param   int    $offset
	 * @param   int    $length
	 * @param   mixed  $replacement
	 *
	 * @return static
	 */
	public function splice($offset, $length = 0, $replacement = [])
	{
		return new static(array_splice($this->items, $offset, $length, $replacement));
	}

	/**
	 * Get the sum of the given values.
	 *
	 * @param   Closure|string  $callback
	 *
	 * @return mixed
	 */
	public function sum($callback)
	{
		if (is_string($callback))
		{
			$callback = $this->valueRetriever($callback);
		}

		return $this->reduce(function ($result, $item) use ($callback) {
			return $result += $callback($item);

		}, 0);
	}

	/**
	 * Take the first or last {$limit} items.
	 *
	 * @param   int  $limit
	 *
	 * @return static
	 */
	public function take($limit = null)
	{
		if ($limit < 0)
		{
			return $this->slice($limit, abs($limit));
		}

		return $this->slice(0, $limit);
	}

	/**
	 * Resets the Collection (removes all items)
	 *
	 * @return  Collection
	 */
	public function reset()
	{
		$this->items = [];

		return $this;
	}

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

		return $this;
	}

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

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

		return $this;
	}

	/**
	 * Get the collection of items as a plain array.
	 *
	 * @return array
	 */
	public function toArray()
	{
		return array_map(function ($value) {
			return (is_object($value) && method_exists($value, 'toArray')) ? $value->toArray() : $value;

		}, $this->items);
	}

	/**
	 * Convert the object into something JSON serializable.
	 *
	 * @return array
	 */
	public function jsonSerialize()
	{
		return $this->toArray();
	}

	/**
	 * Get the collection of items as JSON.
	 *
	 * @param   int  $options
	 *
	 * @return string
	 */
	public function toJson($options = 0)
	{
		return json_encode($this->toArray(), $options);
	}

	/**
	 * Get an iterator for the items.
	 *
	 * @return ArrayIterator
	 */
	public function getIterator()
	{
		return new ArrayIterator($this->items);
	}

	/**
	 * Get a CachingIterator instance.
	 *
	 * @param   integer  $flags  Caching iterator flags
	 *
	 * @return CachingIterator
	 */
	public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING)
	{
		return new CachingIterator($this->getIterator(), $flags);
	}

	/**
	 * Count the number of items in the collection.
	 *
	 * @return int
	 */
	public function count()
	{
		return count($this->items);
	}

	/**
	 * Determine if an item exists at an offset.
	 *
	 * @param   mixed  $key
	 *
	 * @return bool
	 */
	public function offsetExists($key)
	{
		return array_key_exists($key, $this->items);
	}

	/**
	 * Get an item at a given offset.
	 *
	 * @param   mixed  $key
	 *
	 * @return mixed
	 */
	public function offsetGet($key)
	{
		return $this->items[$key];
	}

	/**
	 * Set the item at a given offset.
	 *
	 * @param   mixed  $key
	 * @param   mixed  $value
	 *
	 * @return void
	 */
	public function offsetSet($key, $value)
	{
		if (is_null($key))
		{
			$this->items[] = $value;
		}
		else
		{
			$this->items[$key] = $value;
		}
	}

	/**
	 * Unset the item at a given offset.
	 *
	 * @param   string  $key
	 *
	 * @return void
	 */
	public function offsetUnset($key)
	{
		unset($this->items[$key]);
	}

	/**
	 * Convert the collection to its string representation.
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->toJson();
	}

	/**
	 * Get a value retrieving callback.
	 *
	 * @param   string  $value
	 *
	 * @return Closure
	 */
	protected function valueRetriever($value)
	{
		return function ($item) use ($value) {
			return is_object($item) ? $item->{$value} : array_get($item, $value);
		};
	}

	/**
	 * Results array of items from Collection.
	 *
	 * @param   Collection|array  $items
	 *
	 * @return array
	 */
	private function getArrayableItems($items)
	{
		if ($items instanceof Collection)
		{
			$items = $items->all();
		}
		elseif (is_object($items) && method_exists($items, 'toArray'))
		{
			$items = $items->toArray();
		}

		return $items;
	}

}
Utils/CacheCleaner.php000060400000006260152455514240010661 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;

/**
 * A utility class to help you quickly clean the Joomla! cache
 */
class CacheCleaner
{
	/**
	 * Clears the com_modules and com_plugins cache. You need to call this whenever you alter the publish state or
	 * parameters of a module or plugin from your code.
	 *
	 * @return  void
	 */
	public static function clearPluginsAndModulesCache()
	{
		self::clearPluginsCache();
		self::clearModulesCache();
	}

	/**
	 * Clears the com_plugins cache. You need to call this whenever you alter the publish state or parameters of a
	 * plugin from your code.
	 *
	 * @return  void
	 */
	public static function clearPluginsCache()
	{
		self::clearCacheGroups(['com_plugins'], [0, 1]);
	}

	/**
	 * Clears the com_modules cache. You need to call this whenever you alter the publish state or parameters of a
	 * module from your code.
	 *
	 * @return  void
	 */
	public static function clearModulesCache()
	{
		self::clearCacheGroups(['com_modules'], [0, 1]);
	}

	/**
	 * Clears the specified cache groups.
	 *
	 * @param   array        $clearGroups   Which cache groups to clear. Usually this is com_yourcomponent to clear
	 *                                      your component's cache.
	 * @param   array        $cacheClients  Which cache clients to clear. 0 is the back-end, 1 is the front-end. If you
	 *                                      do not specify anything, both cache clients will be cleared.
	 * @param   string|null  $event         An event to run upon trying to clear the cache. Empty string to disable. If
	 *                                      NULL and the group is "com_content" I will trigger onContentCleanCache.
	 *
	 * @return  void
	 */
	public static function clearCacheGroups(array $clearGroups, array $cacheClients = [0, 1], $event = null)
	{
		try
		{
			$app = Factory::getApplication();
		}
		catch (Exception $e)
		{
			return;
		}

		foreach ($clearGroups as $group)
		{
			foreach ($cacheClients as $client_id)
			{
				$options = [
					'defaultgroup' => $group,
					'cachebase'    => ($client_id) ? JPATH_ADMINISTRATOR . '/cache' : $app->get('cache_path', JPATH_SITE . '/cache'),
					'result'       => true,
				];

				try
				{
					$cache = Cache::getInstance('callback', $options);
					/** @noinspection PhpUndefinedMethodInspection Available via __call(), not tagged in Joomla core */
					$cache->clean();
				}
				catch (Exception $e)
				{
					$options['result'] = false;
				}

				/**
				 * Call Joomla's cache cleaning plugin event (e.g. onContentCleanCache) as well.
				 *
				 * @see BaseDatabaseModel::cleanCache()
				 */
				if (!empty($event))
				{
					$app->triggerEvent($event, $options);
				}
				/**
				 * If you're cleaning com_content and you have passed no event name I will use onContentCleanCache.
				 */
				elseif (($group == 'com_content') && ($event !== ''))
				{
					$app->triggerEvent('onContentCleanCache', $options);
				}
			}
		}
	}
}
Utils/DynamicGroups.php000060400000013433152455514240011150 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use ReflectionClass;
use ReflectionException;
use ReflectionObject;

/**
 * Dynamic user to user group assignment.
 *
 * This class allows you to add / remove the currently logged in user to a user group without writing the information to
 * the database. This is useful when you want to allow core and third party code to allow or prohibit display of
 * information and / or taking actions based on a condition controlled in your code.
 */
class DynamicGroups
{
	/**
	 * Add the current user to a user group just for this page load.
	 *
	 * @param   int  $groupID  The group ID to add the current user into.
	 *
	 * @return  void
	 */
	public static function addGroup($groupID)
	{
		self::addRemoveGroup($groupID, true);
		self::cleanUpUserObjectCache();
	}

	/**
	 * Remove the current user from a user group just for this page load.
	 *
	 * @param   int  $groupID  The group ID to remove the current user from.
	 *
	 * @return  void
	 */
	public static function removeGroup($groupID)
	{
		self::addRemoveGroup($groupID, false);
		self::cleanUpUserObjectCache();
	}

	/**
	 * Internal function to add or remove the current user from a user group just for this page load.
	 *
	 * @param   int   $groupID  The group ID to add / remove the current user from.
	 * @param   bool  $add      Add (true) or remove (false) the user?
	 *
	 * @return  void
	 */
	protected static function addRemoveGroup($groupID, $add)
	{
		// Get a fake container (we need it for its platform interface)
		$container = Container::getInstance('com_FOOBAR');

		/**
		 * Make sure that Joomla has retrieved the user's groups from the database.
		 *
		 * By going through the User object's getAuthorisedGroups we force Joomla to go through Access::getGroupsByUser
		 * which retrieves the information from the database and caches it into the Access helper class.
		 */
		$container->platform->getUser()->getAuthorisedGroups();
		$container->platform->getUser($container->platform->getUser()->id)->getAuthorisedGroups();

		/**
		 * Now we can get a Reflection object into Joomla's Access helper class and manipulate its groupsByUser cache.
		 */
		$className = class_exists('Joomla\\CMS\\Access\\Access') ? 'Joomla\\CMS\\Access\\Access' : 'JAccess';

		try
		{
			$reflectedAccess = new ReflectionClass($className);
		}
		catch (ReflectionException $e)
		{
			// This should never happen!
			$container->platform->logDebug('Cannot locate the Joomla\\CMS\\Access\\Access or JAccess class. Is your Joomla installation broken or too old / too new?');

			return;
		}

		$groupsByUser = $reflectedAccess->getProperty('groupsByUser');
		$groupsByUser->setAccessible(true);
		$rawGroupsByUser = $groupsByUser->getValue();

		/**
		 * Next up, we need to manipulate the keys of the cache which contain user to user group assignments.
		 *
		 * $rawGroupsByUser (JAccess::$groupsByUser) stored the group ownership as userID:recursive e.g. 0:1 for the
		 * default user, recursive. We need to deal with four keys: 0:1, 0:0, myID:1 and myID:0
		 */
		$user = $container->platform->getUser();
		$keys = ['0:1', '0:0', $user->id . ':1', $user->id . ':0'];

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

			$groups = $rawGroupsByUser[$key];

			if ($add)
			{
				if (in_array($groupID, $groups))
				{
					continue;
				}

				$groups[] = $groupID;
			}
			else
			{
				if (!in_array($groupID, $groups))
				{
					continue;
				}

				$removeKey = array_search($groupID, $groups);
				unset($groups[$removeKey]);
			}

			$rawGroupsByUser[$key] = $groups;
		}

		// We can commit our changes back to the cache property and make it publicly inaccessible again.
		$groupsByUser->setValue(null, $rawGroupsByUser);
		$groupsByUser->setAccessible(false);

		/**
		 * We are not done. Caching user groups is only one aspect of Joomla access management. Joomla also caches the
		 * identities, i.e. the user group assignment per user, in a different cache. We need to reset it to for our
		 * user.
		 *
		 * Do note that we CAN NOT use clearStatics since that also clears the user group assignment which we assigned
		 * dynamically. Therefore calling it would destroy our work so far.
		 */
		$refProperty = $reflectedAccess->getProperty('identities');
		$refProperty->setAccessible(true);
		$identities = $refProperty->getValue();

		$keys = [$user->id, 0];

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

			unset($identities[$key]);
		}

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

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

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

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

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

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

namespace FOF30\Utils;

defined('_JEXEC') || die;

/**
 * IP address helper
 *
 * Makes sure that we get the real IP of the user
 */
class Ip
{
	/**
	 * The IP address of the current visitor
	 *
	 * @var   string
	 */
	protected static $ip = null;

	/**
	 * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers?
	 *
	 * @var    bool
	 */
	protected static $allowIpOverrides = true;

	/**
	 * See self::detectAndCleanIP and setUseFirstIpInChain
	 *
	 * If this is enabled (default) self::detectAndCleanIP will return the FIRST IP in case there is an IP chain coming
	 * for example from an X-Forwarded-For HTTP header. When set to false it will simulate the old behavior in FOF up to
	 * and including 3.1.1 which returned the LAST IP in the list.
	 *
	 * @var   bool
	 */
	protected static $useFirstIpInChain = true;

	/**
	 * List of headers we should check to know if the user is behind a proxy. PLEASE NOTE: ORDER MATTERS!
	 *
	 * @var array
	 */
	protected static $proxyHeaders = [
		'HTTP_CF_CONNECTING_IP',        // CloudFlare
		'HTTP_X_FORWARDED_FOR',         // Standard for transparent proxy (e.g. NginX)
		'HTTP_X_SUCURI_CLIENTIP',       // Sucuri firewall uses its own header
	];

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

	/**
	 * Getter for the list of proxy headers we can check
	 *
	 * @return array
	 */
	public static function getProxyHeaders()
	{
		return static::$proxyHeaders;
	}

	/**
	 * Get the current visitor's IP address
	 *
	 * @return string
	 */
	public static function getIp()
	{
		if (is_null(static::$ip))
		{
			$ip = self::detectAndCleanIP();

			if (!empty($ip) && ($ip != '0.0.0.0') && function_exists('inet_pton') && function_exists('inet_ntop'))
			{
				$myIP = @inet_pton($ip);

				if ($myIP !== false)
				{
					$ip = inet_ntop($myIP);
				}
			}

			static::setIp($ip);
		}

		return static::$ip;
	}

	/**
	 * Set the IP address of the current visitor
	 *
	 * @param   string  $ip
	 *
	 * @return  void
	 */
	public static function setIp($ip)
	{
		static::$ip = $ip;
	}

	/**
	 * Checks if an IP is contained in a list of IPs or IP expressions
	 *
	 * @param   string        $ip       The IPv4/IPv6 address to check
	 * @param   array|string  $ipTable  An IP expression (or a comma-separated or array list of IP expressions) to
	 *                                  check against
	 *
	 * @return  null|boolean  True if it's in the list
	 */
	public static function IPinList($ip, $ipTable = '')
	{
		// No point proceeding with an empty IP list
		if (empty($ipTable))
		{
			return false;
		}

		// If the IP list is not an array, convert it to an array
		if (!is_array($ipTable))
		{
			if (strpos($ipTable, ',') !== false)
			{
				$ipTable = explode(',', $ipTable);
				$ipTable = array_map(function ($x) {
					return trim($x);
				}, $ipTable);
			}
			else
			{
				$ipTable = trim($ipTable);
				$ipTable = [$ipTable];
			}
		}

		// If no IP address is found, return false
		if ($ip == '0.0.0.0')
		{
			return false;
		}

		// If no IP is given, return false
		if (empty($ip))
		{
			return false;
		}

		// Sanity check
		if (!function_exists('inet_pton'))
		{
			return false;
		}

		// Get the IP's in_adds representation
		$myIP = @inet_pton($ip);

		// If the IP is in an unrecognisable format, quite
		if ($myIP === false)
		{
			return false;
		}

		$ipv6 = self::isIPv6($ip);

		foreach ($ipTable as $ipExpression)
		{
			$ipExpression = trim($ipExpression);

			// Inclusive IP range, i.e. 123.123.123.123-124.125.126.127
			if (strstr($ipExpression, '-'))
			{
				[$from, $to] = explode('-', $ipExpression, 2);

				if ($ipv6 && (!self::isIPv6($from) || !self::isIPv6($to)))
				{
					// Do not apply IPv4 filtering on an IPv6 address
					continue;
				}
				elseif (!$ipv6 && (self::isIPv6($from) || self::isIPv6($to)))
				{
					// Do not apply IPv6 filtering on an IPv4 address
					continue;
				}

				$from = @inet_pton(trim($from));
				$to   = @inet_pton(trim($to));

				// Sanity check
				if (($from === false) || ($to === false))
				{
					continue;
				}

				// Swap from/to if they're in the wrong order
				if ($from > $to)
				{
					[$from, $to] = [$to, $from];
				}

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

				[$net, $maskbits] = explode('/', $ipExpression, 2);
				if ($ipv6 && !self::isIPv6($net))
				{
					// Do not apply IPv4 filtering on an IPv6 address
					continue;
				}
				elseif (!$ipv6 && self::isIPv6($net))
				{
					// Do not apply IPv6 filtering on an IPv4 address
					continue;
				}
				elseif ($ipv6 && strstr($maskbits, ':'))
				{
					// Perform an IPv6 CIDR check
					if (self::checkIPv6CIDR($myIP, $ipExpression))
					{
						return true;
					}

					// If we didn't match it proceed to the next expression
					continue;
				}
				elseif (!$ipv6 && strstr($maskbits, '.'))
				{
					// Convert IPv4 netmask to CIDR
					$long     = ip2long($maskbits);
					$base     = ip2long('255.255.255.255');
					$maskbits = 32 - log(($long ^ $base) + 1, 2);
				}

				// Convert network IP to in_addr representation
				$net = @inet_pton($net);

				// Sanity check
				if ($net === false)
				{
					continue;
				}

				// Get the network's binary representation
				$binarynet            = self::inet_to_bits($net);
				$expectedNumberOfBits = $ipv6 ? 128 : 24;
				$binarynet            = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);

				// Check the corresponding bits of the IP and the network
				$ip_net_bits = substr($binaryip, 0, $maskbits);
				$net_bits    = substr($binarynet, 0, $maskbits);

				if ($ip_net_bits == $net_bits)
				{
					return true;
				}
			}
			else
			{
				// IPv6: Only single IPs are supported
				if ($ipv6)
				{
					$ipExpression = trim($ipExpression);

					if (!self::isIPv6($ipExpression))
					{
						continue;
					}

					$ipCheck = @inet_pton($ipExpression);
					if ($ipCheck === false)
					{
						continue;
					}

					if ($ipCheck == $myIP)
					{
						return true;
					}
				}
				else
				{
					// Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123]
					$dots = 0;
					if (substr($ipExpression, -1) == '.')
					{
						// Partial IP address. Convert to CIDR and re-match
						foreach (count_chars($ipExpression, 1) as $i => $val)
						{
							if ($i == 46)
							{
								$dots = $val;
							}
						}

						$netmask = '255.255.255.255';

						switch ($dots)
						{
							case 1:
								$netmask      = '255.0.0.0';
								$ipExpression .= '0.0.0';
								break;

							case 2:
								$netmask      = '255.255.0.0';
								$ipExpression .= '0.0';
								break;

							case 3:
								$netmask      = '255.255.255.0';
								$ipExpression .= '0';
								break;

							default:
								$dots = 0;
						}

						if ($dots)
						{
							$binaryip = self::inet_to_bits($myIP);

							// Convert netmask to CIDR
							$long     = ip2long($netmask);
							$base     = ip2long('255.255.255.255');
							$maskbits = 32 - log(($long ^ $base) + 1, 2);

							$net = @inet_pton($ipExpression);

							// Sanity check
							if ($net === false)
							{
								continue;
							}

							// Get the network's binary representation
							$binarynet            = self::inet_to_bits($net);
							$expectedNumberOfBits = $ipv6 ? 128 : 24;
							$binarynet            = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);

							// Check the corresponding bits of the IP and the network
							$ip_net_bits = substr($binaryip, 0, $maskbits);
							$net_bits    = substr($binarynet, 0, $maskbits);

							if ($ip_net_bits == $net_bits)
							{
								return true;
							}
						}
					}
					if (!$dots)
					{
						$ip = @inet_pton(trim($ipExpression));
						if ($ip == $myIP)
						{
							return true;
						}
					}
				}
			}
		}

		return false;
	}

	/**
	 * Works around the REMOTE_ADDR not containing the user's IP
	 */
	public static function workaroundIPIssues()
	{
		$ip = self::getIp();

		if (array_key_exists('REMOTE_ADDR', $_SERVER) && ($_SERVER['REMOTE_ADDR'] == $ip))
		{
			return;
		}

		if (array_key_exists('REMOTE_ADDR', $_SERVER))
		{
			$_SERVER['FOF_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
		}
		elseif (function_exists('getenv'))
		{
			if (getenv('REMOTE_ADDR'))
			{
				$_SERVER['FOF_REMOTE_ADDR'] = getenv('REMOTE_ADDR');
			}
		}

		$_SERVER['REMOTE_ADDR'] = $ip;
	}

	/**
	 * Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header?
	 *
	 * @param   bool  $newState  True to allow the override
	 *
	 * @return  void
	 */
	public static function setAllowIpOverrides($newState)
	{
		self::$allowIpOverrides = $newState ? true : false;
	}

	/**
	 * Is it an IPv6 IP address?
	 *
	 * @param   string  $ip  An IPv4 or IPv6 address
	 *
	 * @return  boolean  True if it's IPv6
	 */
	protected static function isIPv6($ip)
	{
		if (strstr($ip, ':'))
		{
			return true;
		}

		return false;
	}

	/**
	 * Gets the visitor's IP address. Automatically handles reverse proxies
	 * reporting the IPs of intermediate devices, like load balancers. Examples:
	 * https://www.akeeba.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html
	 * http://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips
	 * The solution used is assuming that the first IP address is the external one (unless $useFirstIpInChain is set to
	 * false)
	 *
	 * @return  string
	 */
	protected static function detectAndCleanIP()
	{
		$ip = static::detectIP();

		if ((strstr($ip, ',') !== false) || (strstr($ip, ' ') !== false))
		{
			$ip  = str_replace(' ', ',', $ip);
			$ip  = str_replace(',,', ',', $ip);
			$ips = explode(',', $ip);
			$ip  = '';

			// Loop until we're running out of parts or we have a hit
			while ($ips)
			{
				$ip = array_shift($ips);
				$ip = trim($ip);

				if (self::$useFirstIpInChain)
				{
					return self::cleanIP($ip);
				}
			}
		}

		return self::cleanIP($ip);
	}

	protected static function cleanIP($ip)
	{
		$ip = trim($ip);
		$ip = strtoupper($ip);

		/**
		 * Work around IPv4-mapped addresses.
		 *
		 * IPv4 addresses may be embedded in an IPv6 address. This is always 80 zeroes, 16 ones and the IPv4 address.
		 * In all possible IPv6 notations this is:
		 * 0:0:0:0:0:FFFF:192.168.1.1
		 * ::FFFF:192.168.1.1
		 * ::FFFF:C0A8:0101
		 *
		 * @see http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding-2.htm
		 */
		if ((strpos($ip, '::FFFF:') === 0) || (strpos($ip, '0:0:0:0:0:FFFF:') === 0))
		{
			// Fast path: the embedded IPv4 is in decimal notation.
			if (strstr($ip, '.') !== false)
			{
				return substr($ip, strrpos($ip, ':') + 1);
			}

			// Get the embedded IPv4 (in hex notation)
			$ip = substr($ip, strpos($ip, ':FFFF:') + 6);
			// Convert each 16-bit WORD to decimal
			[$word1, $word2] = explode(':', $ip);
			$word1  = hexdec($word1);
			$word2  = hexdec($word2);
			$longIp = $word1 * 65536 + $word2;

			return long2ip($longIp);
		}

		return $ip;
	}

	/**
	 * Gets the visitor's IP address
	 *
	 * @return  string
	 */
	protected static function detectIP()
	{
		// Normally the $_SERVER superglobal is set
		if (isset($_SERVER))
		{
			// Do we have IP overrides enabled?
			if (static::$allowIpOverrides)
			{
				// If so, check for every proxy header
				foreach (static::$proxyHeaders as $header)
				{
					if (array_key_exists($header, $_SERVER))
					{
						return $_SERVER[$header];
					}
				}
			}

			// CLI applications
			if (!array_key_exists('REMOTE_ADDR', $_SERVER))
			{
				return '';
			}

			// Normal, non-proxied server or server behind a transparent proxy
			return $_SERVER['REMOTE_ADDR'];
		}

		// This part is executed on PHP running as CGI, or on SAPIs which do
		// not set the $_SERVER superglobal
		// If getenv() is disabled, you're screwed
		if (!function_exists('getenv'))
		{
			return '';
		}

		// Do we have IP overrides enabled?
		if (static::$allowIpOverrides)
		{
			// If so, check for every proxy header
			foreach (static::$proxyHeaders as $header)
			{
				if (getenv($header))
				{
					return getenv($header);
				}
			}
		}

		// Normal, non-proxied server or server behind a transparent proxy
		if (getenv('REMOTE_ADDR'))
		{
			return getenv('REMOTE_ADDR');
		}

		// Catch-all case for broken servers and CLI applications
		return '';
	}

	/**
	 * Converts inet_pton output to bits string
	 *
	 * @param   string  $inet  The in_addr representation of an IPv4 or IPv6 address
	 *
	 * @return  string
	 */
	protected static function inet_to_bits($inet)
	{
		if (strlen($inet) == 4)
		{
			$unpacked = unpack('C4', $inet);
		}
		else
		{
			$unpacked = unpack('C16', $inet);
		}

		$binaryip = '';

		foreach ($unpacked as $byte)
		{
			$binaryip .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
		}

		return $binaryip;
	}

	/**
	 * Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet
	 *
	 * @param   string  $ip       The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A
	 * @param   string  $cidrnet  The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64
	 *
	 * @return  bool
	 */
	protected static function checkIPv6CIDR($ip, $cidrnet)
	{
		$ip       = inet_pton($ip);
		$binaryip = self::inet_to_bits($ip);

		[$net, $maskbits] = explode('/', $cidrnet);
		$net       = inet_pton($net);
		$binarynet = self::inet_to_bits($net);

		$ip_net_bits = substr($binaryip, 0, $maskbits);
		$net_bits    = substr($binarynet, 0, $maskbits);

		return $ip_net_bits === $net_bits;
	}
}
Utils/SelectOptions.php000060400000024746152455514240011170 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use InvalidArgumentException;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use stdClass;

/**
 * Returns arrays of JHtml select options for Joomla-specific information such as access levels.
 */
class SelectOptions
{
	private static $cache = [];

	/**
	 * Magic method to handle static calls
	 *
	 * @param   string  $name       The name of the static method being called
	 * @param   string  $arguments  Ignored.
	 *
	 * @return mixed
	 * @since   3.3.0
	 *
	 */
	public static function __callStatic($name, $arguments)
	{
		return self::getOptions($name, $arguments);
	}

	/**
	 * Get a list of Joomla options of the type you specify. Supported types
	 * - access         View access levels
	 * - usergroups     User groups
	 * - cachehandlers  Cache handlers
	 * - components     Installed components accessible by the current user
	 * - languages      Site or administrator languages
	 * - published      Published status
	 *
	 * Global params:
	 * - cache  Should I returned cached data? Default: true.
	 *
	 * See the private static methods of this class for more information on params.
	 *
	 * @param   string  $type    The options type to get
	 * @param   array   $params  Optional arguments, if they are supported by the options type.
	 *
	 * @return  stdClass[]
	 * @since   3.3.0
	 *
	 */
	public static function getOptions($type, array $params = [])
	{
		if ((substr($type, 0, 1) == '_') || !method_exists(__CLASS__, $type))
		{
			throw new InvalidArgumentException(__CLASS__ . "does not support option type '$type'.");
		}

		$useCache = true;

		if (isset($params['cache']))
		{
			$useCache = isset($params['cache']);
			unset($params['cache']);
		}

		$cacheKey = sha1($type . '--' . print_r($params, true));
		$fetchNew = !$useCache || ($useCache && !isset(self::$cache[$cacheKey]));

		if ($fetchNew)
		{
			$ret = forward_static_call_array([__CLASS__, $type], [$params]);
		}

		if (!$useCache)
		{
			return $ret;
		}

		if ($fetchNew)
		{
			self::$cache[$cacheKey] = $ret;
		}

		return self::$cache[$cacheKey];
	}

	/**
	 * Joomla! Access Levels (previously: view access levels)
	 *
	 * Available params:
	 * - allLevels: Show an option for all levels (default: false)
	 *
	 * @param   array  $params  Parameters
	 *
	 * @return  stdClass[]
	 *
	 * @since   3.3.0
	 *
	 * @see     \JHtmlAccess::level()
	 */
	private static function access(array $params = [])
	{
		$db    = Factory::getDbo();
		$query = $db->getQuery(true)
			->select($db->quoteName('a.id', 'value') . ', ' . $db->quoteName('a.title', 'text'))
			->from($db->quoteName('#__viewlevels', 'a'))
			->group($db->quoteName(['a.id', 'a.title', 'a.ordering']))
			->order($db->quoteName('a.ordering') . ' ASC')
			->order($db->quoteName('title') . ' ASC');

		// Get the options.
		$db->setQuery($query);
		$options = $db->loadObjectList();

		if (isset($params['allLevels']) && $params['allLevels'])
		{
			array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_LEVELS')));
		}

		return $options;
	}

	/**
	 * Joomla! User Groups
	 *
	 * Available params:
	 * - allGroups: Show an option for all groups (default: false)
	 *
	 * @param   array  $params  Parameters
	 *
	 * @return  stdClass[]
	 *
	 * @since   3.3.0
	 *
	 * @see     \JHtmlAccess::usergroup()
	 */
	private static function usergroups(array $params = [])
	{
		$options = array_values(UserGroupsHelper::getInstance()->getAll());

		for ($i = 0, $n = count($options); $i < $n; $i++)
		{
			$options[$i]->value = $options[$i]->id;
			$options[$i]->text  = str_repeat('- ', $options[$i]->level) . $options[$i]->title;
		}

		// If all usergroups is allowed, push it into the array.
		if (isset($params['allGroups']) && $params['allGroups'])
		{
			array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_GROUPS')));
		}

		return $options;
	}

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

		// Convert to name => name array.
		foreach (Cache::getStores() as $store)
		{
			$options[] = HTMLHelper::_('select.option', $store, Text::_('JLIB_FORM_VALUE_CACHE_' . $store), 'value', 'text');
		}

		return $options;
	}

	/**
	 * Get a list of all installed components and also translates them.
	 *
	 * Available params:
	 * - client_ids  Array of Joomla application client IDs
	 *
	 * @param   array  $params
	 *
	 * @return    stdClass[]
	 * @since   3.3.0
	 *
	 */
	private static function components(array $params)
	{
		$db = Factory::getDbo();

		// Check for client_ids override
		$client_ids = $params['client_ids'] ?? [0, 1];

		if (is_string($client_ids))
		{
			$client_ids = explode(',', $client_ids);
		}

		// Calculate client_ids where clause
		$client_ids = array_map(function ($client_id) use ($db) {
			return $db->q((int) trim($client_id));
		}, $client_ids);

		$query      = $db->getQuery(true)
			->select(
				[
					$db->qn('name'),
					$db->qn('element'),
					$db->qn('client_id'),
					$db->qn('manifest_cache'),
				]
			)
			->from($db->qn('#__extensions'))
			->where($db->qn('type') . ' = ' . $db->q('component'))
			->where($db->qn('client_id') . ' IN (' . implode(',', $client_ids) . ')');
		$components = $db->setQuery($query)->loadObjectList('element');

		// Convert to array of objects, so we can use sortObjects()
		// Also translate component names with JText::_()
		$aComponents = [];
		$user        = Factory::getUser();

		foreach ($components as $component)
		{
			// Don't show components in the list where the user doesn't have access for
			// TODO: perhaps add an option for this
			if (!$user->authorise('core.manage', $component->element))
			{
				continue;
			}

			$aComponents[$component->element] = (object) [
				'value' => $component->element,
				'text'  => self::_translateComponentName($component),
			];
		}

		// Reorder the components array, because the alphabetical
		// ordering changed due to the JText::_() translation
		uasort(
			$aComponents,
			function ($a, $b) {
				return strcasecmp($a->text, $b->text);
			}
		);

		return $aComponents;
	}

	/**
	 * Method to get the field options.
	 *
	 * Available params:
	 * - client  'site' (default) or 'administrator'
	 * - none    Text to show for "all languages" option, use empty string to remove it
	 *
	 * @return  array  Languages for the specified client
	 * @since   3.3.0
	 *
	 */
	private static function languages($params)
	{
		$db = Factory::getDbo();

		$client = $params['client'] ?? 'site';

		if (!in_array($client, ['site', 'administrator']))
		{
			$client = 'site';
		}

		// Make sure the languages are sorted base on locale instead of random sorting
		$options = LanguageHelper::createLanguageList(null, constant('JPATH_' . strtoupper($client)), true, true);

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

		$none = $params['none'] ?? '*';

		if (!empty($none))
		{
			array_unshift($options, HTMLHelper::_('select.option', '*', Text::_($none)));
		}

		return $options;
	}

	/**
	 * Options for a Published field
	 *
	 * Params:
	 * - none           Placeholder for no selection (empty key). Default: null.
	 * - published      Show "Published"? Default: true
	 * - unpublished    Show "Unpublished"? Default: true
	 * - archived       Show "Archived"? Default: false
	 * - trash          Show "Trashed"? Default: false
	 * - all            Show "All" option? This is different than none, the key is '*'. Default: false
	 *
	 * @param $params
	 *
	 * @return array
	 */
	private static function published(array $params = [])
	{
		$config = array_merge([
			'none'        => '',
			'published'   => true,
			'unpublished' => true,
			'archived'    => false,
			'trash'       => false,
			'all'         => false,
		], $params);

		$options = [];

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

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

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

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

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

		if ($config['all'])
		{
			$options[] = HTMLHelper::_('select.option', '*', Text::_('JALL'));
		}

		return $options;
	}

	/**
	 * Options for a Published field
	 *
	 * Params:
	 * - none           Placeholder for no selection (empty key). Default: null.
	 *
	 * @param $params
	 *
	 * @return array
	 */
	private static function boolean(array $params = [])
	{
		$config = array_merge([
			'none' => '',
		], $params);

		$options = [];

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

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

		return $options;
	}


	/**
	 * Translate a component name
	 *
	 * @param   stdClass  $item  The component object
	 *
	 * @return  string  $text  The translated name of the extension
	 *
	 * @since   3.3.0
	 *
	 * @see     administrator/com_installer/models/extension.php
	 */
	private static function _translateComponentName($item)
	{
		// Map the manifest cache to $item. This is needed to get the name from the
		// manifest_cache and NOT from the name column, else some JText::_() translations fails.
		$mData = json_decode($item->manifest_cache);

		if ($mData)
		{
			foreach ($mData as $key => $value)
			{
				if ($key == 'type')
				{
					// Ignore the type field
					continue;
				}

				$item->$key = $value;
			}
		}

		$lang   = Factory::getLanguage();
		$source = JPATH_ADMINISTRATOR . '/components/' . $item->element;
		$lang->load("$item->element.sys", JPATH_ADMINISTRATOR, null, false, false)
		|| $lang->load("$item->element.sys", $source, null, false, false)
		|| $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, $lang->getDefault(), false, false)
		|| $lang->load("$item->element.sys", $source, $lang->getDefault(), false, false);

		return Text::_($item->name);
	}
}
Utils/InstallScript.php000060400000001360152455514240011153 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use FOF30\Utils\InstallScript\Component;

// Make sure the new class can be loaded
if (!class_exists('FOF30\\Utils\\InstallScript\\Component', true))
{
	require_once __DIR__ . '/InstallScript/Component.php';
}

/**
 * A helper class which you can use to create component installation scripts.
 *
 * This is the old location of the installation script class, maintained for backwards compatibility with FOF 3.0. Please
 * use the new class FOF30\Utils\InstallScript\Component instead.
 */
class InstallScript extends Component
{

}
Utils/ArrayHelper.php000060400000031320152455514240010575 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use ArrayAccess;
use InvalidArgumentException;
use Traversable;

/**
 * ArrayHelper is an array utility class for doing all sorts of odds and ends with arrays.
 *
 * Copied from Joomla Framework to avoid class name issues between Joomla! versions 3 and 4. sortObjects is not included
 * because it needs the UTF-8 package. If you need to use that then you should be using the Joomla! Framework's helper
 * anyway.
 */
final class ArrayHelper
{
	/**
	 * Private constructor to prevent instantiation of this class
	 *
	 * @since   1.0
	 */
	private function __construct()
	{
	}

	/**
	 * Function to convert array to integer values
	 *
	 * @param   array  $array    The source array to convert
	 * @param   mixed  $default  A default value (int|array) to assign if $array is not an array
	 *
	 * @return  array
	 *
	 * @since   1.0
	 */
	public static function toInteger($array, $default = null)
	{
		if (is_array($array))
		{
			return array_map('intval', $array);
		}

		if ($default === null)
		{
			return [];
		}

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

		return [(int) $default];
	}

	/**
	 * Utility function to map an array to a stdClass object.
	 *
	 * @param   array    $array      The array to map.
	 * @param   string   $class      Name of the class to create
	 * @param   boolean  $recursive  Convert also any array inside the main array
	 *
	 * @return  object
	 *
	 * @since   1.0
	 */
	public static function toObject(array $array, $class = 'stdClass', $recursive = true)
	{
		$obj = new $class;

		foreach ($array as $k => $v)
		{
			if ($recursive && is_array($v))
			{
				$obj->$k = static::toObject($v, $class);
			}
			else
			{
				$obj->$k = $v;
			}
		}

		return $obj;
	}

	/**
	 * Utility function to map an array to a string.
	 *
	 * @param   array    $array         The array to map.
	 * @param   string   $inner_glue    The glue (optional, defaults to '=') between the key and the value.
	 * @param   string   $outer_glue    The glue (optional, defaults to ' ') between array elements.
	 * @param   boolean  $keepOuterKey  True if final key should be kept.
	 *
	 * @return  string
	 *
	 * @since   1.0
	 */
	public static function toString(array $array, $inner_glue = '=', $outer_glue = ' ', $keepOuterKey = false)
	{
		$output = [];

		foreach ($array as $key => $item)
		{
			if (is_array($item))
			{
				if ($keepOuterKey)
				{
					$output[] = $key;
				}

				// This is value is an array, go and do it again!
				$output[] = static::toString($item, $inner_glue, $outer_glue, $keepOuterKey);
			}
			else
			{
				$output[] = $key . $inner_glue . '"' . $item . '"';
			}
		}

		return implode($outer_glue, $output);
	}

	/**
	 * Utility function to map an object to an array
	 *
	 * @param   object   $p_obj    The source object
	 * @param   boolean  $recurse  True to recurse through multi-level objects
	 * @param   string   $regex    An optional regular expression to match on field names
	 *
	 * @return  array
	 *
	 * @since   1.0
	 */
	public static function fromObject($p_obj, $recurse = true, $regex = null)
	{
		if (is_object($p_obj) || is_array($p_obj))
		{
			return self::arrayFromObject($p_obj, $recurse, $regex);
		}

		return [];
	}

	/**
	 * Extracts a column from an array of arrays or objects
	 *
	 * @param   array   $array     The source array
	 * @param   string  $valueCol  The index of the column or name of object property to be used as value
	 *                             It may also be NULL to return complete arrays or objects (this is
	 *                             useful together with <var>$keyCol</var> to reindex the array).
	 * @param   string  $keyCol    The index of the column or name of object property to be used as key
	 *
	 * @return  array  Column of values from the source array
	 *
	 * @since   1.0
	 * @see     http://php.net/manual/en/language.types.array.php
	 * @see     http://php.net/manual/en/function.array-column.php
	 */
	public static function getColumn(array $array, $valueCol, $keyCol = null)
	{
		$result = [];

		foreach ($array as $item)
		{
			// Convert object to array
			$subject = is_object($item) ? static::fromObject($item) : $item;

			/*
			 * We process arrays (and objects already converted to array)
			 * Only if the value column (if required) exists in this item
			 */
			if (is_array($subject) && (!isset($valueCol) || isset($subject[$valueCol])))
			{
				// Use whole $item if valueCol is null, else use the value column.
				$value = isset($valueCol) ? $subject[$valueCol] : $item;

				// Array keys can only be integer or string. Casting will occur as per the PHP Manual.
				if (isset($keyCol) && isset($subject[$keyCol]) && is_scalar($subject[$keyCol]))
				{
					$key          = $subject[$keyCol];
					$result[$key] = $value;
				}
				else
				{
					$result[] = $value;
				}
			}
		}

		return $result;
	}

	/**
	 * Utility function to return a value from a named array or a specified default
	 *
	 * @param   array|ArrayAccess  $array    A named array or object that implements ArrayAccess
	 * @param   string             $name     The key to search for
	 * @param   mixed              $default  The default value to give if no key found
	 * @param   string             $type     Return type for the variable (INT, FLOAT, STRING, WORD, BOOLEAN, ARRAY)
	 *
	 * @return  mixed
	 *
	 * @throws  InvalidArgumentException
	 * @since   1.0
	 */
	public static function getValue($array, $name, $default = null, $type = '')
	{
		if (!is_array($array) && !($array instanceof ArrayAccess))
		{
			throw new InvalidArgumentException('The object must be an array or an object that implements ArrayAccess');
		}

		$result = null;

		if (isset($array[$name]))
		{
			$result = $array[$name];
		}

		// Handle the default case
		if (is_null($result))
		{
			$result = $default;
		}

		// Handle the type constraint
		switch (strtoupper($type))
		{
			case 'INT':
			case 'INTEGER':
				// Only use the first integer value
				@preg_match('/-?[0-9]+/', $result, $matches);
				$result = @(int) $matches[0];
				break;

			case 'FLOAT':
			case 'DOUBLE':
				// Only use the first floating point value
				@preg_match('/-?[0-9]+(\.[0-9]+)?/', $result, $matches);
				$result = @(float) $matches[0];
				break;

			case 'BOOL':
			case 'BOOLEAN':
				$result = (bool) $result;
				break;

			case 'ARRAY':
				if (!is_array($result))
				{
					$result = [$result];
				}
				break;

			case 'STRING':
				$result = (string) $result;
				break;

			case 'WORD':
				$result = (string) preg_replace('#\W#', '', $result);
				break;

			case 'NONE':
			default:
				// No casting necessary
				break;
		}

		return $result;
	}

	/**
	 * Takes an associative array of arrays and inverts the array keys to values using the array values as keys.
	 *
	 * Example:
	 * $input = array(
	 *     'New' => array('1000', '1500', '1750'),
	 *     'Used' => array('3000', '4000', '5000', '6000')
	 * );
	 * $output = ArrayHelper::invert($input);
	 *
	 * Output would be equal to:
	 * $output = array(
	 *     '1000' => 'New',
	 *     '1500' => 'New',
	 *     '1750' => 'New',
	 *     '3000' => 'Used',
	 *     '4000' => 'Used',
	 *     '5000' => 'Used',
	 *     '6000' => 'Used'
	 * );
	 *
	 * @param   array  $array  The source array.
	 *
	 * @return  array
	 *
	 * @since   1.0
	 */
	public static function invert(array $array)
	{
		$return = [];

		foreach ($array as $base => $values)
		{
			if (!is_array($values))
			{
				continue;
			}

			foreach ($values as $key)
			{
				// If the key isn't scalar then ignore it.
				if (is_scalar($key))
				{
					$return[$key] = $base;
				}
			}
		}

		return $return;
	}

	/**
	 * Method to determine if an array is an associative array.
	 *
	 * @param   array  $array  An array to test.
	 *
	 * @return  boolean
	 *
	 * @since   1.0
	 */
	public static function isAssociative($array)
	{
		if (is_array($array))
		{
			foreach (array_keys($array) as $k => $v)
			{
				if ($k !== $v)
				{
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Pivots an array to create a reverse lookup of an array of scalars, arrays or objects.
	 *
	 * @param   array   $source  The source array.
	 * @param   string  $key     Where the elements of the source array are objects or arrays, the key to pivot on.
	 *
	 * @return  array  An array of arrays pivoted either on the value of the keys, or an individual key of an object or
	 *                 array.
	 *
	 * @since   1.0
	 */
	public static function pivot(array $source, $key = null)
	{
		$result  = [];
		$counter = [];

		foreach ($source as $index => $value)
		{
			// Determine the name of the pivot key, and its value.
			if (is_array($value))
			{
				// If the key does not exist, ignore it.
				if (!isset($value[$key]))
				{
					continue;
				}

				$resultKey   = $value[$key];
				$resultValue = $source[$index];
			}
			elseif (is_object($value))
			{
				// If the key does not exist, ignore it.
				if (!isset($value->$key))
				{
					continue;
				}

				$resultKey   = $value->$key;
				$resultValue = $source[$index];
			}
			else
			{
				// Just a scalar value.
				$resultKey   = $value;
				$resultValue = $index;
			}

			// The counter tracks how many times a key has been used.
			if (empty($counter[$resultKey]))
			{
				// The first time around we just assign the value to the key.
				$result[$resultKey]  = $resultValue;
				$counter[$resultKey] = 1;
			}
			elseif ($counter[$resultKey] == 1)
			{
				// If there is a second time, we convert the value into an array.
				$result[$resultKey] = [
					$result[$resultKey],
					$resultValue,
				];
				$counter[$resultKey]++;
			}
			else
			{
				// After the second time, no need to track any more. Just append to the existing array.
				$result[$resultKey][] = $resultValue;
			}
		}

		unset($counter);

		return $result;
	}

	/**
	 * Multidimensional array safe unique test
	 *
	 * @param   array  $array  The array to make unique.
	 *
	 * @return  array
	 *
	 * @see     http://php.net/manual/en/function.array-unique.php
	 * @since   1.0
	 */
	public static function arrayUnique(array $array)
	{
		$array = array_map('serialize', $array);
		$array = array_unique($array);
		$array = array_map('unserialize', $array);

		return $array;
	}

	/**
	 * An improved array_search that allows for partial matching of strings values in associative arrays.
	 *
	 * @param   string   $needle         The text to search for within the array.
	 * @param   array    $haystack       Associative array to search in to find $needle.
	 * @param   boolean  $caseSensitive  True to search case sensitive, false otherwise.
	 *
	 * @return  mixed    Returns the matching array $key if found, otherwise false.
	 *
	 * @since   1.0
	 */
	public static function arraySearch($needle, array $haystack, $caseSensitive = true)
	{
		foreach ($haystack as $key => $value)
		{
			$searchFunc = ($caseSensitive) ? 'strpos' : 'stripos';

			if ($searchFunc($value, $needle) === 0)
			{
				return $key;
			}
		}

		return false;
	}

	/**
	 * Method to recursively convert data to a one dimension array.
	 *
	 * @param   array|object  $array      The array or object to convert.
	 * @param   string        $separator  The key separator.
	 * @param   string        $prefix     Last level key prefix.
	 *
	 * @return  array
	 *
	 * @since   1.3.0
	 */
	public static function flatten($array, $separator = '.', $prefix = '')
	{
		if ($array instanceof Traversable)
		{
			$array = iterator_to_array($array);
		}
		elseif (is_object($array))
		{
			$array = get_object_vars($array);
		}

		foreach ($array as $k => $v)
		{
			$key = $prefix ? $prefix . $separator . $k : $k;

			if (is_object($v) || is_array($v))
			{
				$array = array_merge($array, static::flatten($v, $separator, $key));
			}
			else
			{
				$array[$key] = $v;
			}
		}

		return $array;
	}

	/**
	 * Utility function to map an object or array to an array
	 *
	 * @param   mixed    $item     The source object or array
	 * @param   boolean  $recurse  True to recurse through multi-level objects
	 * @param   string   $regex    An optional regular expression to match on field names
	 *
	 * @return  array
	 *
	 * @since   1.0
	 */
	private static function arrayFromObject($item, $recurse, $regex)
	{
		if (is_object($item))
		{
			$result = [];

			foreach (get_object_vars($item) as $k => $v)
			{
				if (!$regex || preg_match($regex, $k))
				{
					if ($recurse)
					{
						$result[$k] = self::arrayFromObject($v, $recurse, $regex);
					}
					else
					{
						$result[$k] = $v;
					}
				}
			}

			return $result;
		}

		if (is_array($item))
		{
			$result = [];

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

			return $result;
		}

		return $item;
	}
}
Utils/Buffer.php000060400000014700152455514240007573 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

/**
 * Registers a fof:// stream wrapper
 */
class Buffer
{
	/**
	 * Buffer hash
	 *
	 * @var    array
	 */
	public static $buffers = [];
	public static $canRegisterWrapper = null;
	/**
	 * Stream position
	 *
	 * @var    integer
	 */
	public $position = 0;
	/**
	 * Buffer name
	 *
	 * @var    string
	 */
	public $name = null;

	/**
	 * Should I register the fof:// stream wrapper
	 *
	 * @return  bool  True if the stream wrapper can be registered
	 */
	public static function canRegisterWrapper()
	{
		if (is_null(static::$canRegisterWrapper))
		{
			static::$canRegisterWrapper = false;

			// Maybe the host has disabled registering stream wrappers altogether?
			if (!function_exists('stream_wrapper_register'))
			{
				return false;
			}

			// Check for Suhosin
			if (function_exists('extension_loaded'))
			{
				$hasSuhosin = extension_loaded('suhosin');
			}
			else
			{
				$hasSuhosin = -1; // Can't detect
			}

			if ($hasSuhosin !== true)
			{
				$hasSuhosin = defined('SUHOSIN_PATCH') ? true : -1;
			}

			if ($hasSuhosin === -1)
			{
				if (function_exists('ini_get'))
				{
					$hasSuhosin = false;

					$maxIdLength = ini_get('suhosin.session.max_id_length');

					if ($maxIdLength !== false)
					{
						$hasSuhosin = ini_get('suhosin.session.max_id_length') !== '';
					}
				}
			}

			// If we can't detect whether Suhosin is installed we won't proceed to prevent a White Screen of Death
			if ($hasSuhosin === -1)
			{
				return false;
			}

			// If Suhosin is installed but ini_get is not available we won't proceed to prevent a WSoD
			if ($hasSuhosin && !function_exists('ini_get'))
			{
				return false;
			}

			// If Suhosin is installed check if fof:// is whitelisted
			if ($hasSuhosin)
			{
				$whiteList = ini_get('suhosin.executor.include.whitelist');

				// Nothing in the whitelist? I can't go on, sorry.
				if (empty($whiteList))
				{
					return false;
				}

				$whiteList = explode(',', $whiteList);
				$whiteList = array_map(function ($x) {
					return trim($x);
				}, $whiteList);

				if (!in_array('fof://', $whiteList))
				{
					return false;
				}
			}

			static::$canRegisterWrapper = true;
		}

		return static::$canRegisterWrapper;
	}

	/**
	 * Function to open file or url
	 *
	 * @param   string   $path          The URL that was passed
	 * @param   string   $mode          Mode used to open the file @see fopen
	 * @param   integer  $options       Flags used by the API, may be STREAM_USE_PATH and
	 *                                  STREAM_REPORT_ERRORS
	 * @param   string  &$opened_path   Full path of the resource. Used with STREAM_USE_PATH option
	 *
	 * @return  boolean
	 *
	 * @see     streamWrapper::stream_open
	 */
	public function stream_open($path, $mode, $options, &$opened_path)
	{
		$url            = parse_url($path);
		$this->name     = $url['host'] . $url['path'];
		$this->position = 0;

		if (!isset(static::$buffers[$this->name]))
		{
			static::$buffers[$this->name] = null;
		}

		return true;
	}

	public function unlink($path)
	{
		$url  = parse_url($path);
		$name = $url['host'];

		if (isset(static::$buffers[$name]))
		{
			unset (static::$buffers[$name]);
		}
	}

	public function stream_stat()
	{
		return [
			'dev'     => 0,
			'ino'     => 0,
			'mode'    => 0644,
			'nlink'   => 0,
			'uid'     => 0,
			'gid'     => 0,
			'rdev'    => 0,
			'size'    => strlen(static::$buffers[$this->name]),
			'atime'   => 0,
			'mtime'   => 0,
			'ctime'   => 0,
			'blksize' => -1,
			'blocks'  => -1,
		];
	}

	/**
	 * Read stream
	 *
	 * @param   integer  $count  How many bytes of data from the current position should be returned.
	 *
	 * @return  mixed    The data from the stream up to the specified number of bytes (all data if
	 *                   the total number of bytes in the stream is less than $count. Null if
	 *                   the stream is empty.
	 *
	 * @see     streamWrapper::stream_read
	 * @since   11.1
	 */
	public function stream_read($count)
	{
		$ret            = substr(static::$buffers[$this->name], $this->position, $count);
		$this->position += strlen($ret);

		return $ret;
	}

	/**
	 * Write stream
	 *
	 * @param   string  $data  The data to write to the stream.
	 *
	 * @return  integer
	 *
	 * @see     streamWrapper::stream_write
	 * @since   11.1
	 */
	public function stream_write($data)
	{
		$left                         = substr(static::$buffers[$this->name], 0, $this->position);
		$right                        = substr(static::$buffers[$this->name], $this->position + strlen($data));
		static::$buffers[$this->name] = $left . $data . $right;
		$this->position               += strlen($data);

		return strlen($data);
	}

	/**
	 * Function to get the current position of the stream
	 *
	 * @return  integer
	 *
	 * @see     streamWrapper::stream_tell
	 * @since   11.1
	 */
	public function stream_tell()
	{
		return $this->position;
	}

	/**
	 * Function to test for end of file pointer
	 *
	 * @return  boolean  True if the pointer is at the end of the stream
	 *
	 * @see     streamWrapper::stream_eof
	 * @since   11.1
	 */
	public function stream_eof()
	{
		return $this->position >= strlen(static::$buffers[$this->name]);
	}

	/**
	 * The read write position updates in response to $offset and $whence
	 *
	 * @param   integer  $offset  The offset in bytes
	 * @param   integer  $whence  Position the offset is added to
	 *                            Options are SEEK_SET, SEEK_CUR, and SEEK_END
	 *
	 * @return  boolean  True if updated
	 *
	 * @see     streamWrapper::stream_seek
	 * @since   11.1
	 */
	public function stream_seek($offset, $whence)
	{
		switch ($whence)
		{
			case SEEK_SET:
				if ($offset < strlen(static::$buffers[$this->name]) && $offset >= 0)
				{
					$this->position = $offset;

					return true;
				}
				else
				{
					return false;
				}
				break;

			case SEEK_CUR:
				if ($offset >= 0)
				{
					$this->position += $offset;

					return true;
				}
				else
				{
					return false;
				}
				break;

			case SEEK_END:
				if (strlen(static::$buffers[$this->name]) + $offset >= 0)
				{
					$this->position = strlen(static::$buffers[$this->name]) + $offset;

					return true;
				}
				else
				{
					return false;
				}
				break;

			default:
				return false;
		}
	}
}

if (Buffer::canRegisterWrapper())
{
	stream_wrapper_register('fof', 'FOF30\\Utils\\Buffer');
}
Utils/ModelTypeHints.php000060400000014115152455514240011272 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Relation\Exception\RelationNotFound;

/**
 * Generate phpDoc type hints for the magic fields of your DataModels
 *
 * @package FOF30\Utils
 */
class ModelTypeHints
{
	/**
	 * The model for which to create type hints
	 *
	 * @var DataModel
	 */
	protected $model = null;

	/**
	 * Name of the class. If empty will be inferred from the current object
	 *
	 * @var string
	 */
	protected $className = null;

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

	/**
	 * Translates the database field type into a PHP base type
	 *
	 * @param   string  $type  The type of the field
	 *
	 * @return  string  The PHP base type
	 */
	public static function getFieldType($type)
	{
		// Remove parentheses, indicating field options / size (they don't matter in type detection)
		if (!empty($type))
		{
			[$type, ] = explode('(', $type);
		}

		$detectedType = null;

		switch (trim($type))
		{
			case 'varchar':
			case 'text':
			case 'smalltext':
			case 'longtext':
			case 'char':
			case 'mediumtext':
			case 'character varying':
			case 'nvarchar':
			case 'nchar':
				$detectedType = 'string';
				break;

			case 'date':
			case 'datetime':
			case 'time':
			case 'year':
			case 'timestamp':
			case 'timestamp without time zone':
			case 'timestamp with time zone':
				$detectedType = 'string';
				break;

			case 'tinyint':
			case 'smallint':
				$detectedType = 'bool';
				break;

			case 'float':
			case 'currency':
			case 'single':
			case 'double':
				$detectedType = 'float';
				break;
		}

		// Sometimes we have character types followed by a space and some cruft. Let's handle them.
		if (is_null($detectedType) && !empty($type))
		{
			[$type, ] = explode(' ', $type);

			switch (trim($type))
			{
				case 'varchar':
				case 'text':
				case 'smalltext':
				case 'longtext':
				case 'char':
				case 'mediumtext':
				case 'nvarchar':
				case 'nchar':
					$detectedType = 'string';
					break;

				case 'date':
				case 'datetime':
				case 'time':
				case 'year':
				case 'timestamp':
				case 'enum':
					$detectedType = 'string';
					break;

				case 'tinyint':
				case 'smallint':
					$detectedType = 'bool';
					break;

				case 'float':
				case 'currency':
				case 'single':
				case 'double':
					$detectedType = 'float';
					break;

				default:
					$detectedType = 'int';
					break;
			}
		}

		// If all else fails assume it's an int and hope for the best
		if (empty($detectedType))
		{
			$detectedType = 'int';
		}

		return $detectedType;
	}

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

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

		$hints = [
			'property'      => [],
			'method'        => [],
			'property-read' => [],
		];

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

		$magicFields = [
			'enabled', 'ordering', 'created_on', 'created_by', 'modified_on', 'modified_by', 'locked_on', 'locked_by',
		];

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

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

			if ($hasFilters)
			{
				$hints['method'][] = [
					'$this',
					$fieldName . '()',
					$fieldName . '(' . $fieldType . ' $v)',
				];
			}
		}

		$relations = $model->getRelations()->getRelationNames();

		$modelType      = get_class($model);
		$modelTypeParts = explode('\\', $modelType);
		array_pop($modelTypeParts);
		$modelType = implode('\\', $modelTypeParts) . '\\';

		if ($relations)
		{
			foreach ($relations as $relationName)
			{
				$relationObject = $model->getRelations()->getRelation($relationName)->getForeignModel();
				$relationType   = get_class($relationObject);
				$relationType   = str_replace($modelType, '', $relationType);

				$hints['property-read'][] = [
					$relationType,
					'$' . $relationName,
				];
			}
		}

		return $hints;
	}

	/**
	 * Returns the docblock with the magic field hints for the model class
	 *
	 * @return  string
	 */
	public function getHints()
	{
		$modelName = $this->className;

		$text = "/**\n * Model $modelName\n *\n";

		$hints = $this->getRawHints();

		if (!empty($hints['property']))
		{
			$text .= " * Fields:\n *\n";

			$colWidth = 0;

			foreach ($hints['property'] as $hintLine)
			{
				$colWidth = max($colWidth, strlen($hintLine[0]));
			}

			$colWidth += 2;

			foreach ($hints['property'] as $hintLine)
			{
				$text .= " * @property  " . str_pad($hintLine[0], $colWidth, ' ') . $hintLine[1] . "\n";
			}

			$text .= " *\n";
		}

		if (!empty($hints['method']))
		{
			$text .= " * Filters:\n *\n";

			$colWidth  = 0;
			$col2Width = 0;

			foreach ($hints['method'] as $hintLine)
			{
				$colWidth  = max($colWidth, strlen($hintLine[0]));
				$col2Width = max($col2Width, strlen($hintLine[1]));
			}

			$colWidth  += 2;
			$col2Width += 2;

			foreach ($hints['method'] as $hintLine)
			{
				$text .= " * @method  " . str_pad($hintLine[0], $colWidth, ' ')
					. str_pad($hintLine[1], $col2Width, ' ')
					. $hintLine[2] . "\n";
			}

			$text .= " *\n";
		}

		if (!empty($hints['property-read']))
		{
			$text .= " * Relations:\n *\n";

			$colWidth = 0;

			foreach ($hints['property-read'] as $hintLine)
			{
				$colWidth = max($colWidth, strlen($hintLine[0]));
			}

			$colWidth += 2;

			foreach ($hints['property-read'] as $hintLine)
			{
				$text .= " * @property  " . str_pad($hintLine[0], $colWidth, ' ') . $hintLine[1] . "\n";
			}

			$text .= " *\n";
		}

		$text .= "**/\n";

		return $text;
	}
}
Utils/TimezoneWrangler.php000060400000023103152455514240011653 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use DateTime;
use DateTimeZone;
use Exception;
use FOF30\Container\Container;
use FOF30\Date\Date;
use Joomla\CMS\User\User;

/**
 * A helper class to wrangle timezones, as used by Joomla!.
 *
 * @package  FOF30\Utils
 *
 * @since    3.1.3
 */
class TimezoneWrangler
{
	/**
	 * The default timestamp format string to use when one is not provided
	 *
	 * @var   string
	 */
	protected $defaultFormat = 'Y-m-d H:i:s T';

	/**
	 * When set, this timezone will be used instead of the Joomla! applicable timezone for the user.
	 *
	 * @var DateTimeZone
	 */
	protected $forcedTimezone = null;

	/**
	 * Cache of user IDs to applicable timezones
	 *
	 * @var array
	 */
	protected $userToTimezone = [];

	/**
	 * The component container for which we are created
	 *
	 * @var   Container
	 */
	protected $container;

	public function __construct(Container $container)
	{
		$this->container = $container;
	}

	/**
	 * Get the default timestamp format to use when one is not provided
	 *
	 * @return  string
	 */
	public function getDefaultFormat()
	{
		return $this->defaultFormat;
	}

	/**
	 * Set the default timestamp format to use when one is not provided
	 *
	 * @param   string  $defaultFormat
	 *
	 * @return  void
	 */
	public function setDefaultFormat($defaultFormat)
	{
		$this->defaultFormat = $defaultFormat;
	}

	/**
	 * Returns the forced timezone which is used instead of the applicable Joomla! timezone.
	 *
	 * @return  DateTimeZone
	 */
	public function getForcedTimezone()
	{
		return $this->forcedTimezone;
	}

	/**
	 * Sets the forced timezone which is used instead of the applicable Joomla! timezone. If the new timezone is
	 * different than the existing one we will also reset the user to timezone cache.
	 *
	 * @param   DateTimeZone|string  $forcedTimezone
	 *
	 * @return  void
	 */
	public function setForcedTimezone($forcedTimezone)
	{
		// Are we unsetting the forced TZ?
		if (empty($forcedTimezone))
		{
			$this->forcedTimezone = null;
			$this->resetCache();

			return;
		}

		// If the new TZ is a string we have to create an object
		if (is_string($forcedTimezone))
		{
			$forcedTimezone = new DateTimeZone($forcedTimezone);
		}

		$oldTZ = '';

		if (is_object($this->forcedTimezone) && ($this->forcedTimezone instanceof DateTimeZone))
		{
			$oldTZ = $this->forcedTimezone->getName();
		}

		if ($oldTZ == $forcedTimezone->getName())
		{
			return;
		}

		$this->forcedTimezone = $forcedTimezone;

		$this->resetCache();
	}

	/**
	 * Reset the user to timezone cache. This is done automatically every time you change the forced timezone.
	 */
	public function resetCache()
	{
		$this->userToTimezone = [];
	}

	/**
	 * Get the applicable timezone for a user. If the user is not a guest and they have a timezone set up in their
	 * profile it will be used. Otherwise we fall back to the Server Timezone as set up in Global Configuration. If that
	 * fails, we use GMT. However, if you have used a non-blank forced timezone that will be used instead, circumventing
	 * this calculation. Therefore the returned timezone is one of the following, by descending order of priority:
	 * - Forced timezone
	 * - User's timezone (explicitly set in their user profile)
	 * - Server Timezone (from Joomla's Global Configuration)
	 * - GMT
	 *
	 * @param   User|null  $user
	 *
	 * @return  DateTimeZone
	 */
	public function getApplicableTimezone($user = null)
	{
		// If we have a forced timezone use it instead of trying to figure anything out.
		if (is_object($this->forcedTimezone))
		{
			return $this->forcedTimezone;
		}

		// No user? Get the current user.
		if (is_null($user))
		{
			$user = $this->container->platform->getUser();
		}

		// If there is a cached timezone return that instead.
		if (isset($this->userToTimezone[$user->id]))
		{
			return $this->userToTimezone[$user->id];
		}

		// Prefer the user timezone if it's set.
		if (!$user->guest)
		{
			$tz = $user->getParam('timezone', null);

			if (!empty($tz))
			{
				try
				{
					$this->userToTimezone[$user->id] = new DateTimeZone($tz);

					return $this->userToTimezone[$user->id];
				}
				catch (Exception $e)
				{

				}
			}
		}

		// Get the Server Timezone from Global Configuration with a fallback to GMT
		$tz = $this->container->platform->getConfig()->get('offset', 'GMT');

		try
		{
			$this->userToTimezone[$user->id] = new DateTimeZone($tz);
		}
		catch (Exception $e)
		{
			// If an invalid timezone was set we get to use GMT
			$this->userToTimezone[$user->id] = new DateTimeZone('GMT');
		}

		return $this->userToTimezone[$user->id];
	}

	/**
	 * Returns a FOF Date object with its timezone set to the user's applicable timezone.
	 *
	 * If no user is specified the current user will be used.
	 *
	 * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
	 * timezone is specified in a date string we assume it's GMT.
	 *
	 * @param   User   $user  Applicable user for timezone calculation. Null = current user.
	 * @param   mixed  $time  Source time. Leave blank for current date/time.
	 *
	 * @return  Date
	 */
	public function getLocalDateTime($user, $time = null)
	{
		$time = empty($time) ? 'now' : $time;
		$date = new Date($time);
		$tz   = $this->getApplicableTimezone($user);
		$date->setTimezone($tz);

		return $date;
	}

	/**
	 * Returns a FOF Date object with its timezone set to GMT.
	 *
	 * If no user is specified the current user will be used.
	 *
	 * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
	 * timezone is specified in a date string we assume it's the user's applicable timezone.
	 *
	 * @param   User   $user
	 * @param   mixed  $time
	 *
	 * @return  Date
	 */
	public function getGMTDateTime($user, $time)
	{
		$time        = empty($time) ? 'now' : $time;
		$tz          = $this->getApplicableTimezone($user);
		$date        = new Date($time, $tz);
		$gmtTimezone = new DateTimeZone('GMT');
		$date->setTimezone($gmtTimezone);

		return $date;
	}

	/**
	 * Returns a formatted date string in the user's applicable timezone.
	 *
	 * If no format is specified we will use $defaultFormat.
	 *
	 * If no user is specified the current user will be used.
	 *
	 * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
	 * timezone is specified in a date string we assume it's GMT.
	 *
	 * $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). JApplicationCms does that
	 * for you automatically. If you're under CLI, a custom JApplicationWeb etc you will probably have to load this
	 * file
	 * manually.
	 *
	 * @param   string|null                    $format     Timestamp format. If empty $defaultFormat is used.
	 * @param   User|null                      $user       Applicable user for timezone calculation. Null = current
	 *                                                     user.
	 * @param   DateTime|Date|string|int|null  $time       Source time. Leave blank for current date/time.
	 * @param   bool                           $translate  Translate day of week and month names?
	 *
	 * @return  string
	 */
	public function getLocalTimeStamp($format = null, $user = null, $time = null, $translate = false)
	{
		$date   = $this->getLocalDateTime($user, $time);
		$format = empty($format) ? $this->defaultFormat : $format;

		return $date->format($format, true, $translate);
	}

	/**
	 * Returns a formatted date string in the GMT timezone.
	 *
	 * If no format is specified we will use $defaultFormat.
	 *
	 * If no user is specified the current user will be used.
	 *
	 * $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
	 * timezone is specified in a date string we assume it's the user's applicable timezone.
	 *
	 * $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). JApplicationCms does that
	 * for you automatically. If you're under CLI, a custom JApplicationWeb etc you will probably have to load this
	 * file
	 * manually.
	 *
	 * @param   string|null                    $format     Timestamp format. If empty $defaultFormat is used.
	 * @param   User|null                      $user       Applicable user for timezone calculation. Null = current
	 *                                                     user.
	 * @param   DateTime|Date|string|int|null  $time       Source time. Leave blank for current date/time.
	 * @param   bool                           $translate  Translate day of week and month names?
	 *
	 * @return  string
	 */
	public function getGMTTimeStamp($format = null, $user = null, $time = null, $translate = false)
	{
		$date   = $this->localToGMT($user, $time);
		$format = empty($format) ? $this->defaultFormat : $format;

		return $date->format($format, true, $translate);
	}

	/**
	 * Convert a local time back to GMT. Returns a Date object with its timezone set to GMT.
	 *
	 * This is an alias to getGMTDateTime
	 *
	 * @param   string|Date  $time
	 * @param   User|null    $user
	 *
	 * @return  Date
	 */
	public function localToGMT($time, $user = null)
	{
		return $this->getGMTDateTime($user, $time);
	}

	/**
	 * Convert a GMT time to local timezone. Returns a Date object with its timezone set to the applicable user's TZ.
	 *
	 * This is an alias to getLocalDateTime
	 *
	 * @param   string|Date  $time
	 * @param   User|null    $user
	 *
	 * @return  Date
	 */
	public function GMTToLocal($time, $user = null)
	{
		return $this->getLocalDateTime($user, $time);
	}
}
Utils/MediaVersion.php000060400000011527152455514240010753 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Utils;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use JDatabaseDriver;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;

/**
 * Class MediaVersion
 * @package FOF30\Utils
 *
 * @since   3.5.3
 */
class MediaVersion
{
	/**
	 * Cached the version and date of FOF-powered components
	 *
	 * @var   array
	 * @since 3.5.3
	 */
	protected static $componentVersionCache = [];

	/**
	 * The current component's container
	 *
	 * @var   Container
	 * @since 3.5.3
	 */
	protected $container;

	/**
	 * The configured media query version
	 *
	 * @var   string|null;
	 * @since 3.5.3
	 */
	protected $mediaVersion;

	/**
	 * MediaVersion constructor.
	 *
	 * @param   Container  $c  The component container
	 *
	 * @since   3.5.3
	 */
	public function __construct(Container $c)
	{
		$this->container = $c;
	}

	/**
	 * Get a component's version and date
	 *
	 * @param   string           $component
	 * @param   JDatabaseDriver  $db
	 *
	 * @return  array
	 * @since   3.5.3
	 */
	protected static function getComponentVersionAndDate($component, $db)
	{
		if (array_key_exists($component, self::$componentVersionCache))
		{
			return self::$componentVersionCache[$component];
		}

		$version = '0.0.0';
		$date    = date('Y-m-d H:i:s');

		try
		{
			$query = $db->getQuery(true)
				->select([
					$db->qn('manifest_cache'),
				])->from($db->qn('#__extensions'))
				->where($db->qn('type') . ' = ' . $db->q('component'))
				->where($db->qn('name') . ' = ' . $db->q($component));

			$db->setQuery($query);

			$json = $db->loadResult();

			if (class_exists('JRegistry'))
			{
				$params = new Registry($json);
			}
			else
			{
				$params = new Registry($json);
			}

			$version = $params->get('version', $version);
			$date    = $params->get('creationDate', $date);
		}
		catch (Exception $e)
		{
		}

		self::$componentVersionCache[$component] = [$version, $date];

		return self::$componentVersionCache[$component];
	}

	/**
	 * Serialization helper
	 *
	 * This is for the benefit of legacy components which might use Joomla's JS/CSS inclusion directly passing
	 * $container->mediaVersion as the version argument. In FOF 3.5.2 and lower that was always string or null, making
	 * it a safe bet. In FOF 3.5.3 and later it's an object. It's not converted to a string until Joomla builds its
	 * template header. However, Joomla's cache system will try to serialize all CSS and JS definitions, including their
	 * parameters of which version is one. Therefore, for those legacy applications, Joomla would be trying to serialize
	 * the MediaVersion object which would try to serialize the container. That would cause an immediate failure since
	 * we protect the Container from being serialized.
	 *
	 * Our Template service knows about this and stringifies the MediaVersion before passing it to Joomla. Legacy apps
	 * may not do that. Using the __sleep and __wakeup methods in this class we make sure that we are essentially
	 * storing nothing but strings in the serialized representation and we reconstruct the container upon
	 * unseralization. That said, it's a good idea to use the Template service instead of $container->mediaVersion
	 * directly or, at the very least, use (string) $container->mediaVersion when using the Template service is not a
	 * viable option.
	 *
	 * @return  string[]
	 */
	public function __sleep()
	{
		$this->componentName = $this->container->componentName;

		return [
			'mediaVersion',
			'componentName',
		];
	}

	/**
	 * Unserialization helper
	 *
	 * @return  void
	 * @see     __sleep
	 */
	public function __wakeup()
	{
		if (isset($this->componentName))
		{
			$this->container = Container::getInstance($this->componentName);
		}
	}

	/**
	 * Returns the media query version string
	 *
	 * @return  string
	 * @since   3.5.3
	 */
	public function __toString()
	{
		if (empty($this->mediaVersion))
		{
			$this->mediaVersion = $this->getDefaultMediaVersion();
		}

		return $this->mediaVersion;
	}

	/**
	 * Sets the media query version string
	 *
	 * @param   mixed  $mediaVersion
	 *
	 * @since   3.5.3
	 */
	public function setMediaVersion($mediaVersion)
	{
		$this->mediaVersion = $mediaVersion;
	}

	/**
	 * Returns the default media query version string if none is already defined
	 *
	 * @return  string
	 * @since   3.5.3
	 */
	protected function getDefaultMediaVersion()
	{
		// Initialise
		[$version, $date] = self::getComponentVersionAndDate($this->container->componentName, $this->container->db);

		// Get the site's secret
		try
		{
			$app = Factory::getApplication();

			if (method_exists($app, 'get'))
			{
				$secret = $app->get('secret');
			}
		}
		catch (Exception $e)
		{
		}

		// Generate the version string
		return md5($version . $date . $secret);
	}
}
file_fof30.xml000060400000011167152455514240007213 0ustar00<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ @package   FOF
  ~ @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
  ~ @license   GNU General Public License version 2, or later
  -->

<!--
A legitimate question among developers reading this file may be why we are using a "files" extension type instead of the
"library" type which, on the face of it, seems more appropriate.

We have not lost our mind. We are working around the adverse effects of the very different way Joomla treats "library"
packages than any other package type.

When applying an update to a library package Joomla! will uninstall it BEFORE it executes the installation script's
preflight event. This means that any checks made there to prevent the installation of the library in an incompatible
environment (e.g. wrong PHP or Joomla! version, or even preventing an accidental downgrade) results in the library files
being UNINSTALLED.

This is really bad for anyone who tries to install a library package on an unsupported environment. If the library
package runs no checks the installed library version causes the extensions that depend on it to crash, taking down the
site. If the library package runs checks in the earliest available point in time (preflight) you end up with the old
library files having been uninstalled which again causes the extensions that depend on it to crash, taking down the
site. No matter what you do, the very action of TRYING to install an unsupported library version KILLS THE SITE. This is
madness. Worse than that, this is a known issue in Joomla since ~2017 but nobody will fix it until a new major version.
Since this doesn't look likely in Joomla 4.0 we are talking about Joomla 5 which could be anywhere from two to ten years
into the future. Clearly this doesn't cut it for us: we don't want trying to install our software causing sites to stop
working!

The only thing we can do to prevent your sites from crashing to the ground if you try to install a version of our
software which does not support your PHP and/or Joomla! versions is to deliver our library as a *files* package. This
is nonsensical, it is 100% architecturally wrong BUT it is also the only way we can apply pre-installation checks which
fail gracefully instead of causing your site to crash and burn.
-->
<extension type="file" version="3.8" method="upgrade">
    <name>file_fof30</name>
	<description>
		<![CDATA[
		Framework-on-Framework (FOF) 3.x - The rapid application development framework for Joomla!.<br/>
		<b>WARNING</b>: This is NOT a duplicate of the FOF library already installed with Joomla!. It is a different version used by other extensions on your site. Do NOT uninstall either FOF package. If you do you will break your site.
		]]>
	</description>
	<creationDate>2020-07-14</creationDate>
	<author>Nicholas K. Dionysopoulos / Akeeba Ltd</author>
	<authorEmail>nicholas@akeeba.com</authorEmail>
	<authorUrl>https://www.akeeba.com</authorUrl>
	<copyright>(c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd</copyright>
	<license>GNU GPLv2 or later</license>
	<version>3.6.2</version>
	<packager>Akeeba Ltd</packager>
	<packagerurl>https://www.akeeba.com/download.html</packagerurl>

	<fileset>
		<files folder="fof" target="libraries/fof30">
			<folder>Autoloader</folder>
			<folder>Cli</folder>
			<folder>Configuration</folder>
			<folder>Container</folder>
			<folder>Controller</folder>
			<folder>Database</folder>
			<folder>Date</folder>
			<folder>Dispatcher</folder>
			<folder>Download</folder>
			<folder>Encrypt</folder>
			<folder>Event</folder>
			<folder>Factory</folder>
			<folder>Inflector</folder>
			<folder>Input</folder>
			<folder>Layout</folder>
			<folder>Model</folder>
			<folder>Params</folder>
			<folder>Pimple</folder>
			<folder>Platform</folder>
			<folder>Render</folder>
			<folder>Template</folder>
			<folder>Timer</folder>
			<folder>Toolbar</folder>
			<folder>TransparentAuthentication</folder>
			<folder>Update</folder>
			<folder>Utils</folder>
			<folder>View</folder>
			<folder>ViewTemplates</folder>

			<file>LICENSE.txt</file>
			<file>include.php</file>
			<file>version.txt</file>
			<file>.htaccess</file>
			<file>web.config</file>
		</files>
		<files folder="fof/language/en-GB" target="language/en-GB">
			<file>en-GB.lib_fof30.ini</file>
		</files>
		<files folder="fof/language/en-GB" target="administrator/language/en-GB">
			<file>en-GB.lib_fof30.ini</file>
		</files>
	</fileset>

	<!-- Installation / uninstallation script file -->
	<scriptfile>script.fof.php</scriptfile>

    <updateservers>
        <server type="extension" priority="1" name="FOF 3.x">http://cdn.akeeba.com/updates/fof3_file.xml</server>
    </updateservers>
</extension>View/Engine/BladeEngine.php000060400000001051152455514240011531 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Engine;

defined('_JEXEC') || die;

use FOF30\View\View;

/**
 * View engine for compiling PHP template files.
 */
class BladeEngine extends CompilingEngine implements EngineInterface
{
	public function __construct(View $view)
	{
		parent::__construct($view);

		// Assign the Blade compiler to this engine
		$this->compiler = $view->getContainer()->blade;
	}
}
View/Engine/CompilingEngine.php000060400000015255152455514240012456 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Engine;

defined('_JEXEC') || die;

use FOF30\Utils\Buffer;
use FOF30\View\Compiler\CompilerInterface;
use FOF30\View\Exception\PossiblySuhosin;
use JLoader;
use Joomla\CMS\Filesystem\File;

/**
 * View engine for compiling PHP template files.
 */
abstract class CompilingEngine extends AbstractEngine implements EngineInterface
{
	/** @var  CompilerInterface  The compiler used by this engine */
	protected $compiler = null;

	/**
	 * Get the 3ναlυa+3d contents of the view template. (I use leetspeak here because of bad quality hosts with broken
	 * scanners)
	 *
	 * @param   string  $path         The path to the view template
	 * @param   array   $forceParams  Any additional information to pass to the view template engine
	 *
	 * @return  array  Content evaluation information
	 */
	public function get($path, array $forceParams = [])
	{
		// If it's cached return the path to the cached file's path
		if ($this->isCached($path))
		{
			return [
				'type'    => 'path',
				'content' => $this->getCachePath($path),
			];
		}

		/**
		 * Compile and cache the file. We also add the file path in a comment at the top of the file so phpStorm can
		 * debug it.
		 *
		 * @see https://blog.jetbrains.com/phpstorm/2019/02/phpstorm-2019-1-eap-191-5849-26/
		 * @see https://laravel-news.com/laravel-5-8-blade-template-file-path
		 */
		$content   = "<?php /* $path */ ?>\n";
		$content   .= $this->compile($path, $forceParams);
		$cachePath = $this->putToCache($path, $content);
		$isPHPFile = substr($path, -4) == '.php';

		// If we could cache it, return the cached file's path
		if ($cachePath !== false)
		{
			// Bust the opcode cache for .php files
			if ($isPHPFile)
			{
				$this->bustOpCache($path);
			}

			return [
				'type'    => 'path',
				'content' => $cachePath,
			];
		}

		// We could not write to the cache. Hm, can I use a stream wrapper?
		$canUseStreams = Buffer::canRegisterWrapper();

		if ($canUseStreams)
		{
			$id         = $this->getIdentifier($path);
			$streamPath = 'fof://' . $this->view->getContainer()->componentName . '/compiled_templates/' . $id . '.php';
			file_put_contents($streamPath, $content);

			// Bust the opcode cache for .php files
			if ($isPHPFile)
			{
				$this->bustOpCache($path);
			}

			return [
				'type'    => 'path',
				'content' => $streamPath,
			];
		}

		// I couldn't use a stream wrapper. I have to give up.
		throw new PossiblySuhosin;
	}

	/**
	 * Returns the path where I can find a precompiled version of the unprocessed view template which lives in $path
	 *
	 * @param   string  $path  The path to the uncompiled view template
	 *
	 * @return  bool|string  False if the view template is outside the component's front- or backend.
	 *
	 * @since   3.3.1
	 */
	public function getPrecompiledPath($path)
	{
		// Normalize the path to the file
		$path = realpath($path);

		if ($path === false)
		{
			// The file doesn't exist
			return false;
		}

		// Is this path under the component's front- or backend?
		$frontendPath = realpath($this->view->getContainer()->frontEndPath);
		$backendPath  = realpath($this->view->getContainer()->backEndPath);

		$backPos  = strpos($path, $backendPath);
		$frontPos = strpos($path, $frontendPath);

		if (($backPos !== 0) && ($frontPos !== 0))
		{
			// This is not a view template shipped with the component, i.e. it can't be precompiled
			return false;
		}

		// Eliminate the component path from $path to get the relative path to the file
		$componentPath = $frontendPath;

		if ($backPos === 0)
		{
			$componentPath = $backendPath;
		}

		$relativePath = ltrim(substr($path, strlen($componentPath)), '\\/');

		// Break down the relative path to its parts
		$relativePath = str_replace('\\', '/', $relativePath);
		$pathParts    = explode('/', $relativePath);

		// Remove the prefix
		$prefix = array_shift($pathParts);

		// If it's a legacy view, View, Views, or views prefix remove the 'tmpl' part
		if ($prefix != 'ViewTemplates')
		{
			unset($pathParts[1]);
		}

		// Get the last part and process the extension
		$viewFile            = array_pop($pathParts);
		$extensionWithoutDot = $this->compiler->getFileExtension();
		$pathParts[]         = substr($viewFile, 0, -strlen($extensionWithoutDot)) . 'php';

		$precompiledRelativePath = implode(DIRECTORY_SEPARATOR, $pathParts);

		return $componentPath . DIRECTORY_SEPARATOR . 'PrecompiledTemplates' . DIRECTORY_SEPARATOR . $precompiledRelativePath;
	}

	/**
	 * A method to compile the raw view template into valid PHP
	 *
	 * @param   string  $path         The path to the view template
	 * @param   array   $forceParams  Any additional information to pass to the view template compiler
	 *
	 * @return  string  The template compiled to executable PHP
	 */
	protected function compile($path, array $forceParams = [])
	{
		return $this->compiler->compile($path, $forceParams);
	}

	protected function getIdentifier($path)
	{
		if (function_exists('sha1'))
		{
			return sha1($path);
		}

		return md5($path);
	}

	protected function getCachePath($path)
	{
		$id = $this->getIdentifier($path);

		return JPATH_CACHE . '/' . $this->view->getContainer()->componentName . '/compiled_templates/' . $id . '.php';
	}

	protected function isCached($path)
	{
		if (!$this->compiler->isCacheable())
		{
			return false;
		}

		$cachePath = $this->getCachePath($path);

		if (!file_exists($cachePath))
		{
			return false;
		}

		$cacheTime = filemtime($cachePath);
		$fileTime  = filemtime($path);

		return $fileTime <= $cacheTime;
	}

	protected function getCached($path)
	{
		$cachePath = $this->getCachePath($path);

		return file_get_contents($cachePath);
	}

	protected function putToCache($path, $content)
	{
		$cachePath = $this->getCachePath($path);

		if (@file_put_contents($cachePath, $content))
		{
			return $cachePath;
		}

		if (File::write($cachePath, $content))
		{
			return $cachePath;
		}

		return false;
	}

	/**
	 * Bust the opcode cache for a given .php file
	 *
	 * This method can address opcode caching with:
	 * - Zend OPcache
	 * - Alternative PHP Cache (now defunct)
	 * - Windows Cache Extension for PHP (versions lower than 2.0.0)
	 * - XCache (now defunct)
	 *
	 * @param   string  $path  The file to bus the cache for
	 *
	 * @return  void
	 */
	private function bustOpCache($path)
	{
		if (function_exists('opcache_invalidate'))
		{
			opcache_invalidate($path);
		}

		if (function_exists('apc_compile_file'))
		{
			apc_compile_file($path);
		}

		if (function_exists('wincache_refresh_if_changed'))
		{
			wincache_refresh_if_changed([$path]);
		}

		if (function_exists('xcache_asm'))
		{
			xcache_asm($path);
		}
	}
}
View/Engine/PhpEngine.php000060400000001666152455514240011265 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Engine;

defined('_JEXEC') || die;

/**
 * View engine for plain PHP template files (no translation).
 */
class PhpEngine extends AbstractEngine implements EngineInterface
{
	/**
	 * Get the 3ναluα+3d contents of the view template. (I use leetspeak here because of bad quality hosts with broken
	 * scanners)
	 *
	 * @param   string  $path         The path to the view template
	 * @param   array   $forceParams  Any additional information to pass to the view template engine
	 *
	 * @return  array  Content 3ναlυα+ιοη information (I use leetspeak here because of asshole hosts with broken
	 *                 scanners)
	 */
	public function get($path, array $forceParams = [])
	{
		return [
			'type'    => 'path',
			'content' => $path,
		];
	}
}
View/Engine/EngineInterface.php000060400000001610152455514240012423 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Engine;

defined('_JEXEC') || die;

use FOF30\View\View;

interface EngineInterface
{
	/**
	 * Public constructor
	 *
	 * @param   View  $view  The view we belong to
	 */
	public function __construct(View $view);

	/**
	 * Get the include path for a parsed view template
	 *
	 * @param   string  $path         The path to the view template
	 * @param   array   $forceParams  Any additional information to pass to the view template engine
	 *
	 * @return  array  Content 3ναlυα+ιοη information ['type' => 'raw|path', 'content' => 'path or raw content'] (I use
	 *                 leetspeak here because of bad quality hosts with broken scanners)
	 */
	public function get($path, array $forceParams = []);
}
View/Engine/AbstractEngine.php000060400000002015152455514240012266 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Engine;

defined('_JEXEC') || die;

use FOF30\View\View;

abstract class AbstractEngine implements EngineInterface
{
	/** @var   View  The view we belong to */
	protected $view = null;

	/**
	 * Public constructor
	 *
	 * @param   View  $view  The view we belong to
	 */
	public function __construct(View $view)
	{
		$this->view = $view;
	}

	/**
	 * Get the include path for a parsed view template
	 *
	 * @param   string  $path         The path to the view template
	 * @param   array   $forceParams  Any additional information to pass to the view template engine
	 *
	 * @return  array  Content 3ναlυα+ιοη information (I use leetspeak here because of bad quality hosts with broken
	 *                 scanners)
	 */
	public function get($path, array $forceParams = [])
	{
		return [
			'type'    => 'raw',
			'content' => '',
		];
	}
}
View/Exception/EmptyStack.php000060400000001163152455514240012215 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when we are trying to operate on an empty section stack
 */
class EmptyStack extends RuntimeException
{
	public function __construct($message = "", $code = 500, Exception $previous = null)
	{
		$message = Text::_('LIB_FOF_VIEW_EMPTYSECTIONSTACK');

		parent::__construct($message, $code, $previous);
	}
}
View/Exception/ModelNotFound.php000060400000001174152455514240012650 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when we can't get a Controller's name
 */
class ModelNotFound extends RuntimeException
{
	public function __construct($path, $viewName, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_VIEW_MODELNOTINVIEW', $path, $viewName);

		parent::__construct($message, $code, $previous);
	}
}
View/Exception/UnrecognisedExtension.php000060400000001240152455514240014447 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Exception;

defined('_JEXEC') || die;

use Exception;
use InvalidArgumentException;
use Joomla\CMS\Language\Text;

/**
 * Exception thrown when we can't figure out which engine to use for a view template
 */
class UnrecognisedExtension extends InvalidArgumentException
{
	public function __construct($path, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_VIEW_UNRECOGNISEDEXTENSION', $path);

		parent::__construct($message, $code, $previous);
	}
}
View/Exception/CannotGetName.php000060400000001177152455514240012621 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when we can't get a Controller's name
 */
class CannotGetName extends RuntimeException
{
	public function __construct($message = "", $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_VIEW_ERR_GET_NAME');
		}

		parent::__construct($message, $code, $previous);
	}
}
View/Exception/PossiblySuhosin.php000060400000001272152455514240013307 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when the access to the requested resource is forbidden under the current execution context
 */
class PossiblySuhosin extends RuntimeException
{
	public function __construct($message = "", $code = 403, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_VIEW_POSSIBLYSUHOSIN');
		}

		parent::__construct($message, $code, $previous);
	}

}
View/Exception/AccessForbidden.php000060400000001305152455514240013145 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when the access to the requested resource is forbidden under the current execution context
 */
class AccessForbidden extends RuntimeException
{
	public function __construct($message = "", $code = 403, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN');
		}

		parent::__construct($message, $code, $previous);
	}

}
View/Compiler/CompilerInterface.php000060400000001637152455514240013346 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Compiler;

defined('_JEXEC') || die;

interface CompilerInterface
{
	/**
	 * Are the results of this compiler engine cacheable? If the engine makes use of the forcedParams it must return
	 * false.
	 *
	 * @return  mixed
	 */
	public function isCacheable();

	/**
	 * Compile a view template into PHP and HTML
	 *
	 * @param   string  $path         The absolute filesystem path of the view template
	 * @param   array   $forceParams  Any parameters to force (only for engines returning raw HTML)
	 *
	 * @return mixed
	 */
	public function compile($path, array $forceParams = []);

	/**
	 * Returns the file extension supported by this compiler
	 *
	 * @return  string
	 *
	 * @since   3.3.1
	 */
	public function getFileExtension();
}
View/Compiler/Blade.php000060400000061730152455514240010762 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\Compiler;

defined('_JEXEC') || die;

use FOF30\Container\Container;

class Blade implements CompilerInterface
{
	/**
	 * Are the results of this engine cacheable?
	 *
	 * @var bool
	 */
	protected $isCacheable = true;

	/**
	 * The extension of the template files supported by this compiler
	 *
	 * @var    string
	 * @since  3.3.1
	 */
	protected $fileExtension = 'blade.php';

	/**
	 * All of the registered compiler extensions.
	 *
	 * @var array
	 */
	protected $extensions = [];

	/**
	 * The file currently being compiled.
	 *
	 * @var string
	 */
	protected $path;

	/**
	 * All of the available compiler functions. Each one is called against every HTML block in the template.
	 *
	 * @var array
	 */
	protected $compilers = [
		'Extensions',
		'Statements',
		'Comments',
		'Echos',
	];

	/**
	 * Array of opening and closing tags for escaped echos.
	 *
	 * @var array
	 */
	protected $contentTags = ['{{', '}}'];

	/**
	 * Array of opening and closing tags for escaped echos.
	 *
	 * @var array
	 */
	protected $escapedTags = ['{{{', '}}}'];

	/**
	 * Array of footer lines to be added to template.
	 *
	 * @var array
	 */
	protected $footer = [];

	/**
	 * Counter to keep track of nested forelse statements.
	 *
	 * @var int
	 */
	protected $forelseCounter = 0;

	/**
	 * The FOF container we are attached to
	 *
	 * @var Container
	 */
	protected $container;

	/**
	 * Should I use the PHP Tokenizer extension to compile Blade templates? Default is true and is preferable. We expect
	 * this to be false only on bad quality hosts. It can be overridden with Reflection for testing purposes.
	 *
	 * @var bool
	 */
	protected $usingTokenizer = false;

	public function __construct(Container $container)
	{
		$this->container      = $container;
		$this->usingTokenizer = false;

		if (function_exists('token_get_all') && defined('T_INLINE_HTML'))
		{
			$this->usingTokenizer = true;
		}
	}

	/**
	 * Report if the PHP Tokenizer extension is being used
	 *
	 * @return  bool
	 */
	public function isUsingTokenizer()
	{
		return $this->usingTokenizer;
	}

	/**
	 * Are the results of this compiler engine cacheable? If the engine makes use of the forcedParams it must return
	 * false.
	 *
	 * @return  mixed
	 */
	public function isCacheable()
	{
		if (defined('JDEBUG') && JDEBUG)
		{
			return false;
		}

		return $this->isCacheable;
	}

	/**
	 * Compile a view template into PHP and HTML
	 *
	 * @param   string  $path         The absolute filesystem path of the view template
	 * @param   array   $forceParams  Any parameters to force (only for engines returning raw HTML)
	 *
	 * @return mixed
	 */
	public function compile($path, array $forceParams = [])
	{
		$this->footer = [];

		$fileData = @file_get_contents($path);

		if ($path)
		{
			$this->setPath($path);
		}

		return $this->compileString($fileData);
	}


	/**
	 * Get the path currently being compiled.
	 *
	 * @return string
	 */
	public function getPath()
	{
		return $this->path;
	}

	/**
	 * Set the path currently being compiled.
	 *
	 * @param   string  $path
	 *
	 * @return void
	 */
	public function setPath($path)
	{
		$this->path = $path;
	}

	/**
	 * Compile the given Blade template contents.
	 *
	 * @param   string  $value
	 *
	 * @return string
	 */
	public function compileString($value)
	{
		$result = '';

		if ($this->usingTokenizer)
		{
			// Here we will loop through all of the tokens returned by the Zend lexer and
			// parse each one into the corresponding valid PHP. We will then have this
			// template as the correctly rendered PHP that can be rendered natively.
			foreach (token_get_all($value) as $token)
			{
				$result .= is_array($token) ? $this->parseToken($token) : $token;
			}
		}
		else
		{
			foreach ($this->compilers as $type)
			{
				$value = $this->{"compile{$type}"}($value);
			}

			$result .= $value;
		}

		// If there are any footer lines that need to get added to a template we will
		// add them here at the end of the template. This gets used mainly for the
		// template inheritance via the extends keyword that should be appended.
		if (count($this->footer) > 0)
		{
			$result = ltrim($result, PHP_EOL)
				. PHP_EOL . implode(PHP_EOL, array_reverse($this->footer));
		}

		return $result;
	}

	/**
	 * Compile the default values for the echo statement.
	 *
	 * @param   string  $value
	 *
	 * @return string
	 */
	public function compileEchoDefaults($value)
	{
		return preg_replace('/^(?=\$)(.+?)(?:\s+or\s+)(.+?)$/s', 'isset($1) ? $1 : $2', $value);
	}

	/**
	 * Register a custom Blade compiler. Using a tag or not changes the behavior of this method when you try to redefine
	 * an existing custom Blade compiler.
	 *
	 * If you use a tag which already exists the old compiler is replaced by the new one you are defining.
	 *
	 * If you do not use a tag, the new compiler you are defining will always be added to the bottom of the list. That
	 * is to say, if another compiler would be matching the same function name (e.g. `@foobar`) it would get compiled by
	 * the first compiler, the one already set, not the one you are defining now. You are suggested to always use a tag
	 * for this reason.
	 *
	 * Finally, note that custom Blade compilers are compiled last. This means that you cannot override a core Blade
	 * compiler with a custom one. If you need to do that you need to create a new Compiler class -- probably extending
	 * this one -- and override the protected compiler methods. Remember to also create a custom Container and override
	 * its 'blade' key with a callable which returns an object of your custom class.
	 *
	 * @param   callable  $compiler
	 * @param   string    $tag  Optional. Give the callable a tag you can look for with hasExtensionByName
	 *
	 * @return void
	 */
	public function extend($compiler, $tag = null)
	{
		if (!is_null($tag))
		{
			$this->extensions[$tag] = $compiler;

			return;
		}

		$this->extensions[] = $compiler;
	}

	/**
	 * Look if a custom Blade compiler exists given its optional tag name.
	 *
	 * @param   string  $tag
	 *
	 * @return  bool
	 */
	public function hasExtension($tag)
	{
		return array_key_exists($tag, $this->extensions);
	}

	/**
	 * Remove a custom BLade compiler given its optional tag name
	 *
	 * @param   string  $tag
	 */
	public function removeExtension($tag)
	{
		if (!$this->hasExtension($tag))
		{
			return;
		}

		unset ($this->extensions[$tag]);
	}

	/**
	 * Get the regular expression for a generic Blade function.
	 *
	 * @param   string  $function
	 *
	 * @return string
	 */
	public function createMatcher($function)
	{
		return '/(?<!\w)(\s*)@' . $function . '(\s*\(.*\))/';
	}

	/**
	 * Get the regular expression for a generic Blade function.
	 *
	 * @param   string  $function
	 *
	 * @return string
	 */
	public function createOpenMatcher($function)
	{
		return '/(?<!\w)(\s*)@' . $function . '(\s*\(.*)\)/';
	}

	/**
	 * Create a plain Blade matcher.
	 *
	 * @param   string  $function
	 *
	 * @return string
	 */
	public function createPlainMatcher($function)
	{
		return '/(?<!\w)(\s*)@' . $function . '(\s*)/';
	}

	/**
	 * Sets the escaped content tags used for the compiler.
	 *
	 * @param   string  $openTag
	 * @param   string  $closeTag
	 *
	 * @return void
	 */
	public function setEscapedContentTags($openTag, $closeTag)
	{
		$this->setContentTags($openTag, $closeTag, true);
	}

	/**
	 * Gets the content tags used for the compiler.
	 *
	 * @return string
	 */
	public function getContentTags()
	{
		return $this->getTags();
	}

	/**
	 * Sets the content tags used for the compiler.
	 *
	 * @param   string  $openTag
	 * @param   string  $closeTag
	 * @param   bool    $escaped
	 *
	 * @return void
	 */
	public function setContentTags($openTag, $closeTag, $escaped = false)
	{
		$property = ($escaped === true) ? 'escapedTags' : 'contentTags';

		$this->{$property} = [preg_quote($openTag), preg_quote($closeTag)];
	}

	/**
	 * Gets the escaped content tags used for the compiler.
	 *
	 * @return string
	 */
	public function getEscapedContentTags()
	{
		return $this->getTags(true);
	}

	/**
	 * Returns the file extension supported by this compiler
	 *
	 * @return  string
	 *
	 * @since   3.3.1
	 */
	public function getFileExtension()
	{
		return $this->fileExtension;
	}

	/**
	 * Parse the tokens from the template.
	 *
	 * @param   array  $token
	 *
	 * @return string
	 */
	protected function parseToken($token)
	{
		[$id, $content] = $token;

		if ($id == T_INLINE_HTML)
		{
			foreach ($this->compilers as $type)
			{
				$content = $this->{"compile{$type}"}($content);
			}
		}

		return $content;
	}

	/**
	 * Execute the user defined extensions.
	 *
	 * @param   string  $value
	 *
	 * @return string
	 */
	protected function compileExtensions($value)
	{
		foreach ($this->extensions as $compiler)
		{
			$value = call_user_func($compiler, $value, $this);
		}

		return $value;
	}

	/**
	 * Compile Blade comments into valid PHP.
	 *
	 * @param   string  $value
	 *
	 * @return string
	 */
	protected function compileComments($value)
	{
		$pattern = sprintf('/%s--((.|\s)*?)--%s/', $this->contentTags[0], $this->contentTags[1]);

		return preg_replace($pattern, '<?php /*$1*/ ?>', $value);
	}

	/**
	 * Compile Blade echos into valid PHP.
	 *
	 * @param   string  $value
	 *
	 * @return string
	 */
	protected function compileEchos($value)
	{
		$difference = strlen($this->contentTags[0]) - strlen($this->escapedTags[0]);

		if ($difference > 0)
		{
			return $this->compileEscapedEchos($this->compileRegularEchos($value));
		}

		return $this->compileRegularEchos($this->compileEscapedEchos($value));
	}

	/**
	 * Compile Blade Statements that start with "@"
	 *
	 * @param   string  $value
	 *
	 * @return mixed
	 */
	protected function compileStatements($value)
	{
		return preg_replace_callback('/\B@(\w+)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', [
			$this, 'compileStatementsCallback',
		], $value);
	}

	/**
	 * Callback for compileStatements, since $this is not allowed in Closures under PHP 5.3.
	 *
	 * @param   $match
	 *
	 * @return  string
	 */
	protected function compileStatementsCallback($match)
	{
		if (method_exists($this, $method = 'compile' . ucfirst($match[1])))
		{
			$match[0] = $this->$method(array_get($match, 3));
		}

		return isset($match[3]) ? $match[0] : $match[0] . $match[2];
	}

	/**
	 * Compile the "regular" echo statements.
	 *
	 * @param   string  $value
	 *
	 * @return  string
	 */
	protected function compileRegularEchos($value)
	{
		$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->contentTags[0], $this->contentTags[1]);

		return preg_replace_callback($pattern, [$this, 'compileRegularEchosCallback'], $value);
	}

	/**
	 * Callback for compileRegularEchos, since $this is not allowed in Closures under PHP 5.3.
	 *
	 * @param   array  $matches
	 *
	 * @return  string
	 */
	protected function compileRegularEchosCallback($matches)
	{
		$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];

		return $matches[1] ? substr($matches[0], 1) : '<?php echo ' . $this->compileEchoDefaults($matches[2]) . '; ?>' . $whitespace;
	}

	/**
	 * Compile the escaped echo statements.
	 *
	 * @param   string  $value
	 *
	 * @return string
	 */
	protected function compileEscapedEchos($value)
	{
		$pattern = sprintf('/%s\s*(.+?)\s*%s(\r?\n)?/s', $this->escapedTags[0], $this->escapedTags[1]);

		return preg_replace_callback($pattern, [$this, 'compileEscapedEchosCallback'], $value);
	}

	/**
	 * Callback for compileEscapedEchos, since $this is not allowed in Closures under PHP 5.3.
	 *
	 * @param   array  $matches
	 *
	 * @return  string
	 */
	protected function compileEscapedEchosCallback($matches)
	{
		$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];

		return '<?php echo $this->escape(' . $this->compileEchoDefaults($matches[1]) . '); ?>' . $whitespace;
	}

	/**
	 * Compile the each statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEach($expression)
	{
		return "<?php echo \$this->renderEach{$expression}; ?>";
	}

	/**
	 * Compile the yield statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileYield($expression)
	{
		return "<?php echo \$this->yieldContent{$expression}; ?>";
	}

	/**
	 * Compile the show statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileShow($expression)
	{
		return "<?php echo \$this->yieldSection(); ?>";
	}

	/**
	 * Compile the section statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileSection($expression)
	{
		return "<?php \$this->startSection{$expression}; ?>";
	}

	/**
	 * Compile the append statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileAppend($expression)
	{
		return "<?php \$this->appendSection(); ?>";
	}

	/**
	 * Compile the end-section statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndsection($expression)
	{
		return "<?php \$this->stopSection(); ?>";
	}

	/**
	 * Compile the stop statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileStop($expression)
	{
		return "<?php \$this->stopSection(); ?>";
	}

	/**
	 * Compile the overwrite statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileOverwrite($expression)
	{
		return "<?php \$this->stopSection(true); ?>";
	}

	/**
	 * Compile the unless statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileUnless($expression)
	{
		return "<?php if ( ! $expression): ?>";
	}

	/**
	 * Compile the end unless statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndunless($expression)
	{
		return "<?php endif; ?>";
	}

	/**
	 * Compile the end repeatable statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileRepeatable($expression)
	{
		$expression = trim($expression, '()');
		$parts      = explode(',', $expression, 2);

		$functionName  = '_fof_blade_repeatable_' . md5($this->path . trim($parts[0]));
		$argumentsList = $parts[1] ?? '';

		return "<?php @\$$functionName = function($argumentsList) { ?>";
	}

	/**
	 * Compile the end endRepeatable statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndRepeatable($expression)
	{
		return "<?php }; ?>";
	}

	/**
	 * Compile the end yieldRepeatable statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileYieldRepeatable($expression)
	{
		$expression = trim($expression, '()');
		$parts      = explode(',', $expression, 2);

		$functionName  = '_fof_blade_repeatable_' . md5($this->path . trim($parts[0]));
		$argumentsList = $parts[1] ?? '';

		return "<?php \$$functionName($argumentsList); ?>";
	}

	/**
	 * Compile the lang statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileLang($expression)
	{
		return "<?php echo \\Joomla\\CMS\\Language\\Text::_$expression; ?>";
	}

	/**
	 * Compile the sprintf statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileSprintf($expression)
	{
		return "<?php echo \\Joomla\\CMS\\Language\\Text::sprintf$expression; ?>";
	}

	/**
	 * Compile the plural statements into valid PHP.
	 *
	 * e.g. @plural('COM_FOOBAR_N_ITEMS_SAVED', $countItemsSaved)
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 * @see JText::plural()
	 *
	 */
	protected function compilePlural($expression)
	{
		return "<?php echo \\Joomla\\CMS\\Language\\Text::plural$expression; ?>";
	}

	/**
	 * Compile the token statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileToken($expression)
	{
		return "<?php echo \$this->container->platform->getToken(true); ?>";
	}

	/**
	 * Compile the else statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileElse($expression)
	{
		return "<?php else: ?>";
	}

	/**
	 * Compile the for statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileFor($expression)
	{
		return "<?php for{$expression}: ?>";
	}

	/**
	 * Compile the foreach statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileForeach($expression)
	{
		return "<?php foreach{$expression}: ?>";
	}

	/**
	 * Compile the forelse statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileForelse($expression)
	{
		$empty = '$__empty_' . ++$this->forelseCounter;

		return "<?php {$empty} = true; foreach{$expression}: {$empty} = false; ?>";
	}

	/**
	 * Compile the if statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileIf($expression)
	{
		return "<?php if{$expression}: ?>";
	}

	/**
	 * Compile the else-if statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileElseif($expression)
	{
		return "<?php elseif{$expression}: ?>";
	}

	/**
	 * Compile the forelse statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEmpty($expression)
	{
		$empty = '$__empty_' . $this->forelseCounter--;

		return "<?php endforeach; if ({$empty}): ?>";
	}

	/**
	 * Compile the while statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileWhile($expression)
	{
		return "<?php while{$expression}: ?>";
	}

	/**
	 * Compile the end-while statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndwhile($expression)
	{
		return "<?php endwhile; ?>";
	}

	/**
	 * Compile the end-for statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndfor($expression)
	{
		return "<?php endfor; ?>";
	}

	/**
	 * Compile the end-for-each statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndforeach($expression)
	{
		return "<?php endforeach; ?>";
	}

	/**
	 * Compile the end-if statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndif($expression)
	{
		return "<?php endif; ?>";
	}

	/**
	 * Compile the end-for-else statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndforelse($expression)
	{
		return "<?php endif; ?>";
	}

	/**
	 * Compile the extends statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileExtends($expression)
	{
		if (starts_with($expression, '('))
		{
			$expression = substr($expression, 1, -1);
		}

		$data = "<?php echo \$this->loadAnyTemplate($expression); ?>";

		$this->footer[] = $data;

		return '';
	}

	/**
	 * Compile the include statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileInclude($expression)
	{
		if (starts_with($expression, '('))
		{
			$expression = substr($expression, 1, -1);
		}

		return "<?php echo \$this->loadAnyTemplate($expression); ?>";
	}

	/**
	 * Compile the jlayout statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileJlayout($expression)
	{
		if (starts_with($expression, '('))
		{
			$expression = substr($expression, 1, -1);
		}

		return "<?php echo \\FOF30\\Layout\\LayoutHelper::render(\$this->container, $expression); ?>";
	}

	/**
	 * Compile the stack statements into the content
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileStack($expression)
	{
		return "<?php echo \$this->yieldContent{$expression}; ?>";
	}

	/**
	 * Compile the push statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compilePush($expression)
	{
		return "<?php \$this->startSection{$expression}; ?>";
	}

	/**
	 * Compile the endpush statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEndpush($expression)
	{
		return "<?php \$this->appendSection(); ?>";
	}

	/**
	 * Compile the route statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileRoute($expression)
	{
		return "<?php echo \$this->container->template->route{$expression}; ?>";
	}

	/**
	 * Compile the css statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileCss($expression)
	{
		return "<?php \$this->addCssFile{$expression}; ?>";
	}

	/**
	 * Compile the inlineCss statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileInlineCss($expression)
	{
		return "<?php \$this->addCssInline{$expression}; ?>";
	}

	/**
	 * Compile the inlineJs statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileInlineJs($expression)
	{
		return "<?php \$this->addJavascriptInline{$expression}; ?>";
	}

	/**
	 * Compile the js statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileJs($expression)
	{
		return "<?php \$this->addJavascriptFile{$expression}; ?>";
	}

	/**
	 * Compile the jhtml statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileJhtml($expression)
	{
		return "<?php echo \\Joomla\\CMS\\HTML\\HTMLHelper::_{$expression}; ?>";
	}

	/**
	 * Compile the `sortgrid` statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	protected function compileSortgrid($expression)
	{
		return "<?php echo \\FOF30\\Utils\\FEFHelper\\BrowseView::sortGrid{$expression} ?>";
	}

	/**
	 * Compile the `fieldtitle` statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	protected function compileFieldtitle($expression)
	{
		return "<?php echo \\FOF30\\Utils\\FEFHelper\\BrowseView::fieldLabel{$expression} ?>";
	}

	/**
	 * Compile the `modelfilter($localField, [$modelTitleField, $modelName, $placeholder, $params])` statements into
	 * valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	protected function compileModelfilter($expression)
	{
		return "<?php echo \\FOF30\\Utils\\FEFHelper\\BrowseView::modelFilter{$expression} ?>";
	}

	/**
	 * Compile the `selectfilter($localField, $options [, $placeholder, $params])` statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	protected function compileSelectfilter($expression)
	{
		return "<?php echo \\FOF30\\Utils\\FEFHelper\\BrowseView::selectFilter{$expression} ?>";
	}

	/**
	 * Compile the `searchfilter($localField, $searchField = null, $placeholder = null, array $attributes = [])`
	 * statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	protected function compileSearchfilter($expression)
	{
		return "<?php echo \\FOF30\\Utils\\FEFHelper\\BrowseView::searchFilter{$expression} ?>";
	}

	/**
	 * Compile the media statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileMedia($expression)
	{
		return "<?php echo \$this->container->template->parsePath{$expression}; ?>";
	}

	/**
	 * Compile the modules statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileModules($expression)
	{
		return "<?php echo \$this->container->template->loadPosition{$expression}; ?>";
	}

	/**
	 * Compile the module statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileModule($expression)
	{
		return "<?php echo \$this->container->template->loadModule{$expression}; ?>";
	}

	/**
	 * Compile the editor statements into valid PHP.
	 *
	 * @param   string  $expression
	 *
	 * @return string
	 */
	protected function compileEditor($expression)
	{
		return '<?php echo \\Joomla\\CMS\\Editor\\Editor::getInstance($this->container->platform->getConfig()->get(\'editor\', \'tinymce\'))' .
			'->display' . $expression . '; ?>';
	}

	/**
	 * Gets the tags used for the compiler.
	 *
	 * @param   bool  $escaped
	 *
	 * @return array
	 */
	protected function getTags($escaped = false)
	{
		$tags = $escaped ? $this->escapedTags : $this->contentTags;

		return array_map('stripcslashes', $tags);
	}
}
View/ViewTemplateFinder.php000060400000040102152455514240011725 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Locates the appropriate template file for a view
 */
class ViewTemplateFinder
{
	/** @var  View  The view we are attached to */
	protected $view;

	/** @var  Container  The container of the view, for quick reference */
	protected $container;

	/** @var  array  The layout template extensions to look for */
	protected $extensions = ['.blade.php', '.php'];

	/** @var  string  Default layout's name (default: "default") */
	protected $defaultLayout = 'default';

	/** @var  string  Default subtemplate name (default: empty) */
	protected $defaultTpl = '';

	/** @var  bool  Should I only look in the specified view (true) or also the pluralised/singularised (false) */
	protected $strictView = true;

	/** @var  bool  Should I only look for the defined subtemplate or also no subtemplate? */
	protected $strictTpl = true;

	/** @var  bool  Should  Should I only look for this layout or also the default layout? */
	protected $strictLayout = true;

	/** @var  string  Which application side prefix should I use by default (site, admin, auto, any) */
	protected $sidePrefix = 'auto';

	/**
	 * Public constructor. The config array can contain the following keys
	 * extensions       array
	 * defaultLayout    string
	 * defaultTpl       string
	 * strictView       bool
	 * strictTpl        bool
	 * strictLayout     bool
	 * sidePrefix       string
	 * For the descriptions of each key please see the same-named property of this class
	 *
	 * @param   View   $view    The view we are attached to
	 * @param   array  $config  The configuration for this view template finder
	 */
	function __construct(View $view, array $config = [])
	{
		$this->view      = $view;
		$this->container = $view->getContainer();

		if (isset($config['extensions']))
		{
			if (!is_array($config['extensions']))
			{
				$config['extensions'] = trim($config['extensions']);
				$config['extensions'] = explode(',', $config['extensions']);
				$config['extensions'] = array_map(function ($x) {
					return trim($x);
				}, $config['extensions']);
			}

			$this->setExtensions($config['extensions']);
		}

		if (isset($config['defaultLayout']))
		{
			$this->setDefaultLayout($config['defaultLayout']);
		}

		if (isset($config['defaultTpl']))
		{
			$this->setDefaultTpl($config['defaultTpl']);
		}

		if (isset($config['strictView']))
		{
			$config['strictView'] = in_array($config['strictView'], [true, 'true', 'yes', 'on', 1]);

			$this->setStrictView($config['strictView']);
		}

		if (isset($config['strictTpl']))
		{
			$config['strictTpl'] = in_array($config['strictTpl'], [true, 'true', 'yes', 'on', 1]);

			$this->setStrictTpl($config['strictTpl']);
		}

		if (isset($config['strictLayout']))
		{
			$config['strictLayout'] = in_array($config['strictLayout'], [true, 'true', 'yes', 'on', 1]);

			$this->setStrictLayout($config['strictLayout']);
		}

		if (isset($config['sidePrefix']))
		{
			$this->setSidePrefix($config['sidePrefix']);
		}
	}

	/**
	 * Returns a list of template URIs for a specific component, view, template and sub-template. The $parameters array
	 * can have any of the following keys:
	 * component        string  The name of the component, e.g. com_something
	 * view             string  The name of the view
	 * layout           string  The name of the layout
	 * tpl              string  The name of the subtemplate
	 * strictView       bool    Should I only look in the specified view, or should I look in the
	 * pluralised/singularised view as well? strictLayout     bool    Should I only look for this layout, or also for
	 * the default layout? strictTpl        bool    Should I only look for this subtemplate or also for no subtemplate?
	 * sidePrefix       string  The application side prefix (site, admin, auto, any)
	 *
	 * @param   array  $parameters  See above
	 *
	 * @return  array
	 */
	public function getViewTemplateUris(array $parameters)
	{
		// Merge the default parameters with the parameters given
		$parameters = array_merge([
			'component'    => $this->container->componentName,
			'view'         => $this->view->getName(),
			'layout'       => $this->defaultLayout,
			'tpl'          => $this->defaultTpl,
			'strictView'   => $this->strictView,
			'strictLayout' => $this->strictLayout,
			'strictTpl'    => $this->strictTpl,
			'sidePrefix'   => $this->sidePrefix,
		], $parameters);

		$uris = [];

		$component    = $parameters['component'];
		$view         = $parameters['view'];
		$layout       = $parameters['layout'];
		$tpl          = $parameters['tpl'];
		$strictView   = $parameters['strictView'];
		$strictLayout = $parameters['strictLayout'];
		$strictTpl    = $parameters['strictTpl'];
		$sidePrefix   = $parameters['sidePrefix'];

		$basePath = $sidePrefix . ':' . $component . '/' . $view . '/';

		$uris[] = $basePath . $layout . ($tpl ? "_$tpl" : '');

		if (!$strictTpl)
		{
			$uris[] = $basePath . $layout;
		}

		if (!$strictLayout)
		{
			$uris[] = $basePath . 'default' . ($tpl ? "_$tpl" : '');

			if (!$strictTpl)
			{
				$uris[] = $basePath . 'default';
			}
		}

		if (!$strictView)
		{
			$parameters['view']       = $this->container->inflector->isSingular($view) ? $this->container->inflector->pluralize($view) : $this->container->inflector->singularize($view);
			$parameters['strictView'] = true;

			$extraUris = $this->getViewTemplateUris($parameters);
			$uris      = array_merge($uris, $extraUris);
			unset ($extraUris);
		}

		return array_unique($uris);
	}

	/**
	 * Parses a template URI in the form of admin:component/view/layout to an array listing the application section
	 * (admin, site), component, view and template referenced therein.
	 *
	 * @param   string  $uri  The template path to parse
	 *
	 * @return  array  A hash array with the parsed path parts. Keys: admin, component, view, template
	 */
	public function parseTemplateUri($uri = '')
	{
		$parts = [
			'admin'     => 0,
			'component' => $this->container->componentName,
			'view'      => $this->view->getName(),
			'template'  => 'default',
		];

		if (substr($uri, 0, 5) == 'auto:')
		{
			$replacement = $this->container->platform->isBackend() ? 'admin:' : 'site:';
			$uri         = $replacement . substr($uri, 5);
		}

		if (substr($uri, 0, 6) == 'admin:')
		{
			$parts['admin'] = 1;
			$uri            = substr($uri, 6);
		}
		elseif (substr($uri, 0, 5) == 'site:')
		{
			$uri = substr($uri, 5);
		}
		elseif (substr($uri, 0, 4) == 'any:')
		{
			$parts['admin'] = -1;
			$uri            = substr($uri, 4);
		}

		if (empty($uri))
		{
			return $parts;
		}

		$uriParts  = explode('/', $uri, 3);
		$partCount = count($uriParts);

		if ($partCount >= 1)
		{
			$parts['component'] = $uriParts[0];
		}

		if ($partCount >= 2)
		{
			$parts['view'] = $uriParts[1];
		}

		if ($partCount >= 3)
		{
			$parts['template'] = $uriParts[2];
		}

		return $parts;
	}

	/**
	 * Resolves a view template URI (e.g. any:com_foobar/Items/cheese) to an absolute filesystem path
	 * (e.g. /var/www/html/administrator/components/com_foobar/View/Items/tmpl/cheese.php)
	 *
	 * @param   string  $uri             The view template URI to parse
	 * @param   string  $layoutTemplate  The layout template override of the View class
	 * @param   array   $extraPaths      Any extra lookup paths where we'll be looking for this view template
	 * @param   bool    $noOverride      If true we will not load Joomla! template overrides
	 *
	 * @return  string
	 *
	 * @throws RuntimeException
	 */
	public function resolveUriToPath($uri, $layoutTemplate = '', array $extraPaths = [], $noOverride = false)
	{
		// Parse the URI into its parts
		$parts = $this->parseTemplateUri($uri);

		// Get some useful values
		$isAdmin        = $this->container->platform->isBackend() ? 1 : 0;
		$componentPaths = $this->container->platform->getComponentBaseDirs($parts['component']);
		$templatePath   = $this->container->platform->getTemplateOverridePath($parts['component']);

		// Get the lookup paths
		$paths = [];

		// If we are on the correct side of the application or we have an "any:" URI look for a template override
		if (!$noOverride && (($parts['admin'] == -1) || ($parts['admin'] == $isAdmin)))
		{
			$paths[] = $templatePath . '/' . $parts['view'];
		}

		// Add the requested side of the application
		$requestedAdmin = ($parts['admin'] == -1) ? $isAdmin : $parts['admin'];

		$paths[] = ($requestedAdmin ? $componentPaths['admin'] : $componentPaths['site']) . '/ViewTemplates/' . $parts['view'];
		$paths[] = ($requestedAdmin ? $componentPaths['admin'] : $componentPaths['site']) . '/View/' . $parts['view'] . '/tmpl';

		// Add the other side of the application for "any:" URIs
		if ($parts['admin'] == -1)
		{
			$paths[] = ($requestedAdmin ? $componentPaths['site'] : $componentPaths['admin']) . '/ViewTemplates/' . $parts['view'];
			$paths[] = ($requestedAdmin ? $componentPaths['site'] : $componentPaths['admin']) . '/View/' . $parts['view'] . '/tmpl';
		}

		// Add extra paths
		if (!empty($extraPaths))
		{
			$paths = array_merge($paths, $extraPaths);
		}

		// Remove duplicate paths
		$paths = array_unique($paths);

		// Look for a template layout override
		if (!empty($layoutTemplate) && ($layoutTemplate != '_') && ($layoutTemplate != $parts['template']))
		{
			$apath = array_shift($paths);
			array_unshift($paths, str_replace($parts['template'], $layoutTemplate, $apath));
		}

		// Get the Joomla! version template suffixes
		$jVersionSuffixes = array_merge($this->container->platform->getTemplateSuffixes(), ['']);

		// Get the renderer name suffixes
		$rendererNameSuffixes = [
			'.' . $this->container->renderer->getInformation()->name,
			'',
		];

		$filesystem = $this->container->filesystem;

		foreach ($this->extensions as $extension)
		{
			foreach ($jVersionSuffixes as $JVersionSuffix)
			{
				foreach ($rendererNameSuffixes as $rendererNameSuffix)
				{
					$filenameToFind = $parts['template'] . $JVersionSuffix . $rendererNameSuffix . $extension;

					$fileName = $filesystem->pathFind($paths, $filenameToFind);

					if ($fileName)
					{
						return $fileName;
					}
				}
			}
		}

		/**
		 * If no view template was found for the component fall back to FOF's core Blade templates -- located in
		 * <libdir>/ViewTemplates/<viewName>/<templateName> -- and their template overrides.
		 */
		$paths   = [];
		$paths[] = $this->container->platform->getTemplateOverridePath('lib_fof30') . '/' . $parts['view'];
		$paths[] = realpath(__DIR__ . '/..') . '/ViewTemplates/' . $parts['view'];

		foreach ($jVersionSuffixes as $JVersionSuffix)
		{
			foreach ($rendererNameSuffixes as $rendererNameSuffix)
			{
				$filenameToFind = $parts['template'] . $JVersionSuffix . $rendererNameSuffix . '.blade.php';

				$fileName = $filesystem->pathFind($paths, $filenameToFind);

				if (!empty($fileName))
				{
					return $fileName;
				}
			}
		}

		throw new RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_LAYOUTFILE_NOT_FOUND', $uri), 500);
	}

	/**
	 * Get the list of view template extensions
	 *
	 * @return  array
	 */
	public function getExtensions()
	{
		return $this->extensions;
	}

	/**
	 * Set the list of view template extensions
	 *
	 * @param   array  $extensions
	 *
	 * @return  void
	 */
	public function setExtensions(array $extensions)
	{
		$this->extensions = $extensions;
	}

	/**
	 * Add an extension to the list of view template extensions
	 *
	 * @param   string  $extension
	 *
	 * @return  void
	 */
	public function addExtension($extension)
	{
		if (empty($extension))
		{
			return;
		}

		if (substr($extension, 0, 1) != '.')
		{
			$extension = '.' . $extension;
		}

		if (!in_array($extension, $this->extensions))
		{
			$this->extensions[] = $extension;
		}
	}

	/**
	 * Remove an extension from the list of view template extensions
	 *
	 * @param   string  $extension
	 *
	 * @return  void
	 */
	public function removeExtension($extension)
	{
		if (empty($extension))
		{
			return;
		}

		if (substr($extension, 0, 1) != '.')
		{
			$extension = '.' . $extension;
		}

		if (!in_array($extension, $this->extensions))
		{
			return;
		}

		$pos = array_search($extension, $this->extensions);
		unset ($this->extensions[$pos]);
	}

	/**
	 * Returns the default layout name
	 *
	 * @return  string
	 */
	public function getDefaultLayout()
	{
		return $this->defaultLayout;
	}

	/**
	 * Sets the default layout name
	 *
	 * @param   string  $defaultLayout
	 *
	 * @return  void
	 */
	public function setDefaultLayout($defaultLayout)
	{
		$this->defaultLayout = $defaultLayout;
	}

	/**
	 * Returns the default subtemplate name
	 *
	 * @return  string
	 */
	public function getDefaultTpl()
	{
		return $this->defaultTpl;
	}

	/**
	 * Sets the default subtemplate name
	 *
	 * @param   string  $defaultTpl
	 */
	public function setDefaultTpl($defaultTpl)
	{
		$this->defaultTpl = $defaultTpl;
	}

	/**
	 * Returns the "strict view" flag. When the flag is false we will look for the view template in both the
	 * singularised and pluralised view. If it's true we will only look for the view template in the view
	 * specified in getViewTemplateUris.
	 *
	 * @return  boolean
	 */
	public function isStrictView()
	{
		return $this->strictView;
	}

	/**
	 * Sets the "strict view" flag. When the flag is false we will look for the view template in both the
	 * singularised and pluralised view. If it's true we will only look for the view template in the view
	 * specified in getViewTemplateUris.
	 *
	 * @param   boolean  $strictView
	 *
	 * @return  void
	 */
	public function setStrictView($strictView)
	{
		$this->strictView = $strictView;
	}

	/**
	 * Returns the "strict template" flag. When the flag is false we will look for a view template with or without the
	 * subtemplate defined in getViewTemplateUris. If it's true we will only look for the subtemplate specified.
	 *
	 * @return boolean
	 */
	public function isStrictTpl()
	{
		return $this->strictTpl;
	}

	/**
	 * Sets the "strict template" flag. When the flag is false we will look for a view template with or without the
	 * subtemplate defined in getViewTemplateUris. If it's true we will only look for the subtemplate specified.
	 *
	 * @param   boolean  $strictTpl
	 *
	 * @return  void
	 */
	public function setStrictTpl($strictTpl)
	{
		$this->strictTpl = $strictTpl;
	}

	/**
	 * Returns the "strict layout" flag. When the flag is false we will look for a view template with both the specified
	 * and the default template name in getViewTemplateUris. When true we will only look for the specified view
	 * template.
	 *
	 * @return  boolean
	 */
	public function isStrictLayout()
	{
		return $this->strictLayout;
	}

	/**
	 * Sets the "strict layout" flag. When the flag is false we will look for a view template with both the specified
	 * and the default template name in getViewTemplateUris. When true we will only look for the specified view
	 * template.
	 *
	 * @param   boolean  $strictLayout
	 *
	 * @return  void
	 */
	public function setStrictLayout($strictLayout)
	{
		$this->strictLayout = $strictLayout;
	}

	/**
	 * Returns the application side prefix which will be used by default in getViewTemplateUris. It can be one of:
	 * site     Public front-end
	 * admin    Administrator back-end
	 * auto     Automatically figure out if it should be site or admin
	 * any      First look in the current application side, then look on the other side of the application
	 *
	 * @return  string
	 */
	public function getSidePrefix()
	{
		return $this->sidePrefix;
	}

	/**
	 * Sets the application side prefix which will be used by default in getViewTemplateUris. It can be one of:
	 * site     Public front-end
	 * admin    Administrator back-end
	 * auto     Automatically figure out if it should be site or admin
	 * any      First look in the current application side, then look on the other side of the application
	 *
	 * @param   string  $sidePrefix
	 *
	 * @return  void
	 */
	public function setSidePrefix($sidePrefix)
	{
		$sidePrefix = strtolower($sidePrefix);
		$sidePrefix = trim($sidePrefix);

		if (!in_array($sidePrefix, ['site', 'admin', 'auto', 'any']))
		{
			$sidePrefix = 'auto';
		}

		$this->sidePrefix = $sidePrefix;
	}


}
View/View.php000060400000103630152455514240007107 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View;

defined('_JEXEC') || die;

use ErrorException;
use Exception;
use FOF30\Container\Container;
use FOF30\Input\Input;
use FOF30\Model\Model;
use FOF30\View\Engine\EngineInterface;
use FOF30\View\Engine\PhpEngine;
use FOF30\View\Exception\CannotGetName;
use FOF30\View\Exception\EmptyStack;
use FOF30\View\Exception\ModelNotFound;
use FOF30\View\Exception\UnrecognisedExtension;
use Joomla\CMS\Language\Text;

/**
 * Class View
 *
 * A generic MVC view implementation
 *
 * @property-read  Input $input  The input object (magic __get returns the Input from the Container)
 */
class View
{
	public $baseurl = null;
	/**
	 * Current or most recently performed task.
	 * Currently public, it should be reduced to protected in the future
	 *
	 * @var  string
	 */
	public $task;
	/**
	 * The mapped task that was performed.
	 * Currently public, it should be reduced to protected in the future
	 *
	 * @var  string
	 */
	public $doTask;
	/**
	 * The name of the view
	 *
	 * @var    array
	 */
	protected $name = null;
	/**
	 * Registered models
	 *
	 * @var    array
	 */
	protected $modelInstances = [];
	/**
	 * The default model
	 *
	 * @var    string
	 */
	protected $defaultModel = null;
	/**
	 * Layout name
	 *
	 * @var    string
	 */
	protected $layout = 'default';
	/**
	 * Layout template
	 *
	 * @var    string
	 */
	protected $layoutTemplate = '_';
	/**
	 * The set of search directories for view templates
	 *
	 * @var   array
	 */
	protected $templatePaths = [];
	/**
	 * The name of the default template source file.
	 *
	 * @var   string
	 */
	protected $template = null;
	/**
	 * The output of the template script.
	 *
	 * @var   string
	 */
	protected $output = null;
	/**
	 * A cached copy of the configuration
	 *
	 * @var   array
	 */
	protected $config = [];
	/**
	 * The container attached to this view
	 *
	 * @var   Container
	 */
	protected $container;
	/**
	 * The object used to locate view templates in the filesystem
	 *
	 * @var   ViewTemplateFinder
	 */
	protected $viewFinder = null;
	/**
	 * Used when loading template files to avoid variable scope issues
	 *
	 * @var   null
	 */
	protected $_tempFilePath = null;
	/**
	 * Should I run the pre-render step?
	 *
	 * @var    boolean
	 */
	protected $doPreRender = true;

	/**
	 * Should I run the post-render step?
	 *
	 * @var    boolean
	 */
	protected $doPostRender = true;

	/**
	 * Maps view template extensions to view engine classes
	 *
	 * @var    array
	 */
	protected $viewEngineMap = [
		'.blade.php' => 'FOF30\\View\\Engine\\BladeEngine',
		'.php'       => 'FOF30\\View\\Engine\\PhpEngine',
	];

	/**
	 * All of the finished, captured sections.
	 *
	 * @var array
	 */
	protected $sections = [];

	/**
	 * The stack of in-progress sections.
	 *
	 * @var array
	 */
	protected $sectionStack = [];

	/**
	 * The number of active rendering operations.
	 *
	 * @var int
	 */
	protected $renderCount = 0;

	/**
	 * Aliases of view templates. For example:
	 *
	 * array('userProfile' => 'site://com_foobar/users/profile')
	 *
	 * allows you to do something like $this->loadAnyTemplate('userProfile') to display the frontend view template
	 * site://com_foobar/users/profile. You can also alias one view template with another, e.g.
	 * 'site://com_something/users/profile' => 'admin://com_foobar/clients/record'
	 *
	 * @var  array
	 */
	protected $viewTemplateAliases = [];

	/**
	 * Constructor.
	 *
	 * The $config array can contain the following overrides:
	 * name           string  The name of the view (defaults to the view class name)
	 * template_path  string  The path of the layout directory
	 * layout         string  The layout for displaying the view
	 * viewFinder     ViewTemplateFinder  The object used to locate view templates in the filesystem
	 * viewEngineMap  array   Maps view template extensions to view engine classes
	 *
	 * @param   Container  $container  The container we belong to
	 * @param   array      $config     The configuration overrides for the view
	 *
	 * @return  View
	 */
	public function __construct(Container $container, array $config = [])
	{
		$this->container = $container;

		$this->config = $config;

		// Get the view name
		if (isset($this->config['name']))
		{
			$this->name = $this->config['name'];
		}

		$this->name = $this->getName();

		// Set the default template search path
		if (array_key_exists('template_path', $this->config))
		{
			// User-defined dirs
			$this->setTemplatePath($this->config['template_path']);
		}
		else
		{
			$this->setTemplatePath($this->container->thisPath . '/View/' . ucfirst($this->name) . '/tmpl');
		}

		// Set the layout
		if (array_key_exists('layout', $this->config))
		{
			$this->setLayout($this->config['layout']);
		}

		// Apply the viewEngineMap
		if (isset($config['viewEngineMap']))
		{
			// If the overrides are a string convert it to an array first.
			if (!is_array($config['viewEngineMap']))
			{
				$temp                    = explode(',', $config['viewEngineMap']);
				$config['viewEngineMap'] = [];

				foreach ($temp as $assignment)
				{
					$parts = explode('=>', $assignment, 2);

					if (count($parts) != 2)
					{
						continue;
					}

					$parts = array_map(function ($x) {
						return trim($x);
					}, $parts);

					$config['viewEngineMap'][$parts[0]] = $parts[1];
				}
			}

			/**
			 * We want to always have a sane fallback to plain .php template files at the end of the view engine stack.
			 * For this to happen to we need to remove it from the current stack, disallow overriding it in the
			 * viewEngineMap overrides, merge the overrides and then add the fallback at the very end of the stack.
			 *
			 * In previous versions we didn't do that which had two side effects:
			 * 1. It was no longer a fallback, it was in the middle of the stack
			 * 2. Any new template engines using a .something.php extension wouldn't work, see
			 *    https://github.com/akeeba/fof/issues/694
			 */
			// Do not allow overriding the fallback .php handler
			if (isset($config['viewEngineMap']['.php']))
			{
				unset ($config['viewEngineMap']['.php']);
			}

			// Temporarily remove the fallback .php handler
			$phpHandler = $this->viewEngineMap['.php'] ?? PhpEngine::class;
			unset ($this->viewEngineMap['.php']);

			// Add the overrides
			$this->viewEngineMap = array_merge($this->viewEngineMap, $config['viewEngineMap']);

			// Push the fallback .php handler to the end of the view engine map stack
			$this->viewEngineMap = array_merge($this->viewEngineMap, [
				'.php' => $phpHandler
			]);
		}

		// Set the ViewFinder
		$this->viewFinder = $this->container->factory->viewFinder($this);

		if (isset($config['viewFinder']) && !empty($config['viewFinder']) && is_object($config['viewFinder']) && ($config['viewFinder'] instanceof ViewTemplateFinder))
		{
			$this->viewFinder = $config['viewFinder'];
		}

		// Apply the registered view template extensions to the view finder
		$this->viewFinder->setExtensions(array_keys($this->viewEngineMap));

		// Apply the base URL
		$this->baseurl = $this->container->platform->URIbase();
	}

	/**
	 * Magic get method. Handles magic properties:
	 * $this->input  mapped to $this->container->input
	 *
	 * @param   string  $name  The property to fetch
	 *
	 * @return  mixed|null
	 */
	public function __get($name)
	{
		// Handle $this->input
		if ($name == 'input')
		{
			return $this->container->input;
		}

		// Property not found; raise error
		$trace = debug_backtrace();
		trigger_error(
			'Undefined property via __get(): ' . $name .
			' in ' . $trace[0]['file'] .
			' on line ' . $trace[0]['line'],
			E_USER_NOTICE);

		return null;
	}

	/**
	 * Method to get the view name
	 *
	 * The model name by default parsed using the classname, or it can be set
	 * by passing a $config['name'] in the class constructor
	 *
	 * @return  string  The name of the model
	 *
	 * @throws  Exception
	 */
	public function getName()
	{
		if (empty($this->name))
		{
			$r = null;

			if (!preg_match('/(.*)\\\\View\\\\(.*)\\\\(.*)/i', get_class($this), $r))
			{
				throw new CannotGetName;
			}

			$this->name = $r[2];
		}

		return $this->name;
	}

	/**
	 * Escapes a value for output in a view script.
	 *
	 * @param   mixed  $var  The output to escape.
	 *
	 * @return  mixed  The escaped value.
	 */
	public function escape($var)
	{
		return htmlspecialchars($var, ENT_COMPAT, 'UTF-8');
	}

	/**
	 * Method to get data from a registered model or a property of the view
	 *
	 * @param   string  $property   The name of the method to call on the Model or the property to get
	 * @param   string  $default    The default value [optional]
	 * @param   string  $modelName  The name of the Model to reference [optional]
	 *
	 * @return  mixed  The return value of the method
	 */
	public function get($property, $default = null, $modelName = null)
	{
		// If $model is null we use the default model
		if (is_null($modelName))
		{
			$model = $this->defaultModel;
		}
		else
		{
			$model = $modelName;
		}

		// First check to make sure the model requested exists
		if (isset($this->modelInstances[$model]))
		{
			// Model exists, let's build the method name
			$method = 'get' . ucfirst($property);

			// Does the method exist?
			if (method_exists($this->modelInstances[$model], $method))
			{
				// The method exists, let's call it and return what we get
				$result = $this->modelInstances[$model]->$method();

				return $result;
			}
			else
			{
				$result = $this->modelInstances[$model]->$property();

				if (is_null($result))
				{
					return $default;
				}

				return $result;
			}
		}
		// If the model doesn't exist, try to fetch a View property
		else
		{
			if (@isset($this->$property))
			{
				return $this->$property;
			}
			else
			{
				return $default;
			}
		}
	}

	/**
	 * Returns a named Model object
	 *
	 * @param   string  $name    The Model name. If null we'll use the modelName
	 *                           variable or, if it's empty, the same name as
	 *                           the Controller
	 *
	 * @return  Model  The instance of the Model known to this Controller
	 */
	public function getModel($name = null)
	{
		if (!empty($name))
		{
			$modelName = $name;
		}
		elseif (!empty($this->defaultModel))
		{
			$modelName = $this->defaultModel;
		}
		else
		{
			$modelName = $this->name;
		}

		if (!array_key_exists($modelName, $this->modelInstances))
		{
			throw new ModelNotFound($modelName, $this->name);
		}

		return $this->modelInstances[$modelName];
	}

	/**
	 * Pushes the default Model to the View
	 *
	 * @param   Model  $model  The model to push
	 */
	public function setDefaultModel(Model &$model)
	{
		$name = $model->getName();

		$this->setDefaultModelName($name);
		$this->setModel($this->defaultModel, $model);
	}

	/**
	 * Set the name of the Model to be used by this View
	 *
	 * @param   string  $modelName  The name of the Model
	 *
	 * @return  void
	 */
	public function setDefaultModelName($modelName)
	{
		$this->defaultModel = $modelName;
	}

	/**
	 * Pushes a named model to the View
	 *
	 * @param   string  $modelName  The name of the Model
	 * @param   Model   $model      The actual Model object to push
	 *
	 * @return  void
	 */
	public function setModel($modelName, Model &$model)
	{
		$this->modelInstances[$modelName] = $model;
	}

	/**
	 * Overrides the default method to execute and display a template script.
	 * Instead of loadTemplate is uses loadAnyTemplate.
	 *
	 * @param   string  $tpl  The name of the template file to parse
	 *
	 * @return  boolean  True on success
	 *
	 * @throws  Exception  When the layout file is not found
	 */
	public function display($tpl = null)
	{
		$eventName = 'onBefore' . ucfirst($this->doTask);
		$this->triggerEvent($eventName, [$tpl]);

		$preRenderResult = '';

		if ($this->doPreRender)
		{
			@ob_start();
			$this->preRender();
			$preRenderResult = @ob_get_contents();
			@ob_end_clean();
		}

		$templateResult = $this->loadTemplate($tpl);

		$eventName = 'onAfter' . ucfirst($this->doTask);
		$this->triggerEvent($eventName, [$tpl]);

		if (is_object($templateResult) && ($templateResult instanceof Exception))
		{
			throw $templateResult;
		}

		echo $preRenderResult . $templateResult;

		if ($this->doPostRender)
		{
			$this->postRender();
		}

		return true;
	}

	/**
	 * Get the layout.
	 *
	 * @return  string  The layout name
	 */
	public function getLayout()
	{
		return $this->layout;
	}

	/**
	 * Sets the layout name to use
	 *
	 * @param   string  $layout  The layout name or a string in format <template>:<layout file>
	 *
	 * @return  string  Previous value.
	 */
	public function setLayout($layout)
	{
		$previous = $this->layout;

		if (is_null($layout))
		{
			$layout = 'default';
		}

		if (strpos($layout, ':') === false)
		{
			$this->layout = $layout;
		}
		else
		{
			// Convert parameter to array based on :
			$temp         = explode(':', $layout);
			$this->layout = $temp[1];

			// Set layout template
			$this->layoutTemplate = $temp[0];
		}

		return $previous;
	}

	/**
	 * Our function uses loadAnyTemplate to provide smarter view template loading.
	 *
	 * @param   string   $tpl     The name of the template file to parse
	 * @param   boolean  $strict  Should we use strict naming, i.e. force a non-empty $tpl?
	 *
	 * @return  mixed  A string if successful, otherwise an Exception
	 */
	public function loadTemplate($tpl = null, $strict = false)
	{
		$result = '';

		$uris = $this->viewFinder->getViewTemplateUris([
			'component' => $this->container->componentName,
			'view'      => $this->getName(),
			'layout'    => $this->getLayout(),
			'tpl'       => $tpl,
			'strictTpl' => $strict,
		]);

		foreach ($uris as $uri)
		{
			try
			{
				$result = $this->loadAnyTemplate($uri);

				break;
			}
			catch (Exception $e)
			{
				$result = $e;
			}
		}

		if ($result instanceof Exception)
		{
			$this->container->platform->raiseError($result->getCode(), $result->getMessage());
		}

		return $result;
	}

	/**
	 * Loads a template given any path. The path is in the format componentPart://componentName/viewName/layoutName,
	 * for example
	 * site:com_example/items/default
	 * admin:com_example/items/default_subtemplate
	 * auto:com_example/things/chair
	 * any:com_example/invoices/printpreview
	 *
	 * @param   string    $uri          The template path
	 * @param   array     $forceParams  A hash array of variables to be extracted in the local scope of the template
	 *                                  file
	 * @param   callable  $callback     A method to post-process the 3ναluα+3d view template (I use leetspeak here
	 *                                  because of bad quality hosts with broken scanners)
	 * @param   bool      $noOverride   If true we will not load Joomla! template overrides
	 *
	 * @return  string  The output of the template
	 *
	 * @throws  Exception  When the layout file is not found
	 */
	public function loadAnyTemplate($uri = '', $forceParams = [], $callback = null, $noOverride = false)
	{
		if (isset($this->viewTemplateAliases[$uri]))
		{
			$uri = $this->viewTemplateAliases[$uri];
		}

		$layoutTemplate = $this->getLayoutTemplate();

		$extraPaths = [];

		if (!empty($this->templatePaths))
		{
			$extraPaths = $this->templatePaths;
		}

		// First get the raw view template path
		$path = $this->viewFinder->resolveUriToPath($uri, $layoutTemplate, $extraPaths, $noOverride);

		// Now get the parsed view template path
		$this->_tempFilePath = $this->getEngine($path)->get($path, $forceParams);

		// We will keep track of the amount of views being rendered so we can flush
		// the section after the complete rendering operation is done. This will
		// clear out the sections for any separate views that may be rendered.
		$this->incrementRender();

		// Get the processed template
		$contents = $this->processTemplate($forceParams);

		// Once we've finished rendering the view, we'll decrement the render count
		// so that each sections get flushed out next time a view is created and
		// no old sections are staying around in the memory of an environment.
		$this->decrementRender();

		$response = isset($callback) ? $callback($this, $contents) : null;

		if (!is_null($response))
		{
			$contents = $response;
		}

		// Once we have the contents of the view, we will flush the sections if we are
		// done rendering all views so that there is nothing left hanging over when
		// another view gets rendered in the future by the application developer.
		$this->flushSectionsIfDoneRendering();

		return $contents;
	}

	/**
	 * Increment the rendering counter.
	 *
	 * @return void
	 */
	public function incrementRender()
	{
		$this->renderCount++;
	}

	/**
	 * Decrement the rendering counter.
	 *
	 * @return void
	 */
	public function decrementRender()
	{
		$this->renderCount--;
	}

	/**
	 * Check if there are no active render operations.
	 *
	 * @return bool
	 */
	public function doneRendering()
	{
		return $this->renderCount == 0;
	}

	/**
	 * Go through a data array and render a subtemplate against each record (think master-detail views). This is
	 * accessible through Blade templates as @each
	 *
	 * @param   string  $viewTemplate  The view template to use for each subitem, format
	 *                                 componentPart://componentName/viewName/layoutName
	 * @param   array   $data          The array of data you want to render. It can be a DataModel\Collection, array,
	 *                                 ...
	 * @param   string  $eachItemName  How to call each item in the loaded subtemplate (passed through $forceParams)
	 * @param   string  $empty         What to display if the array is empty
	 *
	 * @return string
	 */
	public function renderEach($viewTemplate, $data, $eachItemName, $empty = 'raw|')
	{
		$result = '';

		// If is actually data in the array, we will loop through the data and append
		// an instance of the partial view to the final result HTML passing in the
		// iterated value of this data array, allowing the views to access them.
		if (count($data) > 0)
		{
			foreach ($data as $key => $value)
			{
				$data = ['key' => $key, $eachItemName => $value];

				$result .= $this->loadAnyTemplate($viewTemplate, $data);
			}
		}
		// If there is no data in the array, we will render the contents of the empty
		// view. Alternatively, the "empty view" could be a raw string that begins
		// with "raw|" for convenience and to let this know that it is a string. Or
		// a language string starting with text|.
		else
		{
			if (starts_with($empty, 'raw|'))
			{
				$result = substr($empty, 4);
			}
			elseif (starts_with($empty, 'text|'))
			{
				$result = Text::_(substr($empty, 5));
			}
			else
			{
				$result = $this->loadAnyTemplate($empty);
			}
		}

		return $result;
	}

	/**
	 * Start injecting content into a section.
	 *
	 * @param   string  $section
	 * @param   string  $content
	 *
	 * @return void
	 */
	public function startSection($section, $content = '')
	{
		if ($content === '')
		{
			if (ob_start())
			{
				$this->sectionStack[] = $section;
			}
		}
		else
		{
			$this->extendSection($section, $content);
		}
	}

	/**
	 * Stop injecting content into a section and return its contents.
	 *
	 * @return string
	 */
	public function yieldSection()
	{
		return $this->yieldContent($this->stopSection());
	}

	/**
	 * Stop injecting content into a section.
	 *
	 * @param   bool  $overwrite
	 *
	 * @return string
	 */
	public function stopSection($overwrite = false)
	{
		if (empty($this->sectionStack))
		{
			// Let's close the output buffering
			ob_get_clean();

			throw new EmptyStack();
		}

		$last = array_pop($this->sectionStack);

		if ($overwrite)
		{
			$this->sections[$last] = ob_get_clean();
		}
		else
		{
			$this->extendSection($last, ob_get_clean());
		}

		return $last;
	}

	/**
	 * Stop injecting content into a section and append it.
	 *
	 * @return string
	 */
	public function appendSection()
	{
		if (empty($this->sectionStack))
		{
			// Let's close the output buffering
			ob_get_clean();

			throw new EmptyStack();
		}

		$last = array_pop($this->sectionStack);

		if (isset($this->sections[$last]))
		{
			$this->sections[$last] .= ob_get_clean();
		}
		else
		{
			$this->sections[$last] = ob_get_clean();
		}

		return $last;
	}

	/**
	 * Get the string contents of a section.
	 *
	 * @param   string  $section
	 * @param   string  $default
	 *
	 * @return string
	 */
	public function yieldContent($section, $default = '')
	{
		$sectionContent = $default;

		if (isset($this->sections[$section]))
		{
			$sectionContent = $this->sections[$section];
		}

		return str_replace('@parent', '', $sectionContent);
	}

	/**
	 * Flush all of the section contents.
	 *
	 * @return void
	 */
	public function flushSections()
	{
		$this->sections = [];

		$this->sectionStack = [];
	}

	/**
	 * Flush all of the section contents if done rendering.
	 *
	 * @return void
	 */
	public function flushSectionsIfDoneRendering()
	{
		if ($this->doneRendering())
		{
			$this->flushSections();
		}
	}

	/**
	 * Get the layout template.
	 *
	 * @return  string  The layout template name
	 */
	public function getLayoutTemplate()
	{
		return $this->layoutTemplate;
	}

	/**
	 * Load a helper file
	 *
	 * @param   string  $helperClass   The last part of the name of the helper
	 *                                 class.
	 *
	 * @return  void
	 *
	 * @deprecated  3.0  Just use the class in your code. That's what the autoloader is for.
	 */
	public function loadHelper($helperClass = null)
	{
		// Get the helper class name
		$className = '\\' . $this->container->getNamespacePrefix() . 'Helper\\' . ucfirst($helperClass);

		// This trick autoloads the helper class. We can't instantiate it as
		// helpers are (supposed to be) abstract classes with static method
		// interfaces.
		class_exists($className);
	}

	/**
	 * Returns a reference to the container attached to this View
	 *
	 * @return Container
	 */
	public function &getContainer()
	{
		return $this->container;
	}

	public function getTask()
	{
		return $this->task;
	}

	/**
	 * @param   string  $task
	 *
	 * @return  $this   This for chaining
	 */
	public function setTask($task)
	{
		$this->task = $task;

		return $this;
	}

	public function getDoTask()
	{
		return $this->doTask;
	}

	/**
	 * @param   string  $task
	 *
	 * @return  $this   This for chaining
	 */
	public function setDoTask($task)
	{
		$this->doTask = $task;

		return $this;
	}

	/**
	 * Sets the pre-render flag
	 *
	 * @param   boolean  $value  True to enable the pre-render step
	 *
	 * @return  void
	 */
	public function setPreRender($value)
	{
		$this->doPreRender = $value;
	}

	/**
	 * Sets the post-render flag
	 *
	 * @param   boolean  $value  True to enable the post-render step
	 *
	 * @return  void
	 */
	public function setPostRender($value)
	{
		$this->doPostRender = $value;
	}

	/**
	 * Add an alias for a view template.
	 *
	 * @param   string  $viewTemplate  Existing view template, in the format
	 *                                 componentPart://componentName/viewName/layoutName
	 * @param   string  $alias         The alias of the view template (any string will do)
	 *
	 * @return void
	 */
	public function alias($viewTemplate, $alias)
	{
		$this->viewTemplateAliases[$alias] = $viewTemplate;
	}

	/**
	 * Add a JS script file to the page generated by the CMS.
	 *
	 * There are three combinations of defer and async (see http://www.w3schools.com/tags/att_script_defer.asp):
	 * * $defer false, $async true: The script is executed asynchronously with the rest of the page
	 *   (the script will be executed while the page continues the parsing)
	 * * $defer true, $async false: The script is executed when the page has finished parsing.
	 * * $defer false, $async false. (default) The script is loaded and executed immediately. When it finishes
	 *   loading the browser continues parsing the rest of the page.
	 *
	 * When you are using $defer = true there is no guarantee about the load order of the scripts. Whichever
	 * script loads first will be executed first. The order they appear on the page is completely irrelevant.
	 *
	 * @param   string   $uri      A path definition understood by parsePath, e.g. media://com_example/js/foo.js
	 * @param   string   $version  (optional) Version string to be added to the URL
	 * @param   string   $type     MIME type of the script
	 * @param   boolean  $defer    Adds the defer attribute, see above
	 * @param   boolean  $async    Adds the async attribute, see above
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addJavascriptFile($uri, $version = null, $type = 'text/javascript', $defer = false, $async = false)
	{
		// Add an automatic version if $version is null. For no version parameter pass an empty string to $version.
		if (is_null($version))
		{
			$version = $this->container->mediaVersion;
		}

		$this->container->template->addJS($uri, $defer, $async, $version, $type);

		return $this;
	}

	/**
	 * Adds an inline JavaScript script to the page header
	 *
	 * @param   string  $script  The script content to add
	 * @param   string  $type    The MIME type of the script
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addJavascriptInline($script, $type = 'text/javascript')
	{
		$this->container->template->addJSInline($script, $type);

		return $this;
	}

	/**
	 * Add a CSS file to the page generated by the CMS
	 *
	 * @param   string  $uri      A path definition understood by parsePath, e.g. media://com_example/css/foo.css
	 * @param   string  $version  (optional) Version string to be added to the URL
	 * @param   string  $type     MIME type of the stylesheeet
	 * @param   string  $media    Media target definition of the style sheet, e.g. "screen"
	 * @param   array   $attribs  Array of attributes
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addCssFile($uri, $version = null, $type = 'text/css', $media = null, $attribs = [])
	{
		// Add an automatic version if $version is null. For no version parameter pass an empty string to $version.
		if (is_null($version))
		{
			$version = $this->container->mediaVersion;
		}

		$this->container->template->addCSS($uri, $version, $type, $media, $attribs);

		return $this;
	}

	/**
	 * Adds an inline stylesheet (inline CSS) to the page header
	 *
	 * @param   string  $css   The stylesheet content to add
	 * @param   string  $type  The MIME type of the script
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addCssInline($css, $type = 'text/css')
	{
		$this->container->template->addCSSInline($css, $type);

		return $this;
	}

	/**
	 * Sets an entire array of search paths for templates or resources.
	 *
	 * @param   mixed  $path  The new search path, or an array of search paths.  If null or false, resets to the current
	 *                        directory only.
	 *
	 * @return  void
	 */
	protected function setTemplatePath($path)
	{
		// Clear out the prior search dirs
		$this->templatePaths = [];

		// Actually add the user-specified directories
		$this->addTemplatePath($path);

		// Set the alternative template search dir
		$templatePath = JPATH_THEMES;
		$fallback     = $templatePath . '/' . $this->container->platform->getTemplate() . '/html/' . $this->container->componentName . '/' . $this->name;
		$this->addTemplatePath($fallback);

		// Get extra directories through event dispatchers
		$extraPathsResults = $this->container->platform->runPlugins('onGetViewTemplatePaths', [
			$this->container->componentName,
			$this->getName(),
		]);

		if (is_array($extraPathsResults) && !empty($extraPathsResults))
		{
			foreach ($extraPathsResults as $somePaths)
			{
				if (!empty($somePaths))
				{
					foreach ($somePaths as $aPath)
					{
						$this->addTemplatePath($aPath);
					}
				}
			}
		}
	}

	/**
	 * Adds to the search path for templates and resources.
	 *
	 * @param   mixed  $path  The directory or stream, or an array of either, to search.
	 *
	 * @return  void
	 */
	protected function addTemplatePath($path)
	{
		// Just force to array
		$path = (array) $path;

		// Loop through the path directories
		foreach ($path as $dir)
		{
			// No surrounding spaces allowed!
			$dir = trim($dir);

			// Add trailing separators as needed
			if (substr($dir, -1) != DIRECTORY_SEPARATOR)
			{
				// Directory
				$dir .= DIRECTORY_SEPARATOR;
			}

			// Add to the top of the search dirs
			array_unshift($this->templatePaths, $dir);
		}
	}

	/**
	 * Append content to a given section.
	 *
	 * @param   string  $section
	 * @param   string  $content
	 *
	 * @return void
	 */
	protected function extendSection($section, $content)
	{
		if (isset($this->sections[$section]))
		{
			$content = str_replace('@parent', $content, $this->sections[$section]);
		}

		$this->sections[$section] = $content;
	}

	/**
	 * Evaluates the template described in the _tempFilePath property
	 *
	 * @param   array  $forceParams  Forced parameters
	 *
	 * @return string
	 * @throws Exception
	 */
	protected function processTemplate(array &$forceParams)
	{
		// If the engine returned raw content, return the raw content immediately
		if ($this->_tempFilePath['type'] == 'raw')
		{
			return $this->_tempFilePath['content'];
		}

		if (substr($this->_tempFilePath['content'], 0, 4) == 'raw|')
		{
			return substr($this->_tempFilePath['content'], 4);
		}

		$obLevel = ob_get_level();

		ob_start();

		// We'll process the contents of the view inside a try/catch block so we can
		// flush out any stray output that might get out before an error occurs or
		// an exception is thrown. This prevents any partial views from leaking.
		try
		{
			$this->includeTemplateFile($forceParams);
		}
		catch (Exception $e)
		{
			$this->handleViewException($e, $obLevel);
		}

		return ob_get_clean();
	}

	/**
	 * Handle a view exception.
	 *
	 * @param   Exception  $e        The exception to handle
	 * @param   int        $obLevel  The target output buffering level
	 *
	 * @return  void
	 *
	 * @throws  $e
	 */
	protected function handleViewException(Exception $e, $obLevel)
	{
		while (ob_get_level() > $obLevel)
		{
			ob_end_clean();
		}

		$message = $e->getMessage() . ' (View template: ' . realpath($this->_tempFilePath['content']) . ')';

		$newException = new ErrorException($message, 0, 1, $e->getFile(), $e->getLine(), $e);

		throw $newException;
	}

	/**
	 * Get the appropriate view engine for the given view template path.
	 *
	 * @param   string  $path  The path of the view template
	 *
	 * @return  EngineInterface
	 *
	 * @throws  UnrecognisedExtension
	 */
	protected function getEngine($path)
	{
		foreach ($this->viewEngineMap as $extension => $engine)
		{
			if (substr($path, -strlen($extension)) == $extension)
			{
				return new $engine($this);
			}
		}

		throw new UnrecognisedExtension($path);
	}

	/**
	 * Get the extension used by the view file.
	 *
	 * @param   string  $path
	 *
	 * @return string
	 */
	protected function getExtension($path)
	{
		$extensions = array_keys($this->viewEngineMap);

		return array_first($extensions, function ($key, $value) use ($path) {
			return ends_with($path, $value);
		});
	}

	/**
	 * Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
	 * Joomla! plugin system. A true/false return value is expected. The first false return cancels the event.
	 *
	 * EXAMPLE
	 * Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456)
	 * The event calls:
	 * 1. $this->onBeforeSomething(123, 456)
	 * 2. Joomla! plugin event onComFoobarViewItemBeforeSomething($this, 123, 456)
	 *
	 * @param   string  $event      The name of the event, typically named onPredicateVerb e.g. onBeforeKick
	 * @param   array   $arguments  The arguments to pass to the event handlers
	 *
	 * @return  bool
	 */
	protected function triggerEvent($event, array $arguments = [])
	{
		$result = true;

		// If there is an object method for this event, call it
		if (method_exists($this, $event))
		{
			switch (count($arguments))
			{
				case 0:
					$result = $this->{$event}();
					break;
				case 1:
					$result = $this->{$event}($arguments[0]);
					break;
				case 2:
					$result = $this->{$event}($arguments[0], $arguments[1]);
					break;
				case 3:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2]);
					break;
				case 4:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
					break;
				case 5:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
					break;
				default:
					$result = call_user_func_array([$this, $event], $arguments);
					break;
			}
		}

		if ($result === false)
		{
			return false;
		}

		// All other event handlers live outside this object, therefore they need to be passed a reference to this
		// objects as the first argument.
		array_unshift($arguments, $this);

		// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
		$prefix = '';

		if (substr($event, 0, 2) == 'on')
		{
			$prefix = 'on';
			$event  = substr($event, 2);
		}

		// Get the component/model prefix for the event
		$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'View';
		$prefix .= ucfirst($this->getName());

		// The event name will be something like onComFoobarItemsBeforeSomething
		$event = $prefix . $event;

		// Call the Joomla! plugins
		$results = $this->container->platform->runPlugins($event, $arguments);

		if (!empty($results))
		{
			foreach ($results as $result)
			{
				if ($result === false)
				{
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Runs before rendering the view template, echoing HTML to put before the
	 * view template's generated HTML
	 *
	 * @return void
	 */
	protected function preRender()
	{
		// You need to implement this in children classes
	}

	/**
	 * Runs after rendering the view template, echoing HTML to put after the
	 * view template's generated HTML
	 *
	 * @return  void
	 */
	protected function postRender()
	{
		// You need to implement this in children classes
	}

	/**
	 * This method makes sure the current scope isn't polluted with variables when including a view template
	 *
	 * @param   array  $forceParams  Forced parameters
	 *
	 * @return  void
	 */
	private function includeTemplateFile(array &$forceParams)
	{
		// Extract forced parameters
		if (!empty($forceParams))
		{
			extract($forceParams);
		}

		include $this->_tempFilePath['content'];
	}
}
View/DataView/Csv.php000060400000013452152455514240010436 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\DataView;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Model\DataModel;
use FOF30\View\Exception\AccessForbidden;
use Joomla\CMS\Document\Document;

class Csv extends Html implements DataViewInterface
{
	/**
	 *  Should I produce a CSV header row.
	 *
	 * @var  boolean
	 */
	protected $csvHeader = true;

	/**
	 * The filename of the downloaded CSV file.
	 *
	 * @var  string
	 */
	protected $csvFilename = null;

	/**
	 * The columns to include in the CSV output. If it's empty it will be ignored.
	 *
	 * @var  array
	 */
	protected $csvFields = [];


	/**
	 * Public constructor. Instantiates a F0FViewCsv object.
	 *
	 *
	 * @param   Container  $container  The container we belong to
	 * @param   array      $config     The configuration overrides for the view
	 */
	public function __construct(Container $container, array $config = [])
	{
		parent::__construct($container, $config);

		if (array_key_exists('csv_header', $config))
		{
			$this->csvHeader = $config['csv_header'];
		}
		else
		{
			$this->csvHeader = $this->input->getBool('csv_header', true);
		}

		if (array_key_exists('csv_filename', $config))
		{
			$this->csvFilename = $config['csv_filename'];
		}
		else
		{
			$this->csvFilename = $this->input->getString('csv_filename', '');
		}

		if (empty($this->csvFilename))
		{
			$view              = $this->input->getCmd('view', 'cpanel');
			$view              = $this->container->inflector->pluralize($view);
			$this->csvFilename = strtolower($view) . '.csv';
		}

		if (array_key_exists('csv_fields', $config))
		{
			$this->csvFields = $config['csv_fields'];
		}
	}

	/**
	 * Overrides the default method to execute and display a template script.
	 * Instead of loadTemplate is uses loadAnyTemplate.
	 *
	 * @param   string  $tpl  The name of the template file to parse
	 *
	 * @return  boolean  True on success
	 *
	 * @throws  Exception  When the layout file is not found
	 */
	public function display($tpl = null)
	{
		$eventName = 'onBefore' . ucfirst($this->doTask);
		$this->triggerEvent($eventName, [$tpl]);

		// Load the model
		/** @var DataModel $model */
		$model = $this->getModel();

		$items       = $model->get();
		$this->items = $items;

		$platform = $this->container->platform;
		$document = $platform->getDocument();

		if ($document instanceof Document)
		{
			$document->setMimeEncoding('text/csv');
		}

		$platform->setHeader('Pragma', 'public');
		$platform->setHeader('Expires', '0');

		/**
		 * This construct is required to work around bad quality hosts who blacklist files based on broken malware
		 * scanners. The only way to beat them is... wait for it... write our software using the same obscure constructs
		 * actual malware is using to evade these broken malware scanners. The irony is not lost on me.
		 */
		$xo   = substr("revenge", 0, 3);
		$xoxo = substr("calibrate", 1, 2);
		$platform->setHeader('Cache-Control', 'must-' . $xo . $xoxo . 'idate, post-check=0, pre-check=0');

		$platform->setHeader('Cache-Control', 'public', false);
		$platform->setHeader('Content-Description', 'File Transfer');
		$platform->setHeader('Content-Disposition', 'attachment; filename="' . $this->csvFilename . '"');

		if (is_null($tpl))
		{
			$tpl = 'csv';
		}

		$hasFailed = false;

		try
		{
			$result = $this->loadTemplate($tpl, true);

			if ($result instanceof Exception)
			{
				$hasFailed = true;
			}
		}
		catch (Exception $e)
		{
			$hasFailed = true;
		}

		if (!$hasFailed)
		{
			echo $result;
		}
		else
		{
			// Default CSV behaviour in case the template isn't there!
			if (count($items) === 0)
			{
				throw new AccessForbidden;
			}

			$item = $items->last();
			$keys = $item->getData();
			$keys = array_keys($keys);

			reset($items);

			if (!empty($this->csvFields))
			{
				$temp = [];

				foreach ($this->csvFields as $f)
				{
					$exist = false;

					// If we have a dot and it isn't part of the field name, we are dealing with relations
					if (!$model->hasField($f) && strpos($f, '.'))
					{
						$methods = explode('.', $f);
						$object  = $item;
						// Let's see if the relation exists
						foreach ($methods as $method)
						{
							if (isset($object->$method) || property_exists($object, $method))
							{
								$exist  = true;
								$object = $object->$method;
							}
							else
							{
								$exist = false;
								break;
							}
						}
					}

					if (in_array($f, $keys))
					{
						$temp[] = $f;
					}
					elseif ($exist)
					{
						$temp[] = $f;
					}
				}

				$keys = $temp;
			}

			if ($this->csvHeader)
			{
				$csv = [];

				foreach ($keys as $k)
				{
					$k = str_replace('"', '""', $k);
					$k = str_replace("\r", '\\r', $k);
					$k = str_replace("\n", '\\n', $k);
					$k = '"' . $k . '"';

					$csv[] = $k;
				}

				echo implode(",", $csv) . "\r\n";
			}

			foreach ($items as $item)
			{
				$csv = [];

				foreach ($keys as $k)
				{
					// If our key contains a dot and it isn't part of the field name, we are dealing with relations
					if (!$model->hasField($k) && strpos($k, '.'))
					{
						$methods = explode('.', $k);
						$v       = $item;

						foreach ($methods as $method)
						{
							$v = $v->$method;
						}
					}
					else
					{
						$v = $item->$k;
					}

					if (is_array($v))
					{
						$v = 'Array';
					}
					elseif (is_object($v))
					{
						$v = 'Object';
					}

					$v = str_replace('"', '""', $v);
					$v = str_replace("\r", '\\r', $v);
					$v = str_replace("\n", '\\n', $v);
					$v = '"' . $v . '"';

					$csv[] = $v;
				}

				echo implode(",", $csv) . "\r\n";
			}
		}

		$eventName = 'onAfter' . ucfirst($this->doTask);
		$this->triggerEvent($eventName, [$tpl]);

		return true;
	}
}
View/DataView/Raw.php000060400000023547152455514240010442 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\DataView;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Collection;
use FOF30\View\View;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Pagination\Pagination;
use Joomla\Registry\Registry;
use stdClass;

/**
 * View for a raw data-driven view
 */
class Raw extends View implements DataViewInterface
{
	/** @var   stdClass  Data lists */
	protected $lists = null;

	/** @var Pagination The pagination object */
	protected $pagination = null;

	/** @var Registry|Registry Page parameters object, for front-end views */
	protected $pageParams = null;

	/** @var Collection The records loaded (browse views) */
	protected $items = null;

	/** @var DataModel The record loaded (read, edit, add views) */
	protected $item = null;

	/** @var int The total number of items in the model (more than those loaded) */
	protected $itemCount = 0;

	/** @var stdClass ACL permissions map */
	protected $permissions = null;

	/** @var array Additional permissions to fetch on object creation, see getPermissions() */
	protected $additionalPermissions = [];

	/**
	 * Overrides the constructor to apply Joomla! ACL permissions
	 *
	 * @param   Container  $container  The container we belong to
	 * @param   array      $config     The configuration overrides for the view
	 */
	public function __construct(Container $container, array $config = [])
	{
		parent::__construct($container, $config);

		$this->permissions = $this->getPermissions(null, $this->additionalPermissions);
	}

	/**
	 * Determines if the current Joomla! version and your current table support AJAX-powered drag and drop reordering.
	 * If they do, it will set up the drag & drop reordering feature.
	 *
	 * @return  boolean|array  False if not supported, otherwise a table with necessary information (saveOrder: should
	 *                           you enable DnD reordering; orderingColumn: which column has the ordering information).
	 */
	public function hasAjaxOrderingSupport()
	{
		/** @var DataModel $model */
		$model = $this->getModel();

		if (!$model->hasField('ordering'))
		{
			return false;
		}

		$listOrder       = $this->escape($model->getState('filter_order', null, 'cmd'));
		$listDirn        = $this->escape($model->getState('filter_order_Dir', null, 'cmd'));
		$saveOrder       = $listOrder == $model->getFieldAlias('ordering');
		$saveOrderingUrl = '';

		if ($saveOrder)
		{
			$saveOrderingUrl = 'index.php?option=' . $this->container->componentName . '&view=' . $this->getName() . '&task=saveorder&format=json';
			$helper          = version_compare(JVERSION, '3.999.999', 'le') ? 'sortablelist.sortable' : 'draggablelist.draggable';

			HtmlHelper::_($helper, 'itemsList', 'adminForm', strtolower($listDirn), $saveOrderingUrl);
		}

		return [
			'saveOrder'      => $saveOrder,
			'saveOrderURL'   => $saveOrderingUrl . '&' . $this->container->platform->getToken() . '=1',
			'orderingColumn' => $model->getFieldAlias('ordering'),
		];
	}

	/**
	 * Returns the internal list of useful variables to the benefit of header fields.
	 *
	 * @return stdClass
	 */
	public function getLists()
	{
		return $this->lists;
	}

	/**
	 * Returns a reference to the permissions object of this view
	 *
	 * @return stdClass
	 */
	public function getPerms()
	{
		return $this->permissions;
	}

	/**
	 * Returns a reference to the pagination object of this view
	 *
	 * @return Pagination
	 */
	public function getPagination()
	{
		return $this->pagination;
	}

	/**
	 * Get the items collection for browse views
	 *
	 * @return Collection
	 */
	public function getItems()
	{
		return $this->items;
	}

	/**
	 * Get the item for read, edit, add views
	 *
	 * @return DataModel
	 */
	public function getItem()
	{
		return $this->item;
	}

	/**
	 * Get the items count for browse views
	 *
	 * @return int
	 */
	public function getItemCount()
	{
		return $this->itemCount;
	}

	/**
	 * Get the Joomla! page parameters
	 *
	 * @return Registry|Registry
	 */
	public function getPageParams()
	{
		return $this->pageParams;
	}

	/**
	 * Returns a permissions object.
	 *
	 * The additionalPermissions array is a hashed array of local key => Joomla! ACL key value pairs. Local key is the
	 * name of the permission in the permissions object, whereas Joomla! ACL key is the name of the ACL permission
	 * known to Joomla! e.g. "core.manage", "foobar.something" and so on.
	 *
	 * Note: on CLI applications all permissions are set to TRUE. There is no ACL check there.
	 *
	 * @param   null|string  $component              The name of the component. Leave empty for automatic detection.
	 * @param   array        $additionalPermissions  Any additional permissions you want to add to the object.
	 *
	 * @return  object
	 */
	protected function getPermissions($component = null, array $additionalPermissions = [])
	{
		// Make sure we have a component
		if (empty($component))
		{
			$component = $this->container->componentName;
		}

		// Initialise with all true
		$permissions = [
			'create'    => true,
			'edit'      => true,
			'editown'   => true,
			'editstate' => true,
			'delete'    => true,
		];

		if (!empty($additionalPermissions))
		{
			foreach ($additionalPermissions as $localKey => $joomlaPermission)
			{
				$permissions[$localKey] = true;
			}
		}

		$platform = $this->container->platform;

		// If this is a CLI application we don't make any ACL checks
		if ($platform->isCli())
		{
			return (object) $permissions;
		}

		// Get the core permissions
		$permissions = [
			'create'    => $platform->authorise('core.create', $component),
			'edit'      => $platform->authorise('core.edit', $component),
			'editown'   => $platform->authorise('core.edit.own', $component),
			'editstate' => $platform->authorise('core.edit.state', $component),
			'delete'    => $platform->authorise('core.delete', $component),
		];

		if (!empty($additionalPermissions))
		{
			foreach ($additionalPermissions as $localKey => $joomlaPermission)
			{
				$permissions[$localKey] = $platform->authorise($joomlaPermission, $component);
			}
		}

		return (object) $permissions;
	}

	/**
	 * Executes before rendering the page for the Browse task.
	 */
	protected function onBeforeBrowse()
	{
		// Create the lists object
		$this->lists = new stdClass();

		// Load the model
		/** @var DataModel $model */
		$model = $this->getModel();

		// We want to persist the state in the session
		$model->savestate(1);

		// Display limits
		$defaultLimit = 20;

		if (!$this->container->platform->isCli() && class_exists('JFactory'))
		{
			$app = Factory::getApplication();

			if (method_exists($app, 'get'))
			{
				$defaultLimit = $app->get('list_limit');
			}
			else
			{
				$defaultLimit = 20;
			}
		}

		$this->lists->limitStart = $model->getState('limitstart', 0, 'int');
		$this->lists->limit      = $model->getState('limit', $defaultLimit, 'int');

		$model->limitstart = $this->lists->limitStart;
		$model->limit      = $this->lists->limit;

		// Assign items to the view
		$this->items     = $model->get(false);
		$this->itemCount = $model->count();

		// Ordering information
		$this->lists->order     = $model->getState('filter_order', $model->getIdFieldName(), 'cmd');
		$this->lists->order_Dir = $model->getState('filter_order_Dir', null, 'cmd');

		if ($this->lists->order_Dir)
		{
			$this->lists->order_Dir = strtolower($this->lists->order_Dir);
		}

		// Pagination
		$this->pagination = new Pagination($this->itemCount, $this->lists->limitStart, $this->lists->limit);

		// Pass page params on frontend only
		if ($this->container->platform->isFrontend())
		{
			/** @var SiteApplication $app */
			$app              = Factory::getApplication();
			$params           = $app->getParams();
			$this->pageParams = $params;
		}
	}

	/**
	 * Executes before rendering the page for the add task.
	 */
	protected function onBeforeAdd()
	{
		/** @var DataModel $model */
		$model = $this->getModel();

		/**
		 * The model is pushed into the View by the Controller. As you can see in DataController::add() it is possible
		 * to push both default values (defaultsForAdd) as well as data from the state (e.g. when saving a new record
		 * failed for some reason and the user needs to edit it). That's why we populate defaultFields from $model. We
		 * still do a full reset on a clone of the Model to get a clean object and merge default values (instead of null
		 * values) with the data pushed by the controller.
		 */
		$defaultFields = $model->getData();
		$this->item    = $model->getClone()->reset(true, true);

		foreach ($defaultFields as $k => $v)
		{
			try
			{
				$this->item->setFieldValue($k, $v);
			}
			catch (Exception $e)
			{
				// Suppress errors in field assignments at this stage
			}
		}
	}

	/**
	 * Executes before rendering the page for the Edit task.
	 */
	protected function onBeforeEdit()
	{
		/** @var DataModel $model */
		$model = $this->getModel();

		// It seems that I can't edit records, maybe I can edit only this one due asset tracking?
		if (!$this->permissions->edit || !$this->permissions->editown)
		{
			if ($model)
			{
				// Ok, record is tracked, let's see if I can this record
				if ($model->isAssetsTracked())
				{
					$platform = $this->container->platform;

					if (!$this->permissions->edit)
					{
						$this->permissions->edit = $platform->authorise('core.edit', $model->getAssetName());
					}

					if (!$this->permissions->editown)
					{
						$this->permissions->editown = $platform->authorise('core.edit.own', $model->getAssetName());
					}
				}
			}
		}

		$this->item = $model->findOrFail();
	}

	/**
	 * Executes before rendering the page for the Read task.
	 */
	protected function onBeforeRead()
	{
		/** @var DataModel $model */
		$model = $this->getModel();

		$this->item = $model->findOrFail();
	}
}
View/DataView/Json.php000060400000012250152455514240010607 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\DataView;

defined('_JEXEC') || die;

use Exception;
use FOF30\Model\DataModel;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Document\JsonDocument as JDocumentJSON;

class Json extends Raw implements DataViewInterface
{
	/**
	 * Set to true if your onBefore* methods have already populated the item, items, limitstart etc properties used to
	 * render a JSON document.
	 *
	 * @var bool
	 */
	public $alreadyLoaded = false;

	/**
	 * Record listing offset (how many records to skip before starting showing some)
	 *
	 * @var   int
	 */
	protected $limitStart = 0;

	/**
	 * Record listing limit (how many records to show)
	 *
	 * @var   int
	 */
	protected $limit = 10;

	/**
	 * Total number of records in the result set
	 *
	 * @var   int
	 */
	protected $total = 0;

	/**
	 * The record being displayed
	 *
	 * @var   DataModel
	 */
	protected $item = null;

	/**
	 * Overrides the default method to execute and display a template script.
	 * Instead of loadTemplate is uses loadAnyTemplate.
	 *
	 * @param   string  $tpl  The name of the template file to parse
	 *
	 * @return  boolean  True on success
	 *
	 * @throws  Exception  When the layout file is not found
	 */
	public function display($tpl = null)
	{
		$eventName = 'onBefore' . ucfirst($this->doTask);
		$this->triggerEvent($eventName, [$tpl]);

		$eventName = 'onAfter' . ucfirst($this->doTask);
		$this->triggerEvent($eventName, [$tpl]);

		return true;
	}

	/**
	 * The event which runs when we are displaying the record list JSON view
	 *
	 * @param   string  $tpl  The sub-template to use
	 */
	public function onBeforeBrowse($tpl = null)
	{
		// Load the model
		/** @var DataModel $model */
		$model = $this->getModel();

		$result = '';

		if (!$this->alreadyLoaded)
		{
			$this->limitStart = $model->getState('limitstart', 0);
			$this->limit      = $model->getState('limit', 0);
			$this->items      = $model->get(true, $this->limitStart, $this->limit);
			$this->total      = $model->count();
		}

		$document = $this->container->platform->getDocument();

		/** @var JDocumentJSON $document */
		if ($document instanceof Document)
		{
			$document->setMimeEncoding('application/json');
		}

		if (is_null($tpl))
		{
			$tpl = 'json';
		}

		$hasFailed = false;

		try
		{
			$result = $this->loadTemplate($tpl, true);

			if ($result instanceof Exception)
			{
				$hasFailed = true;
			}
		}
		catch (Exception $e)
		{
			$hasFailed = true;
		}

		if ($hasFailed)
		{
			// Default JSON behaviour in case the template isn't there!
			$result = [];

			foreach ($this->items as $item)
			{
				if (is_object($item) && method_exists($item, 'toArray'))
				{
					$result[] = $item->toArray();
				}
				else
				{
					$result[] = $item;
				}
			}

			$json = json_encode($result, JSON_PRETTY_PRINT);

			// JSONP support
			$callback = $this->input->get('callback', null, 'raw');

			if (!empty($callback))
			{
				echo $callback . '(' . $json . ')';
			}
			else
			{
				$defaultName = $this->input->get('view', 'main', 'cmd');
				$filename    = $this->input->get('basename', $defaultName, 'cmd');

				$document->setName($filename);
				echo $json;
			}
		}
		else
		{
			echo $result;
		}
	}

	/**
	 * The event which runs when we are displaying a single item JSON view
	 *
	 * @param   string  $tpl  The view sub-template to use
	 */
	protected function onBeforeRead($tpl = null)
	{
		self::renderSingleItem($tpl);
	}

	/**
	 * The event which runs when we are displaying a single item JSON view
	 *
	 * @param   string  $tpl  The view sub-template to use
	 */
	protected function onAfterSave($tpl = null)
	{
		self::renderSingleItem($tpl);
	}

	/**
	 * Renders a single item JSON view
	 *
	 * @param   string  $tpl  The view sub-template to use
	 */
	protected function renderSingleItem($tpl)
	{
		// Load the model
		/** @var DataModel $model */
		$model = $this->getModel();

		$result = '';

		if (!$this->alreadyLoaded)
		{
			$this->item = $model->find();
		}


		$document = $this->container->platform->getDocument();

		/** @var JDocumentJSON $document */
		if ($document instanceof Document)
		{
			$document->setMimeEncoding('application/json');
		}

		if (is_null($tpl))
		{
			$tpl = 'json';
		}

		$hasFailed = false;

		try
		{
			$result = $this->loadTemplate($tpl, true);

			if ($result instanceof Exception)
			{
				$hasFailed = true;
			}
		}
		catch (Exception $e)
		{
			$hasFailed = true;
		}

		if ($hasFailed)
		{
			// Default JSON behaviour in case the template isn't there!

			if (is_object($this->item) && method_exists($this->item, 'toArray'))
			{
				$data = $this->item->toArray();
			}
			else
			{
				$data = $this->item;
			}

			$json = json_encode($data, JSON_PRETTY_PRINT);

			// JSONP support
			$callback = $this->input->get('callback', null);

			if (!empty($callback))
			{
				echo $callback . '(' . $json . ')';
			}
			else
			{
				$defaultName = $this->input->get('view', 'main', 'cmd');
				$filename    = $this->input->get('basename', $defaultName, 'cmd');
				$document->setName($filename);

				echo $json;
			}
		}
		else
		{
			echo $result;
		}
	}
}
View/DataView/DataViewInterface.php000060400000011041152455514240013220 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\DataView;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use Joomla\CMS\Pagination\Pagination;
use stdClass;

interface DataViewInterface
{
	/**
	 * Determines if the current Joomla! version and your current table support AJAX-powered drag and drop reordering.
	 * If they do, it will set up the drag & drop reordering feature.
	 *
	 * @return  boolean|array  False if not supported, otherwise a table with necessary information (saveOrder: should
	 *                           you enable DnD reordering; orderingColumn: which column has the ordering information).
	 */
	public function hasAjaxOrderingSupport();

	/**
	 * Returns the internal list of useful variables to the benefit of header fields.
	 *
	 * @return stdClass
	 */
	public function getLists();

	/**
	 * Returns a reference to the permissions object of this view
	 *
	 * @return stdClass
	 */
	public function getPerms();

	/**
	 * Returns a reference to the pagination object of this view
	 *
	 * @return Pagination
	 */
	public function getPagination();

	/**
	 * Method to get the view name
	 *
	 * The model name by default parsed using the classname, or it can be set
	 * by passing a $config['name'] in the class constructor
	 *
	 * @return  string  The name of the model
	 *
	 * @throws  Exception
	 */
	public function getName();

	/**
	 * Returns a reference to the container attached to this View
	 *
	 * @return  Container
	 */
	public function &getContainer();

	/**
	 * Escapes a value for output in a view script.
	 *
	 * @param   mixed  $var  The output to escape.
	 *
	 * @return  string  The escaped value.
	 */
	public function escape($var);

	/**
	 * Returns the task being rendered by the view
	 *
	 * @return  string
	 */
	public function getTask();

	/**
	 * Get the layout.
	 *
	 * @return  string  The layout name
	 */
	public function getLayout();

	/**
	 * Add a JS script file to the page generated by the CMS.
	 *
	 * There are three combinations of defer and async (see http://www.w3schools.com/tags/att_script_defer.asp):
	 * * $defer false, $async true: The script is executed asynchronously with the rest of the page
	 *   (the script will be executed while the page continues the parsing)
	 * * $defer true, $async false: The script is executed when the page has finished parsing.
	 * * $defer false, $async false. (default) The script is loaded and executed immediately. When it finishes
	 *   loading the browser continues parsing the rest of the page.
	 *
	 * When you are using $defer = true there is no guarantee about the load order of the scripts. Whichever
	 * script loads first will be executed first. The order they appear on the page is completely irrelevant.
	 *
	 * @param   string   $uri      A path definition understood by parsePath, e.g. media://com_example/js/foo.js
	 * @param   string   $version  (optional) Version string to be added to the URL
	 * @param   string   $type     MIME type of the script
	 * @param   boolean  $defer    Adds the defer attribute, see above
	 * @param   boolean  $async    Adds the async attribute, see above
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addJavascriptFile($uri, $version = null, $type = 'text/javascript', $defer = false, $async = false);

	/**
	 * Adds an inline JavaScript script to the page header
	 *
	 * @param   string  $script  The script content to add
	 * @param   string  $type    The MIME type of the script
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addJavascriptInline($script, $type = 'text/javascript');

	/**
	 * Add a CSS file to the page generated by the CMS
	 *
	 * @param   string  $uri      A path definition understood by parsePath, e.g. media://com_example/css/foo.css
	 * @param   string  $version  (optional) Version string to be added to the URL
	 * @param   string  $type     MIME type of the stylesheeet
	 * @param   string  $media    Media target definition of the style sheet, e.g. "screen"
	 * @param   array   $attribs  Array of attributes
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addCssFile($uri, $version = null, $type = 'text/css', $media = null, $attribs = []);

	/**
	 * Adds an inline stylesheet (inline CSS) to the page header
	 *
	 * @param   string  $css   The stylesheet content to add
	 * @param   string  $type  The MIME type of the script
	 *
	 * @return  $this  Self, for chaining
	 */
	public function addCssInline($css, $type = 'text/css');
}
View/DataView/Html.php000060400000007462152455514240010613 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\View\DataView;

defined('_JEXEC') || die;

use Exception;
use FOF30\Render\RenderInterface;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;

class Html extends Raw implements DataViewInterface
{
	/** @var bool Should I set the page title in the front-end of the site? */
	public $setFrontendPageTitle = false;

	/** @var string The translation key for the default page title */
	public $defaultPageTitle = null;

	public function setPageTitle()
	{
		if (!$this->container->platform->isFrontend())
		{
			return '';
		}

		/** @var SiteApplication $app */
		$app      = Factory::getApplication();
		$document = Factory::getDocument();
		$menus    = $app->getMenu();
		$menu     = $menus->getActive();
		$title    = null;

		// Get the option and view name
		$option = $this->container->componentName;
		$view   = $this->getName();

		// Get the default page title translation key
		$default = empty($this->defaultPageTitle) ? $option . '_TITLE_' . $view : $this->defaultPageTitle;

		$params = $app->getParams($option);

		// Set the default value for page_heading
		if ($menu)
		{
			$params->def('page_heading', $params->get('page_title', $menu->title));
		}
		else
		{
			$params->def('page_heading', Text::_($default));
		}

		// Set the document title
		$title    = $params->get('page_title', '');
		$sitename = $app->get('sitename');

		if ($title == $sitename)
		{
			$title = Text::_($default);
		}

		if (empty($title))
		{
			$title = $sitename;
		}
		elseif ($app->get('sitename_pagetitles', 0) == 1)
		{
			$title = Text::sprintf('JPAGETITLE', $app->get('sitename'), $title);
		}
		elseif ($app->get('sitename_pagetitles', 0) == 2)
		{
			$title = Text::sprintf('JPAGETITLE', $title, $app->get('sitename'));
		}

		$document->setTitle($title);

		// Set meta
		if ($params->get('menu-meta_description'))
		{
			$document->setDescription($params->get('menu-meta_description'));
		}

		if ($params->get('menu-meta_keywords'))
		{
			$document->setMetadata('keywords', $params->get('menu-meta_keywords'));
		}

		if ($params->get('robots'))
		{
			$document->setMetadata('robots', $params->get('robots'));
		}

		return $title;
	}

	/**
	 * Runs before rendering the view template, echoing HTML to put before the
	 * view template's generated HTML
	 *
	 * @return  void
	 *
	 * @throws Exception
	 */
	protected function preRender()
	{
		$view = $this->getName();
		$task = $this->task;

		// Don't load the toolbar on CLI
		$platform = $this->container->platform;

		if (!$platform->isCli())
		{
			$toolbar        = $this->container->toolbar;
			$toolbar->perms = $this->permissions;
			$toolbar->renderToolbar($view, $task);
		}

		if ($platform->isFrontend() && $this->setFrontendPageTitle)
		{
			$this->setPageTitle();
		}

		$renderer = $this->container->renderer;
		$renderer->preRender($view, $task);
	}

	/**
	 * Runs after rendering the view template, echoing HTML to put after the
	 * view template's generated HTML
	 *
	 * @return  void
	 *
	 * @throws Exception
	 */
	protected function postRender()
	{
		$view = $this->getName();
		$task = $this->task;

		$renderer = $this->container->renderer;

		if ($renderer instanceof RenderInterface)
		{
			$renderer->postRender($view, $task);
		}
	}

	/**
	 * Executes before rendering the page for the Add task.
	 */
	protected function onBeforeAdd()
	{
		// Hide main menu
		Factory::getApplication()->input->set('hidemainmenu', true);

		parent::onBeforeAdd();
	}

	/**
	 * Executes before rendering the page for the Edit task.
	 */
	protected function onBeforeEdit()
	{
		// Hide main menu
		Factory::getApplication()->input->set('hidemainmenu', true);

		parent::onBeforeEdit();
	}
}
Platform/FilesystemInterface.php000060400000011715152455514240013016 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Platform;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use UnexpectedValueException;

interface FilesystemInterface
{
	/**
	 * Public constructor.
	 *
	 * @param   Container  $c  The component container
	 */
	public function __construct(Container $c);

	/**
	 * Does the file exists?
	 *
	 * @param   $path  string   Path to the file to test
	 *
	 * @return  bool
	 */
	public function fileExists($path);

	/**
	 * Delete a file or array of files
	 *
	 * @param   mixed  $file  The file name or an array of file names
	 *
	 * @return  boolean  True on success
	 *
	 */
	public function fileDelete($file);

	/**
	 * Copies a file
	 *
	 * @param   string  $src   The path to the source file
	 * @param   string  $dest  The path to the destination file
	 *
	 * @return  boolean  True on success
	 */
	public function fileCopy($src, $dest);

	/**
	 * Write contents to a file
	 *
	 * @param   string    $file    The full file path
	 * @param   string   &$buffer  The buffer to write
	 *
	 * @return  boolean  True on success
	 */
	public function fileWrite($file, &$buffer);

	/**
	 * Checks for snooping outside of the file system root.
	 *
	 * @param   string  $path  A file system path to check.
	 *
	 * @return  string  A cleaned version of the path or exit on error.
	 *
	 * @throws  Exception
	 */
	public function pathCheck($path);

	/**
	 * Function to strip additional / or \ in a path name.
	 *
	 * @param   string  $path  The path to clean.
	 * @param   string  $ds    Directory separator (optional).
	 *
	 * @return  string  The cleaned path.
	 *
	 * @throws  UnexpectedValueException
	 */
	public function pathClean($path, $ds = DIRECTORY_SEPARATOR);

	/**
	 * Searches the directory paths for a given file.
	 *
	 * @param   mixed   $paths  An path string or array of path strings to search in
	 * @param   string  $file   The file name to look for.
	 *
	 * @return  mixed   The full path and file name for the target file, or boolean false if the file is not found in
	 *                  any of the paths.
	 */
	public function pathFind($paths, $file);

	/**
	 * Wrapper for the standard file_exists function
	 *
	 * @param   string  $path  Folder name relative to installation dir
	 *
	 * @return  boolean  True if path is a folder
	 */
	public function folderExists($path);

	/**
	 * Utility function to read the files in a folder.
	 *
	 * @param   string   $path           The path of the folder to read.
	 * @param   string   $filter         A filter for file names.
	 * @param   mixed    $recurse        True to recursively search into sub-folders, or an integer to specify the
	 *                                   maximum depth.
	 * @param   boolean  $full           True to return the full path to the file.
	 * @param   array    $exclude        Array with names of files which should not be shown in the result.
	 * @param   array    $excludefilter  Array of filter to exclude
	 *
	 * @return  array  Files in the given folder.
	 */
	public function folderFiles($path, $filter = '.', $recurse = false, $full = false, $exclude = [
		'.svn', 'CVS', '.DS_Store', '__MACOSX',
	],
	                            $excludefilter = ['^\..*', '.*~']);

	/**
	 * Utility function to read the folders in a folder.
	 *
	 * @param   string   $path           The path of the folder to read.
	 * @param   string   $filter         A filter for folder names.
	 * @param   mixed    $recurse        True to recursively search into sub-folders, or an integer to specify the
	 *                                   maximum depth.
	 * @param   boolean  $full           True to return the full path to the folders.
	 * @param   array    $exclude        Array with names of folders which should not be shown in the result.
	 * @param   array    $excludefilter  Array with regular expressions matching folders which should not be shown in
	 *                                   the result.
	 *
	 * @return  array  Folders in the given folder.
	 */
	public function folderFolders($path, $filter = '.', $recurse = false, $full = false, $exclude = [
		'.svn', 'CVS', '.DS_Store', '__MACOSX',
	],
	                              $excludefilter = ['^\..*']);

	/**
	 * Create a folder -- and all necessary parent folders.
	 *
	 * @param   string   $path  A path to create from the base path.
	 * @param   integer  $mode  Directory permissions to set for folders created. 0755 by default.
	 *
	 * @return  boolean  True if successful.
	 */
	public function folderCreate($path = '', $mode = 0755);

	/**
	 * Gets the extension of a file name
	 *
	 * @param   string  $file  The file name
	 *
	 * @return  string  The file extension
	 */
	public function getExt($file);

	/**
	 * Strips the last extension off of a file name
	 *
	 * @param   string  $file  The file name
	 *
	 * @return  string  The file name without the extension
	 */
	public function stripExt($file);
}
Platform/Base/Platform.php000060400000024071152455514240011506 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Platform\Base;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Input\Input;
use FOF30\Platform\PlatformInterface;
use Joomla\CMS\Document\Document;
use Joomla\CMS\User\User;

/**
 * Abstract implementation of the Platform integration
 *
 * @package FOF30\Platform\Base
 */
abstract class Platform implements PlatformInterface
{
	/** @var  Container  The component container */
	protected $container = null;

	/** @var  bool  Are plugins allowed to run in CLI mode? */
	protected $allowPluginsInCli = false;

	/**
	 * Public constructor.
	 *
	 * @param   Container  $c  The component container
	 */
	public function __construct(Container $c)
	{
		$this->container = $c;
	}

	/**
	 * Returns the base (root) directories for a given component.
	 *
	 * @param   string  $component  The name of the component. For Joomla! this
	 *                              is something like "com_example"
	 *
	 * @return  array  A hash array with keys main, alt, site and admin.
	 * @see F0FPlatformInterface::getComponentBaseDirs()
	 *
	 */
	public function getComponentBaseDirs($component)
	{
		return [
			'main'  => '',
			'alt'   => '',
			'site'  => '',
			'admin' => '',
		];
	}

	/**
	 * Returns the application's template name
	 *
	 * @param   boolean|array  $params  An optional associative array of configuration settings
	 *
	 * @return  string  The template name. System is the fallback.
	 */
	public function getTemplate($params = false)
	{
		return 'system';
	}

	/**
	 * Get application-specific suffixes to use with template paths. This allows
	 * you to look for view template overrides based on the application version.
	 *
	 * @return  array  A plain array of suffixes to try in template names
	 */
	public function getTemplateSuffixes()
	{
		return [];
	}

	/**
	 * Return the absolute path to the application's template overrides
	 * directory for a specific component. We will use it to look for template
	 * files instead of the regular component directories. If the application
	 * does not have such a thing as template overrides return an empty string.
	 *
	 * @param   string   $component  The name of the component for which to fetch the overrides
	 * @param   boolean  $absolute   Should I return an absolute or relative path?
	 *
	 * @return  string  The path to the template overrides directory
	 */
	public function getTemplateOverridePath($component, $absolute = true)
	{
		return '';
	}

	/**
	 * Load the translation files for a given component.
	 *
	 * @param   string  $component  The name of the component. For Joomla! this
	 *                              is something like "com_example"
	 *
	 * @return  void
	 * @see F0FPlatformInterface::loadTranslations()
	 *
	 */
	public function loadTranslations($component)
	{
		return null;
	}

	/**
	 * Authorise access to the component in the back-end.
	 *
	 * @param   string  $component  The name of the component.
	 *
	 * @return  boolean  True to allow loading the component, false to halt loading
	 * @see F0FPlatformInterface::authorizeAdmin()
	 *
	 */
	public function authorizeAdmin($component)
	{
		return true;
	}

	/**
	 * Returns the JUser object for the current user
	 *
	 * @param   integer  $id  The ID of the user to fetch
	 *
	 * @return  Document
	 * @see F0FPlatformInterface::getUser()
	 *
	 */
	public function getUser($id = null)
	{
		return null;
	}

	/**
	 * Returns the JDocument object which handles this component's response.
	 *
	 * @return  Document
	 * @see F0FPlatformInterface::getDocument()
	 *
	 */
	public function getDocument()
	{
		return null;
	}

	/**
	 * This method will try retrieving a variable from the request (input) data.
	 *
	 * @param   string   $key           The user state key for the variable
	 * @param   string   $request       The request variable name for the variable
	 * @param   Input    $input         The Input object with the request (input) data
	 * @param   mixed    $default       The default value. Default: null
	 * @param   string   $type          The filter type for the variable data. Default: none (no filtering)
	 * @param   boolean  $setUserState  Should I set the user state with the fetched value?
	 *
	 * @return  mixed  The value of the variable
	 * @see F0FPlatformInterface::getUserStateFromRequest()
	 *
	 */
	public function getUserStateFromRequest($key, $request, $input, $default = null, $type = 'none', $setUserState = true)
	{
		return $input->get($request, $default, $type);
	}

	/**
	 * Load plugins of a specific type. Obviously this seems to only be required
	 * in the Joomla! CMS.
	 *
	 * @param   string  $type  The type of the plugins to be loaded
	 *
	 * @return void
	 * @see F0FPlatformInterface::importPlugin()
	 *
	 */
	public function importPlugin($type)
	{
	}

	/**
	 * Execute plugins (system-level triggers) and fetch back an array with
	 * their return values.
	 *
	 * @param   string  $event  The event (trigger) name, e.g. onBeforeScratchMyEar
	 * @param   array   $data   A hash array of data sent to the plugins as part of the trigger
	 *
	 * @return  array  A simple array containing the results of the plugins triggered
	 * @see F0FPlatformInterface::runPlugins()
	 *
	 */
	public function runPlugins($event, $data)
	{
		return [];
	}

	/**
	 * Perform an ACL check.
	 *
	 * @param   string  $action     The ACL privilege to check, e.g. core.edit
	 * @param   string  $assetname  The asset name to check, typically the component's name
	 *
	 * @return  boolean  True if the user is allowed this action
	 * @see F0FPlatformInterface::authorise()
	 *
	 */
	public function authorise($action, $assetname)
	{
		return true;
	}

	/**
	 * Is this the administrative section of the component?
	 *
	 * @return  boolean
	 * @see F0FPlatformInterface::isBackend()
	 *
	 */
	public function isBackend()
	{
		return true;
	}

	/**
	 * Is this the public section of the component?
	 *
	 * @return  boolean
	 * @see F0FPlatformInterface::isFrontend()
	 *
	 */
	public function isFrontend()
	{
		return true;
	}

	/**
	 * Is this a component running in a CLI application?
	 *
	 * @return  boolean
	 * @see F0FPlatformInterface::isCli()
	 *
	 */
	public function isCli()
	{
		return true;
	}

	/**
	 * Is AJAX re-ordering supported? This is 100% Joomla!-CMS specific. All
	 * other platforms should return false and never ask why.
	 *
	 * @return  boolean
	 * @see F0FPlatformInterface::supportsAjaxOrdering()
	 *
	 */
	public function supportsAjaxOrdering()
	{
		return true;
	}

	/**
	 * Saves something to the cache. This is supposed to be used for system-wide
	 * F0F data, not application data.
	 *
	 * @param   string  $key      The key of the data to save
	 * @param   string  $content  The actual data to save
	 *
	 * @return  boolean  True on success
	 */
	public function setCache($key, $content)
	{
		return false;
	}

	/**
	 * Retrieves data from the cache. This is supposed to be used for system-side
	 * F0F data, not application data.
	 *
	 * @param   string  $key      The key of the data to retrieve
	 * @param   string  $default  The default value to return if the key is not found or the cache is not populated
	 *
	 * @return  string  The cached value
	 */
	public function getCache($key, $default = null)
	{
		return false;
	}

	/**
	 * Is the global F0F cache enabled?
	 *
	 * @return  boolean
	 */
	public function isGlobalFOFCacheEnabled()
	{
		return true;
	}

	/**
	 * Clears the cache of system-wide F0F data. You are supposed to call this in
	 * your components' installation script post-installation and post-upgrade
	 * methods or whenever you are modifying the structure of database tables
	 * accessed by F0F. Please note that F0F's cache never expires and is not
	 * purged by Joomla!. You MUST use this method to manually purge the cache.
	 *
	 * @return  boolean  True on success
	 */
	public function clearCache()
	{
		return false;
	}

	/**
	 * logs in a user
	 *
	 * @param   array  $authInfo  Authentication information
	 *
	 * @return  boolean  True on success
	 */
	public function loginUser($authInfo)
	{
		return true;
	}

	/**
	 * logs out a user
	 *
	 * @return  boolean  True on success
	 */
	public function logoutUser()
	{
		return true;
	}

	/**
	 * Logs a deprecated practice. In Joomla! this results in the $message being output in the
	 * deprecated log file, found in your site's log directory.
	 *
	 * @param   string  $message  The deprecated practice log message
	 *
	 * @return  void
	 */
	public function logDeprecated($message)
	{
		// The default implementation does nothing. Override this in your platform classes.
	}

	/**
	 * Adds a message
	 *
	 * @param   string|array  $title      A title, or an array of additional fields to add to the log entry
	 * @param   string        $logText    The translation key to the log text
	 * @param   string        $extension  The name of the extension logging this entry
	 * @param   User|null     $user       The user the action is being logged for
	 *
	 * @return  void
	 */
	public function logUserAction($title, $logText, $extension, $user = null)
	{
		// The default implementation does nothing. Override this in your platform classes.
	}

	/**
	 * Returns the version number string of the platform, e.g. "4.5.6". If
	 * implementation integrates with a CMS or a versioned foundation (e.g.
	 * a framework) it is advisable to return that version.
	 *
	 * @return  string
	 *
	 * @since  2.1.2
	 */
	public function getPlatformVersion()
	{
		return '';
	}

	/**
	 * Handle an exception in a way that results to an error page.
	 *
	 * @param   Exception  $exception  The exception to handle
	 *
	 * @throws  Exception  Possibly rethrown exception
	 */
	public function showErrorPage(Exception $exception)
	{
		throw $exception;
	}

	/**
	 * Are plugins allowed to run in CLI mode?
	 *
	 * @return  bool
	 */
	public function isAllowPluginsInCli()
	{
		return $this->allowPluginsInCli;
	}

	/**
	 * Set whether plugins are allowed to run in CLI mode
	 *
	 * @param   bool  $allowPluginsInCli
	 */
	public function setAllowPluginsInCli($allowPluginsInCli)
	{
		$this->allowPluginsInCli = $allowPluginsInCli;
	}
}
Platform/Base/Filesystem.php000060400000007060152455514240012045 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Platform\Base;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Platform\FilesystemInterface;

abstract class Filesystem implements FilesystemInterface
{
	/**
	 * The list of paths where platform class files will be looked for
	 *
	 * @var  array
	 */
	protected static $paths = [];
	/** @var  Container  The component container */
	protected $container = null;

	/**
	 * Public constructor.
	 *
	 * @param   Container  $c  The component container
	 */
	public function __construct(Container $c)
	{
		$this->container = $c;
	}

	/**
	 * This method will crawl a starting directory and get all the valid files that will be analyzed by getInstance.
	 * Then it organizes them into an associative array.
	 *
	 * @param   string  $path           Folder where we should start looking
	 * @param   array   $ignoreFolders  Folder ignore list
	 * @param   array   $ignoreFiles    File ignore list
	 *
	 * @return  array   Associative array, where the `fullpath` key contains the path to the file,
	 *                  and the `classname` key contains the name of the class
	 */
	protected static function getFiles($path, array $ignoreFolders = [], array $ignoreFiles = [])
	{
		$return = [];

		$files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles);

		// Ok, I got the files, now I have to organize them
		foreach ($files as $file)
		{
			$clean = str_replace($path, '', $file);
			$clean = trim(str_replace('\\', '/', $clean), '/');

			$parts = explode('/', $clean);

			// If I have less than 3 fragments, it means that the file was inside the generic folder
			// (interface + abstract) so I have to skip it
			if (count($parts) < 3)
			{
				continue;
			}

			$return[] = [
				'fullpath'  => $file,
				'classname' => 'F0FPlatform' . ucfirst($parts[0]) . ucfirst(basename($parts[1], '.php')),
			];
		}

		return $return;
	}

	/**
	 * Recursive function that will scan every directory unless it's in the ignore list. Files that aren't in the
	 * ignore list are returned.
	 *
	 * @param   string  $path           Folder where we should start looking
	 * @param   array   $ignoreFolders  Folder ignore list
	 * @param   array   $ignoreFiles    File ignore list
	 *
	 * @return  array   List of all the files
	 */
	protected static function scanDirectory($path, array $ignoreFolders = [], array $ignoreFiles = [])
	{
		$return = [];

		$handle = @opendir($path);

		if (!$handle)
		{
			return $return;
		}

		while (($file = readdir($handle)) !== false)
		{
			if ($file == '.' || $file == '..')
			{
				continue;
			}

			$fullpath = $path . '/' . $file;

			if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles)))
			{
				continue;
			}

			if (is_dir($fullpath))
			{
				$return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return);
			}
			else
			{
				$return[] = $path . '/' . $file;
			}
		}

		return $return;
	}

	/**
	 * Gets the extension of a file name
	 *
	 * @param   string  $file  The file name
	 *
	 * @return  string  The file extension
	 */
	public function getExt($file)
	{
		$dot = strrpos($file, '.') + 1;

		return substr($file, $dot);
	}

	/**
	 * Strips the last extension off of a file name
	 *
	 * @param   string  $file  The file name
	 *
	 * @return  string  The file name without the extension
	 */
	public function stripExt($file)
	{
		return preg_replace('#\.[^.]*$#', '', $file);
	}
}
Platform/PlatformInterface.php000060400000042617152455514240012463 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Platform;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Date\Date;
use FOF30\Input\Input;
use JDatabaseDriver;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Language\Language;
use Joomla\CMS\User\User;
use Joomla\Registry\Registry;
use JsonSerializable;

/**
 * Part of the F0F Platform Abstraction Layer. It implements everything that
 * depends on the platform F0F is running under, e.g. the Joomla! CMS front-end,
 * the Joomla! CMS back-end, a CLI Joomla! Platform app, a bespoke Joomla!
 * Platform / Framework web application and so on.
 */
interface PlatformInterface
{
	/**
	 * Public constructor.
	 *
	 * @param   Container  $c  The component container
	 */
	public function __construct(Container $c);

	/**
	 * Checks if the current script is run inside a valid CMS execution
	 *
	 * @return bool
	 */
	public function checkExecution();

	/**
	 * Raises an error, using the logic requested by the CMS (PHP Exception or dedicated class)
	 *
	 * @param   integer  $code
	 * @param   string   $message
	 *
	 * @return mixed
	 */
	public function raiseError($code, $message);

	/**
	 * Returns the version number string of the CMS/application we're running in
	 *
	 * @return  string
	 *
	 * @since  2.1.2
	 */
	public function getPlatformVersion();

	/**
	 * Returns absolute path to directories used by the containing CMS/application.
	 *
	 * The return is a table with the following key:
	 * * root    Path to the site root
	 * * public  Path to the public area of the site
	 * * admin   Path to the administrative area of the site
	 * * tmp     Path to the temp directory
	 * * log     Path to the log directory
	 *
	 * @return  array  A hash array with keys root, public, admin, tmp and log.
	 */
	public function getPlatformBaseDirs();

	/**
	 * Returns the base (root) directories for a given component, i.e the application
	 * which is running inside our main application (CMS, web app).
	 *
	 * The return is a table with the following keys:
	 * * main    The normal location of component files. For a back-end Joomla!
	 *          component this is the administrator/components/com_example
	 *          directory.
	 * * alt    The alternate location of component files. For a back-end
	 *          Joomla! component this is the front-end directory, e.g.
	 *          components/com_example
	 * * site    The location of the component files serving the public part of
	 *          the application.
	 * * admin    The location of the component files serving the administrative
	 *          part of the application.
	 *
	 * All paths MUST be absolute. All four paths MAY be the same if the
	 * platform doesn't make a distinction between public and private parts,
	 * or when the component does not provide both a public and private part.
	 * All of the directories MUST be defined and non-empty.
	 *
	 * @param   string  $component  The name of the component. For Joomla! this
	 *                              is something like "com_example"
	 *
	 * @return  array  A hash array with keys main, alt, site and admin.
	 */
	public function getComponentBaseDirs($component);

	/**
	 * Returns the application's template name
	 *
	 * @param   boolean|array  $params  An optional associative array of configuration settings
	 *
	 * @return  string  The template name. System is the fallback.
	 */
	public function getTemplate($params = false);

	/**
	 * Get application-specific suffixes to use with template paths. This allows
	 * you to look for view template overrides based on the application version.
	 *
	 * @return  array  A plain array of suffixes to try in template names
	 */
	public function getTemplateSuffixes();

	/**
	 * Return the absolute path to the application's template overrides
	 * directory for a specific component. We will use it to look for template
	 * files instead of the regular component directories. If the application
	 * does not have such a thing as template overrides return an empty string.
	 *
	 * @param   string   $component  The name of the component for which to fetch the overrides
	 * @param   boolean  $absolute   Should I return an absolute or relative path?
	 *
	 * @return  string  The path to the template overrides directory
	 */
	public function getTemplateOverridePath($component, $absolute = true);

	/**
	 * Load the translation files for a given component.
	 *
	 * @param   string  $component  The name of the component. For Joomla! this
	 *                              is something like "com_example"
	 *
	 * @return  void
	 */
	public function loadTranslations($component);

	/**
	 * By default FOF will only use the Controller's onBefore* methods to
	 * perform user authorisation. In some cases, like the Joomla! back-end,
	 * you also need to perform component-wide user authorisation in the
	 * Dispatcher. This method MUST implement this authorisation check. If you
	 * do not need this in your platform, please always return true.
	 *
	 * @param   string  $component  The name of the component.
	 *
	 * @return  boolean  True to allow loading the component, false to halt loading
	 */
	public function authorizeAdmin($component);

	/**
	 * This method will try retrieving a variable from the request (input) data.
	 * If it doesn't exist it will be loaded from the user state, typically
	 * stored in the session. If it doesn't exist there either, the $default
	 * value will be used. If $setUserState is set to true, the retrieved
	 * variable will be stored in the user session.
	 *
	 * @param   string   $key           The user state key for the variable
	 * @param   string   $request       The request variable name for the variable
	 * @param   Input    $input         The Input object with the request (input) data
	 * @param   mixed    $default       The default value. Default: null
	 * @param   string   $type          The filter type for the variable data. Default: none (no filtering)
	 * @param   boolean  $setUserState  Should I set the user state with the fetched value?
	 *
	 * @return  mixed  The value of the variable
	 */
	public function getUserStateFromRequest($key, $request, $input, $default = null, $type = 'none', $setUserState = true);

	/**
	 * Load plugins of a specific type. Obviously this seems to only be required
	 * in the Joomla! CMS itself.
	 *
	 * @param   string  $type  The type of the plugins to be loaded
	 *
	 * @return void
	 */
	public function importPlugin($type);

	/**
	 * Execute plugins (system-level triggers) and fetch back an array with
	 * their return values.
	 *
	 * @param   string  $event  The event (trigger) name, e.g. onBeforeScratchMyEar
	 * @param   array   $data   A hash array of data sent to the plugins as part of the trigger
	 *
	 * @return  array  A simple array containing the results of the plugins triggered
	 */
	public function runPlugins($event, $data);

	/**
	 * Perform an ACL check. Please note that FOF uses by default the Joomla!
	 * CMS convention for ACL privileges, e.g core.edit for the edit privilege.
	 * If your platform uses different conventions you'll have to override the
	 * F0F defaults using fof.xml or by specialising the controller.
	 *
	 * @param   string  $action     The ACL privilege to check, e.g. core.edit
	 * @param   string  $assetname  The asset name to check, typically the component's name
	 *
	 * @return  boolean  True if the user is allowed this action
	 */
	public function authorise($action, $assetname);

	/**
	 * Returns a user object.
	 *
	 * @param   integer  $id  The user ID to load. Skip or use null to retrieve
	 *                        the object for the currently logged in user.
	 *
	 * @return  User  The \JUser object for the specified user
	 */
	public function getUser($id = null);

	/**
	 * Returns the \JDocument object which handles this component's response. You
	 * may also return null and FOF will a. try to figure out the output type by
	 * examining the "format" input parameter (or fall back to "html") and b.
	 * FOF will not attempt to load CSS and Javascript files (as it doesn't make
	 * sense if there's no \JDocument to handle them).
	 *
	 * @return  Document
	 */
	public function getDocument();

	/**
	 * Returns an object to handle dates
	 *
	 * @param   mixed  $time      The initial time
	 * @param   null   $tzOffest  The timezone offset
	 * @param   bool   $locale    Should I try to load a specific class for current language?
	 *
	 * @return  Date object
	 */
	public function getDate($time = 'now', $tzOffest = null, $locale = true);

	/**
	 * Return the \JLanguage instance of the CMS/application
	 *
	 * @return Language
	 */
	public function getLanguage();

	/**
	 * Returns the database driver object of the CMS/application
	 *
	 * @return JDatabaseDriver
	 */
	public function getDbo();

	/**
	 * Is this the administrative section of the component?
	 *
	 * @return  boolean
	 */
	public function isBackend();

	/**
	 * Is this the public section of the component?
	 *
	 * @return  boolean
	 */
	public function isFrontend();

	/**
	 * Is this the Joomla 4 API application?
	 *
	 * @return  boolean
	 */
	public function isApi();

	/**
	 * Is this a component running in a CLI application?
	 *
	 * @return  boolean
	 */
	public function isCli();

	/**
	 * Is AJAX re-ordering supported? This is 100% Joomla! CMS (version 3+) specific.
	 *
	 * @return  boolean
	 */
	public function supportsAjaxOrdering();

	/**
	 * Saves something to the cache. This is supposed to be used for system-wide
	 * FOF data, not application data.
	 *
	 * @param   string  $key      The key of the data to save
	 * @param   string  $content  The actual data to save
	 *
	 * @return  boolean  True on success
	 */
	public function setCache($key, $content);

	/**
	 * Retrieves data from the cache. This is supposed to be used for system-side
	 * FOF data, not application data.
	 *
	 * @param   string  $key      The key of the data to retrieve
	 * @param   string  $default  The default value to return if the key is not found or the cache is not populated
	 *
	 * @return  string  The cached value
	 */
	public function getCache($key, $default = null);

	/**
	 * Clears the cache of system-wide FOF data. You are supposed to call this in
	 * your components' installation script post-installation and post-upgrade
	 * methods or whenever you are modifying the structure of database tables
	 * accessed by F0F. Please note that FOF's cache never expires and is not
	 * purged by Joomla!. You MUST use this method to manually purge the cache.
	 *
	 * @return  boolean  True on success
	 */
	public function clearCache();

	/**
	 * Returns an object that holds the configuration of the current site.
	 *
	 * @return  Registry
	 */
	public function getConfig();

	/**
	 * Is the global FOF cache enabled?
	 *
	 * @return  boolean
	 */
	public function isGlobalFOFCacheEnabled();

	/**
	 * logs in a user
	 *
	 * @param   array  $authInfo  Authentication information
	 *
	 * @return  boolean  True on success
	 */
	public function loginUser($authInfo);

	/**
	 * logs out a user
	 *
	 * @return  boolean  True on success
	 */
	public function logoutUser();

	/**
	 * Add a log file for FOF
	 *
	 * @param   string  $file
	 *
	 * @return  void
	 */
	public function logAddLogger($file);

	/**
	 * Logs a deprecated practice. In Joomla! this results in the $message being output in the
	 * deprecated log file, found in your site's log directory.
	 *
	 * @param   string  $message  The deprecated practice log message
	 *
	 * @return  void
	 */
	public function logDeprecated($message);

	/**
	 * Adds a message to the application's debug log
	 *
	 * @param   string  $message
	 *
	 * @return  void
	 */
	public function logDebug($message);

	/**
	 * Adds a message
	 *
	 * @param   string|array  $title      A title, or an array of additional fields to add to the log entry
	 * @param   string        $logText    The translation key to the log text
	 * @param   string        $extension  The name of the extension logging this entry
	 * @param   User|null     $user       The user the action is being logged for
	 *
	 * @return  void
	 */
	public function logUserAction($title, $logText, $extension, $user = null);

	/**
	 * Returns the root URI for the request.
	 *
	 * @param   boolean  $pathonly  If false, prepend the scheme, host and port information. Default is false.
	 * @param   string   $path      The path
	 *
	 * @return  string  The root URI string.
	 */
	public function URIroot($pathonly = false, $path = null);

	/**
	 * Returns the base URI for the request.
	 *
	 * @param   boolean  $pathonly  If false, prepend the scheme, host and port information. Default is false.
	 *                              |
	 *
	 * @return  string  The base URI string
	 */
	public function URIbase($pathonly = false);

	/**
	 * Method to set a response header.  If the replace flag is set then all headers
	 * with the given name will be replaced by the new one (only if the current platform supports header caching)
	 *
	 * @param   string   $name     The name of the header to set.
	 * @param   string   $value    The value of the header to set.
	 * @param   boolean  $replace  True to replace any headers with the same name.
	 *
	 * @return  void
	 */
	public function setHeader($name, $value, $replace = false);

	/**
	 * In platforms that perform header caching, send all headers.
	 *
	 * @return  void
	 */
	public function sendHeaders();

	/**
	 * Immediately terminate the containing application's execution
	 *
	 * @param   int  $code  The result code which should be returned by the application
	 *
	 * @return  void
	 */
	public function closeApplication($code = 0);

	/**
	 * Perform a redirection to a different page, optionally enqueuing a message for the user.
	 *
	 * @param   string  $url     The URL to redirect to
	 * @param   int     $status  (optional) The HTTP redirection status code, default 301
	 * @param   string  $msg     (optional) A message to enqueue
	 * @param   string  $type    (optional) The message type, e.g. 'message' (default), 'warning' or 'error'.
	 *
	 * @return  void
	 *
	 * @throws  Exception
	 */
	public function redirect($url, $status = 301, $msg = null, $type = 'message');

	/**
	 * Handle an exception in a way that results to an error page.
	 *
	 * @param   Exception  $exception  The exception to handle
	 *
	 * @throws  Exception  Possibly rethrown exception
	 */
	public function showErrorPage(Exception $exception);

	/**
	 * Set a variable in the user session
	 *
	 * @param   string  $name       The name of the variable to set
	 * @param   string  $value      (optional) The value to set it to, default is null
	 * @param   string  $namespace  (optional) The variable's namespace e.g. the component name. Default: 'default'
	 *
	 * @return  void
	 */
	public function setSessionVar($name, $value = null, $namespace = 'default');

	/**
	 * Get a variable from the user session
	 *
	 * @param   string  $name       The name of the variable to set
	 * @param   string  $default    (optional) The default value to return if the variable does not exit, default: null
	 * @param   string  $namespace  (optional) The variable's namespace e.g. the component name. Default: 'default'
	 *
	 * @return  mixed
	 */
	public function getSessionVar($name, $default = null, $namespace = 'default');

	/**
	 * Unset a variable from the user session
	 *
	 * @param   string  $name       The name of the variable to unset
	 * @param   string  $namespace  (optional) The variable's namespace e.g. the component name. Default: 'default'
	 *
	 * @return  void
	 */
	public function unsetSessionVar($name, $namespace = 'default');

	/**
	 * Return the session token. Two types of tokens can be returned:
	 *
	 * Session token ($formToken == false): Used for anti-spam protection of forms. This is specific to a session
	 *   object.
	 *
	 * Form token ($formToken == true): A secure hash of the user ID with the session token. Both the session and the
	 *   user are fetched from the application container.
	 *
	 * @param   bool  $formToken  Should I return a form token?
	 * @param   bool  $forceNew   Should I force the creation of a new token?
	 *
	 * @return  mixed
	 */
	public function getToken($formToken = false, $forceNew = false);

	/**
	 * Are plugins allowed to run in CLI mode?
	 *
	 * @return  bool
	 */
	public function isAllowPluginsInCli();

	/**
	 * Set whether plugins are allowed to run in CLI mode
	 *
	 * @param   bool  $allowPluginsInCli
	 */
	public function setAllowPluginsInCli($allowPluginsInCli);

	/**
	 * Set a script option.
	 *
	 * This allows the backend code to set up configuration options for frontend (JavaScript) code in a way that's safe
	 * for async / deferred scripts. The options are stored in the document's head as an inline JSON document. This
	 * JSON document is then parsed by a JavaScript helper function which makes the options available to the scripts
	 * that consume them.
	 *
	 * @param   string                  $key     The option key
	 * @param   mixed|JsonSerializable  $value   The option value. Must be a scalar or a JSON serializable object
	 * @param   bool                    $merge   Should I merge an array value with existing stored values? Default:
	 *                                           true
	 *
	 * @return  void
	 */
	public function addScriptOptions($key, $value, $merge = true);

	/**
	 * Get a script option, or all of the script options
	 *
	 * @param   string|null  $key  The script option to retrieve. Null for all options.
	 *
	 * @return  array|mixed  Options for given $key, or all script options
	 */
	public function getScriptOptions($key = null);
}
Platform/Joomla/Filesystem.php000060400000014145152455514240012416 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Platform\Joomla;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Platform\Base\Filesystem as BaseFilesystem;
use JLoader;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Filesystem\Path;
use UnexpectedValueException;

/**
 * Abstraction for Joomla! filesystem API
 */
class Filesystem extends BaseFilesystem
{
	/**
	 * Does the file exists?
	 *
	 * @param   $path  string   Path to the file to test
	 *
	 * @return  bool
	 */
	public function fileExists($path)
	{
		return File::exists($path);
	}

	/**
	 * Delete a file or array of files
	 *
	 * @param   mixed  $file  The file name or an array of file names
	 *
	 * @return  boolean  True on success
	 *
	 */
	public function fileDelete($file)
	{
		return File::delete($file);
	}

	/**
	 * Copies a file
	 *
	 * @param   string   $src          The path to the source file
	 * @param   string   $dest         The path to the destination file
	 * @param   string   $path         An optional base path to prefix to the file names
	 * @param   boolean  $use_streams  True to use streams
	 *
	 * @return  boolean  True on success
	 */
	public function fileCopy($src, $dest, $path = null, $use_streams = false)
	{
		return File::copy($src, $dest, $path, $use_streams);
	}

	/**
	 * Write contents to a file
	 *
	 * @param   string    $file         The full file path
	 * @param   string   &$buffer       The buffer to write
	 * @param   boolean   $use_streams  Use streams
	 *
	 * @return  boolean  True on success
	 */
	public function fileWrite($file, &$buffer, $use_streams = false)
	{
		return File::write($file, $buffer, $use_streams);
	}

	/**
	 * Checks for snooping outside of the file system root.
	 *
	 * @param   string  $path  A file system path to check.
	 *
	 * @return  string  A cleaned version of the path or exit on error.
	 *
	 * @throws  Exception
	 */
	public function pathCheck($path)
	{
		return Path::check($path);
	}

	/**
	 * Function to strip additional / or \ in a path name.
	 *
	 * @param   string  $path  The path to clean.
	 * @param   string  $ds    Directory separator (optional).
	 *
	 * @return  string  The cleaned path.
	 *
	 * @throws  UnexpectedValueException
	 */
	public function pathClean($path, $ds = DIRECTORY_SEPARATOR)
	{
		return Path::clean($path, $ds);
	}

	/**
	 * Searches the directory paths for a given file.
	 *
	 * @param   mixed   $paths  An path string or array of path strings to search in
	 * @param   string  $file   The file name to look for.
	 *
	 * @return  mixed   The full path and file name for the target file, or boolean false if the file is not found in
	 *                  any of the paths.
	 */
	public function pathFind($paths, $file)
	{
		return Path::find($paths, $file);
	}

	/**
	 * Wrapper for the standard file_exists function
	 *
	 * @param   string  $path  Folder name relative to installation dir
	 *
	 * @return  boolean  True if path is a folder
	 */
	public function folderExists($path)
	{
		try
		{
			return Folder::exists($path);
		}
		catch (Exception $e)
		{
			return false;
		}
	}

	/**
	 * Utility function to read the files in a folder.
	 *
	 * @param   string   $path           The path of the folder to read.
	 * @param   string   $filter         A filter for file names.
	 * @param   mixed    $recurse        True to recursively search into sub-folders, or an integer to specify the
	 *                                   maximum depth.
	 * @param   boolean  $full           True to return the full path to the file.
	 * @param   array    $exclude        Array with names of files which should not be shown in the result.
	 * @param   array    $excludefilter  Array of filter to exclude
	 * @param   boolean  $naturalSort    False for asort, true for natsort
	 *
	 * @return  array  Files in the given folder.
	 */
	public function folderFiles($path, $filter = '.', $recurse = false, $full = false, $exclude = [
		'.svn', 'CVS', '.DS_Store', '__MACOSX',
	],
	                            $excludefilter = ['^\..*', '.*~'], $naturalSort = false)
	{
		// JFolder throws nonsense errors if the path is not a folder
		try
		{
			$path = Path::clean($path);
		}
		catch (Exception $e)
		{
			return [];
		}

		if (!@is_dir($path))
		{
			return [];
		}

		// Now call JFolder
		return Folder::files($path, $filter, $recurse, $full, $exclude, $excludefilter, $naturalSort);
	}

	/**
	 * Utility function to read the folders in a folder.
	 *
	 * @param   string   $path           The path of the folder to read.
	 * @param   string   $filter         A filter for folder names.
	 * @param   mixed    $recurse        True to recursively search into sub-folders, or an integer to specify the
	 *                                   maximum depth.
	 * @param   boolean  $full           True to return the full path to the folders.
	 * @param   array    $exclude        Array with names of folders which should not be shown in the result.
	 * @param   array    $excludefilter  Array with regular expressions matching folders which should not be shown in
	 *                                   the result.
	 *
	 * @return  array  Folders in the given folder.
	 */
	public function folderFolders($path, $filter = '.', $recurse = false, $full = false, $exclude = [
		'.svn', 'CVS', '.DS_Store', '__MACOSX',
	],
	                              $excludefilter = ['^\..*'])
	{
		// JFolder throws idiotic errors if the path is not a folder
		try
		{
			$path = Path::clean($path);
		}
		catch (Exception $e)
		{
			return [];
		}

		if (!@is_dir($path))
		{
			return [];
		}

		// Now call JFolder
		return Folder::folders($path, $filter, $recurse, $full, $exclude, $excludefilter);
	}

	/**
	 * Create a folder -- and all necessary parent folders.
	 *
	 * @param   string   $path  A path to create from the base path.
	 * @param   integer  $mode  Directory permissions to set for folders created. 0755 by default.
	 *
	 * @return  boolean  True if successful.
	 */
	public function folderCreate($path = '', $mode = 0755)
	{
		return Folder::create($path, $mode);
	}
}
Platform/Joomla/Platform.php000060400000107711152455514240012060 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Platform\Joomla;

defined('_JEXEC') || die;

use ActionlogsModelActionlog;
use Exception;
use FOF30\Container\Container;
use FOF30\Date\Date;
use FOF30\Date\DateDecorator;
use FOF30\Input\Input;
use FOF30\Platform\Base\Platform as BasePlatform;
use JDatabaseDriver;
use JEventDispatcher;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Application\CliApplication as JApplicationCli;
use Joomla\CMS\Application\CMSApplication as JApplicationCms;
use Joomla\CMS\Application\ConsoleApplication;
use Joomla\CMS\Application\WebApplication as JApplicationWeb;
use Joomla\CMS\Authentication\Authentication as JAuthentication;
use Joomla\CMS\Authentication\AuthenticationResponse as JAuthenticationResponse;
use Joomla\CMS\Cache\Cache as JCache;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Factory;
use Joomla\CMS\Factory as JFactory;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Log\Log;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Session\Session as JSession;
use Joomla\CMS\Uri\Uri as JUri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserHelper;
use Joomla\CMS\Version;
use Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel;
use Joomla\Registry\Registry;

/**
 * Part of the FOF Platform Abstraction Layer.
 *
 * This implements the platform class for Joomla! 3
 *
 * @since    2.1
 */
class Platform extends BasePlatform
{
	/**
	 * Is this a CLI application?
	 *
	 * @var   bool
	 */
	protected static $isCLI = null;

	/**
	 * Is this an administrator application?
	 *
	 * @var   bool
	 */
	protected static $isAdmin = null;

	/**
	 * Is this an API application?
	 *
	 * @var   bool
	 */
	protected static $isApi = null;

	/**
	 * A fake session storage for CLI apps. This is only used for legacy CLI applications which are not using the FOF
	 * Base CLI script.
	 *
	 * @var   Registry
	 */
	protected static $fakeSession = null;

	/**
	 * The table and table field cache object, used to speed up database access
	 *
	 * @var  Registry|null
	 */
	private $_cache = null;

	/**
	 * Public constructor.
	 *
	 * Overridden to cater for CLI applications not having access to a session object.
	 *
	 * @param   Container  $c  The component container
	 */
	public function __construct(Container $c)
	{
		parent::__construct($c);

		if ($this->isCli())
		{
			static::$fakeSession = new Registry();
		}
	}

	/**
	 * Checks if the current script is run inside a valid CMS execution
	 *
	 * @return  bool
	 * @see     PlatformInterface::checkExecution()
	 *
	 */
	public function checkExecution()
	{
		return defined('_JEXEC');
	}

	/**
	 * Raises an error, using the logic requested by the CMS (PHP Exception or dedicated class)
	 *
	 * @param   integer  $code
	 * @param   string   $message
	 *
	 * @return  void
	 *
	 * @throws  Exception
	 */
	public function raiseError($code, $message)
	{
		$this->showErrorPage(new Exception($message, $code));
	}

	/**
	 * Returns absolute path to directories used by the CMS.
	 *
	 * @return  array  A hash array with keys root, public, admin, tmp and log.
	 * @see PlatformInterface::getPlatformBaseDirs()
	 *
	 */
	public function getPlatformBaseDirs()
	{
		return [
			'root'   => JPATH_ROOT,
			'public' => JPATH_SITE,
			'media'  => JPATH_SITE . '/media',
			'admin'  => JPATH_ADMINISTRATOR,
			'tmp'    => JFactory::getConfig()->get('tmp_path'),
			'log'    => JFactory::getConfig()->get('log_path'),
		];
	}

	/**
	 * Returns the base (root) directories for a given component.
	 *
	 * @param   string  $component  The name of the component. For Joomla! this
	 *                              is something like "com_example"
	 *
	 * @return  array  A hash array with keys main, alt, site and admin.
	 * @see PlatformInterface::getComponentBaseDirs()
	 *
	 */
	public function getComponentBaseDirs($component)
	{
		if (!$this->isBackend())
		{
			$mainPath = JPATH_SITE . '/components/' . $component;
			$altPath  = JPATH_ADMINISTRATOR . '/components/' . $component;
		}
		else
		{
			$mainPath = JPATH_ADMINISTRATOR . '/components/' . $component;
			$altPath  = JPATH_SITE . '/components/' . $component;
		}

		return [
			'main'  => $mainPath,
			'alt'   => $altPath,
			'site'  => JPATH_SITE . '/components/' . $component,
			'admin' => JPATH_ADMINISTRATOR . '/components/' . $component,
		];
	}

	/**
	 * Returns the application's template name
	 *
	 * @param   boolean|array  $params  An optional associative array of configuration settings
	 *
	 * @return  string  The template name. System is the fallback.
	 */
	public function getTemplate($params = false)
	{
		return JFactory::getApplication()->getTemplate($params);
	}

	/**
	 * Get application-specific suffixes to use with template paths. This allows
	 * you to look for view template overrides based on the application version.
	 *
	 * @return  array  A plain array of suffixes to try in template names
	 */
	public function getTemplateSuffixes()
	{
		$jversion     = new Version;
		$versionParts = explode('.', $jversion->getShortVersion());
		$majorVersion = array_shift($versionParts);
		$suffixes     = [
			'.j' . str_replace('.', '', $jversion->getHelpVersion()),
			'.j' . $majorVersion,
		];

		return $suffixes;
	}

	/**
	 * Return the absolute path to the application's template overrides
	 * directory for a specific component. We will use it to look for template
	 * files instead of the regular component directories. If the application
	 * does not have such a thing as template overrides return an empty string.
	 *
	 * @param   string   $component  The name of the component for which to fetch the overrides
	 * @param   boolean  $absolute   Should I return an absolute or relative path?
	 *
	 * @return  string  The path to the template overrides directory
	 */
	public function getTemplateOverridePath($component, $absolute = true)
	{
		[$isCli, $isAdmin] = $this->isCliAdmin();

		if (!$isCli)
		{
			if ($absolute)
			{
				$path = JPATH_THEMES . '/';
			}
			else
			{
				$path = $isAdmin ? 'administrator/templates/' : 'templates/';
			}

			if (substr($component, 0, 7) == 'media:/')
			{
				$directory = 'media/' . substr($component, 7);
			}
			else
			{
				$directory = 'html/' . $component;
			}

			$path .= $this->getTemplate() .
				'/' . $directory;
		}
		else
		{
			$path = '';
		}

		return $path;
	}

	/**
	 * Load the translation files for a given component.
	 *
	 * @param   string  $component  The name of the component. For Joomla! this
	 *                              is something like "com_example"
	 *
	 * @return  void
	 * @see PlatformInterface::loadTranslations()
	 *
	 */
	public function loadTranslations($component)
	{
		if ($this->isBackend())
		{
			$paths = [JPATH_ROOT, JPATH_ADMINISTRATOR];
		}
		else
		{
			$paths = [JPATH_ADMINISTRATOR, JPATH_ROOT];
		}

		$jlang = $this->getLanguage();
		$jlang->load($component, $paths[0], 'en-GB', true);
		$jlang->load($component, $paths[0], null, true);
		$jlang->load($component, $paths[1], 'en-GB', true);
		$jlang->load($component, $paths[1], null, true);
	}

	/**
	 * Authorise access to the component in the back-end.
	 *
	 * @param   string  $component  The name of the component.
	 *
	 * @return  boolean  True to allow loading the component, false to halt loading
	 * @see PlatformInterface::authorizeAdmin()
	 *
	 */
	public function authorizeAdmin($component)
	{
		if ($this->isBackend())
		{
			// Master access check for the back-end, Joomla! 1.6 style.
			$user = $this->getUser();

			if (!$user->authorise('core.manage', $component)
				&& !$user->authorise('core.admin', $component)
			)
			{
				return false;
			}
		}

		return true;
	}

	/**
	 * Return a user object.
	 *
	 * @param   integer  $id  The user ID to load. Skip or use null to retrieve
	 *                        the object for the currently logged in user.
	 *
	 * @return  User  The JUser object for the specified user
	 * @see PlatformInterface::getUser()
	 */
	public function getUser($id = null)
	{
		/**
		 * If I'm in CLI I need load the User directly, otherwise JFactory will check the session (which doesn't exist
		 * in CLI)
		 */
		if ($this->isCli())
		{
			if ($id)
			{
				return User::getInstance($id);
			}

			return new User();
		}

		return JFactory::getUser($id);
	}

	/**
	 * Returns the JDocument object which handles this component's response.
	 *
	 * @return  Document
	 * @see PlatformInterface::getDocument()
	 */
	public function getDocument()
	{
		$document = null;

		if (!$this->isCli())
		{
			try
			{
				$document = JFactory::getDocument();
			}
			catch (Exception $exc)
			{
				$document = null;
			}
		}

		return $document;
	}

	/**
	 * Returns an object to handle dates
	 *
	 * @param   mixed  $time      The initial time
	 * @param   null   $tzOffest  The timezone offset
	 * @param   bool   $locale    Should I try to load a specific class for current language?
	 *
	 * @return  Date object
	 */
	public function getDate($time = 'now', $tzOffest = null, $locale = true)
	{
		if ($locale)
		{
			// Work around a bug in Joomla! 3.7.0.
			if ($time == 'now')
			{
				$time = time();
			}

			$coreObject = JFactory::getDate($time, $tzOffest);

			return new DateDecorator($coreObject);
		}
		else
		{
			return new Date($time, $tzOffest);
		}
	}

	/**
	 * Return the \JLanguage instance of the CMS/application
	 *
	 * @return Language
	 */
	public function getLanguage()
	{
		return JFactory::getLanguage();
	}

	/**
	 * Returns the database driver object of the CMS/application
	 *
	 * @return JDatabaseDriver
	 */
	public function getDbo()
	{
		return JFactory::getDbo();
	}

	/**
	 * This method will try retrieving a variable from the request (input) data.
	 *
	 * @param   string   $key           The user state key for the variable
	 * @param   string   $request       The request variable name for the variable
	 * @param   Input    $input         The Input object with the request (input) data
	 * @param   mixed    $default       The default value. Default: null
	 * @param   string   $type          The filter type for the variable data. Default: none (no filtering)
	 * @param   boolean  $setUserState  Should I set the user state with the fetched value?
	 *
	 * @return  mixed  The value of the variable
	 * @see PlatformInterface::getUserStateFromRequest()
	 *
	 */
	public function getUserStateFromRequest($key, $request, $input, $default = null, $type = 'none', $setUserState = true)
	{
		[$isCLI, $isAdmin] = $this->isCliAdmin();

		unset($isAdmin); // Just to make phpStorm happy

		if ($isCLI)
		{
			$ret = $input->get($request, $default, $type);

			if ($ret === $default)
			{
				$input->set($request, $ret);
			}

			return $ret;
		}

		$app = JFactory::getApplication();

		if (method_exists($app, 'getUserState'))
		{
			$old_state = $app->getUserState($key, $default);
		}
		else
		{
			$old_state = null;
		}

		$cur_state = (!is_null($old_state)) ? $old_state : $default;
		$new_state = $input->get($request, null, $type);

		// Save the new value only if it was set in this request
		if ($setUserState)
		{
			if ($new_state !== null)
			{
				$app->setUserState($key, $new_state);
			}
			else
			{
				$new_state = $cur_state;
			}
		}
		elseif (is_null($new_state))
		{
			$new_state = $cur_state;
		}

		return $new_state;
	}

	/**
	 * Load plugins of a specific type. Obviously this seems to only be required
	 * in the Joomla! CMS.
	 *
	 * @param   string  $type  The type of the plugins to be loaded
	 *
	 * @return void
	 *
	 * @codeCoverageIgnore
	 * @see PlatformInterface::importPlugin()
	 *
	 */
	public function importPlugin($type)
	{
		// Should I actually run the plugins?
		$runPlugins = $this->isAllowPluginsInCli() || !$this->isCli();

		if ($runPlugins)
		{
			PluginHelper::importPlugin($type);
		}
	}

	/**
	 * Execute plugins (system-level triggers) and fetch back an array with
	 * their return values.
	 *
	 * @param   string  $event  The event (trigger) name, e.g. onBeforeScratchMyEar
	 * @param   array   $data   A hash array of data sent to the plugins as part of the trigger
	 *
	 * @return  array  A simple array containing the results of the plugins triggered
	 *
	 * @codeCoverageIgnore
	 * @see PlatformInterface::runPlugins()
	 *
	 */
	public function runPlugins($event, $data)
	{
		// Should I actually run the plugins?
		$runPlugins = $this->isAllowPluginsInCli() || !$this->isCli();

		if ($runPlugins)
		{
			// First, try with JEventDispatcher (Joomla 3.x)
			if (class_exists('JEventDispatcher'))
			{
				return JEventDispatcher::getInstance()->trigger($event, $data);
			}

			// If there's no JEventDispatcher try getting JApplication
			try
			{
				$app = JFactory::getApplication();
			}
			catch (Exception $e)
			{
				// If I can't get JApplication I cannot run the plugins.
				return [];
			}

			return $app->triggerEvent($event, $data);
		}
		else
		{
			// I am not allowed to run plugins
			return [];
		}
	}

	/**
	 * Perform an ACL check.
	 *
	 * @param   string  $action     The ACL privilege to check, e.g. core.edit
	 * @param   string  $assetname  The asset name to check, typically the component's name
	 *
	 * @return  boolean  True if the user is allowed this action
	 * @see PlatformInterface::authorise()
	 *
	 */
	public function authorise($action, $assetname)
	{
		if ($this->isCli())
		{
			return true;
		}

		$ret = JFactory::getUser()->authorise($action, $assetname);

		// Work around Joomla returning null instead of false in some cases.
		return $ret ? true : false;
	}

	/**
	 * Is this the administrative section of the component?
	 *
	 * @return  boolean
	 * @see PlatformInterface::isBackend()
	 *
	 */
	public function isBackend()
	{
		[$isCli, $isAdmin] = $this->isCliAdmin();

		return $isAdmin && !$isCli;
	}

	/**
	 * Is this the public section of the component?
	 *
	 * @return  boolean
	 * @see PlatformInterface::isFrontend()
	 *
	 */
	public function isFrontend()
	{
		[$isCli, $isAdmin] = $this->isCliAdmin();

		return !$isAdmin && !$isCli && !$this->isApi();
	}

	/**
	 * Is this a component running in a CLI application?
	 *
	 * @return  boolean
	 * @see PlatformInterface::isCli()
	 *
	 */
	public function isCli()
	{
		[$isCli, $isAdmin] = $this->isCliAdmin();

		return !$isAdmin && $isCli;
	}

	public function isApi()
	{
		if (!is_null(static::$isApi))
		{
			return static::$isApi;
		}

		[$isCli, $isAdmin] = $this->isCliAdmin();

		if (version_compare(JVERSION, '3.999.999', 'le') || $isCli || $isAdmin)
		{
			static::$isApi = false;

			return static::$isApi;
		}

		try
		{
			$app         = JFactory::getApplication();
			static::$isApi = $app->isClient('api');
		}
		catch (Exception $e)
		{
			static::$isApi = false;
		}

		if (static::$isApi)
		{
			static::$isCLI   = false;
			static::$isAdmin = false;
		}

		return static::$isApi;
	}

	/**
	 * Is AJAX re-ordering supported? This is 100% Joomla!-CMS specific. All
	 * other platforms should return false and never ask why.
	 *
	 * @return  boolean
	 *
	 * @codeCoverageIgnore
	 * @see PlatformInterface::supportsAjaxOrdering()
	 *
	 */
	public function supportsAjaxOrdering()
	{
		return true;
	}

	/**
	 * Is the global F0F cache enabled?
	 *
	 * @return  boolean
	 *
	 * @codeCoverageIgnore
	 */
	public function isGlobalF0FCacheEnabled()
	{
		return !(defined('JDEBUG') && JDEBUG);
	}

	/**
	 * Retrieves data from the cache. This is supposed to be used for system-side
	 * F0F data, not application data.
	 *
	 * @param   string  $key      The key of the data to retrieve
	 * @param   string  $default  The default value to return if the key is not found or the cache is not populated
	 *
	 * @return  string  The cached value
	 */
	public function getCache($key, $default = null)
	{
		$registry = $this->getCacheObject();

		return $registry->get($key, $default);
	}

	/**
	 * Saves something to the cache. This is supposed to be used for system-wide
	 * F0F data, not application data.
	 *
	 * @param   string  $key      The key of the data to save
	 * @param   string  $content  The actual data to save
	 *
	 * @return  boolean  True on success
	 */
	public function setCache($key, $content)
	{
		$registry = $this->getCacheObject();

		$registry->set($key, $content);

		return $this->saveCache();
	}

	/**
	 * Clears the cache of system-wide F0F data. You are supposed to call this in
	 * your components' installation script post-installation and post-upgrade
	 * methods or whenever you are modifying the structure of database tables
	 * accessed by F0F. Please note that F0F's cache never expires and is not
	 * purged by Joomla!. You MUST use this method to manually purge the cache.
	 *
	 * @return  boolean  True on success
	 */
	public function clearCache()
	{
		$false = false;
		$cache = JFactory::getCache('fof', '');
		$cache->store($false, 'cache', 'fof');
	}

	/**
	 * Returns an object that holds the configuration of the current site.
	 *
	 * @return  Registry
	 *
	 * @codeCoverageIgnore
	 */
	public function getConfig()
	{
		return JFactory::getConfig();
	}

	/**
	 * logs in a user
	 *
	 * @param   array  $authInfo  Authentication information
	 *
	 * @return  boolean  True on success
	 */
	public function loginUser($authInfo)
	{
		$options = ['remember' => false];

		$response         = new JAuthenticationResponse();
		$response->type   = 'fof';
		$response->status = JAuthentication::STATUS_FAILURE;

		if (isset($authInfo['username']))
		{
			$authenticate = JAuthentication::getInstance();
			$response     = $authenticate->authenticate($authInfo, $options);
		}

		// Use our own authentication handler, onFOFUserAuthenticate, as a fallback
		if ($response->status != JAuthentication::STATUS_SUCCESS)
		{
			$this->container->platform->importPlugin('user');
			$this->container->platform->importPlugin('fof');
			$pluginResults = $this->container->platform->runPlugins('onFOFUserAuthenticate', [$authInfo, $options]);

			/**
			 * Loop through all plugin results until we find a successful login. On failure we fall back to Joomla's
			 * previous authentication response.
			 */
			foreach ($pluginResults as $result)
			{
				if (empty($result))
				{
					continue;
				}

				if (!is_object($result) || !($result instanceof JAuthenticationResponse))
				{
					continue;
				}

				if ($result->status != JAuthentication::STATUS_SUCCESS)
				{
					continue;
				}

				$response = $result;

				break;
			}
		}

		// User failed to authenticate: maybe he enabled two factor authentication?
		// Let's try again "manually", skipping the check vs two factor auth
		// Due the big mess with encryption algorithms and libraries, we are doing this extra check only
		// if we're in Joomla 2.5.18+ or 3.2.1+
		if ($response->status != JAuthentication::STATUS_SUCCESS && method_exists('JUserHelper', 'verifyPassword') && isset($authInfo['username']))
		{
			$db     = JFactory::getDbo();
			$query  = $db->getQuery(true)
				->select($db->qn(['id', 'password']))
				->from('#__users')
				->where('username=' . $db->quote($authInfo['username']));
			$result = $db->setQuery($query)->loadObject();

			if ($result)
			{
				$match = UserHelper::verifyPassword($authInfo['password'], $result->password, $result->id);

				if ($match === true)
				{
					// Bring this in line with the rest of the system
					$user               = User::getInstance($result->id);
					$response->email    = $user->email;
					$response->fullname = $user->name;

					[$isCli, $isAdmin] = $this->isCliAdmin();

					if ($isAdmin)
					{
						$response->language = $user->getParam('admin_language');
					}
					else
					{
						$response->language = $user->getParam('language');
					}

					$response->status        = JAuthentication::STATUS_SUCCESS;
					$response->error_message = '';
				}
			}
		}

		if ($response->status == JAuthentication::STATUS_SUCCESS)
		{
			$this->importPlugin('user');
			$results = $this->runPlugins('onLoginUser', [(array) $response, $options]);

			unset($results); // Just to make phpStorm happy

			$userid = UserHelper::getUserId($response->username);
			$user   = $this->getUser($userid);

			$session = $this->container->session;
			$session->set('user', $user);

			return true;
		}

		return false;
	}

	/**
	 * logs out a user
	 *
	 * @return  boolean  True on success
	 */
	public function logoutUser()
	{
		$app        = JFactory::getApplication();
		$user       = $this->getUser();
		$options    = ['remember' => false];
		$parameters = [
			'username' => $user->username,
			'id'       => $user->id,
		];

		// Set clientid in the options array if it hasn't been set already and shared sessions are not enabled.
		if (!$app->get('shared_session', '0'))
		{
			$options['clientid'] = $app->getClientId();
		}

		$ret = $app->triggerEvent('onUserLogout', [$parameters, $options]);

		return !in_array(false, $ret, true);
	}

	/**
	 * Add a log file for FOF
	 *
	 * @param   string  $file
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 */
	public function logAddLogger($file)
	{
		Log::addLogger(['text_file' => $file], Log::ALL, ['fof']);
	}

	/**
	 * Logs a deprecated practice. In Joomla! this results in the $message being output in the
	 * deprecated log file, found in your site's log directory.
	 *
	 * @param   string  $message  The deprecated practice log message
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 */
	public function logDeprecated($message)
	{
		Log::add($message, Log::WARNING, 'deprecated');
	}

	/**
	 * Adds a message to the application's debug log
	 *
	 * @param   string  $message
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 */
	public function logDebug($message)
	{
		Log::add($message, Log::DEBUG, 'fof');
	}

	/**
	 * Adds a message
	 *
	 * @param   string|array  $title      A title, or an array of additional fields to add to the log entry
	 * @param   string        $logText    The translation key to the log text
	 * @param   string        $extension  The name of the extension logging this entry
	 * @param   User|null     $user       The user the action is being logged for
	 *
	 * @return  void
	 */
	public function logUserAction($title, $logText, $extension, $user = null)
	{
		static $joomlaModelAdded = false;

		// Find out which Joomla version I am running under
		$isJoomla4 = version_compare(JVERSION, '3.999.999', 'gt');
		$isJoomla3 = !$isJoomla4 && version_compare(JVERSION, '3.9.0', 'ge');

		// I need Joomla! 3.9 or later
		if (!$isJoomla4 && !$isJoomla3)
		{
			return;
		}

		/**
		 * Do not perform user actions logging under CLI and Joomla 3.
		 *
		 * In Joomla 3 the ActionlogsModelActionlog always goes through JFactory to get the current user. However, this
		 * goes through the session which is normally not initialized under CLI – that's why we needed to come up with
		 * our own CLI implementation to begin with.
		 *
		 * Joomla 4 doesn't have that problem. J4 CLI scripts use a CLI-aware session service provider.
		 */
		if ($isJoomla3 && $this->isCli())
		{
			return;
		}

		// Include required Joomla Model. Only applicable on Joomla 3.
		if ($isJoomla3 && !$joomlaModelAdded)
		{
			BaseDatabaseModel::addIncludePath(JPATH_ROOT . '/administrator/components/com_actionlogs/models', 'ActionlogsModel');
			$joomlaModelAdded = true;
		}

		if (is_null($user))
		{
			$user = $this->getUser();
		}

		// No log for guest users
		if ($user->guest)
		{
			return;
		}

		$message = [
			'title'       => $title,
			'username'    => $user->username,
			'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id,
		];

		if (is_array($title))
		{
			unset ($message['title']);

			$message = array_merge($message, $title);
		}

		if (version_compare(JVERSION, '3.999.999', 'le'))
		{
			/** @var ActionlogsModelActionlog $model * */
			try
			{
				$model = BaseDatabaseModel::getInstance('Actionlog', 'ActionlogsModel');
			}
			catch (Exception $e)
			{
				return;
			}
		}
		else
		{
			try
			{
				/** @var MVCFactoryInterface $factory */
				$factory = JFactory::getApplication()->bootComponent('com_actionlogs')->getMVCFactory();
				/** @var ActionlogModel $model */
				$model = $factory->createModel('Actionlog', 'Administrator');
			}
			catch (Exception $e)
			{
				return;
			}
		}

		try
		{
			$model->addLog([$message], $logText, $extension, $user->id);
		}
		catch (Exception $e)
		{
			return;
		}
	}

	/**
	 * Returns the root URI for the request.
	 *
	 * @param   boolean  $pathonly  If false, prepend the scheme, host and port information. Default is false.
	 * @param   string   $path      The path
	 *
	 * @return  string  The root URI string.
	 *
	 * @codeCoverageIgnore
	 */
	public function URIroot($pathonly = false, $path = null)
	{
		return JUri::root($pathonly, $path);
	}

	/**
	 * Returns the base URI for the request.
	 *
	 * @param   boolean  $pathonly  If false, prepend the scheme, host and port information. Default is false.
	 *
	 * @return  string  The base URI string
	 *
	 * @codeCoverageIgnore
	 */
	public function URIbase($pathonly = false)
	{
		return JUri::base($pathonly);
	}

	/**
	 * Method to set a response header.  If the replace flag is set then all headers
	 * with the given name will be replaced by the new one (only if the current platform supports header caching)
	 *
	 * @param   string   $name     The name of the header to set.
	 * @param   string   $value    The value of the header to set.
	 * @param   boolean  $replace  True to replace any headers with the same name.
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 */
	public function setHeader($name, $value, $replace = false)
	{
		JFactory::getApplication()->setHeader($name, $value, $replace);
	}

	/**
	 * In platforms that perform header caching, send all headers.
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 */
	public function sendHeaders()
	{
		JFactory::getApplication()->sendHeaders();
	}

	/**
	 * Immediately terminate the containing application's execution
	 *
	 * @param   int  $code  The result code which should be returned by the application
	 *
	 * @return  void
	 */
	public function closeApplication($code = 0)
	{
		// Necessary workaround for broken System - Page Cache plugin in Joomla! 3.7.0
		$this->bugfixJoomlaCachePlugin();

		JFactory::getApplication()->close($code);
	}

	/**
	 * Perform a redirection to a different page, optionally enqueuing a message for the user.
	 *
	 * @param   string  $url     The URL to redirect to
	 * @param   int     $status  (optional) The HTTP redirection status code, default 303 (See Other)
	 * @param   string  $msg     (optional) A message to enqueue
	 * @param   string  $type    (optional) The message type, e.g. 'message' (default), 'warning' or 'error'.
	 *
	 * @return  void
	 *
	 * @throws  Exception
	 */
	public function redirect($url, $status = 303, $msg = '', $type = 'message')
	{
		// Necessary workaround for broken System - Page Cache plugin in Joomla! 3.7.0
		$this->bugfixJoomlaCachePlugin();

		$app = JFactory::getApplication();

		if (class_exists('JApplicationCms') && class_exists('JApplicationWeb')
			&& ($app instanceof JApplicationCms)
			&& ($app instanceof JApplicationWeb))
		{
			// In modern Joomla! versions we have versatility on setting the message and the redirection HTTP code
			if (!empty($msg))
			{
				if (empty($type))
				{
					$type = 'message';
				}

				$app->enqueueMessage($msg, $type);
			}

			$app->redirect($url, $status);
		}

		/**
		 * If you're here, you have an ancient Joomla version and we have to use the legacy four parameter method...
		 * Note that we can't set a custom HTTP code, we can only tell it if it's a permanent redirection or not.
		 */
		$app->redirect($url, $msg, $type, $status == 301);
	}

	/**
	 * Handle an exception in a way that results to an error page. We use this under Joomla! to work around a bug in
	 * Joomla! 3.7 which results in error pages leading to white pages because Joomla's System - Page Cache plugin is
	 * broken.
	 *
	 * @param   Exception  $exception  The exception to handle
	 *
	 * @throws  Exception  We rethrow the exception
	 */
	public function showErrorPage(Exception $exception)
	{
		// Necessary workaround for broken System - Page Cache plugin in Joomla! 3.7.0
		$this->bugfixJoomlaCachePlugin();

		throw $exception;
	}

	/**
	 * Set a variable in the user session
	 *
	 * @param   string  $name       The name of the variable to set
	 * @param   string  $value      (optional) The value to set it to, default is null
	 * @param   string  $namespace  (optional) The variable's namespace e.g. the component name. Default: 'default'
	 *
	 * @return  void
	 */
	public function setSessionVar($name, $value = null, $namespace = 'default')
	{
		// CLI
		if ($this->isCli() && !class_exists('FOFApplicationCLI'))
		{
			static::$fakeSession->set("$namespace.$name", $value);

			return;
		}

		// Joomla 3
		if (version_compare(JVERSION, '3.9999.9999', 'le'))
		{
			$this->container->session->set($name, $value, $namespace);
		}

		// Joomla 4
		if (empty($namespace))
		{
			$this->container->session->set($name, $value);

			return;
		}

		$registry = $this->container->session->get('registry');

		if (is_null($registry))
		{
			$registry = new Registry();

			$this->container->session->set('registry', $registry);
		}

		$registry->set($namespace . '.' . $name, $value);
	}

	/**
	 * Get a variable from the user session
	 *
	 * @param   string  $name       The name of the variable to set
	 * @param   string  $default    (optional) The default value to return if the variable does not exit, default: null
	 * @param   string  $namespace  (optional) The variable's namespace e.g. the component name. Default: 'default'
	 *
	 * @return  mixed
	 */
	public function getSessionVar($name, $default = null, $namespace = 'default')
	{
		// CLI
		if ($this->isCli() && !class_exists('FOFApplicationCLI'))
		{
			return static::$fakeSession->get("$namespace.$name", $default);
		}

		// Joomla 3
		if (version_compare(JVERSION, '3.9999.9999', 'le'))
		{
			return $this->container->session->get($name, $default, $namespace);
		}

		// Joomla 4
		if (empty($namespace))
		{
			return $this->container->session->get($name, $default);
		}

		$registry = $this->container->session->get('registry');

		if (is_null($registry))
		{
			$registry = new Registry();

			$this->container->session->set('registry', $registry);
		}

		return $registry->get($namespace . '.' . $name, $default);
	}

	/**
	 * Unset a variable from the user session
	 *
	 * @param   string  $name       The name of the variable to unset
	 * @param   string  $namespace  (optional) The variable's namespace e.g. the component name. Default: 'default'
	 *
	 * @return  void
	 */
	public function unsetSessionVar($name, $namespace = 'default')
	{
		$this->setSessionVar($name, null, $namespace);
	}

	/**
	 * Return the session token. Two types of tokens can be returned:
	 *
	 * Session token ($formToken == false): Used for anti-spam protection of forms. This is specific to a session
	 *   object.
	 *
	 * Form token ($formToken == true): A secure hash of the user ID with the session token. Both the session and the
	 *   user are fetched from the application container. They are interpolated with the site's secret and passed
	 *   through MD5, making this harder to spoof than the plain old session token.
	 *
	 * @param   bool  $formToken  Should I return a form token?
	 * @param   bool  $forceNew   Should I force the creation of a new token?
	 *
	 * @return  mixed
	 */
	public function getToken($formToken = false, $forceNew = false)
	{
		// For CLI apps we implement our own fake token system
		if ($this->isCli())
		{
			$token = $this->getSessionVar('session.token');

			// Create a token
			if (is_null($token) || $forceNew)
			{
				$token = UserHelper::genRandomPassword(32);
				$this->setSessionVar('session.token', $token);
			}

			if (!$formToken)
			{
				return $token;
			}

			$user = $this->getUser();

			return ApplicationHelper::getHash($user->id . $token);
		}

		// Web application, go through the regular Joomla! API.
		if ($formToken)
		{
			return JSession::getFormToken($forceNew);
		}

		return $this->container->session->getToken($forceNew);
	}

	/** @inheritDoc */
	public function addScriptOptions($key, $value, $merge = true)
	{
		/** @var HtmlDocument $document */
		$document = $this->getDocument();

		if (!method_exists($document, 'addScriptOptions'))
		{
			return;
		}

		$document->addScriptOptions($key, $value, $merge);
	}

	/** @inheritDoc */
	public function getScriptOptions($key = null)
	{
		/** @var HtmlDocument $document */
		$document = $this->getDocument();

		if (!method_exists($document, 'getScriptOptions'))
		{
			return [];
		}

		return $document->getScriptOptions($key);
	}

	/**
	 * Main function to detect if we're running in a CLI environment and we're admin
	 *
	 * @return  array  isCLI and isAdmin. It's not an associative array, so we can use list().
	 */
	protected function isCliAdmin()
	{
		if (is_null(static::$isCLI) && is_null(static::$isAdmin))
		{
			static::$isCLI   = false;
			static::$isAdmin = false;

			try
			{
				if (is_null(JFactory::$application))
				{
					static::$isCLI   = true;
					static::$isAdmin = false;

					return [static::$isCLI, static::$isAdmin];
				}

				$app           = JFactory::getApplication();
				static::$isCLI = $app instanceof Exception;

				if (class_exists('Joomla\CMS\Application\CliApplication'))
				{
					static::$isCLI = static::$isCLI || $app instanceof JApplicationCli;
				}

				if (class_exists('Joomla\CMS\Application\ConsoleApplication'))
				{
					static::$isCLI = static::$isCLI || ($app instanceof ConsoleApplication);
				}
			}
			catch (Exception $e)
			{
				static::$isCLI = true;
			}

			if (static::$isCLI)
			{
				return [static::$isCLI, static::$isAdmin];
			}

			try
			{
				$app = JFactory::getApplication();
			}
			catch (Exception $e)
			{
				return [static::$isCLI, static::$isAdmin];
			}

			if (method_exists($app, 'isAdmin'))
			{
				static::$isAdmin = $app->isAdmin();
			}
			elseif (method_exists($app, 'isClient'))
			{
				static::$isAdmin = $app->isClient('administrator');
			}
		}

		return [static::$isCLI, static::$isAdmin];
	}

	/**
	 * Gets a reference to the cache object, loading it from the disk if
	 * needed.
	 *
	 * @param   boolean  $force  Should I forcibly reload the registry?
	 *
	 * @return  Registry
	 */
	private function &getCacheObject($force = false)
	{
		// Check if we have to load the cache file or we are forced to do that
		if (is_null($this->_cache) || $force)
		{
			// Try to get data from Joomla!'s cache
			$cache        = JFactory::getCache('fof', '');
			$this->_cache = $cache->get('cache', 'fof');

			$isRegistry = is_object($this->_cache);

			if ($isRegistry)
			{
				$isRegistry = class_exists('JRegistry') ? ($this->_cache instanceof Registry) : ($this->_cache instanceof Registry);
			}

			if (!$isRegistry)
			{
				// Create a new Registry object
				$this->_cache = class_exists('JRegistry') ? new Registry() : new Registry();
			}
		}

		return $this->_cache;
	}

	/**
	 * Save the cache object back to disk
	 *
	 * @return  boolean  True on success
	 */
	private function saveCache()
	{
		// Get the Registry object of our cached data
		$registry = $this->getCacheObject();

		$cache = JFactory::getCache('fof', '');

		return $cache->store($registry, 'cache', 'fof');
	}

	/**
	 * Joomla! 3.7 has a broken System - Page Cache plugin. When this plugin is enabled it FORCES the caching of all
	 * pages as soon as Joomla! starts loading, before the plugin has a chance to request to not be cached. Event worse,
	 * in case of a redirection, it doesn't try to remove the cache lock. This means that the next request will be
	 * treated as though the result of the page should be cached. Since there is NO cache content for the page Joomla!
	 * returns an empty response with a 200 OK header. This will, of course, get in the way of every single attempt to
	 * perform a redirection in the frontend of the site.
	 */
	private function bugfixJoomlaCachePlugin()
	{
		// Only Joomla! 3.7 and later is broken.
		if (version_compare(JVERSION, '3.6.999', 'le'))
		{
			return;
		}

		// Only do something when the System - Cache plugin is activated
		if (!class_exists('PlgSystemCache'))
		{
			return;
		}

		// Forcibly uncache the current request
		$options = [
			'defaultgroup' => 'page',
			'browsercache' => false,
			'caching'      => false,
		];

		$cache_key = JUri::getInstance()->toString();
		JCache::getInstance('page', $options)->cache->remove($cache_key, 'page');
	}
}
sql/postgresql.xml000060400000001150152455514240010270 0ustar00<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ @package   FOF
  ~ @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
  ~ @license   GNU General Public License version 2, or later
  -->

<schema>
	<meta>
		<drivers>
			<driver>postgres</driver>
			<driver>postgresql</driver>
		</drivers>
	</meta>

	<sql>
		<action table="#__akeeba_common" canfail="0">
			<condition type="missing" value=""/>
			<query><![CDATA[
CREATE TABLE IF NOT EXISTS "#__akeeba_common" (
	"key" character varying(192) NOT NULL,
	"value" text NOT NULL,
	PRIMARY KEY ("key")
);
			]]></query>
		</action>
	</sql>
</schema>sql/mysql.xml000060400000002427152455514240007242 0ustar00<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ @package   FOF
  ~ @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
  ~ @license   GNU General Public License version 2, or later
  -->

<schema>
	<meta>
		<drivers>
			<driver>mysql</driver>
			<driver>mysqli</driver>
			<driver>pdomysql</driver>
		</drivers>
		<!-- We don't want this table to be converted to a different charset automatically. We will only convert a single
		 column. -->
		<autocollation>false</autocollation>
	</meta>

	<sql>
		<action table="#__akeeba_common" canfail="0">
			<condition type="missing" value=""/>
			<query><![CDATA[
CREATE TABLE IF NOT EXISTS `#__akeeba_common` (
	`key` varchar(190) NOT NULL,
	`value` longtext NOT NULL,
	PRIMARY KEY (`key`)
) DEFAULT COLLATE utf8_general_ci CHARSET=utf8;
			]]></query>
		</action>

		<action table="#__akeeba_common" canfail="1">
			<condition type="utf8mb4upgrade"/>
			<query><![CDATA[
ALTER TABLE `#__akeeba_common` MODIFY COLUMN `value` longtext COLLATE utf8mb4_unicode_ci NOT NULL;
			]]></query>
		</action>

		<action table="#__akeeba_common" canfail="1">
			<condition type="true" />
			<query><![CDATA[
ALTER TABLE `#__akeeba_common` MODIFY COLUMN `key` varchar(190) COLLATE utf8_unicode_ci NOT NULL;
			]]></query>
		</action>
	</sql>
</schema>sql/sqlsrv.xml000060400000001300152455514240007414 0ustar00<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ @package   FOF
  ~ @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
  ~ @license   GNU General Public License version 2, or later
  -->

<schema>
	<meta>
		<drivers>
			<driver>sqlsrv</driver>
			<driver>sqlazure</driver>
		</drivers>
	</meta>

	<sql>
		<action table="#__akeeba_common" canfail="0">
			<condition type="missing" value=""/>
			<query><![CDATA[
CREATE TABLE [#__akeeba_common] (
	[key] [NVARCHAR](192) NOT NULL,
	[value] [TEXT] NOT NULL,
	CONSTRAINT [PK_#__akeeba_common] PRIMARY KEY CLUSTERED
	(
		[key] ASC
	) WITH (STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF)
)
			]]></query>
		</action>
	</sql>
</schema>Pimple/Container.php000060400000020057152455514240010434 0ustar00<?php
/*
 * This file is part of Pimple.
 *
 * Copyright (c) 2009 Fabien Potencier
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

namespace FOF30\Pimple;

defined('_JEXEC') || die;

use ArrayAccess;
use InvalidArgumentException;
use RuntimeException;
use SplObjectStorage;

/**
 * Container main class.
 *
 * @author  Fabien Potencier
 */
class Container implements ArrayAccess
{
	private $values = [];
	private $factories;
	private $protected;
	private $frozen = [];
	private $raw = [];
	private $keys = [];

	/**
	 * Instantiate the container.
	 *
	 * Objects and parameters can be passed as argument to the constructor.
	 *
	 * @param   array  $values  The parameters or objects.
	 */
	public function __construct(array $values = [])
	{
		$this->factories = new SplObjectStorage();
		$this->protected = new SplObjectStorage();

		foreach ($values as $key => $value)
		{
			$this->offsetSet($key, $value);
		}
	}

	/**
	 * Sets a parameter or an object.
	 *
	 * Objects must be defined as Closures.
	 *
	 * Allowing any PHP callable leads to difficult to debug problems
	 * as function names (strings) are callable (creating a function with
	 * the same name as an existing parameter would break your container).
	 *
	 * @param   string  $id     The unique identifier for the parameter or object
	 * @param   mixed   $value  The value of the parameter or a closure to define an object
	 *
	 * @throws RuntimeException Prevent override of a frozen service
	 */
	public function offsetSet($id, $value)
	{
		if (isset($this->frozen[$id]))
		{
			throw new RuntimeException(sprintf('Cannot override frozen service "%s".', $id));
		}

		$this->values[$id] = $value;
		$this->keys[$id]   = true;
	}

	/**
	 * Gets a parameter or an object.
	 *
	 * @param   string  $id  The unique identifier for the parameter or object
	 *
	 * @return mixed The value of the parameter or an object
	 *
	 * @throws InvalidArgumentException if the identifier is not defined
	 */
	public function offsetGet($id)
	{
		if (!isset($this->keys[$id]))
		{
			throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id));
		}

		if (
			isset($this->raw[$id])
			|| !is_object($this->values[$id])
			|| isset($this->protected[$this->values[$id]])
			|| !method_exists($this->values[$id], '__invoke')
		)
		{
			return $this->values[$id];
		}

		if (isset($this->factories[$this->values[$id]]))
		{
			return $this->values[$id]($this);
		}

		$raw            = $this->values[$id];
		$val            = $this->values[$id] = $raw($this);
		$this->raw[$id] = $raw;

		$this->frozen[$id] = true;

		return $val;
	}

	/**
	 * Checks if a parameter or an object is set.
	 *
	 * @param   string  $id  The unique identifier for the parameter or object
	 *
	 * @return bool
	 */
	public function offsetExists($id)
	{
		return isset($this->keys[$id]);
	}

	/**
	 * Unsets a parameter or an object.
	 *
	 * @param   string  $id  The unique identifier for the parameter or object
	 */
	public function offsetUnset($id)
	{
		if (isset($this->keys[$id]))
		{
			if (is_object($this->values[$id]))
			{
				unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]);
			}

			unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]);
		}
	}

	/**
	 * Marks a callable as being a factory service.
	 *
	 * @param   callable  $callable  A service definition to be used as a factory
	 *
	 * @return callable The passed callable
	 *
	 * @throws InvalidArgumentException Service definition has to be a closure of an invokable object
	 */
	public function factory($callable)
	{
		if (!is_object($callable) || !method_exists($callable, '__invoke'))
		{
			throw new InvalidArgumentException('Service definition is not a Closure or invokable object.');
		}

		$this->factories->attach($callable);

		return $callable;
	}

	/**
	 * Protects a callable from being interpreted as a service.
	 *
	 * This is useful when you want to store a callable as a parameter.
	 *
	 * @param   callable  $callable  A callable to protect from being Εν4LυΑ+ΕD (I have to use a stupid mix Greek and
	 *                               leetspeak because low quality hosts blacklist files due to their file scanners
	 *                               being utterly broken)
	 *
	 * @return callable The passed callable
	 *
	 * @throws InvalidArgumentException Service definition has to be a closure of an invokable object
	 */
	public function protect($callable)
	{
		if (!is_object($callable) || !method_exists($callable, '__invoke'))
		{
			throw new InvalidArgumentException('Callable is not a Closure or invokable object.');
		}

		$this->protected->attach($callable);

		return $callable;
	}

	/**
	 * Gets a parameter or the closure defining an object.
	 *
	 * @param   string  $id  The unique identifier for the parameter or object
	 *
	 * @return mixed The value of the parameter or the closure defining an object
	 *
	 * @throws InvalidArgumentException if the identifier is not defined
	 */
	public function raw($id)
	{
		if (!isset($this->keys[$id]))
		{
			throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id));
		}

		if (isset($this->raw[$id]))
		{
			return $this->raw[$id];
		}

		return $this->values[$id];
	}

	/**
	 * Extends an object definition.
	 *
	 * Useful when you want to extend an existing object definition,
	 * without necessarily loading that object.
	 *
	 * @param   string    $id        The unique identifier for the object
	 * @param   callable  $callable  A service definition to extend the original
	 *
	 * @return callable The wrapped callable
	 *
	 * @throws InvalidArgumentException if the identifier is not defined or not a service definition
	 */
	public function extend($id, $callable)
	{
		if (!isset($this->keys[$id]))
		{
			throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id));
		}

		if (!is_object($this->values[$id]) || !method_exists($this->values[$id], '__invoke'))
		{
			throw new InvalidArgumentException(sprintf('Identifier "%s" does not contain an object definition.', $id));
		}

		if (!is_object($callable) || !method_exists($callable, '__invoke'))
		{
			throw new InvalidArgumentException('Extension service definition is not a Closure or invokable object.');
		}

		$factory = $this->values[$id];

		$extended = function ($c) use ($callable, $factory) {
			return $callable($factory($c), $c);
		};

		if (isset($this->factories[$factory]))
		{
			$this->factories->detach($factory);
			$this->factories->attach($extended);
		}

		return $this[$id] = $extended;
	}

	/**
	 * Returns all defined value names.
	 *
	 * @return array An array of value names
	 */
	public function keys()
	{
		return array_keys($this->values);
	}

	/**
	 * Registers a service provider.
	 *
	 * @param   ServiceProviderInterface  $provider  A ServiceProviderInterface instance
	 * @param   array                     $values    An array of values that customizes the provider
	 *
	 * @return static
	 */
	public function register(ServiceProviderInterface $provider, array $values = [])
	{
		$provider->register($this);

		foreach ($values as $key => $value)
		{
			$this[$key] = $value;
		}

		return $this;
	}
}
Pimple/ServiceProviderInterface.php000060400000003135152455514240013444 0ustar00<?php
/*
 * This file is part of Pimple.
 *
 * Copyright (c) 2009 Fabien Potencier
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

namespace FOF30\Pimple;

defined('_JEXEC') || die;

/**
 * Pimple service provider interface.
 *
 * @author  Fabien Potencier
 * @author  Dominik Zogg
 */
interface ServiceProviderInterface
{
	/**
	 * Registers services on the given container.
	 *
	 * This method should only be used to configure services and parameters.
	 * It should not get services.
	 *
	 * @param   Container  $pimple  An Container instance
	 */
	public function register(Container $pimple);
}
Model/DataModel.php000060400000333522152455514240010162 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Controller\Exception\LockedRecord;
use FOF30\Date\Date;
use FOF30\Event\Dispatcher;
use FOF30\Event\Observer;
use FOF30\Model\DataModel\Collection as DataCollection;
use FOF30\Model\DataModel\Exception\BaseException;
use FOF30\Model\DataModel\Exception\CannotLockNotLoadedRecord;
use FOF30\Model\DataModel\Exception\InvalidSearchMethod;
use FOF30\Model\DataModel\Exception\NoAssetKey;
use FOF30\Model\DataModel\Exception\NoContentType;
use FOF30\Model\DataModel\Exception\NoItemsFound;
use FOF30\Model\DataModel\Exception\NoTableColumns;
use FOF30\Model\DataModel\Exception\RecordNotLoaded;
use FOF30\Model\DataModel\Exception\SpecialColumnMissing;
use FOF30\Model\DataModel\Relation\Exception\RelationNotFound;
use FOF30\Model\DataModel\RelationManager;
use FOF30\Utils\ArrayHelper;
use InvalidArgumentException;
use JDatabaseDriver;
use JDatabaseQuery;
use JLoader;
use Joomla\CMS\Access\Rules;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Table\Asset;
use Joomla\CMS\Table\ContentType;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Table\TableInterface;
use Joomla\CMS\UCM\UCMContent;
use RuntimeException;
use UnexpectedValueException;

/**
 * Data-aware model, implementing a convenient ORM
 *
 * Type hinting -- start
 *
 * @method $this hasOne() hasOne(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
 * @method $this belongsTo() belongsTo(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
 * @method $this hasMany() hasMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
 * @method $this belongsToMany() belongsToMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null, string $pivotTable = null, string $pivotLocalKey = null, string $pivotForeignKey = null)
 *
 * @method $this filter_order() filter_order(string $orderingField)
 * @method $this filter_order_Dir() filter_order_Dir(string $direction)
 * @method $this limit() limit(int $limit)
 * @method $this limitstart() limitstart(int $limitStart)
 * @method $this enabled() enabled(int $enabled)
 * @method DataModel getNew() getNew(string $relationName)
 *
 * @property  int    $enabled      Publish status of this record
 * @property  int    $ordering     Sort ordering of this record
 * @property  int    $created_by   ID of the user who created this record
 * @property  string $created_on   Date/time stamp of record creation
 * @property  int    $modified_by  ID of the user who modified this record
 * @property  string $modified_on  Date/time stamp of record modification
 * @property  int    $locked_by    ID of the user who locked this record
 * @property  string $locked_on    Date/time stamp of record locking
 *
 * Type hinting -- end
 */
class DataModel extends Model implements TableInterface
{
	/** @var   array  A list of tables in the database */
	protected static $tableCache = [];

	/** @var   array  A list of table fields, keyed per table */
	protected static $tableFieldCache = [];

	/** @var   array  A list of permutations of the prefix with upper/lowercase letters */
	protected static $prefixCasePermutations = [];

	/** @var   array  Table field name aliases, defined as aliasFieldName => actualFieldName */
	protected $aliasFields = [];

	/** @var   boolean  Should I run automatic checks on the table data? */
	protected $autoChecks = true;

	/** @var   boolean  Should I auto-fill the fields of the model object when constructing it? */
	protected $autoFill = false;

	/** @var   Dispatcher  An event dispatcher for model behaviours */
	protected $behavioursDispatcher = null;

	/** @var   JDatabaseDriver  The database driver for this model */
	protected $dbo = null;

	/** @var   array  Which fields should be exempt from automatic checks when autoChecks is enabled */
	protected $fieldsSkipChecks = [];

	/** @var   array  Which fields should be auto-filled from the model state (by extent, the request)? */
	protected $fillable = [];

	/** @var   array  Which fields should never be auto-filled from the model state (by extent, the request)? */
	protected $guarded = [];

	/** @var   string  The identity field's name */
	protected $idFieldName = '';

	/** @var   array  A hash array with the table fields we know about and their information. Each key is the field name, the value is the field information */
	protected $knownFields = [];

	/** @var   array  The data of the current record */
	protected $recordData = [];

	/** @var   boolean  What will delete() do? True: trash (enabled set to -2); false: hard delete (remove from database) */
	protected $softDelete = false;

	/** @var   string  The name of the database table we connect to */
	protected $tableName = '';

	/** @var   array  A collection of custom, additional where clauses to apply during buildQuery */
	protected $whereClauses = [];

	/** @var   RelationManager  The relation manager of this model */
	protected $relationManager = null;

	/** @var   array  A list of all eager loaded relations and their attached callbacks */
	protected $eagerRelations = [];

	/** @var   array  A list of the relation filter definitions for this model */
	protected $relationFilters = [];

	/** @var   array  A list of the relations which will be auto-touched by save() and touch() methods */
	protected $touches = [];

	/** @var bool Should rows be tracked as ACL assets? */
	protected $_trackAssets = false;

	/** @var bool Does the resource support joomla tags? */
	protected $_has_tags = false;

	/** @var  Rules  The rules associated with this record. */
	protected $_rules;

	/** @var  string  The UCM content type (typically: com_something.viewname, e.g. com_foobar.items) */
	protected $contentType = null;

	/** @var  array  Shared parameters for behaviors */
	protected $_behaviorParams = [];

	/**
	 * The asset key for items in this table. It's usually something in the
	 * com_example.viewname format. They asset name will be this key appended
	 * with the item's ID, e.g. com_example.viewname.123
	 *
	 * @var    string
	 */
	protected $_assetKey = '';

	/**
	 * Public constructor. Overrides the parent constructor, adding support for database-aware models.
	 *
	 * You can use the $config array to pass some configuration values to the object:
	 *
	 * tableName             String   The name of the database table to use. Default: #__appName_viewNamePlural (Ruby
	 * on Rails convention) idFieldName           String   The table key field name. Default:
	 * appName_viewNameSingular_id (Ruby on Rails convention) knownFields           Array    The known fields in the
	 * table. Default: read from the table itself autoChecks            Boolean  Should I turn on automatic data
	 * validation checks? fieldsSkipChecks      Array    List of fields which should not participate in automatic data
	 * validation checks. aliasFields           Array    Associative array of "magic" field aliases.
	 * behavioursDispatcher  EventDispatcher  The model behaviours event dispatcher. behaviourObservers    Array    The
	 * model behaviour observers to attach to the behavioursDispatcher. behaviours            Array    A list of
	 * behaviour names to instantiate and attach to the behavioursDispatcher. fillable_fields       Array    Which
	 * fields should be auto-filled from the model state (by extent, the request)? guarded_fields        Array    Which
	 * fields should never be auto-filled from the model state (by extent, the request)? relations             Array    (hashed)  The relations to autoload on model creation. contentType           String   The UCM content type, e.g. "com_foobar.items"
	 *
	 * Setting either fillable_fields or guarded_fields turns on automatic filling of fields in the constructor. If
	 * both
	 * are set only guarded_fields is taken into account. Fields are not filled automatically outside the constructor.
	 *
	 * @param   Container  $container  The configuration variables to this model
	 * @param   array      $config     Configuration values for this model
	 *
	 * @throws NoTableColumns
	 * @see Model::__construct()
	 *
	 */
	public function __construct(Container $container, array $config = [])
	{
		// First call the parent constructor.
		parent::__construct($container, $config);

		// Should I use a different database object?
		$this->dbo = $container->db;

		// Do I have a table name?
		if (isset($config['tableName']))
		{
			$this->tableName = $config['tableName'];
		}
		elseif (empty($this->tableName))
		{
			// The table name is by default: #__appName_viewNamePlural (Ruby on Rails convention)
			$viewPlural      = $container->inflector->pluralize($this->getName());
			$this->tableName = '#__' . strtolower($this->container->bareComponentName) . '_' . strtolower($viewPlural);
		}

		// Do I have a table key name?
		if (isset($config['idFieldName']))
		{
			$this->idFieldName = $config['idFieldName'];
		}
		elseif (empty($this->idFieldName))
		{
			// The default ID field is: appName_viewNameSingular_id (Ruby on Rails convention)
			$viewSingular      = $container->inflector->singularize($this->getName());
			$this->idFieldName = strtolower($this->container->bareComponentName) . '_' . strtolower($viewSingular) . '_id';
		}

		// Do I have a list of known fields?
		if (isset($config['knownFields']) && !empty($config['knownFields']))
		{
			if (!is_array($config['knownFields']))
			{
				$config['knownFields'] = explode(',', $config['knownFields']);
			}

			$this->knownFields = $config['knownFields'];
		}
		else
		{
			// By default the known fields are fetched from the table itself (slow!)
			$this->knownFields = $this->getTableFields();
		}

		if (empty($this->knownFields))
		{
			throw new NoTableColumns(sprintf('Model %s could not fetch column list for the table %s', $this->getName(), $this->tableName));
		}

		// Should I turn on autoChecks?
		if (isset($config['autoChecks']))
		{
			if (!is_bool($config['autoChecks']))
			{
				$config['autoChecks'] = strtolower($config['autoChecks']);
				$config['autoChecks'] = in_array($config['autoChecks'], ['yes', 'true', 'on', 1]);
			}

			$this->autoChecks = $config['autoChecks'];
		}

		// Should I exempt fields from autoChecks?
		if (isset($config['fieldsSkipChecks']))
		{
			if (!is_array($config['fieldsSkipChecks']))
			{
				$config['fieldsSkipChecks'] = explode(',', $config['fieldsSkipChecks']);
				$config['fieldsSkipChecks'] = array_map(function ($x) {
					return trim($x);
				}, $config['fieldsSkipChecks']);
			}

			$this->fieldsSkipChecks = $config['fieldsSkipChecks'];
		}

		// Do I have alias fields?
		if (isset($config['aliasFields']))
		{
			$this->aliasFields = $config['aliasFields'];
		}

		// Do I have a behaviours dispatcher?
		if (isset($config['behavioursDispatcher']) && ($config['behavioursDispatcher'] instanceof Dispatcher))
		{
			$this->behavioursDispatcher = $config['behavioursDispatcher'];
		}
		// Otherwise create the model behaviours dispatcher
		else
		{
			$this->behavioursDispatcher = new Dispatcher($this->container);
		}

		// Do I have an array of behaviour observers
		if (isset($config['behaviourObservers']) && is_array($config['behaviourObservers']))
		{
			foreach ($config['behaviourObservers'] as $observer)
			{
				$this->behavioursDispatcher->attach($observer);
			}
		}

		// Do I have a list of behaviours?
		if (isset($config['behaviours']) && is_array($config['behaviours']))
		{
			foreach ($config['behaviours'] as $behaviour)
			{
				$this->addBehaviour($behaviour);
			}
		}

		// Add extra behaviours
		foreach (['Created', 'Modified'] as $behaviour)
		{
			$this->addBehaviour($behaviour);
		}

		// Do I have a list of fillable fields?
		if (isset($config['fillable_fields']) && !empty($config['fillable_fields']))
		{
			if (!is_array($config['fillable_fields']))
			{
				$config['fillable_fields'] = explode(',', $config['fillable_fields']);
				$config['fillable_fields'] = array_map(function ($x) {
					return trim($x);
				}, $config['fillable_fields']);
			}

			$this->fillable = [];
			$this->autoFill = true;

			foreach ($config['fillable_fields'] as $field)
			{
				if (array_key_exists($field, $this->knownFields))
				{
					$this->fillable[] = $field;
				}
				elseif (isset($this->aliasFields[$field]))
				{
					$this->fillable[] = $this->aliasFields[$field];
				}
			}
		}

		// Do I have a list of guarded fields?
		if (isset($config['guarded_fields']) && !empty($config['guarded_fields']))
		{
			if (!is_array($config['guarded_fields']))
			{
				$config['guarded_fields'] = explode(',', $config['guarded_fields']);
				$config['guarded_fields'] = array_map(function ($x) {
					return trim($x);
				}, $config['guarded_fields']);
			}

			$this->guarded  = [];
			$this->autoFill = true;

			foreach ($config['guarded_fields'] as $field)
			{
				if (array_key_exists($field, $this->knownFields))
				{
					$this->guarded[] = $field;
				}
				elseif (isset($this->aliasFields[$field]))
				{
					$this->guarded[] = $this->aliasFields[$field];
				}
			}
		}

		// If we are tracking assets, make sure an access field exists and initially set the default.
		$asset_id_field = $this->getFieldAlias('asset_id');
		$access_field   = $this->getFieldAlias('access');

		if (array_key_exists($asset_id_field, $this->knownFields))
		{
			$this->_trackAssets = true;
		}

		/**
		 * if ($this->_trackAssets && array_key_exists($access_field, $this->knownFields) && !($this->getState($access_field, null)))
		 * {
		 * $this->$access_field = (int) $this->container->platform->getConfig()->get('access');
		 * }
		 **/

		$assetKey = $this->container->componentName . '.' . strtolower($container->inflector->singularize($this->getName()));
		$this->setAssetKey($assetKey);

		// Set the UCM content type if applicable
		if (isset($config['contentType']))
		{
			$this->contentType = $config['contentType'];
		}

		// Do I have to auto-fill the fields?
		if ($this->autoFill)
		{
			// If I have guarded fields, I'll try to fill everything, using such fields as a "blacklist"
			if (!empty($this->guarded))
			{
				$fields = array_keys($this->knownFields);
			}
			else
			{
				// Otherwise I'll fill only the fillable ones (act like having a "whitelist")
				$fields = $this->fillable;
			}

			foreach ($fields as $field)
			{
				if (in_array($field, $this->guarded))
				{
					// Do not set guarded fields
					continue;
				}

				$stateValue = $this->getState($field, null);

				if (!is_null($stateValue))
				{
					$this->setFieldValue($field, $stateValue);
				}
			}
		}

		// Create a relation manager
		$this->relationManager = new RelationManager($this);

		// Do I have a list of relations?
		if (isset($config['relations']) && is_array($config['relations']))
		{
			foreach ($config['relations'] as $relConfig)
			{
				if (!is_array($relConfig))
				{
					continue;
				}

				$defaultRelConfig = [
					'type'              => 'hasOne',
					'foreignModelClass' => null,
					'localKey'          => null,
					'foreignKey'        => null,
					'pivotTable'        => null,
					'pivotLocalKey'     => null,
					'pivotForeignKey'   => null,
				];

				$relConfig = array_merge($defaultRelConfig, $relConfig);

				$this->relationManager->addRelation($relConfig['itemName'], $relConfig['type'], $relConfig['foreignModelClass'],
					$relConfig['localKey'], $relConfig['foreignKey'], $relConfig['pivotTable'],
					$relConfig['pivotLocalKey'], $relConfig['pivotForeignKey']);
			}
		}

		// Initialise the data model
		foreach ($this->knownFields as $fieldName => $information)
		{
			// Initialize only the null or not yet set records
			if (!isset($this->recordData[$fieldName]))
			{
				$this->recordData[$fieldName] = $information->Default;
			}
		}

		// Trigger the onAfterConstruct event. This allows you to set up model state etc.
		$this->triggerEvent('onAfterConstruct');
	}

	/**
	 * Magic caller. It works like the magic setter and returns ourselves for chaining. If no arguments are passed we'll
	 * only look for a scope filter.
	 *
	 * @param   string  $name
	 * @param   array   $arguments
	 *
	 * @return  static
	 */
	public function __call($name, $arguments)
	{
		// If no arguments are provided try mapping to the scopeSomething() method
		if (empty($arguments))
		{
			$methodName = 'scope' . ucfirst($name);
			if (method_exists($this, $methodName))
			{
				$this->{$methodName}();

				return $this;
			}
		}

		// Implements getNew($relationName)
		if (($name == 'getNew') && count($arguments))
		{
			return $this->relationManager->getNew($arguments[0]);
		}

		// Magically map relations to methods, e.g. $this->foobar will return the "foobar" relations' contents
		if ($this->relationManager->isMagicMethod($name))
		{
			return call_user_func_array([$this->relationManager, $name], $arguments);
		}

		// Otherwise call the parent
		return parent::__call($name, $arguments);
	}

	/**
	 * Magic checker on a property. It follows the same logic of the __get magic method, however, if nothing is found,
	 * it won't return the state of a variable (we are checking if a property is set)
	 *
	 * @param   string  $name  The name of the field to check
	 *
	 * @return  bool    Is the field set?
	 */
	public function __isset($name)
	{
		$value   = null;
		$isState = false;

		if (substr($name, 0, 3) == 'flt')
		{
			$isState = true;
			$name    = strtolower(substr($name, 3, 1)) . substr($name, 4);
		}

		// If $name is a field name, get its value
		if (!$isState && array_key_exists($name, $this->recordData))
		{
			$value = $this->getFieldValue($name);
		}
		elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
		{
			$name = $this->aliasFields[$name];

			$value = $this->getFieldValue($name);
		}
		elseif ($this->relationManager->isMagicProperty($name))
		{
			$value = $this->relationManager->$name;
		}

		// As the core function isset, the property must exists AND must be NOT null
		return ($value !== null);
	}

	/**
	 * Magic getter. It will return the value of a field or, if no such field is found, the value of the relevant state
	 * variable.
	 *
	 * Tip: Trying to get fltSomething will always return the value of the state variable "something"
	 *
	 * Tip: You can define custom field getter methods as getFieldNameAttribute, where FieldName is your field's name,
	 *      in CamelCase (even if the field name itself is in snake_case).
	 *
	 * @param   string  $name  The name of the field / state variable to retrieve
	 *
	 * @return  static|mixed
	 */
	public function __get($name)
	{
		// Handle $this->input
		if ($name == 'input')
		{
			return $this->container->input;
		}

		$isState = false;

		if (substr($name, 0, 3) == 'flt')
		{
			$isState = true;
			$name    = strtolower(substr($name, 3, 1)) . substr($name, 4);
		}

		// If $name is a field name, get its value
		if (!$isState && array_key_exists($name, $this->recordData))
		{
			return $this->getFieldValue($name);
		}
		elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
		{
			$name = $this->aliasFields[$name];

			return $this->getFieldValue($name);
		}
		elseif ($this->relationManager->isMagicProperty($name))
		{
			return $this->relationManager->$name;
		}
		// If $name is not a field name, get the value of a state variable
		else
		{
			return $this->getState($name);
		}
	}

	/**
	 * Magic setter. It will set the value of a field or the value of a dynamic scope filter, or the value of the
	 * relevant state variable.
	 *
	 * Tip: Trying to set fltSomething will always return the value of the state variable "something"
	 *
	 * Tip: Trying to set scopeSomething will always return the value of the dynamic scope filter "something"
	 *
	 * Tip: You can define custom field setter methods as setFieldNameAttribute, where FieldName is your field's name,
	 *      in CamelCase (even if the field name itself is in snake_case).
	 *
	 * @param   string  $name   The name of the field / scope / state variable to set
	 * @param   mixed   $value  The value to set
	 *
	 * @return  void
	 */
	public function __set($name, $value)
	{
		$isState = false;
		$isScope = false;

		if (substr($name, 0, 3) == 'flt')
		{
			$isState = true;
			$name    = strtolower(substr($name, 3, 1)) . substr($name, 4);
		}
		elseif (substr($name, 0, 5) == 'scope')
		{
			$isScope = true;
			$name    = strtolower(substr($name, 5, 1)) . substr($name, 5);
		}

		// If $name is a field name, set its value
		if (!$isState && !$isScope && array_key_exists($name, $this->recordData))
		{
			$this->setFieldValue($name, $value);
		}
		elseif (!$isState && !$isScope && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
		{
			$name = $this->aliasFields[$name];
			$this->setFieldValue($name, $value);
		}
		// If $name is a dynamic scope filter, set its value
		elseif ($isScope || method_exists($this, 'scope' . ucfirst($name)))
		{
			$method = 'scope' . ucfirst($name);
			$this->{$method}($value);
		}
		// If $name is not a field name, set the value of a state variable
		else
		{
			$this->setState($name, $value);
		}
	}

	/**
	 * Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the
	 * original object. The new object is set up to not save its stats, ignore the request when getting state variables
	 * and comes with an empty state. The temporary object instance has its data reset as well.
	 *
	 * @return  static
	 */
	public function tmpInstance()
	{
		return parent::tmpInstance()->reset(true, true);
	}

	/**
	 * Adds a known field to the DataModel. This is only necessary if you are using a custom buildQuery with JOINs or
	 * field aliases. Please note that you need to make further modifications for bind() and save() to work in this
	 * case. Please refer to the documentation blocks of these methods for more information. It is generally considered
	 * a very BAD idea using JOINs instead of relations. It complicates your life and is bound to cause bugs that are
	 * very hard to track back.
	 *
	 * Basically, if you find yourself using this method you are probably doing something very wrong or very advanced.
	 * If you do not feel confident with debugging FOF code STOP WHATEVER YOU'RE DOING and rethink your Model. Why are
	 * you using a JOIN? If you want to filter the records by a field found in another table you can still use
	 * relations and whereHas with a callback.
	 *
	 * @param   string  $fieldName  The name of the field
	 * @param   mixed   $default    Default value, used by reset() (default: null)
	 * @param   string  $type       Database type for the field. If unsure use 'integer', 'float' or 'text'.
	 * @param   bool    $replace    Should we replace an existing known field definition?
	 *
	 * @return  static
	 */
	public function addKnownField($fieldName, $default = null, $type = 'integer', $replace = false)
	{
		if (array_key_exists($fieldName, $this->knownFields) && !$replace)
		{
			return $this;
		}

		$info = (object) [
			'Default' => $default,
			'Type'    => $type,
			'Null'    => 'YES',
		];

		$this->knownFields[$fieldName] = $info;

		// Initialize only the null or not yet set records
		if (!isset($this->recordData[$fieldName]))
		{
			$this->recordData[$fieldName] = $default;
		}

		return $this;
	}

	/**
	 * Get the columns from database table. For JTableInterface compatibility.
	 *
	 * @return  mixed  An array of the field names, or false if an error occurs.
	 */
	public function getFields()
	{
		return $this->getTableFields();
	}

	/**
	 * Get the columns from a database table.
	 *
	 * @param   string  $tableName  Table name. If null current table is used
	 *
	 * @return  mixed  An array of the field names, or false if an error occurs.
	 */
	public function getTableFields($tableName = null)
	{
		// Make sure we have a list of tables in this db
		if (empty(static::$tableCache))
		{
			static::$tableCache = $this->getDbo()->getTableList();
		}

		if (!$tableName)
		{
			$tableName = $this->tableName;
		}

		// Try to load again column specifications if the table is not loaded OR if it's loaded and
		// the previous call returned an error
		if (!array_key_exists($tableName, static::$tableFieldCache) ||
			(isset(static::$tableFieldCache[$tableName]) && !static::$tableFieldCache[$tableName])
		)
		{
			// Lookup the fields for this table only once.
			$name = $tableName;

			$prefix = $this->getDbo()->getPrefix();

			if (substr($name, 0, 3) == '#__')
			{
				$checkName = $prefix . substr($name, 3);
			}
			else
			{
				$checkName = $name;
			}

			// Iterate through all lower/uppercase permutations of the prefix if we have a prefix with at least one uppercase letter
			if (!in_array($checkName, static::$tableCache) && preg_match('/[A-Z]/', $prefix) && (substr($name, 0, 3) == '#__'))
			{
				$prefixPermutations = $this->getPrefixCasePermutations();
				$partialCheckName   = substr($name, 3);

				foreach ($prefixPermutations as $permutatedPrefix)
				{
					$checkName = $permutatedPrefix . $partialCheckName;

					if (in_array($checkName, static::$tableCache))
					{
						break;
					}
				}
			}

			if (!in_array($checkName, static::$tableCache))
			{
				// The table doesn't exist. Return false.
				static::$tableFieldCache[$tableName] = false;
			}
			else
			{
				$fields = $this->getDbo()->getTableColumns($name, false);

				if (empty($fields))
				{
					$fields = false;
				}

				static::$tableFieldCache[$tableName] = $fields;
			}

			// PostgreSQL date type compatibility
			if (($this->getDbo()->name == 'postgresql') && (static::$tableFieldCache[$tableName] != false))
			{
				foreach (static::$tableFieldCache[$tableName] as $field)
				{
					if (strtolower($field->type) == 'timestamp without time zone')
					{
						if (stristr($field->Default, '\'::timestamp without time zone'))
						{
							[$date, ] = explode('::', $field->Default, 2);
							$field->Default = trim($date, "'");
						}
					}
				}
			}
		}

		return static::$tableFieldCache[$tableName];
	}

	/**
	 * Get the database connection associated with this data Model
	 *
	 * @return  JDatabaseDriver
	 */
	public function getDbo()
	{
		if (!is_object($this->dbo))
		{
			$this->dbo = $this->container->db;
		}

		return $this->dbo;
	}

	/**
	 * Returns the data currently bound to the model in an array format. Similar to toArray() but returns a copy instead
	 * of the internal table itself.
	 *
	 * @return array
	 */
	public function getData()
	{
		$ret = [];

		foreach ($this->knownFields as $field => $info)
		{
			$ret[$field] = $this->getFieldValue($field);
		}

		return $ret;
	}

	/**
	 * Return the value of the identity column of the currently loaded record
	 *
	 * @return   mixed
	 */
	public function getId()
	{
		return $this->{$this->idFieldName};
	}

	/**
	 * Returns the name of the table's id field (primary key) name
	 *
	 * @return  string
	 */
	public function getIdFieldName()
	{
		return $this->idFieldName;
	}

	/**
	 * Alias of getIdFieldName. Used for JTableInterface compatibility.
	 *
	 * @return  string  The name of the primary key for the table.
	 *
	 * @codeCoverageIgnore
	 */
	public function getKeyName()
	{
		return $this->getIdFieldName();
	}

	/**
	 * Returns the database table name this model talks to
	 *
	 * @return  string
	 */
	public function getTableName()
	{
		return $this->tableName;
	}

	/**
	 * Returns the value of a field. If a field is not set it uses the $default value. Automatically uses magic
	 * getter variables if required.
	 *
	 * @param   string  $name     The name of the field to retrieve
	 * @param   mixed   $default  Default value, if the field is not set and doesn't have a getter method
	 *
	 * @return  mixed  The value of the field
	 */
	public function getFieldValue($name, $default = null)
	{
		if (array_key_exists($name, $this->aliasFields))
		{
			$name = $this->aliasFields[$name];
		}

		if (!array_key_exists($name, $this->knownFields))
		{
			return $default;
		}

		if (!isset($this->recordData[$name]))
		{
			$this->recordData[$name] = $default;
		}

		return $this->recordData[$name];
	}

	/**
	 * Sets the value of a field.
	 *
	 * @param   string  $name   The name of the field to set
	 * @param   mixed   $value  The value to set it to
	 *
	 * @return  void
	 */
	public function setFieldValue($name, $value = null)
	{
		if (array_key_exists($name, $this->aliasFields))
		{
			$name = $this->aliasFields[$name];
		}

		if (array_key_exists($name, $this->knownFields))
		{
			$this->recordData[$name] = $value;
		}
	}

	/**
	 * Applies the getSomethingAttribute methods to $this->recordData, converting the database representation of the
	 * data to the record representation. $this->recordData is directly modified.
	 *
	 * @return  void
	 */
	public function databaseDataToRecordData()
	{
		foreach ($this->recordData as $name => $value)
		{
			$method = $this->container->inflector->camelize('get_' . $name . '_attribute');

			if (method_exists($this, $method))
			{
				$this->recordData[$name] = $this->{$method}($value);
			}
		}
	}

	/**
	 * Applies the setSomethingAttribute methods to $this->recordData, converting the record representation to database
	 * representation. It does not modify $this->recordData, it returns a copy of the data array.
	 *
	 * If you are using custom knownFields to cater for table JOINs you need to override this method and _remove_ the
	 * fields which do not belong to the table you are saving to. It's generally a bad idea using JOINs instead of
	 * relations. You have been warned!
	 *
	 * @return  array
	 */
	public function recordDataToDatabaseData()
	{
		$copy = array_merge($this->recordData);

		foreach ($copy as $name => $value)
		{
			$method = $this->container->inflector->camelize('set_' . $name . '_attribute');

			if (method_exists($this, $method))
			{
				$copy[$name] = $this->{$method}($value);
			}
		}

		return $copy;
	}

	/**
	 * Does this model know about a field called $fieldName? Automatically uses aliases when necessary.
	 *
	 * @param   string  $fieldName  Field name to check
	 *
	 * @return  boolean  True if the field exists
	 */
	public function hasField($fieldName)
	{
		$realFieldName = $this->getFieldAlias($fieldName);

		return array_key_exists($realFieldName, $this->knownFields);
	}

	/**
	 * Is this field known to the model and marked as nullable in the database?
	 *
	 * Automatically uses aliases when necessary.
	 *
	 * @param   string  $fieldName  Field name to check
	 *
	 * @return  bool  True if the field is nullable or doesn't exist
	 */
	public function isNullableField(string $fieldName): bool
	{
		if (!$this->hasField($fieldName))
		{
			return true;
		}

		$realFieldName = $this->getFieldAlias($fieldName);

		return strtolower($this->knownFields[$realFieldName]->Null ?? 'YES') == 'yes';
	}

	/**
	 * Get the real name of a field name based on its alias. If the field is not aliased $alias is returned
	 *
	 * @param   string  $alias  The field to get an alias for
	 *
	 * @return  string  The real name of the field
	 */
	public function getFieldAlias($alias)
	{
		if (array_key_exists($alias, $this->aliasFields))
		{
			return $this->aliasFields[$alias];
		}
		else
		{
			return $alias;
		}
	}

	/**
	 * Returns an array mapping relation names to their local key field names.
	 *
	 * For example, given a relation "foobar" with local key name "example_item_id" it will return:
	 * ["foobar" => "example_item_id"]
	 *
	 * @return  array  Array of [relationName => fieldName] arrays
	 *
	 * @throws  RelationNotFound
	 */
	public function getRelationFields()
	{
		$fields = [];

		$relationNames = $this->relationManager->getRelationNames();

		if (empty($relationNames))
		{
			return $fields;
		}

		foreach ($relationNames as $name)
		{
			$fields[$name] = $this->relationManager->getRelation($name)->getLocalKey();
		}

		return $fields;
	}

	/**
	 * Returns the qualified foreign model name, in the format "componentName.modelName", for the specified model
	 * field. First it checks the relations you have defined. If none is found it will try to parse the field name as
	 * following the componentName_modelName_id naming convention (FOF best practice and recommendation).
	 *
	 * This feature is used by the Blade compiler.
	 *
	 * @param   string  $fieldName  The field name for which we'll get a foreign model name
	 *
	 * @return  string
	 */
	public function getForeignModelNameFor($fieldName)
	{
		// First look for a local field mapped in a relationship
		try
		{
			$relationMap  = $this->getRelationFields();
			$relationName = array_search($fieldName, $relationMap);

			if ($relationName !== false)
			{
				$model     = $this->relationManager->getRelation($relationName)->getForeignModel();
				$component = $model->getContainer()->componentName;
				$modelName = $model->getName();

				return "$component.$modelName";
			}
		}
		catch (RelationNotFound $e)
		{
			// Bummer. The relation cannot be found. I will fall back to parsing the field name.
		}

		// Do I have a field following the componentName_modelName_id format?
		$parts = explode('_', $fieldName);
		if ((substr($fieldName, -3) != '_id') || (count($parts) < 3))
		{
			throw new RuntimeException("Cannot determine the foreign model for local field '$fieldName'; it does not follow the expected component_model_id convention.");
		}

		$fieldName = substr($fieldName, 0, -3);
		[$component, $modelName] = explode('_', $fieldName, 2);
		$modelName = $this->container->inflector->camelize($modelName);

		return "$component.$modelName";
	}

	/**
	 * Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
	 * data, unless you provide a $data array.
	 *
	 * Special note if you are using a custom buildQuery with JOINs or field aliases:
	 * You will need to override the recordDataToDatabaseData method. Make sure that you _remove_ or rename any fields
	 * which do not exist in the table defined in $this->tableName. Otherwise Joomla! will not know how to insert /
	 * update the data on the table and will throw an Exception denoting a database error. It is generally a BAD idea
	 * using JOINs instead of relations. If unsure, use relations.
	 *
	 * @param   null|array  $data            [Optional] Data to bind
	 * @param   string      $orderingFilter  A WHERE clause used to apply table item reordering
	 * @param   array       $ignore          A list of fields to ignore when binding $data
	 *
	 * @para    boolean     $resetRelations  Should I automatically reset relations if relation-important fields are
	 *          changed?
	 *
	 * @return   static  Self, for chaining
	 */
	public function save($data = null, $orderingFilter = '', $ignore = null, $resetRelations = true)
	{
		// Stash the primary key
		$oldPKValue = $this->getId();

		// Call the onBeforeSave event
		$this->triggerEvent('onBeforeSave', [&$data]);

		// Get the relation to local field map and initialise the relationsAffected array
		$relationImportantFields = $this->getRelationFields();
		$dataBeforeBind          = [];

		// If we have relations we keep a copy of the data before bind.
		if (count($relationImportantFields))
		{
			$dataBeforeBind = array_merge($this->recordData);
		}

		// Bind any (optional) data. If no data is provided, the current record data is used
		if (!is_null($data))
		{
			$this->bind($data, $ignore);
		}

		// Is this a new record?
		if (empty($oldPKValue))
		{
			$isNewRecord = true;
		}
		else
		{
			$isNewRecord = $oldPKValue != $this->getId();
		}

		// Check the validity of the data
		$this->check();

		// Get the database object
		$db = $this->getDbo();

		// Insert or update the record. Note that the object we use for insertion / update is the a copy holding
		// the transformed data.
		$dataObject = $this->recordDataToDatabaseData();
		$dataObject = (object) $dataObject;

		if ($isNewRecord)
		{
			$this->triggerEvent('onBeforeCreate', [&$dataObject]);

			// Insert the new record
			$db->insertObject($this->tableName, $dataObject, $this->idFieldName);

			// Update ourselves with the new ID field's value
			$this->{$this->idFieldName} = $db->insertid();

			// Rebase the relations with the newly created model
			if ($resetRelations)
			{
				$this->relationManager->rebase($this);
			}

			$this->triggerEvent('onAfterCreate');
		}
		else
		{
			$this->triggerEvent('onBeforeUpdate', [&$dataObject]);

			$db->updateObject($this->tableName, $dataObject, $this->idFieldName, true);

			$this->triggerEvent('onAfterUpdate');
		}

		// If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
		if ($orderingFilter)
		{
			$filterValue = $this->$orderingFilter;
			$this->reorder($orderingFilter ? $db->qn($orderingFilter) . ' = ' . $db->q($filterValue) : '');
		}

		// One more thing... Touch all relations in the $touches array
		if (!empty($this->touches))
		{
			foreach ($this->touches as $relation)
			{
				$records = $this->getRelations()->getData($relation);

				if (!empty($records))
				{
					if ($records instanceof DataModel)
					{
						$records = [$records];
					}

					/** @var DataModel $record */
					foreach ($records as $record)
					{
						$record->touch();
					}
				}
			}
		}

		// If we have relations we compare the data to the copy of the data before bind.
		if (count($relationImportantFields) && $resetRelations)
		{
			// Since array_diff_assoc doesn't work recursively we have to do it the EXCRUCIATINGLY SLOW WAY. Sad panda :(
			$keysRecord     = (is_array($this->recordData) && !empty($this->recordData)) ? array_keys($this->recordData) : [];
			$keysBefore     = (is_array($dataBeforeBind) && !empty($dataBeforeBind)) ? array_keys($dataBeforeBind) : [];
			$keysAll        = array_merge($keysRecord, $keysBefore);
			$keysAll        = array_unique($keysAll);
			$modifiedFields = [];

			foreach ($keysAll as $key)
			{
				if (!isset($dataBeforeBind[$key]) || !isset($this->recordData[$key]))
				{
					$modifiedFields[] = $key;
				}
				elseif ($dataBeforeBind[$key] != $this->recordData[$key])
				{
					$modifiedFields[] = $key;
				}
			}

			unset ($dataBeforeBind);

			if (count($modifiedFields))
			{
				$relationsAffected = [];

				unset($modifiedData);

				foreach ($relationImportantFields as $relationName => $fieldName)
				{
					if (in_array($fieldName, $modifiedFields))
					{
						$relationsAffected[] = $relationName;
					}
				}

				// Reset the relations which are affected by the save. This will force-reload the relations when you try to
				// access them again.
				$this->relationManager->resetRelationData($relationsAffected);
			}
		}

		// Finally, call the onAfterSave event
		$this->triggerEvent('onAfterSave');

		return $this;
	}

	/**
	 * Alias of save. For JTableInterface compatibility.
	 *
	 * @param   boolean  $updateNulls  Blatantly ignored.
	 *
	 * @return  boolean  True on success.
	 */
	public function store($updateNulls = false)
	{
		try
		{
			$this->save();
		}
		catch (Exception $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
	 * data, unless you provide a $data array. On top of that, it also saves all specified relations. If $relations is
	 * null it will save all relations known to this model.
	 *
	 * @param   null|array  $data            [Optional] Data to bind
	 * @param   string      $orderingFilter  A WHERE clause used to apply table item reordering
	 * @param   array       $ignore          A list of fields to ignore when binding $data
	 * @param   array       $relations       Which relations to save with the model's record. Leave null for all
	 *                                       relations
	 *
	 * @return static Self, for chaining
	 */
	public function push($data = null, $orderingFilter = '', $ignore = null, array $relations = null)
	{
		// Store the model's $touches definition
		$touches = $this->touches;

		// If $relations is non-null, remove $relations from $this->touches. Since $relations will be saved, they are
		// implicitly touched. We don't want to double-touch those records, do we?
		if (is_array($relations))
		{
			$this->touches = array_diff($this->touches, $relations);
		}
		// Otherwise empty $this->touches completely as we'll be pushing all relations
		else
		{
			$this->touches = [];
		}

		// Save this record
		$this->save($data, $orderingFilter, $ignore, false);

		// Push all relations specified (or all relations if $relations is null)
		$relManager   = $this->getRelations();
		$allRelations = $relManager->getRelationNames();

		if (!empty($allRelations))
		{
			foreach ($allRelations as $relationName)
			{
				if (!is_null($relations) && !in_array($relationName, $relations))
				{
					continue;
				}

				$relManager->save($relationName);
			}
		}

		// Restore the model's $touches definition
		$this->touches = $touches;

		// Return self for chaining
		return $this;
	}

	/**
	 * Method to bind an associative array or object to the DataModel instance. This method optionally takes an array of
	 * properties to ignore when binding.
	 *
	 * Special note if you are using a custom buildQuery with JOINs or field aliases:
	 * You will need to use addKnownField to let FOF know that the fields from your JOINs and the aliased fields should
	 * be bound to the record data. If you are using aliased fields you may also want to override the
	 * databaseDataToRecordData method. Generally, it is a BAD idea using JOINs instead of relations.
	 *
	 * @param   mixed  $data    An associative array or object to bind to the DataModel instance.
	 * @param   mixed  $ignore  An optional array or space separated list of properties to ignore while binding.
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  InvalidArgumentException
	 * @throws    Exception
	 */
	public function bind($data, $ignore = [])
	{
		$this->triggerEvent('onBeforeBind', [&$data]);

		// If the source value is not an array or object return false.
		if (!is_object($data) && !is_array($data))
		{
			throw new InvalidArgumentException(Text::sprintf('LIB_FOF_MODEL_ERR_BIND', get_class($this), gettype($data)));
		}

		// If the ignore value is a string, explode it over spaces.
		if (!is_array($ignore))
		{
			$ignore = explode(' ', $ignore);
		}

		// Bind the source value, excluding the ignored fields.
		foreach ($this->recordData as $k => $currentValue)
		{
			// Only process fields not in the ignore array.
			if (!in_array($k, $ignore))
			{
				if (is_array($data) && isset($data[$k]))
				{
					$this->setFieldValue($k, $data[$k]);
				}
				elseif (is_object($data) && isset($data->$k))
				{
					$this->setFieldValue($k, $data->$k);
				}
			}
		}

		// Perform data transformation
		$this->databaseDataToRecordData();

		$this->triggerEvent('onAfterBind', [$data]);

		return $this;
	}

	/**
	 * Check the data for validity. By default it only checks for fields declared as NOT NULL
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws RuntimeException  When the data bound to this record is invalid
	 */
	public function check()
	{
		if (!$this->autoChecks)
		{
			return $this;
		}

		// Run a custom event
		$this->triggerEvent('onBeforeCheck');

		// Create a slug if there is a title and an empty slug
		$slugField  = $this->getFieldAlias('slug');
		$titleField = $this->getFieldAlias('title');

		if ($this->hasField('title') && $this->hasField('slug') && !$this->$slugField)
		{
			$this->$slugField = ApplicationHelper::stringURLSafe($this->$titleField);
		}

		// Special handling of the ordering field
		if ($this->hasField('ordering') && is_null($this->getFieldValue('ordering')))
		{
			$this->setFieldValue('ordering', 0);
		}

		foreach ($this->knownFields as $fieldName => $field)
		{
			// Never check the key if it's empty; an empty key is normal for new records
			if ($fieldName == $this->idFieldName)
			{
				continue;
			}

			$value = $this->$fieldName;

			if (isset($field->Null) && ($field->Null == 'NO') && empty($value) && !is_numeric($value) && !in_array($fieldName, $this->fieldsSkipChecks))
			{
				if (!is_null($field->Default))
				{
					$this->$fieldName = $field->Default;

					continue;
				}

				$text = $this->container->componentName . '_' . $this->container->inflector->singularize($this->getName()) . '_ERR_'
					. $fieldName . '_EMPTY';

				throw new RuntimeException(Text::_(strtoupper($text)), 500);
			}
		}

		return $this;
	}

	/**
	 * Change the ordering of the records of the table
	 *
	 * @param   string  $where  The WHERE clause of the SQL used to fetch the order
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  UnexpectedValueException
	 */
	public function reorder($where = '')
	{
		// If there is no ordering field set an error and return false.
		if (!$this->hasField('ordering'))
		{
			throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
		}

		$this->triggerEvent('onBeforeReorder', [&$where]);

		$order_field = $this->getFieldAlias('ordering');
		$k           = $this->getIdFieldName();
		$db          = $this->getDbo();

		// Get the primary keys and ordering values for the selection.
		$query = $db->getQuery(true)
			->select($db->qn($k) . ', ' . $db->qn($order_field))
			->from($db->qn($this->getTableName()))
			->where($db->qn($order_field) . ' >= ' . $db->q(0))
			->order($db->qn($order_field) . 'ASC, ' . $db->qn($k) . 'ASC');

		// Setup the extra where and ordering clause data.
		if ($where)
		{
			$query->where($where);
		}

		$rows = $db->setQuery($query)->loadObjectList();

		// Compact the ordering values.
		foreach ($rows as $i => $row)
		{
			// Make sure the ordering is a positive integer.
			if ($row->$order_field >= 0)
			{
				// Only update rows that are necessary.
				if ($row->$order_field != $i + 1)
				{
					// Update the row ordering field.
					$query = $db->getQuery(true)
						->update($db->qn($this->getTableName()))
						->set($db->qn($order_field) . ' = ' . $db->q($i + 1))
						->where($db->qn($k) . ' = ' . $db->q($row->$k));
					$db->setQuery($query)->execute();
				}
			}
		}

		$this->triggerEvent('onAfterReorder');

		return $this;
	}

	/**
	 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
	 * Negative numbers move the row up in the sequence and positive numbers move it down.
	 *
	 * @param   integer  $delta  The direction and magnitude to move the row in the ordering sequence.
	 * @param   string   $where  WHERE clause to use for limiting the selection of rows to compact the
	 *                           ordering values.
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  UnexpectedValueException  If the table does not support reordering
	 * @throws  RuntimeException  If the record is not loaded
	 */
	public function move($delta, $where = '')
	{
		if (!$this->hasField('ordering'))
		{
			throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
		}

		$this->triggerEvent('onBeforeMove', [&$delta, &$where]);

		$ordering_field = $this->getFieldAlias('ordering');

		// If the change is none, do nothing.
		if (empty($delta))
		{
			$this->triggerEvent('onAfterMove');

			return $this;
		}

		$k     = $this->idFieldName;
		$row   = null;
		$db    = $this->getDbo();
		$query = $db->getQuery(true);

		// If the table is not loaded, return false
		if (empty($this->$k))
		{
			throw new RecordNotLoaded(sprintf("Model %s does not have a loaded record", $this->getName()));
		}

		// Select the primary key and ordering values from the table.
		$query->select([
				$db->qn($this->idFieldName), $db->qn($ordering_field),
			]
		)->from($db->qn($this->tableName));

		// If the movement delta is negative move the row up.
		if ($delta < 0)
		{
			$query->where($db->qn($ordering_field) . ' < ' . $db->q((int) $this->$ordering_field));
			$query->order($db->qn($ordering_field) . ' DESC');
		}
		// If the movement delta is positive move the row down.
		elseif ($delta > 0)
		{
			$query->where($db->qn($ordering_field) . ' > ' . $db->q((int) $this->$ordering_field));
			$query->order($db->qn($ordering_field) . ' ASC');
		}

		// Add the custom WHERE clause if set.
		if ($where)
		{
			$query->where($where);
		}

		// Select the first row with the criteria.
		$row = $db->setQuery($query, 0, 1)->loadObject();

		// If a row is found, move the item.
		if (!empty($row))
		{
			// Update the ordering field for this instance to the row's ordering value.
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($db->qn($ordering_field) . ' = ' . $db->q((int) $row->$ordering_field))
				->where($db->qn($k) . ' = ' . $db->q($this->$k));
			$db->setQuery($query)->execute();

			// Update the ordering field for the row to this instance's ordering value.
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($db->qn($ordering_field) . ' = ' . $db->q((int) $this->$ordering_field))
				->where($db->qn($k) . ' = ' . $db->q($row->$k));
			$db->setQuery($query)->execute();

			// Update the instance value.
			$this->$ordering_field = $row->$ordering_field;
		}

		$this->triggerEvent('onAfterMove');

		return $this;
	}

	/**
	 * Process a large collection of records a few at a time.
	 *
	 * @param   integer   $chunkSize  How many records to process at once
	 * @param   callable  $callback   A callable to process each record
	 *
	 * @return  static  Self, for chaining
	 */
	public function chunk($chunkSize, $callback)
	{
		$totalItems = $this->count();

		if (!$totalItems)
		{
			return $this;
		}

		$start = 0;

		while ($start < ($totalItems - 1))
		{
			$this->get(true, $start, $chunkSize)->transform($callback);

			$start += $chunkSize;
		}

		return $this;
	}

	/**
	 * Get the number of all items
	 *
	 * @return  integer
	 */
	public function count()
	{
		// Get a "count all" query
		$db    = $this->getDbo();
		$query = $this->buildQuery(true);
		$query->clear('select')->clear('order')->select('COUNT(*)');

		// Run the "before build query" hook and behaviours
		$this->triggerEvent('onBuildCountQuery', [&$query]);

		$total = $db->setQuery($query)->loadResult();

		return $total;
	}

	/**
	 * Build the query to fetch data from the database
	 *
	 * @param   boolean  $overrideLimits  Should I override limits
	 *
	 * @return  JDatabaseQuery  The database query to use
	 */
	public function buildQuery($overrideLimits = false)
	{
		// Get a "select all" query
		$db    = $this->getDbo();
		$query = $db->getQuery(true)
			->select('*')
			->from($this->getTableName());

		// Run the "before build query" hook and behaviours
		$this->triggerEvent('onBeforeBuildQuery', [&$query, $overrideLimits]);

		// Apply custom WHERE clauses
		if (count($this->whereClauses))
		{
			foreach ($this->whereClauses as $clause)
			{
				$query->where($clause);
			}
		}

		$order = $this->getState('filter_order', null, 'cmd');

		if (!array_key_exists($order, $this->knownFields))
		{
			$order = $this->getIdFieldName();
			$this->setState('filter_order', $order);
		}

		$order = $db->qn($order);

		$dir = strtoupper($this->getState('filter_order_Dir', null, 'cmd'));

		if (!in_array($dir, ['ASC', 'DESC']))
		{
			$dir = 'ASC';
			$this->setState('filter_order_Dir', $dir);
		}

		$query->order($order . ' ' . $dir);

		// Run the "before after query" hook and behaviours
		$this->triggerEvent('onAfterBuildQuery', [&$query, $overrideLimits]);

		return $query;
	}

	/**
	 * Returns a DataCollection iterator based on your currently set Model state
	 *
	 * @param   boolean  $overrideLimits  Should I ignore limits set in the Model?
	 * @param   integer  $limitstart      How many items to skip from the start, only when $overrideLimits = true
	 * @param   integer  $limit           How many items to return, only when $overrideLimits = true
	 *
	 * @return  DataCollection  The data collection
	 */
	public function get($overrideLimits = false, $limitstart = 0, $limit = 0)
	{
		if (!$overrideLimits)
		{
			$limitstart = $this->getState('limitstart', 0);
			$limit      = $this->getState('limit', 0);
		}

		$dataCollection = DataCollection::make($this->getItemsArray($limitstart, $limit, $overrideLimits));

		$this->eagerLoad($dataCollection, null);

		return $dataCollection;
	}

	/**
	 * Returns a raw array of DataModel instances based on your currently set Model state
	 *
	 * @param   integer  $limitstart      How many items from the start to skip (0 = do not skip)
	 * @param   integer  $limit           How many items to return (0 = all)
	 * @param   bool     $overrideLimits  Set to true to override limitstart, limit and ordering
	 *
	 * @return  array  Array of DataModel objects
	 */
	public function &getItemsArray($limitstart = 0, $limit = 0, $overrideLimits = false)
	{
		$itemsTemp = $this->getRawDataArray($limitstart, $limit, $overrideLimits);
		$items     = [];

		while (!empty($itemsTemp))
		{
			$data = array_shift($itemsTemp);
			/** @var DataModel $item */
			$item = clone $this;
			$item->clearState()->reset(true);
			$item->bind($data);
			$items[$item->getId()] = $item;
			$item->relationManager = clone $this->relationManager;
			$item->relationManager->rebase($item);
		}

		$this->triggerEvent('onAfterGetItemsArray', [&$items]);

		return $items;
	}

	/**
	 * Returns the raw data array, as fetched from the database, based on your currently set Model state
	 *
	 * @param   integer  $limitstart      How many items from the start to skip (0 = do not skip)
	 * @param   integer  $limit           How many items to return (0 = all)
	 * @param   bool     $overrideLimits  Set to true to override limitstart, limit and ordering
	 *
	 * @return  array  Array of hashed arrays
	 */
	public function &getRawDataArray($limitstart = 0, $limit = 0, $overrideLimits = false)
	{
		$limitstart = max($limitstart, 0);
		$limit      = max($limit, 0);

		$query = $this->buildQuery($overrideLimits);

		$db = $this->getDbo();
		$db->setQuery($query, $limitstart, $limit);
		$rawData = $db->loadAssocList();

		return $rawData;
	}

	/**
	 * Eager loads the provided relations and assigns their data to a data collection
	 *
	 * @param   DataCollection  $dataCollection  The data collection on which the eager loaded relations will be
	 *                                           applied
	 * @param   array|null      $relations       The relations to eager load. Leave empty to use the already defined
	 *                                           relations
	 *
	 * @return static for chaining
	 */
	public function eagerLoad(DataCollection &$dataCollection, array $relations = null)
	{
		if (empty($relations))
		{
			$relations = $this->eagerRelations;
		}

		// Apply eager loaded relations
		if ($dataCollection->count() && !empty($relations))
		{
			$relationManager = $this->getRelations();

			foreach ($relations as $relation => $callback)
			{
				// Did they give us a relation name without a callback?
				if (!is_callable($callback) && is_string($callback) && !empty($callback))
				{
					$relation = $callback;
					$callback = null;
				}

				$relationData  = $relationManager->getData($relation, $callback, $dataCollection);
				$foreignKeyMap = $relationManager->getForeignKeyMap($relation);

				/** @var DataModel $item */
				foreach ($dataCollection as $item)
				{
					$item->getRelations()->setDataFromCollection($relation, $relationData, $foreignKeyMap);
				}
			}
		}

		return $this;
	}

	/**
	 * Archive the record, i.e. set enabled to 2
	 *
	 * @return   static  For chaining
	 */
	public function archive()
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't archive a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		$this->triggerEvent('onBeforeArchive', []);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = 2;
		$this->save();

		$this->triggerEvent('onAfterArchive');

		return $this;
	}

	/**
	 * Trashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
	 * is loaded before trying to trash it. Unlike a hard delete, trashing is a "soft delete", only setting the enabled
	 * field to -2.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  static  for chaining
	 */
	public function trash($id = null)
	{
		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$id = $this->getId();

		if (!$id)
		{
			throw new RecordNotLoaded("Can't trash a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			throw new SpecialColumnMissing("DataModel::trash method needs an 'enabled' field");
		}

		$this->triggerEvent('onBeforeTrash', [&$id]);

		$enabled        = $this->getFieldAlias('enabled');
		$this->$enabled = -2;
		$this->save();

		$this->triggerEvent('onAfterTrash', [&$id]);

		return $this;
	}

	/**
	 * Change the publish state of a record. By default it will set it to 1 (published) unless you specify a different
	 * value.
	 *
	 * @param   int  $state  The publish state. Default: 1 (published).
	 *
	 * @return   static  For chaining
	 */
	public function publish($state = 1)
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		$this->triggerEvent('onBeforePublish', []);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = $state;
		$this->save();

		$this->triggerEvent('onAfterPublish');

		return $this;
	}

	/**
	 * Unpublish the record, i.e. set enabled to 0
	 *
	 * @return   static  For chaining
	 */
	public function unpublish()
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't unpublish a not loaded DataModel");
		}

		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		$this->triggerEvent('onBeforeUnpublish', []);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = 0;
		$this->save();

		$this->triggerEvent('onAfterUnpublish');

		return $this;
	}

	/**
	 * Untrashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that
	 * record is loaded before trying to untrash it. Please note that enabled is set to 0 (unpublished) when you untrash
	 * an item.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  static  for chaining
	 */
	public function restore($id = null)
	{
		if (!$this->hasField('enabled'))
		{
			return $this;
		}

		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$id = $this->getId();

		if (!$id)
		{
			throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
		}

		$this->triggerEvent('onBeforeRestore', [&$id]);

		$enabled = $this->getFieldAlias('enabled');

		$this->$enabled = 0;
		$this->save();

		$this->triggerEvent('onAfterRestore', [&$id]);

		return $this;
	}

	/**
	 * Creates a copy of the current record. After the copy is performed, the data model contains the data of the new
	 * record.
	 *
	 * @param   array|DataModel  An associative array or object to bind to the DataModel instance. Allows you to
	 *                              override values on the copied object.
	 *
	 * @return   static
	 */
	public function copy($data = null)
	{
		$this->triggerEvent('onBeforeCopy');

		$this->{$this->idFieldName} = null;

		if ($this->hasField('created_by'))
		{
			$this->setFieldValue('created_by', null);
		}

		if ($this->hasField('modified_by'))
		{
			$this->setFieldValue('modified_by', null);
		}

		if ($this->hasField('locked_by'))
		{
			$this->setFieldValue('locked_by', null);
		}

		if ($this->hasField('created_on'))
		{
			$this->setFieldValue('created_on', null);
		}

		if ($this->hasField('modified_on'))
		{
			$this->setFieldValue('modified_on', null);
		}

		if ($this->hasField('locked_on'))
		{
			$this->setFieldValue('locked_on', null);
		}

		$result = $this->save($data);

		$this->triggerEvent('onAfterCopy', [&$result]);

		return $result;
	}

	/**
	 * Check-in an item. This works similar to unlock() but performs additional checks. If the item is locked by another
	 * user you need to have adequate ACL privileges to unlock it, i.e. core.admin or core.manage component-wide
	 * privileges; core.edit.state privileges component-wide or per asset; or be the creator of the item and have
	 * core.edit.own privileges component-wide or per asset.
	 *
	 * @return  static
	 *
	 * @throws  LockedRecord  If you don't have the privilege to check in this item
	 */
	public function checkIn($userId = null)
	{
		// If there is no loaded record we can't do much, I'm afraid
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't checkin a not loaded DataModel");
		}

		// If the lock fields are missing we have nothing to do
		if (!$this->hasField('locked_by') && !$this->hasField('locked_on'))
		{
			return $this;
		}

		// If there's no locked_by field we just unlock and return
		if (!$this->hasField('locked_by'))
		{
			return $this->unlock();
		}

		// If the current user and the user who locked the record are the same, unlock it.
		if (empty($userId))
		{
			$userId = $this->container->platform->getUser()->id;
		}

		$lockedBy = $this->getFieldValue('locked_by');

		if (empty($lockedBy) || ($lockedBy == $userId))
		{
			return $this->unlock();
		}

		// Get the component privileges
		$platform  = $this->container->platform;
		$component = $this->container->componentName;

		$privileges = [
			'editown'   => $platform->authorise('core.edit.own', $component),
			'editstate' => $platform->authorise('core.edit.state', $component),
			'admin'     => $platform->authorise('core.admin', $component),
			'manage'    => $platform->authorise('core.manage', $component),
		];

		// If we are trackign assets get the item's privileges
		if ($this->isAssetsTracked())
		{
			$assetKey        = $this->getAssetKey();
			$assetPrivileges = [
				'editown'   => $platform->authorise('core.edit.own', $assetKey),
				'editstate' => $platform->authorise('core.edit.state', $assetKey),
			];

			foreach ($assetPrivileges as $k => $v)
			{
				$privileges[$k] = $privileges[$k] || $v;
			}
		}

		// If you are a Super User, component manager or allowed to edit the state of records we unlock it
		if ($privileges['admin'] || $privileges['manage'] || $privileges['editstate'])
		{
			return $this->unlock();
		}

		// If you are the owner of the record and have core.edit.own privilege we will unlock it.
		$owner = 0;

		if ($this->hasField('created_by'))
		{
			$owner = $this->getFieldValue('created_by');
		}

		if ($privileges['editown'] && ($owner == $userId))
		{
			return $this->unlock();
		}

		// All else failed, you don't have the privilege to unlock this item.
		throw new LockedRecord;
	}

	/**
	 * Reset the record data
	 *
	 * @param   boolean  $useDefaults     Should I use the default values? Default: yes
	 * @param   boolean  $resetRelations  Should I reset the relations too? Default: no
	 *
	 * @return  static  Self, for chaining
	 */
	public function reset($useDefaults = true, $resetRelations = false)
	{
		$this->recordData   = [];
		$this->whereClauses = [];

		foreach ($this->knownFields as $fieldName => $information)
		{
			if ($useDefaults)
			{
				$this->recordData[$fieldName] = $information->Default;
			}
			else
			{
				$this->recordData[$fieldName] = null;
			}
		}

		if ($resetRelations)
		{
			$this->relationManager->resetRelationData();
			$this->eagerRelations = [];
		}

		$this->relationFilters = [];

		$this->triggerEvent('onAfterReset', [$useDefaults, $resetRelations]);

		return $this;
	}

	/**
	 * Automatically performs a hard or soft delete, based on the value of $this->softDelete. A soft delete simply sets
	 * enabled to -2 whereas a hard delete removes the data from the database. If you want to force a specific behaviour
	 * directly call trash() for a soft delete or forceDelete() for a hard delete.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  static  for chaining
	 */
	public function delete($id = null)
	{
		if ($this->softDelete)
		{
			return $this->trash($id);
		}
		else
		{
			return $this->forceDelete($id);
		}
	}

	/**
	 * Delete a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
	 * is loaded before trying to delete it. In the end the data model is reset.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  static  for chaining
	 */
	public function forceDelete($id = null)
	{
		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$id = $this->getId();

		if (!$id)
		{
			throw new RecordNotLoaded("Can't delete a not loaded DataModel object");
		}

		$this->triggerEvent('onBeforeDelete', [&$id]);

		$db = $this->getDbo();

		$query = $db->getQuery(true)
			->delete()
			->from($this->tableName)
			->where($db->qn($this->idFieldName) . ' = ' . $db->q($id));
		$db->setQuery($query)->execute();

		$this->triggerEvent('onAfterDelete', [&$id]);

		$this->reset();

		return $this;
	}

	/**
	 * Generic check for whether dependencies exist for this object in the db schema. This method is NOT used by
	 * default. If you want to use it you will have to override your delete(), trash() or forceDelete() method,
	 * or create an onBeforeDelete and/or onBeforeTrash event handler.
	 *
	 * @param   integer  $oid    The primary key of the record to delete
	 * @param   array    $joins  Any joins to foreign table, used to determine if dependent records exist
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException  If you should not delete the record (the message tells you why)
	 */
	public function canDelete($oid = null, $joins = null)
	{
		$pkField = $this->getKeyName();

		if ($oid)
		{
			$this->$pkField = intval($oid);
		}

		if (!$this->$pkField)
		{
			throw new InvalidArgumentException('Master table should be loaded or an ID should be passed');
		}

		if (is_array($joins))
		{
			$db      = $this->getDbo();
			$query   = $db->getQuery(true)
				->select($db->qn('master') . '.' . $db->qn($pkField))
				->from($db->qn($this->tableName) . ' AS ' . $db->qn('master'));
			$tableNo = 0;

			foreach ($joins as $table)
			{
				// Sanity check on passed array
				$check  = ['idfield', 'idalias', 'name', 'joinfield', 'label'];
				$result = array_intersect($check, array_keys($table));

				if (count($result) != count($check))
				{
					throw new InvalidArgumentException('Join array missing some keys, please check the documentation');
				}

				$tableNo++;
				$query->select(
					[
						'COUNT(DISTINCT ' . $db->qn('t' . $tableNo) .
						'.' . $db->qn($table['idfield']) . ') AS ' . $db->qn($table['idalias']),
					]
				);
				$query->join('LEFT', $db->qn($table['name']) .
					' AS ' . $db->qn('t' . $tableNo) .
					' ON ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['joinfield']) .
					' = ' . $db->qn('master') . '.' . $db->qn($pkField)
				);
			}

			$query->where($db->qn('master') . '.' . $db->qn($pkField) . ' = ' . $db->q($this->$pkField));
			$query->group($db->qn('master') . '.' . $db->qn($pkField));
			$this->getDbo()->setQuery((string) $query);

			$obj = $this->getDbo()->loadObject();

			$msg = [];
			$i   = 0;

			foreach ($joins as $table)
			{
				$pkField = $table['idalias'];

				if ($obj->$pkField > 0)
				{
					$msg[] = Text::_($table['label']);
				}

				$i++;
			}

			if (count($msg))
			{
				$option  = $this->container->componentName;
				$comName = $this->container->bareComponentName;
				$tbl     = $this->getTableName();
				$tview   = str_replace('#__' . $comName . '_', '', $tbl);
				$prefix  = $option . '_' . $tview . '_NODELETE_';

				$message = '<ul>';

				foreach ($msg as $key)
				{
					$message .= '<li>' . Text::_(strtoupper($prefix . $key)) . '</li>';
				}

				$message .= '</ul>';

				throw new RuntimeException($message);
			}
		}
	}

	/**
	 * Find and load a single record based on the provided key values. If the record is not found an exception is thrown
	 *
	 * @param   array|mixed  $keys  An optional primary key value to load the row by, or an array of fields to match.
	 *                              If not set the "id" state variable or, if empty, the identity column's value is used
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  RuntimeException  When the row is not found
	 */
	public function findOrFail($keys = null)
	{
		$this->find($keys);

		// We have to assign the value, since empty() is not triggering the __get magic method
		// http://stackoverflow.com/questions/2045791/php-empty-on-get-accessor
		$value = $this->getId();

		if (empty($value))
		{
			throw new RecordNotLoaded;
		}

		return $this;
	}

	/**
	 * Method to load a row from the database by primary key. Used for JTableInterface compatibility.
	 *
	 * @param   mixed    $keys   An optional primary key value to load the row by, or an array of fields to match.  If
	 *                           not set the instance property value is used.
	 * @param   boolean  $reset  True to reset the default values before loading the new row.
	 *
	 * @return  boolean  True if successful. False if row not found.
	 *
	 * @throws  RuntimeException
	 * @throws  UnexpectedValueException
	 * @link    http://docs.joomla.org/JTable/load
	 * @since   3.2
	 */
	public function load($keys = null, $reset = true)
	{
		if ($reset)
		{
			$this->reset();
		}

		try
		{
			$this->findOrFail($keys);
		}
		catch (Exception $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Find and load a single record based on the provided key values
	 *
	 * @param   array|mixed  $keys  An optional primary key value to load the row by, or an array of fields to match.
	 *                              If not set the "id" state variable or, if empty, the identity column's value is used
	 *
	 * @return  static  Self, for chaining
	 */
	public function find($keys = null)
	{
		// Execute the onBeforeLoad event
		$this->triggerEvent('onBeforeLoad', [&$keys]);

		// If we are not given any keys, try to get the ID from the state or the table data
		if (empty($keys))
		{
			$id = $this->getState('id', 0);

			if (empty($id))
			{
				$id = $this->getId();
			}

			if (empty($id))
			{
				$this->triggerEvent('onAfterLoad', [false, &$keys]);

				$this->reset();

				return $this;
			}

			$keys = [$this->idFieldName => $id];
		}
		elseif (!is_array($keys))
		{
			if (empty($keys))
			{
				$this->triggerEvent('onAfterLoad', [false, &$keys]);

				$this->reset();

				return $this;
			}

			$keys = [$this->idFieldName => $keys];
		}

		// Reset the table
		$this->reset();

		// Get the query
		$db    = $this->getDbo();
		$query = $db->getQuery(true)
			->select('*')
			->from($db->qn($this->tableName));

		// Apply key filters
		foreach ($keys as $filterKey => $filterValue)
		{
			if ($filterKey == 'id')
			{
				$filterKey = $this->getIdFieldName();
			}

			if (array_key_exists($filterKey, $this->recordData))
			{
				$query->where($db->qn($filterKey) . ' = ' . $db->q($filterValue));
			}
		}

		// Get the row
		$db->setQuery($query);

		try
		{
			$row = $db->loadAssoc();
		}
		catch (Exception $e)
		{
			$row = null;
		}

		if (empty($row))
		{
			$this->triggerEvent('onAfterLoad', [false, &$keys]);

			return $this;
		}

		// Bind the data
		$this->bind($row);

		$this->relationManager->rebase($this);

		// Execute the onAfterLoad event
		$this->triggerEvent('onAfterLoad', [true, &$keys]);

		return $this;
	}

	/**
	 * Create a new record with the provided data
	 *
	 * @param   array  $data  The data to use in the new record
	 *
	 * @return  static  Self, for chaining
	 */
	public function create($data)
	{
		return $this->reset()->bind($data)->save();
	}

	/**
	 * Return the first item found or create a new one based on the provided $data
	 *
	 * @param   array  $data  Data for the newly created item
	 *
	 * @return  static
	 */
	public function firstOrCreate($data)
	{
		$item = $this->get(true, 0, 1)->first();

		if (is_null($item))
		{
			$item = clone $this;
			$item->create($data);
		}

		return $item;
	}

	/**
	 * Return the first item found or throw a \RuntimeException
	 *
	 * @return  static
	 *
	 * @throws  RuntimeException
	 */
	public function firstOrFail()
	{
		$item = $this->get(true, 0, 1)->first();

		if (is_null($item))
		{
			throw new NoItemsFound(get_class($this));
		}

		return $item;
	}

	/**
	 * Return the first item found or create a new, blank one
	 *
	 * @return  static
	 */
	public function firstOrNew()
	{
		$item = $this->get(true, 0, 1)->first();

		if (is_null($item))
		{
			$item = clone $this;
			$item->reset();
		}

		return $item;
	}

	/**
	 * Adds a behaviour by its name. It will search the following classes, in this order:
	 * \component_namespace\Model\modelName\Behaviour\behaviourName
	 * \component_namespace\Model\Behaviour\behaviourName
	 * \FOF30\Model\DataModel\Behaviour\behaviourName
	 * where:
	 * component_namespace  is the namespace of the component as defined in the container
	 * modelName            is the model's name, first character uppercase, e.g. Baz
	 * behaviourName        is the $behaviour parameter, first character uppercase, e.g. Something
	 *
	 * @param   string  $behaviour  The behaviour's name
	 *
	 * @return  static  Self, for chaining
	 */
	public function addBehaviour($behaviour)
	{
		$prefixes = [
			$this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()),
			$this->container->getNamespacePrefix() . 'Model\\Behaviour',
			'\\FOF30\\Model\\DataModel\\Behaviour',
		];

		foreach ($prefixes as $prefix)
		{
			$className = $prefix . '\\' . ucfirst($behaviour);

			if (class_exists($className, true) && !$this->behavioursDispatcher->hasObserverClass($className))
			{
				/** @var Observer $o */
				$observer = new $className($this->behavioursDispatcher);
				$this->behavioursDispatcher->attach($observer);

				return $this;
			}
		}

		return $this;
	}

	/**
	 * Removes a behaviour by its name. It will search the following classes, in this order:
	 * \component_namespace\Model\modelName\Behaviour\behaviourName
	 * \component_namespace\Model\DataModel\Behaviour\behaviourName
	 * \FOF30\Model\DataModel\Behaviour\behaviourName
	 * where:
	 * component_namespace  is the namespace of the component as defined in the container
	 * modelName            is the model's name, first character uppercase, e.g. Baz
	 * behaviourName        is the $behaviour parameter, first character uppercase, e.g. Something
	 *
	 * @param   string  $behaviour  The behaviour's name
	 *
	 * @return  static  Self, for chaining
	 */
	public function removeBehaviour($behaviour)
	{
		$prefixes = [
			$this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()),
			$this->container->getNamespacePrefix() . 'Model\\Behaviour',
			'\\FOF30\\Model\\DataModel\\Behaviour',
		];

		foreach ($prefixes as $prefix)
		{
			$className = ltrim($prefix . '\\' . ucfirst($behaviour), '\\');

			$observer = $this->behavioursDispatcher->getObserverByClass($className);

			if (is_null($observer))
			{
				continue;
			}

			$this->behavioursDispatcher->detach($observer);

			return $this;
		}

		return $this;
	}

	/**
	 * Gives you access to the behaviours dispatcher, allowing to attach/detach behaviour observers
	 *
	 * @return Dispatcher
	 */
	public function &getBehavioursDispatcher()
	{
		return $this->behavioursDispatcher;
	}

	/**
	 * Set the field and direction of ordering for the query returned by buildQuery.
	 * Alias of $this->setState('filter_order', $fieldName) and $this->setState('filter_order_Dir', $direction)
	 *
	 * @param   string  $fieldName  The field name to order by
	 * @param   string  $direction  The direction to order by (ASC for ascending or DESC for descending)
	 *
	 * @return  static  For chaining
	 */
	public function orderBy($fieldName, $direction = 'ASC')
	{
		$direction = strtoupper($direction);

		if (!in_array($direction, ['ASC', 'DESC']))
		{
			$direction = 'ASC';
		}

		$this->setState('filter_order', $fieldName);
		$this->setState('filter_order_Dir', $direction);

		return $this;
	}

	/**
	 * Set the limitStart for the query, i.e. how many records to skip.
	 * Alias of $this->setState('limitstart', $limitStart);
	 *
	 * @param   integer  $limitStart  Records to skip from the start
	 *
	 * @return  static  For chaining
	 */
	public function skip($limitStart = null)
	{
		// Only positive integers are allowed
		if (!is_int($limitStart) || $limitStart < 0 || !$limitStart)
		{
			$limitStart = 0;
		}

		$this->setState('limitstart', $limitStart);

		return $this;
	}

	/**
	 * Set the limit for the query, i.e. how many records to return.
	 * Alias of $this->setState('limit', $limit);
	 *
	 * @param   integer  $limit  Maximum number of records to return
	 *
	 * @return  static  For chaining
	 */
	public function take($limit = null)
	{
		// Only positive integers are allowed
		if (!is_int($limit) || $limit < 0 || !$limit)
		{
			$limit = 0;
		}

		$this->setState('limit', $limit);

		return $this;
	}

	/**
	 * Return the record's data as an array
	 *
	 * @return  array
	 */
	public function toArray()
	{
		return $this->recordData;
	}

	/**
	 * Returns the record's data as a JSON string
	 *
	 * @param   boolean  $prettyPrint  Should I format the JSON for pretty printing
	 *
	 * @return  string
	 */
	public function toJson($prettyPrint = false)
	{
		if (defined('JSON_PRETTY_PRINT'))
		{
			$options = $prettyPrint ? JSON_PRETTY_PRINT : 0;
		}
		else
		{
			$options = 0;
		}

		return json_encode($this->recordData, $options);
	}

	/**
	 * Touch a record, updating its modified_on and/or modified_by columns
	 *
	 * @param   integer  $userId  Optional user ID of the user touching the record
	 *
	 * @return  static  Self, for chaining
	 */
	public function touch($userId = null)
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't touch a not loaded DataModel");
		}

		if (!$this->hasField('modified_on') && !$this->hasField('modified_by'))
		{
			return $this;
		}

		$db   = $this->getDbo();
		$date = new Date();

		// Update the created_on / modified_on
		if ($this->hasField('modified_on'))
		{
			$modified_on        = $this->getFieldAlias('modified_on');
			$this->$modified_on = $date->toSql(false, $db);
		}

		// Update the created_by / modified_by values if necessary
		if ($this->hasField('modified_by'))
		{
			if (empty($userId))
			{
				$userId = $this->container->platform->getUser()->id;
			}

			$modified_by        = $this->getFieldAlias('modified_by');
			$this->$modified_by = $userId;
		}

		$this->save();

		return $this;
	}

	/**
	 * Lock a record by setting its locked_on and/or locked_by columns
	 *
	 * @param   integer  $userId
	 *
	 * @return  static  Self, for chaining
	 */
	public function lock($userId = null)
	{
		if (!$this->getId())
		{
			throw new CannotLockNotLoadedRecord;
		}

		if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
		{
			return $this;
		}

		$this->triggerEvent('onBeforeLock', []);

		$db = $this->getDbo();

		if ($this->hasField('locked_on'))
		{
			$date             = new Date();
			$locked_on        = $this->getFieldAlias('locked_on');
			$this->$locked_on = $date->toSql(false, $db);
		}

		if ($this->hasField('locked_by'))
		{
			if (empty($userId))
			{
				$userId = $this->container->platform->getUser()->id;
			}

			$locked_by        = $this->getFieldAlias('locked_by');
			$this->$locked_by = $userId;
		}

		$this->save();

		$this->triggerEvent('onAfterLock');

		return $this;
	}

	/**
	 * Unlock a record by resetting its locked_on and/or locked_by columns
	 *
	 * @return  static  Self, for chaining
	 */
	public function unlock()
	{
		if (!$this->getId())
		{
			throw new RecordNotLoaded("Can't unlock a not loaded DataModel");
		}

		if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
		{
			return $this;
		}

		$this->triggerEvent('onBeforeUnlock', []);

		$db = $this->getDbo();

		if ($this->hasField('locked_on'))
		{
			$locked_on        = $this->getFieldAlias('locked_on');
			$this->$locked_on = $this->isNullableField('locked_on') ? null : $db->getNullDate();
		}

		if ($this->hasField('locked_by'))
		{
			$locked_by        = $this->getFieldAlias('locked_by');
			$this->$locked_by = 0;
		}

		$this->save();

		$this->triggerEvent('onAfterUnlock');

		return $this;
	}

	/**
	 * Is this record locked by a different user than $userId?
	 *
	 * @param   integer  $userId
	 *
	 * @return  bool  True if the record is locked
	 */
	public function isLocked($userId = null)
	{
		if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
		{
			return false;
		}

		$nullDate = $this->isNullableField('locked_on') ? null : $this->getDbo()->getNullDate();

		// Get the locked_by / locked_on
		$locked_on = $nullDate;
		$locked_by = 0;

		if ($this->hasField('locked_on'))
		{
			$locked_on = $this->getFieldValue('locked_on', $nullDate);

			if (empty($locked_on))
			{
				$locked_on = $nullDate;
			}
		}

		if ($this->hasField('locked_by'))
		{
			$locked_by = $this->getFieldValue('locked_by', 0);

			if (empty($locked_by))
			{
				$locked_by = 0;
			}
		}

		$allowedUsers = [0];

		if (!empty($userId))
		{
			$allowedUsers[] = $userId;
		}

		if (in_array($locked_by, $allowedUsers))
		{
			return false;
		}

		return !is_null($locked_on) && ($locked_on != $nullDate);
	}

	/**
	 * Automatically uses the Filters behaviour to filter records in the model based on your criteria.
	 *
	 * @param   string  $fieldName  The field name to filter on
	 * @param   string  $method     The filtering method, e.g. <>, =, != and so on
	 * @param   mixed   $values     The value you're filtering on. Some filters (e.g. interval or between) require an
	 *                              array of values
	 *
	 * @return  static  For chaining
	 */
	public function where($fieldName, $method = '=', $values = null)
	{
		// Make sure the Filters behaviour is added to the model
		if (!$this->behavioursDispatcher->hasObserverClass('FOF30\\Model\\DataModel\\Behaviour\\Filters'))
		{
			$this->addBehaviour('filters');
		}

		// If we are dealing with the primary key, let's set the field name to "id". This is a convention and it will
		// be used inside the Filters behaviour
		// -- Let's not do this. The Filters behaviour works just fine with the regular field name!
		/**
		 * if ($fieldName == $this->getIdFieldName())
		 * {
		 * $fieldName = 'id';
		 * }
		 **/

		$options = [
			'method' => $method,
			'value'  => $values,
		];

		// Handle method aliases
		switch ($method)
		{
			case '<>':
				$options['method']   = 'search';
				$options['operator'] = '!=';
				break;

			case 'lt':
				$options['method']   = 'search';
				$options['operator'] = '<';
				break;

			case 'le':
				$options['method']   = 'search';
				$options['operator'] = '<=';
				break;

			case 'gt':
				$options['method']   = 'search';
				$options['operator'] = '>';
				break;

			case 'ge':
				$options['method']   = 'search';
				$options['operator'] = '>=';
				break;

			case 'eq':
				$options['method']   = 'search';
				$options['operator'] = '=';
				break;

			case 'neq':
			case 'ne':
				$options['method']   = 'search';
				$options['operator'] = '!=';
				break;

			case '<':
			case '!<':
			case '<=':
			case '!<=':
			case '>':
			case '!>':
			case '>=':
			case '!>=':
			case '!=':
			case '=':
				$options['method']   = 'search';
				$options['operator'] = $method;
				break;

			case 'like':
			case '~':
			case '%':
				$options['method'] = 'partial';
				break;

			case '==':
			case '=[]':
			case '=()':
			case 'in':
				$options['method'] = 'exact';
				break;

			case '()':
			case '[]':
			case '[)':
			case '(]':
				$options['method'] = 'between';
				break;

			case ')(':
			case ')[':
			case '](':
			case '][':
				$options['method'] = 'outside';
				break;

			case '*=':
			case 'every':
				$options['method'] = 'interval';
				break;

			case '?=':
				$options['method'] = 'search';
				break;

			default:

				throw new InvalidSearchMethod('Method ' . $method . ' is unsupported');

				break;
		}

		// Handle real methods
		switch ($options['method'])
		{
			case 'between':
			case 'outside':
				if (is_array($values) && (count($values) > 1))
				{
					// Get the from and to values from the $values array
					if (isset($values['from']) && isset($values['to']))
					{
						$options['from'] = $values['from'];
						$options['to']   = $values['to'];
					}
					else
					{
						$options['from'] = array_shift($values);
						$options['to']   = array_shift($values);
					}

					unset($options['value']);
				}
				else
				{
					// $values is not a from/to array. Treat as = (between) or != (outside)
					if (is_array($values))
					{
						$values = array_shift($values);
					}

					$options['operator'] = ($options['method'] == 'between') ? '=' : '!=';
					$options['value']    = $values;
					$options['method']   = 'search';
				}

				break;

			case 'interval':
				if (is_array($values) && (count($values) > 1))
				{
					// Get the value and interval from the $values array
					if (isset($values['value']) && isset($values['interval']))
					{
						$options['value']    = $values['value'];
						$options['interval'] = $values['interval'];
					}
					else
					{
						$options['value']    = array_shift($values);
						$options['interval'] = array_shift($values);
					}
				}
				else
				{
					// $values is not a value/interval array. Treat as =
					if (is_array($values))
					{
						$values = array_shift($values);
					}

					$options['value']    = $values;
					$options['method']   = 'search';
					$options['operator'] = '=';
				}
				break;

			case 'search':
				// We don't have to do anything if the operator is already set
				if (isset($options['operator']))
				{
					break;
				}

				if (is_array($values) && (count($values) > 1))
				{
					// Get the operator and value from the $values array
					if (isset($values['operator']) && isset($values['value']))
					{
						$options['operator'] = $values['operator'];
						$options['value']    = $values['value'];
					}
					else
					{
						$options['operator'] = array_shift($values);
						$options['value']    = array_shift($values);
					}
				}
				break;
		}

		$this->setState($fieldName, $options);

		return $this;
	}

	/**
	 * Add custom, pre-compiled WHERE clauses for use in buildQuery. The raw WHERE clause you specify is added as is to
	 * the query generated by buildQuery. You are responsible for quoting and escaping the field names and data found
	 * inside the WHERE clause.
	 *
	 * Using this method is a generally bad idea. You are better off overriding buildQuery and using state variables to
	 * customise the query build built instead of using this method to push raw SQL to the query builder. Mixing your
	 * business logic with raw SQL makes your application harder to maintain and refactor as dependencies to your
	 * database schema creep in areas of your code that should have nothing to do with it.
	 *
	 * @param   string  $rawWhereClause  The raw WHERE clause to add
	 *
	 * @return  static  For chaining
	 */
	public function whereRaw($rawWhereClause)
	{
		$this->whereClauses[] = $rawWhereClause;

		return $this;
	}

	/**
	 * Instructs the model to eager load the specified relations. The $relations array can have the format:
	 *
	 * array('relation1', 'relation2')
	 *        Eager load relation1 and relation2 without any callbacks
	 * array('relation1' => $callable1, 'relation2' => $callable2)
	 *        Eager load relation1 with callback $callable1 etc
	 * array('relation1', 'relation2' => $callable2)
	 *        Eager load relation1 without a callback, relation2 with callback $callable2
	 *
	 * The callback must have the signature function(\JDatabaseQuery $query) and doesn't return a value. It is
	 * supposed to modify the query directly.
	 *
	 * Please note that eager loaded relations produce their queries without going through the respective model. Instead
	 * they generate a SQL query directly, then map the loaded results into a DataCollection.
	 *
	 * @param   array  $relations  The relations to eager load. See above for more information.
	 *
	 * @return static For chaining
	 */
	public function with(array $relations)
	{
		if (empty($relations))
		{
			$this->eagerRelations = [];

			return $this;
		}

		$knownRelations = $this->relationManager->getRelationNames();

		foreach ($relations as $k => $v)
		{
			if (is_callable($v))
			{
				$relName  = $k;
				$callback = $v;
			}
			else
			{
				$relName  = $v;
				$callback = null;
			}

			if (in_array($relName, $knownRelations))
			{
				$this->eagerRelations[$relName] = $callback;
			}
		}

		return $this;
	}

	/**
	 * Filter the model based on the fulfilment of relations. For example:
	 * $posts->has('comments', '>=', 10)->get();
	 * will return all posts with at least 10 comments.
	 *
	 * @param   string  $relation  The relation to query
	 * @param   string  $operator  The comparison operator. Same operators as the where() method.
	 * @param   mixed   $value     The value(s) to compare against.
	 * @param   bool    $replace   When true (default) any existing relation filters for the same relation will be
	 *                             replaced
	 *
	 * @return static
	 */
	public function has($relation, $operator = '>=', $value = 1, $replace = true)
	{
		// Make sure the Filters behaviour is added to the model
		if (!$this->behavioursDispatcher->hasObserverClass('FOF30\\Model\\DataModel\\Behaviour\\RelationFilters'))
		{
			$this->addBehaviour('relationFilters');
		}

		$filter = [
			'relation' => $relation,
			'method'   => $operator,
			'operator' => $operator,
			'value'    => $value,
		];

		// Handle method aliases
		switch ($operator)
		{
			case '<>':
				$filter['method']   = 'search';
				$filter['operator'] = '!=';
				break;

			case 'lt':
				$filter['method']   = 'search';
				$filter['operator'] = '<';
				break;

			case 'le':
				$filter['method']   = 'search';
				$filter['operator'] = '<=';
				break;

			case 'gt':
				$filter['method']   = 'search';
				$filter['operator'] = '>';
				break;

			case 'ge':
				$filter['method']   = 'search';
				$filter['operator'] = '>=';
				break;

			case 'eq':
				$filter['method']   = 'search';
				$filter['operator'] = '=';
				break;

			case 'neq':
			case 'ne':
				$filter['method']   = 'search';
				$filter['operator'] = '!=';
				break;

			case '<':
			case '!<':
			case '<=':
			case '!<=':
			case '>':
			case '!>':
			case '>=':
			case '!>=':
			case '!=':
			case '=':
				$filter['method']   = 'search';
				$filter['operator'] = $operator;
				break;

			case 'like':
			case '~':
			case '%':
				$filter['method'] = 'partial';
				break;

			case '==':
			case '=[]':
			case '=()':
			case 'in':
				$filter['method'] = 'exact';
				break;

			case '()':
			case '[]':
			case '[)':
			case '(]':
				$filter['method'] = 'between';
				break;

			case ')(':
			case ')[':
			case '](':
			case '][':
				$filter['method'] = 'outside';
				break;

			case '*=':
			case 'every':
				$filter['method'] = 'interval';
				break;

			case '?=':
				$filter['method'] = 'search';
				break;

			case 'callback':
				$filter['method']   = 'callback';
				$filter['operator'] = 'callback';
				break;

			default:
				throw new InvalidSearchMethod('Operator ' . $operator . ' is unsupported');
				break;
		}

		// Handle real methods
		switch ($filter['method'])
		{
			case 'between':
			case 'outside':
				if (is_array($value) && (count($value) > 1))
				{
					// Get the from and to values from the $value array
					if (isset($value['from']) && isset($value['to']))
					{
						$filter['from'] = $value['from'];
						$filter['to']   = $value['to'];
					}
					else
					{
						$filter['from'] = array_shift($value);
						$filter['to']   = array_shift($value);
					}

					unset($filter['value']);
				}
				else
				{
					// $value is not a from/to array. Treat as = (between) or != (outside)
					if (is_array($value))
					{
						$value = array_shift($value);
					}

					$filter['operator'] = ($filter['method'] == 'between') ? '=' : '!=';
					$filter['value']    = $value;
					$filter['method']   = 'search';
				}

				break;

			case 'interval':
				if (is_array($value) && (count($value) > 1))
				{
					// Get the value and interval from the $value array
					if (isset($value['value']) && isset($value['interval']))
					{
						$filter['value']    = $value['value'];
						$filter['interval'] = $value['interval'];
					}
					else
					{
						$filter['value']    = array_shift($value);
						$filter['interval'] = array_shift($value);
					}
				}
				else
				{
					// $value is not a value/interval array. Treat as =
					if (is_array($value))
					{
						$value = array_shift($value);
					}

					$filter['value']    = $value;
					$filter['method']   = 'search';
					$filter['operator'] = '=';
				}
				break;

			case 'search':
				// We don't have to do anything if the operator is already set
				if (isset($filter['operator']))
				{
					break;
				}

				if (is_array($value) && ((is_array($value) || $value instanceof \Countable ? count($value) : 0) > 1))
				{
					// Get the operator and value from the $value array
					if (isset($value['operator']) && isset($value['value']))
					{
						$filter['operator'] = $value['operator'];
						$filter['value']    = $value['value'];
					}
					else
					{
						$filter['operator'] = array_shift($value);
						$filter['value']    = array_shift($value);
					}
				}
				break;

			case 'callback':
				if (!is_callable($filter['value']))
				{
					$filter['method']   = 'search';
					$filter['operator'] = '=';
					$filter['value']    = 1;
				}
				break;
		}

		if ($replace && !empty($this->relationFilters))
		{
			foreach ($this->relationFilters as $k => $v)
			{
				if ($v['relation'] == $relation)
				{
					unset ($this->relationFilters[$k]);
				}
			}
		}

		$this->relationFilters[] = $filter;

		return $this;
	}

	/**
	 * Advanced model filtering on the fulfilment of relations. Unlike has() you can provide your own callback which
	 * modifies the COUNT subquery used to compare against the relation. The $callBack has the signature
	 * function(\JDatabaseQuery $query)
	 * and MUST return a string. The $query you are passed is the COUNT subquery of the relation, e.g.
	 * SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id
	 * You have to return a WHERE clause for the model's query, e.g.
	 * (SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id) BETWEEN 1 AND 20
	 *
	 * @param   string    $relation  The relation to query against
	 * @param   callable  $callBack  The callback to use for filtering
	 * @param   bool      $replace   When true (default) any existing relation filters for the same relation will be
	 *                               replaced
	 *
	 * @return static
	 */
	public function whereHas($relation, $callBack, $replace = true)
	{
		$this->has($relation, 'callback', $callBack, $replace);

		return $this;
	}

	/**
	 * Returns the relations manager of the model
	 *
	 * @return RelationManager
	 */
	public function &getRelations()
	{
		return $this->relationManager;
	}

	/**
	 * Gets the relation filter definitions, for use by the RelationFilters behaviour
	 *
	 * @return array
	 */
	public function getRelationFilters()
	{
		return $this->relationFilters;
	}

	/**
	 * Returns the list of relations which are touched by save() and touch()
	 *
	 * @return array
	 */
	public function &getTouches()
	{
		return $this->touches;
	}

	/**
	 * Method to get the rules for the record.
	 *
	 * @return  Rules object
	 */
	public function getRules()
	{
		return $this->_rules;
	}

	/**
	 * Method to set rules for the record.
	 *
	 * @param   mixed  $input  A JAccessRules object, JSON string, or array.
	 *
	 * @return  void
	 */
	public function setRules($input)
	{
		if ($input instanceof Rules)
		{
			$this->_rules = $input;
		}
		else
		{
			$this->_rules = new Rules($input);
		}
	}

	/**
	 * Method to check if the record is treated as an ACL asset
	 *
	 * @return  boolean [description]
	 */
	public function isAssetsTracked()
	{
		return $this->_trackAssets;
	}

	/**
	 * Method to manually set this record as ACL asset or not.
	 * We have to do this since the automatic check is made in the constructor, but here we can't set any alias.
	 * So, even if you have an alias for `asset_id`, it wouldn't be reconized and assets won't be tracked.
	 *
	 * @param $state
	 */
	public function setAssetsTracked($state)
	{
		$state = (bool) $state;

		$this->_trackAssets = $state;
	}

	/**
	 * Gets the has tags switch state
	 *
	 * @return bool
	 */
	public function hasTags()
	{
		return $this->_has_tags;
	}

	/**
	 * Sets the has tags switch state
	 *
	 * @param   bool  $newState
	 */
	public function setHasTags($newState = false)
	{
		$this->_has_tags = $newState;
	}

	/**
	 * Method to compute the default name of the asset.
	 * The default name is in the form table_name.id
	 * where id is the value of the primary key of the table.
	 *
	 * @return  string
	 * @throws  NoAssetKey
	 *
	 */
	public function getAssetName()
	{
		$k = $this->getKeyName();

		// If there is no assetKey defined, stop here, or we'll get a wrong name
		if (!$this->_assetKey || !$this->$k)
		{
			throw new NoAssetKey;
		}

		return $this->_assetKey . '.' . (int) $this->$k;
	}

	/**
	 * Method to compute the default name of the asset.
	 * The default name is in the form table_name.id
	 * where id is the value of the primary key of the table.
	 *
	 * @return  string
	 */
	public function getAssetKey()
	{
		return $this->_assetKey;
	}

	/**
	 * This method sets the asset key for the items of this table. Obviously, it
	 * is only meant to be used when you have a table with an asset field.
	 *
	 * @param   string  $assetKey  The name of the asset key to use
	 *
	 * @return  void
	 */
	public function setAssetKey($assetKey)
	{
		$this->_assetKey = $assetKey;
	}

	/**
	 * Method to return the title to use for the asset table.  In
	 * tracking the assets a title is kept for each asset so that there is some
	 * context available in a unified access manager.  Usually this would just
	 * return $this->title or $this->name or whatever is being used for the
	 * primary name of the row. If this method is not overridden, the asset name is used.
	 *
	 * @return  string  The string to use as the title in the asset table.
	 *
	 * @codeCoverageIgnore
	 */
	public function getAssetTitle()
	{
		return $this->getAssetName();
	}

	/**
	 * Method to get the parent asset under which to register this one.
	 * By default, all assets are registered to the ROOT node with ID,
	 * which will default to 1 if none exists.
	 * The extended class can define a table and id to lookup.  If the
	 * asset does not exist it will be created.
	 *
	 * @param   DataModel  $model  A model object for the asset parent.
	 * @param   integer    $id     Id to look up
	 *
	 * @return  integer
	 */
	public function getAssetParentId($model = null, $id = null)
	{
		if ($model)
		{
		} // Prevent phpStorm's inspections from freaking out
		if ($id)
		{
		} // Prevent phpStorm's inspections from freaking out

		// For simple cases, parent to the asset root.
		$assets = Table::getInstance('Asset', 'JTable', ['dbo' => $this->getDbo()]);
		$rootId = $assets->getRootId();

		if (!empty($rootId))
		{
			return $rootId;
		}

		return 1;
	}

	/**
	 * Method to load a row for editing from the version history table.
	 *
	 * @param   integer  $version_id  Key to the version history table.
	 * @param   string   $alias       The type_alias in #__content_types
	 *
	 * @return  boolean  True on success
	 *
	 * @throws  RecordNotLoaded
	 * @throws  BaseException
	 * @since   2.3
	 *
	 */
	public function loadhistory($version_id, $alias)
	{
		// Only attempt to check the row in if it exists.
		if (!$version_id)
		{
			throw new RecordNotLoaded;
		}

		// Get an instance of the row to checkout.
		$historyTable = Table::getInstance('Contenthistory');

		if (!$historyTable->load($version_id))
		{
			throw new BaseException($historyTable->getError());
		}

		$rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data));

		$typeId = Table::getInstance('Contenttype')->getTypeId($alias);

		if ($historyTable->ucm_type_id != $typeId)
		{
			$key = $this->getKeyName();

			if (isset($rowArray[$key]))
			{
				$this->{$this->idFieldName} = $rowArray[$key];
				$this->unlock();
			}

			throw new BaseException(Text::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH'));
		}

		$this->setState('save_date', $historyTable->save_date);
		$this->setState('version_note', $historyTable->version_note);

		$this->bind($rowArray);

		return true;
	}

	/**
	 * Applies view access level filtering for the specified user. Useful to
	 * filter a front-end items listing.
	 *
	 * @param   integer  $userID  The user ID to use. Skip it to use the currently logged in user.
	 *
	 * @return  static  Reference to self
	 */
	public function applyAccessFiltering($userID = null)
	{
		if (!$this->hasField('access'))
		{
			return $this;
		}

		$user = $this->container->platform->getUser($userID);

		$accessField = $this->getFieldAlias('access');

		$this->setState($accessField, $user->getAuthorisedViewLevels());

		return $this;
	}

	/**
	 * Get the content type for ucm
	 *
	 * @return   string  The content type alias
	 *
	 * @throws   Exception  If you have not set the contentType configuration variable
	 */
	public function getContentType()
	{
		if ($this->contentType)
		{
			return $this->contentType;
		}

		throw new NoContentType(get_class($this));
	}

	/**
	 * Check if a UCM content type exists for this resource, and
	 * create it if it does not
	 *
	 * @param   string  $alias  The content type alias (optional)
	 *
	 * @return  void
	 */
	public function checkContentType($alias = null)
	{
		$contentType = new ContentType($this->getDbo());

		if (!$alias)
		{
			$alias = $this->getContentType();
		}

		$aliasParts = explode('.', $alias);

		// Fetch the extension name
		$component = $aliasParts[0];
		$component = ComponentHelper::getComponent($component);

		// Fetch the name using the menu item
		$query = $this->getDbo()->getQuery(true);
		$query->select('title')->from('#__menu')->where('component_id = ' . (int) $component->id);
		$this->getDbo()->setQuery($query);
		$component_name = Text::_($this->getDbo()->loadResult());

		$name = $component_name . ' ' . ucfirst($aliasParts[1]);

		// Create a new content type for our resource
		if (!$contentType->load(['type_alias' => $alias]))
		{
			$contentType->type_title = $name;
			$contentType->type_alias = $alias;
			$contentType->table      = json_encode(
				[
					'special' => [
						'dbtable' => $this->getTableName(),
						'key'     => $this->getKeyName(),
						'type'    => $name,
						'prefix'  => $this->container->getNamespacePrefix() . '\\Model\\',
						'class'   => $this->getName(),
						'config'  => 'array()',
					],
					'common'  => [
						'dbtable' => '#__ucm_content',
						'key'     => 'ucm_id',
						'type'    => 'CoreContent',
						'prefix'  => 'JTable',
						'config'  => 'array()',
					],
				]
			);

			$contentType->field_mappings = json_encode(
				[
					'common'  => [
						0 => [
							"core_content_item_id" => $this->getKeyName(),
							"core_title"           => $this->getUcmCoreAlias('title'),
							"core_state"           => $this->getUcmCoreAlias('enabled'),
							"core_alias"           => $this->getUcmCoreAlias('alias'),
							"core_created_time"    => $this->getUcmCoreAlias('created_on'),
							"core_modified_time"   => $this->getUcmCoreAlias('created_by'),
							"core_body"            => $this->getUcmCoreAlias('body'),
							"core_hits"            => $this->getUcmCoreAlias('hits'),
							"core_publish_up"      => $this->getUcmCoreAlias('publish_up'),
							"core_publish_down"    => $this->getUcmCoreAlias('publish_down'),
							"core_access"          => $this->getUcmCoreAlias('access'),
							"core_params"          => $this->getUcmCoreAlias('params'),
							"core_featured"        => $this->getUcmCoreAlias('featured'),
							"core_metadata"        => $this->getUcmCoreAlias('metadata'),
							"core_language"        => $this->getUcmCoreAlias('language'),
							"core_images"          => $this->getUcmCoreAlias('images'),
							"core_urls"            => $this->getUcmCoreAlias('urls'),
							"core_version"         => $this->getUcmCoreAlias('version'),
							"core_ordering"        => $this->getUcmCoreAlias('ordering'),
							"core_metakey"         => $this->getUcmCoreAlias('metakey'),
							"core_metadesc"        => $this->getUcmCoreAlias('metadesc'),
							"core_catid"           => $this->getUcmCoreAlias('cat_id'),
							"core_xreference"      => $this->getUcmCoreAlias('xreference'),
							"asset_id"             => $this->getUcmCoreAlias('asset_id'),
						],
					],
					'special' => [
						0 => [
						],
					],
				]
			);

			$ignoreFields = [
				$this->getUcmCoreAlias('modified_on', null),
				$this->getUcmCoreAlias('modified_by', null),
				$this->getUcmCoreAlias('locked_by', null),
				$this->getUcmCoreAlias('locked_on', null),
				$this->getUcmCoreAlias('hits', null),
				$this->getUcmCoreAlias('version', null),
			];

			$contentType->content_history_options = json_encode(
				[
					"ignoreChanges" => array_filter($ignoreFields, 'strlen'),
				]
			);

			$contentType->router = '';

			$contentType->store();
		}
	}

	/**
	 * Set a behavior param
	 *
	 * @param   string  $name   The name of the param you want to set
	 * @param   mixed   $value  The value to set
	 *
	 * @return  static   Self, for chaining
	 */
	public function setBehaviorParam($name, $value)
	{
		$this->_behaviorParams[$name] = $value;

		return $this;
	}

	/**
	 * Get a behavior param
	 *
	 * @param   string  $name     The name of the param you want to get
	 * @param   mixed   $default  The default value returned if not set
	 *
	 * @return  mixed
	 */
	public function getBehaviorParam($name, $default = null)
	{
		return $this->_behaviorParams[$name] ?? $default;
	}

	/**
	 * Set or get the backlisted filters.
	 *
	 * Note: passing a null $list to get the filter blacklist is deprecated as of FOF 3.1. Pleas use getBlacklistFilters
	 *       instead.
	 *
	 * @param   mixed    $list   A filter or list of filters to backlist. If null return the list of backlisted filter
	 * @param   boolean  $reset  Reset the blacklist if true
	 *
	 * @return  null|array  Return an array of value if $list is null
	 */
	public function blacklistFilters($list = null, $reset = false)
	{
		if (!isset($list))
		{
			return $this->getBehaviorParam('blacklistFilters', []);
		}

		if (is_string($list))
		{
			$list = (array) $list;
		}

		if (!$reset)
		{
			$list = array_unique(array_merge($this->getBehaviorParam('blacklistFilters', []), $list));
		}

		$this->setBehaviorParam('blacklistFilters', $list);

		return null;
	}

	/**
	 * Get the blacklisted filters.
	 *
	 * @return  array
	 */
	public function getBlacklistFilters()
	{
		return $this->getBehaviorParam('blacklistFilters', []);
	}

	/**
	 * This method is called by Joomla! itself when it needs to update the UCM content
	 *
	 * @return  bool
	 */
	public function updateUcmContent()
	{
		// Process the tags
		$data            = $this->getData();
		$alias           = $this->getContentType();
		$ucmContentTable = Table::getInstance('Corecontent');

		$ucm     = new UCMContent($this, $alias);
		$ucmData = $data ? $ucm->mapData($data) : $ucm->ucmData;

		$primaryId = $ucm->getPrimaryKey($ucmData['common']['core_type_id'], $ucmData['common']['core_content_item_id']);
		$result    = $ucmContentTable->load($primaryId);
		$result    = $result && $ucmContentTable->bind($ucmData['common']);
		$result    = $result && $ucmContentTable->check();
		$result    = $result && $ucmContentTable->store();
		$ucmId     = $ucmContentTable->core_content_id;

		return $result;
	}

	/**
	 * Add a field to the list of fields to be ignored by the check() method
	 *
	 * @param   string  $fieldName  The field to add (can be a field alias)
	 *
	 * @return  void
	 */
	public function addSkipCheckField($fieldName)
	{
		if (!is_array($this->fieldsSkipChecks))
		{
			$this->fieldsSkipChecks = [];
		}

		if (!$this->hasField($fieldName))
		{
			return;
		}

		$fieldName = $this->getFieldAlias($fieldName);

		if (!in_array($fieldName, $this->fieldsSkipChecks))
		{
			$this->fieldsSkipChecks[] = $fieldName;
		}
	}

	/**
	 * Remove a field from the list of fields to be ignored by the check() method
	 *
	 * @param   string  $fieldName  The field to remove (can be a field alias)
	 *
	 * @return  void
	 */
	public function removeSkipCheckField($fieldName)
	{
		if (!is_array($this->fieldsSkipChecks))
		{
			$this->fieldsSkipChecks = [];

			return;
		}

		if (!$this->hasField($fieldName))
		{
			return;
		}

		$fieldName = $this->getFieldAlias($fieldName);

		if (in_array($fieldName, $this->fieldsSkipChecks))
		{
			$index = array_search($fieldName, $this->fieldsSkipChecks);
			unset($this->fieldsSkipChecks[$index]);
		}
	}

	/**
	 * Is a field present in the list of fields to be ignored by the check() method?
	 *
	 * @param   string  $fieldName  The field to check (can be a field alias)
	 *
	 * @return  bool  True if the field is skipped from checks, false if not or if the field doesn't exist.
	 */
	public function hasSkipCheckField($fieldName)
	{
		if (!is_array($this->fieldsSkipChecks))
		{
			$this->fieldsSkipChecks = [];

			return false;
		}

		if (!$this->hasField($fieldName))
		{
			return false;
		}

		$fieldName = $this->getFieldAlias($fieldName);

		return in_array($fieldName, $this->fieldsSkipChecks);
	}

	/**
	 * Loads the asset table related to this table.
	 * This will help tests, too, since we can mock this function.
	 *
	 * @return bool|Asset     False on failure, otherwise JTableAsset
	 */
	protected function getAsset()
	{
		$name = $this->getAssetName();

		// Do NOT touch JTable here -- we are loading the core asset table which is a JTable, not a F0FTable
		$asset = Table::getInstance('Asset');

		if (!$asset->loadByName($name))
		{
			return false;
		}

		return $asset;
	}

	/**
	 * Utility methods that fetches the column name for the field.
	 * If it does not exists, returns a "null" string
	 *
	 * @param   string  $alias  The alias for the column
	 * @param   string  $null   What to return if no column exists
	 *
	 * @return string The column name
	 */
	protected function getUcmCoreAlias($alias, $null = "null")
	{
		if (!$this->hasField($alias))
		{
			return $null;
		}

		return $this->getFieldAlias($alias);
	}

	/**
	 * Returns all lower and upper case permutations of the database prefix
	 *
	 * @return  array
	 */
	protected function getPrefixCasePermutations()
	{
		if (empty(self::$prefixCasePermutations))
		{
			$prefix = $this->getDbo()->getPrefix();
			$suffix = '';

			if (substr($prefix, -1) == '_')
			{
				$suffix = '_';
				$prefix = substr($prefix, 0, -1);
			}

			$letters      = str_split($prefix, 1);
			$permutations = [''];

			foreach ($letters as $nextLetter)
			{
				$lower = strtolower($nextLetter);
				$upper = strtoupper($nextLetter);
				$ret   = [];

				foreach ($permutations as $perm)
				{
					$ret[] = $perm . $lower;

					if ($lower != $upper)
					{
						$ret[] = $perm . $upper;
					}

					$permutations = $ret;
				}
			}

			$permutations = array_merge([
				strtolower($prefix),
				strtoupper($prefix),
			], $permutations);
			$permutations = array_map(function ($x) use ($suffix) {
				return $x . $suffix;
			}, $permutations);

			self::$prefixCasePermutations = array_unique($permutations);
		}

		return self::$prefixCasePermutations;
	}
}
Model/Mixin/DateManipulation.php000060400000006402152455514240012644 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\Mixin;

defined('_JEXEC') || die;

use FOF30\Date\Date;
use FOF30\Model\DataModel;
use JDatabaseDriver;
use JLoader;

/**
 * Trait for date manipulations commonly used in models
 */
trait DateManipulation
{
	/**
	 * Normalise a date into SQL format
	 *
	 * @param   string  $value    The date to normalise
	 * @param   string  $default  The default date to use if the normalised date is invalid or empty (use 'now' for
	 *                            current date/time)
	 *
	 * @return  string
	 */
	protected function normaliseDate($value, $default = '2001-01-01')
	{
		/** @var DataModel $this */

		$db = $this->container->platform->getDbo();

		if (empty($value) || ($value == $db->getNullDate()))
		{
			$value = $default;
		}

		if (empty($value) || ($value == $db->getNullDate()))
		{
			return $value;
		}

		$regex = '/^\d{1,4}(\/|-)\d{1,2}(\/|-)\d{2,4}[[:space:]]{0,}(\d{1,2}:\d{1,2}(:\d{1,2}){0,1}){0,1}$/';

		if (!preg_match($regex, $value))
		{
			$value = $default;
		}

		if (empty($value) || ($value == $db->getNullDate()))
		{
			return $value;
		}

		$date = new Date($value);

		$value = $date->toSql();

		return $value;
	}

	/**
	 * Sort the published up/down times in case they are give out of order. If publish_up equals publish_down the
	 * foreverDate will be used for publish_down.
	 *
	 * @param   string  $publish_up    Publish Up date
	 * @param   string  $publish_down  Publish Down date
	 * @param   string  $foreverDate   See above
	 *
	 * @return  array  (publish_up, publish_down)
	 */
	protected function sortPublishDates($publish_up, $publish_down, $foreverDate = '2038-01-18 00:00:00')
	{
		$jUp   = new Date($publish_up);
		$jDown = new Date($publish_down);

		if ($jDown->toUnix() < $jUp->toUnix())
		{
			$temp         = $publish_up;
			$publish_up   = $publish_down;
			$publish_down = $temp;
		}
		elseif ($jDown->toUnix() == $jUp->toUnix())
		{
			$jDown        = new Date($foreverDate);
			$publish_down = $jDown->toSql();
		}

		return [$publish_up, $publish_down];
	}

	/**
	 * Publish or unpublish a DataModel item based on its publish_up / publish_down fields
	 *
	 * @param   DataModel  $row  The DataModel to publish/unpublish
	 *
	 * @return  void
	 */
	protected function publishByDate(DataModel $row)
	{
		static $uNow = null;

		if (is_null($uNow))
		{
			$jNow = new Date();
			$uNow = $jNow->toUnix();
		}

		/** @var JDatabaseDriver $db */
		$db = $this->container->platform->getDbo();

		$triggered = false;

		$publishDown = $row->getFieldValue('publish_down');

		if (!empty($publishDown) && ($publishDown != $db->getNullDate()))
		{
			$publish_down = $this->normaliseDate($publishDown, '2038-01-18 00:00:00');
			$publish_up   = $this->normaliseDate($row->publish_up, '2001-01-01 00:00:00');

			$jDown = new Date($publish_down);
			$jUp   = new Date($publish_up);

			if (($uNow >= $jDown->toUnix()) && $row->enabled)
			{
				$row->enabled = 0;
				$triggered    = true;
			}
			elseif (($uNow >= $jUp->toUnix()) && !$row->enabled && ($uNow < $jDown->toUnix()))
			{
				$row->enabled = 1;
				$triggered    = true;
			}
		}

		if ($triggered)
		{
			$row->save();
		}
	}
}
Model/Mixin/ImplodedArrays.php000060400000002163152455514240012325 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\Mixin;

defined('_JEXEC') || die;

/**
 * Trait for dealing with imploded arrays, stored as comma-separated values
 */
trait ImplodedArrays
{
	/**
	 * Converts the loaded comma-separated list into an array
	 *
	 * @param   string  $value  The comma-separated list
	 *
	 * @return  array  The exploded array
	 */
	protected function getAttributeForImplodedArray($value)
	{
		if (is_array($value))
		{
			return $value;
		}

		if (empty($value))
		{
			return [];
		}

		$value = explode(',', $value);
		$value = array_map('trim', $value);

		return $value;
	}

	/**
	 * Converts an array of values into a comma separated list
	 *
	 * @param   array  $value  The array of values
	 *
	 * @return  string  The imploded comma-separated list
	 */
	protected function setAttributeForImplodedArray($value)
	{
		if (!is_array($value))
		{
			return $value;
		}

		$value = array_map('trim', $value);
		$value = implode(',', $value);

		return $value;
	}
}
Model/Mixin/JsonData.php000060400000001746152455514240011117 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\Mixin;

defined('_JEXEC') || die;

/**
 * Trait for dealing with data stored as JSON-encoded strings
 */
trait JsonData
{
	/**
	 * Converts the loaded JSON string into an array
	 *
	 * @param   string  $value  The JSON string
	 *
	 * @return  array  The data
	 */
	protected function getAttributeForJson($value)
	{
		if (is_array($value))
		{
			return $value;
		}

		if (empty($value))
		{
			return [];
		}

		$value = json_decode($value, true);

		if (empty($value))
		{
			return [];
		}

		return $value;
	}

	/**
	 * Converts and array into a JSON string
	 *
	 * @param   array  $value  The data
	 *
	 * @return  string  The JSON string
	 */
	protected function setAttributeForJson($value)
	{
		if (!is_array($value))
		{
			return $value;
		}

		$value = json_encode($value);

		return $value;
	}
}
Model/Mixin/Generators.php000060400000003750152455514240011522 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\Mixin;

defined('_JEXEC') || die;

use Exception;
use Generator;
use ReflectionObject;

/**
 * Trait for PHP 5.5 Generators
 */
trait Generators
{
	/**
	 * Returns a PHP Generator of DataModel instances based on your currently set Model state. You can foreach() the
	 * returned generator to walk through each item of the data set.
	 *
	 * WARNING! This only works on PHP 5.5 and later.
	 *
	 * When the generator is done you might get a PHP warning. This is normal. Joomla! doesn't support multiple db
	 * cursors being open at once. What we do instead is clone the database object. Of course it cannot close the db
	 * connection when we dispose of it (since it's already in use by Joomla), hence the warning. Pay no attention.
	 *
	 * @param   integer  $limitstart      How many items from the start to skip (0 = do not skip)
	 * @param   integer  $limit           How many items to return (0 = all)
	 * @param   bool     $overrideLimits  Set to true to override limitstart, limit and ordering
	 *
	 * @return  Generator  A PHP generator of DataModel objects
	 * @throws  Exception
	 * @since   3.3.2
	 */
	public function &getGenerator($limitstart = 0, $limit = 0, $overrideLimits = false)
	{
		$limitstart = max($limitstart, 0);
		$limit      = max($limit, 0);

		$query = $this->buildQuery($overrideLimits);

		$db = clone $this->getDbo();
		$db->setQuery($query, $limitstart, $limit);
		$cursor = $db->execute();

		$reflectDB     = new ReflectionObject($db);
		$refFetchAssoc = $reflectDB->getMethod('fetchAssoc');
		$refFetchAssoc->setAccessible(true);

		while ($data = $refFetchAssoc->invoke($db, $cursor))
		{
			$item = clone $this;
			$item->clearState()->reset(true);
			$item->bind($data);
			$item->relationManager = clone $this->relationManager;
			$item->relationManager->rebase($item);

			yield $item;
		}
	}
}
Model/Mixin/Assertions.php000060400000004171152455514240011541 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\Mixin;

defined('_JEXEC') || die;

use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Trait for check() method assertions
 */
trait Assertions
{
	/**
	 * Make sure $condition is true or throw a RuntimeException with the $message language string
	 *
	 * @param   bool    $condition  The condition which must be true
	 * @param   string  $message    The language key for the message to throw
	 *
	 * @throws  RuntimeException
	 */
	protected function assert($condition, $message)
	{
		if (!$condition)
		{
			throw new RuntimeException(Text::_($message));
		}
	}

	/**
	 * Assert that $value is not empty or throw a RuntimeException with the $message language string
	 *
	 * @param   mixed   $value    The value to check
	 * @param   string  $message  The language key for the message to throw
	 *
	 * @throws  RuntimeException
	 */
	protected function assertNotEmpty($value, $message)
	{
		$this->assert(!empty($value), $message);
	}

	/**
	 * Assert that $value is set to one of $validValues or throw a RuntimeException with the $message language string
	 *
	 * @param   mixed   $value        The value to check
	 * @param   array   $validValues  An array of valid values for $value
	 * @param   string  $message      The language key for the message to throw
	 *
	 * @throws  RuntimeException
	 */
	protected function assertInArray($value, array $validValues, $message)
	{
		$this->assert(in_array($value, $validValues), $message);
	}

	/**
	 * Assert that $value is set to none of $validValues. Otherwise throw a RuntimeException with the $message language
	 * string.
	 *
	 * @param   mixed   $value        The value to check
	 * @param   array   $validValues  An array of invalid values for $value
	 * @param   string  $message      The language key for the message to throw
	 *
	 * @throws  RuntimeException
	 */
	protected function assertNotInArray($value, array $validValues, $message)
	{
		$this->assert(!in_array($value, $validValues, true), $message);
	}

}
Model/Model.php000060400000034050152455514240007362 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Input\Input;
use FOF30\Model\Exception\CannotGetName;
use Joomla\CMS\Filter\InputFilter;
use RuntimeException;
use stdClass;

/**
 * Class Model
 *
 * A generic MVC model implementation
 *
 * @property-read  Input $input  The input object (magic __get returns the Input from the Container)
 */
class Model
{
	/**
	 * Should I save the model's state in the session?
	 *
	 * @var   boolean
	 */
	protected $_savestate = true;

	/**
	 * Should we ignore request data when trying to get state data not already set in the Model?
	 *
	 * @var bool
	 */
	protected $_ignoreRequest = false;

	/**
	 * The model (base) name
	 *
	 * @var    string
	 */
	protected $name;

	/**
	 * A state object
	 *
	 * @var    string
	 */
	protected $state;

	/**
	 * Are the state variables already set?
	 *
	 * @var   boolean
	 */
	protected $_state_set = false;

	/**
	 * The container attached to the model
	 *
	 * @var Container
	 */
	protected $container;

	/**
	 * The state key hash returned by getHash(). This is typically something like "com_foobar.example." (note the dot
	 * at the end). Always use getHash to get it and setHash to set it.
	 *
	 * @var null|string
	 */
	private $stateHash = null;

	/**
	 * Public class constructor
	 *
	 * You can use the $config array to pass some configuration values to the object:
	 *
	 * state            stdClass|array. The state variables of the Model.
	 * use_populate        Boolean. When true the model will set its state from populateState() instead of the request.
	 * ignore_request    Boolean. When true getState will not automatically load state data from the request.
	 *
	 * @param   Container  $container  The configuration variables to this model
	 * @param   array      $config     Configuration values for this model
	 */
	public function __construct(Container $container, array $config = [])
	{
		$this->container = $container;

		// Set the model's name from $config
		if (isset($config['name']))
		{
			$this->name = $config['name'];
		}

		// If $config['name'] is not set, auto-detect the model's name
		$this->name = $this->getName();

		// Do we have a configured state hash? Since 3.1.2.
		if (isset($config['hash']) && !empty($config['hash']))
		{
			$this->setHash($config['hash']);
		}
		elseif (isset($config['hash_view']) && !empty($config['hash_view']))
		{
			$this->getHash($config['hash_view']);
		}

		// Set the model state
		if (array_key_exists('state', $config))
		{
			if (is_object($config['state']))
			{
				$this->state = $config['state'];
			}
			elseif (is_array($config['state']))
			{
				$this->state = (object) $config['state'];
			}
			// Protect vs malformed state
			else
			{
				$this->state = new stdClass();
			}
		}
		else
		{
			$this->state = new stdClass();
		}

		// Set the internal state marker
		if (!empty($config['use_populate']))
		{
			$this->_state_set = true;
		}

		// Set the internal state marker
		if (!empty($config['ignore_request']))
		{
			$this->_ignoreRequest = true;
		}
	}

	/**
	 * Method to get the model name
	 *
	 * The model name. By default parsed using the classname or it can be set
	 * by passing a $config['name'] in the class constructor
	 *
	 * @return  string  The name of the model
	 *
	 * @throws  RuntimeException  If it's impossible to get the name
	 */
	public function getName()
	{
		if (empty($this->name))
		{
			$r = null;

			if (!preg_match('/(.*)\\\\Model\\\\(.*)/i', get_class($this), $r))
			{
				throw new CannotGetName;
			}

			$this->name = $r[2];
		}

		return $this->name;
	}

	/**
	 * Get a filtered state variable
	 *
	 * @param   string  $key          The state variable's name
	 * @param   mixed   $default      The default value to return if it's not already set
	 * @param   string  $filter_type  The filter type to use
	 *
	 * @return  mixed  The state variable's contents
	 */
	public function getState($key = null, $default = null, $filter_type = 'raw')
	{
		if (empty($key))
		{
			return $this->internal_getState();
		}

		// Get the savestate status
		$value = $this->internal_getState($key);

		// Value is not found in the internal state
		if (is_null($value))
		{
			// Can I fetch it from the request?
			if (!$this->_ignoreRequest)
			{
				$value = $this->container->platform->getUserStateFromRequest($this->getHash() . $key, $key, $this->input, $value, 'none', $this->_savestate);

				// Did I get any useful value from the request?
				if (is_null($value))
				{
					return $default;
				}
			}
			// Nope! Let's return the default value
			else
			{
				return $default;
			}
		}

		if (strtoupper($filter_type) == 'RAW')
		{
			return $value;
		}
		else
		{
			$filter = new InputFilter();

			return $filter->clean($value, $filter_type);
		}
	}

	/**
	 * Method to set model state variables
	 *
	 * @param   string  $property  The name of the property.
	 * @param   mixed   $value     The value of the property to set or null.
	 *
	 * @return  mixed  The previous value of the property or null if not set.
	 */
	public function setState($property, $value = null)
	{
		if (is_null($this->state))
		{
			$this->state = new stdClass();
		}

		return $this->state->$property = $value;
	}

	/**
	 * Returns a unique hash for each view, used to prefix the state variables to allow us to retrieve them from the
	 * state later on. If it's not already set (with setHash) it will be set in the form com_something.myModel. If you
	 * pass a non-empty $viewName then if it's not already set it will be instead set in the form of
	 * com_something.viewName.myModel  which is useful when you are reusing models in multiple views and want to avoid
	 * state bleedover among views.
	 *
	 * Also see the hash and hash_view parameters in the constructor's options.
	 *
	 * @return  string
	 */
	public function getHash($viewName = null)
	{
		if (is_null($this->stateHash))
		{
			$this->stateHash = ucfirst($this->container->componentName) . '.';

			if (!empty($viewName))
			{
				$this->stateHash .= $viewName . '.';
			}

			$this->stateHash .= $this->getName() . '.';

		}

		return $this->stateHash;
	}

	/**
	 * Sets the unique hash to prefix the state variables. The hash is cleaned according to the 'CMD' input filtering,
	 * must end in a dot (if not a dot is added automatically) and cannot be empty.
	 *
	 * @param   string  $hash
	 *
	 * @return  void
	 *
	 * @see   self::getHash()
	 */
	public function setHash($hash)
	{
		// Clean the hash, it has to conform to 'CMD' filtering
		$tempInput = new Input(['hash' => $hash]);
		$hash      = $tempInput->getCmd('hash', null);

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

		if (substr($hash, -1) == '_')
		{
			$hash = substr($hash, 0, -1);
		}

		if (substr($hash, -1) != '.')
		{
			$hash .= '.';
		}

		$this->stateHash = $hash;
	}

	/**
	 * Clears the model state, but doesn't touch the internal lists of records,
	 * record tables or record id variables. To clear these values, please use
	 * reset().
	 *
	 * @return  static
	 */
	public function clearState()
	{
		$this->state = new stdClass();

		return $this;
	}

	/**
	 * Clones the model object and returns the clone
	 *
	 * @return  static
	 */
	public function getClone()
	{
		$clone = clone($this);

		return $clone;
	}

	/**
	 * Returns a reference to the model's container
	 *
	 * @return Container
	 */
	public function getContainer()
	{
		return $this->container;
	}

	/**
	 * Magic getter; allows to use the name of model state keys as properties. Also handles magic properties:
	 * $this->input  mapped to $this->container->input
	 *
	 * @param   string  $name  The state variable key
	 *
	 * @return  static
	 */
	public function __get($name)
	{
		// Handle $this->input
		if ($name == 'input')
		{
			return $this->container->input;
		}

		return $this->getState($name);
	}

	/**
	 * Magic setter; allows to use the name of model state keys as properties
	 *
	 * @param   string  $name   The state variable key
	 * @param   mixed   $value  The state variable value
	 *
	 * @return  static
	 */
	public function __set($name, $value)
	{
		return $this->setState($name, $value);
	}

	/**
	 * Magic caller; allows to use the name of model state keys as methods to
	 * set their values.
	 *
	 * @param   string  $name       The state variable key
	 * @param   mixed   $arguments  The state variable contents
	 *
	 * @return  static
	 */
	public function __call($name, $arguments)
	{
		$arg1 = array_shift($arguments);
		$this->setState($name, $arg1);

		return $this;
	}

	/**
	 * Sets the model state auto-save status. By default the model is set up to
	 * save its state to the session.
	 *
	 * @param   boolean  $newState  True to save the state, false to not save it.
	 *
	 * @return  static
	 */
	public function savestate($newState)
	{
		$this->_savestate = $newState ? true : false;

		return $this;
	}

	/**
	 * Public setter for the _savestate variable. Set it to true to save the state
	 * of the Model in the session.
	 *
	 * @return  static
	 */
	public function populateSavestate()
	{
		if (is_null($this->_savestate))
		{
			$savestate = $this->input->getInt('savestate', -999);

			if ($savestate == -999)
			{
				$savestate = true;
			}
			$this->savestate($savestate);
		}
	}

	/**
	 * Gets the ignore request flag. When false, getState() will try to populate state variables not already set from
	 * same-named state variables in the request.
	 *
	 * @return boolean
	 */
	public function getIgnoreRequest()
	{
		return $this->_ignoreRequest;
	}

	/**
	 * Sets the ignore request flag. When false, getState() will try to populate state variables not already set from
	 * same-named state variables in the request.
	 *
	 * @param   boolean  $ignoreRequest
	 *
	 * @return  static
	 */
	public function setIgnoreRequest($ignoreRequest)
	{
		$this->_ignoreRequest = $ignoreRequest;

		return $this;
	}

	/**
	 * Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the
	 * original object. The new object is set up to not save its stats, ignore the request when getting state variables
	 * and comes with an empty state.
	 *
	 * @return  static
	 */
	public function tmpInstance()
	{
		return $this->getClone()->savestate(false)->setIgnoreRequest(true)->clearState();
	}

	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * @return  void
	 *
	 * @note    Calling getState in this method will result in recursion.
	 */
	protected function populateState()
	{
	}

	/**
	 * Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
	 * object's behaviours dispatcher and Joomla! plugin system. Neither handler is expected to return anything (return
	 * values are ignored). If you want to mark an error and cancel the event you have to raise an exception.
	 *
	 * EXAMPLE
	 * Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456)
	 * The event calls:
	 * 1. $this->onBeforeSomething(123, 456)
	 * 2. $his->behavioursDispatcher->trigger('onBeforeSomething', array(&$this, 123, 456))
	 * 3. Joomla! plugin event onComFoobarModelItemBeforeSomething($this, 123, 456)
	 *
	 * @param   string  $event      The name of the event, typically named onPredicateVerb e.g. onBeforeKick
	 * @param   array   $arguments  The arguments to pass to the event handlers
	 *
	 * @return  void
	 */
	protected function triggerEvent($event, array $arguments = [])
	{
		// If there is an object method for this event, call it
		if (method_exists($this, $event))
		{
			switch (count($arguments))
			{
				case 0:
					$this->{$event}();
					break;
				case 1:
					$this->{$event}($arguments[0]);
					break;
				case 2:
					$this->{$event}($arguments[0], $arguments[1]);
					break;
				case 3:
					$this->{$event}($arguments[0], $arguments[1], $arguments[2]);
					break;
				case 4:
					$this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
					break;
				case 5:
					$this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
					break;
				default:
					call_user_func_array([$this, $event], $arguments);
					break;
			}
		}

		// All other event handlers live outside this object, therefore they need to be passed a reference to this
		// objects as the first argument.
		array_unshift($arguments, $this);

		// Trigger the object's behaviours dispatcher, if such a thing exists
		if (property_exists($this, 'behavioursDispatcher') && method_exists($this->behavioursDispatcher, 'trigger'))
		{
			$this->behavioursDispatcher->trigger($event, $arguments);
		}

		// Prepare to run the Joomla! plugins now.

		// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
		$prefix = '';

		if (substr($event, 0, 2) == 'on')
		{
			$prefix = 'on';
			$event  = substr($event, 2);
		}

		// Get the component/model prefix for the event
		$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Model';
		$prefix .= ucfirst($this->getName());

		// The event name will be something like onComFoobarItemsBeforeSomething
		$event = $prefix . $event;

		// Call the Joomla! plugins
		$this->container->platform->runPlugins($event, $arguments);
	}

	/**
	 * Method to get model state variables
	 *
	 * @param   string  $property  Optional parameter name
	 * @param   mixed   $default   Optional default value
	 *
	 * @return  object  The property where specified, the state object where omitted
	 */
	private function internal_getState($property = null, $default = null)
	{
		if (!$this->_state_set)
		{
			// Protected method to auto-populate the model state.
			$this->populateState();

			// Set the model state set flag to true.
			$this->_state_set = true;
		}

		if (is_null($property))
		{
			return $this->state;
		}
		else
		{
			if (property_exists($this->state, $property))
			{
				return $this->state->$property;
			}
			else
			{
				return $default;
			}
		}
	}
}
Model/TreeModel.php000060400000161332152455514240010206 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Model\DataModel\Exception\TreeIncompatibleTable;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtCurrent;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtOther;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtParent;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtSibling;
use FOF30\Model\DataModel\Exception\TreeMethodOnlyAllowedInRoot;
use FOF30\Model\DataModel\Exception\TreeRootNotFound;
use FOF30\Model\DataModel\Exception\TreeUnexpectedPrimaryKey;
use FOF30\Model\DataModel\Exception\TreeUnsupportedMethod;
use JDatabaseQuery;
use Joomla\CMS\Application\ApplicationHelper;
use RuntimeException;
use UnexpectedValueException;

/**
 * A DataModel which implements nested trees
 *
 * @property int    $lft  Left value (for nested set implementation)
 * @property int    $rgt  Right value (for nested set implementation)
 * @property string $hash Slug hash (for faster searching)
 */
class TreeModel extends DataModel
{
	/** @var int The level (depth) of this node in the tree */
	protected $treeDepth = null;

	/** @var TreeModel The root node in the tree */
	protected $treeRoot = null;

	/** @var TreeModel The parent node of ourselves */
	protected $treeParent = null;

	/** @var bool Should I perform a nested get (used to query ascendants/descendants) */
	protected $treeNestedGet = false;

	/**
	 * Public constructor. Overrides the parent constructor, making sure there are lft/rgt columns which make it
	 * compatible with nested sets.
	 *
	 * @param   Container  $container  The configuration variables to this model
	 * @param   array      $config     Configuration values for this model
	 *
	 * @throws RuntimeException When lft/rgt columns are not found
	 * @see \FOF30\Model\DataModel::__construct()
	 *
	 */
	public function __construct(Container $container = null, array $config = [])
	{
		parent::__construct($container, $config);

		if (!$this->hasField('lft') || !$this->hasField('rgt'))
		{
			throw new TreeIncompatibleTable($this->tableName);
		}
	}

	/**
	 * Overrides the automated table checks to handle the 'hash' column for faster searching
	 *
	 * @return $this|DataModel
	 */
	public function check()
	{
		// Create a slug if there is a title and an empty slug
		if ($this->hasField('title') && $this->hasField('slug') && !$this->slug)
		{
			$this->slug = ApplicationHelper::stringURLSafe($this->title);
		}

		// Create the SHA-1 hash of the slug for faster searching (make sure the hash column is CHAR(64) to take
		// advantage of MySQL's optimised searching for fixed size CHAR columns)
		if ($this->hasField('hash') && $this->hasField('slug'))
		{
			$this->hash = sha1($this->slug);
		}

		// Reset cached values
		$this->resetTreeCache();

		// Run the parent checks
		parent::check();

		return $this;
	}

	/**
	 * Delete a node, either the currently loaded one or the one specified in $id. If an $id is specified that node
	 * is loaded before trying to delete it. In the end the data model is reset. If the node has any children nodes
	 * they will be removed before the node itself is deleted.
	 *
	 * @param   mixed  $id  Primary key (id field) value
	 *
	 * @return  $this  for chaining
	 * @throws UnexpectedValueException
	 *
	 */
	public function forceDelete($id = null)
	{
		// Load the specified record (if necessary)
		if (!empty($id))
		{
			$this->findOrFail($id);
		}

		$k  = $this->getIdFieldName();
		$pk = (!$id) ? $this->$k : $id;

		// If no primary key is given, return false.
		if (!$pk)
		{
			throw new TreeUnexpectedPrimaryKey;
		}

		// Execute the logic only if I have a primary key, otherwise I could have weird results
		// Perform the checks on the current node *BEFORE* starting to delete the children
		try
		{
			$this->triggerEvent('onBeforeDelete', [&$pk]);
		}
		catch (Exception $e)
		{
			return false;
		}

		$result = true;

		// Recursively delete all children nodes as long as we are not a leaf node
		if (!$this->isLeaf())
		{
			// Get all sub-nodes
			$table = $this->getClone();
			$table->bind($this->getData());
			$subNodes = $table->getDescendants();

			// Delete all subnodes (goes through the model to trigger the observers)
			if (!empty($subNodes))
			{
				/** @var TreeModel $item */
				foreach ($subNodes as $item)
				{
					// We have to pass the id, so we are getting it again from the database.
					// We have to do in this way, since a previous child could have changed our lft and rgt values
					if (!$item->forceDelete($item->$k))
					{
						// A subnode failed or prevents the delete, continue deleting other nodes,
						// but preserve the current node (ie the parent)
						$result = false;
					}
				}

				// Load it again, since while deleting a children we could have updated ourselves, too
				$this->find($pk);
			}
		}

		if ($result)
		{
			$db = $this->getDbo();

			// Delete the row by primary key.
			$query = $db->getQuery(true);
			$query->delete();
			$query->from($this->getTableName());
			$query->where($db->qn($this->getIdFieldName()) . ' = ' . $db->q($pk));

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

			$this->triggerEvent('onAfterDelete', [&$pk]);
		}

		return $this;
	}

	/**
	 * Not supported in nested sets
	 *
	 * @param   string  $where  Ignored
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  RuntimeException
	 */
	public function reorder($where = '')
	{
		throw new TreeUnsupportedMethod(__METHOD__);
	}

	/**
	 * Not supported in nested sets
	 *
	 * @param   integer  $delta  Ignored
	 * @param   string   $where  Ignored
	 *
	 * @return  static  Self, for chaining
	 *
	 * @throws  RuntimeException
	 */
	public function move($delta, $where = '')
	{
		throw new TreeUnsupportedMethod(__METHOD__);
	}

	/**
	 * Create a new record with the provided data. It is inserted as the last child of the current node's parent
	 *
	 * @param   array  $data  The data to use in the new record
	 *
	 * @return  static  The new node
	 */
	public function create($data)
	{
		$newNode = $this->getClone();
		$newNode->reset();
		$newNode->bind($data);

		if ($this->isRoot())
		{
			return $newNode->insertAsChildOf($this);
		}
		else
		{
			$parentNode = $this->getParent();

			return $newNode->insertAsChildOf($parentNode);
		}
	}

	/**
	 * Makes a copy of the record, inserting it as the last child of the current node's parent.
	 *
	 * @return static
	 *
	 * @codeCoverageIgnore
	 */
	public function copy($data = null)
	{
		$selfData = $this->toArray();

		if (!is_array($data))
		{
			$data = [];
		}

		$data = array_merge($data, $selfData);

		return $this->create($data);
	}

	/**
	 * Reset the record data and the tree cache
	 *
	 * @param   boolean  $useDefaults     Should I use the default values? Default: yes
	 * @param   boolean  $resetRelations  Should I reset the relations too? Default: no
	 *
	 * @return  static  Self, for chaining
	 *
	 * @codeCoverageIgnore
	 */
	public function reset($useDefaults = true, $resetRelations = false)
	{
		$this->resetTreeCache();

		return parent::reset($useDefaults, $resetRelations);
	}

	/**
	 * Insert the current node as a tree root. It is a good idea to never use this method, instead providing a root node
	 * in your schema installation and then sticking to only one root.
	 *
	 * @return static
	 *
	 * @throws  RuntimeException
	 */
	public function insertAsRoot()
	{
		// You can't insert a node that is already saved i.e. the table has an id
		if ($this->getId())
		{
			throw new TreeMethodOnlyAllowedInRoot(__METHOD__);
		}

		// First we need to find the right value of the last parent, a.k.a. the max(rgt) of the table
		$db = $this->getDbo();

		// Get the lft/rgt names
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$query  = $db->getQuery(true)
			->select('MAX(' . $fldRgt . ')')
			->from($db->qn($this->tableName));
		$maxRgt = $db->setQuery($query, 0, 1)->loadResult();

		if (empty($maxRgt))
		{
			$maxRgt = 0;
		}

		$this->lft = ++$maxRgt;
		$this->rgt = ++$maxRgt;

		return $this->save();
	}

	/**
	 * Insert the current node as the first (leftmost) child of a parent node.
	 *
	 * WARNING: If it's an existing node it will be COPIED, not moved.
	 *
	 * @param   TreeModel  $parentNode  The node which will become our parent
	 *
	 * @return $this for chaining
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function insertAsFirstChildOf(TreeModel &$parentNode)
	{
		if ($parentNode->lft >= $parentNode->rgt)
		{
			throw new TreeInvalidLftRgtParent;
		}

		// Get a reference to the database
		$db = $this->getDbo();

		// Get the field names
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));
		$fldLft = $db->qn($this->getFieldAlias('lft'));

		// Nullify the PK, so a new record will be created
		$this->{$this->idFieldName} = null;

		// Get the value of the parent node's rgt
		$myLeft = $parentNode->lft;

		// Update my lft/rgt values
		$this->lft = $myLeft + 1;
		$this->rgt = $myLeft + 2;

		// Update parent node's right (we added two elements in there, remember?)
		$parentNode->rgt += 2;

		// Wrap everything in a transaction
		$db->transactionStart();

		try
		{
			// Make a hole (2 queries)
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($fldLft . ' = ' . $fldLft . '+2')
				->where($fldLft . ' > ' . $db->q($myLeft));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($fldRgt . ' = ' . $fldRgt . '+ 2')
				->where($fldRgt . '>' . $db->q($myLeft));
			$db->setQuery($query)->execute();

			// Insert the new node
			$this->save();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			// Roll back the transaction on error
			$db->transactionRollback();

			throw $e;
		}

		return $this;
	}

	/**
	 * Insert the current node as the last (rightmost) child of a parent node.
	 *
	 * WARNING: If it's an existing node it will be COPIED, not moved.
	 *
	 * @param   TreeModel  $parentNode  The node which will become our parent
	 *
	 * @return $this for chaining
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function insertAsLastChildOf(TreeModel &$parentNode)
	{
		if ($parentNode->lft >= $parentNode->rgt)
		{
			throw new TreeInvalidLftRgtParent;
		}

		// Get a reference to the database
		$db = $this->getDbo();

		// Get the field names
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));
		$fldLft = $db->qn($this->getFieldAlias('lft'));

		// Nullify the PK, so a new record will be created
		$this->{$this->idFieldName} = null;

		// Get the value of the parent node's lft
		$myRight = $parentNode->rgt;

		// Update my lft/rgt values
		$this->lft = $myRight;
		$this->rgt = $myRight + 1;

		// Update parent node's right (we added two elements in there, remember?)
		$parentNode->rgt += 2;

		// Wrap everything in a transaction
		$db->transactionStart();

		try
		{
			// Make a hole (2 queries)
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($fldRgt . ' = ' . $fldRgt . '+2')
				->where($fldRgt . '>=' . $db->q($myRight));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($fldLft . ' = ' . $fldLft . '+2')
				->where($fldLft . '>' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Insert the new node
			$this->save();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			// Roll back the transaction on error
			$db->transactionRollback();

			throw $e;
		}

		return $this;
	}

	/**
	 * Alias for insertAsLastchildOf
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   TreeModel  $parentNode
	 *
	 * @return $this for chaining
	 */
	public function insertAsChildOf(TreeModel &$parentNode)
	{
		return $this->insertAsLastChildOf($parentNode);
	}

	/**
	 * Insert the current node to the left of (before) a sibling node
	 *
	 * WARNING: If it's an existing node it will be COPIED, not moved.
	 *
	 * @param   TreeModel  $siblingNode  We will be inserted before this node
	 *
	 * @return $this for chaining
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function insertLeftOf(TreeModel &$siblingNode)
	{
		if ($siblingNode->lft >= $siblingNode->rgt)
		{
			throw new TreeInvalidLftRgtSibling;
		}

		// Get a reference to the database
		$db = $this->getDbo();

		// Get the field names
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));
		$fldLft = $db->qn($this->getFieldAlias('lft'));

		// Nullify the PK, so a new record will be created
		$this->{$this->idFieldName} = null;

		// Get the value of the parent node's rgt
		$myLeft = $siblingNode->lft;

		// Update my lft/rgt values
		$this->lft = $myLeft;
		$this->rgt = $myLeft + 1;

		// Update sibling's lft/rgt values
		$siblingNode->lft += 2;
		$siblingNode->rgt += 2;

		$db->transactionStart();

		try
		{
			$db->setQuery(
				$db->getQuery(true)
					->update($db->qn($this->tableName))
					->set($fldLft . ' = ' . $fldLft . '+2')
					->where($fldLft . ' >= ' . $db->q($myLeft))
			)->execute();

			$db->setQuery(
				$db->getQuery(true)
					->update($db->qn($this->tableName))
					->set($fldRgt . ' = ' . $fldRgt . '+2')
					->where($fldRgt . ' > ' . $db->q($myLeft))
			)->execute();

			$this->save();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			$db->transactionRollback();

			throw $e;
		}

		return $this;
	}

	/**
	 * Insert the current node to the right of (after) a sibling node
	 *
	 * WARNING: If it's an existing node it will be COPIED, not moved.
	 *
	 * @param   TreeModel  $siblingNode  We will be inserted after this node
	 *
	 * @return $this for chaining
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function insertRightOf(TreeModel &$siblingNode)
	{
		if ($siblingNode->lft >= $siblingNode->rgt)
		{
			throw new TreeInvalidLftRgtSibling;
		}

		// Get a reference to the database
		$db = $this->getDbo();

		// Get the field names
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));
		$fldLft = $db->qn($this->getFieldAlias('lft'));

		// Nullify the PK, so a new record will be created
		$this->{$this->idFieldName} = null;

		// Get the value of the parent node's lft
		$myRight = $siblingNode->rgt;

		// Update my lft/rgt values
		$this->lft = $myRight + 1;
		$this->rgt = $myRight + 2;

		$db->transactionStart();

		try
		{
			$db->setQuery(
				$db->getQuery(true)
					->update($db->qn($this->tableName))
					->set($fldRgt . ' = ' . $fldRgt . '+2')
					->where($fldRgt . ' > ' . $db->q($myRight))
			)->execute();

			$db->setQuery(
				$db->getQuery(true)
					->update($db->qn($this->tableName))
					->set($fldLft . ' = ' . $fldLft . '+2')
					->where($fldLft . ' > ' . $db->q($myRight))
			)->execute();

			$this->save();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			$db->transactionRollback();

			throw $e;
		}

		return $this;
	}

	/**
	 * Alias for insertRightOf
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   TreeModel  $siblingNode
	 *
	 * @return $this for chaining
	 */
	public function insertAsSiblingOf(TreeModel &$siblingNode)
	{
		return $this->insertRightOf($siblingNode);
	}

	/**
	 * Move the current node (and its subtree) one position to the left in the tree, i.e. before its left-hand sibling
	 *
	 * @return $this
	 * @throws  RuntimeException
	 *
	 */
	public function moveLeft()
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		// If it is a root node we will not move the node (roots don't participate in tree ordering)
		if ($this->isRoot())
		{
			return $this;
		}

		// Are we already the leftmost node?
		$parentNode = $this->getParent();

		if ($parentNode->lft == ($this->lft - 1))
		{
			return $this;
		}

		// Get the sibling to the left
		$db          = $this->getDbo();
		$leftSibling = $this->getClone()->reset()
			->whereRaw($db->qn($this->getFieldAlias('rgt')) . ' = ' . $db->q($this->lft - 1))
			->firstOrFail();

		// Move the node
		return $this->moveToLeftOf($leftSibling);
	}

	/**
	 * Move the current node (and its subtree) one position to the right in the tree, i.e. after its right-hand sibling
	 *
	 * @return $this
	 * @throws RuntimeException
	 *
	 */
	public function moveRight()
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		// If it is a root node we will not move the node (roots don't participate in tree ordering)
		if ($this->isRoot())
		{
			return $this;
		}

		// Are we already the rightmost node?
		$parentNode = $this->getParent();

		if ($parentNode->rgt == ($this->rgt + 1))
		{
			return $this;
		}

		// Get the sibling to the right
		$db = $this->getDbo();

		$rightSibling = $this->getClone()->reset()
			->whereRaw($db->qn($this->getFieldAlias('lft')) . ' = ' . $db->q($this->rgt + 1))
			->firstOrFail();

		// Move the node
		return $this->moveToRightOf($rightSibling);
	}

	/**
	 * Moves the current node (and its subtree) to the left of another node. The other node can be in a different
	 * position in the tree or even under a different root.
	 *
	 * @param   TreeModel  $siblingNode
	 *
	 * @return $this for chaining
	 *
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function moveToLeftOf(TreeModel $siblingNode)
	{
		// Sanity checks on current and sibling node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($siblingNode->lft >= $siblingNode->rgt)
		{
			throw new TreeInvalidLftRgtSibling;
		}

		$db    = $this->getDbo();
		$left  = $db->qn($this->getFieldAlias('lft'));
		$right = $db->qn($this->getFieldAlias('rgt'));

		// Get node metrics
		$myLeft  = $this->lft;
		$myRight = $this->rgt;
		$myWidth = $myRight - $myLeft + 1;

		// Get parent metrics
		$sibLeft = $siblingNode->lft;

		// Start the transaction
		$db->transactionStart();

		try
		{
			// Temporary remove subtree being moved
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set("$left = " . $db->q(0) . " - $left")
				->set("$right = " . $db->q(0) . " - $right")
				->where($left . ' >= ' . $db->q($myLeft))
				->where($right . ' <= ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Close hole left behind
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
				->where($left . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
				->where($right . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Make a hole for the new items
			$newSibLeft = ($sibLeft > $myRight) ? $sibLeft - $myWidth : $sibLeft;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
				->where($right . ' >= ' . $db->q($newSibLeft));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
				->where($left . ' >= ' . $db->q($newSibLeft));
			$db->setQuery($query)->execute();

			// Move node and subnodes
			$moveRight = $newSibLeft - $myLeft;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
				->where($left . ' <= 0 - ' . $db->q($myLeft))
				->where($right . ' >= 0 - ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			$db->transactionRollback();

			throw $e;
		}

		// Let's load the record again to fetch the new values for lft and rgt
		$this->findOrFail();

		return $this;
	}

	/**
	 * Moves the current node (and its subtree) to the right of another node. The other node can be in a different
	 * position in the tree or even under a different root.
	 *
	 * @param   TreeModel  $siblingNode
	 *
	 * @return $this for chaining
	 *
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function moveToRightOf(TreeModel $siblingNode)
	{
		// Sanity checks on current and sibling node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($siblingNode->lft >= $siblingNode->rgt)
		{
			throw new TreeInvalidLftRgtSibling;
		}

		$db    = $this->getDbo();
		$left  = $db->qn($this->getFieldAlias('lft'));
		$right = $db->qn($this->getFieldAlias('rgt'));

		// Get node metrics
		$myLeft  = $this->lft;
		$myRight = $this->rgt;
		$myWidth = $myRight - $myLeft + 1;

		// Get parent metrics
		$sibRight = $siblingNode->rgt;

		// Start the transaction
		$db->transactionStart();

		try
		{
			// Temporary remove subtree being moved
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set("$left = " . $db->q(0) . " - $left")
				->set("$right = " . $db->q(0) . " - $right")
				->where($left . ' >= ' . $db->q($myLeft))
				->where($right . ' <= ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Close hole left behind
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
				->where($left . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
				->where($right . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Make a hole for the new items
			$newSibRight = ($sibRight > $myRight) ? $sibRight - $myWidth : $sibRight;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
				->where($left . ' > ' . $db->q($newSibRight));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
				->where($right . ' > ' . $db->q($newSibRight));
			$db->setQuery($query)->execute();

			// Move node and subnodes
			$moveRight = ($sibRight > $myRight) ? $sibRight - $myRight : $sibRight - $myRight + $myWidth;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
				->where($left . ' <= 0 - ' . $db->q($myLeft))
				->where($right . ' >= 0 - ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			$db->transactionRollback();

			throw $e;
		}

		// Let's load the record again to fetch the new values for lft and rgt
		$this->findOrFail();

		return $this;
	}

	/**
	 * Alias for moveToRightOf
	 *
	 * @param   TreeModel  $siblingNode
	 *
	 * @return $this for chaining
	 *
	 * @codeCoverageIgnore
	 */
	public function makeNextSiblingOf(TreeModel $siblingNode)
	{
		return $this->moveToRightOf($siblingNode);
	}

	/**
	 * Alias for makeNextSiblingOf
	 *
	 * @param   TreeModel  $siblingNode
	 *
	 * @return $this for chaining
	 *
	 * @codeCoverageIgnore
	 */
	public function makeSiblingOf(TreeModel $siblingNode)
	{
		return $this->makeNextSiblingOf($siblingNode);
	}

	/**
	 * Alias for moveToLeftOf
	 *
	 * @param   TreeModel  $siblingNode
	 *
	 * @return $this for chaining
	 *
	 * @codeCoverageIgnore
	 */
	public function makePreviousSiblingOf(TreeModel $siblingNode)
	{
		return $this->moveToLeftOf($siblingNode);
	}

	/**
	 * Moves a node and its subtree as a the first (leftmost) child of $parentNode
	 *
	 * @param   TreeModel  $parentNode
	 *
	 * @return $this for chaining
	 *
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function makeFirstChildOf(TreeModel $parentNode)
	{
		// Sanity checks on current and sibling node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($parentNode->lft >= $parentNode->rgt)
		{
			throw new TreeInvalidLftRgtParent;
		}

		$db    = $this->getDbo();
		$left  = $db->qn($this->getFieldAlias('lft'));
		$right = $db->qn($this->getFieldAlias('rgt'));

		// Get node metrics
		$myLeft  = $this->lft;
		$myRight = $this->rgt;
		$myWidth = $myRight - $myLeft + 1;

		// Get parent metrics
		$parentRight = $parentNode->rgt;
		$parentLeft  = $parentNode->lft;

		// Start the transaction
		$db->transactionStart();

		try
		{
			// Temporary remove subtree being moved
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set("$left = " . $db->q(0) . " - $left")
				->set("$right = " . $db->q(0) . " - $right")
				->where($left . ' >= ' . $db->q($myLeft))
				->where($right . ' <= ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Close hole left behind
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
				->where($left . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
				->where($right . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Make a hole for the new items
			$newParentLeft  = ($parentLeft > $myRight) ? $parentLeft - $myWidth : $parentLeft;
			$newParentRight = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
				->where($right . ' >= ' . $db->q($newParentLeft));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
				->where($left . ' > ' . $db->q($newParentLeft));
			$db->setQuery($query)->execute();

			// Move node and subnodes
			$moveRight = $newParentLeft - $myLeft + 1;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
				->where($left . ' <= 0 - ' . $db->q($myLeft))
				->where($right . ' >= 0 - ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			$db->transactionRollback();

			throw $e;
		}

		// Let's load the record again to fetch the new values for lft and rgt
		$this->findOrFail();

		return $this;
	}

	/**
	 * Moves a node and its subtree as a the last (rightmost) child of $parentNode
	 *
	 * @param   TreeModel  $parentNode
	 *
	 * @return $this for chaining
	 *
	 * @throws Exception
	 * @throws RuntimeException
	 */
	public function makeLastChildOf(TreeModel $parentNode)
	{
		// Sanity checks on current and sibling node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($parentNode->lft >= $parentNode->rgt)
		{
			throw new TreeInvalidLftRgtParent;
		}

		$db    = $this->getDbo();
		$left  = $db->qn($this->getFieldAlias('lft'));
		$right = $db->qn($this->getFieldAlias('rgt'));

		// Get node metrics
		$myLeft  = $this->lft;
		$myRight = $this->rgt;
		$myWidth = $myRight - $myLeft + 1;

		// Get parent metrics
		$parentRight = $parentNode->rgt;

		// Start the transaction
		$db->transactionStart();

		try
		{
			// Temporary remove subtree being moved
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set("$left = " . $db->q(0) . " - $left")
				->set("$right = " . $db->q(0) . " - $right")
				->where($left . ' >= ' . $db->q($myLeft))
				->where($right . ' <= ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Close hole left behind
			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
				->where($left . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
				->where($right . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Make a hole for the new items
			$newLeft = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
				->where($left . ' >= ' . $db->q($newLeft));
			$db->setQuery($query)->execute();

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
				->where($right . ' >= ' . $db->q($newLeft));
			$db->setQuery($query)->execute();

			// Move node and subnodes
			$moveRight = ($parentRight > $myRight) ? $parentRight - $myRight - 1 : $parentRight - $myRight - 1 + $myWidth;

			$query = $db->getQuery(true)
				->update($db->qn($this->tableName))
				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
				->where($left . ' <= 0 - ' . $db->q($myLeft))
				->where($right . ' >= 0 - ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			$db->transactionRollback();

			throw $e;
		}

		// Let's load the record again to fetch the new values for lft and rgt
		$this->findOrFail();

		return $this;
	}

	/**
	 * Alias for makeLastChildOf
	 *
	 * @param   TreeModel  $parentNode
	 *
	 * @return $this for chaining
	 *
	 * @codeCoverageIgnore
	 */
	public function makeChildOf(TreeModel $parentNode)
	{
		return $this->makeLastChildOf($parentNode);
	}

	/**
	 * Makes the current node a root (and moving its entire subtree along the way). This is achieved by moving the node
	 * to the right of its root node
	 *
	 * @return  $this  for chaining
	 */
	public function makeRoot()
	{
		// Make sure we are not a root
		if ($this->isRoot())
		{
			return $this;
		}

		// Get a reference to my root
		$myRoot = $this->getRoot();

		// Double check I am not a root
		if ($this->equals($myRoot))
		{
			return $this;
		}

		// Move myself to the right of my root
		$this->moveToRightOf($myRoot);
		$this->treeDepth = 0;

		return $this;
	}

	/**
	 * Gets the level (depth) of this node in the tree. The result is cached in $this->treeDepth for faster fetch.
	 *
	 * @return int|mixed
	 * @throws RuntimeException
	 *
	 */
	public function getLevel()
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if (is_null($this->treeDepth))
		{
			$db = $this->getDbo();

			$fldLft = $db->qn($this->getFieldAlias('lft'));
			$fldRgt = $db->qn($this->getFieldAlias('rgt'));

			$query = $db->getQuery(true)
				->select('(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'))
				->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
				->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
				->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
				->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
				->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
				->group($db->qn('node') . '.' . $fldLft)
				->order($db->qn('node') . '.' . $fldLft . ' ASC');

			$this->treeDepth = $db->setQuery($query, 0, 1)->loadResult();
		}

		return $this->treeDepth;
	}

	/**
	 * Returns the immediate parent of the current node
	 *
	 * @return static
	 * @throws  RuntimeException
	 *
	 */
	public function getParent()
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($this->isRoot())
		{
			return $this;
		}

		if (empty($this->treeParent) || !is_object($this->treeParent) || !($this->treeParent instanceof TreeModel))
		{
			$db = $this->getDbo();

			$fldLft = $db->qn($this->getFieldAlias('lft'));
			$fldRgt = $db->qn($this->getFieldAlias('rgt'));

			$query     = $db->getQuery(true)
				->select($db->qn('parent') . '.' . $fldLft)
				->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
				->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
				->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
				->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
				->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
				->order($db->qn('parent') . '.' . $fldLft . ' DESC');
			$targetLft = $db->setQuery($query, 1, 1)->loadResult();

			$this->treeParent = $this->getClone()->reset()
				->whereRaw($fldLft . ' = ' . $db->q($targetLft))
				->firstOrFail();
		}

		return $this->treeParent;
	}

	/**
	 * Is this a top-level root node?
	 *
	 * @return bool
	 */
	public function isRoot()
	{
		// If lft=1 it is necessarily a root node
		if ($this->lft == 1)
		{
			return true;
		}

		// Otherwise make sure its level is 0
		return $this->getLevel() == 0;
	}

	/**
	 * Is this a leaf node (a node without children)?
	 *
	 * @return bool
	 * @throws  RuntimeException
	 *
	 */
	public function isLeaf()
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		return ($this->rgt - 1) == $this->lft;
	}

	/**
	 * Is this a child node (not root)?
	 *
	 * @codeCoverageIgnore
	 *
	 * @return bool
	 */
	public function isChild()
	{
		return !$this->isRoot();
	}

	/**
	 * Returns true if we are a descendant of $otherNode
	 *
	 * @param   TreeModel  $otherNode
	 *
	 * @return bool
	 * @throws  RuntimeException
	 *
	 */
	public function isDescendantOf(TreeModel $otherNode)
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($otherNode->lft >= $otherNode->rgt)
		{
			throw new TreeInvalidLftRgtOther;
		}

		return ($otherNode->lft < $this->lft) && ($otherNode->rgt > $this->rgt);
	}

	/**
	 * Returns true if $otherNode is ourselves or if we are a descendant of $otherNode
	 *
	 * @param   TreeModel  $otherNode
	 *
	 * @return bool
	 */
	public function isSelfOrDescendantOf(TreeModel $otherNode)
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($otherNode->lft >= $otherNode->rgt)
		{
			throw new TreeInvalidLftRgtOther;
		}

		return ($otherNode->lft <= $this->lft) && ($otherNode->rgt >= $this->rgt);
	}

	/**
	 * Returns true if we are an ancestor of $otherNode
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   TreeModel  $otherNode
	 *
	 * @return bool
	 */
	public function isAncestorOf(TreeModel $otherNode)
	{
		return $otherNode->isDescendantOf($this);
	}

	/**
	 * Returns true if $otherNode is ourselves or we are an ancestor of $otherNode
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   TreeModel  $otherNode
	 *
	 * @return bool
	 */
	public function isSelfOrAncestorOf(TreeModel $otherNode)
	{
		return $otherNode->isSelfOrDescendantOf($this);
	}

	/**
	 * Is $node this very node?
	 *
	 * @param   TreeModel  $node
	 *
	 * @return bool
	 * @throws  RuntimeException
	 *
	 */
	public function equals(TreeModel &$node)
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($node->lft >= $node->rgt)
		{
			throw new TreeInvalidLftRgtOther;
		}

		return (
			($this->getId() == $node->getId())
			&& ($this->lft == $node->lft)
			&& ($this->rgt == $node->rgt)
		);
	}

	/**
	 * Checks if our node is inside the subtree of $otherNode. This is a fast check as only lft and rgt values have to
	 * be compared.
	 *
	 * @param   TreeModel  $otherNode
	 *
	 * @return bool
	 * @throws  RuntimeException
	 *
	 */
	public function insideSubtree(TreeModel $otherNode)
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		if ($otherNode->lft >= $otherNode->rgt)
		{
			throw new TreeInvalidLftRgtOther;
		}

		return ($this->lft > $otherNode->lft) && ($this->rgt < $otherNode->rgt);
	}

	/**
	 * Returns true if both this node and $otherNode are root, leaf or child (same tree scope)
	 *
	 * @param   TreeModel  $otherNode
	 *
	 * @return bool
	 */
	public function inSameScope(TreeModel $otherNode)
	{
		if ($this->isLeaf())
		{
			return $otherNode->isLeaf();
		}
		elseif ($this->isRoot())
		{
			return $otherNode->isRoot();
		}
		elseif ($this->isChild())
		{
			return $otherNode->isChild();
		}
		else
		{
			return false;
		}
	}

	/**
	 * get() will not return the selected node if it's part of the query results
	 *
	 * @param   TreeModel  $node  The node to exclude from the results
	 *
	 * @return void
	 */
	public function withoutNode(TreeModel $node)
	{
		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));

		$this->whereRaw('NOT(' . $db->qn('node') . '.' . $fldLft . ' = ' . $db->q($node->lft) . ')');
	}

	/**
	 * Returns the root node of the tree this node belongs to
	 *
	 * @return static
	 *
	 * @throws RuntimeException
	 */
	public function getRoot()
	{
		// Empty node, let's try to get the first available root, ie lft=1
		if (!$this->getId())
		{
			$this->load(['lft' => 1]);
		}

		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		// If this is a root node return itself (there is no such thing as the root of a root node)
		if ($this->isRoot())
		{
			return $this;
		}

		if (empty($this->treeRoot) || !is_object($this->treeRoot) || !($this->treeRoot instanceof TreeModel))
		{
			$this->treeRoot = null;

			// First try to get the record with the minimum ID
			$db = $this->getDbo();

			$fldLft = $db->qn($this->getFieldAlias('lft'));
			$fldRgt = $db->qn($this->getFieldAlias('rgt'));

			$subQuery = $db->getQuery(true)
				->select('MIN(' . $fldLft . ')')
				->from($db->qn($this->tableName));

			try
			{
				$root = $this->getClone()->reset()
					->whereRaw($fldLft . ' = (' . (string) $subQuery . ')')
					->firstOrFail();

				if ($this->isDescendantOf($root))
				{
					$this->treeRoot = $root;
				}
			}
			catch (RuntimeException $e)
			{
				// If there is no root found throw an exception. Basically: your table is FUBAR.
				throw new TreeRootNotFound($this->tableName, $this->lft);
			}

			// If the above method didn't work, get all roots and select the one with the appropriate lft/rgt values
			if (is_null($this->treeRoot))
			{
				// Find the node with depth = 0, lft < our lft and rgt > our right. That's our root node.
				$query = $db->getQuery(true)
					->select([
						$db->qn('node') . '.' . $fldLft,
						'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'),
					])
					->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
					->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
					->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
					->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
					->where($db->qn('node') . '.' . $fldLft . ' < ' . $db->q($this->lft))
					->where($db->qn('node') . '.' . $fldRgt . ' > ' . $db->q($this->rgt))
					->having($db->qn('depth') . ' = ' . $db->q(0))
					->group($db->qn('node') . '.' . $fldLft);

				// Get the lft value
				$targetLeft = $db->setQuery($query)->loadResult();

				if (empty($targetLeft))
				{
					// If there is no root found throw an exception. Basically: your table is FUBAR.
					throw new TreeRootNotFound($this->tableName, $this->lft);
				}

				try
				{
					$this->treeRoot = $this->getClone()->reset()
						->whereRaw($fldLft . ' = ' . $db->q($targetLeft))
						->firstOrFail();
				}
				catch (RuntimeException $e)
				{
					// If there is no root found throw an exception. Basically: your table is FUBAR.
					throw new TreeRootNotFound($this->tableName, $this->lft);
				}
			}
		}

		return $this->treeRoot;
	}

	/**
	 * Get all ancestors to this node and the node itself. In other words it gets the full path to the node and the node
	 * itself.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getAncestorsAndSelf()
	{
		$this->scopeAncestorsAndSelf();

		return $this->get(true);
	}

	/**
	 * Get all ancestors to this node and the node itself, but not the root node. If you want to
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getAncestorsAndSelfWithoutRoot()
	{
		$this->scopeAncestorsAndSelf();
		$this->scopeWithoutRoot();

		return $this->get(true);
	}

	/**
	 * Get all ancestors to this node but not the node itself. In other words it gets the path to the node, without the
	 * node itself.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getAncestors()
	{
		$this->scopeAncestorsAndSelf();
		$this->scopeWithoutSelf();

		return $this->get(true);
	}

	/**
	 * Get all ancestors to this node but not the node itself and its root.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getAncestorsWithoutRoot()
	{
		$this->scopeAncestors();
		$this->scopeWithoutRoot();

		return $this->get(true);
	}

	/**
	 * Get all sibling nodes, including ourselves
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getSiblingsAndSelf()
	{
		$this->scopeSiblingsAndSelf();

		return $this->get(true);
	}

	/**
	 * Get all sibling nodes, except ourselves
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getSiblings()
	{
		$this->scopeSiblings();

		return $this->get(true);
	}

	/**
	 * Get all leaf nodes in the tree. You may want to use the scopes to narrow down the search in a specific subtree or
	 * path.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getLeaves()
	{
		$this->scopeLeaves();

		return $this->get(true);
	}

	/**
	 * Get all descendant (children) nodes and ourselves.
	 *
	 * Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getDescendantsAndSelf()
	{
		$this->scopeDescendantsAndSelf();

		return $this->get(true);
	}

	/**
	 * Get only our descendant (children) nodes, not ourselves.
	 *
	 * Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getDescendants()
	{
		$this->scopeDescendants();

		return $this->get(true);
	}

	/**
	 * Get the immediate descendants (children). Unlike getDescendants it only goes one level deep into the tree
	 * structure. Descendants of descendant nodes will not be returned.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return DataModel\Collection
	 */
	public function getImmediateDescendants()
	{
		$this->scopeImmediateDescendants();

		return $this->get(true);
	}

	/**
	 * Returns a hashed array where each element's key is the value of the $key column (default: the ID column of the
	 * table) and its value is the value of the $column column (default: title). Each nesting level will have the value
	 * of the $column column prefixed by a number of $separator strings, as many as its nesting level (depth).
	 *
	 * This is useful for creating HTML select elements showing the hierarchy in a human readable format.
	 *
	 * @param   string  $column
	 * @param   null    $key
	 * @param   string  $seperator
	 *
	 * @return array
	 */
	public function getNestedList($column = 'title', $key = null, $seperator = '  ')
	{
		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		if (empty($key) || !$this->hasField($key))
		{
			$key = $this->getIdFieldName();
		}

		if (empty($column))
		{
			$column = 'title';
		}

		$fldKey    = $db->qn($this->getFieldAlias($key));
		$fldColumn = $db->qn($this->getFieldAlias($column));

		$query = $db->getQuery(true)
			->select([
				$db->qn('node') . '.' . $fldKey,
				$db->qn('node') . '.' . $fldColumn,
				'(COUNT(' . $db->qn('parent') . '.' . $fldKey . ') - 1) AS ' . $db->qn('depth'),
			])
			->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
			->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
			->group($db->qn('node') . '.' . $fldLft)
			->order($db->qn('node') . '.' . $fldLft . ' ASC');

		$tempResults = $db->setQuery($query)->loadAssocList();
		$ret         = [];

		if (!empty($tempResults))
		{
			foreach ($tempResults as $row)
			{
				$ret[$row[$key]] = str_repeat($seperator, $row['depth']) . $row[$column];
			}
		}

		return $ret;
	}

	/**
	 * Locate a node from a given path, e.g. "/some/other/leaf"
	 *
	 * Notes:
	 * - This will only work when you have a "slug" and a "hash" field in your table.
	 * - If the path starts with "/" we will use the root with lft=1. Otherwise the first component of the path is
	 *   supposed to be the slug of the root node.
	 * - If the root node is not found you'll get null as the return value
	 * - You will also get null if any component of the path is not found
	 *
	 * @param   string  $path  The path to locate
	 *
	 * @return TreeModel|null The found node or null if nothing is found
	 */
	public function findByPath($path)
	{
		// No path? No node.
		if (empty($path))
		{
			return null;
		}

		// Extract the path parts
		$pathParts = explode('/', $path);

		$firstElement = array_shift($pathParts);

		if (!empty($firstElement))
		{
			array_unshift($pathParts, $firstElement);
		}

		// Just a slash? Return the root
		if (empty($pathParts[0]))
		{
			return $this->getRoot();
		}

		// Get the quoted field names
		$db = $this->getDbo();

		$fldLeft  = $db->qn($this->getFieldAlias('lft'));
		$fldRight = $db->qn($this->getFieldAlias('rgt'));
		$fldHash  = $db->qn($this->getFieldAlias('hash'));

		// Get the quoted hashes of the slugs
		$pathHashesQuoted = [];

		foreach ($pathParts as $part)
		{
			$pathHashesQuoted[] = $db->q(sha1($part));
		}

		// Get all nodes with slugs matching our path
		$query        = $db->getQuery(true)
			->select([
				$db->qn('node') . '.*',
				'(COUNT(' . $db->qn('parent') . '.' . $db->qn($this->getFieldAlias('lft')) . ') - 1) AS ' . $db->qn('depth'),
			])->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
			->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
			->where($db->qn('node') . '.' . $fldLeft . ' >= ' . $db->qn('parent') . '.' . $fldLeft)
			->where($db->qn('node') . '.' . $fldLeft . ' <= ' . $db->qn('parent') . '.' . $fldRight)
			->where($db->qn('node') . '.' . $fldHash . ' IN (' . implode(',', $pathHashesQuoted) . ')')
			->group($db->qn('node') . '.' . $fldLeft)
			->order([
				$db->qn('depth') . ' ASC',
				$db->qn('node') . '.' . $fldLeft . ' ASC',
			]);
		$queryResults = $db->setQuery($query)->loadAssocList();

		$pathComponents = [];

		// Handle paths with (no root slug provided) and without (root slug provided) a leading slash
		$currentLevel = (substr($path, 0, 1) == '/') ? 0 : -1;
		$maxLevel     = count($pathParts) + $currentLevel;

		// Initialise the path results array
		$i = $currentLevel;

		foreach ($pathParts as $part)
		{
			$i++;
			$pathComponents[$i] = [
				'slug' => $part,
				'id'   => null,
				'lft'  => null,
				'rgt'  => null,
			];
		}

		// Search for the best matching nodes
		$colSlug = $this->getFieldAlias('slug');
		$colLft  = $this->getFieldAlias('lft');
		$colRgt  = $this->getFieldAlias('rgt');
		$colId   = $this->getIdFieldName();

		foreach ($queryResults as $row)
		{
			if ($row['depth'] == $currentLevel + 1)
			{
				if ($row[$colSlug] != $pathComponents[$currentLevel + 1]['slug'])
				{
					continue;
				}

				if ($currentLevel > 0)
				{
					if ($row[$colLft] < $pathComponents[$currentLevel]['lft'])
					{
						continue;
					}

					if ($row[$colRgt] > $pathComponents[$currentLevel]['rgt'])
					{
						continue;
					}
				}

				$currentLevel++;
				$pathComponents[$currentLevel]['id']  = $row[$colId];
				$pathComponents[$currentLevel]['lft'] = $row[$colLft];
				$pathComponents[$currentLevel]['rgt'] = $row[$colRgt];
			}

			if ($currentLevel == $maxLevel)
			{
				break;
			}
		}

		// Get the last found node
		$lastNode = array_pop($pathComponents);

		// If the node exists, return it...
		if (!empty($lastNode['lft']))
		{
			return $this->getClone()->reset()->where($colLft, '=', $lastNode['lft'])->firstOrFail();
		}

		// ...otherwise return null
		return null;
	}

	/**
	 * Overrides the DataModel's buildQuery to allow nested set searches using the provided scopes
	 *
	 * @param   bool  $overrideLimits
	 *
	 * @return JDatabaseQuery
	 */
	public function buildQuery($overrideLimits = false)
	{
		$db = $this->getDbo();

		$query = parent::buildQuery($overrideLimits);

		// Wipe out select and from sections
		$query->clear('select');
		$query->clear('from');

		$query
			->select($db->qn('node') . '.*')
			->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'));

		if ($this->treeNestedGet)
		{
			$query
				->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'));
		}

		return $query;
	}

	protected function onAfterDelete($oid)
	{
		$db = $this->getDbo();

		$myLeft  = $this->lft;
		$myRight = $this->rgt;

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		// Move all siblings to the left
		$width = $this->rgt - $this->lft + 1;

		// Wrap everything in a transaction
		$db->transactionStart();

		try
		{
			// Shrink lft values
			$query = $db->getQuery(true)
				->update($db->qn($this->getTableName()))
				->set($fldLft . ' = ' . $fldLft . ' - ' . $width)
				->where($fldLft . ' > ' . $db->q($myLeft));
			$db->setQuery($query)->execute();

			// Shrink rgt values
			$query = $db->getQuery(true)
				->update($db->qn($this->getTableName()))
				->set($fldRgt . ' = ' . $fldRgt . ' - ' . $width)
				->where($fldRgt . ' > ' . $db->q($myRight));
			$db->setQuery($query)->execute();

			// Commit the transaction
			$db->transactionCommit();
		}
		catch (Exception $e)
		{
			// Roll back the transaction on error
			$db->transactionRollback();

			throw $e;
		}

		return $this;
	}

	/**
	 * get() will return all ancestor nodes and ourselves
	 *
	 * @return void
	 */
	protected function scopeAncestorsAndSelf()
	{
		$this->treeNestedGet = true;

		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' >= ' . $db->qn('node') . '.' . $fldLft);
		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' <= ' . $db->qn('node') . '.' . $fldRgt);
		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
	}

	/**
	 * get() will return all ancestor nodes but not ourselves
	 *
	 * @return void
	 */
	protected function scopeAncestors()
	{
		$this->treeNestedGet = true;

		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' > ' . $db->qn('node') . '.' . $fldLft);
		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' < ' . $db->qn('node') . '.' . $fldRgt);
		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
	}

	/**
	 * get() will return all sibling nodes and ourselves
	 *
	 * @return void
	 */
	protected function scopeSiblingsAndSelf()
	{
		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$parent = $this->getParent();
		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->q($parent->lft));
		$this->whereRaw($db->qn('node') . '.' . $fldRgt . ' < ' . $db->q($parent->rgt));
	}

	/**
	 * get() will return all sibling nodes but not ourselves
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	protected function scopeSiblings()
	{
		$this->scopeSiblingsAndSelf();
		$this->scopeWithoutSelf();
	}

	/**
	 * get() will return only leaf nodes
	 *
	 * @return void
	 */
	protected function scopeLeaves()
	{
		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' = ' . $db->qn('node') . '.' . $fldRgt . ' - ' . $db->q(1));
	}

	/**
	 * get() will return all descendants (even subtrees of subtrees!) and ourselves
	 *
	 * @return void
	 */
	protected function scopeDescendantsAndSelf()
	{
		$this->treeNestedGet = true;

		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft);
		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt);
		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
	}

	/**
	 * get() will return all descendants (even subtrees of subtrees!) but not ourselves
	 *
	 * @return void
	 */
	protected function scopeDescendants()
	{
		$this->treeNestedGet = true;

		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->qn('parent') . '.' . $fldLft);
		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' < ' . $db->qn('parent') . '.' . $fldRgt);
		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
	}

	/**
	 * get() will only return immediate descendants (first level children) of the current node
	 *
	 * @return void
	 * @throws RuntimeException
	 *
	 */
	protected function scopeImmediateDescendants()
	{
		// Sanity checks on current node position
		if ($this->lft >= $this->rgt)
		{
			throw new TreeInvalidLftRgtCurrent;
		}

		$db = $this->getDbo();

		$fldLft = $db->qn($this->getFieldAlias('lft'));
		$fldRgt = $db->qn($this->getFieldAlias('rgt'));

		$subQuery = $db->getQuery(true)
			->select([
				$db->qn('node') . '.' . $fldLft,
				'(COUNT(*) - 1) AS ' . $db->qn('depth'),
			])
			->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
			->from($db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
			->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
			->group($db->qn('node') . '.' . $fldLft)
			->order($db->qn('node') . '.' . $fldLft . ' ASC');

		$query = $db->getQuery(true)
			->select([
				$db->qn('node') . '.' . $fldLft,
				'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - (' .
				$db->qn('sub_tree') . '.' . $db->qn('depth') . ' + 1)) AS ' . $db->qn('depth'),
			])
			->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
			->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
			->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('sub_parent'))
			->join('CROSS', '(' . $subQuery . ') AS ' . $db->qn('sub_tree'))
			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('sub_parent') . '.' . $fldLft)
			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('sub_parent') . '.' . $fldRgt)
			->where($db->qn('sub_parent') . '.' . $fldLft . ' = ' . $db->qn('sub_tree') . '.' . $fldLft)
			->group($db->qn('node') . '.' . $fldLft)
			->having([
				$db->qn('depth') . ' > ' . $db->q(0),
				$db->qn('depth') . ' <= ' . $db->q(1),
			])
			->order($db->qn('node') . '.' . $fldLft . ' ASC');

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

		if (empty($leftValues))
		{
			$leftValues = [0];
		}

		array_walk($leftValues, function (&$item, $key) use (&$db) {
			$item = $db->q($item);
		});

		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' IN (' . implode(',', $leftValues) . ')');
	}

	/**
	 * get() will not return ourselves if it's part of the query results
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	protected function scopeWithoutSelf()
	{
		$this->withoutNode($this);
	}

	/**
	 * get() will not return our root if it's part of the query results
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	protected function scopeWithoutRoot()
	{
		$rootNode = $this->getRoot();
		$this->withoutNode($rootNode);
	}

	/**
	 * Resets cached values used to speed up querying the tree
	 *
	 * @return  static  for chaining
	 */
	protected function resetTreeCache()
	{
		$this->treeDepth     = null;
		$this->treeRoot      = null;
		$this->treeParent    = null;
		$this->treeNestedGet = false;

		return $this;
	}
}
Model/DataModel/Relation/BelongsTo.php000060400000004735152455514240013634 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation;

defined('_JEXEC') || die;

use FOF30\Model\DataModel;

/**
 * BelongsTo (reverse 1-to-1 or 1-to-many) relation: this model is a child which belongs to the foreign table
 *
 * For example, parentModel is Articles and foreignModel is Users. Each article belongs to one user. One user can have
 * one or more article.
 *
 * Example #2: parentModel is Phones and foreignModel is Users. Each phone belongs to one user. One user can have zero
 * or one phones.
 */
class BelongsTo extends HasOne
{
	/**
	 * Public constructor. Initialises the relation.
	 *
	 * @param   DataModel  $parentModel       The data model we are attached to
	 * @param   string     $foreignModelName  The name of the foreign key's model in the format
	 *                                        "modelName@com_something"
	 * @param   string     $localKey          The local table key for this relation, default: parentModel's ID field
	 *                                        name
	 * @param   string     $foreignKey        The foreign key for this relation, default: parentModel's ID field name
	 * @param   string     $pivotTable        IGNORED
	 * @param   string     $pivotLocalKey     IGNORED
	 * @param   string     $pivotForeignKey   IGNORED
	 */
	public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
	{
		parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);

		if (empty($localKey))
		{
			/** @var DataModel $foreignModel */
			$foreignModel = $this->getForeignModel();
			$foreignModel->setIgnoreRequest(true);

			$this->localKey = $foreignModel->getIdFieldName();
		}

		if (empty($foreignKey))
		{
			if (!isset($foreignModel))
			{
				/** @var DataModel $foreignModel */
				$foreignModel = $this->getForeignModel();
				$foreignModel->setIgnoreRequest(true);
			}

			$this->foreignKey = $foreignModel->getIdFieldName();
		}
	}

	/**
	 * This is not supported by the belongsTo relation
	 *
	 * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
	 */
	public function getNew()
	{
		throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported by the belongsTo relation type");
	}

}
Model/DataModel/Relation/HasMany.php000060400000011700152455514240013266 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation;

defined('_JEXEC') || die;

use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Relation;
use JDatabaseQuery;

/**
 * HasMany (1-to-many) relation: this model is a parent which has zero or more children in the foreign table
 *
 * For example, parentModel is Users and foreignModel is Articles. Each user has zero or more articles.
 */
class HasMany extends Relation
{
	/**
	 * Public constructor. Initialises the relation.
	 *
	 * @param   DataModel  $parentModel       The data model we are attached to
	 * @param   string     $foreignModelName  The name of the foreign key's model in the format
	 *                                        "modelName@com_something"
	 * @param   string     $localKey          The local table key for this relation, default: parentModel's ID field
	 *                                        name
	 * @param   string     $foreignKey        The foreign key for this relation, default: parentModel's ID field name
	 * @param   string     $pivotTable        IGNORED
	 * @param   string     $pivotLocalKey     IGNORED
	 * @param   string     $pivotForeignKey   IGNORED
	 */
	public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
	{
		parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);

		if (empty($this->localKey))
		{
			$this->localKey = $parentModel->getIdFieldName();
		}

		if (empty($this->foreignKey))
		{
			$this->foreignKey = $this->localKey;
		}
	}

	/**
	 * Returns the count sub-query for DataModel's has() and whereHas() methods.
	 *
	 * @param   string  $tableAlias  The alias of the local table in the query. Leave blank to use the table's name.
	 *
	 * @return JDatabaseQuery
	 */
	public function getCountSubquery($tableAlias = null)
	{
		// Get a model instance
		$foreignModel = $this->getForeignModel();
		$foreignModel->setIgnoreRequest(true);

		$db = $foreignModel->getDbo();

		if (empty($tableAlias))
		{
			$tableAlias = $this->parentModel->getTableName();
		}

		$query = $db->getQuery(true)
			->select('COUNT(*)')
			->from($db->qn($foreignModel->getTableName(), 'reltbl'))
			->where($db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey)) . ' = '
				. $db->qn($tableAlias) . '.'
				. $db->qn($this->parentModel->getFieldAlias($this->localKey)));

		return $query;
	}

	/**
	 * Returns a new item of the foreignModel type, pre-initialised to fulfil this relation
	 *
	 * @return DataModel
	 *
	 * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
	 */
	public function getNew()
	{
		// Get a model instance
		$foreignModel = $this->getForeignModel();
		$foreignModel->setIgnoreRequest(true);

		// Prime the model
		$foreignModel->setFieldValue($this->foreignKey, $this->parentModel->getFieldValue($this->localKey));

		// Make sure we do have a data list
		if (!($this->data instanceof DataModel\Collection))
		{
			$this->getData();
		}

		// Add the model to the data list
		$this->data->add($foreignModel);

		return $this->data->last();
	}

	/**
	 * Applies the relation filters to the foreign model when getData is called
	 *
	 * @param   DataModel             $foreignModel    The foreign model you're operating on
	 * @param   DataModel\Collection  $dataCollection  If it's an eager loaded relation, the collection of loaded
	 *                                                 parent records
	 *
	 * @return boolean Return false to force an empty data collection
	 */
	protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null)
	{
		// Decide how to proceed, based on eager or lazy loading
		if (is_object($dataCollection))
		{
			// Eager loaded relation
			if (!empty($dataCollection))
			{
				// Get a list of local keys from the collection
				$values = [];

				/** @var $item DataModel */
				foreach ($dataCollection as $item)
				{
					$v = $item->getFieldValue($this->localKey, null);

					if (!is_null($v))
					{
						$values[] = $v;
					}
				}

				// Keep only unique values. This double step is required to re-index the array and avoid issues with
				// Joomla Registry class. See issue #681
				$values = array_values(array_unique($values));

				// Apply the filter
				if (!empty($values))
				{
					$foreignModel->where($this->foreignKey, 'in', $values);
				}
				else
				{
					return false;
				}
			}
			else
			{
				return false;
			}
		}
		else
		{
			// Lazy loaded relation; get the single local key
			$localKey = $this->parentModel->getFieldValue($this->localKey, null);

			if (is_null($localKey) || ($localKey === ''))
			{
				return false;
			}

			$foreignModel->where($this->foreignKey, '==', $localKey);
		}

		return true;
	}
}
Model/DataModel/Relation/Exception/ForeignModelNotFound.php000060400000000473152455514240017720 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation\Exception;

defined('_JEXEC') || die;

use Exception;

class ForeignModelNotFound extends Exception
{
}
Model/DataModel/Relation/Exception/SaveNotSupported.php000060400000000467152455514240017161 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation\Exception;

defined('_JEXEC') || die;

use Exception;

class SaveNotSupported extends Exception
{
}
Model/DataModel/Relation/Exception/RelationNotFound.php000060400000000467152455514240017126 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation\Exception;

defined('_JEXEC') || die;

use Exception;

class RelationNotFound extends Exception
{
}
Model/DataModel/Relation/Exception/RelationTypeNotFound.php000060400000000473152455514240017765 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation\Exception;

defined('_JEXEC') || die;

use Exception;

class RelationTypeNotFound extends Exception
{
}
Model/DataModel/Relation/Exception/PivotTableNotFound.php000060400000000471152455514240017415 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation\Exception;

defined('_JEXEC') || die;

use Exception;

class PivotTableNotFound extends Exception
{
}
Model/DataModel/Relation/Exception/NewNotSupported.php000060400000000466152455514240017013 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation\Exception;

defined('_JEXEC') || die;

use Exception;

class NewNotSupported extends Exception
{
}
Model/DataModel/Relation/HasOne.php000060400000002557152455514240013115 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation;

defined('_JEXEC') || die;

use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Collection;

/**
 * HasOne (straight 1-to-1) relation: this model is a parent which has exactly one child in the foreign table
 *
 * For example, parentModel is Users and foreignModel is Phones. Each uses has exactly one Phone.
 */
class HasOne extends HasMany
{
	/**
	 * Get the relation data.
	 *
	 * If you want to apply additional filtering to the foreign model, use the $callback. It can be any function,
	 * static method, public method or closure with an interface of function(DataModel $foreignModel). You are not
	 * supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do:
	 * $foreignModel->setState('foo', 'bar')
	 *
	 * @param   callable    $callback  The callback to run on the remote model.
	 * @param   Collection  $dataCollection
	 *
	 * @return Collection|DataModel
	 */
	public function getData($callback = null, Collection $dataCollection = null)
	{
		if (is_null($dataCollection))
		{
			return parent::getData($callback, $dataCollection)->first();
		}
		else
		{
			return parent::getData($callback, $dataCollection);
		}
	}
}
Model/DataModel/Relation/BelongsToMany.php000060400000027241152455514240014456 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Relation;

defined('_JEXEC') || die;

use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Relation;
use JDatabaseQuery;

/**
 * BelongsToMany (many-to-many) relation: one or more records of this model are related to one or more records in the
 * foreign model.
 *
 * For example, parentModel is Users and foreignModel is Groups. Each user can be assigned to many groups. Each group
 * can be assigned to many users.
 */
class BelongsToMany extends Relation
{
	/**
	 * Public constructor. Initialises the relation.
	 *
	 * @param   DataModel  $parentModel       The data model we are attached to
	 * @param   string     $foreignModelName  The name of the foreign key's model in the format
	 *                                        "modelName@com_something"
	 * @param   string     $localKey          The local table key for this relation, default: parentModel's ID field
	 *                                        name
	 * @param   string     $foreignKey        The foreign key for this relation, default: parentModel's ID field name
	 * @param   string     $pivotTable        For many-to-many relations, the pivot (glue) table
	 * @param   string     $pivotLocalKey     For many-to-many relations, the pivot table's column storing the local
	 *                                        key
	 * @param   string     $pivotForeignKey   For many-to-many relations, the pivot table's column storing the foreign
	 *                                        key
	 *
	 * @throws  DataModel\Relation\Exception\PivotTableNotFound
	 */
	public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
	{
		parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);

		if (empty($localKey))
		{
			$this->localKey = $parentModel->getIdFieldName();
		}

		if (empty($pivotLocalKey))
		{
			$this->pivotLocalKey = $this->localKey;
		}

		if (empty($foreignKey))
		{
			/** @var DataModel $foreignModel */
			$foreignModel = $this->getForeignModel();
			$foreignModel->setIgnoreRequest(true);

			$this->foreignKey = $foreignModel->getIdFieldName();
		}

		if (empty($pivotForeignKey))
		{
			$this->pivotForeignKey = $this->foreignKey;
		}

		if (empty($pivotTable))
		{
			// Get the local model's name (e.g. "users")
			$localName = $parentModel->getName();
			$localName = strtolower($localName);

			// Get the foreign model's name (e.g. "groups")
			if (!isset($foreignModel))
			{
				/** @var DataModel $foreignModel */
				$foreignModel = $this->getForeignModel();
				$foreignModel->setIgnoreRequest(true);
			}

			$foreignName = $foreignModel->getName();
			$foreignName = strtolower($foreignName);

			// Get the local model's app name
			$parentModelBareComponent  = $parentModel->getContainer()->bareComponentName;
			$foreignModelBareComponent = $foreignModel->getContainer()->bareComponentName;

			// There are two possibilities for the table name: #__component_local_foreign or #__component_foreign_local.
			// There are also two possibilities for a component name (local or foreign model's)
			$db     = $parentModel->getDbo();
			$prefix = $db->getPrefix();

			$tableNames = [
				'#__' . strtolower($parentModelBareComponent) . '_' . $localName . '_' . $foreignName,
				'#__' . strtolower($parentModelBareComponent) . '_' . $foreignName . '_' . $localName,
				'#__' . strtolower($foreignModelBareComponent) . '_' . $localName . '_' . $foreignName,
				'#__' . strtolower($foreignModelBareComponent) . '_' . $foreignName . '_' . $localName,
			];

			$allTables = $db->getTableList();

			$this->pivotTable = null;

			foreach ($tableNames as $tableName)
			{
				$checkName = $prefix . substr($tableName, 3);

				if (in_array($checkName, $allTables))
				{
					$this->pivotTable = $tableName;
				}
			}

			if (empty($this->pivotTable))
			{
				throw new DataModel\Relation\Exception\PivotTableNotFound("Pivot table for many-to-many relation between '$localName and '$foreignName' not found'");
			}
		}
	}

	/**
	 * Populates the internal $this->data collection from the contents of the provided collection. This is used by
	 * DataModel to push the eager loaded data into each item's relation.
	 *
	 * @param   DataModel\Collection  $data    The relation data to push into this relation
	 * @param   mixed                 $keyMap  Passes around the local to foreign key map
	 *
	 * @return void
	 */
	public function setDataFromCollection(DataModel\Collection &$data, $keyMap = null)
	{
		$this->data = new DataModel\Collection();

		if (!is_array($keyMap))
		{
			return;
		}

		if (!empty($data))
		{
			// Get the local key value
			$localKeyValue = $this->parentModel->getFieldValue($this->localKey);

			// Make sure this local key exists in the (cached) pivot table
			if (!isset($keyMap[$localKeyValue]))
			{
				return;
			}

			/** @var DataModel $item */
			foreach ($data as $key => $item)
			{
				// Only accept foreign items whose key is associated in the pivot table with our local key
				if (in_array($item->getFieldValue($this->foreignKey), $keyMap[$localKeyValue]))
				{
					$this->data->add($item);
				}
			}
		}
	}

	/**
	 * Returns the count sub-query for DataModel's has() and whereHas() methods.
	 *
	 * @param   string  $tableAlias  The alias of the local table in the query. Leave blank to use the table's name.
	 *
	 * @return JDatabaseQuery
	 */
	public function getCountSubquery($tableAlias = null)
	{
		/** @var DataModel $foreignModel */
		$foreignModel = $this->getForeignModel();
		$foreignModel->setIgnoreRequest(true);

		$db = $foreignModel->getDbo();

		if (empty($tableAlias))
		{
			$tableAlias = $this->parentModel->getTableName();
		}

		$query = $db->getQuery(true)
			->select('COUNT(*)')
			->from($db->qn($foreignModel->getTableName()) . ' AS ' . $db->qn('reltbl'))
			->innerJoin(
				$db->qn($this->pivotTable) . ' AS ' . $db->qn('pivotTable') . ' ON('
				. $db->qn('pivotTable') . '.' . $db->qn($this->pivotForeignKey) . ' = '
				. $db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey))
				. ')'
			)
			->where(
				$db->qn('pivotTable') . '.' . $db->qn($this->pivotLocalKey) . ' ='
				. $db->qn($tableAlias) . '.'
				. $db->qn($this->parentModel->getFieldAlias($this->localKey))
			);

		return $query;
	}

	/**
	 * Saves all related items. For many-to-many relations there are two things we have to do:
	 * 1. Save all related items; and
	 * 2. Overwrite the pivot table data with the new associations
	 */
	public function saveAll()
	{
		// Save all related items
		parent::saveAll();

		$this->saveRelations();
	}

	/**
	 * Overwrite the pivot table data with the new associations
	 */
	public function saveRelations()
	{
		// Get all the new keys
		$newKeys = [];

		if ($this->data instanceof DataModel\Collection)
		{
			foreach ($this->data as $item)
			{
				if ($item instanceof DataModel)
				{
					$newKeys[] = $item->getId();
				}
				elseif (!is_object($item))
				{
					$newKeys[] = $item;
				}
			}
		}

		$newKeys = array_unique($newKeys);

		$db            = $this->parentModel->getDbo();
		$localKeyValue = $this->parentModel->getFieldValue($this->localKey);

		// Kill all existing relations in the pivot table
		$query = $db->getQuery(true)
			->delete($db->qn($this->pivotTable))
			->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKeyValue));
		$db->setQuery($query);
		$db->execute();

		// Write the new relations to the database
		$protoQuery = $db->getQuery(true)
			->insert($db->qn($this->pivotTable))
			->columns([$db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)]);

		$i     = 0;
		$query = null;

		foreach ($newKeys as $key)
		{
			$i++;

			if (is_null($query))
			{
				$query = clone $protoQuery;
			}

			$query->values($db->q($localKeyValue) . ', ' . $db->q($key));

			if (($i % 50) == 0)
			{
				$db->setQuery($query);
				$db->execute();
				$query = null;
			}
		}

		if (!is_null($query))
		{
			$db->setQuery($query);
			$db->execute();
		}
	}

	/**
	 * This is not supported by the belongsTo relation
	 *
	 * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
	 */
	public function getNew()
	{
		throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported for many-to-may relations. Please add/remove items from the relation data and use push() to effect changes.");
	}

	/**
	 * Applies the relation filters to the foreign model when getData is called
	 *
	 * @param   DataModel             $foreignModel    The foreign model you're operating on
	 * @param   DataModel\Collection  $dataCollection  If it's an eager loaded relation, the collection of loaded
	 *                                                 parent records
	 *
	 * @return boolean Return false to force an empty data collection
	 */
	protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null)
	{
		$db = $this->parentModel->getDbo();

		// Decide how to proceed, based on eager or lazy loading
		if (is_object($dataCollection))
		{
			// Eager loaded relation
			if (!empty($dataCollection))
			{
				// Get a list of local keys from the collection
				$values = [];

				/** @var $item DataModel */
				foreach ($dataCollection as $item)
				{
					$v = $item->getFieldValue($this->localKey, null);

					if (!is_null($v))
					{
						$values[] = $v;
					}
				}

				// Keep only unique values
				$values = array_unique($values);
				$values = array_map(function ($x) use (&$db) {
					return $db->q($x);
				}, $values);

				// Get the foreign keys from the glue table
				$query = $db->getQuery(true)
					->select([$db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)])
					->from($db->qn($this->pivotTable))
					->where($db->qn($this->pivotLocalKey) . ' IN(' . implode(',', $values) . ')');
				$db->setQuery($query);
				$foreignKeysUnmapped = $db->loadRowList();

				$this->foreignKeyMap = [];
				$foreignKeys         = [];

				foreach ($foreignKeysUnmapped as $unmapped)
				{
					$local   = $unmapped[0];
					$foreign = $unmapped[1];

					if (!isset($this->foreignKeyMap[$local]))
					{
						$this->foreignKeyMap[$local] = [];
					}

					$this->foreignKeyMap[$local][] = $foreign;

					$foreignKeys[] = $foreign;
				}

				// Keep only unique values. However, the array keys are all screwed up. See below.
				$foreignKeys = array_unique($foreignKeys);

				// This looks stupid, but it's required to reset the array keys. Without it where() below fails.
				$foreignKeys = array_merge($foreignKeys);

				// Apply the filter
				if (!empty($foreignKeys))
				{
					$foreignModel->where($this->foreignKey, 'in', $foreignKeys);
				}
				else
				{
					return false;
				}
			}
			else
			{
				return false;
			}
		}
		else
		{
			// Lazy loaded relation; get the single local key
			$localKey = $this->parentModel->getFieldValue($this->localKey, null);

			if (is_null($localKey) || ($localKey === ''))
			{
				return false;
			}

			$query = $db->getQuery(true)
				->select($db->qn($this->pivotForeignKey))
				->from($db->qn($this->pivotTable))
				->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKey));
			$db->setQuery($query);
			$foreignKeys = $db->loadColumn();

			$this->foreignKeyMap[$localKey] = $foreignKeys;

			// If there are no foreign keys (no foreign items assigned to our item) we return false which then causes
			// the relation to return null, marking the lack of data.
			if (empty($foreignKeys))
			{
				return false;
			}

			$foreignModel->where($this->foreignKey, 'in', $this->foreignKeyMap[$localKey]);
		}

		return true;
	}
}
Model/DataModel/Collection.php000060400000015352152455514240012253 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel;

defined('_JEXEC') || die;

use FOF30\Model\DataModel;
use FOF30\Utils\Collection as BaseCollection;

/**
 * A collection of data models. You can enumerate it like an array, use it everywhere a collection is expected (e.g. a
 * foreach loop) and even implements a countable interface. You can also batch-apply DataModel methods on it thanks to
 * its magic __call() method, hence the type-hinting below.
 *
 * @method void setFieldValue(string $name, mixed $value = '')
 * @method void archive()
 * @method void save(mixed $data, string $orderingFilter = '', bool $ignore = null)
 * @method void push(mixed $data, string $orderingFilter = '', bool $ignore = null, array $relations = null)
 * @method void bind(mixed $data, array $ignore = [])
 * @method void check()
 * @method void reorder(string $where = '')
 * @method void delete(mixed $id = null)
 * @method void trash(mixed $id)
 * @method void forceDelete(mixed $id = null)
 * @method void lock(int $userId = null)
 * @method void move(int $delta, string $where = '')
 * @method void publish()
 * @method void restore(mixed $id)
 * @method void touch(int $userId = null)
 * @method void unlock()
 * @method void unpublish()
 */
class Collection extends BaseCollection
{
	/**
	 * Find a model in the collection by key.
	 *
	 * @param   mixed  $key
	 * @param   mixed  $default
	 *
	 * @return DataModel
	 */
	public function find($key, $default = null)
	{
		if ($key instanceof DataModel)
		{
			$key = $key->getId();
		}

		return array_first($this->items, function ($itemKey, $model) use ($key) {
			/** @var DataModel $model */
			return $model->getId() == $key;

		}, $default);
	}

	/**
	 * Remove an item in the collection by key
	 *
	 * @param   mixed  $key
	 *
	 * @return void
	 */
	public function removeById($key)
	{
		if ($key instanceof DataModel)
		{
			$key = $key->getId();
		}

		$index = array_search($key, $this->modelKeys());

		if ($index !== false)
		{
			unset($this->items[$index]);
		}
	}

	/**
	 * Add an item to the collection.
	 *
	 * @param   mixed  $item
	 *
	 * @return Collection
	 */
	public function add($item)
	{
		$this->items[] = $item;

		return $this;
	}

	/**
	 * Determine if a key exists in the collection.
	 *
	 * @param   mixed  $key
	 *
	 * @return bool
	 */
	public function contains($key)
	{
		return !is_null($this->find($key));
	}

	/**
	 * Fetch a nested element of the collection.
	 *
	 * @param   string  $key
	 *
	 * @return Collection
	 */
	public function fetch($key)
	{
		return new static(array_fetch($this->toArray(), $key));
	}

	/**
	 * Get the max value of a given key.
	 *
	 * @param   string  $key
	 *
	 * @return mixed
	 */
	public function max($key)
	{
		return $this->reduce(function ($result, $item) use ($key) {
			return (is_null($result) || $item->{$key} > $result) ? $item->{$key} : $result;
		});
	}

	/**
	 * Get the min value of a given key.
	 *
	 * @param   string  $key
	 *
	 * @return mixed
	 */
	public function min($key)
	{
		return $this->reduce(function ($result, $item) use ($key) {
			return (is_null($result) || $item->{$key} < $result) ? $item->{$key} : $result;
		});
	}

	/**
	 * Get the array of primary keys
	 *
	 * @return array
	 */
	public function modelKeys()
	{
		return array_map(
			function ($m) {
				/** @var DataModel $m */
				return $m->getId();
			},
			$this->items);
	}

	/**
	 * Merge the collection with the given items.
	 *
	 * @param   BaseCollection|array  $collection
	 *
	 * @return BaseCollection
	 */
	public function merge($collection)
	{
		$dictionary = $this->getDictionary($this);

		foreach ($collection as $item)
		{
			$dictionary[$item->getId()] = $item;
		}

		return new static(array_values($dictionary));
	}

	/**
	 * Diff the collection with the given items.
	 *
	 * @param   BaseCollection|array  $collection
	 *
	 * @return  BaseCollection
	 */
	public function diff($collection)
	{
		$diff = new static;

		$dictionary = $this->getDictionary($collection);

		foreach ($this->items as $item)
		{
			/** @var DataModel $item */
			if (!isset($dictionary[$item->getId()]))
			{
				$diff->add($item);
			}
		}

		return $diff;
	}

	/**
	 * Intersect the collection with the given items.
	 *
	 * @param   BaseCollection|array  $collection
	 *
	 * @return  Collection
	 */
	public function intersect($collection)
	{
		$intersect = new static;

		$dictionary = $this->getDictionary($collection);

		foreach ($this->items as $item)
		{
			/** @var DataModel $item */
			if (isset($dictionary[$item->getId()]))
			{
				$intersect->add($item);
			}
		}

		return $intersect;
	}

	/**
	 * Return only unique items from the collection.
	 *
	 * @return BaseCollection
	 */
	public function unique()
	{
		$dictionary = $this->getDictionary($this);

		return new static(array_values($dictionary));
	}

	/**
	 * Get a base Support collection instance from this collection.
	 *
	 * @return BaseCollection
	 */
	public function toBase()
	{
		return new BaseCollection($this->items);
	}

	/**
	 * Magic method which allows you to run a DataModel method to all items in the collection.
	 *
	 * For example, you can do $collection->save('foobar' => 1) to update the 'foobar' column to 1 across all items in
	 * the collection.
	 *
	 * IMPORTANT: The return value of the method call is not returned back to you!
	 *
	 * @param   string  $name       The method to call
	 * @param   array   $arguments  The arguments to the method
	 */
	public function __call($name, $arguments)
	{
		if (!count($this))
		{
			return;
		}

		$class = get_class($this->first());

		if (method_exists($class, $name))
		{
			foreach ($this as $item)
			{
				switch (count($arguments))
				{
					case 0:
						$item->$name();
						break;

					case 1:
						$item->$name($arguments[0]);
						break;

					case 2:
						$item->$name($arguments[0], $arguments[1]);
						break;

					case 3:
						$item->$name($arguments[0], $arguments[1], $arguments[2]);
						break;

					case 4:
						$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
						break;

					case 5:
						$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
						break;

					case 6:
						$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
						break;

					default:
						call_user_func_array([$item, $name], $arguments);
						break;
				}
			}
		}
	}

	/**
	 * Get a dictionary keyed by primary keys.
	 *
	 * @param   BaseCollection  $collection
	 *
	 * @return array
	 */
	protected function getDictionary($collection)
	{
		$dictionary = [];

		foreach ($collection as $value)
		{
			$dictionary[$value->getId()] = $value;
		}

		return $dictionary;
	}
}
Model/DataModel/Behaviour/RelationFilters.php000060400000004435152455514240015212 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;

class RelationFilters extends Observer
{
	/**
	 * This event runs after we have built the query used to fetch a record list in a model. It is used to apply
	 * automatic query filters based on model relations.
	 *
	 * @param   DataModel  &          $model  The model which calls this event
	 * @param   JDatabaseQuery      & $query  The query we are manipulating
	 *
	 * @return  void
	 */
	public function onAfterBuildQuery(&$model, &$query)
	{
		$relationFilters = $model->getRelationFilters();

		foreach ($relationFilters as $filterState)
		{
			$relationName = $filterState['relation'];

			$tableAlias = $model->getBehaviorParam('tableAlias', null);
			$subQuery   = $model->getRelations()->getCountSubquery($relationName, $tableAlias);

			// Callback method needs different handling
			if (isset($filterState['method']) && ($filterState['method'] == 'callback'))
			{
				call_user_func_array($filterState['value'], [&$subQuery]);
				$filterState['method']   = 'search';
				$filterState['operator'] = '>=';
				$filterState['value']    = '1';
			}

			$options = class_exists('JRegistry') ? new Registry($filterState) : new Registry($filterState);

			$filter  = new DataModel\Filter\Relation($model->getDbo(), $relationName, $subQuery);
			$methods = $filter->getSearchMethods();
			$method  = $options->get('method', $filter->getDefaultSearchMethod());

			if (!in_array($method, $methods))
			{
				$method = 'exact';
			}

			switch ($method)
			{
				case 'between':
				case 'outside':
					$sql = $filter->$method($options->get('from', null), $options->get('to'));
					break;

				case 'interval':
					$sql = $filter->$method($options->get('value', null), $options->get('interval'));
					break;

				case 'search':
					$sql = $filter->$method($options->get('value', null), $options->get('operator', '='));
					break;

				default:
					$sql = $filter->$method($options->get('value', null));
					break;
			}

			if ($sql)
			{
				$query->where($sql);
			}
		}
	}
}
Model/DataModel/Behaviour/Enabled.php000060400000003277152455514240013441 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;

/**
 * FOF model behavior class to filter access to items based on the enabled status
 *
 * @since    2.1
 */
class Enabled extends Observer
{
	/**
	 * This event runs before we have built the query used to fetch a record
	 * list in a model. It is used to apply automatic query filters.
	 *
	 * @param   DataModel      &$model  The model which calls this event
	 * @param   JDatabaseQuery &$query  The query we are manipulating
	 *
	 * @return  void
	 */
	public function onBeforeBuildQuery(&$model, &$query)
	{
		// Make sure the field actually exists
		if (!$model->hasField('enabled'))
		{
			return;
		}

		$fieldName = $model->getFieldAlias('enabled');
		$db        = $model->getDbo();

		$model->whereRaw($db->qn($fieldName) . ' = ' . $db->q(1));
	}

	/**
	 * The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
	 * filters.
	 *
	 * @param   DataModel &$model  The model which was called
	 * @param   Array     &$keys   The keys used to locate the record which was loaded
	 *
	 * @return  void
	 */
	public function onAfterLoad(&$model, &$keys)
	{
		// Make sure we have a DataModel
		if (!($model instanceof DataModel))
		{
			return;
		}

		// Make sure the field actually exists
		if (!$model->hasField('enabled'))
		{
			return;
		}

		// Filter by enabled status
		if (!$model->getFieldValue('enabled', 0))
		{
			$model->reset(true);
		}
	}
}
Model/DataModel/Behaviour/EmptyNonZero.php000060400000001605152455514240014511 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;

/**
 * FOF model behavior class to let the Filters behaviour know that zero value is a valid filter value
 *
 * @since    2.1
 */
class EmptyNonZero extends Observer
{
	/**
	 * This event runs after we have built the query used to fetch a record
	 * list in a model. It is used to apply automatic query filters.
	 *
	 * @param   DataModel      &$model  The model which calls this event
	 * @param   JDatabaseQuery &$query  The query we are manipulating
	 *
	 * @return  void
	 */
	public function onAfterBuildQuery(&$model, &$query)
	{
		$model->setBehaviorParam('filterZero', 1);
	}
}
Model/DataModel/Behaviour/Language.php000060400000011114152455514240013617 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
use PlgSystemLanguageFilter;

/**
 * FOF model behavior class to filter front-end access to items
 * based on the language.
 *
 * @since    2.1
 */
class Language extends Observer
{
	/** @var  PlgSystemLanguageFilter */
	protected $lang_filter_plugin;

	/**
	 * This event runs before we have built the query used to fetch a record
	 * list in a model. It is used to blacklist the language filter
	 *
	 * @param   DataModel       &$model  The model which calls this event
	 * @param   JDatabaseQuery  &$query  The model which calls this event
	 *
	 * @return  void
	 */
	public function onBeforeBuildQuery(&$model, &$query)
	{
		if ($model->getContainer()->platform->isFrontend())
		{
			$model->blacklistFilters('language');
		}

		// Make sure the field actually exists AND we're not in CLI
		if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
		{
			return;
		}

		/** @var SiteApplication $app */
		$app               = Factory::getApplication();
		$hasLanguageFilter = method_exists($app, 'getLanguageFilter');

		if ($hasLanguageFilter)
		{
			$hasLanguageFilter = $app->getLanguageFilter();
		}

		if (!$hasLanguageFilter)
		{
			return;
		}

		// Ask Joomla for the plugin only if we don't already have it. Useful for tests
		if (!$this->lang_filter_plugin)
		{
			$this->lang_filter_plugin = PluginHelper::getPlugin('system', 'languagefilter');
		}

		$lang_filter_params = class_exists('JRegistry') ? new Registry($this->lang_filter_plugin->params) : new Registry($this->lang_filter_plugin->params);

		$languages = ['*'];

		if ($lang_filter_params->get('remove_default_prefix'))
		{
			// Get default site language
			$platform    = $model->getContainer()->platform;
			$lg          = $platform->getLanguage();
			$languages[] = $lg->getTag();
		}
		else
		{
			// We have to use JInput since the language fragment is not set in the $_REQUEST, thus we won't have it in our model
			// TODO Double check the previous assumption
			$languages[] = Factory::getApplication()->input->getCmd('language', '*');
		}

		// Filter out double languages
		$languages = array_unique($languages);

		// And filter the query output by these languages
		$db        = $model->getDbo();
		$languages = array_map([$db, 'quote'], $languages);
		$fieldName = $model->getFieldAlias('language');

		$model->whereRaw($db->qn($fieldName) . ' IN(' . implode(', ', $languages) . ')');
	}

	/**
	 * The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
	 * filters.
	 *
	 * @param   DataModel &$model  The model which was called
	 * @param   Array     &$keys   The keys used to locate the record which was loaded
	 *
	 * @return  void
	 */
	public function onAfterLoad(&$model, &$keys)
	{
		// Make sure we have a DataModel
		if (!($model instanceof DataModel))
		{
			return;
		}

		// Make sure the field actually exists AND we're not in CLI
		if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
		{
			return;
		}

		// Make sure it is a multilingual site and get a list of languages
		/** @var SiteApplication $app */
		$app               = Factory::getApplication();
		$hasLanguageFilter = method_exists($app, 'getLanguageFilter');

		if ($hasLanguageFilter)
		{
			$hasLanguageFilter = $app->getLanguageFilter();
		}

		if (!$hasLanguageFilter)
		{
			return;
		}

		// Ask Joomla for the plugin only if we don't already have it. Useful for tests
		if (!$this->lang_filter_plugin)
		{
			$this->lang_filter_plugin = PluginHelper::getPlugin('system', 'languagefilter');
		}

		$lang_filter_params = class_exists('JRegistry') ? new Registry($this->lang_filter_plugin->params) : new Registry($this->lang_filter_plugin->params);

		$languages = ['*'];

		if ($lang_filter_params->get('remove_default_prefix'))
		{
			// Get default site language
			$lg          = $model->getContainer()->platform->getLanguage();
			$languages[] = $lg->getTag();
		}
		else
		{
			$languages[] = Factory::getApplication()->input->getCmd('language', '*');
		}

		// Filter out double languages
		$languages = array_unique($languages);

		// Filter by language
		if (!in_array($model->getFieldValue('language'), $languages))
		{
			$model->reset();
		}
	}
}
Model/DataModel/Behaviour/Created.php000060400000004041152455514240013444 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use stdClass;

/**
 * FOF model behavior class to updated the created_by and created_on fields on newly created records.
 *
 * This behaviour is added to DataModel by default. If you want to remove it you need to do
 * $this->behavioursDispatcher->removeBehaviour('Created');
 *
 * @since  3.0
 */
class Created extends Observer
{
	/**
	 * Add the created_on and created_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
	 * so that we can fill them in through this behaviour.
	 *
	 * @param   DataModel  $model
	 */
	public function onBeforeCheck(&$model)
	{
		$model->addSkipCheckField('created_on');
		$model->addSkipCheckField('created_by');
	}

	/**
	 * @param   DataModel  $model
	 * @param   stdClass  $dataObject
	 */
	public function onBeforeCreate(&$model, &$dataObject)
	{
		// Handle the created_on field
		if ($model->hasField('created_on'))
		{
			$nullDate   = $model->isNullableField('created_on') ? null : $model->getDbo()->getNullDate();
			$created_on = $model->getFieldValue('created_on');

			if (empty($created_on) || ($created_on == $nullDate))
			{
				$model->setFieldValue('created_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));

				$createdOnField              = $model->getFieldAlias('created_on');
				$dataObject->$createdOnField = $model->getFieldValue('created_on');
			}
		}

		// Handle the created_by field
		if ($model->hasField('created_by'))
		{
			$created_by = $model->getFieldValue('created_by');

			if (empty($created_by))
			{
				$model->setFieldValue('created_by', $model->getContainer()->platform->getUser()->id);

				$createdByField              = $model->getFieldAlias('created_by');
				$dataObject->$createdByField = $model->getFieldValue('created_by');
			}
		}
	}
}
Model/DataModel/Behaviour/Filters.php000060400000006761152455514240013520 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;

class Filters extends Observer
{
	/**
	 * This event runs after we have built the query used to fetch a record
	 * list in a model. It is used to apply automatic query filters.
	 *
	 * @param   DataModel  &         $model  The model which calls this event
	 * @param   JDatabaseQuery      &$query  The query we are manipulating
	 *
	 * @return  void
	 */
	public function onAfterBuildQuery(&$model, &$query)
	{
		$tableKey = $model->getIdFieldName();
		$db       = $model->getDbo();

		$fields     = $model->getTableFields();
		$blacklist  = $model->getBlacklistFilters();
		$filterZero = $model->getBehaviorParam('filterZero', null);
		$tableAlias = $model->getBehaviorParam('tableAlias', null);

		foreach ($fields as $fieldname => $fieldmeta)
		{
			if (in_array($fieldname, $blacklist))
			{
				continue;
			}

			$fieldInfo = (object) [
				'name'       => $fieldname,
				'type'       => $fieldmeta->Type,
				'filterZero' => $filterZero,
				'tableAlias' => $tableAlias,
			];

			$filterName  = $fieldInfo->name;
			$filterState = $model->getState($filterName, null);

			// Special primary key handling: if ignore request is set we'll also look for an 'id' state variable if a
			// state variable by the same name as the key doesn't exist. If ignore request is not set in the model we
			// do not filter by 'id' since this interferes with going from an edit page to a browse page (the list is
			// filtered by id without user controls to unset it).
			if ($fieldInfo->name == $tableKey)
			{
				$filterState = $model->getState($filterName, null);

				if (!$model->getIgnoreRequest())
				{
					continue;
				}

				if (empty($filterState))
				{
					$filterState = $model->getState('id', null);
				}
			}

			$field = DataModel\Filter\AbstractFilter::getField($fieldInfo, ['dbo' => $db]);

			if (!is_object($field) || !($field instanceof DataModel\Filter\AbstractFilter))
			{
				continue;
			}

			if ((is_array($filterState) && (
						array_key_exists('value', $filterState) ||
						array_key_exists('from', $filterState) ||
						array_key_exists('to', $filterState)
					)) || is_object($filterState))
			{
				$options = class_exists('JRegistry') ? new Registry($filterState) : new Registry($filterState);
			}
			else
			{
				$options = class_exists('JRegistry') ? new Registry() : new Registry();
				$options->set('value', $filterState);
			}

			$methods = $field->getSearchMethods();
			$method  = $options->get('method', $field->getDefaultSearchMethod());

			if (!in_array($method, $methods))
			{
				$method = 'exact';
			}

			switch ($method)
			{
				case 'between':
				case 'outside':
				case 'range' :
					$sql = $field->$method($options->get('from', null), $options->get('to', null), $options->get('include', false));
					break;

				case 'interval':
				case 'modulo':
					$sql = $field->$method($options->get('value', null), $options->get('interval'));
					break;

				case 'search':
					$sql = $field->$method($options->get('value', null), $options->get('operator', '='));
					break;

				case 'exact':
				case 'partial':
				default:
					$sql = $field->$method($options->get('value', null));
					break;
			}

			if ($sql)
			{
				$query->where($sql);
			}
		}
	}
}
Model/DataModel/Behaviour/Own.php000060400000003747152455514240012654 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;

/**
 * FOF model behavior class to filter access to items owned by the currently logged in user only
 *
 * @since    2.1
 */
class Own extends Observer
{
	/**
	 * This event runs after we have built the query used to fetch a record
	 * list in a model. It is used to apply automatic query filters.
	 *
	 * @param   DataModel      &$model  The model which calls this event
	 * @param   JDatabaseQuery &$query  The query we are manipulating
	 *
	 * @return  void
	 */
	public function onAfterBuildQuery(&$model, &$query)
	{
		// Make sure the field actually exists
		if (!$model->hasField('created_by'))
		{
			return;
		}

		// Get the current user's id
		$user_id = $model->getContainer()->platform->getUser()->id;

		// And filter the query output by the user id
		$db = $model->getContainer()->platform->getDbo();

		$query->where($db->qn($model->getFieldAlias('created_by')) . ' = ' . $db->q($user_id));
	}

	/**
	 * The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
	 * filters.
	 *
	 * @param   DataModel &$model  The model which was called
	 * @param   Array     &$keys   The keys used to locate the record which was loaded
	 *
	 * @return  void
	 */
	public function onAfterLoad(&$model, &$keys)
	{
		// Make sure we have a DataModel
		if (!($model instanceof DataModel))
		{
			return;
		}

		// Make sure the field actually exists
		if (!$model->hasField('created_by'))
		{
			return;
		}

		// Get the user
		$user_id    = $model->getContainer()->platform->getUser()->id;
		$recordUser = $model->getFieldValue('created_by', null);

		// Filter by authorised access levels
		if ($recordUser != $user_id)
		{
			$model->reset(true);
		}
	}
}
Model/DataModel/Behaviour/Modified.php000060400000003623152455514240013622 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use stdClass;

/**
 * FOF model behavior class to updated the modified_by and modified_on fields on newly created records.
 *
 * This behaviour is added to DataModel by default. If you want to remove it you need to do
 * $this->behavioursDispatcher->removeBehaviour('Modified');
 *
 * @since  3.0
 */
class Modified extends Observer
{
	/**
	 * Add the modified_on and modified_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
	 * so that we can fill them in through this behaviour.
	 *
	 * @param   DataModel  $model
	 */
	public function onBeforeCheck(&$model)
	{
		$model->addSkipCheckField('modified_on');
		$model->addSkipCheckField('modified_by');
	}

	/**
	 * @param   DataModel  $model
	 * @param   stdClass  $dataObject
	 */
	public function onBeforeUpdate(&$model, &$dataObject)
	{
		// Make sure we're not modifying a locked record
		$userId   = $model->getContainer()->platform->getUser()->id;
		$isLocked = $model->isLocked($userId);

		if ($isLocked)
		{
			return;
		}

		// Handle the modified_on field
		if ($model->hasField('modified_on'))
		{
			$model->setFieldValue('modified_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));

			$modifiedOnField              = $model->getFieldAlias('modified_on');
			$dataObject->$modifiedOnField = $model->getFieldValue('modified_on');
		}

		// Handle the modified_by field
		if ($model->hasField('modified_by'))
		{
			$model->setFieldValue('modified_by', $userId);

			$modifiedByField              = $model->getFieldAlias('modified_by');
			$dataObject->$modifiedByField = $model->getFieldValue('modified_by');
		}
	}
}
Model/DataModel/Behaviour/Assets.php000060400000011227152455514240013343 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use Exception;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use Joomla\CMS\Access\Rules;
use Joomla\CMS\Table\Table;

/**
 * FOF model behavior class to add Joomla! ACL assets support
 *
 * @since    2.1
 */
class Assets extends Observer
{
	public function onAfterSave(DataModel &$model)
	{
		if (!$model->hasField('asset_id') || !$model->isAssetsTracked())
		{
			return true;
		}

		$assetFieldAlias = $model->getFieldAlias('asset_id');
		$currentAssetId  = $model->getFieldValue('asset_id');

		unset($model->$assetFieldAlias);

		// Create the object used for inserting/updating data to the database
		$fields = $model->getTableFields();

		// Let's remove the asset_id field, since we unset the property above and we would get a PHP notice
		if (isset($fields[$assetFieldAlias]))
		{
			unset($fields[$assetFieldAlias]);
		}

		// Asset Tracking
		$parentId = $model->getAssetParentId();
		$name     = $model->getAssetName();
		$title    = $model->getAssetTitle();

		$asset = Table::getInstance('Asset');
		$asset->loadByName($name);

		// Re-inject the asset id.
		$this->$assetFieldAlias = $asset->id;

		// Check for an error.
		$error = $asset->getError();

		// Since we are using JTable, there is no way to mock it and test for failures :(
		// @codeCoverageIgnoreStart
		if ($error)
		{
			throw new Exception($error);
		}
		// @codeCoverageIgnoreEnd

		// Specify how a new or moved node asset is inserted into the tree.
		// Since we're unsetting the table field before, this statement is always true...
		if (empty($model->$assetFieldAlias) || $asset->parent_id != $parentId)
		{
			$asset->setLocation($parentId, 'last-child');
		}

		// Prepare the asset to be stored.
		$asset->parent_id = $parentId;
		$asset->name      = $name;
		$asset->title     = $title;

		if ($model->getRules() instanceof Rules)
		{
			$asset->rules = (string) $model->getRules();
		}

		// Since we are using JTable, there is no way to mock it and test for failures :(
		// @codeCoverageIgnoreStart
		if (!$asset->check() || !$asset->store())
		{
			throw new Exception($asset->getError());
		}
		// @codeCoverageIgnoreEnd

		// Create an asset_id or heal one that is corrupted.
		if (empty($model->$assetFieldAlias) || (($currentAssetId != $model->$assetFieldAlias) && !empty($model->$assetFieldAlias)))
		{
			// Update the asset_id field in this table.
			$model->$assetFieldAlias = (int) $asset->id;

			$k = $model->getKeyName();

			$db = $model->getDbo();

			$query = $db->getQuery(true)
				->update($db->qn($model->getTableName()))
				->set($db->qn($assetFieldAlias) . ' = ' . (int) $model->$assetFieldAlias)
				->where($db->qn($k) . ' = ' . (int) $model->$k);

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

		return true;
	}

	public function onAfterBind(DataModel &$model, &$src)
	{
		if (!$model->isAssetsTracked())
		{
			return true;
		}

		$rawRules = [];

		if (is_array($src) && array_key_exists('rules', $src) && is_array($src['rules']))
		{
			$rawRules = $src['rules'];
		}
		elseif (is_object($src) && isset($src->rules) && is_array($src->rules))
		{
			$rawRules = $src->rules;
		}

		if (empty($rawRules))
		{
			return true;
		}

		// Bind the rules.
		if (isset($rawRules) && is_array($rawRules))
		{
			// We have to manually remove any empty value, since they will be converted to int,
			// and "Inherited" values will become "Denied". Joomla is doing this manually, too.
			$rules = [];

			foreach ($rawRules as $action => $ids)
			{
				// Build the rules array.
				$rules[$action] = [];

				foreach ($ids as $id => $p)
				{
					if ($p !== '')
					{
						$rules[$action][$id] = ($p == '1' || $p == 'true') ? true : false;
					}
				}
			}

			$model->setRules($rules);
		}

		return true;
	}

	public function onBeforeDelete(DataModel &$model, $oid)
	{
		if (!$model->isAssetsTracked())
		{
			return true;
		}

		$k = $model->getKeyName();

		// If the table is not loaded, let's try to load it with the id
		if (!$model->$k)
		{
			$model->load($oid);
		}

		// If I have an invalid assetName I have to stop
		$name = $model->getAssetName();

		// Do NOT touch JTable here -- we are loading the core asset table which is a JTable, not a FOF Table
		$asset = Table::getInstance('Asset');

		if ($asset->loadByName($name))
		{
			// Since we are using JTable, there is no way to mock it and test for failures :(
			// @codeCoverageIgnoreStart
			if (!$asset->delete())
			{
				throw new Exception($asset->getError());
			}
			// @codeCoverageIgnoreEnd
		}

		return true;
	}
}
Model/DataModel/Behaviour/PageParametersToState.php000060400000003101152455514240016275 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;

/**
 * FOF model behavior class to populate the state with the front-end page parameters
 *
 * @since    2.1
 */
class PageParametersToState extends Observer
{
	public function onAfterConstruct(DataModel &$model)
	{
		// This only applies to the front-end
		if (!$model->getContainer()->platform->isFrontend())
		{
			return;
		}

		// Get the page parameters
		/** @var SiteApplication $app */
		$app = Factory::getApplication();
		/** @var Registry|Registry $params */
		$params = $app->getParams();

		// Extract the page parameter keys
		$asArray = $params->toArray();

		if (empty($asArray))
		{
			// There are no keys; no point in going on.
			return;
		}

		$keys = array_keys($asArray);
		unset($asArray);

		// Loop all page parameter keys
		foreach ($keys as $key)
		{
			// This is the current model state
			$currentState = $model->getState($key);
			// This is the explicitly requested state in the input
			$explicitInput = $model->input->get($key, null, 'raw');

			// If the current state is empty and there's no explicit input we'll use the page parameters instead
			if (is_null($currentState) && is_null($explicitInput))
			{
				$model->setState($key, $params->get($key));
			}
		}
	}
}
Model/DataModel/Behaviour/Tags.php000060400000007744152455514240013010 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use Exception;
use FOF30\Event\Observable;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use Joomla\CMS\Helper\TagsHelper;
use stdClass;

/**
 * FOF model behavior class to add Joomla! Tags support
 *
 * @since    2.1
 */
class Tags extends Observer
{
	/** @var TagsHelper */
	protected $tagsHelper;

	public function __construct(Observable &$subject)
	{
		parent::__construct($subject);

		$this->tagsHelper = new TagsHelper();
	}

	/**
	 * This event runs after unpublishing a record in a model
	 *
	 * @param   DataModel  &$model       The model which calls this event
	 * @param   stdClass  & $dataObject  The data to bind to the form
	 *
	 * @return  void
	 */
	public function onBeforeCreate(&$model, &$dataObject)
	{
		$tagField = $model->getBehaviorParam('tagFieldName', 'tags');

		unset($dataObject->$tagField);
	}

	/**
	 * This event runs after unpublishing a record in a model
	 *
	 * @param   DataModel  &$model       The model which calls this event
	 * @param   stdClass  & $dataObject  The data to bind to the form
	 *
	 * @return  void
	 */
	public function onBeforeUpdate(&$model, &$dataObject)
	{
		$tagField = $model->getBehaviorParam('tagFieldName', 'tags');

		unset($dataObject->$tagField);
	}

	/**
	 * The event which runs after binding data to the table
	 *
	 * @param   DataModel    &$model  The model which calls this event
	 *
	 * @return  void
	 *
	 * @throws  Exception  Error message if failed to store tags
	 */
	public function onAfterSave(&$model)
	{
		$tagField = $model->getBehaviorParam('tagFieldName', 'tags');

		// Avoid to update on other method (e.g. publish, ...)
		if (!in_array($model->getContainer()->input->getCmd('task'), ['apply', 'save', 'savenew']))
		{
			return;
		}

		$oldTags = $this->tagsHelper->getTagIds($model->getId(), $model->getContentType());
		$newTags = $model->$tagField ? implode(',', $model->$tagField) : null;

		// If no changes, we stop here
		if ($oldTags == $newTags)
		{
			return;
		}

		// Check if the content type exists, and create it if it does not
		$model->checkContentType();

		$this->tagsHelper->typeAlias = $model->getContentType();

		if (!$this->tagsHelper->postStoreProcess($model, $model->$tagField))
		{
			throw new Exception('Error storing tags');
		}
	}

	/**
	 * The event which runs after deleting a record
	 *
	 * @param   DataModel &$model  The model which calls this event
	 * @param   integer    $oid    The PK value of the record which was deleted
	 *
	 * @return  void
	 *
	 * @throws  Exception  Error message if failed to delete tags
	 */
	public function onAfterDelete(&$model, $oid)
	{
		$this->tagsHelper->typeAlias = $model->getContentType();

		if (!$this->tagsHelper->deleteTagData($model, $oid))
		{
			throw new Exception('Error deleting Tags');
		}
	}

	/**
	 * This event runs after unpublishing a record in a model
	 *
	 * @param   DataModel  &$model  The model which calls this event
	 * @param   mixed       $data   An associative array or object to bind to the DataModel instance.
	 *
	 * @return  void
	 */
	public function onAfterBind(&$model, &$data)
	{
		$tagField = $model->getBehaviorParam('tagFieldName', 'tags');

		if ($model->$tagField)
		{
			return;
		}

		$type = $model->getContentType();

		$model->addKnownField($tagField);
		$model->$tagField = $this->tagsHelper->getTagIds($model->getId(), $type);
	}

	/**
	 * This event runs after publishing a record in a model
	 *
	 * @param   DataModel  &$model  The model which calls this event
	 *
	 * @return  void
	 */
	public function onAfterPublish(&$model)
	{
		$model->updateUcmContent();
	}

	/**
	 * This event runs after unpublishing a record in a model
	 *
	 * @param   DataModel  &$model  The model which calls this event
	 *
	 * @return  void
	 */
	public function onAfterUnpublish(&$model)
	{
		$model->updateUcmContent();
	}
}
Model/DataModel/Behaviour/ContentHistory.php000060400000004361152455514240015076 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Helper\ContentHistoryHelper;

/**
 * FOF model behavior class to add Joomla! content history support
 *
 * @since    2.1
 */
class ContentHistory extends Observer
{
	/** @var  ContentHistoryHelper */
	protected $historyHelper;

	/**
	 * The event which runs after storing (saving) data to the database
	 *
	 * @param   DataModel  &$model  The model which calls this event
	 *
	 * @return  boolean  True to allow saving without an error
	 */
	public function onAfterSave(&$model)
	{
		$aliasParts = explode('.', $model->getContentType());
		$model->checkContentType();

		if (ComponentHelper::getParams($aliasParts[0])->get('save_history', 0))
		{
			if (!$this->historyHelper)
			{
				$this->historyHelper = new ContentHistoryHelper($model->getContentType());
			}

			$this->historyHelper->store($model);
		}

		return true;
	}

	/**
	 * The event which runs before deleting a record
	 *
	 * @param   DataModel &$model  The model which calls this event
	 * @param   integer    $oid    The PK value of the record to delete
	 *
	 * @return  boolean  True to allow the deletion
	 */
	public function onBeforeDelete(&$model, $oid)
	{
		$aliasParts = explode('.', $model->getContentType());

		if (ComponentHelper::getParams($aliasParts[0])->get('save_history', 0))
		{
			if (!$this->historyHelper)
			{
				$this->historyHelper = new ContentHistoryHelper($model->getContentType());
			}

			$this->historyHelper->deleteHistory($model);
		}

		return true;
	}

	/**
	 * This event runs after publishing a record in a model
	 *
	 * @param   DataModel  &$model  The model which calls this event
	 *
	 * @return  void
	 */
	public function onAfterPublish(&$model)
	{
		$model->updateUcmContent();
	}

	/**
	 * This event runs after unpublishing a record in a model
	 *
	 * @param   DataModel  &$model  The model which calls this event
	 *
	 * @return  void
	 */
	public function onAfterUnpublish(&$model)
	{
		$model->updateUcmContent();
	}
}
Model/DataModel/Behaviour/Access.php000060400000003422152455514240013300 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Behaviour;

defined('_JEXEC') || die;

use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;

/**
 * FOF model behavior class to filter access to items based on the viewing access levels.
 *
 * @since    2.1
 */
class Access extends Observer
{
	/**
	 * This event runs after we have built the query used to fetch a record
	 * list in a model. It is used to apply automatic query filters.
	 *
	 * @param   DataModel      &$model  The model which calls this event
	 * @param   JDatabaseQuery &$query  The query we are manipulating
	 *
	 * @return  void
	 */
	public function onAfterBuildQuery(&$model, &$query)
	{
		// Make sure the field actually exists
		if (!$model->hasField('access'))
		{
			return;
		}

		$model->applyAccessFiltering(null);
	}

	/**
	 * The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
	 * filters.
	 *
	 * @param   DataModel &$model  The model which was called
	 * @param   Array     &$keys   The keys used to locate the record which was loaded
	 *
	 * @return  void
	 */
	public function onAfterLoad(&$model, &$keys)
	{
		// Make sure we have a DataModel
		if (!($model instanceof DataModel))
		{
			return;
		}

		// Make sure the field actually exists
		if (!$model->hasField('access'))
		{
			return;
		}

		// Get the user
		$user              = $model->getContainer()->platform->getUser();
		$recordAccessLevel = $model->getFieldValue('access', null);

		// Filter by authorised access levels
		if (!in_array($recordAccessLevel, $user->getAuthorisedViewLevels()))
		{
			$model->reset(true);
		}
	}
}
Model/DataModel/RelationManager.php000060400000033165152455514240013232 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel;

defined('_JEXEC') || die;

use DirectoryIterator;
use FOF30\Model\DataModel;
use InvalidArgumentException;
use JDatabaseQuery;

class RelationManager
{
	/** @var array The known relation types */
	protected static $relationTypes = [];
	/** @var DataModel The data model we are attached to */
	protected $parentModel = null;
	/** @var Relation[] The relations known to us */
	protected $relations = [];
	/** @var array A list of the names of eager loaded relations */
	protected $eager = [];

	/**
	 * Creates a new relation manager for the defined parent model
	 *
	 * @param   DataModel  $parentModel  The model we are attached to
	 */
	public function __construct(DataModel $parentModel)
	{
		// Set the parent model
		$this->parentModel = $parentModel;

		// Make sure the relation types are initialised
		static::getRelationTypes();

		// @todo Maybe set up a few relations automatically?
	}

	/**
	 * Populates the static map of relation type methods and relation handling classes
	 *
	 * @return array Key = method name, Value = relation handling class
	 */
	public static function getRelationTypes()
	{
		if (empty(static::$relationTypes))
		{
			$relationTypeDirectory = __DIR__ . '/Relation';
			$fs                    = new DirectoryIterator($relationTypeDirectory);

			/** @var $file DirectoryIterator */
			foreach ($fs as $file)
			{
				if ($file->isDir())
				{
					continue;
				}

				if ($file->getExtension() != 'php')
				{
					continue;
				}

				$baseName   = ucfirst($file->getBasename('.php'));
				$methodName = strtolower($baseName[0]) . substr($baseName, 1);
				$className  = '\\FOF30\\Model\\DataModel\\Relation\\' . $baseName;

				if (!class_exists($className, true))
				{
					continue;
				}

				static::$relationTypes[$methodName] = $className;
			}
		}

		return static::$relationTypes;
	}

	/**
	 * Implements deep cloning of the relation object
	 */
	function __clone()
	{
		$relations = [];

		if (!empty($this->relations))
		{
			/** @var Relation[] $relations */
			foreach ($this->relations as $key => $relation)
			{
				$relations[$key] = clone($relation);
				$relations[$key]->reset();
			}
		}

		$this->relations = $relations;
	}

	/**
	 * Rebase a relation manager
	 *
	 * @param   DataModel  $parentModel
	 */
	public function rebase(DataModel $parentModel)
	{
		$this->parentModel = $parentModel;

		if (count($this->relations))
		{
			foreach ($this->relations as $name => $relation)
			{
				/** @var Relation $relation */
				$relation->rebase($parentModel);
			}
		}
	}

	/**
	 * Populates the internal $this->data collection of a relation from the contents of the provided collection. This is
	 * used by DataModel to push the eager loaded data into each item's relation.
	 *
	 * @param   string      $name    Relation name
	 * @param   Collection  $data    The relation data to push into this relation
	 * @param   mixed       $keyMap  Used by many-to-many relations to pass around the local to foreign key map
	 *
	 * @return void
	 *
	 * @throws Relation\Exception\RelationNotFound
	 */
	public function setDataFromCollection($name, Collection &$data, $keyMap = null)
	{
		if (!isset($this->relations[$name]))
		{
			throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
		}

		$this->relations[$name]->setDataFromCollection($data, $keyMap);
	}

	/**
	 * Adds a relation to the relation manager
	 *
	 * @param   string  $name              The name of the relation as known to this relation manager, e.g. 'phone'
	 * @param   string  $type              The relation type, e.g. 'hasOne'
	 * @param   string  $foreignModelName  The name of the foreign key's model in the format "modelName@com_something"
	 * @param   string  $localKey          The local table key for this relation
	 * @param   string  $foreignKey        The foreign key for this relation
	 * @param   string  $pivotTable        For many-to-many relations, the pivot (glue) table
	 * @param   string  $pivotLocalKey     For many-to-many relations, the pivot table's column storing the local key
	 * @param   string  $pivotForeignKey   For many-to-many relations, the pivot table's column storing the foreign key
	 *
	 * @return DataModel The parent model, for chaining
	 *
	 * @throws Relation\Exception\RelationTypeNotFound when $type is not known
	 * @throws Relation\Exception\ForeignModelNotFound when $foreignModelClass doesn't exist
	 */
	public function addRelation($name, $type, $foreignModelName = null, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
	{
		if (!isset(static::$relationTypes[$type]))
		{
			throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$type' not found");
		}

		// Guess the foreign model class if necessary
		if (empty($foreignModelName))
		{
			$foreignModelName = ucfirst($name);
		}

		$className = static::$relationTypes[$type];

		/** @var Relation $relation */
		$relation = new $className($this->parentModel, $foreignModelName, $localKey, $foreignKey,
			$pivotTable, $pivotLocalKey, $pivotForeignKey);

		$this->relations[$name] = $relation;

		return $this->parentModel;
	}

	/**
	 * Removes a known relation
	 *
	 * @param   string  $name  The name of the relation to remove
	 *
	 * @return DataModel The parent model, for chaining
	 */
	public function removeRelation($name)
	{
		if (isset($this->relations[$name]))
		{
			unset ($this->relations[$name]);
		}

		return $this->parentModel;
	}

	/**
	 * Removes all known relations
	 */
	public function resetRelations()
	{
		$this->relations = [];
	}

	/**
	 * Resets the data of all relations in this manager. This doesn't remove relations, just their data so that they
	 * get loaded again.
	 *
	 * @param   array  $relationsToReset  The names of the relations to reset. Pass an empty array (default) to reset
	 *                                    all relations.
	 */
	public function resetRelationData(array $relationsToReset = [])
	{
		/** @var Relation $relation */
		foreach ($this->relations as $name => $relation)
		{
			if (!empty($relationsToReset) && !in_array($name, $relationsToReset))
			{
				continue;
			}

			$relation->reset();
		}
	}

	/**
	 * Returns a list of all known relations' names
	 *
	 * @return array
	 */
	public function getRelationNames()
	{
		return array_keys($this->relations);
	}

	/**
	 * Gets the related items of a relation
	 *
	 * @param   string  $name  The name of the relation to return data for
	 *
	 * @return Relation
	 *
	 * @throws Relation\Exception\RelationNotFound
	 */
	public function &getRelation($name)
	{
		if (!isset($this->relations[$name]))
		{
			throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
		}

		return $this->relations[$name];
	}


	/**
	 * Get a new related item which satisfies relation $name and adds it to this relation's data list.
	 *
	 * @param   string  $name  The relation based on which a new item is returned
	 *
	 * @return DataModel
	 *
	 * @throws Relation\Exception\RelationNotFound
	 */
	public function getNew($name)
	{
		if (!isset($this->relations[$name]))
		{
			throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
		}

		return $this->relations[$name]->getNew();
	}

	/**
	 * Saves all related items belonging to the specified relation or, if $name is null, all known relations which
	 * support saving.
	 *
	 * @param   null|string  $name  The relation to save, or null to save all known relations
	 *
	 * @return DataModel The parent model, for chaining
	 *
	 * @throws Relation\Exception\RelationNotFound
	 */
	public function save($name = null)
	{
		if (is_null($name))
		{
			foreach ($this->relations as $name => $relation)
			{
				try
				{
					$relation->saveAll();
				}
				catch (DataModel\Relation\Exception\SaveNotSupported $e)
				{
					// We don't care if a relation doesn't support saving
				}
			}
		}
		else
		{
			if (!isset($this->relations[$name]))
			{
				throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
			}

			$this->relations[$name]->saveAll();
		}

		return $this->parentModel;
	}

	/**
	 * Gets the related items of a relation
	 *
	 * @param   string                   $name            The name of the relation to return data for
	 * @param   callable                 $callback        A callback to customise the returned data
	 * @param   \FOF30\Utils\Collection  $dataCollection  Used when fetching the data of an eager loaded relation
	 *
	 * @return Collection|DataModel
	 *
	 * @throws Relation\Exception\RelationNotFound
	 * @see Relation::getData()
	 *
	 */
	public function getData($name, $callback = null, \FOF30\Utils\Collection $dataCollection = null)
	{
		if (!isset($this->relations[$name]))
		{
			throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
		}

		return $this->relations[$name]->getData($callback, $dataCollection);
	}

	/**
	 * Gets the foreign key map of a many-to-many relation
	 *
	 * @param   string  $name  The name of the relation to return data for
	 *
	 * @return array
	 *
	 * @throws Relation\Exception\RelationNotFound
	 */
	public function &getForeignKeyMap($name)
	{
		if (!isset($this->relations[$name]))
		{
			throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
		}

		return $this->relations[$name]->getForeignKeyMap();
	}

	/**
	 * Returns the count sub-query for a relation, used for relation filters (whereHas in the DataModel).
	 *
	 * @param   string  $name        The relation to get the sub-query for
	 * @param   string  $tableAlias  The alias to use for the local table
	 *
	 * @return JDatabaseQuery
	 * @throws Relation\Exception\RelationNotFound
	 */
	public function getCountSubquery($name, $tableAlias = null)
	{
		if (!isset($this->relations[$name]))
		{
			throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
		}

		return $this->relations[$name]->getCountSubquery($tableAlias);
	}

	/**
	 * A magic method which allows us to define relations using shorthand notation, e.g. $manager->hasOne('phone')
	 * instead of $manager->addRelation('phone', 'hasOne')
	 *
	 * You can also use it to get data of a relation using shorthand notation, e.g. $manager->getPhone($callback)
	 * instead of $manager->getData('phone', $callback);
	 *
	 * @param   string  $name       The magic method to call
	 * @param   array   $arguments  The arguments to the magic method
	 *
	 * @return DataModel The parent model, for chaining
	 *
	 * @throws InvalidArgumentException
	 * @throws DataModel\Relation\Exception\RelationTypeNotFound
	 */
	function __call($name, $arguments)
	{
		$numberOfArguments = count($arguments);

		if (isset(static::$relationTypes[$name]))
		{
			if ($numberOfArguments == 1)
			{
				return $this->addRelation($arguments[0], $name);
			}
			elseif ($numberOfArguments == 2)
			{
				return $this->addRelation($arguments[0], $name, $arguments[1]);
			}
			elseif ($numberOfArguments == 3)
			{
				return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2]);
			}
			elseif ($numberOfArguments == 4)
			{
				return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3]);
			}
			elseif ($numberOfArguments == 5)
			{
				return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
			}
			elseif ($numberOfArguments == 6)
			{
				return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
			}
			elseif ($numberOfArguments >= 7)
			{
				return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]);
			}
			else
			{
				throw new InvalidArgumentException("You can not create an unnamed '$name' relation");
			}
		}
		elseif (substr($name, 0, 3) == 'get')
		{
			$relationName = substr($name, 3);
			$relationName = strtolower($relationName[0]) . substr($relationName, 1);

			if ($numberOfArguments == 0)
			{
				return $this->getData($relationName);
			}
			elseif ($numberOfArguments == 1)
			{
				return $this->getData($relationName, $arguments[0]);
			}
			elseif ($numberOfArguments == 2)
			{
				return $this->getData($relationName, $arguments[0], $arguments[1]);
			}
			else
			{
				throw new InvalidArgumentException("Invalid number of arguments getting data for the '$relationName' relation");
			}
		}

		// Throw an exception otherwise
		throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$name' not known to relation manager");
	}

	/**
	 * Is $name a magic-callable method?
	 *
	 * @param   string  $name  The name of a potential magic-callable method
	 *
	 * @return bool
	 */
	public function isMagicMethod($name)
	{
		if (isset(static::$relationTypes[$name]))
		{
			return true;
		}
		elseif (substr($name, 0, 3) == 'get')
		{
			$relationName = substr($name, 3);
			$relationName = strtolower($relationName[0]) . substr($relationName, 1);

			if (isset($this->relations[$relationName]))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Is $name a magic property? Corollary: returns true if a relation of this name is known to the relation manager.
	 *
	 * @param   string  $name  The name of a potential magic property
	 *
	 * @return bool
	 */
	public function isMagicProperty($name)
	{
		return isset($this->relations[$name]);
	}

	/**
	 * Magic method to get the data of a relation using shorthand notation, e.g. $manager->phone instead of
	 * $manager->getData('phone')
	 *
	 * @param $name
	 *
	 * @return Collection
	 */
	function __get($name)
	{
		return $this->getData($name);
	}
}
Model/DataModel/Filter/Number.php000060400000016045152455514240012635 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter;

defined('_JEXEC') || die;

class Number extends AbstractFilter
{
	/**
	 * The partial match is mapped to an exact match
	 *
	 * @param   mixed  $value  The value to compare to
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function partial($value)
	{
		return $this->exact($value);
	}

	/**
	 * Perform a between limits match. When $include is true
	 * the condition tested is:
	 * $from <= VALUE <= $to
	 * When $include is false the condition tested is:
	 * $from < VALUE < $to
	 *
	 * @param   mixed    $from     The lowest value to compare to
	 * @param   mixed    $to       The higherst value to compare to
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function between($from, $to, $include = true)
	{
		$from = (float) $from;
		$to   = (float) $to;

		if ($this->isEmpty($from) || $this->isEmpty($to))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$from = $this->sanitiseValue($from);
		$to   = $this->sanitiseValue($to);

		$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND ';
		$sql .= '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))';

		return $sql;
	}

	/**
	 * Perform an outside limits match. When $include is true
	 * the condition tested is:
	 * (VALUE <= $from) || (VALUE >= $to)
	 * When $include is false the condition tested is:
	 * (VALUE < $from) || (VALUE > $to)
	 *
	 * @param   mixed    $from     The lowest value of the excluded range
	 * @param   mixed    $to       The higherst value of the excluded range
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function outside($from, $to, $include = false)
	{
		$from = (float) $from;
		$to   = (float) $to;

		if ($this->isEmpty($from) || $this->isEmpty($to))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$from = $this->sanitiseValue($from);
		$to   = $this->sanitiseValue($to);

		$sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $from . ') OR ';
		$sql .= '(' . $this->getFieldName() . ' >' . $extra . ' ' . $to . '))';

		return $sql;
	}

	/**
	 * Perform an interval match. It's similar to a 'between' match, but the
	 * from and to values are calculated based on $value and $interval:
	 * $value - $interval < VALUE < $value + $interval
	 *
	 * @param   integer|float  $value     The center value of the search space
	 * @param   integer|float  $interval  The width of the search space
	 * @param   boolean        $include   Should I include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause
	 */
	public function interval($value, $interval, $include = true)
	{
		if ($this->isEmpty($value))
		{
			return '';
		}

		// Convert them to float, just to be sure
		$value    = (float) $value;
		$interval = (float) $interval;

		$from = $value - $interval;
		$to   = $value + $interval;

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$from = $this->sanitiseValue($from);
		$to   = $this->sanitiseValue($to);

		$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND ';
		$sql .= '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))';

		return $sql;
	}

	/**
	 * Perform a range limits match. When $include is true
	 * the condition tested is:
	 * $from <= VALUE <= $to
	 * When $include is false the condition tested is:
	 * $from < VALUE < $to
	 *
	 * @param   mixed    $from     The lowest value to compare to
	 * @param   mixed    $to       The higherst value to compare to
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function range($from, $to, $include = true)
	{
		if ($this->isEmpty($from) && $this->isEmpty($to))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$sql = [];

		if ($from)
		{
			$sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ')';
		}
		if ($to)
		{
			$sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . ')';
		}

		$sql = '(' . implode(' AND ', $sql) . ')';

		return $sql;
	}

	/**
	 * Perform an interval match. It's similar to a 'between' match, but the
	 * from and to values are calculated based on $value and $interval:
	 * $value - $interval < VALUE < $value + $interval
	 *
	 * @param   integer|float  $value     The starting value of the search space
	 * @param   integer|float  $interval  The interval period of the search space
	 * @param   boolean        $include   Should I include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause
	 */
	public function modulo($value, $interval, $include = true)
	{
		if ($this->isEmpty($value) || $this->isEmpty($interval))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $value . ' AND ';
		$sql .= '(' . $this->getFieldName() . ' - ' . $value . ') % ' . $interval . ' = 0)';

		return $sql;
	}

	/**
	 * Overrides the parent to handle floats in locales where the decimal separator is a comma instead of a dot
	 *
	 * @param   mixed   $value
	 * @param   string  $operator
	 *
	 * @return  string
	 */
	public function search($value, $operator = '=')
	{
		$value = $this->sanitiseValue($value);

		return parent::search($value, $operator);
	}

	/**
	 * Sanitises float values. Really ugly and desperate workaround. Read below.
	 *
	 * Some locales, such as el-GR, use a comma as the decimal separator. This means that $x = 1.23; echo (string) $x;
	 * will yield 1,23 (with a comma!) instead of 1.23 (with a dot!). This affects the way the SQL WHERE clauses are
	 * generated. All database servers expect a dot as the decimal separator. If they see a decimal with a comma as the
	 * separator they throw a SQL error.
	 *
	 * This method will try to replace commas with dots. I tried working around this with locale switching and the %F
	 * (capital F) format option in sprintf to no avail. I'm pretty sure I was doing something wrong, but I ran out of
	 * time trying to find an academically correct solution. The current implementation of sanitiseValue is a silly
	 * hack around the problem. If you have a proper –and better performing– solution please send in a PR and I'll put
	 * it to the test.
	 *
	 * @param   mixed  $value  A string representing a number, integer, float or array of them.
	 *
	 * @return  mixed  The sanitised value, or null if the input wasn't numeric.
	 */
	public function sanitiseValue($value)
	{
		if (!is_numeric($value) && !is_string($value) && !is_array($value))
		{
			$value = null;
		}

		if (!is_array($value))
		{
			$value = str_replace(',', '.', (string) $value);
		}
		else
		{
			$value = array_map([$this, 'sanitiseValue'], $value);
		}

		return $value;
	}
}
Model/DataModel/Filter/Date.php000060400000012017152455514240012255 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter;

defined('_JEXEC') || die;

class Date extends Text
{
	/**
	 * Returns the default search method for this field.
	 *
	 * @return  string
	 */
	public function getDefaultSearchMethod()
	{
		return 'exact';
	}

	/**
	 * Perform a between limits match. When $include is true
	 * the condition tested is:
	 * $from <= VALUE <= $to
	 * When $include is false the condition tested is:
	 * $from < VALUE < $to
	 *
	 * @param   mixed    $from     The lowest value to compare to
	 * @param   mixed    $to       The highest value to compare to
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function between($from, $to, $include = true)
	{
		if ($this->isEmpty($from) || $this->isEmpty($to))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ') AND ';
		$sql .= '(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . '))';

		return $sql;
	}

	/**
	 * Perform an outside limits match. When $include is true
	 * the condition tested is:
	 * (VALUE <= $from) || (VALUE >= $to)
	 * When $include is false the condition tested is:
	 * (VALUE < $from) || (VALUE > $to)
	 *
	 * @param   mixed    $from     The lowest value of the excluded range
	 * @param   mixed    $to       The highest value of the excluded range
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function outside($from, $to, $include = false)
	{
		if ($this->isEmpty($from) || $this->isEmpty($to))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($from) . ') AND ';
		$sql .= '(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($to) . '))';

		return $sql;
	}

	/**
	 * Interval date search
	 *
	 * @param   string               $value     The value to search
	 * @param   string|array|object  $interval  The interval. Can be (+1 MONTH or array('value' => 1, 'unit' =>
	 *                                          'MONTH', 'sign' => '+'))
	 * @param   boolean              $include   If the borders should be included
	 *
	 * @return  string  the sql string
	 */
	public function interval($value, $interval, $include = true)
	{
		if ($this->isEmpty($value) || $this->isEmpty($interval))
		{
			return '';
		}

		$interval = $this->getInterval($interval);

		// Sanity check on $interval array
		if (!isset($interval['sign']) || !isset($interval['value']) || !isset($interval['unit']))
		{
			return '';
		}

		if ($interval['sign'] == '+')
		{
			$function = 'DATE_ADD';
		}
		else
		{
			$function = 'DATE_SUB';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $function;
		$sql .= '(' . $this->getFieldName() . ', INTERVAL ' . $interval['value'] . ' ' . $interval['unit'] . '))';

		return $sql;
	}

	/**
	 * Perform a between limits match. When $include is true
	 * the condition tested is:
	 * $from <= VALUE <= $to
	 * When $include is false the condition tested is:
	 * $from < VALUE < $to
	 *
	 * @param   mixed    $from     The lowest value to compare to
	 * @param   mixed    $to       The highest value to compare to
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function range($from, $to, $include = true)
	{
		if ($this->isEmpty($from) && $this->isEmpty($to))
		{
			return '';
		}

		$extra = '';

		if ($include)
		{
			$extra = '=';
		}

		$sql = [];

		if ($from)
		{
			$sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ')';
		}
		if ($to)
		{
			$sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . ')';
		}

		$sql = '(' . implode(' AND ', $sql) . ')';

		return $sql;
	}

	/**
	 * Parses an interval –which may be given as a string, array or object– into
	 * a standardised hash array that can then be used bu the interval() method.
	 *
	 * @param   string|array|object  $interval  The interval expression to parse
	 *
	 * @return  array  The parsed, hash array form of the interval
	 */
	protected function getInterval($interval)
	{
		if (is_string($interval))
		{
			if (strlen($interval) > 2)
			{
				$interval = explode(" ", $interval);
				$sign     = ($interval[0] == '-') ? '-' : '+';
				$value    = (int) substr($interval[0], 1);

				$interval = [
					'unit'  => $interval[1],
					'value' => $value,
					'sign'  => $sign,
				];
			}
			else
			{
				$interval = [
					'unit'  => 'MONTH',
					'value' => 1,
					'sign'  => '+',
				];
			}
		}
		else
		{
			$interval = (array) $interval;
		}

		return $interval;
	}

}
Model/DataModel/Filter/AbstractFilter.php000060400000023301152455514240014307 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter;

defined('_JEXEC') || die;

use FOF30\Model\DataModel\Filter\Exception\InvalidFieldObject;
use FOF30\Model\DataModel\Filter\Exception\NoDatabaseObject;
use InvalidArgumentException;
use JDatabaseDriver;
use ReflectionClass;
use ReflectionMethod;

abstract class AbstractFilter
{
	/**
	 * The null value for this type
	 *
	 * @var  mixed
	 */
	public $null_value = null;
	protected $db = null;
	/**
	 * The column name of the table field
	 *
	 * @var string
	 */
	protected $name = '';
	/**
	 * The column type of the table field
	 *
	 * @var string
	 */
	protected $type = '';
	/**
	 * Should I allow filtering against the number 0?
	 *
	 * @var bool
	 */
	protected $filterZero = true;
	/**
	 * Prefix each table name with this table alias. For example, field bar normally creates a WHERE clause:
	 * `bar` = '1'
	 * If tableAlias is set to "foo" then the WHERE clause it generates becomes
	 * `foo`.`bar` = '1'
	 *
	 * @var  null
	 */
	protected $tableAlias = null;

	/**
	 * Constructor
	 *
	 * @param   JDatabaseDriver  $db     The database object
	 * @param   object            $field  The field information as taken from the db
	 */
	public function __construct($db, $field)
	{
		$this->db = $db;

		if (!is_object($field) || !isset($field->name) || !isset($field->type))
		{
			throw new InvalidFieldObject;
		}

		$this->name = $field->name;
		$this->type = $field->type;

		if (isset ($field->filterZero))
		{
			$this->filterZero = $field->filterZero;
		}

		if (isset ($field->tableAlias))
		{
			$this->tableAlias = $field->tableAlias;
		}
	}

	/**
	 * Creates a field Object based on the field column type
	 *
	 * @param   object  $field   The field information
	 * @param   array   $config  The field configuration (like the db object to use)
	 *
	 * @return  AbstractFilter  The Filter object
	 *
	 * @throws  InvalidArgumentException
	 */
	public static function getField($field, $config = [])
	{
		if (!is_object($field) || !isset($field->name) || !isset($field->type))
		{
			throw new InvalidFieldObject;
		}

		$type = $field->type;

		$classType = self::getFieldType($type);

		$className = '\\FOF30\\Model\\DataModel\\Filter\\' . ucfirst($classType);

		if (($classType !== false) && class_exists($className, true))
		{
			if (!isset($config['dbo']))
			{
				throw new NoDatabaseObject($className);
			}

			$db = $config['dbo'];

			$field = new $className($db, $field);

			return $field;
		}

		return null;
	}

	/**
	 * Get the class name based on the field Type
	 *
	 * @param   string  $type  The type of the field
	 *
	 * @return  string  the class name suffix
	 */
	public static function getFieldType($type)
	{
		// Remove parentheses, indicating field options / size (they don't matter in type detection)
		if (!empty($type))
		{
			[$type, ] = explode('(', $type);
		}

		$detectedType = null;

		switch (trim($type))
		{
			case 'varchar':
			case 'text':
			case 'smalltext':
			case 'longtext':
			case 'char':
			case 'mediumtext':
			case 'character varying':
			case 'nvarchar':
			case 'nchar':
				$detectedType = 'Text';
				break;

			case 'date':
			case 'datetime':
			case 'time':
			case 'year':
			case 'timestamp':
			case 'timestamp without time zone':
			case 'timestamp with time zone':
				$detectedType = 'Date';
				break;

			case 'tinyint':
			case 'smallint':
				$detectedType = 'Boolean';
				break;
		}

		// Sometimes we have character types followed by a space and some cruft. Let's handle them.
		if (is_null($detectedType) && !empty($type))
		{
			[$type, ] = explode(' ', $type);

			switch (trim($type))
			{
				case 'varchar':
				case 'text':
				case 'smalltext':
				case 'longtext':
				case 'char':
				case 'mediumtext':
				case 'nvarchar':
				case 'nchar':
					$detectedType = 'Text';
					break;

				case 'date':
				case 'datetime':
				case 'time':
				case 'year':
				case 'timestamp':
					$detectedType = 'Date';
					break;

				case 'tinyint':
				case 'smallint':
					$detectedType = 'Boolean';
					break;

				default:
					$detectedType = 'Number';
					break;
			}
		}

		// If all else fails assume it's a Number and hope for the best
		if (empty($detectedType))
		{
			$detectedType = 'Number';
		}

		return $detectedType;
	}

	/**
	 * Is it a null or otherwise empty value?
	 *
	 * @param   mixed  $value  The value to test for emptiness
	 *
	 * @return  boolean
	 */
	public function isEmpty($value)
	{
		return (($value === $this->null_value) || empty($value))
			&& !($this->filterZero && ($value === "0"));
	}

	/**
	 * Returns the default search method for a field. This always returns 'exact'
	 * and you are supposed to override it in specialised classes. The possible
	 * values are exact, partial, between and outside, unless something
	 * different is returned by getSearchMethods().
	 *
	 * @return  string
	 * @see  self::getSearchMethods()
	 *
	 */
	public function getDefaultSearchMethod()
	{
		return 'exact';
	}

	/**
	 * Return the search methods available for this field class,
	 *
	 * @return  array
	 */
	public function getSearchMethods()
	{
		$ignore = [
			'isEmpty', 'getField', 'getFieldType', '__construct', 'getDefaultSearchMethod', 'getSearchMethods',
			'getFieldName',
		];

		$class   = new ReflectionClass(__CLASS__);
		$methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);

		$tmp = [];

		foreach ($methods as $method)
		{
			$tmp[] = $method->name;
		}

		$methods = $tmp;

		if ($methods = array_diff($methods, $ignore))
		{
			return $methods;
		}

		return [];
	}

	/**
	 * Perform an exact match (equality matching)
	 *
	 * @param   mixed  $value  The value to compare to
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function exact($value)
	{
		if ($this->isEmpty($value))
		{
			return '';
		}

		if (is_array($value))
		{
			$db    = $this->db;
			$value = array_map([$db, 'quote'], $value);

			return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))';
		}
		else
		{
			return $this->search($value, '=');
		}
	}

	/**
	 * Perform a partial match (usually: search in string)
	 *
	 * @param   mixed  $value  The value to compare to
	 *
	 * @return  string  The SQL where clause for this search
	 */
	abstract public function partial($value);

	/**
	 * Perform a between limits match (usually: search for a value between
	 * two numbers or a date between two preset dates). When $include is true
	 * the condition tested is:
	 * $from <= VALUE <= $to
	 * When $include is false the condition tested is:
	 * $from < VALUE < $to
	 *
	 * @param   mixed    $from     The lowest value to compare to
	 * @param   mixed    $to       The highest value to compare to
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	abstract public function between($from, $to, $include = true);

	/**
	 * Perform an outside limits match (usually: search for a value outside an
	 * area or a date outside a preset period). When $include is true
	 * the condition tested is:
	 * (VALUE <= $from) || (VALUE >= $to)
	 * When $include is false the condition tested is:
	 * (VALUE < $from) || (VALUE > $to)
	 *
	 * @param   mixed    $from     The lowest value of the excluded range
	 * @param   mixed    $to       The highest value of the excluded range
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	abstract public function outside($from, $to, $include = false);

	/**
	 * Perform an interval search (usually: a date interval check)
	 *
	 * @param   string               $from      The value to search
	 * @param   string|array|object  $interval  The interval
	 *
	 * @return  string  The SQL where clause for this search
	 */
	abstract public function interval($from, $interval);

	/**
	 * Perform a between limits match (usually: search for a value between
	 * two numbers or a date between two preset dates). When $include is true
	 * the condition tested is:
	 * $from <= VALUE <= $to
	 * When $include is false the condition tested is:
	 * $from < VALUE < $to
	 *
	 * @param   mixed    $from     The lowest value to compare to
	 * @param   mixed    $to       The highest value to compare to
	 * @param   boolean  $include  Should we include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause for this search
	 */
	abstract public function range($from, $to, $include = true);

	/**
	 * Perform an modulo search
	 *
	 * @param   integer|float  $from      The starting value of the search space
	 * @param   integer|float  $interval  The interval period of the search space
	 * @param   boolean        $include   Should I include the boundaries in the search?
	 *
	 * @return  string  The SQL where clause
	 */
	abstract public function modulo($from, $interval, $include = true);

	/**
	 * Return the SQL where clause for a search
	 *
	 * @param   mixed   $value     The value to search for
	 * @param   string  $operator  The operator to use
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function search($value, $operator = '=')
	{
		if ($this->isEmpty($value))
		{
			return '';
		}

		$prefix = '';

		if (substr($operator, 0, 1) == '!')
		{
			$prefix   = 'NOT ';
			$operator = substr($operator, 1);
		}

		return $prefix . '(' . $this->getFieldName() . ' ' . $operator . ' ' . $this->db->quote($value) . ')';
	}

	/**
	 * Get the field name
	 *
	 * @return  string    The field name
	 */
	public function getFieldName()
	{
		$name = $this->db->qn($this->name);

		if (!empty($this->tableAlias))
		{
			$name = $this->db->qn($this->tableAlias) . '.' . $name;
		}

		return $name;
	}
}
Model/DataModel/Filter/Exception/InvalidFieldObject.php000060400000001161152455514240017015 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter\Exception;

defined('_JEXEC') || die;

use Exception;
use InvalidArgumentException;
use Joomla\CMS\Language\Text;

class InvalidFieldObject extends InvalidArgumentException
{
	public function __construct($message = "", $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_FILTER_INVALIDFIELD');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Filter/Exception/NoDatabaseObject.php000060400000001134152455514240016464 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter\Exception;

defined('_JEXEC') || die;

use Exception;
use InvalidArgumentException;
use Joomla\CMS\Language\Text;

class NoDatabaseObject extends InvalidArgumentException
{
	public function __construct($fieldType, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_FILTER_NODBOBJECT', $fieldType);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Filter/Text.php000060400000006234152455514240012330 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter;

defined('_JEXEC') || die;

use JDatabaseDriver;

class Text extends AbstractFilter
{
	/**
	 * Constructor
	 *
	 * @param   JDatabaseDriver  $db     The database object
	 * @param   object           $field  The field information as taken from the db
	 */
	public function __construct($db, $field)
	{
		parent::__construct($db, $field);

		$this->null_value = '';
	}

	/**
	 * Returns the default search method for this field.
	 *
	 * @return  string
	 */
	public function getDefaultSearchMethod()
	{
		return 'partial';
	}

	/**
	 * Perform a partial match (search in string)
	 *
	 * @param   mixed  $value  The value to compare to
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function partial($value)
	{
		if ($this->isEmpty($value))
		{
			return '';
		}

		return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote('%' . $value . '%') . ')';
	}

	/**
	 * Perform an exact match (match string)
	 *
	 * @param   mixed  $value  The value to compare to
	 *
	 * @return  string  The SQL where clause for this search
	 */
	public function exact($value)
	{
		if ($this->isEmpty($value))
		{
			return '';
		}

		if (is_array($value) || is_object($value))
		{
			$value = (array) $value;

			$db    = $this->db;
			$value = array_map([$db, 'quote'], $value);

			return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))';
		}

		return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote($value) . ')';
	}

	/**
	 * Dummy method; this search makes no sense for text fields
	 *
	 * @param   mixed    $from     Ignored
	 * @param   mixed    $to       Ignored
	 * @param   boolean  $include  Ignored
	 *
	 * @return  string  Empty string
	 */
	public function between($from, $to, $include = true)
	{
		return '';
	}

	/**
	 * Dummy method; this search makes no sense for text fields
	 *
	 * @param   mixed    $from     Ignored
	 * @param   mixed    $to       Ignored
	 * @param   boolean  $include  Ignored
	 *
	 * @return  string  Empty string
	 */
	public function outside($from, $to, $include = false)
	{
		return '';
	}

	/**
	 * Dummy method; this search makes no sense for text fields
	 *
	 * @param   mixed    $value     Ignored
	 * @param   mixed    $interval  Ignored
	 * @param   boolean  $include   Ignored
	 *
	 * @return  string  Empty string
	 */
	public function interval($value, $interval, $include = true)
	{
		return '';
	}

	/**
	 * Dummy method; this search makes no sense for text fields
	 *
	 * @param   mixed    $from     Ignored
	 * @param   mixed    $to       Ignored
	 * @param   boolean  $include  Ignored
	 *
	 * @return  string  Empty string
	 */
	public function range($from, $to, $include = false)
	{
		return '';
	}

	/**
	 * Dummy method; this search makes no sense for text fields
	 *
	 * @param   mixed    $from      Ignored
	 * @param   mixed    $interval  Ignored
	 * @param   boolean  $include   Ignored
	 *
	 * @return  string  Empty string
	 */
	public function modulo($from, $interval, $include = false)
	{
		return '';
	}
}
Model/DataModel/Filter/Boolean.php000060400000000761152455514240012762 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter;

defined('_JEXEC') || die;

class Boolean extends Number
{
	/**
	 * Is it a null or otherwise empty value?
	 *
	 * @param   mixed  $value  The value to test for emptiness
	 *
	 * @return  boolean
	 */
	public function isEmpty($value)
	{
		return is_null($value) || ($value === '');
	}
}
Model/DataModel/Filter/Relation.php000060400000001410152455514240013150 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Filter;

use JDatabaseQuery;

defined('_JEXEC') || die;

class Relation extends Number
{
	/** @var JDatabaseQuery The COUNT sub-query to filter by */
	protected $subQuery = null;

	public function __construct($db, $relationName, $subQuery)
	{
		$field = (object) [
			'name' => $relationName,
			'type' => 'relation',
		];

		parent::__construct($db, $field);

		$this->subQuery = $subQuery;
	}

	public function callback($value)
	{
		return call_user_func($value, $this->subQuery);
	}

	public function getFieldName()
	{
		return '(' . (string) $this->subQuery . ')';
	}
}
Model/DataModel/Relation.php000060400000020212152455514240011724 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Model\DataModel;
use JDatabaseQuery;

abstract class Relation
{
	/** @var   DataModel  The data model we are attached to */
	protected $parentModel = null;

	/** @var   string  The class name of the foreign key's model */
	protected $foreignModelClass = null;

	/** @var   string  The application name of the foreign model */
	protected $foreignModelComponent = null;

	/** @var   string  The bade name of the foreign model */
	protected $foreignModelName = null;

	/** @var   string   The local table key for this relation */
	protected $localKey = null;

	/** @var   string   The foreign table key for this relation */
	protected $foreignKey = null;

	/** @var   null  For many-to-many relations, the pivot (glue) table */
	protected $pivotTable = null;

	/** @var   null  For many-to-many relations, the pivot table's column storing the local key */
	protected $pivotLocalKey = null;

	/** @var   null  For many-to-many relations, the pivot table's column storing the foreign key */
	protected $pivotForeignKey = null;

	/** @var   Collection  The data loaded by this relation */
	protected $data = null;

	/** @var  array  Maps each local table key to an array of foreign table keys, used in many-to-many relations */
	protected $foreignKeyMap = [];

	/** @var  Container  The component container for this relation */
	protected $container = null;

	/**
	 * Public constructor. Initialises the relation.
	 *
	 * @param   DataModel  $parentModel       The data model we are attached to
	 * @param   string     $foreignModelName  The name of the foreign key's model in the format
	 *                                        "modelName@com_something"
	 * @param   string     $localKey          The local table key for this relation
	 * @param   string     $foreignKey        The foreign key for this relation
	 * @param   string     $pivotTable        For many-to-many relations, the pivot (glue) table
	 * @param   string     $pivotLocalKey     For many-to-many relations, the pivot table's column storing the local
	 *                                        key
	 * @param   string     $pivotForeignKey   For many-to-many relations, the pivot table's column storing the foreign
	 *                                        key
	 */
	public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
	{
		$this->parentModel       = $parentModel;
		$this->foreignModelClass = $foreignModelName;
		$this->localKey          = $localKey;
		$this->foreignKey        = $foreignKey;
		$this->pivotTable        = $pivotTable;
		$this->pivotLocalKey     = $pivotLocalKey;
		$this->pivotForeignKey   = $pivotForeignKey;

		$this->container = $parentModel->getContainer();

		$class = $foreignModelName;

		if (strpos($class, '@') === false)
		{
			$this->foreignModelComponent = null;
			$this->foreignModelName      = $class;
		}
		else
		{
			$foreignParts                = explode('@', $class, 2);
			$this->foreignModelComponent = $foreignParts[1];
			$this->foreignModelName      = $foreignParts[0];
		}
	}

	/**
	 * Reset the relation data
	 *
	 * @return $this For chaining
	 */
	public function reset()
	{
		$this->data          = null;
		$this->foreignKeyMap = [];

		return $this;
	}

	/**
	 * Rebase the relation to a different model
	 *
	 * @param   DataModel  $model
	 *
	 * @return $this For chaining
	 */
	public function rebase(DataModel $model)
	{
		$this->parentModel = $model;

		return $this->reset();
	}

	/**
	 * Get the relation data.
	 *
	 * If you want to apply additional filtering to the foreign model, use the $callback. It can be any function,
	 * static method, public method or closure with an interface of function(DataModel $foreignModel). You are not
	 * supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do:
	 * $foreignModel->setState('foo', 'bar')
	 *
	 * @param   callable    $callback  The callback to run on the remote model.
	 * @param   Collection  $dataCollection
	 *
	 * @return Collection|DataModel
	 */
	public function getData($callback = null, Collection $dataCollection = null)
	{
		if (is_null($this->data))
		{
			// Initialise
			$this->data = new Collection();

			// Get a model instance
			$foreignModel = $this->getForeignModel();
			$foreignModel->setIgnoreRequest(true);

			$filtered = $this->filterForeignModel($foreignModel, $dataCollection);

			if (!$filtered)
			{
				return $this->data;
			}

			// Apply the callback, if applicable
			if (!is_null($callback) && is_callable($callback))
			{
				call_user_func($callback, $foreignModel);
			}

			// Get the list of items from the foreign model and cache in $this->data
			$this->data = $foreignModel->get(true);
		}

		return $this->data;
	}

	/**
	 * Populates the internal $this->data collection from the contents of the provided collection. This is used by
	 * DataModel to push the eager loaded data into each item's relation.
	 *
	 * @param   Collection  $data    The relation data to push into this relation
	 * @param   mixed       $keyMap  Used by many-to-many relations to pass around the local to foreign key map
	 *
	 * @return void
	 */
	public function setDataFromCollection(Collection &$data, $keyMap = null)
	{
		$this->data = new Collection();

		if (!empty($data))
		{
			$localKeyValue = $this->parentModel->getFieldValue($this->localKey);

			/** @var DataModel $item */
			foreach ($data as $key => $item)
			{
				if ($item->getFieldValue($this->foreignKey) == $localKeyValue)
				{
					$this->data->add($item);
				}
			}
		}
	}

	/**
	 * Returns the count sub-query for DataModel's has() and whereHas() methods.
	 *
	 * @return JDatabaseQuery
	 */
	abstract public function getCountSubquery();

	/**
	 * Returns a new item of the foreignModel type, pre-initialised to fulfil this relation
	 *
	 * @return DataModel
	 *
	 * @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
	 */
	abstract public function getNew();

	/**
	 * Saves all related items. You can use it to touch items as well: every item being saved causes the modified_by and
	 * modified_on fields to be changed automatically, thanks to the DataModel's magic.
	 */
	public function saveAll()
	{
		if ($this->data instanceof Collection)
		{
			foreach ($this->data as $item)
			{
				if ($item instanceof DataModel)
				{
					$item->save();
				}
			}
		}
	}

	/**
	 * Returns the foreign key map of a many-to-many relation, used for eager loading many-to-many relations
	 *
	 * @return array
	 */
	public function &getForeignKeyMap()
	{
		return $this->foreignKeyMap;
	}

	/**
	 * Gets an object instance of the foreign model
	 *
	 * @param   array  $config  Optional configuration information for the Model
	 *
	 * @return DataModel
	 */
	public function &getForeignModel(array $config = [])
	{
		// If the model comes from this component go through our Factory
		if (is_null($this->foreignModelComponent))
		{
			$model = $this->container->factory->model($this->foreignModelName, $config)->tmpInstance();

			return $model;
		}

		// The model comes from another component. Create a container and go through its factory.
		$foreignContainer = Container::getInstance($this->foreignModelComponent, ['tempInstance' => true]);
		$model            = $foreignContainer->factory->model($this->foreignModelName, $config)->tmpInstance();

		return $model;
	}

	/**
	 * Returns the name of the local key of the relation
	 *
	 * @return  string
	 */
	public function getLocalKey()
	{
		return $this->localKey;
	}

	/**
	 * Applies the relation filters to the foreign model when getData is called
	 *
	 * @param   DataModel   $foreignModel    The foreign model you're operating on
	 * @param   Collection  $dataCollection  If it's an eager loaded relation, the collection of loaded parent records
	 *
	 * @return boolean Return false to force an empty data collection
	 */
	abstract protected function filterForeignModel(DataModel $foreignModel, Collection $dataCollection = null);
}
Model/DataModel/Exception/NoContentType.php000060400000001116152455514240014660 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use UnexpectedValueException;

class NoContentType extends UnexpectedValueException
{
	public function __construct($className, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_NOCONTENTTYPE', $className);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeIncompatibleTable.php000060400000001137152455514240016310 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use UnexpectedValueException;

class TreeIncompatibleTable extends UnexpectedValueException
{
	public function __construct($tableName, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_TREE_INCOMPATIBLETABLE', $tableName);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeUnexpectedPrimaryKey.php000060400000001156152455514240017054 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use UnexpectedValueException;

class TreeUnexpectedPrimaryKey extends UnexpectedValueException
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_TREE_UNEXPECTEDPK');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeInvalidLftRgt.php000060400000000744152455514240015446 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use RuntimeException;

abstract class TreeInvalidLftRgt extends RuntimeException
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeInvalidLftRgtSibling.php000060400000001122152455514240016745 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class TreeInvalidLftRgtSibling extends TreeInvalidLftRgt
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_SIBLING');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeInvalidLftRgtCurrent.php000060400000001122152455514240017000 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class TreeInvalidLftRgtCurrent extends TreeInvalidLftRgt
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_CURRENT');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/NoTableColumns.php000060400000000441152455514240014774 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

class NoTableColumns extends BaseException
{

}
Model/DataModel/Exception/CannotLockNotLoadedRecord.php000060400000001116152455514240017073 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class CannotLockNotLoadedRecord extends BaseException
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_CANNOTLOCKNOTLOADEDRECORD');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/SpecialColumnMissing.php000060400000000447152455514240016205 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

class SpecialColumnMissing extends BaseException
{

}
Model/DataModel/Exception/InvalidSearchMethod.php000060400000000446152455514240015771 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

class InvalidSearchMethod extends BaseException
{

}
Model/DataModel/Exception/TreeInvalidLftRgtOther.php000060400000001116152455514240016442 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class TreeInvalidLftRgtOther extends TreeInvalidLftRgt
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_OTHER');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/RecordNotLoaded.php000060400000001067152455514240015124 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class RecordNotLoaded extends BaseException
{
	public function __construct($message = "", $code = 404, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_COULDNOTLOAD');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeRootNotFound.php000060400000001121152455514240015323 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class TreeRootNotFound extends RuntimeException
{
	public function __construct($tableName, $lft, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_TREE_ROOTNOTFOUND', $tableName, $lft);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeMethodOnlyAllowedInRoot.php000060400000001115152455514240017453 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class TreeMethodOnlyAllowedInRoot extends RuntimeException
{
	public function __construct($method = '', $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_TREE_ONLYINROOT', $method);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeInvalidLftRgtParent.php000060400000001120152455514240016605 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class TreeInvalidLftRgtParent extends TreeInvalidLftRgt
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_PARENT');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/NoItemsFound.php000060400000001043152455514240014460 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

class NoItemsFound extends BaseException
{
	public function __construct($className, $code = 404, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_NOITEMSFOUND', $className);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/TreeUnsupportedMethod.php000060400000001112152455514240016414 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use LogicException;

class TreeUnsupportedMethod extends LogicException
{
	public function __construct($method = '', $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_TREE_UNSUPPORTEDMETHOD', $method);

		parent::__construct($message, $code, $previous);
	}

}
Model/DataModel/Exception/BaseException.php000060400000000472152455514240014644 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use RuntimeException;

class BaseException extends RuntimeException
{

}
Model/DataModel/Exception/NoAssetKey.php000060400000001131152455514240014131 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\DataModel\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use UnexpectedValueException;

class NoAssetKey extends UnexpectedValueException
{
	public function __construct($message = '', $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_NOASSETKEY');
		}

		parent::__construct($message, $code, $previous);
	}

}
Model/Exception/CannotGetName.php000060400000001202152455514240012734 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Model\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when we can't get a Controller's name
 */
class CannotGetName extends RuntimeException
{
	public function __construct($message = "", $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_MODEL_ERR_GET_NAME');
		}

		parent::__construct($message, $code, $previous);
	}

}
Factory/FactoryInterface.php000060400000005323152455514240012122 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Controller\Controller;
use FOF30\Dispatcher\Dispatcher;
use FOF30\Model\Model;
use FOF30\Toolbar\Toolbar;
use FOF30\TransparentAuthentication\TransparentAuthentication;
use FOF30\View\View;

/**
 * Interface for the MVC object factory
 */
interface FactoryInterface
{
	/**
	 * Public constructor for the factory object
	 *
	 * @param   Container  $container  The container we belong to
	 */
	function __construct(Container $container);

	/**
	 * Create a new Controller object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Controller for.
	 * @param   array   $config    Optional MVC configuration values for the Controller object.
	 *
	 * @return  Controller
	 */
	function controller($viewName, array $config = []);

	/**
	 * Create a new Model object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Model for.
	 * @param   array   $config    Optional MVC configuration values for the Model object.
	 *
	 * @return  Model
	 */
	function model($viewName, array $config = []);

	/**
	 * Create a new View object
	 *
	 * @param   string  $viewName  The name of the view we're getting a View object for.
	 * @param   string  $viewType  The type of the View object. By default it's "html".
	 * @param   array   $config    Optional MVC configuration values for the View object.
	 *
	 * @return  View
	 */
	function view($viewName, $viewType = 'html', array $config = []);

	/**
	 * Creates a new Toolbar
	 *
	 * @param   array  $config  The configuration values for the Toolbar object
	 *
	 * @return  Toolbar
	 */
	function toolbar(array $config = []);

	/**
	 * Creates a new Dispatcher
	 *
	 * @param   array  $config  The configuration values for the Dispatcher object
	 *
	 * @return  Dispatcher
	 */
	function dispatcher(array $config = []);

	/**
	 * Creates a new TransparentAuthentication handler
	 *
	 * @param   array  $config  The configuration values for the TransparentAuthentication object
	 *
	 * @return  TransparentAuthentication
	 */
	function transparentAuthentication(array $config = []);

	/**
	 * Creates a view template finder object for a specific View
	 *
	 * @param   View   $view    The view this view template finder will be attached to
	 * @param   array  $config  Configuration variables for the object
	 *
	 * @return  mixed
	 */
	function viewFinder(View $view, array $config = []);

	/**
	 * @return string
	 */
	public function getSection();

	/**
	 * @param   string  $section
	 */
	public function setSection($section);
}
Factory/Exception/ToolbarNotFound.php000060400000001074152455514240013706 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class ToolbarNotFound extends RuntimeException
{
	public function __construct($toolbarClass, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_TOOLBAR_ERR_NOT_FOUND', $toolbarClass);

		parent::__construct($message, $code, $previous);
	}

}
Factory/Exception/ControllerNotFound.php000060400000001076152455514240014431 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class ControllerNotFound extends RuntimeException
{
	public function __construct($controller, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_CONTROLLER_ERR_NOT_FOUND', $controller);

		parent::__construct($message, $code, $previous);
	}

}
Factory/Exception/DispatcherNotFound.php000060400000001110152455514240014361 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class DispatcherNotFound extends RuntimeException
{
	public function __construct($dispatcherClass, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_DISPATCHER_ERR_NOT_FOUND', $dispatcherClass);

		parent::__construct($message, $code, $previous);
	}

}
Factory/Exception/ModelNotFound.php000060400000001064152455514240013343 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class ModelNotFound extends RuntimeException
{
	public function __construct($modelClass, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_MODEL_ERR_NOT_FOUND', $modelClass);

		parent::__construct($message, $code, $previous);
	}

}
Factory/Exception/TransparentAuthenticationNotFound.php000060400000001114152455514240017500 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class TransparentAuthenticationNotFound extends RuntimeException
{
	public function __construct($taClass, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_TRANSPARENTAUTH_ERR_NOT_FOUND', $taClass);

		parent::__construct($message, $code, $previous);
	}

}
Factory/Exception/ViewNotFound.php000060400000001060152455514240013211 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

class ViewNotFound extends RuntimeException
{
	public function __construct($viewClass, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_VIEW_ERR_NOT_FOUND', $viewClass);

		parent::__construct($message, $code, $previous);
	}

}
Factory/SwitchFactory.php000060400000016256152455514240011472 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Controller\Controller;
use FOF30\Dispatcher\Dispatcher;
use FOF30\Factory\Exception\ControllerNotFound;
use FOF30\Factory\Exception\DispatcherNotFound;
use FOF30\Factory\Exception\ModelNotFound;
use FOF30\Factory\Exception\ToolbarNotFound;
use FOF30\Factory\Exception\TransparentAuthenticationNotFound;
use FOF30\Factory\Exception\ViewNotFound;
use FOF30\Model\Model;
use FOF30\Toolbar\Toolbar;
use FOF30\TransparentAuthentication\TransparentAuthentication;
use FOF30\View\View;

/**
 * MVC object factory. This implements the advanced functionality, i.e. creating MVC objects only if the classes exist
 * in any component section (front-end, back-end). For example, if you're in the front-end and a Model class doesn't
 * exist there but does exist in the back-end then the back-end class will be returned.
 *
 * The Dispatcher and Toolbar will be created from default objects if specialised classes are not found in your
 * application.
 */
class SwitchFactory extends BasicFactory implements FactoryInterface
{
	/**
	 * Create a new Controller object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Controller for.
	 * @param   array   $config    Optional MVC configuration values for the Controller object.
	 *
	 * @return  Controller
	 */
	public function controller($viewName, array $config = [])
	{
		try
		{
			return parent::controller($viewName, $config);
		}
		catch (ControllerNotFound $e)
		{
		}

		$controllerClass = $this->container->getNamespacePrefix('inverse') . 'Controller\\' . ucfirst($viewName);

		try
		{
			return $this->createController($controllerClass, $config);
		}
		catch (ControllerNotFound $e)
		{
		}

		$controllerClass = $this->container->getNamespacePrefix('inverse') . 'Controller\\' . ucfirst($this->container->inflector->singularize($viewName));

		return $this->createController($controllerClass, $config);
	}

	/**
	 * Create a new Model object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Model for.
	 * @param   array   $config    Optional MVC configuration values for the Model object.
	 *
	 * @return  Model
	 */
	public function model($viewName, array $config = [])
	{
		try
		{
			return parent::model($viewName, $config);
		}
		catch (ModelNotFound $e)
		{
		}

		$modelClass = $this->container->getNamespacePrefix('inverse') . 'Model\\' . ucfirst($viewName);

		try
		{
			return $this->createModel($modelClass, $config);
		}
		catch (ModelNotFound $e)
		{
			$modelClass = $this->container->getNamespacePrefix('inverse') . 'Model\\' . ucfirst($this->container->inflector->singularize($viewName));

			return $this->createModel($modelClass, $config);
		}
	}

	/**
	 * Create a new View object
	 *
	 * @param   string  $viewName  The name of the view we're getting a View object for.
	 * @param   string  $viewType  The type of the View object. By default it's "html".
	 * @param   array   $config    Optional MVC configuration values for the View object.
	 *
	 * @return  View
	 */
	public function view($viewName, $viewType = 'html', array $config = [])
	{
		try
		{
			return parent::view($viewName, $viewType, $config);
		}
		catch (ViewNotFound $e)
		{
		}

		$viewClass = $this->container->getNamespacePrefix('inverse') . 'View\\' . ucfirst($viewName) . '\\' . ucfirst($viewType);

		try
		{
			return $this->createView($viewClass, $config);
		}
		catch (ViewNotFound $e)
		{
			$viewClass = $this->container->getNamespacePrefix('inverse') . 'View\\' . ucfirst($this->container->inflector->singularize($viewName)) . '\\' . ucfirst($viewType);

			return $this->createView($viewClass, $config);
		}
	}

	/**
	 * Creates a new Dispatcher
	 *
	 * @param   array  $config  The configuration values for the Dispatcher object
	 *
	 * @return  Dispatcher
	 */
	public function dispatcher(array $config = [])
	{
		$dispatcherClass = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\Dispatcher';

		try
		{
			return $this->createDispatcher($dispatcherClass, $config);
		}
		catch (DispatcherNotFound $e)
		{
			// Not found. Let's go on.
		}

		$dispatcherClass = $this->container->getNamespacePrefix('inverse') . 'Dispatcher\\Dispatcher';

		try
		{
			return $this->createDispatcher($dispatcherClass, $config);
		}
		catch (DispatcherNotFound $e)
		{
			// Not found. Return the default Dispatcher
			return new Dispatcher($this->container, $config);
		}
	}

	/**
	 * Creates a new Toolbar
	 *
	 * @param   array  $config  The configuration values for the Toolbar object
	 *
	 * @return  Toolbar
	 */
	public function toolbar(array $config = [])
	{
		$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'Toolbar\\Toolbar';

		try
		{
			return $this->createToolbar($toolbarClass, $config);
		}
		catch (ToolbarNotFound $e)
		{
			// Not found. Let's go on.
		}

		$toolbarClass = $this->container->getNamespacePrefix('inverse') . 'Toolbar\\Toolbar';

		try
		{
			return $this->createToolbar($toolbarClass, $config);
		}
		catch (ToolbarNotFound $e)
		{
			// Not found. Return the default Toolbar
			return new Toolbar($this->container, $config);
		}
	}


	/**
	 * Creates a new TransparentAuthentication
	 *
	 * @param   array  $config  The configuration values for the TransparentAuthentication object
	 *
	 * @return  TransparentAuthentication
	 */
	public function transparentAuthentication(array $config = [])
	{
		$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\TransparentAuthentication';

		try
		{
			return $this->createTransparentAuthentication($toolbarClass, $config);
		}
		catch (TransparentAuthenticationNotFound $e)
		{
			// Not found. Let's go on.
		}

		$toolbarClass = $this->container->getNamespacePrefix('inverse') . 'TransparentAuthentication\\TransparentAuthentication';

		try
		{
			return $this->createTransparentAuthentication($toolbarClass, $config);
		}
		catch (TransparentAuthenticationNotFound $e)
		{
			// Not found. Return the default TransparentAuthentication
			return new TransparentAuthentication($this->container, $config);
		}
	}

	/**
	 * Creates a view template finder object for a specific View.
	 *
	 * The default configuration is:
	 * Look for .php, .blade.php files; default layout "default"; no default sub-template;
	 * look for both pluralised and singular views; fall back to the default layout without sub-template;
	 * look for templates in both site and admin
	 *
	 * @param   View   $view    The view this view template finder will be attached to
	 * @param   array  $config  Configuration variables for the object
	 *
	 * @return  mixed
	 */
	public function viewFinder(View $view, array $config = [])
	{
		// Initialise the configuration with the default values
		$defaultConfig = [
			'extensions'    => ['.php', '.blade.php'],
			'defaultLayout' => 'default',
			'defaultTpl'    => '',
			'strictView'    => false,
			'strictTpl'     => false,
			'strictLayout'  => false,
			'sidePrefix'    => 'any',
		];

		$config = array_merge($defaultConfig, $config);

		return parent::viewFinder($view, $config);
	}
}
Factory/MagicSwitchFactory.php000060400000013361152455514240012425 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory;

defined('_JEXEC') || die;

use FOF30\Controller\Controller;
use FOF30\Dispatcher\Dispatcher;
use FOF30\Factory\Exception\ControllerNotFound;
use FOF30\Factory\Exception\DispatcherNotFound;
use FOF30\Factory\Exception\ModelNotFound;
use FOF30\Factory\Exception\TransparentAuthenticationNotFound;
use FOF30\Factory\Exception\ViewNotFound;
use FOF30\Factory\Magic\DispatcherFactory;
use FOF30\Factory\Magic\TransparentAuthenticationFactory;
use FOF30\Model\Model;
use FOF30\Toolbar\Toolbar;
use FOF30\TransparentAuthentication\TransparentAuthentication;
use FOF30\View\View;

/**
 * Magic MVC object factory. This factory will "magically" create MVC objects even if the respective classes do not
 * exist, based on information in your fof.xml file.
 *
 * Note: This factory class will look for MVC objects in BOTH component sections (front-end, back-end), not just the one
 * you are currently running in. If no class is found a new object will be created magically. This is the same behaviour
 * as FOF 2.x.
 */
class MagicSwitchFactory extends SwitchFactory implements FactoryInterface
{
	/**
	 * Create a new Controller object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Controller for.
	 * @param   array   $config    Optional MVC configuration values for the Controller object.
	 *
	 * @return  Controller
	 */
	public function controller($viewName, array $config = [])
	{
		try
		{
			return parent::controller($viewName, $config);
		}
		catch (ControllerNotFound $e)
		{
			$magic = new Magic\ControllerFactory($this->container);
			// Let's pass the section override (if any)
			$magic->setSection($this->getSection());

			return $magic->make($viewName, $config);
		}
	}

	/**
	 * Create a new Model object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Model for.
	 * @param   array   $config    Optional MVC configuration values for the Model object.
	 *
	 * @return  Model
	 */
	public function model($viewName, array $config = [])
	{
		try
		{
			return parent::model($viewName, $config);
		}
		catch (ModelNotFound $e)
		{
			$magic = new Magic\ModelFactory($this->container);
			// Let's pass the section override (if any)
			$magic->setSection($this->getSection());

			return $magic->make($viewName, $config);
		}
	}

	/**
	 * Create a new View object
	 *
	 * @param   string  $viewName  The name of the view we're getting a View object for.
	 * @param   string  $viewType  The type of the View object. By default it's "html".
	 * @param   array   $config    Optional MVC configuration values for the View object.
	 *
	 * @return  View
	 */
	public function view($viewName, $viewType = 'html', array $config = [])
	{
		try
		{
			return parent::view($viewName, $viewType, $config);
		}
		catch (ViewNotFound $e)
		{
			$magic = new Magic\ViewFactory($this->container);
			// Let's pass the section override (if any)
			$magic->setSection($this->getSection());

			return $magic->make($viewName, $viewType, $config);
		}
	}

	/**
	 * Creates a new Toolbar
	 *
	 * @param   array  $config  The configuration values for the Toolbar object
	 *
	 * @return  Toolbar
	 */
	public function toolbar(array $config = [])
	{
		$appConfig = $this->container->appConfig;

		$defaultConfig = [
			'useConfigurationFile' => true,
			'renderFrontendButtons' => in_array($appConfig->get("views.*.config.renderFrontendButtons"), [
				true, 'true', 'yes', 'on', 1,
			]),
			'renderFrontendSubmenu' => in_array($appConfig->get("views.*.config.renderFrontendSubmenu"), [
				true, 'true', 'yes', 'on', 1,
			]),
		];

		$config = array_merge($defaultConfig, $config);

		return parent::toolbar($config);
	}

	/**
	 * Creates a new Dispatcher
	 *
	 * @param   array  $config  The configuration values for the Dispatcher object
	 *
	 * @return  Dispatcher
	 */
	public function dispatcher(array $config = [])
	{
		$dispatcherClass = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\Dispatcher';

		try
		{
			return $this->createDispatcher($dispatcherClass, $config);
		}
		catch (DispatcherNotFound $e)
		{
			// Not found. Let's go on.
		}

		$dispatcherClass = $this->container->getNamespacePrefix('inverse') . 'Dispatcher\\Dispatcher';

		try
		{
			return $this->createDispatcher($dispatcherClass, $config);
		}
		catch (DispatcherNotFound $e)
		{
			// Not found. Return the magically created Dispatcher
			$magic = new DispatcherFactory($this->container);
			// Let's pass the section override (if any)
			$magic->setSection($this->getSection());

			return $magic->make($config);
		}
	}

	/**
	 * Creates a new TransparentAuthentication
	 *
	 * @param   array  $config  The configuration values for the TransparentAuthentication object
	 *
	 * @return  TransparentAuthentication
	 */
	public function transparentAuthentication(array $config = [])
	{
		$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\TransparentAuthentication';

		try
		{
			return $this->createTransparentAuthentication($toolbarClass, $config);
		}
		catch (TransparentAuthenticationNotFound $e)
		{
			// Not found. Let's go on.
		}

		$toolbarClass = $this->container->getNamespacePrefix('inverse') . 'TransparentAuthentication\\TransparentAuthentication';

		try
		{
			return $this->createTransparentAuthentication($toolbarClass, $config);
		}
		catch (TransparentAuthenticationNotFound $e)
		{
			// Not found. Return the magically created TransparentAuthentication
			$magic = new TransparentAuthenticationFactory($this->container);
			// Let's pass the section override (if any)
			$magic->setSection($this->getSection());

			return $magic->make($config);
		}
	}
}
Factory/MagicFactory.php000060400000010732152455514240011242 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory;

defined('_JEXEC') || die;

use FOF30\Controller\Controller;
use FOF30\Factory\Exception\ControllerNotFound;
use FOF30\Factory\Exception\DispatcherNotFound;
use FOF30\Factory\Exception\ModelNotFound;
use FOF30\Factory\Exception\TransparentAuthenticationNotFound;
use FOF30\Factory\Exception\ViewNotFound;
use FOF30\Factory\Magic\DispatcherFactory;
use FOF30\Factory\Magic\TransparentAuthenticationFactory;
use FOF30\Model\Model;
use FOF30\Toolbar\Toolbar;
use FOF30\TransparentAuthentication\TransparentAuthentication;
use FOF30\View\View;

/**
 * Magic MVC object factory. This factory will "magically" create MVC objects even if the respective classes do not
 * exist, based on information in your fof.xml file.
 *
 * Note: This factory class will ONLY look for MVC objects in the same component section (front-end, back-end) you are
 * currently running in. If they are not found a new one will be created magically.
 */
class MagicFactory extends BasicFactory implements FactoryInterface
{
	/**
	 * Create a new Controller object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Controller for.
	 * @param   array   $config    Optional MVC configuration values for the Controller object.
	 *
	 * @return  Controller
	 */
	public function controller($viewName, array $config = [])
	{
		try
		{
			return parent::controller($viewName, $config);
		}
		catch (ControllerNotFound $e)
		{
			$magic = new Magic\ControllerFactory($this->container);

			return $magic->make($viewName, $config);
		}
	}

	/**
	 * Create a new Model object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Model for.
	 * @param   array   $config    Optional MVC configuration values for the Model object.
	 *
	 * @return  Model
	 */
	public function model($viewName, array $config = [])
	{
		try
		{
			return parent::model($viewName, $config);
		}
		catch (ModelNotFound $e)
		{
			$magic = new Magic\ModelFactory($this->container);

			return $magic->make($viewName, $config);
		}
	}

	/**
	 * Create a new View object
	 *
	 * @param   string  $viewName  The name of the view we're getting a View object for.
	 * @param   string  $viewType  The type of the View object. By default it's "html".
	 * @param   array   $config    Optional MVC configuration values for the View object.
	 *
	 * @return  View
	 */
	public function view($viewName, $viewType = 'html', array $config = [])
	{
		try
		{
			return parent::view($viewName, $viewType, $config);
		}
		catch (ViewNotFound $e)
		{
			$magic = new Magic\ViewFactory($this->container);

			return $magic->make($viewName, $viewType, $config);
		}
	}

	/**
	 * Creates a new Toolbar
	 *
	 * @param   array  $config  The configuration values for the Toolbar object
	 *
	 * @return  Toolbar
	 */
	public function toolbar(array $config = [])
	{
		$appConfig = $this->container->appConfig;

		$defaultConfig = [
			'useConfigurationFile' => true,
			'renderFrontendButtons' => in_array($appConfig->get("views.*.config.renderFrontendButtons"), [
				true, 'true', 'yes', 'on', 1,
			]),
			'renderFrontendSubmenu' => in_array($appConfig->get("views.*.config.renderFrontendSubmenu"), [
				true, 'true', 'yes', 'on', 1,
			]),
		];

		$config = array_merge($defaultConfig, $config);

		return parent::toolbar($config);
	}

	public function dispatcher(array $config = [])
	{
		$dispatcherClass = $this->container->getNamespacePrefix() . 'Dispatcher\\Dispatcher';

		try
		{
			return $this->createDispatcher($dispatcherClass, $config);
		}
		catch (DispatcherNotFound $e)
		{
			// Not found. Return the magically created Dispatcher
			$magic = new DispatcherFactory($this->container);

			return $magic->make($config);
		}
	}

	/**
	 * Creates a new TransparentAuthentication handler
	 *
	 * @param   array  $config  The configuration values for the TransparentAuthentication object
	 *
	 * @return  TransparentAuthentication
	 */
	public function transparentAuthentication(array $config = [])
	{
		$authClass = $this->container->getNamespacePrefix() . 'TransparentAuthentication\\TransparentAuthentication';

		try
		{
			return $this->createTransparentAuthentication($authClass, $config);
		}
		catch (TransparentAuthenticationNotFound $e)
		{
			// Not found. Return the magically created TA
			$magic = new TransparentAuthenticationFactory($this->container);

			return $magic->make($config);
		}
	}
}
Factory/Magic/TransparentAuthenticationFactory.php000060400000002272152455514240016443 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Magic;

defined('_JEXEC') || die;

use FOF30\Dispatcher\Dispatcher;

/**
 * Creates a TransparentAuthentication object instance based on the information provided by the fof.xml configuration
 * file
 */
class TransparentAuthenticationFactory extends BaseFactory
{
	/**
	 * Create a new object instance
	 *
	 * @param   array  $config  The config parameters which override the fof.xml information
	 *
	 * @return  Dispatcher  A new Dispatcher object
	 */
	public function make(array $config = [])
	{
		$appConfig     = $this->container->appConfig;
		$defaultConfig = $appConfig->get('authentication.*');
		$config        = array_merge($defaultConfig, $config);

		$className = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\DefaultTransparentAuthentication';

		if (!class_exists($className, true))
		{
			$className = '\\FOF30\\TransparentAuthentication\\TransparentAuthentication';
		}

		$dispatcher = new $className($this->container, $config);

		return $dispatcher;
	}
}
Factory/Magic/ControllerFactory.php000060400000004214152455514240013363 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Magic;

defined('_JEXEC') || die;

use FOF30\Controller\DataController;
use FOF30\Factory\Exception\ControllerNotFound;

/**
 * Creates a DataController object instance based on the information provided by the fof.xml configuration file
 */
class ControllerFactory extends BaseFactory
{
	/**
	 * Create a new object instance
	 *
	 * @param   string  $name    The name of the class we're making
	 * @param   array   $config  The config parameters which override the fof.xml information
	 *
	 * @return  DataController  A new DataController object
	 */
	public function make($name = null, array $config = [])
	{
		if (empty($name))
		{
			throw new ControllerNotFound($name);
		}

		$appConfig = $this->container->appConfig;
		$name      = ucfirst($name);

		$defaultConfig = [
			'name'           => $name,
			'default_task'   => $appConfig->get("views.$name.config.default_task", 'main'),
			'autoRouting'    => $appConfig->get("views.$name.config.autoRouting", 1),
			'csrfProtection' => $appConfig->get("views.$name.config.csrfProtection", 2),
			'viewName'       => $appConfig->get("views.$name.config.viewName", null),
			'modelName'      => $appConfig->get("views.$name.config.modelName", null),
			'taskPrivileges' => $appConfig->get("views.$name.acl"),
			'cacheableTasks' => $appConfig->get("views.$name.config.cacheableTasks", [
				'browse',
				'read',
			]),
			'taskMap'        => $appConfig->get("views.$name.taskmap"),
		];

		$config = array_merge($defaultConfig, $config);

		$className = $this->container->getNamespacePrefix($this->getSection()) . 'Controller\\DefaultDataController';

		if (!class_exists($className, true))
		{
			$className = 'FOF30\\Controller\\DataController';
		}

		$controller = new $className($this->container, $config);

		$taskMap = $config['taskMap'];

		if (is_array($taskMap) && !empty($taskMap))
		{
			foreach ($taskMap as $virtualTask => $method)
			{
				$controller->registerTask($virtualTask, $method);
			}
		}

		return $controller;
	}
}
Factory/Magic/ModelFactory.php000060400000005514152455514240012304 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Magic;

defined('_JEXEC') || die;

use FOF30\Factory\Exception\ModelNotFound;
use FOF30\Model\DataModel;
use FOF30\Model\TreeModel;

/**
 * Creates a DataModel/TreeModel object instance based on the information provided by the fof.xml configuration file
 */
class ModelFactory extends BaseFactory
{
	/**
	 * Create a new object instance
	 *
	 * @param   string  $name    The name of the class we're making
	 * @param   array   $config  The config parameters which override the fof.xml information
	 *
	 * @return  TreeModel|DataModel  A new TreeModel or DataModel object
	 */
	public function make($name = null, array $config = [])
	{
		if (empty($name))
		{
			throw new ModelNotFound($name);
		}

		$appConfig = $this->container->appConfig;
		$name      = ucfirst($name);

		$defaultConfig = [
			'name'             => $name,
			'use_populate'     => $appConfig->get("models.$name.config.use_populate"),
			'ignore_request'   => $appConfig->get("models.$name.config.ignore_request"),
			'tableName'        => $appConfig->get("models.$name.config.tbl"),
			'idFieldName'      => $appConfig->get("models.$name.config.tbl_key"),
			'knownFields'      => $appConfig->get("models.$name.config.knownFields", null),
			'autoChecks'       => $appConfig->get("models.$name.config.autoChecks"),
			'contentType'      => $appConfig->get("models.$name.config.contentType"),
			'fieldsSkipChecks' => $appConfig->get("models.$name.config.fieldsSkipChecks", []),
			'aliasFields'      => $appConfig->get("models.$name.field", []),
			'behaviours'       => $appConfig->get("models.$name.behaviors", []),
			'fillable_fields'  => $appConfig->get("models.$name.config.fillable_fields", []),
			'guarded_fields'   => $appConfig->get("models.$name.config.guarded_fields", []),
			'relations'        => $appConfig->get("models.$name.relations", []),
		];

		$config = array_merge($defaultConfig, $config);

		// Get the default class names
		$dataModelClassName = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\DefaultDataModel';

		if (!class_exists($dataModelClassName, true))
		{
			$dataModelClassName = '\\FOF30\\Model\\DataModel';
		}

		$treeModelClassName = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\DefaultTreeModel';

		if (!class_exists($treeModelClassName, true))
		{
			$treeModelClassName = '\\FOF30\\Model\\TreeModel';
		}

		try
		{
			// First try creating a TreeModel
			$model = new $treeModelClassName($this->container, $config);
		}
		catch (DataModel\Exception\TreeIncompatibleTable $e)
		{
			// If the table isn't a nested set, create a regular DataModel
			$model = new $dataModelClassName($this->container, $config);
		}

		return $model;
	}
}
Factory/Magic/ViewFactory.php000060400000004023152455514240012150 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Magic;

defined('_JEXEC') || die;

use FOF30\Factory\Exception\ViewNotFound;
use FOF30\View\DataView\DataViewInterface;

/**
 * Creates a DataModel/TreeModel object instance based on the information provided by the fof.xml configuration file
 */
class ViewFactory extends BaseFactory
{
	/**
	 * Create a new object instance
	 *
	 * @param   string  $name      The name of the class we're making
	 * @param   string  $viewType  The view type, default html, possible values html, form, raw, json, csv
	 * @param   array   $config    The config parameters which override the fof.xml information
	 *
	 * @return  DataViewInterface  A new TreeModel or DataModel object
	 */
	public function make($name = null, $viewType = 'html', array $config = [])
	{
		if (empty($name))
		{
			throw new ViewNotFound("[name : type] = [$name : $viewType]");
		}

		$appConfig = $this->container->appConfig;
		$name      = ucfirst($name);

		$defaultConfig = [
			'name'          => $name,
			'template_path' => $appConfig->get("views.$name.config.template_path"),
			'layout'        => $appConfig->get("views.$name.config.layout"),
			// You can pass something like .php => Class1, .foo.bar => Class 2
			'viewEngineMap' => $appConfig->get("views.$name.config.viewEngineMap"),
		];

		$config = array_merge($defaultConfig, $config);

		$className = $this->container->getNamespacePrefix($this->getSection()) . 'View\\DataView\\Default' . ucfirst($viewType);

		if (!class_exists($className, true))
		{
			$className = '\\FOF30\\View\\DataView\\' . ucfirst($viewType);
		}

		if (!class_exists($className, true))
		{
			$className = $this->container->getNamespacePrefix($this->getSection()) . 'View\\DataView\\DefaultHtml';
		}

		if (!class_exists($className))
		{
			$className = '\\FOF30\\View\\DataView\\Html';
		}

		$view = new $className($this->container, $config);

		return $view;
	}
}
Factory/Magic/BaseFactory.php000060400000001616152455514240012115 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Magic;

defined('_JEXEC') || die;

use FOF30\Container\Container;

abstract class BaseFactory
{
	/**
	 * @var   Container|null  The container where this factory belongs to
	 */
	protected $container = null;

	/**
	 * Section used to build the namespace prefix.
	 *
	 * @var   string
	 */
	protected $section = 'auto';

	/**
	 * Public constructor
	 *
	 * @param   Container  $container  The container we belong to
	 */
	public function __construct(Container $container)
	{
		$this->container = $container;
	}

	/**
	 * @return string
	 */
	public function getSection()
	{
		return $this->section;
	}

	/**
	 * @param   string  $section
	 */
	public function setSection($section)
	{
		$this->section = $section;
	}
}
Factory/Magic/DispatcherFactory.php000060400000002131152455514240013322 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory\Magic;

defined('_JEXEC') || die;

use FOF30\Dispatcher\Dispatcher;

/**
 * Creates a Dispatcher object instance based on the information provided by the fof.xml configuration file
 */
class DispatcherFactory extends BaseFactory
{
	/**
	 * Create a new object instance
	 *
	 * @param   array  $config  The config parameters which override the fof.xml information
	 *
	 * @return  Dispatcher  A new Dispatcher object
	 */
	public function make(array $config = [])
	{
		$appConfig     = $this->container->appConfig;
		$defaultConfig = $appConfig->get('dispatcher.*');
		$config        = array_merge($defaultConfig, $config);

		$className = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\DefaultDispatcher';

		if (!class_exists($className, true))
		{
			$className = '\\FOF30\\Dispatcher\\Dispatcher';
		}

		$dispatcher = new $className($this->container, $config);

		return $dispatcher;
	}
}
Factory/BasicFactory.php000060400000031167152455514240011250 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Factory;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Controller\Controller;
use FOF30\Dispatcher\Dispatcher;
use FOF30\Factory\Exception\ControllerNotFound;
use FOF30\Factory\Exception\DispatcherNotFound;
use FOF30\Factory\Exception\ModelNotFound;
use FOF30\Factory\Exception\ToolbarNotFound;
use FOF30\Factory\Exception\TransparentAuthenticationNotFound;
use FOF30\Factory\Exception\ViewNotFound;
use FOF30\Model\Model;
use FOF30\Toolbar\Toolbar;
use FOF30\TransparentAuthentication\TransparentAuthentication;
use FOF30\View\View;
use FOF30\View\ViewTemplateFinder;
use RuntimeException;

/**
 * MVC object factory. This implements the basic functionality, i.e. creating MVC objects only if the classes exist in
 * the same component section (front-end, back-end) you are currently running in. The Dispatcher and Toolbar will be
 * created from default objects if specialised classes are not found in your application.
 */
class BasicFactory implements FactoryInterface
{
	/** @var  Container  The container we belong to */
	protected $container = null;

	/**
	 * Section used to build the namespace prefix.
	 *
	 * @var   string
	 */
	protected $section = 'auto';

	/**
	 * Public constructor for the factory object
	 *
	 * @param   Container  $container  The container we belong to
	 */
	public function __construct(Container $container)
	{
		$this->container = $container;
	}

	/**
	 * Create a new Controller object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Controller for.
	 * @param   array   $config    Optional MVC configuration values for the Controller object.
	 *
	 * @return  Controller
	 */
	public function controller($viewName, array $config = [])
	{
		$controllerClass = $this->container->getNamespacePrefix($this->getSection()) . 'Controller\\' . ucfirst($viewName);

		try
		{
			return $this->createController($controllerClass, $config);
		}
		catch (ControllerNotFound $e)
		{
		}

		$controllerClass = $this->container->getNamespacePrefix($this->getSection()) . 'Controller\\' . ucfirst($this->container->inflector->singularize($viewName));

		$controller = $this->createController($controllerClass, $config);

		return $controller;
	}

	/**
	 * Create a new Model object
	 *
	 * @param   string  $viewName  The name of the view we're getting a Model for.
	 * @param   array   $config    Optional MVC configuration values for the Model object.
	 *
	 * @return  Model
	 */
	public function model($viewName, array $config = [])
	{
		$modelClass = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\' . ucfirst($viewName);

		try
		{
			return $this->createModel($modelClass, $config);
		}
		catch (ModelNotFound $e)
		{
		}

		$modelClass = $this->container->getNamespacePrefix($this->getSection()) . 'Model\\' . ucfirst($this->container->inflector->singularize($viewName));

		$model = $this->createModel($modelClass, $config);

		return $model;
	}

	/**
	 * Create a new View object
	 *
	 * @param   string  $viewName  The name of the view we're getting a View object for.
	 * @param   string  $viewType  The type of the View object. By default it's "html".
	 * @param   array   $config    Optional MVC configuration values for the View object.
	 *
	 * @return  View
	 */
	public function view($viewName, $viewType = 'html', array $config = [])
	{
		$container = $this->container;
		$prefix    = $this->container->getNamespacePrefix($this->getSection());

		$viewClass = $prefix . 'View\\' . ucfirst($viewName) . '\\' . ucfirst($viewType);

		try
		{
			return $this->createView($viewClass, $config);
		}
		catch (ViewNotFound $e)
		{
		}

		$viewClass = $prefix . 'View\\' . ucfirst($container->inflector->singularize($viewName)) . '\\' . ucfirst($viewType);

		$view = $this->createView($viewClass, $config);

		return $view;
	}

	/**
	 * Creates a new Dispatcher
	 *
	 * @param   array  $config  The configuration values for the Dispatcher object
	 *
	 * @return  Dispatcher
	 */
	public function dispatcher(array $config = [])
	{
		$dispatcherClass = $this->container->getNamespacePrefix($this->getSection()) . 'Dispatcher\\Dispatcher';

		try
		{
			return $this->createDispatcher($dispatcherClass, $config);
		}
		catch (DispatcherNotFound $e)
		{
			// Not found. Return the default Dispatcher
			return new Dispatcher($this->container, $config);
		}
	}

	/**
	 * Creates a new Toolbar
	 *
	 * @param   array  $config  The configuration values for the Toolbar object
	 *
	 * @return  Toolbar
	 */
	public function toolbar(array $config = [])
	{
		$toolbarClass = $this->container->getNamespacePrefix($this->getSection()) . 'Toolbar\\Toolbar';

		try
		{
			return $this->createToolbar($toolbarClass, $config);
		}
		catch (ToolbarNotFound $e)
		{
			// Not found. Return the default Toolbar
			return new Toolbar($this->container, $config);
		}
	}

	/**
	 * Creates a new TransparentAuthentication handler
	 *
	 * @param   array  $config  The configuration values for the TransparentAuthentication object
	 *
	 * @return  TransparentAuthentication
	 */
	public function transparentAuthentication(array $config = [])
	{
		$authClass = $this->container->getNamespacePrefix($this->getSection()) . 'TransparentAuthentication\\TransparentAuthentication';

		try
		{
			return $this->createTransparentAuthentication($authClass, $config);
		}
		catch (TransparentAuthenticationNotFound $e)
		{
			// Not found. Return the default TA
			return new TransparentAuthentication($this->container, $config);
		}
	}

	/**
	 * Creates a view template finder object for a specific View
	 *
	 * The default configuration is:
	 * Look for .php, .blade.php files; default layout "default"; no default sub-template;
	 * look only for the specified view; do NOT fall back to the default layout or sub-template;
	 * look for templates ONLY in site or admin, depending on where we're running from
	 *
	 * @param   View   $view    The view this view template finder will be attached to
	 * @param   array  $config  Configuration variables for the object
	 *
	 * @return  ViewTemplateFinder
	 *
	 * @throws Exception
	 */
	public function viewFinder(View $view, array $config = [])
	{
		// Initialise the configuration with the default values
		$defaultConfig = [
			'extensions'    => ['.php', '.blade.php'],
			'defaultLayout' => 'default',
			'defaultTpl'    => '',
			'strictView'    => true,
			'strictTpl'     => true,
			'strictLayout'  => true,
			'sidePrefix'    => 'auto',
		];

		$config = array_merge($defaultConfig, $config);

		// Apply fof.xml overrides
		$appConfig = $this->container->appConfig;
		$key       = "views." . ucfirst($view->getName()) . ".config";

		$fofXmlConfig = [
			'extensions'   => $appConfig->get("$key.templateExtensions", $config['extensions']),
			'strictView'   => $appConfig->get("$key.templateStrictView", $config['strictView']),
			'strictTpl'    => $appConfig->get("$key.templateStrictTpl", $config['strictTpl']),
			'strictLayout' => $appConfig->get("$key.templateStrictLayout", $config['strictLayout']),
			'sidePrefix'   => $appConfig->get("$key.templateLocation", $config['sidePrefix']),
		];

		$config = array_merge($config, $fofXmlConfig);

		// Create the new view template finder object
		return new ViewTemplateFinder($view, $config);
	}

	/**
	 * @return string
	 */
	public function getSection()
	{
		return $this->section;
	}

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

	/**
	 * Is scaffolding enabled?
	 *
	 * @return  bool
	 *
	 * @deprecated  3.6.0  Always returns false
	 */
	public function isScaffolding()
	{
		return false;
	}

	/**
	 * Set the scaffolding status
	 *
	 * @param   bool  $scaffolding
	 *
	 * @deprecated  Removed since 3.6.0, does nothing
	 */
	public function setScaffolding($scaffolding): void
	{
		// Ignored
	}

	/**
	 * Is saving the scaffolding result to disk enabled?
	 *
	 * @return   bool
	 *
	 * @deprecated  3.6.0  Always returns false
	 */
	public function isSaveScaffolding()
	{
		return false;
	}

	/**
	 * Set the status of saving the scaffolding result to disk.
	 *
	 * @param   bool  $saveScaffolding
	 *
	 * @deprecated  3.6.0  Does nothing
	 */
	public function setSaveScaffolding($saveScaffolding)
	{
		// Ignored
	}

	/**
	 * Should we save controller to disk?
	 *
	 * @param   bool  $state
	 *
	 * @deprecated  3.6.0  Does nothing
	 */
	public function setSaveControllerScaffolding($state)
	{
		// Ignored
	}

	/**
	 * Should we save controller scaffolding to disk?
	 *
	 * @return  bool  $state
	 *
	 * @deprecated  3.6.0  Always returns false
	 */
	public function isSaveControllerScaffolding()
	{
		return false;
	}

	/**
	 * Should we save model to disk?
	 *
	 * @param   bool  $state
	 *
	 * @deprecated  3.6.0  Does nothing
	 */
	public function setSaveModelScaffolding($state)
	{
		// Ignored
	}

	/**
	 * Should we save model scaffolding to disk?
	 *
	 * @return  bool  $state
	 *
	 * @deprecated  3.6.0  Always returns false
	 */
	public function isSaveModelScaffolding()
	{
		return false;
	}

	/**
	 * Should we save view to disk?
	 *
	 * @param   bool  $state
	 *
	 * @deprecated  3.6.0  Does nothing
	 */
	public function setSaveViewScaffolding($state)
	{
		// Ignored
	}

	/**
	 * Should we save view scaffolding to disk?
	 *
	 * @return  bool  $state
	 *
	 * @deprecated  3.6.0  Always returns false
	 */
	public function isSaveViewScaffolding()
	{
		return false;
	}

	/**
	 * Creates a Controller object
	 *
	 * @param   string  $controllerClass  The fully qualified class name for the Controller
	 * @param   array   $config           Optional MVC configuration values for the Controller object.
	 *
	 * @return  Controller
	 *
	 * @throws  RuntimeException  If the $controllerClass does not exist
	 */
	protected function createController($controllerClass, array $config = [])
	{
		if (!class_exists($controllerClass))
		{
			throw new ControllerNotFound($controllerClass);
		}

		return new $controllerClass($this->container, $config);
	}

	/**
	 * Creates a Model object
	 *
	 * @param   string  $modelClass  The fully qualified class name for the Model
	 * @param   array   $config      Optional MVC configuration values for the Model object.
	 *
	 * @return  Model
	 *
	 * @throws  RuntimeException  If the $modelClass does not exist
	 */
	protected function createModel($modelClass, array $config = [])
	{
		if (!class_exists($modelClass))
		{
			throw new ModelNotFound($modelClass);
		}

		return new $modelClass($this->container, $config);
	}

	/**
	 * Creates a View object
	 *
	 * @param   string  $viewClass  The fully qualified class name for the View
	 * @param   array   $config     Optional MVC configuration values for the View object.
	 *
	 * @return  View
	 *
	 * @throws  RuntimeException  If the $viewClass does not exist
	 */
	protected function createView($viewClass, array $config = [])
	{
		if (!class_exists($viewClass))
		{
			throw new ViewNotFound($viewClass);
		}

		return new $viewClass($this->container, $config);
	}

	/**
	 * Creates a Toolbar object
	 *
	 * @param   string  $toolbarClass  The fully qualified class name for the Toolbar
	 * @param   array   $config        The configuration values for the Toolbar object
	 *
	 * @return  Toolbar
	 *
	 * @throws  RuntimeException  If the $toolbarClass does not exist
	 */
	protected function createToolbar($toolbarClass, array $config = [])
	{
		if (!class_exists($toolbarClass))
		{
			throw new ToolbarNotFound($toolbarClass);
		}

		return new $toolbarClass($this->container, $config);
	}

	/**
	 * Creates a Dispatcher object
	 *
	 * @param   string  $dispatcherClass  The fully qualified class name for the Dispatcher
	 * @param   array   $config           The configuration values for the Dispatcher object
	 *
	 * @return  Dispatcher
	 *
	 * @throws  RuntimeException  If the $dispatcherClass does not exist
	 */
	protected function createDispatcher($dispatcherClass, array $config = [])
	{
		if (!class_exists($dispatcherClass))
		{
			throw new DispatcherNotFound($dispatcherClass);
		}

		return new $dispatcherClass($this->container, $config);
	}

	/**
	 * Creates a TransparentAuthentication object
	 *
	 * @param   string  $authClass  The fully qualified class name for the TransparentAuthentication
	 * @param   array   $config     The configuration values for the TransparentAuthentication object
	 *
	 * @return  TransparentAuthentication
	 *
	 * @throws  RuntimeException  If the $authClass does not exist
	 */
	protected function createTransparentAuthentication($authClass, $config)
	{
		if (!class_exists($authClass))
		{
			throw new TransparentAuthenticationNotFound($authClass);
		}

		return new $authClass($this->container, $config);
	}
}
TransparentAuthentication/TransparentAuthentication.php000060400000031154152455514240017666 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\TransparentAuthentication;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Encrypt\Aes;
use FOF30\Encrypt\Totp;

/**
 * Retrieves the values for transparent authentication from the request
 */
class TransparentAuthentication
{
	/** Use HTTP Basic Authentication with time-based one time passwords */
	public const Auth_HTTPBasicAuth_TOTP = 1;

	/** Use Query String Parameter authentication with time-based one time passwords */
	public const Auth_QueryString_TOTP = 2;

	/** Use HTTP Basic Authentication with plain text username and password */
	public const Auth_HTTPBasicAuth_Plaintext = 3;

	/** Use single query string parameter authentication with JSON-encoded, plain text username and password */
	public const Auth_QueryString_Plaintext = 4;

	/** Use two query string parameters for plain text username and password authentication */
	public const Auth_SplitQueryString_Plaintext = 5;

	/** @var int The time step for TOTP authentication */
	protected $timeStep = 6;

	/** @var string The TOTP secret key */
	protected $totpKey = '';
	/** @var array Enabled authentication methods, see the class constants */
	protected $authenticationMethods = [3, 4, 5];
	/** @var string The username required for the Auth_HTTPBasicAuth_TOTP method */
	protected $basicAuthUsername = '_fof_auth';
	/** @var string The query parameter for the Auth_QueryString_Plaintext method */
	protected $queryParam = '_fofauthentication';
	/** @var string The query parameter for the username in the Auth_SplitQueryString_Plaintext method */
	protected $queryParamUsername = '_fofusername';
	/** @var string The query parameter for the password in the Auth_SplitQueryString_Plaintext method */
	protected $queryParamPassword = '_fofpassword';
	/** @var  bool  Should I log out the user after the dispatcher exits? */
	protected $logoutOnExit = true;
	/** @var Container The container we are attached to */
	protected $container = null;
	/** @var string Internal variable */
	private $cryptoKey = '';

	/**
	 * Public constructor.
	 *
	 * The optional $config array can contain the following values (corresponding to the same-named properties of this
	 * class): timeStep, totpKey, cryptoKey, basicAuthUsername, queryParam, queryParamUsername, queryParamPassword,
	 * logoutOnExit. See the property descriptions for more information.
	 *
	 * @param   Container  $container
	 * @param   array      $config
	 */
	function __construct(Container $container, array $config = [])
	{
		$this->container = $container;

		// Initialise from the $config array
		$knownKeys = [
			'timeStep', 'totpKey', 'cryptoKey', 'basicAuthUsername', 'queryParam', 'queryParamUsername',
			'queryParamPassword', 'logoutOnExit',
		];

		foreach ($knownKeys as $key)
		{
			if (isset($config[$key]))
			{
				$this->$key = $config[$key];
			}
		}

		if (isset($config['authenticationMethods']))
		{
			$this->authenticationMethods = $this->parseAuthenticationMethods($config['authenticationMethods']);
		}
	}

	/**
	 * Get the enabled authentication methods
	 *
	 * @return   array
	 *
	 * @codeCoverageIgnore
	 */
	public function getAuthenticationMethods()
	{
		return $this->authenticationMethods;
	}

	/**
	 * Set the enabled authentication methods
	 *
	 * @param   array  $authenticationMethods
	 *
	 * @codeCoverageIgnore
	 */
	public function setAuthenticationMethods($authenticationMethods)
	{
		$this->authenticationMethods = $authenticationMethods;
	}

	/**
	 * Enable an authentication method
	 *
	 * @param   integer  $method
	 */
	public function addAuthenticationMethod($method)
	{
		if (!in_array($method, $this->authenticationMethods))
		{
			$this->authenticationMethods[] = $method;
		}
	}

	/**
	 * Disable an authentication method
	 *
	 * @param   integer  $method
	 */
	public function removeAuthenticationMethod($method)
	{
		if (in_array($method, $this->authenticationMethods))
		{
			$key = array_search($method, $this->authenticationMethods);
			unset($this->authenticationMethods[$key]);
		}
	}

	/**
	 * Get the required username for the HTTP Basic Authentication with TOTP method
	 *
	 * @return string
	 *
	 * @codeCoverageIgnore
	 */
	public function getBasicAuthUsername()
	{
		return $this->basicAuthUsername;
	}

	/**
	 * Set the required username for the HTTP Basic Authentication with TOTP method
	 *
	 * @param   string  $basicAuthUsername
	 *
	 * @codeCoverageIgnore
	 */
	public function setBasicAuthUsername($basicAuthUsername)
	{
		$this->basicAuthUsername = $basicAuthUsername;
	}

	/**
	 * Get the query parameter for the Auth_QueryString_TOTP method
	 *
	 * @return string
	 *
	 * @codeCoverageIgnore
	 */
	public function getQueryParam()
	{
		return $this->queryParam;
	}

	/**
	 * Set the query parameter for the Auth_QueryString_TOTP method
	 *
	 * @param   string  $queryParam
	 *
	 * @codeCoverageIgnore
	 */
	public function setQueryParam($queryParam)
	{
		$this->queryParam = $queryParam;
	}

	/**
	 * Get the query string for the password in the Auth_SplitQueryString_Plaintext method
	 *
	 * @return string
	 *
	 * @codeCoverageIgnore
	 */
	public function getQueryParamPassword()
	{
		return $this->queryParamPassword;
	}

	/**
	 * Set the query string for the password in the Auth_SplitQueryString_Plaintext method
	 *
	 * @param   string  $queryParamPassword
	 *
	 * @codeCoverageIgnore
	 */
	public function setQueryParamPassword($queryParamPassword)
	{
		$this->queryParamPassword = $queryParamPassword;
	}

	/**
	 * Get the query string for the username in the Auth_SplitQueryString_Plaintext method
	 *
	 * @return string
	 *
	 * @codeCoverageIgnore
	 */
	public function getQueryParamUsername()
	{
		return $this->queryParamUsername;
	}

	/**
	 * Set the query string for the username in the Auth_SplitQueryString_Plaintext method
	 *
	 * @param   string  $queryParamUsername
	 *
	 * @codeCoverageIgnore
	 */
	public function setQueryParamUsername($queryParamUsername)
	{
		$this->queryParamUsername = $queryParamUsername;
	}

	/**
	 * Get the time step in seconds for the TOTP in the Auth_HTTPBasicAuth_TOTP method
	 *
	 * @return int
	 *
	 * @codeCoverageIgnore
	 */
	public function getTimeStep()
	{
		return $this->timeStep;
	}

	/**
	 * Set the time step in seconds for the TOTP in the Auth_HTTPBasicAuth_TOTP method
	 *
	 * @param   int  $timeStep
	 *
	 * @codeCoverageIgnore
	 */
	public function setTimeStep($timeStep)
	{
		$this->timeStep = (int) $timeStep;
	}

	/**
	 * Get the secret key for the TOTP in the Auth_HTTPBasicAuth_TOTP method
	 *
	 * @return string
	 *
	 * @codeCoverageIgnore
	 */
	public function getTotpKey()
	{
		return $this->totpKey;
	}

	/**
	 * Set the secret key for the TOTP in the Auth_HTTPBasicAuth_TOTP method
	 *
	 * @param   string  $totpKey
	 *
	 * @codeCoverageIgnore
	 */
	public function setTotpKey($totpKey)
	{
		$this->totpKey = $totpKey;
	}

	/**
	 * Should I log out when the dispatcher finishes?
	 *
	 * @return boolean
	 *
	 * @codeCoverageIgnore
	 */
	public function getLogoutOnExit()
	{
		return $this->logoutOnExit;
	}

	/**
	 * Set the log out on exit flag (for testing)
	 *
	 * @param   boolean  $logoutOnExit
	 *
	 * @codeCoverageIgnore
	 */
	public function setLogoutOnExit($logoutOnExit)
	{
		$this->logoutOnExit = $logoutOnExit;
	}

	/**
	 * Tries to get the transparent authentication credentials from the request
	 *
	 * @return  array|null
	 */
	public function getTransparentAuthenticationCredentials()
	{
		$return = null;

		// Always run onFOFGetTransparentAuthenticationCredentials. These methods take precedence over anything else.
		$this->container->platform->importPlugin('user');
		$this->container->platform->importPlugin('fof');
		$pluginResults = $this->container->platform->runPlugins('onFOFGetTransparentAuthenticationCredentials', [$this->container]);

		foreach ($pluginResults as $result)
		{
			if (empty($result))
			{
				continue;
			}

			if (is_array($result))
			{
				return $result;
			}
		}

		// Make sure there are enabled transparent authentication methods
		if (empty($this->authenticationMethods))
		{
			return $return;
		}

		$input = $this->container->input;

		foreach ($this->authenticationMethods as $method)
		{
			switch ($method)
			{
				case self::Auth_HTTPBasicAuth_TOTP:
					if (empty($this->totpKey))
					{
						continue 2;
					}

					if (empty($this->basicAuthUsername))
					{
						continue 2;
					}

					if (!isset($_SERVER['PHP_AUTH_USER']))
					{
						continue 2;
					}

					if (!isset($_SERVER['PHP_AUTH_PW']))
					{
						continue 2;
					}

					if ($_SERVER['PHP_AUTH_USER'] != $this->basicAuthUsername)
					{
						continue 2;
					}

					$encryptedData = $_SERVER['PHP_AUTH_PW'];

					return $this->decryptWithTOTP($encryptedData);

					break;

				case self::Auth_QueryString_TOTP:
					if (empty($this->queryParam))
					{
						continue 2;
					}

					$encryptedData = $input->get($this->queryParam, '', 'raw');

					if (empty($encryptedData))
					{
						continue 2;
					}

					$return = $this->decryptWithTOTP($encryptedData);

					if (!is_null($return))
					{
						return $return;
					}

					break;

				case self::Auth_HTTPBasicAuth_Plaintext:
					if (!isset($_SERVER['PHP_AUTH_USER']))
					{
						continue 2;
					}

					if (!isset($_SERVER['PHP_AUTH_PW']))
					{
						continue 2;
					}

					return [
						'username' => $_SERVER['PHP_AUTH_USER'],
						'password' => $_SERVER['PHP_AUTH_PW'],
					];

					break;

				case self::Auth_QueryString_Plaintext:
					if (empty($this->queryParam))
					{
						continue 2;
					}

					$jsonEncoded = $input->get($this->queryParam, '', 'raw');

					if (empty($jsonEncoded))
					{
						continue 2;
					}

					$authInfo = json_decode($jsonEncoded, true);

					if (!is_array($authInfo))
					{
						continue 2;
					}

					if (!array_key_exists('username', $authInfo) || !array_key_exists('password', $authInfo))
					{
						continue 2;
					}

					return $authInfo;

					break;

				case self::Auth_SplitQueryString_Plaintext:
					if (empty($this->queryParamUsername))
					{
						continue 2;
					}

					if (empty($this->queryParamPassword))
					{
						continue 2;
					}

					$username = $input->get($this->queryParamUsername, '', 'raw');
					$password = $input->get($this->queryParamPassword, '', 'raw');

					if (empty($username))
					{
						continue 2;
					}

					if (empty($password))
					{
						continue 2;
					}

					return [
						'username' => $username,
						'password' => $password,
					];

					break;
			}
		}

		return $return;
	}

	/**
	 * Parses a list of transparent authentication methods (array or comma separated list of integers or method names)
	 * and converts it into an array of integers this class understands.
	 *
	 * @param $methods
	 *
	 * @return array
	 */
	protected function parseAuthenticationMethods($methods)
	{
		if (empty($methods))
		{
			return [];
		}

		if (!is_array($methods))
		{
			$methods = explode(',', $methods);
		}

		$return = [];

		foreach ($methods as $method)
		{
			if (empty($method))
			{
				continue;
			}

			$method = trim($method);

			if ((int) $method == $method)
			{
				$return[] = (int) $method;
			}

			switch ($method)
			{
				case 'HTTPBasicAuth_TOTP':
					$return[] = 1;
					break;

				case 'QueryString_TOTP':
					$return[] = 2;
					break;

				case 'HTTPBasicAuth_Plaintext':
					$return[] = 3;
					break;

				case 'QueryString_Plaintext':
					$return[] = 4;
					break;

				case 'SplitQueryString_Plaintext':
					$return[] = 5;

			}
		}

		return $return;
	}

	/**
	 * Decrypts a transparent authentication message using a TOTP
	 *
	 * @param   string  $encryptedData  The encrypted data
	 *
	 * @return  array  The decrypted data
	 */
	private function decryptWithTOTP($encryptedData)
	{
		if (empty($this->totpKey))
		{
			$this->cryptoKey = null;

			return null;
		}

		$totp   = new Totp($this->timeStep);
		$period = $totp->getPeriod();
		$period--;

		for ($i = 0; $i <= 2; $i++)
		{
			$time            = ($period + $i) * $this->timeStep;
			$otp             = $totp->getCode($this->totpKey, $time);
			$this->cryptoKey = hash('sha256', $this->totpKey . $otp);

			$aes = new Aes($this->cryptoKey);
			try
			{
				$ret = $aes->decryptString($encryptedData);
			}
			catch (Exception $e)
			{
				continue;
			}
			$ret = rtrim($ret, "\000");

			$ret = json_decode($ret, true);

			if (!is_array($ret))
			{
				continue;
			}

			if (!array_key_exists('username', $ret))
			{
				continue;
			}

			if (!array_key_exists('password', $ret))
			{
				continue;
			}

			// Successful decryption!
			return $ret;
		}

		// Obviously if we're here we could not decrypt anything. Bail out.
		$this->cryptoKey = null;

		return null;
	}
}
Database/Installer.php000060400000056772152455514240010742 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Database;

defined('_JEXEC') || die;

use Exception;
use JDatabaseDriver;
use JLoader;
use Joomla\CMS\Filesystem\Folder;
use SimpleXMLElement;

class Installer
{
	/** @var array Internal cache for table list */
	protected static $allTables = [];
	/** @var  JDatabaseDriver  The database connector object */
	private $db = null;
	/** @var  string  The directory where the XML schema files are stored */
	private $xmlDirectory = null;
	/** @var  string  Force a specific **absolute** file path for the XML schema file */
	private $forcedFile = null;

	/**
	 * Public constructor
	 *
	 * @param   JDatabaseDriver  $db         The database driver we're going to use to install the tables
	 * @param   string           $directory  The directory holding the XML schema update files
	 *
	 * @param   array            $config     The configuration array
	 */
	public function __construct(JDatabaseDriver $db, $directory)
	{
		$this->db = $db;

		$this->xmlDirectory = $directory;
	}

	/**
	 * Returns the directory where XML schema files are stored
	 *
	 * @return  string
	 *
	 * @codeCoverageIgnore
	 */
	public function getXmlDirectory()
	{
		return $this->xmlDirectory;
	}

	/**
	 * Sets the directory where XML schema files are stored
	 *
	 * @param   string  $xmlDirectory
	 *
	 * @codeCoverageIgnore
	 */
	public function setXmlDirectory($xmlDirectory)
	{
		$this->xmlDirectory = $xmlDirectory;
	}

	/**
	 * Returns the absolute path to the forced XML schema file
	 *
	 * @return  string
	 *
	 * @codeCoverageIgnore
	 */
	public function getForcedFile()
	{
		return $this->forcedFile;
	}

	/**
	 * Sets the absolute path to an XML schema file which will be read no matter what. Set to a blank string to let the
	 * Installer class auto-detect your schema file based on your database type.
	 *
	 * @param   string  $forcedFile
	 *
	 * @codeCoverageIgnore
	 */
	public function setForcedFile($forcedFile)
	{
		$this->forcedFile = $forcedFile;
	}

	/**
	 * Clears the internal table list cache
	 *
	 * @return  void
	 */
	public function nukeCache()
	{
		static::$allTables = [];
	}

	/**
	 * Creates or updates the database schema
	 *
	 * @return  void
	 *
	 * @throws  Exception  When a database query fails and it doesn't have the canfail flag
	 */
	public function updateSchema()
	{
		// Get the schema XML file
		$xml = $this->findSchemaXml();

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

		// Make sure there are SQL commands in this file
		if (!$xml->sql)
		{
			return;
		}

		// Walk the sql > action tags to find all tables
		/** @var SimpleXMLElement $actions */
		$actions = $xml->sql->children();

		/**
		 * The meta/autocollation node defines if I should automatically apply the correct collation (utf8 or utf8mb4)
		 * to the database tables managed by the schema updater. When enabled (default) the queries are automatically
		 * converted to the correct collation (utf8mb4_unicode_ci or utf8_general_ci) depending on whether your Joomla!
		 * and MySQL server support Multibyte UTF-8 (UTF8MB4). Moreover, if UTF8MB4 is supported, all CREATE TABLE
		 * queries are analyzed and the tables referenced in them are auto-converted to the proper utf8mb4 collation.
		 */
		$autoCollationConversion = true;

		if ($xml->meta->autocollation)
		{
			$value = (string) $xml->meta->autocollation;
			$value = trim($value);
			$value = strtolower($value);

			$autoCollationConversion = in_array($value, ['true', '1', 'on', 'yes']);
		}

		$hasUtf8mb4Support = method_exists($this->db, 'hasUTF8mb4Support') && $this->db->hasUTF8mb4Support();
		$tablesToConvert   = [];

		// If we have an uppercase db prefix we can expect CREATE TABLE fail because we cannot detect reliably
		// the existence of database tables. See https://github.com/joomla/joomla-cms/issues/10928#issuecomment-228549658
		$prefix             = $this->db->getPrefix();
		$canFailCreateTable = preg_match('/[A-Z]/', $prefix);

		/** @var SimpleXMLElement $action */
		foreach ($actions as $action)
		{
			// Get the attributes
			$attributes = $action->attributes();

			// Get the table / view name
			$table = $attributes->table ? (string) $attributes->table : '';

			if (empty($table))
			{
				continue;
			}

			// Am I allowed to let this action fail?
			$canFailAction = $attributes->canfail ?: 0;

			// Evaluate conditions
			$shouldExecute = true;

			/** @var SimpleXMLElement $node */
			foreach ($action->children() as $node)
			{
				if ($node->getName() == 'condition')
				{
					// Get the operator
					$operator = $node->attributes()->operator ? (string) $node->attributes()->operator : 'and';
					$operator = empty($operator) ? 'and' : $operator;

					$condition = $this->conditionMet($table, $node);

					switch ($operator)
					{
						case 'not':
							$shouldExecute = $shouldExecute && !$condition;
							break;

						case 'or':
							$shouldExecute = $shouldExecute || $condition;
							break;

						case 'nor':
							$shouldExecute = !$shouldExecute && !$condition;
							break;

						case 'xor':
							$shouldExecute = ($shouldExecute xor $condition);
							break;

						case 'maybe':
							$shouldExecute = $condition ? true : $shouldExecute;
							break;

						default:
							$shouldExecute = $shouldExecute && $condition;
							break;
					}
				}

				// DO NOT USE BOOLEAN SHORT CIRCUIT EVALUATION!
				// if (!$shouldExecute) break;
			}

			// Do I have to only collect the tables from CREATE TABLE queries?
			$onlyCollectTables = !$shouldExecute && $autoCollationConversion && $hasUtf8mb4Support;

			// Make sure all conditions are met OR I have to collect tables from CREATE TABLE queries.
			if (!$shouldExecute && !$onlyCollectTables)
			{
				continue;
			}

			// Execute queries
			foreach ($action->children() as $node)
			{
				if ($node->getName() == 'query')
				{
					$query = (string) $node;

					if ($autoCollationConversion && $hasUtf8mb4Support)
					{
						$this->extractTablesToConvert($query, $tablesToConvert);
					}

					// If we're only collecting tables do not run the queries
					if ($onlyCollectTables)
					{
						continue;
					}

					$canFail = $node->attributes->canfail ? (string) $node->attributes->canfail : $canFailAction;

					if (is_string($canFail))
					{
						$canFail = strtoupper($canFail);
					}

					$canFail = (in_array($canFail, [true, 1, 'YES', 'TRUE']));

					// Do I need to automatically convert the collation of all CREATE / ALTER queries?
					if ($autoCollationConversion)
					{
						if ($hasUtf8mb4Support)
						{
							// We have UTF8MB4 support. Convert all queries to UTF8MB4.
							$query = $this->convertUtf8QueryToUtf8mb4($query);
						}
						else
						{
							// We do not have UTF8MB4 support. Convert all queries to plain old UTF8.
							$query = $this->convertUtf8mb4QueryToUtf8($query);
						}
					}

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

					try
					{
						$this->db->execute();
					}
					catch (Exception $e)
					{
						// Special consideration for CREATE TABLE commands on uppercase prefix databases.
						if ($canFailCreateTable && stripos($query, 'CREATE TABLE') !== false)
						{
							$canFail = true;
						}

						// If we are not allowed to fail, throw back the exception we caught
						if (!$canFail)
						{
							throw $e;
						}
					}
				}
			}
		}

		// Auto-convert the collation of tables if we are told to do so, have utf8mb4 support and a list of tables.
		if ($autoCollationConversion && $hasUtf8mb4Support && !empty($tablesToConvert))
		{
			$this->convertTablesToUtf8mb4($tablesToConvert);
		}
	}

	/**
	 * Uninstalls the database schema
	 *
	 * @return  void
	 */
	public function removeSchema()
	{
		// Get the schema XML file
		$xml = $this->findSchemaXml();

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

		// Make sure there are SQL commands in this file
		if (!$xml->sql)
		{
			return;
		}

		// Walk the sql > action tags to find all tables
		$tables = [];
		/** @var SimpleXMLElement $actions */
		$actions = $xml->sql->children();

		/** @var SimpleXMLElement $action */
		foreach ($actions as $action)
		{
			$attributes = $action->attributes();
			$tables[]   = (string) $attributes->table;
		}

		// Simplify the tables list
		$tables = array_unique($tables);

		// Start dropping tables
		foreach ($tables as $table)
		{
			try
			{
				$this->db->dropTable($table);
			}
			catch (Exception $e)
			{
				// Do not fail if I can't drop the table
			}
		}
	}

	/**
	 * Find an suitable schema XML file for this database type and return the SimpleXMLElement holding its information
	 *
	 * @return  null|SimpleXMLElement  Null if no suitable schema XML file is found
	 */
	protected function findSchemaXml()
	{
		$xml = null;

		// Do we have a forced file?
		if ($this->forcedFile)
		{
			$xml = $this->openAndVerify($this->forcedFile);

			if ($xml !== false)
			{
				return $xml;
			}
		}

		// Get all XML files in the schema directory
		$xmlFiles = Folder::files($this->xmlDirectory, '\.xml$');

		if (empty($xmlFiles))
		{
			return $xml;
		}

		foreach ($xmlFiles as $baseName)
		{
			// Remove any accidental whitespace
			$baseName = trim($baseName);

			// Get the full path to the file
			$fileName = $this->xmlDirectory . '/' . $baseName;

			$xml = $this->openAndVerify($fileName);

			if ($xml !== false)
			{
				return $xml;
			}
		}

		return null;
	}

	/**
	 * Opens the schema XML file and return the SimpleXMLElement holding its information. If the file doesn't exist, it
	 * is not a schema file or it doesn't match our database driver we return boolean false.
	 *
	 * @return  false|SimpleXMLElement  False if it's not a suitable XML schema file
	 */
	protected function openAndVerify($fileName)
	{
		$driverType = $this->db->name;

		// Make sure the file exists
		if (!@file_exists($fileName))
		{
			return false;
		}

		// Make sure the file is a valid XML document
		try
		{
			$xml = new SimpleXMLElement($fileName, LIBXML_NONET, true);
		}
		catch (Exception $e)
		{
			$xml = null;

			return false;
		}

		// Make sure the file is an XML schema file
		if ($xml->getName() != 'schema')
		{
			$xml = null;

			return false;
		}

		if (!$xml->meta)
		{
			$xml = null;

			return false;
		}

		if (!$xml->meta->drivers)
		{
			$xml = null;

			return false;
		}

		/** @var SimpleXMLElement $drivers */
		$drivers = $xml->meta->drivers;

		foreach ($drivers->children() as $driverTypeTag)
		{
			$thisDriverType = (string) $driverTypeTag;

			if ($thisDriverType == $driverType)
			{
				return $xml;
			}
		}

		// Some custom database drivers use a non-standard $name variable. Let try a relaxed match.
		foreach ($drivers->children() as $driverTypeTag)
		{
			$thisDriverType = (string) $driverTypeTag;

			if (
				// e.g. $driverType = 'mysqlistupid', $thisDriverType = 'mysqli' => driver matched
				strpos($driverType, $thisDriverType) === 0
				// e.g. $driverType = 'stupidmysqli', $thisDriverType = 'mysqli' => driver matched
				|| (substr($driverType, -strlen($thisDriverType)) == $thisDriverType)
			)
			{
				return $xml;
			}
		}

		return false;
	}

	/**
	 * Checks if a condition is met
	 *
	 * @param   string            $table  The table we're operating on
	 * @param   SimpleXMLElement  $node   The condition definition node
	 *
	 * @return  bool
	 */
	protected function conditionMet($table, SimpleXMLElement $node)
	{
		if (empty(static::$allTables))
		{
			static::$allTables = $this->db->getTableList();
		}

		// Does the table exist?
		$tableNormal = $this->db->replacePrefix($table);
		$tableExists = in_array($tableNormal, static::$allTables);

		// Initialise
		$condition = false;

		// Get the condition's attributes
		$attributes = $node->attributes();
		$type       = $attributes->type ?: null;
		$value      = $attributes->value ? (string) $attributes->value : null;

		switch ($type)
		{
			// Check if a table or column is missing
			case 'missing':
				$fieldName = (string) $value;

				if (empty($fieldName))
				{
					$condition = !$tableExists;
				}
				else
				{
					try
					{
						$tableColumns = $this->db->getTableColumns($tableNormal, true);
					}
					catch (Exception $e)
					{
						$tableColumns = [];
					}

					$condition = !array_key_exists($fieldName, $tableColumns);
				}

				break;

			// Check if a column type matches the "coltype" attribute
			case 'type':
				try
				{
					$tableColumns = $this->db->getTableColumns($tableNormal, true);
				}
				catch (Exception $e)
				{
					$tableColumns = [];
				}

				$condition = false;

				if (array_key_exists($value, $tableColumns))
				{
					$coltype = $attributes->coltype ?: null;

					if (!empty($coltype))
					{
						$coltype     = strtolower($coltype);
						$currentType = is_string($tableColumns[$value]) ? $tableColumns[$value] : strtolower($tableColumns[$value]->Type);

						$condition = ($coltype == $currentType);
					}
				}

				break;

			// Check if a column is nullable
			case 'nullable':
				try
				{
					$tableColumns = $this->db->getTableColumns($tableNormal, true);
				}
				catch (Exception $e)
				{
					$tableColumns = [];
				}

				$condition = false;

				if (array_key_exists($value, $tableColumns))
				{
					$condition = (is_string($tableColumns[$value]) ? 'YES' : strtolower($tableColumns[$value]->Null)) == 'yes';
				}

				break;

			// Check if a (named) index exists on the table. Currently only supported on MySQL.
			case 'index':
				$indexName = (string) $value;
				$condition = true;

				if (!empty($indexName))
				{
					$indexName = str_replace('#__', $this->db->getPrefix(), $indexName);
					$condition = $this->hasIndex($tableNormal, $indexName);
				}

				break;

			// Check if a table or column needs to be upgraded to utf8mb4
			case 'utf8mb4upgrade':
				$condition = false;

				// Check if the driver and the database connection have UTF8MB4 support
				if (method_exists($this->db, 'hasUTF8mb4Support') && $this->db->hasUTF8mb4Support())
				{
					$fieldName = (string) $value;

					if (empty($fieldName))
					{
						$collation = $this->getTableCollation($tableNormal);
					}
					else
					{
						$collation = $this->getColumnCollation($tableNormal, $fieldName);
					}

					$parts    = explode('_', $collation, 3);
					$encoding = empty($parts[0]) ? '' : strtolower($parts[0]);

					$condition = $encoding != 'utf8mb4';
				}

				break;

			// Check if the result of a query matches our expectation
			case 'equals':
				$query = (string) $node;
				$this->db->setQuery($query);

				try
				{
					$result    = $this->db->loadResult();
					$condition = ($result == $value);
				}
				catch (Exception $e)
				{
					return false;
				}

				break;

			// Always returns true
			case 'true':
				return true;
				break;

			default:
				return false;
				break;
		}

		return $condition;
	}

	/**
	 * Get the collation of a table. Uses an internal cache for efficiency.
	 *
	 * @param   string  $tableName  The name of the table
	 *
	 * @return  string  The collation, e.g. "utf8_general_ci"
	 */
	private function getTableCollation($tableName)
	{
		static $cache = [];

		$tableName = $this->db->replacePrefix($tableName);

		if (!isset($cache[$tableName]))
		{
			$cache[$tableName] = $this->realGetTableCollation($tableName);
		}

		return $cache[$tableName];
	}

	/**
	 * Get the collation of a table. This is the internal method used by getTableCollation.
	 *
	 * @param   string  $tableName  The name of the table
	 *
	 * @return  string  The collation, e.g. "utf8_general_ci"
	 */
	private function realGetTableCollation($tableName)
	{
		$utf8Support    = method_exists($this->db, 'hasUTFSupport') && $this->db->hasUTFSupport();
		$utf8mb4Support = $utf8Support && method_exists($this->db, 'hasUTF8mb4Support') && $this->db->hasUTF8mb4Support();

		$collation = $utf8mb4Support ? 'utf8mb4_unicode_ci' : ($utf8Support ? 'utf_general_ci' : 'latin1_swedish_ci');

		$query = 'SHOW TABLE STATUS LIKE ' . $this->db->q($tableName);

		try
		{
			$row = $this->db->setQuery($query)->loadAssoc();
		}
		catch (Exception $e)
		{
			return $collation;
		}

		if (empty($row))
		{
			return $collation;
		}

		if (!isset($row['Collation']))
		{
			return $collation;
		}

		if (empty($row['Collation']))
		{
			return $collation;
		}

		return $row['Collation'];
	}

	/**
	 * Get the collation of a column. Uses an internal cache for efficiency.
	 *
	 * @param   string  $tableName   The name of the table
	 * @param   string  $columnName  The name of the column
	 *
	 * @return  string  The collation, e.g. "utf8_general_ci"
	 */
	private function getColumnCollation($tableName, $columnName)
	{
		static $cache = [];

		$tableName  = $this->db->replacePrefix($tableName);
		$columnName = $this->db->replacePrefix($columnName);

		if (!isset($cache[$tableName]))
		{
			$cache[$tableName] = [];
		}

		if (!isset($cache[$tableName][$columnName]))
		{
			$cache[$tableName][$columnName] = $this->realGetColumnCollation($tableName, $columnName);
		}

		return $cache[$tableName][$columnName];
	}

	/**
	 * Get the collation of a column. This is the internal method used by getColumnCollation.
	 *
	 * @param   string  $tableName   The name of the table
	 * @param   string  $columnName  The name of the column
	 *
	 * @return  string  The collation, e.g. "utf8_general_ci"
	 */
	private function realGetColumnCollation($tableName, $columnName)
	{
		$collation = $this->getTableCollation($tableName);

		$query = 'SHOW FULL COLUMNS FROM ' . $this->db->qn($tableName) . ' LIKE ' . $this->db->q($columnName);

		try
		{
			$row = $this->db->setQuery($query)->loadAssoc();
		}
		catch (Exception $e)
		{
			return $collation;
		}

		if (empty($row))
		{
			return $collation;
		}

		if (!isset($row['Collation']))
		{
			return $collation;
		}

		if (empty($row['Collation']))
		{
			return $collation;
		}

		return $row['Collation'];
	}

	/**
	 * Automatically downgrade a CREATE TABLE or ALTER TABLE query from utf8mb4 (UTF-8 Multibyte) to plain utf8.
	 *
	 * We use our own method so we can be site it works even on Joomla! 3.4 or earlier, where UTF8MB4 support is not
	 * implemented.
	 *
	 * @param   string  $query  The query to convert
	 *
	 * @return  string  The converted query
	 */
	private function convertUtf8mb4QueryToUtf8($query)
	{
		// If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert
		$beginningOfQuery = substr($query, 0, 12);
		$beginningOfQuery = strtoupper($beginningOfQuery);

		if (!in_array($beginningOfQuery, ['ALTER TABLE ', 'CREATE TABLE']))
		{
			return $query;
		}

		// Replace utf8mb4 with utf8
		$from = [
			'utf8mb4_unicode_ci',
			'utf8mb4_',
			'utf8mb4',
		];

		$to = [
			'utf8_general_ci', // Yeah, we convert utf8mb4_unicode_ci to utf8_general_ci per Joomla!'s conventions
			'utf8_',
			'utf8',
		];

		return str_replace($from, $to, $query);
	}

	/**
	 * Automatically upgrade a CREATE TABLE or ALTER TABLE query from plain utf8 to utf8mb4 (UTF-8 Multibyte).
	 *
	 * @param   string  $query  The query to convert
	 *
	 * @return  string  The converted query
	 */
	private function convertUtf8QueryToUtf8mb4($query)
	{
		// If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert
		$beginningOfQuery = substr($query, 0, 12);
		$beginningOfQuery = strtoupper($beginningOfQuery);

		if (!in_array($beginningOfQuery, ['ALTER TABLE ', 'CREATE TABLE']))
		{
			return $query;
		}

		// Replace utf8 with utf8mb4
		$from = [
			'utf8_general_ci',
			'utf8_',
			'utf8',
		];

		$to = [
			'utf8mb4_unicode_ci', // Yeah, we convert utf8_general_ci to utf8mb4_unicode_ci per Joomla!'s conventions
			'utf8mb4_',
			'utf8mb4',
		];

		return str_replace($from, $to, $query);
	}

	/**
	 * Analyzes a query. If it's a CREATE TABLE query the table is added to the $tables array.
	 *
	 * @param   string  $query   The query to analyze
	 * @param   array   $tables  The array where the name of the detected table is added
	 *
	 * @return  void
	 */
	private function extractTablesToConvert($query, &$tables)
	{
		// Normalize the whitespace of the query
		$query = trim($query);
		$query = str_replace(["\r\n", "\r", "\n"], ' ', $query);

		while (strstr($query, '  ') !== false)
		{
			$query = str_replace('  ', ' ', $query);
		}

		// Is it a create table query?
		$queryStart = substr($query, 0, 12);
		$queryStart = strtoupper($queryStart);

		if ($queryStart != 'CREATE TABLE')
		{
			return;
		}

		// Remove the CREATE TABLE keyword. Also, If there's an IF NOT EXISTS clause remove it.
		$query = substr($query, 12);
		$query = str_ireplace('IF NOT EXISTS', '', $query);
		$query = trim($query);

		// Make sure there is a space between the table name and its definition, denoted by an open parenthesis
		$query = str_replace('(', ' (', $query);

		// Now we should have the name of the table, a space and the rest of the query. Extract the table name.
		$parts     = explode(' ', $query, 2);
		$tableName = $parts[0];

		/**
		 * The table name may be quoted. Since UTF8MB4 is only supported in MySQL, the table name can only be
		 * quoted with surrounding backticks. Therefore we can trim backquotes from the table name to unquote it!
		 **/
		$tableName = trim($tableName, '`');

		// Finally, add the table name to $tables if it doesn't already exist.
		if (!in_array($tableName, $tables))
		{
			$tables[] = $tableName;
		}
	}

	/**
	 * Converts the collation of tables listed in $tablesToConvert to utf8mb4_unicode_ci
	 *
	 * @param   array  $tablesToConvert  The list of tables to convert
	 *
	 * @return  void
	 */
	private function convertTablesToUtf8mb4($tablesToConvert)
	{
		// Make sure the database driver REALLY has support for converting character sets
		if (!method_exists($this->db, 'getAlterTableCharacterSet'))
		{
			return;
		}

		asort($tablesToConvert);

		foreach ($tablesToConvert as $tableName)
		{
			$collation = $this->getTableCollation($tableName);

			$parts    = explode('_', $collation, 3);
			$encoding = empty($parts[0]) ? '' : strtolower($parts[0]);

			if ($encoding != 'utf8mb4')
			{
				$queries = $this->db->getAlterTableCharacterSet($tableName);

				try
				{
					foreach ($queries as $query)
					{
						$this->db->setQuery($query)->execute();
					}
				}
				catch (Exception $e)
				{
					// We ignore failed conversions. Remember, you MUST change your indices MANUALLY.
				}
			}
		}
	}

	/**
	 * Returns true if table $tableName has an index named $indexName or if it's impossible to retrieve index names for
	 * the table (not enough privileges, not a MySQL database, ...)
	 *
	 * @param   string  $tableName  The name of the table
	 * @param   string  $indexName  The name of the index
	 *
	 * @return  bool
	 */
	private function hasIndex($tableName, $indexName)
	{
		static $isMySQL = null;
		static $cache = [];

		if (is_null($isMySQL))
		{
			$driverType = $this->db->name;
			$driverType = strtolower($driverType);
			$isMySQL    = true;

			if (
				!strpos($driverType, 'mysql') === 0
				&& !(substr($driverType, -5) == 'mysql')
				&& !(substr($driverType, -6) == 'mysqli')
			)
			{
				$isMySQL = false;
			}
		}

		// Not MySQL? Lie and return true.
		if (!$isMySQL)
		{
			return true;
		}

		if (!isset($cache[$tableName]))
		{
			$cache[$tableName] = [];
		}

		if (!isset($cache[$tableName][$indexName]))
		{
			$cache[$tableName][$indexName] = true;

			try
			{
				$indices          = [];
				$query            = 'SHOW INDEXES FROM ' . $this->db->qn($tableName);
				$indexDefinitions = $this->db->setQuery($query)->loadAssocList();

				if (!empty($indexDefinitions) && is_array($indexDefinitions))
				{
					foreach ($indexDefinitions as $def)
					{
						$indices[] = $def['Key_name'];
					}

					$indices = array_unique($indices);
				}

				$cache[$tableName][$indexName] = in_array($indexName, $indices);
			}
			catch (Exception $e)
			{
				// Ignore errors
			}
		}

		return $cache[$tableName][$indexName];
	}
}
Toolbar/Exception/MissingAttribute.php000060400000001162152455514240014115 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Toolbar\Exception;

defined('_JEXEC') || die;

use Exception;
use InvalidArgumentException;
use Joomla\CMS\Language\Text;

class MissingAttribute extends InvalidArgumentException
{
	public function __construct($missingArgument, $buttonType, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_TOOLBAR_ERR_MISSINGARGUMENT', $missingArgument, $buttonType);

		parent::__construct($message, $code, $previous);
	}
}
Toolbar/Exception/UnknownButtonType.php000060400000001121152455514240014310 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Toolbar\Exception;

defined('_JEXEC') || die;

use Exception;
use InvalidArgumentException;
use Joomla\CMS\Language\Text;

class UnknownButtonType extends InvalidArgumentException
{
	public function __construct($buttonType, $code = 500, Exception $previous = null)
	{
		$message = Text::sprintf('LIB_FOF_TOOLBAR_ERR_UNKNOWNBUTTONTYPE', $buttonType);

		parent::__construct($message, $code, $previous);
	}
}
Toolbar/Toolbar.php000060400000073020152455514240010266 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Toolbar;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Controller\Controller;
use FOF30\Toolbar\Exception\MissingAttribute;
use FOF30\Toolbar\Exception\UnknownButtonType;
use FOF30\Utils\StringHelper;
use FOF30\View\DataView\DataViewInterface;
use FOF30\View\View;
use InvalidArgumentException;
use JArrayHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\Utilities\ArrayHelper;
use JToolbarHelper;
use stdClass;

/**
 * The Toolbar class renders the back-end component title area and the back-
 * and front-end toolbars.
 *
 * @since    1.0
 */
class Toolbar
{
	/** @var   array   Permissions map, see the __construct method for more information */
	public $perms = [];
	/** @var   Container   Component container */
	protected $container = null;
	/** @var   array   The links to be rendered in the toolbar */
	protected $linkbar = [];

	/** @var   bool   Should I render the submenu in the front-end? */
	protected $renderFrontendSubmenu = false;

	/** @var   bool   Should I render buttons in the front-end? */
	protected $renderFrontendButtons = false;

	/** @var   bool  Should I use the configuration file (fof.xml) of the component? */
	protected $useConfigurationFile = false;

	/** @var  null|bool  Are we rendering a data-aware view? */
	protected $isDataView = null;

	/**
	 * Public constructor.
	 *
	 * The $config array can contain the following optional values:
	 *
	 * renderFrontendButtons    bool    Should I render buttons in the front-end of the component?
	 * renderFrontendSubmenu    bool    Should I render the submenu in the front-end of the component?
	 * useConfigurationFile        bool    Should we use the configuration file (fof.xml) of the component?
	 *
	 * @param   Container  $c       The container for the component
	 * @param   array      $config  The configuration overrides, see above
	 */
	public function __construct(Container $c, array $config = [])
	{
		// Store the container reference in this object
		$this->container = $c;

		// Get a reference to some useful objects
		$input    = $this->container->input;
		$platform = $this->container->platform;

		// Get default permissions (can be overridden by the view)
		$perms = (object) [
			'manage'    => $this->container->platform->authorise('core.manage', $input->getCmd('option', 'com_foobar')),
			'create'    => $this->container->platform->authorise('core.create', $input->getCmd('option', 'com_foobar')),
			'edit'      => $this->container->platform->authorise('core.edit', $input->getCmd('option', 'com_foobar')),
			'editstate' => $this->container->platform->authorise('core.edit.state', $input->getCmd('option', 'com_foobar')),
			'delete'    => $this->container->platform->authorise('core.delete', $input->getCmd('option', 'com_foobar')),
		];

		// Save front-end toolbar and submenu rendering flags if present in the config
		if (array_key_exists('renderFrontendButtons', $config))
		{
			$this->renderFrontendButtons = $config['renderFrontendButtons'];
		}

		if (array_key_exists('renderFrontendSubmenu', $config))
		{
			$this->renderFrontendSubmenu = $config['renderFrontendSubmenu'];
		}

		// If not in the administrative area, load the JToolbarHelper
		if (!$platform->isBackend())
		{
			// Needed for tests, so we can inject our "special" helper class
			if (!class_exists('\\JToolbarHelper'))
			{
				$platformDirs = $platform->getPlatformBaseDirs();
				$path         = $platformDirs['root'] . '/administrator/includes/toolbar.php';
				require_once $path;
			}

			// Things to do if we have to render a front-end toolbar
			if ($this->renderFrontendButtons)
			{
				// Load back-end toolbar language files in front-end
				$platform->loadTranslations('');

				// Needed for tests (we can fake we're not in the backend, but we are still in CLI!)
				if (!$platform->isCli())
				{
					// Load the core Javascript
					HTMLHelper::_('behavior.core');
					HTMLHelper::_('jquery.framework', true);
				}
			}
		}

		// Store permissions in the local toolbar object
		$this->perms = $perms;
	}

	/**
	 * Renders the toolbar for the current view and task
	 *
	 * @param   string  $view  The view of the component
	 * @param   string  $task  The exact task of the view
	 *
	 * @return  void
	 */
	public function renderToolbar($view = null, $task = null)
	{
		$input = $this->container->input;

		// If tmpl=component the default behaviour is to not render the toolbar
		if ($input->getCmd('tmpl', '') == 'component')
		{
			$render_toolbar = false;
		}
		else
		{
			$render_toolbar = true;
		}

		// If there is a render_toolbar=0 in the URL, do not render a toolbar
		$render_toolbar = $input->getBool('render_toolbar', $render_toolbar);

		if (!$render_toolbar)
		{
			return;
		}

		// Get the view and task
		$controller       = $this->container->dispatcher->getController();
		$autoDetectedView = 'cpanel';
		$autoDetectedTask = 'main';

		if (is_object($controller) && ($controller instanceof Controller))
		{
			$autoDetectedView = $controller->getName();
			$autoDetectedTask = $controller->getTask();
		}

		if (empty($view))
		{
			$view = $input->getCmd('view', $autoDetectedView);
		}

		if (empty($task))
		{
			$task = $input->getCmd('task', $autoDetectedTask);
		}

		// If there is a fof.xml toolbar configuration use it and return
		$view          = $this->container->inflector->pluralize($view);
		$toolbarConfig = $this->container->appConfig->get('views.' . ucfirst($view) . '.toolbar.' . $task);

		$oldValues = [
			'renderFrontendButtons' => $this->renderFrontendButtons,
			'renderFrontendSubmenu' => $this->renderFrontendSubmenu,
			'useConfigurationFile'  => $this->useConfigurationFile,
		];

		$newValues = [
			'renderFrontendButtons' => $this->container->appConfig->get(
				'views.' . ucfirst($view) . '.config.renderFrontendButtons',
				$oldValues['renderFrontendButtons']
			),
			'renderFrontendSubmenu' => $this->container->appConfig->get(
				'views.' . ucfirst($view) . '.config.renderFrontendSubmenu',
				$oldValues['renderFrontendSubmenu']
			),
			'useConfigurationFile'  => $this->container->appConfig->get(
				'views.' . ucfirst($view) . '.config.useConfigurationFile',
				$oldValues['useConfigurationFile']
			),
		];

		foreach ($newValues as $k => $v)
		{
			$this->$k = $v;
		}

		if (!empty($toolbarConfig) && $this->useConfigurationFile)
		{
			$this->renderFromConfig($toolbarConfig);

			return;
		}

		// Check for an onViewTask method
		$methodName = 'on' . ucfirst($view) . ucfirst($task);

		if (method_exists($this, $methodName))
		{
			$this->$methodName();

			return;
		}

		// Check for an onView method
		$methodName = 'on' . ucfirst($view);

		if (method_exists($this, $methodName))
		{
			$this->$methodName();

			return;
		}

		// Check for an onTask method
		$methodName = 'on' . ucfirst($task);

		if (method_exists($this, $methodName))
		{
			$this->$methodName();

			return;
		}
	}

	/**
	 * Renders the toolbar for the component's Control Panel page
	 *
	 * @return  void
	 */
	public function onCpanelsBrowse()
	{
		if ($this->container->platform->isBackend() || $this->renderFrontendSubmenu)
		{
			$this->renderSubmenu();
		}

		if (!$this->container->platform->isBackend() && !$this->renderFrontendButtons)
		{
			return;
		}

		$option = $this->container->componentName;

		JToolbarHelper::title(Text::_(strtoupper($option)), str_replace('com_', '', $option));

		if (!$this->isDataView())
		{
			return;
		}

		JToolbarHelper::preferences($option);
	}

	/**
	 * Renders the toolbar for the component's Browse pages (the plural views)
	 *
	 * @return  void
	 */
	public function onBrowse()
	{
		// On frontend, buttons must be added specifically
		if ($this->container->platform->isBackend() || $this->renderFrontendSubmenu)
		{
			$this->renderSubmenu();
		}

		if (!$this->container->platform->isBackend() && !$this->renderFrontendButtons)
		{
			return;
		}

		// Setup
		$option = $this->container->componentName;
		$view   = $this->container->input->getCmd('view', 'cpanel');

		// Set toolbar title
		$subtitle_key = strtoupper($option . '_TITLE_' . $view);
		JToolbarHelper::title(Text::_(strtoupper($option)) . ': ' . Text::_($subtitle_key), str_replace('com_', '', $option));

		if (!$this->isDataView())
		{
			return;
		}

		// Add toolbar buttons
		if ($this->perms->create)
		{
			JToolbarHelper::addNew();
		}

		if ($this->perms->edit)
		{
			JToolbarHelper::editList();
		}

		if ($this->perms->create || $this->perms->edit)
		{
			JToolbarHelper::divider();
		}

		// Published buttons are only added if there is a enabled field in the table
		try
		{
			$model = $this->container->factory->model($view);

			if ($model->hasField('enabled') && $this->perms->editstate)
			{
				JToolbarHelper::publishList();
				JToolbarHelper::unpublishList();
				JToolbarHelper::divider();
			}
		}
		catch (Exception $e)
		{
			// Yeah. Let's not add the buttons if we can't load the model...
		}

		if ($this->perms->delete)
		{
			$msg = Text::_($option . '_CONFIRM_DELETE');
			JToolbarHelper::deleteList(strtoupper($msg));
		}

		// A Check-In button is only added if there is a locked_on field in the table
		try
		{
			$model = $this->container->factory->model($view);

			if ($model->hasField('locked_on') && $this->perms->edit)
			{
				JToolbarHelper::checkin();
			}

		}
		catch (Exception $e)
		{
			// Yeah. Let's not add the button if we can't load the model...
		}
	}

	/**
	 * Renders the toolbar for the component's Read pages
	 *
	 * @return  void
	 */
	public function onRead()
	{
		// On frontend, buttons must be added specifically
		if ($this->container->platform->isBackend() || $this->renderFrontendSubmenu)
		{
			$this->renderSubmenu();
		}

		if (!$this->container->platform->isBackend() && !$this->renderFrontendButtons)
		{
			return;
		}

		$option        = $this->container->componentName;
		$componentName = str_replace('com_', '', $option);
		$view          = $this->container->input->getCmd('view', 'cpanel');

		// Set toolbar title
		$subtitle_key = strtoupper($option . '_TITLE_' . $view . '_READ');
		JToolbarHelper::title(Text::_(strtoupper($option)) . ': ' . Text::_($subtitle_key), $componentName);

		if (!$this->isDataView())
		{
			return;
		}

		// Set toolbar icons
		JToolbarHelper::back();
	}

	/**
	 * Renders the toolbar for the component's Add pages
	 *
	 * @return  void
	 */
	public function onAdd()
	{
		// On frontend, buttons must be added specifically
		if (!$this->container->platform->isBackend() && !$this->renderFrontendButtons)
		{
			return;
		}

		$option        = $this->container->componentName;
		$componentName = str_replace('com_', '', $option);
		$view          = $this->container->input->getCmd('view', 'cpanel');

		// Set toolbar title
		$subtitle_key = strtoupper($option . '_TITLE_' . $this->container->inflector->pluralize($view)) . '_EDIT';
		JToolbarHelper::title(Text::_(strtoupper($option)) . ': ' . Text::_($subtitle_key), $componentName);

		if (!$this->isDataView())
		{
			return;
		}

		// Set toolbar icons
		if ($this->perms->edit || $this->perms->editown)
		{
			// Show the apply button only if I can edit the record, otherwise I'll return to the edit form and get a
			// 403 error since I can't do that
			JToolbarHelper::apply();
		}

		JToolbarHelper::save();

		if ($this->perms->create)
		{
			JToolbarHelper::custom('savenew', 'save-new.png', 'save-new_f2.png', 'JTOOLBAR_SAVE_AND_NEW', false);
		}

		JToolbarHelper::cancel();
	}

	/**
	 * Renders the toolbar for the component's Edit pages
	 *
	 * @return  void
	 */
	public function onEdit()
	{
		// On frontend, buttons must be added specifically
		if (!$this->container->platform->isBackend() && !$this->renderFrontendButtons)
		{
			return;
		}

		$this->onAdd();
	}

	/**
	 * Removes all links from the link bar
	 *
	 * @return  void
	 */
	public function clearLinks()
	{
		$this->linkbar = [];
	}

	/**
	 * Get the link bar's link definitions
	 *
	 * @return  array
	 */
	public function &getLinks()
	{
		return $this->linkbar;
	}

	/**
	 * Append a link to the link bar
	 *
	 * @param   string       $name    The text of the link
	 * @param   string|null  $link    The link to render; set to null to render a separator
	 * @param   boolean      $active  True if it's an active link
	 * @param   string|null  $icon    Icon class (used by some renderers, like the Bootstrap renderer)
	 * @param   string|null  $parent  The parent element (referenced by name)) This will create a dropdown list
	 *
	 * @return  void
	 */
	public function appendLink($name, $link = null, $active = false, $icon = null, $parent = '')
	{
		$linkDefinition = [
			'name'   => $name,
			'link'   => $link,
			'active' => $active,
			'icon'   => $icon,
		];

		if (empty($parent))
		{
			if (array_key_exists($name, $this->linkbar))
			{
				$this->linkbar[$name] = array_merge($this->linkbar[$name], $linkDefinition);

				// If there already are some children, I have to put this view link in the "items" array in the first place
				if (array_key_exists('items', $this->linkbar[$name]))
				{
					array_unshift($this->linkbar[$name]['items'], $linkDefinition);
				}
			}
			else
			{
				$this->linkbar[$name] = $linkDefinition;
			}
		}
		else
		{
			if (!array_key_exists($parent, $this->linkbar))
			{
				$parentElement          = $linkDefinition;
				$parentElement['name']  = $parent;
				$parentElement['link']  = null;
				$this->linkbar[$parent] = $parentElement;
				$parentElement['items'] = [];
			}
			else
			{
				$parentElement = $this->linkbar[$parent];

				if (!array_key_exists('dropdown', $parentElement) && !empty($parentElement['link']))
				{
					$newSubElement          = $parentElement;
					$parentElement['items'] = [$newSubElement];
				}
			}

			$parentElement['items'][]  = $linkDefinition;
			$parentElement['dropdown'] = true;

			if ($active)
			{
				$parentElement['active'] = true;
			}

			$this->linkbar[$parent] = $parentElement;
		}
	}

	/**
	 * Prefixes (some people erroneously call this "prepend" – there is no such word) a link to the link bar
	 *
	 * @param   string       $name    The text of the link
	 * @param   string|null  $link    The link to render; set to null to render a separator
	 * @param   boolean      $active  True if it's an active link
	 * @param   string|null  $icon    Icon class (used by some renderers, like the Bootstrap renderer)
	 *
	 * @return  void
	 */
	public function prefixLink($name, $link = null, $active = false, $icon = null)
	{
		$linkDefinition = [
			'name'   => $name,
			'link'   => $link,
			'active' => $active,
			'icon'   => $icon,
		];
		array_unshift($this->linkbar, $linkDefinition);
	}

	/**
	 * Renders the submenu (toolbar links) for all detected views of this component
	 *
	 * @return  void
	 */
	public function renderSubmenu()
	{
		$views = $this->getMyViews();

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

		$activeView = $this->container->input->getCmd('view', 'cpanel');

		foreach ($views as $view)
		{
			// Get the view name
			$key = strtoupper($this->container->componentName) . '_TITLE_' . strtoupper($view);

			//Do we have a translation for this key?
			if (strtoupper(Text::_($key)) == $key)
			{
				$altview = $this->container->inflector->isPlural($view) ? $this->container->inflector->singularize($view) : $this->container->inflector->pluralize($view);
				$key2    = strtoupper($this->container->componentName) . '_TITLE_' . strtoupper($altview);

				// Maybe we have for the alternative view?
				if (strtoupper(Text::_($key2)) == $key2)
				{
					// Nope, let's use the raw name
					$name = ucfirst($view);
				}
				else
				{
					$name = Text::_($key2);
				}
			}
			else
			{
				$name = Text::_($key);
			}

			$link = 'index.php?option=' . $this->container->componentName . '&view=' . $view;

			$active = $view == $activeView;

			$this->appendLink($name, $link, $active);
		}
	}

	/**
	 * Return the front-end toolbar rendering flag
	 *
	 * @return  boolean
	 */
	public function getRenderFrontendButtons()
	{
		return $this->renderFrontendButtons;
	}

	/**
	 * @param   boolean  $renderFrontendButtons
	 */
	public function setRenderFrontendButtons($renderFrontendButtons)
	{
		$this->renderFrontendButtons = $renderFrontendButtons;
	}

	/**
	 * Return the front-end submenu rendering flag
	 *
	 * @return  boolean
	 */
	public function getRenderFrontendSubmenu()
	{
		return $this->renderFrontendSubmenu;
	}

	/**
	 * @param   boolean  $renderFrontendSubmenu
	 */
	public function setRenderFrontendSubmenu($renderFrontendSubmenu)
	{
		$this->renderFrontendSubmenu = $renderFrontendSubmenu;
	}

	/**
	 * Is the view we are rendering the toolbar for a data-aware view?
	 *
	 * @return  bool
	 */
	public function isDataView()
	{
		if (is_null($this->isDataView))
		{
			$this->isDataView = false;
			$controller       = $this->container->dispatcher->getController();
			$view             = null;

			if (is_object($controller) && ($controller instanceof Controller))
			{
				$view = $controller->getView();
			}

			if (is_object($view) && ($view instanceof View))
			{
				$this->isDataView = $view instanceof DataViewInterface;
			}
		}

		return $this->isDataView;
	}

	/**
	 * Automatically detects all views of the component
	 *
	 * @return  array  A list of all views, in the order to be displayed in the toolbar submenu
	 */
	protected function getMyViews()
	{
		$t_views    = [];
		$using_meta = false;

		$componentPaths = $this->container->platform->getComponentBaseDirs($this->container->componentName);
		$searchPath     = $componentPaths['main'] . '/View';
		$filesystem     = $this->container->filesystem;

		$allFolders = $filesystem->folderFolders($searchPath);

		if (!empty($allFolders))
		{
			foreach ($allFolders as $folder)
			{
				$view = $folder;

				// View already added
				if (in_array($this->container->inflector->pluralize($view), $t_views))
				{
					continue;
				}

				// Do we have a 'skip.xml' file in there?
				$files = $filesystem->folderFiles($searchPath . '/' . $view, '^skip\.xml$');

				if (!empty($files))
				{
					continue;
				}

				// Do we have extra information about this view? (ie. ordering)
				$meta = $filesystem->folderFiles($searchPath . '/' . $view, '^metadata\.xml$');

				// Not found, do we have it inside the plural one?
				if (!$meta)
				{
					$plural = $this->container->inflector->pluralize($view);

					if (in_array($plural, $allFolders))
					{
						$view = $plural;
						$meta = $filesystem->folderFiles($searchPath . '/' . $view, '^metadata\.xml$');
					}
				}

				if (!empty($meta))
				{
					$using_meta = true;
					$xml        = simplexml_load_file($searchPath . '/' . $view . '/' . $meta[0]);
					$order      = (int) $xml->foflib->ordering;
				}
				else
				{
					// Next place. It's ok since the index are 0-based and count is 1-based

					if (!isset($to_order))
					{
						$to_order = [];
					}

					$order = count($to_order);
				}

				$view = $this->container->inflector->pluralize($view);

				$t_view           = new stdClass;
				$t_view->ordering = $order;
				$t_view->view     = $view;

				$to_order[] = $t_view;
				$t_views[]  = $view;
			}
		}

		$views = [];

		if (!empty($to_order))
		{
			if (class_exists('JArrayHelper'))
			{
				JArrayHelper::sortObjects($to_order, 'ordering');
				$views = JArrayHelper::getColumn($to_order, 'view');
			}
			else
			{
				ArrayHelper::sortObjects($to_order, 'ordering');
				$views = ArrayHelper::getColumn($to_order, 'view');
			}

		}

		// If not using the metadata file, let's put the cpanel view on top
		if (!$using_meta)
		{
			$cpanel = array_search('cpanels', $views);

			if ($cpanel !== false)
			{
				unset($views[$cpanel]);
				array_unshift($views, 'cpanels');
			}
		}

		return $views;
	}

	/**
	 * Simplified default rendering without any attributes.
	 *
	 * @access    protected
	 *
	 * @param   array  $tasks  Array of tasks.
	 *
	 * @return    void
	 */
	protected function renderToolbarElements($tasks)
	{
		foreach ($tasks as $task)
		{
			$this->renderToolbarElement($task);
		}
	}

	/**
	 * Checks if the current user has enough privileges for the requested ACL privilege of a custom toolbar button.
	 *
	 * @param   string  $area  The ACL privilege as set up in the $this->perms object
	 *
	 * @return  boolean  True if the user has the ACL privilege specified
	 */
	protected function checkACL($area)
	{
		if (is_bool($area))
		{
			return $area;
		}

		if (in_array(strtolower($area), ['false', '0', 'no', '403']))
		{
			return false;
		}

		if (in_array(strtolower($area), ['true', '1', 'yes']))
		{
			return true;
		}

		if (in_array(strtolower($area), ['guest']))
		{
			return $this->container->platform->getUser()->guest;
		}

		if (in_array(strtolower($area), ['user']))
		{
			return !$this->container->platform->getUser()->guest;
		}

		if (empty($area))
		{
			return true;
		}

		if (isset($this->perms->$area))
		{
			return $this->perms->$area;
		}

		return false;
	}

	/**
	 * Render the toolbar from the configuration.
	 *
	 * @param   array  $toolbar  The toolbar definition
	 *
	 * @return  void
	 */
	private function renderFromConfig(array $toolbar)
	{
		$isBackend = $this->container->platform->isBackend();

		if ($isBackend || $this->renderFrontendSubmenu)
		{
			$this->renderSubmenu();
		}

		if (!$isBackend && !$this->renderFrontendButtons)
		{
			return;
		}

		if (!$this->isDataView())
		{
			return;
		}

		// Render each element
		foreach ($toolbar as $elementType => $elementAttributes)
		{
			$value = $elementAttributes['value'] ?? null;
			$this->renderToolbarElement($elementType, $value, $elementAttributes);
		}

		return;
	}

	/**
	 * Render a toolbar element.
	 *
	 * @param   string  $type        The element type.
	 * @param   mixed   $value       The element value.
	 * @param   array   $attributes  The element attributes.
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 * @throws  InvalidArgumentException
	 */
	private function renderToolbarElement($type, $value = null, array $attributes = [])
	{
		switch ($type)
		{
			case 'title':
				$icon = $attributes['icon'] ?? 'generic.png';
				if (isset($attributes['translate']))
				{
					$value = Text::_($value);
				}

				JToolbarHelper::title($value, $icon);
				break;

			case 'divider':
				JToolbarHelper::divider();
				break;

			case 'custom':
				$task       = $attributes['task'] ?? '';
				$icon       = $attributes['icon'] ?? '';
				$iconOver   = $attributes['icon_over'] ?? '';
				$alt        = $attributes['alt'] ?? '';
				$listSelect = isset($attributes['list_select']) ?
					StringHelper::toBool($attributes['list_select']) : true;

				JToolbarHelper::custom($task, $icon, $iconOver, $alt, $listSelect);
				break;

			case 'preview':
				$url            = $attributes['url'] ?? '';
				$update_editors = isset($attributes['update_editors']) ?
					StringHelper::toBool($attributes['update_editors']) : false;

				JToolbarHelper::preview($url, $update_editors);
				break;

			case 'help':
				if (!isset($attributes['help']))
				{
					throw new MissingAttribute('help', 'help');
				}

				$ref       = $attributes['help'];
				$com       = isset($attributes['com']) ? StringHelper::toBool($attributes['com']) : false;
				$override  = $attributes['override'] ?? null;
				$component = $attributes['component'] ?? null;

				JToolbarHelper::help($ref, $com, $override, $component);
				break;

			case 'back':
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_BACK';
				$href = $attributes['href'] ?? 'javascript:history.back();';

				JToolbarHelper::back($alt, $href);
				break;

			case 'media_manager':
				$directory = $attributes['directory'] ?? '';
				$alt       = $attributes['alt'] ?? 'JTOOLBAR_UPLOAD';

				JToolbarHelper::media_manager($directory, $alt);
				break;

			case 'assign':
				$task = $attributes['task'] ?? 'assign';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_ASSIGN';

				JToolbarHelper::assign($task, $alt);
				break;

			case 'addNew':
			case 'new':
				$area = $attributes['acl'] ?? 'create';

				if ($this->checkACL($area))
				{
					$task  = $attributes['task'] ?? 'add';
					$alt   = $attributes['alt'] ?? 'JTOOLBAR_NEW';
					$check = isset($attributes['check']) ?
						StringHelper::toBool($attributes['check']) : false;

					JToolbarHelper::addNew($task, $alt, $check);
				}

				break;

			case 'copy':
				$area = $attributes['acl'] ?? 'create';

				if ($this->checkACL($area))
				{
					$task     = $attributes['task'] ?? 'copy';
					$alt      = $attributes['alt'] ?? 'JLIB_HTML_BATCH_COPY';
					$icon     = $attributes['icon'] ?? 'copy.png';
					$iconOver = $attributes['iconOver'] ?? 'copy_f2.png';

					JToolbarHelper::custom($task, $icon, $iconOver, $alt, false);
				}

				break;

			case 'publish':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task  = $attributes['task'] ?? 'publish';
					$alt   = $attributes['alt'] ?? 'JTOOLBAR_PUBLISH';
					$check = isset($attributes['check']) ?
						StringHelper::toBool($attributes['check']) : false;

					JToolbarHelper::publish($task, $alt, $check);
				}

				break;

			case 'publishList':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task = $attributes['task'] ?? 'publish';
					$alt  = $attributes['alt'] ?? 'JTOOLBAR_PUBLISH';

					JToolbarHelper::publishList($task, $alt);
				}

				break;

			case 'unpublish':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task  = $attributes['task'] ?? 'unpublish';
					$alt   = $attributes['alt'] ?? 'JTOOLBAR_UNPUBLISH';
					$check = isset($attributes['check']) ?
						StringHelper::toBool($attributes['check']) : false;

					JToolbarHelper::unpublish($task, $alt, $check);
				}

				break;

			case 'unpublishList':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task = $attributes['task'] ?? 'unpublish';
					$alt  = $attributes['alt'] ?? 'JTOOLBAR_UNPUBLISH';

					JToolbarHelper::unpublishList($task, $alt);
				}

				break;

			case 'archiveList':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task = $attributes['task'] ?? 'archive';
					$alt  = $attributes['alt'] ?? 'JTOOLBAR_ARCHIVE';

					JToolbarHelper::archiveList($task, $alt);
				}

				break;

			case 'unarchiveList':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task = $attributes['task'] ?? 'unarchive';
					$alt  = $attributes['alt'] ?? 'JTOOLBAR_UNARCHIVE';

					JToolbarHelper::unarchiveList($task, $alt);
				}

				break;

			case 'edit':
			case 'editList':
				$area = $attributes['acl'] ?? 'edit';

				if ($this->checkACL($area))
				{
					$task = $attributes['task'] ?? 'edit';
					$alt  = $attributes['alt'] ?? 'JTOOLBAR_EDIT';

					JToolbarHelper::editList($task, $alt);
				}

				break;

			case 'editHtml':
				$task = $attributes['task'] ?? 'edit_source';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_EDIT_HTML';

				JToolbarHelper::editHtml($task, $alt);
				break;

			case 'editCss':
				$task = $attributes['task'] ?? 'edit_css';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_EDIT_CSS';

				JToolbarHelper::editCss($task, $alt);
				break;

			case 'deleteList':
			case 'delete':
				$area = $attributes['acl'] ?? 'delete';

				if ($this->checkACL($area))
				{
					$msg  = $attributes['msg'] ?? '';
					$task = $attributes['task'] ?? 'remove';
					$alt  = $attributes['alt'] ?? 'JTOOLBAR_DELETE';

					JToolbarHelper::deleteList($msg, $task, $alt);
				}

				break;

			case 'trash':
				$area = $attributes['acl'] ?? 'editstate';

				if ($this->checkACL($area))
				{
					$task  = $attributes['task'] ?? 'trash';
					$alt   = $attributes['alt'] ?? 'JTOOLBAR_TRASH';
					$check = isset($attributes['check']) ?
						StringHelper::toBool($attributes['check']) : true;

					JToolbarHelper::trash($task, $alt, $check);
				}

				break;

			case 'apply':
				$task = $attributes['task'] ?? 'apply';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_APPLY';

				JToolbarHelper::apply($task, $alt);
				break;

			case 'save':
				$task = $attributes['task'] ?? 'save';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_SAVE';

				JToolbarHelper::save($task, $alt);
				break;

			case 'savenew':
				$task     = $attributes['task'] ?? 'savenew';
				$alt      = $attributes['alt'] ?? 'JTOOLBAR_SAVE_AND_NEW';
				$icon     = $attributes['icon'] ?? 'save-new.png';
				$iconOver = $attributes['iconOver'] ?? 'save-new_f2.png';

				JToolbarHelper::custom($task, $icon, $iconOver, $alt, false);
				break;

			case 'save2new':
				$task = $attributes['task'] ?? 'save2new';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_SAVE_AND_NEW';

				JToolbarHelper::save2new($task, $alt);
				break;

			case 'save2copy':
				$task = $attributes['task'] ?? 'save2copy';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_SAVE_AS_COPY';
				JToolbarHelper::save2copy($task, $alt);
				break;

			case 'checkin':
				$task  = $attributes['task'] ?? 'checkin';
				$alt   = $attributes['alt'] ?? 'JTOOLBAR_CHECKIN';
				$check = isset($attributes['check']) ?
					StringHelper::toBool($attributes['check']) : true;

				JToolbarHelper::checkin($task, $alt, $check);
				break;

			case 'cancel':
				$task = $attributes['task'] ?? 'cancel';
				$alt  = $attributes['alt'] ?? 'JTOOLBAR_CANCEL';

				JToolbarHelper::cancel($task, $alt);
				break;

			case 'preferences':
				if (!isset($attributes['component']))
				{
					throw new MissingAttribute('component', 'preferences');
				}

				$component = $attributes['component'];
				$height    = $attributes['height'] ?? '550';
				$width     = $attributes['width'] ?? '875';
				$alt       = $attributes['alt'] ?? 'JToolbar_Options';
				$path      = $attributes['path'] ?? '';

				JToolbarHelper::preferences($component, $height, $width, $alt, $path);
				break;

			default:
				throw new UnknownButtonType($type);
		}
	}
}
Container/ContainerBase.php000060400000002216152455514240011720 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Container;

defined('_JEXEC') || die;

use FOF30\Pimple\Container;

class ContainerBase extends Container
{
	/**
	 * Magic getter for alternative syntax, e.g. $container->foo instead of $container['foo']
	 *
	 * @param   string  $name
	 *
	 * @return  mixed
	 *
	 * @throws \InvalidArgumentException if the identifier is not defined
	 */
	function __get($name)
	{
		return $this->offsetGet($name);
	}

	/**
	 * Magic setter for alternative syntax, e.g. $container->foo instead of $container['foo']
	 *
	 * @param   string  $name   The unique identifier for the parameter or object
	 * @param   mixed   $value  The value of the parameter or a closure for a service
	 *
	 * @throws \RuntimeException Prevent override of a frozen service
	 */
	function __set($name, $value)
	{
		// Special backwards compatible handling for the mediaVersion service
		if ($name == 'mediaVersion')
		{
			$this[$name]->setMediaVersion($value);

			return;
		}

		$this->offsetSet($name, $value);
	}
}
Container/Exception/NoComponent.php000060400000001103152455514240013372 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Container\Exception;

defined('_JEXEC') || die;

use Exception;

class NoComponent extends \Exception
{
	public function __construct($message = "", $code = 0, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = 'No component specified building the Container object';
		}

		if (empty($code))
		{
			$code = 500;
		}

		parent::__construct($message, $code, $previous);
	}
}
Container/Container.php000060400000053075152455514240011136 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Container;

defined('_JEXEC') || die;

use FOF30\Autoloader\Autoloader;
use FOF30\Configuration\Configuration;
use FOF30\Container\Exception\NoComponent;
use FOF30\Dispatcher\Dispatcher;
use FOF30\Encrypt\EncryptService;
use FOF30\Factory\FactoryInterface;
use FOF30\Inflector\Inflector;
use FOF30\Input\Input as FOFInput;
use FOF30\Params\Params;
use FOF30\Platform\FilesystemInterface;
use FOF30\Platform\Joomla\Filesystem as JoomlaFilesystem;
use FOF30\Platform\PlatformInterface;
use FOF30\Render\RenderInterface;
use FOF30\Template\Template;
use FOF30\Toolbar\Toolbar;
use FOF30\TransparentAuthentication\TransparentAuthentication as TransparentAuth;
use FOF30\Utils\MediaVersion;
use FOF30\View\Compiler\Blade;
use JDatabaseDriver;
use Joomla\CMS\Factory;
use Joomla\CMS\Input\Input as CMSInput;

/**
 * Dependency injection container for FOF-powered components.
 *
 * The properties below (except componentName, bareComponentName and the ones marked with property-read) can be
 * configured in the fof.xml component configuration file.
 *
 * Sample fof.xml:
 *
 * <fof>
 *   <common>
 *      <container>
 *         <option name="componentNamespace"><![CDATA[MyCompany\MyApplication]]></option>
 *         <option name="frontEndPath"><![CDATA[%PUBLIC%\components\com_application]]></option>
 *         <option name="factoryClass">magic</option>
 *      </container>
 *   </common>
 * </fof>
 *
 * The paths can use the variables %ROOT%, %PUBLIC%, %ADMIN%, %TMP%, %LOG% i.e. all the path keys returned by
 * Platform's
 * getPlatformBaseDirs() method in uppercase and surrounded by percent signs.
 *
 *
 * @property  string                   $componentName      The name of the component (com_something)
 * @property  string                   $bareComponentName  The name of the component without com_
 *            (something)
 * @property  string                   $componentNamespace The namespace of the component's classes
 *            (\Foobar)
 * @property  string                   $frontEndPath       The absolute path to the front-end files
 * @property  string                   $backEndPath        The absolute path to the back-end files
 * @property  string                   $thisPath           The preferred path. Backend for Admin
 *            application, frontend otherwise
 * @property  string                   $rendererClass      The fully qualified class name of the view
 *            renderer we'll be using. Must implement FOF30\Render\RenderInterface.
 * @property  string                   $factoryClass       The fully qualified class name of the MVC
 *            Factory object, default is FOF30\Factory\BasicFactory.
 * @property  string                   $platformClass      The fully qualified class name of the
 *            Platform abstraction object, default is FOF30\Platform\Joomla\Platform.
 * @property  MediaVersion             $mediaVersion       A version string for media files in forms.
 *            Default: md5 of release version, release date and site secret (if found)
 *
 * @property-read  Configuration       $appConfig          The application configuration registry
 * @property-read  Blade               $blade              The Blade view template compiler engine
 * @property-read  JDatabaseDriver     $db                 The database connection object
 * @property-read  Dispatcher          $dispatcher         The component's dispatcher
 * @property-read  FactoryInterface    $factory            The MVC object factory
 * @property-read  FilesystemInterface $filesystem         The filesystem abstraction layer object
 * @property-read  Inflector           $inflector          The English word inflector (pluralise /
 *                 singularize words etc)
 * @property-read  Params              $params             The component's params
 * @property-read  FOFInput            $input              The input object
 * @property-read  PlatformInterface   $platform           The platform abstraction layer object
 * @property-read  RenderInterface     $renderer           The view renderer
 * @property-read  \JSession           $session            Joomla! session storage
 * @property-read  Template            $template           The template helper
 * @property-read  TransparentAuth     $transparentAuth    Transparent authentication handler
 * @property-read  Toolbar             $toolbar            The component's toolbar
 * @property-read  EncryptService      $crypto             The component's data encryption service
 */
class Container extends ContainerBase
{

	/**
	 * Cache of created container instances
	 *
	 * @var   array
	 */
	protected static $instances = [];

	/**
	 * Public constructor. This does NOT go through the fof.xml file. You are advised to use getInstance() instead.
	 *
	 * @param   array  $values  Overrides for the container configuration and services
	 *
	 * @throws  NoComponent  If no component name is specified
	 */
	public function __construct(array $values = [])
	{
		// Initialise
		$this->bareComponentName  = '';
		$this->componentName      = '';
		$this->componentNamespace = '';
		$this->frontEndPath       = '';
		$this->backEndPath        = '';
		$this->thisPath           = '';
		$this->factoryClass       = 'FOF30\\Factory\\BasicFactory';
		$this->platformClass      = 'FOF30\\Platform\\Joomla\\Platform';

		$initMediaVersion = null;

		if (isset($values['mediaVersion']) && !is_object($values['mediaVersion']))
		{
			$initMediaVersion = $values['mediaVersion'];

			unset($values['mediaVersion']);
		}

		// Try to construct this container object
		parent::__construct($values);

		// Make sure we have a component name
		if (empty($this['componentName']))
		{
			throw new Exception\NoComponent;
		}

		$bareComponent = substr($this->componentName, 4);

		$this['bareComponentName'] = $bareComponent;

		// Try to guess the component's namespace
		if (empty($this['componentNamespace']))
		{
			$this->componentNamespace = ucfirst($bareComponent);
		}
		else
		{
			$this->componentNamespace = trim($this->componentNamespace, '\\');
		}

		// Make sure we have front-end and back-end paths
		if (empty($this['frontEndPath']))
		{
			$this->frontEndPath = JPATH_SITE . '/components/' . $this->componentName;
		}

		if (empty($this['backEndPath']))
		{
			$this->backEndPath = JPATH_ADMINISTRATOR . '/components/' . $this->componentName;
		}

		// Get the namespaces for the front-end and back-end parts of the component
		$frontEndNamespace = '\\' . $this->componentNamespace . '\\Site\\';
		$backEndNamespace  = '\\' . $this->componentNamespace . '\\Admin\\';

		// Special case: if the frontend and backend paths are identical, we don't use the Site and Admin namespace
		// suffixes after $this->componentNamespace (so you may use FOF with JApplicationWeb apps)
		if ($this->frontEndPath == $this->backEndPath)
		{
			$frontEndNamespace = '\\' . $this->componentNamespace . '\\';
			$backEndNamespace  = '\\' . $this->componentNamespace . '\\';
		}

		// Do we have to register the component's namespaces with the autoloader?
		$autoloader = Autoloader::getInstance();

		if (!$autoloader->hasMap($frontEndNamespace))
		{
			$autoloader->addMap($frontEndNamespace, $this->frontEndPath);
		}

		if (!$autoloader->hasMap($backEndNamespace))
		{
			$autoloader->addMap($backEndNamespace, $this->backEndPath);
		}

		// Inflector service
		if (!isset($this['inflector']))
		{
			$this['inflector'] = function (Container $c) {
				return new Inflector();
			};
		}

		// Filesystem abstraction service
		if (!isset($this['filesystem']))
		{
			$this['filesystem'] = function (Container $c) {
				return new JoomlaFilesystem($c);
			};
		}

		// Platform abstraction service
		if (!isset($this['platform']))
		{
			if (empty($c['platformClass']))
			{
				$c['platformClass'] = 'FOF30\\Platform\\Joomla\\Platform';
			}

			$this['platform'] = function (Container $c) {
				$className = $c['platformClass'];

				return new $className($c);
			};
		}

		if (empty($this['thisPath']))
		{
			$this['thisPath'] = $this['frontEndPath'];

			if ($this->platform->isBackend())
			{
				$this['thisPath'] = $this['backEndPath'];
			}
		}

		// MVC Factory service
		if (!isset($this['factory']))
		{
			$this['factory'] = function (Container $c) {
				if (empty($c['factoryClass']))
				{
					$c['factoryClass'] = 'FOF30\\Factory\\BasicFactory';
				}

				if (strpos($c['factoryClass'], '\\') === false)
				{
					$class = $c->getNamespacePrefix() . 'Factory\\' . $c['factoryClass'];

					if (class_exists($class))
					{
						$c['factoryClass'] = $class;
					}
					else
					{
						$c['factoryClass'] = '\\FOF30\\Factory\\' . ucfirst($c['factoryClass']) . 'Factory';
					}
				}

				if (!class_exists($c['factoryClass'], true))
				{
					$c['factoryClass'] = 'FOF30\\Factory\\BasicFactory';
				}

				$factoryClass = $c['factoryClass'];

				/** @var FactoryInterface $factory */
				$factory = new $factoryClass($c);

				if (isset($c['section']))
				{
					$factory->setSection($c['section']);
				}

				return $factory;
			};
		}

		// Component Configuration service
		if (!isset($this['appConfig']))
		{
			$this['appConfig'] = function (Container $c) {
				$class = $c->getNamespacePrefix() . 'Configuration\\Configuration';

				if (!class_exists($class, true))
				{
					$class = '\\FOF30\\Configuration\\Configuration';
				}

				return new $class($c);
			};
		}

		// Component Params service
		if (!isset($this['params']))
		{
			$this['params'] = function (Container $c) {
				return new Params($c);
			};
		}

		// Blade view template compiler service
		if (!isset($this['blade']))
		{
			$this['blade'] = function (Container $c) {
				return new Blade($c);
			};
		}

		// Database Driver service
		if (!isset($this['db']))
		{
			$this['db'] = function (Container $c) {
				return $c->platform->getDbo();
			};
		}

		// Request Dispatcher service
		if (!isset($this['dispatcher']))
		{
			$this['dispatcher'] = function (Container $c) {
				return $c->factory->dispatcher();
			};
		}

		// Component toolbar provider
		if (!isset($this['toolbar']))
		{
			$this['toolbar'] = function (Container $c) {
				return $c->factory->toolbar();
			};
		}

		// Component toolbar provider
		if (!isset($this['transparentAuth']))
		{
			$this['transparentAuth'] = function (Container $c) {
				return $c->factory->transparentAuthentication();
			};
		}

		// View renderer
		if (!isset($this['renderer']))
		{
			$this['renderer'] = function (Container $c) {
				if (isset($c['rendererClass']) && class_exists($c['rendererClass']))
				{
					$class    = $c['rendererClass'];
					$renderer = new $class($c);

					if ($renderer instanceof RenderInterface)
					{
						return $renderer;
					}
				}

				$filesystem = $c->filesystem;

				// Try loading the stock renderers shipped with F0F
				$path        = dirname(__FILE__) . '/../Render/';
				$renderFiles = $filesystem->folderFiles($path, '.php');
				$renderer    = null;
				$priority    = 0;

				if (!empty($renderFiles))
				{
					foreach ($renderFiles as $filename)
					{
						if ($filename == 'RenderBase.php')
						{
							continue;
						}

						if ($filename == 'RenderInterface.php')
						{
							continue;
						}

						$className = 'FOF30\\Render\\' . basename($filename, '.php');

						if (!class_exists($className, true))
						{
							continue;
						}

						/** @var RenderInterface $o */
						$o = new $className($c);

						$info = $o->getInformation();

						if (!$info->enabled)
						{
							continue;
						}

						if ($info->priority > $priority)
						{
							$priority = $info->priority;
							$renderer = $o;
						}
					}
				}

				return $renderer;
			};
		}

		// Input Access service
		if (isset($this['input']) &&
			(!(is_object($this['input'])) ||
				!($this['input'] instanceof FOFInput) ||
				!($this['input'] instanceof CMSInput))
		)
		{
			if (empty($this['input']))
			{
				$this['input'] = [];
			}

			// This swap is necessary to prevent infinite recursion
			$this['rawInputData'] = array_merge($this['input']);
			unset($this['input']);

			$this['input'] = function (Container $c) {
				$input = new FOFInput($c['rawInputData']);
				unset($c['rawInputData']);

				return $input;
			};
		}

		if (!isset($this['input']))
		{
			$this['input'] = function () {
				return new FOFInput();
			};
		}

		// Session service
		if (!isset($this['session']))
		{
			$this['session'] = function (Container $c) {
				if (version_compare(JVERSION, '3.999.999', 'le'))
				{
					return Factory::getSession();
				}

				return Factory::getApplication()->getSession();
			};
		}

		// Template service
		if (!isset($this['template']))
		{
			$this['template'] = function (Container $c) {
				return new Template($c);
			};
		}

		// Media version string
		if (!isset($this['mediaVersion']))
		{
			$this['mediaVersion'] = function (Container $c) {
				return new MediaVersion($c);
			};

			if (!is_null($initMediaVersion))
			{
				$this['mediaVersion']->setMediaVersion($initMediaVersion);
			}
		}

		// Encryption / cryptography service
		if (!isset($this['crypto']))
		{
			$this['crypto'] = function (Container $c) {
				return new EncryptService($c);
			};
		}
	}

	/**
	 * Returns a container instance for a specific component. This method goes through fof.xml to read the default
	 * configuration values for the container. You are advised to use this unless you have a specific reason for
	 * instantiating a Container without going through the fof.xml file.
	 *
	 * Pass the value 'tempInstance' => true in the $values array to get a temporary instance. Otherwise you will get
	 * the cached instance of the previously created container.
	 *
	 * @param   string  $component  The component you want to get a container for, e.g. com_foobar.
	 * @param   array   $values     Container configuration overrides you want to apply. Optional.
	 * @param   string  $section    The application section (site, admin) you want to fetch. Any other value results in
	 *                              auto-detection.
	 *
	 * @return Container
	 */
	public static function &getInstance($component, array $values = [], $section = 'auto')
	{
		$tempInstance = false;

		if (isset($values['tempInstance']))
		{
			$tempInstance = $values['tempInstance'];
			unset($values['tempInstance']);
		}

		if ($tempInstance)
		{
			return self::makeInstance($component, $values, $section);
		}

		$signature = md5($component . '@' . $section);

		if (!isset(self::$instances[$signature]))
		{
			self::$instances[$signature] = self::makeInstance($component, $values, $section);
		}

		return self::$instances[$signature];
	}

	/**
	 * Returns a temporary container instance for a specific component.
	 *
	 * @param   string  $component  The component you want to get a container for, e.g. com_foobar.
	 * @param   array   $values     Container configuration overrides you want to apply. Optional.
	 * @param   string  $section    The application section (site, admin) you want to fetch. Any other value results in
	 *                              auto-detection.
	 *
	 * @return Container
	 */
	protected static function &makeInstance($component, array $values = [], $section = 'auto')
	{
		// Try to auto-detect some defaults
		$tmpConfig    = array_merge($values, ['componentName' => $component]);
		$tmpContainer = new Container($tmpConfig);

		if (!in_array($section, ['site', 'admin']))
		{
			$section = $tmpContainer->platform->isBackend() ? 'admin' : 'site';
		}

		$appConfig = $tmpContainer->appConfig;

		// Get the namespace from fof.xml
		$namespace = $appConfig->get('container.componentNamespace', null);

		// $values always overrides $namespace and fof.xml
		if (isset($values['componentNamespace']))
		{
			$namespace = $values['componentNamespace'];
		}

		// If there is no namespace set, try to guess it.
		if (empty($namespace))
		{
			$bareComponent = $component;

			if (substr($component, 0, 4) == 'com_')
			{
				$bareComponent = substr($component, 4);
			}

			$namespace = ucfirst($bareComponent);
		}

		// Get the default front-end/back-end paths
		$frontEndPath = $appConfig->get('container.frontEndPath', JPATH_SITE . '/components/' . $component);
		$backEndPath  = $appConfig->get('container.backEndPath', JPATH_ADMINISTRATOR . '/components/' . $component);

		// Parse path variables if necessary
		$frontEndPath = $tmpContainer->parsePathVariables($frontEndPath);
		$backEndPath  = $tmpContainer->parsePathVariables($backEndPath);

		// Apply path overrides
		if (isset($values['frontEndPath']))
		{
			$frontEndPath = $values['frontEndPath'];
		}

		if (isset($values['backEndPath']))
		{
			$backEndPath = $values['backEndPath'];
		}

		$thisPath = ($section == 'admin') ? $backEndPath : $frontEndPath;

		// Get the namespaces for the front-end and back-end parts of the component
		$frontEndNamespace = '\\' . $namespace . '\\Site\\';
		$backEndNamespace  = '\\' . $namespace . '\\Admin\\';

		// Special case: if the frontend and backend paths are identical, we don't use the Site and Admin namespace
		// suffixes after $this->componentNamespace (so you may use FOF with JApplicationWeb apps)
		if ($frontEndPath == $backEndPath)
		{
			$frontEndNamespace = '\\' . $namespace . '\\';
			$backEndNamespace  = '\\' . $namespace . '\\';
		}

		// Do we have to register the component's namespaces with the autoloader?
		$autoloader = Autoloader::getInstance();

		if (!$autoloader->hasMap($frontEndNamespace))
		{
			$autoloader->addMap($frontEndNamespace, $frontEndPath);
		}

		if (!$autoloader->hasMap($backEndNamespace))
		{
			$autoloader->addMap($backEndNamespace, $backEndPath);
		}

		// Get the Container class name
		$classNamespace = ($section == 'admin') ? $backEndNamespace : $frontEndNamespace;
		$class          = $classNamespace . 'Container';

		// Get the values overrides from fof.xml
		$values = array_merge([
			'factoryClass'  => '\\FOF30\\Factory\\BasicFactory',
			'platformClass' => '\\FOF30\\Platform\\Joomla\\Platform',
			'section'       => $section,
		], $values);

		$values = array_merge($values, [
			'componentName'      => $component,
			'componentNamespace' => $namespace,
			'frontEndPath'       => $frontEndPath,
			'backEndPath'        => $backEndPath,
			'thisPath'           => $thisPath,
			'rendererClass'      => $appConfig->get('container.rendererClass', null),
			'factoryClass'       => $appConfig->get('container.factoryClass', $values['factoryClass']),
			'platformClass'      => $appConfig->get('container.platformClass', $values['platformClass']),
		]);

		if (empty($values['rendererClass']))
		{
			unset ($values['rendererClass']);
		}

		$mediaVersion = $appConfig->get('container.mediaVersion', null);

		unset($appConfig);
		unset($tmpConfig);
		unset($tmpContainer);

		if (class_exists($class, true))
		{
			$container = new $class($values);
		}
		else
		{
			$container = new Container($values);
		}

		if (!is_null($mediaVersion))
		{
			$container->mediaVersion->setMediaVersion($mediaVersion);
		}

		return $container;
	}

	/**
	 * The container SHOULD NEVER be serialised. If this happens, it means that any of the installed version is doing
	 * something REALLY BAD, so let's die and inform the user of what it's going on.
	 */
	public function __sleep()
	{
		// If the site is in debug mode we die and let the user figure it out
		if (defined('JDEBUG') && JDEBUG)
		{
			$msg = <<< END
Something on your site is broken and tries to save the plugin state in the cache. This is a major security issue and
will cause your site to not work properly. Go to your site's backend, Global Configuration and set Caching to OFF as a
temporary solution. Possible causes: older versions of JoomlaShine templates, JomSocial, BetterPreview and other third
party Joomla! extensions. 
END;

			die($msg);
		}

		// Otherwise we serialise the Container
		return ['values', 'factories', 'protected', 'frozen', 'raw', 'keys'];
	}

	/**
	 * Get the applicable namespace prefix for a component section. Possible sections:
	 * auto            Auto-detect which is the current component section
	 * inverse      The inverse area than auto
	 * site            Frontend
	 * admin        Backend
	 *
	 * @param   string  $section  The section you want to get information for
	 *
	 * @return  string  The namespace prefix for the component's classes, e.g. \Foobar\Example\Site\
	 */
	public function getNamespacePrefix($section = 'auto')
	{
		// Get the namespaces for the front-end and back-end parts of the component
		$frontEndNamespace = '\\' . $this->componentNamespace . '\\Site\\';
		$backEndNamespace  = '\\' . $this->componentNamespace . '\\Admin\\';

		// Special case: if the frontend and backend paths are identical, we don't use the Site and Admin namespace
		// suffixes after $this->componentNamespace (so you may use FOF with JApplicationWeb apps)
		if ($this->frontEndPath == $this->backEndPath)
		{
			$frontEndNamespace = '\\' . $this->componentNamespace . '\\';
			$backEndNamespace  = '\\' . $this->componentNamespace . '\\';
		}

		switch ($section)
		{
			default:
			case 'auto':
				if ($this->platform->isBackend())
				{
					return $backEndNamespace;
				}
				else
				{
					return $frontEndNamespace;
				}
				break;

			case 'inverse':
				if ($this->platform->isBackend())
				{
					return $frontEndNamespace;
				}
				else
				{
					return $backEndNamespace;
				}
				break;

			case 'site':
				return $frontEndNamespace;
				break;

			case 'admin':
				return $backEndNamespace;
				break;
		}
	}

	public function parsePathVariables($path)
	{
		$platformDirs = $this->platform->getPlatformBaseDirs();
		// root public admin tmp log

		$search  = array_map(function ($x) {
			return '%' . strtoupper($x) . '%';
		}, array_keys($platformDirs));
		$replace = array_values($platformDirs);

		return str_replace($search, $replace, $path);
	}
}
Params/Params.php000060400000005707152455514240007737 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Params;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Utils\CacheCleaner;
use Joomla\Registry\Registry;

/**
 * A helper class to quickly get the component parameters
 */
class Params
{

	/** @var  Container  The container we belong to */
	protected $container = null;

	/**
	 * Cached component parameters
	 *
	 * @var Registry
	 */
	private $params = null;

	/**
	 * Public constructor for the params object
	 *
	 * @param   Container  $container  The container we belong to
	 */
	public function __construct(Container $container)
	{
		$this->container = $container;

		$this->reload();
	}

	/**
	 * Reload the params
	 */
	public function reload()
	{
		$db = $this->container->db;

		$sql  = $db->getQuery(true)
			->select($db->qn('params'))
			->from($db->qn('#__extensions'))
			->where($db->qn('type') . " = " . $db->q('component'))
			->where($db->qn('element') . " = " . $db->q($this->container->componentName));
		$json = $db->setQuery($sql)->loadResult();

		$this->params = new Registry($json);
	}

	/**
	 * Returns the value of a component configuration parameter
	 *
	 * @param   string  $key      The parameter to get
	 * @param   mixed   $default  Default value
	 *
	 * @return  mixed
	 */
	public function get($key, $default = null)
	{
		return $this->params->get($key, $default);
	}

	/**
	 * Returns a copy of the loaded component parameters as an array
	 *
	 * @return  array
	 */
	public function getParams()
	{
		return $this->params->toArray();
	}

	/**
	 * Sets the value of multiple component configuration parameters at once
	 *
	 * @param   array  $params  The parameters to set
	 *
	 * @return  void
	 */
	public function setParams(array $params)
	{
		foreach ($params as $key => $value)
		{
			$this->params->set($key, $value);
		}
	}

	/**
	 * Sets the value of a component configuration parameter
	 *
	 * @param   string  $key    The parameter to set
	 * @param   mixed   $value  The value to set
	 *
	 * @return  void
	 */
	public function set($key, $value)
	{
		$this->setParams([$key => $value]);
	}

	/**
	 * Actually Save the params into the db
	 */
	public function save()
	{
		$db   = $this->container->db;
		$data = $this->params->toString();

		$sql = $db->getQuery(true)
			->update($db->qn('#__extensions'))
			->set($db->qn('params') . ' = ' . $db->q($data))
			->where($db->qn('element') . ' = ' . $db->q($this->container->componentName))
			->where($db->qn('type') . ' = ' . $db->q('component'));

		$db->setQuery($sql);

		try
		{
			$db->execute();
			// The component parameters are cached. We just changed them. Therefore we MUST reset the system cache which holds them.
			CacheCleaner::clearCacheGroups(['_system'], $this->container->platform->isBackend() ? [0] : [1]);
		}
		catch (Exception $e)
		{
			// Don't sweat if it fails
		}
	}
}
Date/DateDecorator.php000060400000010406152455514240010656 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Date;

defined('_JEXEC') || die;

use DateTime;
use JDatabaseDriver;

/**
 * This decorator will get any DateTime descendant and turn it into a FOF30\Date\Date compatible class. If the methods
 * specific to Date/JDate are available they will be used. Otherwise a new Date object will be spun from the information
 * in the decorated DateTime object and the results of a call to its method will be returned.
 */
class DateDecorator extends Date
{
	/**
	 * The decorated object
	 *
	 * @var   DateTime
	 */
	protected $decorated = null;

	public function __construct($date = 'now', $tz = null)
	{
		if (is_object($date) && ($date instanceof DateTime))
		{
			$this->decorated = $date;
		}
		else
		{
			$this->decorated = new Date($date, $tz);
		}

		$timestamp = $this->decorated->toISO8601(true);

		parent::__construct($timestamp);

		$this->setTimezone($this->decorated->getTimezone());

		return;
	}

	public static function getInstance($date = 'now', $tz = null)
	{
		$coreObject = new Date($date, $tz);

		return new DateDecorator($coreObject);
	}

	/**
	 * Magic method to access properties of the date given by class to the format method.
	 *
	 * @param   string  $name  The name of the property.
	 *
	 * @return  mixed   A value if the property name is valid, null otherwise.
	 */
	public function __get($name)
	{
		return $this->decorated->$name;
	}

	public function __call($name, $arguments)
	{
		if (method_exists($this->decorated, $name))
		{
			return call_user_func_array([$this->decorated, $name], $arguments);
		}

		throw new \InvalidArgumentException("JDate object does not have a $name method");
	}

	public function sub($interval)
	{
		// Note to self: ignore phpStorm; we must NOT use a typehint for $interval
		return $this->decorated->sub($interval);
	}

	public function add($interval)
	{
		// Note to self: ignore phpStorm; we must NOT use a typehint for $interval
		return $this->decorated->add($interval);
	}

	public function modify($modify)
	{
		return $this->decorated->modify($modify);
	}

	public function __toString()
	{
		return (string) $this->decorated;
	}

	public function dayToString($day, $abbr = false)
	{
		return $this->decorated->dayToString($day, $abbr);
	}

	public function calendar($format, $local = false, $translate = true)
	{
		return $this->decorated->calendar($format, $local, $translate);
	}

	public function format($format, $local = false, $translate = true)
	{
		if (($this->decorated instanceof Date) || ($this->decorated instanceof \Joomla\CMS\Date\Date))
		{
			return $this->decorated->format($format, $local, $translate);
		}

		return $this->decorated->format($format);
	}

	public function getOffsetFromGmt($hours = false)
	{
		return $this->decorated->getOffsetFromGMT($hours);
	}

	public function monthToString($month, $abbr = false)
	{
		return $this->monthToString($month, $abbr);
	}

	public function setTimezone($tz)
	{
		return $this->decorated->setTimezone($tz);
	}

	public function getTimezone()
	{
		return $this->decorated->getTimezone();
	}

	public function getOffset()
	{
		return $this->decorated->getOffset();
	}

	public function toISO8601($local = false)
	{
		return $this->decorated->toISO8601($local);
	}

	public function toSql($local = false, JDatabaseDriver $db = null)
	{
		return $this->decorated->toSql($local, $db);
	}

	public function toRFC822($local = false)
	{
		return $this->decorated->toRFC822($local);
	}

	public function toUnix()
	{
		return $this->decorated->toUnix();
	}

	public function getTimestamp()
	{
		return $this->decorated->getTimestamp();
	}

	public function setTime($hour, $minute, $second = 0, $microseconds = 0)
	{
		return $this->decorated->setTime($hour, $minute, $second, $microseconds);
	}

	public function setDate($year, $month, $day)
	{
		return $this->decorated->setDate($year, $month, $day);
	}

	public function setISODate($year, $week, $day = 1)
	{
		return $this->decorated->setISODate($year, $week, $day);
	}

	public function setTimestamp($unixtimestamp)
	{
		return $this->decorated->setTimestamp($unixtimestamp);
	}

	public function diff($datetime2, $absolute = false)
	{
		return $this->decorated->diff($datetime2, $absolute);
	}
}
Date/Date.php000060400000032630152455514240007016 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Date;

defined('_JEXEC') || die;

use DateInterval;
use DateTime;
use DateTimeZone;
use JDatabaseDriver;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;

/**
 * The Date class is a fork of Joomla's JDate. We had to fork that code in April 2017 when Joomla! 7.0 was released
 * with
 * an untested change that completely broke date handling on PHP 7.0 and earlier versions. Since we can no longer trust
 * Joomla's core date and time handling we are providing our own, stable code.
 *
 * Date is a class that stores a date and provides logic to manipulate and render that date in a variety of formats.
 *
 * @method  Date|bool  add(DateInterval $interval)  Adds an amount of days, months, years, hours, minutes and seconds
 *          to a JDate object.
 * @method  Date|bool  sub(DateInterval $interval)  Subtracts an amount of days, months, years, hours, minutes and
 *          seconds from a JDate object.
 * @method  Date|bool  modify($modify)              Alter the timestamp of this object by incre-/decrementing in a
 *          format accepted by strtotime().
 *
 * @property-read  string  $daysinmonth   t - Number of days in the given month.
 * @property-read  string  $dayofweek     N - ISO-8601 numeric representation of the day of the week.
 * @property-read  string  $dayofyear     z - The day of the year (starting from 0).
 * @property-read  boolean $isleapyear    L - Whether it's a leap year.
 * @property-read  string  $day           d - Day of the month, 2 digits with leading zeros.
 * @property-read  string  $hour          H - 24-hour format of an hour with leading zeros.
 * @property-read  string  $minute        i - Minutes with leading zeros.
 * @property-read  string  $second        s - Seconds with leading zeros.
 * @property-read  string  $microsecond   u - Microseconds with leading zeros.
 * @property-read  string  $month         m - Numeric representation of a month, with leading zeros.
 * @property-read  string  $ordinal       S - English ordinal suffix for the day of the month, 2 characters.
 * @property-read  string  $week          W - ISO-8601 week number of year, weeks starting on Monday.
 * @property-read  string  $year          Y - A full numeric representation of a year, 4 digits.
 */
class Date extends DateTime
{
	public const DAY_ABBR = "\x021\x03";
	public const DAY_NAME = "\x022\x03";
	public const MONTH_ABBR = "\x023\x03";
	public const MONTH_NAME = "\x024\x03";

	/**
	 * The format string to be applied when using the __toString() magic method.
	 *
	 * @var    string
	 */
	public static $format = 'Y-m-d H:i:s';

	/**
	 * Placeholder for a DateTimeZone object with GMT as the time zone.
	 *
	 * @var    object
	 */
	protected static $gmt;

	/**
	 * Placeholder for a DateTimeZone object with the default server
	 * time zone as the time zone.
	 *
	 * @var    object
	 */
	protected static $stz;

	/**
	 * The DateTimeZone object for usage in rending dates as strings.
	 *
	 * @var    DateTimeZone
	 */
	protected $tz;

	/**
	 * Constructor.
	 *
	 * @param   string  $date  String in a format accepted by strtotime(), defaults to "now".
	 * @param   mixed   $tz    Time zone to be used for the date. Might be a string or a DateTimeZone object.
	 */
	public function __construct($date = 'now', $tz = null)
	{
		// Create the base GMT and server time zone objects.
		if (empty(self::$gmt) || empty(self::$stz))
		{
			self::$gmt = new DateTimeZone('GMT');
			self::$stz = new DateTimeZone(@date_default_timezone_get());
		}

		// If the time zone object is not set, attempt to build it.
		if (!($tz instanceof DateTimeZone))
		{
			if ($tz === null)
			{
				$tz = self::$gmt;
			}
			elseif (is_string($tz))
			{
				$tz = new DateTimeZone($tz);
			}
		}

		// On PHP 7.1 and later use an integer timestamp, without microseconds, to preserve backwards compatibility.
		// See http://php.net/manual/en/migration71.incompatible.php#migration71.incompatible.datetime-microseconds
		if ($date === 'now' && version_compare(PHP_VERSION, '7.1.0', '>='))
		{
			$date = time();
		}

		// If the date is numeric assume a unix timestamp and convert it.
		date_default_timezone_set('UTC');
		$date = is_numeric($date) ? date('c', $date) : $date;

		// Call the DateTime constructor.
		parent::__construct($date, $tz);

		// Reset the timezone for 3rd party libraries/extension that does not use JDate
		date_default_timezone_set(self::$stz->getName());

		// Set the timezone object for access later.
		$this->tz = $tz;
	}

	/**
	 * Proxy for new JDate().
	 *
	 * @param   string  $date  String in a format accepted by strtotime(), defaults to "now".
	 * @param   mixed   $tz    Time zone to be used for the date.
	 *
	 * @return  Date
	 */
	public static function getInstance($date = 'now', $tz = null)
	{
		return new Date($date, $tz);
	}

	/**
	 * Magic method to access properties of the date given by class to the format method.
	 *
	 * @param   string  $name  The name of the property.
	 *
	 * @return  mixed   A value if the property name is valid, null otherwise.
	 */
	public function __get($name)
	{
		$value = null;

		switch ($name)
		{
			case 'daysinmonth':
				$value = $this->format('t', true);
				break;

			case 'dayofweek':
				$value = $this->format('N', true);
				break;

			case 'dayofyear':
				$value = $this->format('z', true);
				break;

			case 'isleapyear':
				$value = (boolean) $this->format('L', true);
				break;

			case 'day':
				$value = $this->format('d', true);
				break;

			case 'hour':
				$value = $this->format('H', true);
				break;

			case 'minute':
				$value = $this->format('i', true);
				break;

			case 'second':
				$value = $this->format('s', true);
				break;

			case 'month':
				$value = $this->format('m', true);
				break;

			case 'ordinal':
				$value = $this->format('S', true);
				break;

			case 'week':
				$value = $this->format('W', true);
				break;

			case 'year':
				$value = $this->format('Y', true);
				break;

			default:
				$trace = debug_backtrace();
				trigger_error(
					'Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'],
					E_USER_NOTICE
				);
		}

		return $value;
	}

	/**
	 * Magic method to render the date object in the format specified in the public
	 * static member JDate::$format.
	 *
	 * @return  string  The date as a formatted string.
	 */
	public function __toString()
	{
		return (string) parent::format(self::$format);
	}

	/**
	 * Translates day of week number to a string.
	 *
	 * @param   integer  $day   The numeric day of the week.
	 * @param   boolean  $abbr  Return the abbreviated day string?
	 *
	 * @return  string  The day of the week.
	 */
	public function dayToString($day, $abbr = false)
	{
		switch ($day)
		{
			case 0:
			default:
				return $abbr ? Text::_('SUN') : Text::_('SUNDAY');
			case 1:
				return $abbr ? Text::_('MON') : Text::_('MONDAY');
			case 2:
				return $abbr ? Text::_('TUE') : Text::_('TUESDAY');
			case 3:
				return $abbr ? Text::_('WED') : Text::_('WEDNESDAY');
			case 4:
				return $abbr ? Text::_('THU') : Text::_('THURSDAY');
			case 5:
				return $abbr ? Text::_('FRI') : Text::_('FRIDAY');
			case 6:
				return $abbr ? Text::_('SAT') : Text::_('SATURDAY');
		}
	}

	/**
	 * Gets the date as a formatted string in a local calendar.
	 *
	 * @param   string   $format     The date format specification string (see {@link PHP_MANUAL#date})
	 * @param   boolean  $local      True to return the date string in the local time zone, false to return it in GMT.
	 * @param   boolean  $translate  True to translate localised strings
	 *
	 * @return  string   The date string in the specified format format.
	 */
	public function calendar($format, $local = false, $translate = true)
	{
		return $this->format($format, $local, $translate);
	}

	/**
	 * Gets the date as a formatted string.
	 *
	 * @param   string   $format     The date format specification string (see {@link PHP_MANUAL#date})
	 * @param   boolean  $local      True to return the date string in the local time zone, false to return it in GMT.
	 * @param   boolean  $translate  True to translate localised strings
	 *
	 * @return  string   The date string in the specified format format.
	 */
	public function format($format, $local = false, $translate = true)
	{
		if ($translate)
		{
			// Do string replacements for date format options that can be translated.
			$format = preg_replace('/(^|[^\\\])D/', "\\1" . self::DAY_ABBR, $format);
			$format = preg_replace('/(^|[^\\\])l/', "\\1" . self::DAY_NAME, $format);
			$format = preg_replace('/(^|[^\\\])M/', "\\1" . self::MONTH_ABBR, $format);
			$format = preg_replace('/(^|[^\\\])F/', "\\1" . self::MONTH_NAME, $format);
		}

		// If the returned time should not be local use GMT.
		if ($local == false && !empty(self::$gmt))
		{
			parent::setTimezone(self::$gmt);
		}

		// Format the date.
		$return = parent::format($format);

		if ($translate)
		{
			// Manually modify the month and day strings in the formatted time.
			if (strpos($return, self::DAY_ABBR) !== false)
			{
				$return = str_replace(self::DAY_ABBR, $this->dayToString(parent::format('w'), true), $return);
			}

			if (strpos($return, self::DAY_NAME) !== false)
			{
				$return = str_replace(self::DAY_NAME, $this->dayToString(parent::format('w')), $return);
			}

			if (strpos($return, self::MONTH_ABBR) !== false)
			{
				$return = str_replace(self::MONTH_ABBR, $this->monthToString(parent::format('n'), true), $return);
			}

			if (strpos($return, self::MONTH_NAME) !== false)
			{
				$return = str_replace(self::MONTH_NAME, $this->monthToString(parent::format('n')), $return);
			}
		}

		if ($local == false && !empty($this->tz))
		{
			parent::setTimezone($this->tz);
		}

		return $return;
	}

	/**
	 * Get the time offset from GMT in hours or seconds.
	 *
	 * @param   boolean  $hours  True to return the value in hours.
	 *
	 * @return  float  The time offset from GMT either in hours or in seconds.
	 */
	public function getOffsetFromGmt($hours = false)
	{
		return (float) $hours ? ($this->tz->getOffset($this) / 3600) : $this->tz->getOffset($this);
	}

	/**
	 * Translates month number to a string.
	 *
	 * @param   integer  $month  The numeric month of the year.
	 * @param   boolean  $abbr   If true, return the abbreviated month string
	 *
	 * @return  string  The month of the year.
	 */
	public function monthToString($month, $abbr = false)
	{
		switch ($month)
		{
			case 1:
			default:
				return $abbr ? Text::_('JANUARY_SHORT') : Text::_('JANUARY');
			case 2:
				return $abbr ? Text::_('FEBRUARY_SHORT') : Text::_('FEBRUARY');
			case 3:
				return $abbr ? Text::_('MARCH_SHORT') : Text::_('MARCH');
			case 4:
				return $abbr ? Text::_('APRIL_SHORT') : Text::_('APRIL');
			case 5:
				return $abbr ? Text::_('MAY_SHORT') : Text::_('MAY');
			case 6:
				return $abbr ? Text::_('JUNE_SHORT') : Text::_('JUNE');
			case 7:
				return $abbr ? Text::_('JULY_SHORT') : Text::_('JULY');
			case 8:
				return $abbr ? Text::_('AUGUST_SHORT') : Text::_('AUGUST');
			case 9:
				return $abbr ? Text::_('SEPTEMBER_SHORT') : Text::_('SEPTEMBER');
			case 10:
				return $abbr ? Text::_('OCTOBER_SHORT') : Text::_('OCTOBER');
			case 11:
				return $abbr ? Text::_('NOVEMBER_SHORT') : Text::_('NOVEMBER');
			case 12:
				return $abbr ? Text::_('DECEMBER_SHORT') : Text::_('DECEMBER');
		}
	}

	/**
	 * Method to wrap the setTimezone() function and set the internal time zone object.
	 *
	 * @param   DateTimeZone  $tz  The new DateTimeZone object.
	 *
	 * @return  Date
	 *
	 * @note    This method can't be type hinted due to a PHP bug: https://bugs.php.net/bug.php?id=61483
	 */
	public function setTimezone($tz)
	{
		$this->tz = $tz;

		date_timezone_set($this, $tz);

		return $this;
	}

	/**
	 * Gets the date as an ISO 8601 string.  IETF RFC 3339 defines the ISO 8601 format
	 * and it can be found at the IETF Web site.
	 *
	 * @param   boolean  $local  True to return the date string in the local time zone, false to return it in GMT.
	 *
	 * @return  string  The date string in ISO 8601 format.
	 *
	 * @link    http://www.ietf.org/rfc/rfc3339.txt
	 */
	public function toISO8601($local = false)
	{
		return $this->format(DateTime::RFC3339, $local, false);
	}

	/**
	 * Gets the date as an SQL datetime string.
	 *
	 * @param   boolean          $local  True to return the date string in the local time zone, false to return it in
	 *                                   GMT.
	 * @param   JDatabaseDriver  $db     The database driver or null to use JFactory::getDbo()
	 *
	 * @return  string     The date string in SQL datetime format.
	 *
	 * @link    http://dev.mysql.com/doc/refman/5.0/en/datetime.html
	 */
	public function toSql($local = false, JDatabaseDriver $db = null)
	{
		if ($db === null)
		{
			$db = Factory::getDbo();
		}

		return $this->format($db->getDateFormat(), $local, false);
	}

	/**
	 * Gets the date as an RFC 822 string.  IETF RFC 2822 supersedes RFC 822 and its definition
	 * can be found at the IETF Web site.
	 *
	 * @param   boolean  $local  True to return the date string in the local time zone, false to return it in GMT.
	 *
	 * @return  string   The date string in RFC 822 format.
	 *
	 * @link    http://www.ietf.org/rfc/rfc2822.txt
	 */
	public function toRFC822($local = false)
	{
		return $this->format(DateTime::RFC2822, $local, false);
	}

	/**
	 * Gets the date as UNIX time stamp.
	 *
	 * @return  integer  The date as a UNIX timestamp.
	 */
	public function toUnix()
	{
		return (int) parent::format('U');
	}
}
Controller/Controller.php000060400000075466152455514240011550 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Controller\Exception\CannotGetName;
use FOF30\Controller\Exception\TaskNotFound;
use FOF30\Model\Model;
use FOF30\View\View;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\Controller\ViewController;
use Joomla\CMS\Cache\Exception\CacheExceptionInterface;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;

/**
 * Class Controller
 *
 * A generic MVC controller implementation
 *
 * @property-read  \FOF30\Input\Input $input  The input object (magic __get returns the Input from the Container)
 */
class Controller
{
	/**
	 * Instance container.
	 *
	 * @var    Controller
	 */
	protected static $instance;
	/**
	 * The name of the controller
	 *
	 * @var    array
	 */
	protected $name = null;
	/**
	 * The mapped task that was performed.
	 *
	 * @var    string
	 */
	protected $doTask;
	/**
	 * Bit mask to enable routing through JRoute on redirects. The value can be:
	 *
	 * 0 = never
	 * 1 = frontend only
	 * 2 = backend  only
	 * 3 = always
	 *
	 * @var    int
	 */
	protected $autoRouting = 0;
	/**
	 * Should I protect against state bleedover? When this is enabled the default model's state hash will be
	 * automatically set to include the controller name i.e. `com_example.controllerName.modelName.` instead of
	 * `com_example.modelName.`. This will happen ONLY if the preventStateBleedover flag is set, the controller and
	 * model names are different and the model doesn't set its own hash (or override getHash altogether).
	 *
	 * You should only need to enable this feature when you have multiple controllers using the _same_ Model as their
	 * default. For example, if you have a blog component with Latest and Posts Controllers, both using the Posts Model
	 * as their default Model the state variables set in the latest posts page would bleed over to the posts page. This
	 * can include filtering and pagination preferences, resulting in a confusing experience for the user.
	 *
	 * Caveat: if you are using a different Controller class for singular / plural view names you will need to override
	 *         getModel() yourself. Otherwise the state of the singular view would be disjointed from the state of the
	 *         plural view (since the Controller names are different). That's the reason why this feature is turned off
	 *         by default.
	 *
	 * False = same behavior as FOF 3.0.0 to 3.1.1 inclusive.
	 *
	 * @var   bool
	 */
	protected $preventStateBleedover = false;
	/**
	 * Redirect message.
	 *
	 * @var    string
	 */
	protected $message;
	/**
	 * Redirect message type.
	 *
	 * @var    string
	 */
	protected $messageType;
	/**
	 * Array of class methods
	 *
	 * @var    array
	 */
	protected $methods;
	/**
	 * The set of search directories for resources (views).
	 *
	 * @var    array
	 */
	protected $paths;
	/**
	 * URL for redirection.
	 *
	 * @var    string
	 */
	protected $redirect;
	/**
	 * Current or most recently performed task.
	 *
	 * @var    string
	 */
	protected $task;
	/**
	 * Array of class methods to call for a given task.
	 *
	 * @var    array
	 */
	protected $taskMap;
	/**
	 * The current view name; you can override it in the configuration
	 *
	 * @var string
	 */
	protected $view = '';

	/**
	 * The current layout; you can override it in the configuration
	 *
	 * @var string
	 */
	protected $layout = null;

	/**
	 * A cached copy of the class configuration parameter passed during initialisation
	 *
	 * @var array
	 */
	protected $config = [];

	/**
	 * Overrides the name of the view's default model
	 *
	 * @var string
	 */
	protected $modelName = null;

	/**
	 * Overrides the name of the view's default view
	 *
	 * @var string
	 */
	protected $viewName = null;

	/**
	 * An array of Model instances known to this Controller
	 *
	 * @var   array[Model]
	 */
	protected $modelInstances = [];

	/**
	 * An array of View instances known to this Controller
	 *
	 * @var   array[View]
	 */
	protected $viewInstances = [];

	/**
	 * The container attached to this Controller
	 *
	 * @var Container
	 */
	protected $container = null;

	/**
	 * The tasks for which caching should be enabled by default
	 *
	 * @var array
	 */
	protected $cacheableTasks = [];

	/**
	 * How user group membership affects caching. The values are:
	 * - 0 : Not taken into account, everyone sees the same page, always
	 * - 1 : Only user groups are taken into account (default behaviour of FOF 3.0 to 3.4.2)
	 * - 2 : The user ID itself is taken into account
	 *
	 * @var   bool
	 * @since 3.4.3
	 */
	protected $userCaching = 1;

	/**
	 * An associative array for required ACL privileges per task. For example:
	 * array(
	 *   'edit' => 'core.edit',
	 *   'jump' => 'foobar.jump',
	 *   'alwaysallow' => 'true',
	 *   'neverallow' => 'false'
	 * );
	 *
	 * You can use the notation '@task' which means 'apply the same privileges as "task"'. If you create a reference
	 * back to yourself (e.g. 'mytask' => array('@mytask')) it will return TRUE.
	 *
	 * @var array
	 */
	protected $taskPrivileges = [];

	/**
	 * Enable CSRF protection on selected tasks. The possible values are:
	 *
	 * 0    Disabled; no token checks are performed
	 * 1    Enabled; token checks are always performed
	 * 2    Only on HTML requests and backend; token checks are always performed in the back-end and in the front-end
	 * only when format is 'html'
	 * 3    Only on back-end; token checks are performed only in the back-end
	 *
	 * @var    integer
	 */
	protected $csrfProtection = 2;

	/**
	 * Public constructor of the Controller class. You can pass the following variables in the $config array:
	 * name            string  The name of the Controller. Default: auto detect from the class name
	 * default_task    string  The task to use when none is specified. Default: main
	 * autoRouting     int     See the autoRouting property
	 * csrfProtection  int     See the csrfProtection property
	 * viewName        string  The view name. Default: the same as the controller name
	 * modelName       string  The model name. Default: the same as the controller name
	 * viewConfig      array   The configuration overrides for the View.
	 * modelConfig     array   The configuration overrides for the Model.
	 *
	 * @param   Container  $container  The application container
	 * @param   array      $config     The configuration array
	 *
	 * @return  Controller
	 */
	public function __construct(Container $container, array $config = [])
	{
		// Initialise
		$this->methods     = [];
		$this->message     = null;
		$this->messageType = 'message';
		$this->paths       = [];
		$this->redirect    = null;
		$this->taskMap     = [];

		// Get a local copy of the container
		$this->container = $container;

		// Determine the methods to exclude from the base class.
		$xMethods = get_class_methods('\\FOF30\\Controller\\Controller');

		// Get the public methods in this class using reflection.
		$r        = new \ReflectionClass($this);
		$rMethods = $r->getMethods(\ReflectionMethod::IS_PUBLIC);

		foreach ($rMethods as $rMethod)
		{
			$mName = $rMethod->getName();

			// If the developer screwed up and declared one of the helper method public do NOT make them available as
			// tasks.
			if ((substr($mName, 0, 8) == 'onBefore') || (substr($mName, 0, 7) == 'onAfter') || substr($mName, 0, 1) == '_')
			{
				continue;
			}

			// Add default display method if not explicitly declared.
			if (!in_array($mName, $xMethods) || $mName == 'display' || $mName == 'main')
			{
				$this->methods[] = $mName;

				// Auto register the methods as tasks.
				$this->taskMap[$mName] = $mName;
			}
		}

		if (isset($config['name']))
		{
			$this->name = $config['name'];
		}

		// Get the default values for the component and view names
		$this->view   = $this->getName();
		$this->layout = $this->input->getCmd('layout', null);

		// If the default task is set, register it as such
		if (array_key_exists('default_task', $config) && !empty($config['default_task']))
		{
			$this->registerDefaultTask($config['default_task']);
		}
		else
		{
			$this->registerDefaultTask('main');
		}

		// Cache the config
		$this->config = $config;

		// Set any model/view name overrides
		if (array_key_exists('viewName', $config) && !empty($config['viewName']))
		{
			$this->setViewName($config['viewName']);
		}

		if (array_key_exists('modelName', $config) && !empty($config['modelName']))
		{
			$this->setModelName($config['modelName']);
		}

		// Apply the autoRouting preference
		if (array_key_exists('autoRouting', $config))
		{
			$this->autoRouting = (int) $config['autoRouting'];
		}

		// Apply the csrfProtection preference
		if (array_key_exists('csrfProtection', $config))
		{
			$this->csrfProtection = (int) $config['csrfProtection'];
		}

		// Apply the preventStateBleedover preference
		if (array_key_exists('preventStateBleedover', $config))
		{
			$this->preventStateBleedover = (bool) ((int) $config['preventStateBleedover']);
		}
	}

	/**
	 * Magic get method. Handles magic properties:
	 * $this->input  mapped to $this->container->input
	 *
	 * @param   string  $name  The property to fetch
	 *
	 * @return  mixed|null
	 */
	public function __get($name)
	{
		// Handle $this->input
		if ($name == 'input')
		{
			return $this->container->input;
		}

		// Property not found; raise error
		$trace = debug_backtrace();
		trigger_error(
			'Undefined property via __get(): ' . $name .
			' in ' . $trace[0]['file'] .
			' on line ' . $trace[0]['line'],
			E_USER_NOTICE);

		return null;
	}

	/**
	 * Executes a given controller task. The onBefore<task> and onAfter<task>
	 * methods are called automatically if they exist.
	 *
	 * @param   string  $task  The task to execute, e.g. "browse"
	 *
	 * @return  null|bool  False on execution failure
	 *
	 * @throws  TaskNotFound  When the task is not found
	 */
	public function execute($task)
	{
		$this->task = $task;

		if (!isset($this->taskMap[$task]) && !isset($this->taskMap['__default']))
		{
			throw new TaskNotFound(Text::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 404);
		}

		$result = $this->triggerEvent('onBeforeExecute', [&$task]);

		if ($result === false)
		{
			return false;
		}

		$eventName = 'onBefore' . ucfirst($task);
		$result    = $this->triggerEvent($eventName);

		if ($result === false)
		{
			return false;
		}

		// Do not allow the display task to be directly called
		if (isset($this->taskMap[$task]))
		{
			$doTask = $this->taskMap[$task];
		}
		elseif (isset($this->taskMap['__default']))
		{
			$doTask = $this->taskMap['__default'];
		}
		else
		{
			$doTask = null;
		}

		// Record the actual task being fired
		$this->doTask = $doTask;

		$ret = $this->$doTask();

		$eventName = 'onAfter' . ucfirst($task);
		$result    = $this->triggerEvent($eventName);

		if ($result === false)
		{
			return false;
		}

		$result = $this->triggerEvent('onAfterExecute', [$task]);

		if ($result === false)
		{
			return false;
		}

		return $ret;
	}

	/**
	 * Default task. Assigns a model to the view and asks the view to render
	 * itself.
	 *
	 * YOU MUST NOT USE THIS TASK DIRECTLY IN A URL. It is supposed to be
	 * used ONLY inside your code. In the URL, use task=browse instead.
	 *
	 * @param   bool    $cachable   Is this view cacheable?
	 * @param   bool    $urlparams  Add your safe URL parameters (see further down in the code)
	 * @param   string  $tpl        The name of the template file to parse
	 *
	 * @return  void
	 */
	public function display($cachable = false, $urlparams = false, $tpl = null)
	{
		$document = $this->container->platform->getDocument();

		if ($document instanceof Document)
		{
			$viewType = $document->getType();
		}
		else
		{
			$viewType = $this->input->getCmd('format', 'html');
		}

		$view = $this->getView();
		$view->setTask($this->task);
		$view->setDoTask($this->doTask);

		// Get/Create the model
		if ($model = $this->getModel())
		{
			// Push the model into the view (as default)
			$view->setDefaultModel($model);
		}

		// Set the layout
		if (!is_null($this->layout))
		{
			$view->setLayout($this->layout);
		}

		$conf = $this->container->platform->getConfig();

		if ($cachable && ($viewType != 'feed') && ($conf->get('caching') >= 1))
		{
			// Get a JCache object
			$option = $this->input->get('option', 'com_foobar', 'cmd');

			// Set up a cache ID based on component, view, task and user group assignment
			$user = $this->container->platform->getUser();

			if ($user->guest)
			{
				$groups = [];
			}
			else
			{
				$groups = $user->groups;
			}

			$userId = $user->guest ? 0 : $user->id;

			switch ($this->userCaching)
			{
				case 0:
					// Developer chose to apply the same caching to everyone
					$groups = [];
					$userId = 0;
					break;

				case 1:
					// Developer chose to apply caching per user group membership only
					$userId = 0;
					break;
			}

			$importantParameters = [];

			// Set up safe URL parameters
			if (!is_array($urlparams))
			{
				$urlparams = [
					'option' => 'CMD',
					'view'   => 'CMD',
					'task'   => 'CMD',
					'format' => 'CMD',
					'layout' => 'CMD',
					'id'     => 'INT',
				];
			}

			if (is_array($urlparams))
			{
				/** @var CMSApplication $app */
				$app = Factory::getApplication();

				$registeredurlparams = null;

				if (!empty($app->registeredurlparams))
				{
					$registeredurlparams = $app->registeredurlparams;
				}
				else
				{
					$registeredurlparams = new \stdClass;
				}

				foreach ($urlparams as $key => $value)
				{
					// Add your safe url parameters with variable type as value {@see JFilterInput::clean()}.
					$registeredurlparams->$key = $value;

					// Add the URL-important parameters into the array
					$importantParameters[$key] = $this->input->get($key, null, $value);
				}

				$app->registeredurlparams = $registeredurlparams;
			}

			// Create the cache ID after setting the registered URL params, as they are used to generate the ID
			$cacheId = md5(serialize([
				Cache::makeId(), $view->getName(), $this->doTask, $groups, $userId, $importantParameters,
			]));

			// Get the cached view or cache the current view
			try
			{
				/** @var ViewController $cache */
				$cache = Factory::getCache($option, 'view');
				$cache->get($view, 'display', $cacheId);
			}
			catch (CacheExceptionInterface $e)
			{
				// Display without caching
				$view->display($tpl);
			}
		}
		else
		{
			// Display without caching
			$view->display($tpl);
		}
	}

	/**
	 * Alias to the display() task
	 *
	 * @codeCoverageIgnore
	 */
	public function main()
	{
		$this->display();
	}

	/**
	 * Returns a named Model object
	 *
	 * @param   string  $name    The Model name. If null we'll use the modelName
	 *                           variable or, if it's empty, the same name as
	 *                           the Controller
	 * @param   array   $config  Configuration parameters to the Model. If skipped
	 *                           we will use $this->config
	 *
	 * @return  Model  The instance of the Model known to this Controller
	 */
	public function getModel($name = null, $config = [])
	{
		if (!empty($name))
		{
			$modelName = $name;
		}
		elseif (!empty($this->modelName))
		{
			$modelName = $this->modelName;
		}
		else
		{
			$modelName = $this->view;
		}

		if (!array_key_exists($modelName, $this->modelInstances))
		{
			if (empty($config) && isset($this->config['modelConfig']))
			{
				$config = $this->config['modelConfig'];
			}

			if (empty($name))
			{
				$config['modelTemporaryInstance'] = true;
				$controllerName                   = $this->getName();

				if ($controllerName != $modelName)
				{
					$config['hash_view'] = $controllerName;
				}

			}
			else
			{
				// Other classes are loaded with persistent state disabled and their state/input blanked out
				$config['modelTemporaryInstance'] = false;
				$config['modelClearState']        = true;
				$config['modelClearInput']        = true;
			}

			$this->modelInstances[$modelName] = $this->container->factory->model(ucfirst($modelName), $config);
		}

		return $this->modelInstances[$modelName];
	}

	/**
	 * Returns a named View object
	 *
	 * @param   string  $name    The Model name. If null we'll use the modelName
	 *                           variable or, if it's empty, the same name as
	 *                           the Controller
	 * @param   array   $config  Configuration parameters to the Model. If skipped
	 *                           we will use $this->config
	 *
	 * @return  View  The instance of the Model known to this Controller
	 */
	public function getView($name = null, $config = [])
	{
		if (!empty($name))
		{
			$viewName = $name;
		}
		elseif (!empty($this->viewName))
		{
			$viewName = $this->viewName;
		}
		else
		{
			$viewName = $this->view;
		}

		if (!array_key_exists($viewName, $this->viewInstances))
		{
			if (empty($config) && isset($this->config['viewConfig']))
			{
				$config = $this->config['viewConfig'];
			}

			$viewType = $this->input->getCmd('format', 'html');

			// Get the model's class name
			$this->viewInstances[$viewName] = $this->container->factory->view($viewName, $viewType, $config);
		}

		return $this->viewInstances[$viewName];
	}

	/**
	 * Pushes a named view to the Controller
	 *
	 * @param   string  $viewName  The name of the View
	 * @param   View    $view      The actual View object to push
	 *
	 * @return  void
	 */
	public function setView($viewName, View &$view)
	{
		$this->viewInstances[$viewName] = $view;
	}

	/**
	 * Set the name of the view to be used by this Controller
	 *
	 * @param   string  $viewName  The name of the view
	 *
	 * @return  void
	 */
	public function setViewName($viewName)
	{
		$this->viewName = $viewName;
	}

	/**
	 * Set the name of the model to be used by this Controller
	 *
	 * @param   string  $modelName  The name of the model
	 *
	 * @return  void
	 */
	public function setModelName($modelName)
	{
		$this->modelName = $modelName;
	}

	/**
	 * Pushes a named model to the Controller
	 *
	 * @param   string  $modelName  The name of the Model
	 * @param   Model   $model      The actual Model object to push
	 *
	 * @return  void
	 */
	public function setModel($modelName, Model &$model)
	{
		$this->modelInstances[$modelName] = $model;
	}

	/**
	 * Method to get the controller name
	 *
	 * The controller name is set by default parsed using the classname, or it can be set
	 * by passing a $config['name'] in the class constructor
	 *
	 * @return  string  The name of the controller
	 *
	 * @throws  CannotGetName  If it's impossible to determine the name and it's not set
	 */
	public function getName()
	{
		if (empty($this->name))
		{
			$r = null;

			if (!preg_match('/(.*)\\\\Controller\\\\(.*)/i', get_class($this), $r))
			{
				throw new CannotGetName(Text::_('LIB_FOF_CONTROLLER_ERR_GET_NAME'), 500);
			}

			$this->name = $r[2];
		}

		return $this->name;
	}

	/**
	 * Get the last task that is being performed or was most recently performed.
	 *
	 * @return  string  The task that is being performed or was most recently performed.
	 */
	public function getTask()
	{
		return $this->task;
	}

	/**
	 * Gets the available tasks in the controller.
	 *
	 * @return  array  Array[i] of task names.
	 */
	public function getTasks()
	{
		return $this->methods;
	}

	/**
	 * Redirects the browser or returns false if no redirect is set.
	 *
	 * @return  boolean  False if no redirect exists.
	 */
	public function redirect()
	{
		if ($this->redirect)
		{
			$this->container->platform->redirect($this->redirect, 301, $this->message, $this->messageType);

			return true;
		}

		return false;
	}

	/**
	 * Register the default task to perform if a mapping is not found.
	 *
	 * @param   string  $method  The name of the method in the derived class to perform if a named task is not found.
	 *
	 * @return  Controller  This object to support chaining.
	 */
	public function registerDefaultTask($method)
	{
		$this->registerTask('__default', $method);

		return $this;
	}

	/**
	 * Register (map) a task to a method in the class.
	 *
	 * @param   string  $task    The task.
	 * @param   string  $method  The name of the method in the derived class to perform for this task.
	 *
	 * @return  Controller  This object to support chaining.
	 */
	public function registerTask($task, $method)
	{
		if (in_array($method, $this->methods))
		{
			$this->taskMap[$task] = $method;
		}

		return $this;
	}

	/**
	 * Unregister (unmap) a task in the class.
	 *
	 * @param   string  $task  The task.
	 *
	 * @return  Controller  This object to support chaining.
	 */
	public function unregisterTask($task)
	{
		unset($this->taskMap[$task]);

		return $this;
	}

	/**
	 * Sets the internal message that is passed with a redirect
	 *
	 * @param   string  $text  Message to display on redirect.
	 * @param   string  $type  Message type. Optional, defaults to 'message'.
	 *
	 * @return  string  Previous message
	 */
	public function setMessage($text, $type = 'message')
	{
		$previous          = $this->message;
		$this->message     = $text;
		$this->messageType = $type;

		return $previous;
	}

	/**
	 * Set a URL for browser redirection.
	 *
	 * @param   string  $url   URL to redirect to.
	 * @param   string  $msg   Message to display on redirect. Optional, defaults to value set internally by
	 *                         controller, if any.
	 * @param   string  $type  Message type. Optional, defaults to 'message' or the type set by a previous call to
	 *                         setMessage.
	 *
	 * @return  Controller   This object to support chaining.
	 */
	public function setRedirect($url, $msg = null, $type = null)
	{
		// If we're parsing a non-SEF URL decide whether to use JRoute or not
		if (strpos($url, 'index.php') === 0)
		{
			$isAdmin = $this->container->platform->isBackend();
			$auto    = false;

			if (($this->autoRouting == 2 || $this->autoRouting == 3) && $isAdmin)
			{
				$auto = true;
			}

			if (($this->autoRouting == 1 || $this->autoRouting == 3) && !$isAdmin)
			{
				$auto = true;
			}

			if ($auto)
			{
				$url = Route::_($url, false);
			}

			/**
			 * Joomla 4 does not add the base URI to redirections.
			 *
			 * This means that all bare redirects, e.g. to 'index.php?option=com_example', no longer work correctly.
			 *
			 * In the frontend, if your site is located in a subdirectory e.g. /foobar you get redirected to
			 * /index.php?option=com_example instead of /foobar/index.php?option=com_example
			 *
			 * In the backend, you're redirected to /index.php?option=com_example instead of the expected
			 * /administrator/index.php?option=com_example which breaks your application since the backend redirects to
			 * the frontend.
			 *
			 * This is an undocumented b/c break in Joomla 4. It even breaks some of the core components...
			 *
			 * The following code detects bare redirect URLs and adds the base URI path if auto-routing has been
			 * disabled, automatically fixing the observed issue. It only does that on Joomla 4 since adding the base
			 * URI on Joomla 3 can cause redirection problems.
			 */
			if (!$auto && version_compare(JVERSION, '3.999.999', 'gt'))
			{
				$url = Uri::base() . $url;
			}
		}

		// Set the redirection
		$this->redirect = $url;

		if ($msg !== null)
		{
			// Controller may have set this directly
			$this->message = $msg;
		}

		// Ensure the type is not overwritten by a previous call to setMessage.
		if (empty($this->messageType))
		{
			$this->messageType = 'message';
		}

		// If the type is explicitly set, set it.
		if (!empty($type))
		{
			$this->messageType = $type;
		}

		return $this;
	}

	/**
	 * Returns true if there is a redirect set in the controller
	 *
	 * @return  boolean
	 */
	public function hasRedirect()
	{
		return !empty($this->redirect);
	}

	/**
	 * Provides CSRF protection through the forced use of a secure token. If the token doesn't match the one in the
	 * session we return false.
	 *
	 * @return  bool
	 *
	 * @throws  \Exception
	 */
	protected function csrfProtection()
	{
		static $isCli = null, $isAdmin = null;

		$platform = $this->container->platform;

		if (is_null($isCli))
		{
			$isCli   = $platform->isCli();
			$isAdmin = $platform->isBackend();
		}

		switch ($this->csrfProtection)
		{
			// Never
			case 0:
				return true;
				break;

			// Always
			case 1:
				break;

			// Only back-end and HTML format
			case 2:
				if ($isCli)
				{
					return true;
				}
				elseif (!$isAdmin && ($this->input->get('format', 'html', 'cmd') != 'html'))
				{
					return true;
				}
				break;

			// Only back-end
			case 3:
				if (!$isAdmin)
				{
					return true;
				}
				break;
		}

		// Check for a session token
		$token    = $this->container->platform->getToken(false);
		$hasToken = $this->input->get($token, false, 'none') == 1;

		if (!$hasToken)
		{
			$hasToken = $this->input->get('_token', null, 'none') == $token;
		}

		if ($hasToken)
		{
			$view = $this->input->getCmd('view');
			$task = $this->input->getCmd('task');
			Log::add(
				"FOF: You are using a legacy session token in (view, task)=($view, $task). Support for legacy tokens will go away. Use form tokens instead.",
				Log::WARNING,
				'deprecated'
			);
		}

		// Check for a form token
		if (!$hasToken)
		{
			$token    = $this->container->platform->getToken(true);
			$hasToken = $this->input->get($token, false, 'none') == 1;

			if (!$hasToken)
			{
				$view = $this->input->getCmd('view');
				$task = $this->input->getCmd('task');
				Log::add(
					"FOF: You are using the insecure _token form variable in (view, task)=($view, $task). Support for it will go away. Submit a variable with the token as the name and a value of 1 instead.",
					Log::WARNING,
					'deprecated'
				);


				$hasToken = $this->input->get('_token', null, 'none') == $token;
			}
		}

		if (!$hasToken)
		{
			$platform->raiseError(403, Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'));

			return false;
		}

		return true;
	}

	/**
	 * Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
	 * Joomla! plugin system. A true/false return value is expected. The first false return cancels the event.
	 *
	 * EXAMPLE
	 * Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456)
	 * The event calls:
	 * 1. $this->onBeforeSomething(123, 456)
	 * 2. $this->checkACL('@something') if there is no onBeforeSomething and the event starts with onBefore
	 * 3. Joomla! plugin event onComFoobarControllerItemBeforeSomething($this, 123, 456)
	 *
	 * @param   string  $event      The name of the event, typically named onPredicateVerb e.g. onBeforeKick
	 * @param   array   $arguments  The arguments to pass to the event handlers
	 *
	 * @return  bool
	 */
	protected function triggerEvent($event, array $arguments = [])
	{
		$result = true;

		// If there is an object method for this event, call it
		if (method_exists($this, $event))
		{
			switch (count($arguments))
			{
				case 0:
					$result = $this->{$event}();
					break;
				case 1:
					$result = $this->{$event}($arguments[0]);
					break;
				case 2:
					$result = $this->{$event}($arguments[0], $arguments[1]);
					break;
				case 3:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2]);
					break;
				case 4:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
					break;
				case 5:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
					break;
				default:
					$result = call_user_func_array([$this, $event], $arguments);
					break;
			}
		}
		// If there is no handler method perform a simple ACL check
		elseif (substr($event, 0, 8) == 'onBefore')
		{
			$task   = substr($event, 8);
			$result = $this->checkACL('@' . $task);
		}

		if ($result === false)
		{
			return false;
		}

		// All other event handlers live outside this object, therefore they need to be passed a reference to this
		// objects as the first argument.
		array_unshift($arguments, $this);

		// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
		$prefix = '';

		if (substr($event, 0, 2) == 'on')
		{
			$prefix = 'on';
			$event  = substr($event, 2);
		}

		// Get the component/model prefix for the event
		$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Controller';
		$prefix .= ucfirst($this->getName());

		// The event name will be something like onComFoobarItemsBeforeSomething
		$event = $prefix . $event;

		// Call the Joomla! plugins
		$results = $this->container->platform->runPlugins($event, $arguments);

		if (!empty($results))
		{
			foreach ($results as $result)
			{
				if ($result === false)
				{
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Checks if the current user has enough privileges for the requested ACL area.
	 *
	 * @param   string  $area  The ACL area, e.g. core.manage.
	 *
	 * @return  boolean  True if the user has the ACL privilege specified
	 */
	protected function checkACL($area)
	{
		$area = $this->getACLRuleFor($area);

		if (is_bool($area))
		{
			return $area;
		}

		if (in_array(strtolower($area), ['false', '0', 'no', '403']))
		{
			return false;
		}

		if (in_array(strtolower($area), ['true', '1', 'yes']))
		{
			return true;
		}

		if (in_array(strtolower($area), ['guest']))
		{
			return $this->container->platform->getUser()->guest;
		}

		if (in_array(strtolower($area), ['user']))
		{
			return !$this->container->platform->getUser()->guest;
		}

		if (empty($area))
		{
			return true;
		}

		return $this->container->platform->authorise($area, $this->container->componentName);
	}

	/**
	 * Resolves @task and &callback notations for ACL privileges
	 *
	 * @param   string  $area      The task notation to resolve
	 * @param   array   $oldAreas  Areas we've already been redirected from, used to detect circular references
	 *
	 * @return  mixed  The resolved ACL privilege
	 */
	protected function getACLRuleFor($area, $oldAreas = [])
	{
		// If it's a &notation return the callback result
		if (substr($area, 0, 1) == '&')
		{
			$oldAreas[] = $area;
			$method     = substr($area, 1);

			// Method not found? Assume true.
			if (!method_exists($this, $method))
			{
				return true;
			}

			$area = $this->$method();

			return $this->getACLRuleFor($area, $oldAreas);
		}

		// If it's not an @notation return the raw string
		if (substr($area, 0, 1) != '@')
		{
			return $area;
		}

		// Get the array index (other task)
		$index = substr($area, 1);

		// If the referenced task has no ACL map, return true
		if (!isset($this->taskPrivileges[$index]))
		{
			$index = strtolower($index);

			if (!isset($this->taskPrivileges[$index]))
			{
				return true;
			}
		}

		// Get the new ACL area
		$newArea = $this->taskPrivileges[$index];

		$oldAreas[] = $area;

		// Circular reference found
		if (in_array($newArea, $oldAreas))
		{
			return true;
		}

		// We've found an ACL privilege. Return it.
		if (substr($area, 0, 1) != '@')
		{
			return $newArea;
		}

		// We have another reference. Resolve it.
		return $this->getACLRuleFor($newArea, $oldAreas);
	}


}
Controller/Mixin/PredefinedTaskList.php000060400000003631152455514240014216 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller\Mixin;

defined('_JEXEC') || die;

use FOF30\Controller\Controller;

/**
 * Force a Controller to allow access to specific tasks only, no matter which tasks are already defined in this
 * Controller.
 *
 * Include this Trait and then in your constructor do this:
 * $this->setPredefinedTaskList(['atask', 'anothertask', 'something']);
 *
 * WARNING: If you override execute() you will need to copy the logic from this trait's execute() method.
 */
trait PredefinedTaskList
{

	/**
	 * A list of predefined tasks. Trying to access any other task will result in the first task of this list being
	 * executed instead.
	 *
	 * @var array
	 */
	protected $predefinedTaskList = [];

	/**
	 * Overrides the execute method to implement the predefined task list feature
	 *
	 * @param   string  $task  The task to execute
	 *
	 * @return  mixed   The controller task result
	 */
	public function execute($task)
	{
		if (!in_array($task, $this->predefinedTaskList))
		{
			$task = reset($this->predefinedTaskList);
		}

		return parent::execute($task);
	}

	/**
	 * Sets the predefined task list and registers the first task in the list as the Controller's default task
	 *
	 * @param   array  $taskList  The task list to register
	 *
	 * @return  void
	 */
	public function setPredefinedTaskList(array $taskList)
	{
		/** @var Controller $this */

		// First, unregister all known tasks which are not in the taskList
		$allTasks = $this->getTasks();

		foreach ($allTasks as $task)
		{
			if (in_array($task, $taskList))
			{
				continue;
			}

			$this->unregisterTask($task);
		}

		// Set the predefined task list
		$this->predefinedTaskList = $taskList;

		// Set the default task
		$this->registerDefaultTask(reset($this->predefinedTaskList));

	}
}
Controller/DataController.php000060400000114007152455514240012323 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use FOF30\Controller\Exception\ItemNotFound;
use FOF30\Controller\Exception\LockedRecord;
use FOF30\Controller\Exception\NotADataModel;
use FOF30\Controller\Exception\TaskNotFound;
use FOF30\Model\DataModel;
use FOF30\View\View;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Response\JsonResponse;

/**
 * Database-aware Controller
 *
 * @property-read  \FOF30\Input\Input $input  The input object (magic __get returns the Input from the Container)
 */
class DataController extends Controller
{
	/**
	 * Variables that should be taken in account while working with the cache. You can set them in Controller
	 * constructor or inside onBefore* methods
	 *
	 * @var false|array
	 */
	protected $cacheParams = false;

	/**
	 * An associative array for required ACL privileges per task. For example:
	 * array(
	 *   'edit' => 'core.edit',
	 *   'jump' => 'foobar.jump',
	 *   'alwaysallow' => 'true',
	 *   'neverallow' => 'false'
	 * );
	 *
	 * You can use the notation '@task' which means 'apply the same privileges as "task"'. If you create a reference
	 * back to yourself (e.g. 'mytask' => array('@mytask')) it will return TRUE.
	 *
	 * @var array
	 */
	protected $taskPrivileges = [
		// Special privileges
		'*editown'    => 'core.edit.own', // Privilege required to edit own record
		// Standard tasks
		'add'         => 'core.create',
		'apply'       => '&getACLForApplySave', // Apply task: call the getACLForApplySave method
		'archive'     => 'core.edit.state',
		'cancel'      => 'core.edit.state',
		'copy'        => '@add', // Maps copy ACLs to the add task
		'edit'        => 'core.edit',
		'loadhistory' => '@edit', // Maps loadhistory ACLs to the edit task
		'orderup'     => 'core.edit.state',
		'orderdown'   => 'core.edit.state',
		'publish'     => 'core.edit.state',
		'remove'      => 'core.delete',
		'forceRemove' => 'core.delete',
		'save'        => '&getACLForApplySave', // Save task: call the getACLForApplySave method
		'savenew'     => 'core.create',
		'saveorder'   => 'core.edit.state',
		'trash'       => 'core.edit.state',
		'unpublish'   => 'core.edit.state',
	];

	/**
	 * An indexed array of default values for the add task. Since the add task resets the model you can't set these
	 * values directly to the model. Instead, the defaultsForAdd values will be fed to model's bind() after it's reset
	 * and before the session-stored item data is bound to the model object.
	 *
	 * @var  array
	 */
	protected $defaultsForAdd = [];

	/**
	 * Public constructor of the Controller class. You can pass the following variables in the $config array,
	 * on top of what you already have in the base Controller class:
	 *
	 * taskPrivileges       array   ACL privileges for each task
	 * cacheableTasks       array   The cache-enabled tasks
	 *
	 * @param   Container  $container  The application container
	 * @param   array      $config     The configuration array
	 */
	public function __construct(Container $container, array $config = [])
	{
		parent::__construct($container, $config);

		// Set up a default model name if none is provided
		if (empty($this->modelName))
		{
			$this->modelName = $container->inflector->pluralize($this->view);
		}

		// Set up a default view name if none is provided
		if (empty($this->viewName))
		{
			$this->viewName = $container->inflector->pluralize($this->view);
		}

		if (isset($config['cacheableTasks']))
		{
			if (!is_array($config['cacheableTasks']))
			{
				$config['cacheableTasks'] = explode(',', $config['cacheableTasks']);
				$config['cacheableTasks'] = array_map('trim', $config['cacheableTasks']);
			}

			$this->cacheableTasks = $config['cacheableTasks'];
		}
		elseif ($this->container->platform->isBackend())
		{
			$this->cacheableTasks = [];
		}
		else
		{
			$this->cacheableTasks = ['browse', 'read'];
		}

		if (isset($config['taskPrivileges']) && is_array($config['taskPrivileges']))
		{
			$this->taskPrivileges = array_merge($this->taskPrivileges, $config['taskPrivileges']);
		}
	}

	/**
	 * Executes a given controller task. The onBefore<task> and onAfter<task> methods are called automatically if they
	 * exist.
	 *
	 * If $task == 'default' we will determine the CRUD task to use based on the view name and HTTP verb in the request,
	 * overriding the routing.
	 *
	 * @param   string  $task  The task to execute, e.g. "browse"
	 *
	 * @return  null|bool  False on execution failure
	 *
	 * @throws  TaskNotFound  When the task is not found
	 */
	public function execute($task)
	{
		if ($task == 'default')
		{
			$task = $this->getCrudTask();
		}

		return parent::execute($task);
	}

	/**
	 * Returns a named View object
	 *
	 * @param   string  $name    The Model name. If null we'll use the modelName
	 *                           variable or, if it's empty, the same name as
	 *                           the Controller
	 * @param   array   $config  Configuration parameters to the Model. If skipped
	 *                           we will use $this->config
	 *
	 * @return  View  The instance of the Model known to this Controller
	 */
	public function getView($name = null, $config = [])
	{
		if (!empty($name))
		{
			$viewName = $name;
		}
		elseif (!empty($this->viewName))
		{
			$viewName = $this->viewName;
		}
		else
		{
			$viewName = $this->view;
		}

		if (!array_key_exists($viewName, $this->viewInstances))
		{
			if (empty($config) && isset($this->config['viewConfig']))
			{
				$config = $this->config['viewConfig'];
			}

			$viewType = $this->input->getCmd('format', 'html');

			// Get the model's class name
			$this->viewInstances[$viewName] = $this->container->factory->view($viewName, $viewType, $config);
		}

		return $this->viewInstances[$viewName];
	}

	/**
	 * Implements a default browse task, i.e. read a bunch of records and send
	 * them to the browser.
	 *
	 * @return  void
	 */
	public function browse()
	{
		// Initialise the savestate
		$saveState = $this->input->get('savestate', -999, 'int');

		if ($saveState == -999)
		{
			$saveState = true;
		}

		$this->getModel()->savestate($saveState);

		// Display the view
		$this->display(in_array('browse', $this->cacheableTasks), $this->cacheParams);
	}

	/**
	 * Single record read. The id set in the request is passed to the model and
	 * then the item layout is used to render the result.
	 *
	 * @return  void
	 *
	 * @throws ItemNotFound When the item is not found
	 */
	public function read()
	{
		// Load the model
		/** @var DataModel $model */
		$model = $this->getModel()->savestate(false);

		// If there is no record loaded, try loading a record based on the id passed in the input object
		if (!$model->getId())
		{
			$ids = $this->getIDsFromRequest($model, true);

			if ($model->getId() != reset($ids))
			{
				$key = strtoupper($this->container->componentName . '_ERR_' . $model->getName() . '_NOTFOUND');
				throw new ItemNotFound(Text::_($key), 404);
			}
		}

		// Set the layout to item, if it's not set in the URL
		if (empty($this->layout))
		{
			$this->layout = 'item';
		}
		elseif ($this->layout == 'default')
		{
			$this->layout = 'item';
		}

		// Display the view
		$this->display(in_array('read', $this->cacheableTasks), $this->cacheParams);
	}

	/**
	 * Single record add. The form layout is used to present a blank page.
	 *
	 * @return  void
	 */
	public function add()
	{
		// Load and reset the model
		$model = $this->getModel()->savestate(false);
		$model->reset();

		// Set the layout to form, if it's not set in the URL
		if (empty($this->layout))
		{
			$this->layout = 'form';
		}
		elseif ($this->layout == 'default')
		{
			$this->layout = 'form';
		}

		if (!empty($this->defaultsForAdd))
		{
			$model->bind($this->defaultsForAdd);
		}

		// Get temporary data from the session, set if the save failed and we're redirected back here
		$sessionKey = $this->viewName . '.savedata';
		$itemData   = $this->container->platform->getSessionVar($sessionKey, null, $this->container->componentName);
		$this->container->platform->setSessionVar($sessionKey, null, $this->container->componentName);

		if (!empty($itemData))
		{
			$model->bind($itemData);
		}

		// Display the view
		$this->display(in_array('add', $this->cacheableTasks), $this->cacheParams);
	}

	/**
	 * Single record edit. The ID set in the request is passed to the model,
	 * then the form layout is used to edit the result.
	 *
	 * @return  void
	 */
	public function edit()
	{
		// Load the model
		/** @var DataModel $model */
		$model = $this->getModel()->savestate(false);

		if (!$model->getId())
		{
			$this->getIDsFromRequest($model, true);
		}

		$userId = $this->container->platform->getUser()->id;

		try
		{
			if ($model->isLocked($userId))
			{
				$model->checkIn($userId);
			}

			$model->lock();
		}
		catch (\Exception $e)
		{
			// Redirect on error
			if ($customURL = $this->input->getBase64('returnurl', ''))
			{
				$customURL = base64_decode($customURL);
			}

			$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();
			$this->setRedirect($url, $e->getMessage(), 'error');

			return;
		}

		// Set the layout to form, if it's not set in the URL
		if (empty($this->layout))
		{
			$this->layout = 'form';
		}
		elseif ($this->layout == 'default')
		{
			$this->layout = 'form';
		}

		// Get temporary data from the session, set if the save failed and we're redirected back here
		$sessionKey = $this->viewName . '.savedata';
		$itemData   = $this->container->platform->getSessionVar($sessionKey, null, $this->container->componentName);
		$this->container->platform->setSessionVar($sessionKey, null, $this->container->componentName);

		if (!empty($itemData))
		{
			$model->bind($itemData);
		}

		// Display the view
		$this->display(in_array('edit', $this->cacheableTasks), $this->cacheParams);
	}

	/**
	 * Save the incoming data and then return to the Edit task
	 *
	 * @return  void
	 */
	public function apply()
	{
		// CSRF prevention
		$this->csrfProtection();

		// Redirect to the edit task
		if (!$this->applySave())
		{
			return;
		}

		$id      = $this->input->get('id', 0, 'int');
		$textKey = strtoupper($this->container->componentName . '_LBL_' . $this->container->inflector->singularize($this->view) . '_SAVED');

		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->view . '&task=edit&id=' . $id . $this->getItemidURLSuffix();
		$this->setRedirect($url, Text::_($textKey));
	}

	/**
	 * Duplicates selected items
	 *
	 * @return  void
	 */
	public function copy()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model       = $this->getModel()->savestate(false);
		$ids         = $this->getIDsFromRequest($model, true);
		$error       = null;
		$copiedCount = 0;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);
				$model->copy();
				$copiedCount++;
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');

			return;
		}

		if ($copiedCount > 0)
		{
			$textKey = strtoupper($this->container->componentName . '_LBL_' . $this->container->inflector->singularize($this->view) . '_COPIED');
			$this->setRedirect($url, Text::_($textKey));

			return;
		}

		$this->setRedirect($url);
	}

	/**
	 * Save the incoming data and then return to the Browse task
	 *
	 * @return  void
	 */
	public function save()
	{
		// CSRF prevention
		$this->csrfProtection();

		if (!$this->applySave())
		{
			return;
		}

		$textKey = strtoupper($this->container->componentName . '_LBL_' . $this->container->inflector->singularize($this->view) . '_SAVED');

		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();
		$this->setRedirect($url, Text::_($textKey));
	}

	/**
	 * Save the incoming data and then return to the Add task
	 *
	 * @return  bool
	 */
	public function savenew()
	{
		// CSRF prevention
		$this->csrfProtection();

		if (!$this->applySave())
		{
			return;
		}

		$textKey = strtoupper($this->container->componentName . '_LBL_' . $this->container->inflector->singularize($this->view) . '_SAVED');

		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->singularize($this->view) . '&task=add' . $this->getItemidURLSuffix();
		$this->setRedirect($url, Text::_($textKey));
	}

	/**
	 * Save the incoming data as a copy of the given model and then redirect to the copied object edit view
	 *
	 * @return  bool
	 */
	public function save2copy()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, true);
		$data  = $this->input->getData();

		unset($data[$model->getIdFieldName()]);

		$error = null;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);
				$model = $model->copy($data);
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : $url = 'index.php?option=' . $this->container->componentName . '&view=' . $this->view . '&task=edit&id=' . $model->getId() . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$textKey = strtoupper($this->container->componentName . '_LBL_' . $this->container->inflector->singularize($this->view) . '_COPIED');
			$this->setRedirect($url, Text::_($textKey));
		}
	}

	/**
	 * Cancel the edit, check in the record and return to the Browse task
	 *
	 * @return  void
	 */
	public function cancel()
	{
		$model = $this->getModel()->tmpInstance()->savestate(false);

		if (!$model->getId())
		{
			$this->getIDsFromRequest($model, true);
		}

		if ($model->getId())
		{
			$userId = $this->container->platform->getUser()->id;

			if ($model->isLocked($userId))
			{
				try
				{
					$model->checkIn($userId);
				}
				catch (LockedRecord $e)
				{
					// Redirect to the display task
					if ($customURL = $this->input->getBase64('returnurl', ''))
					{
						$customURL = base64_decode($customURL);
					}

					$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();
					$this->setRedirect($url, $e->getMessage(), 'error');
				}
			}

			$model->unlock();
		}

		// Remove any saved data
		$sessionKey = $this->viewName . '.savedata';
		$this->container->platform->setSessionVar($sessionKey, null, $this->container->componentName);

		// Redirect to the display task
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();
		$this->setRedirect($url);
	}

	/**
	 * Publish (set enabled = 1) an item.
	 *
	 * @return  void
	 */
	public function publish()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, false);
		$error = false;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);

				$userId = $this->container->platform->getUser()->id;

				if ($model->isLocked($userId))
				{
					$model->checkIn($userId);
				}

				$model->publish();
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Unpublish (set enabled = 0) an item.
	 *
	 * @return  void
	 */
	public function unpublish()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, false);
		$error = null;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);

				$userId = $this->container->platform->getUser()->id;

				if ($model->isLocked($userId))
				{
					$model->checkIn($userId);
				}

				$model->unpublish();
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Archive (set enabled = 2) an item.
	 *
	 * @return  void
	 */
	public function archive()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, false);
		$error = null;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);

				$userId = $this->container->platform->getUser()->id;

				if ($model->isLocked($userId))
				{
					$model->checkIn($userId);
				}

				$model->archive();
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Trash (set enabled = -2) an item.
	 *
	 * @return  void
	 */
	public function trash()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, false);
		$error = null;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);

				$userId = $this->container->platform->getUser()->id;

				if ($model->isLocked($userId))
				{
					$model->checkIn($userId);
				}

				$model->trash();
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Check in (unlock) items
	 *
	 * @return  void
	 */
	public function checkin()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, false);
		$error = null;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);
				$model->checkIn();
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Saves the order of the items
	 *
	 * @return  void
	 */
	public function saveorder()
	{
		// CSRF prevention
		$this->csrfProtection();

		$type   = null;
		$msg    = null;
		$model  = $this->getModel()->savestate(false);
		$ids    = $this->getIDsFromRequest($model, false);
		$orders = $this->input->get('order', [], 'array');

		// Before saving the order, I have to check I the table really supports the ordering feature
		if (!$model->hasField('ordering'))
		{
			$msg  = sprintf('%s does not support ordering.', $model->getTableName());
			$type = 'error';
		}
		else
		{
			$ordering = $model->getFieldAlias('ordering');

			// Several methods could throw exceptions, so let's wrap everything in a try-catch
			try
			{
				if ($n = count($ids))
				{
					for ($i = 0; $i < $n; $i++)
					{
						$item     = $model->find($ids[$i]);
						$neworder = (int) $orders[$i];

						if (!($item instanceof DataModel))
						{
							continue;
						}

						if ($item->getId() == $ids[$i])
						{
							$item->$ordering = $neworder;

							$userId = $this->container->platform->getUser()->id;

							if ($model->isLocked($userId))
							{
								$model->checkIn($userId);
							}

							$model->save($item);
						}
					}
				}

				$model->reorder();
			}
			catch (\Exception $e)
			{
				$msg  = $e->getMessage();
				$type = 'error';
			}
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		$this->setRedirect($url, $msg, $type);
	}

	/**
	 * Moves selected items one position down the ordering list
	 *
	 * @return  void
	 */
	public function orderdown()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);

		if (!$model->getId())
		{
			$this->getIDsFromRequest($model, true);
		}

		$error = null;

		try
		{
			$userId = $this->container->platform->getUser()->id;

			if ($model->isLocked($userId))
			{
				$model->checkIn($userId);
			}

			$model->move(1);
			$status = true;
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Moves selected items one position up the ordering list
	 *
	 * @return  void
	 */
	public function orderup()
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);

		if (!$model->getId())
		{
			$this->getIDsFromRequest($model, true);
		}

		$error = null;

		try
		{
			$userId = $this->container->platform->getUser()->id;

			if ($model->isLocked($userId))
			{
				$model->checkIn($userId);
			}

			$model->move(-1);
			$status = true;
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$this->setRedirect($url);
		}
	}

	/**
	 * Delete or trash selected item(s). The model's softDelete flag determines if the items should be trashed (enabled
	 * state changed to -2) or deleted (completely removed from database)
	 *
	 * @return  void
	 */
	public function remove()
	{
		$this->deleteOrTrash(false);
	}

	/**
	 * Deletes the selected item(s). Unlike remove() this method will force delete the record (completely removed from
	 * database)
	 *
	 * @return  void
	 */
	public function forceRemove()
	{
		$this->deleteOrTrash(true);
	}

	/**
	 * Returns a named Model object. Makes sure that the Model is a database-aware model, throwing an exception
	 * otherwise, when $name is null.
	 *
	 * @param   string  $name    The Model name. If null we'll use the modelName
	 *                           variable or, if it's empty, the same name as
	 *                           the Controller
	 * @param   array   $config  Configuration parameters to the Model. If skipped
	 *                           we will use $this->config
	 *
	 * @return  DataModel  The instance of the Model known to this Controller
	 *
	 * @throws  NotADataModel  When the model type doesn't match our expectations
	 */
	public function getModel($name = null, $config = [])
	{
		$model = parent::getModel($name, $config);

		if (is_null($name) && !($model instanceof DataModel))
		{
			throw new NotADataModel('Model ' . get_class($model) . ' is not a database-aware Model');
		}

		return $model;
	}

	/**
	 * Gets the list of IDs from the request data
	 *
	 * @param   DataModel  $model       The model where the record will be loaded
	 * @param   bool       $loadRecord  When true, the record matching the *first* ID found will be loaded into $model
	 *
	 * @return array
	 */
	public function getIDsFromRequest(DataModel &$model, $loadRecord = true)
	{
		// Get the ID or list of IDs from the request or the configuration
		$cid = $this->input->get('cid', [], 'array');
		$id  = $this->input->getInt('id', 0);
		$kid = $this->input->getInt($model->getIdFieldName(), 0);

		$ids = [];

		if (is_array($cid) && !empty($cid))
		{
			$ids = $cid;
		}
		else
		{
			if (empty($id))
			{
				if (!empty($kid))
				{
					$ids = [$kid];
				}
			}
			else
			{
				$ids = [$id];
			}
		}

		if ($loadRecord && !empty($ids))
		{
			$id = reset($ids);
			$model->find(['id' => $id]);
		}

		return $ids;
	}

	/**
	 * Method to load a row from version history
	 *
	 * @return   boolean  True if the content history is reverted, false otherwise
	 *
	 * @since   2.2
	 */
	public function loadhistory()
	{
		$model = $this->getModel();
		$model->lock();

		$historyId = $this->input->get('version_id', null, 'integer');
		$alias     = $this->container->componentName . '.' . $this->view;
		$returnUrl = 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		if (!empty($customURL))
		{
			$returnUrl = $customURL;
		}

		try
		{
			$model->loadhistory($historyId, $alias);
		}
		catch (\Exception $e)
		{
			$this->setRedirect($returnUrl, $e->getMessage(), 'error');
			$model->unlock();

			return false;
		}

		// Access check.
		if (!$this->checkACL('@loadhistory'))
		{
			$this->setRedirect($returnUrl, Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error');
			$model->unlock();

			return false;
		}

		$model->store();
		$this->setRedirect($returnUrl, Text::sprintf('JLIB_APPLICATION_SUCCESS_LOAD_HISTORY', $model->getState('save_date'), $model->getState('version_note')));

		return true;
	}

	/**
	 * Gets a URL suffix with the Itemid parameter. If it's not the front-end of the site, or if
	 * there is no Itemid set it returns an empty string.
	 *
	 * @return  string  The &Itemid=123 URL suffix, or an empty string if Itemid is not applicable
	 */
	public function getItemidURLSuffix()
	{
		if ($this->container->platform->isFrontend() && ($this->input->getCmd('Itemid', 0) != 0))
		{
			return '&Itemid=' . $this->input->getInt('Itemid', 0);
		}
		else
		{
			return '';
		}
	}

	/**
	 * Deal with JSON format: no redirects needed
	 *
	 * @param   string  $task  The task being executed
	 *
	 * @return boolean      True if everything went well
	 */
	protected function onAfterExecute($task)
	{
		// JSON shouldn't have redirects
		if ($this->hasRedirect() && $this->input->getCmd('format', 'html') == 'json')
		{
			// Error: deal with it in REST api way
			if ($this->messageType == 'error')
			{
				$response = new JsonResponse($this->message, $this->message, true);

				echo $response;

				$this->redirect = false;
				$this->container->platform->setHeader('Status', 500);

				return;
			}
			else
			{
				// Not an error, avoid redirect and display the record(s)
				$this->redirect = false;

				return $this->display();
			}
		}

		return true;
	}

	/**
	 * Determines the CRUD task to use based on the view name and HTTP verb used in the request.
	 *
	 * @return  string  The CRUD task (browse, read, edit, delete)
	 */
	protected function getCrudTask()
	{
		// By default, a plural view means 'browse' and a singular view means 'edit'
		$view = $this->input->getCmd('view', null);
		$task = $this->container->inflector->isPlural($view) ? 'browse' : 'edit';

		// If the task is 'edit' but there's no logged in user switch to a 'read' task
		if (($task == 'edit') && !$this->container->platform->getUser()->id)
		{
			$task = 'read';
		}

		// Check if there is an id passed in the request
		$id = $this->input->get('id', null, 'int');

		if ($id == 0)
		{
			$ids = $this->input->get('ids', [], 'array');

			if (!empty($ids))
			{
				$id = array_shift($ids);
			}
		}

		// Get the request HTTP verb
		$requestMethod = 'GET';

		if (isset($_SERVER['REQUEST_METHOD']))
		{
			$requestMethod = strtoupper($_SERVER['REQUEST_METHOD']);
		}

		// Alter the task based on the verb
		switch ($requestMethod)
		{
			// POST and PUT result in a record being saved; no ID means creating a new record
			case 'POST':
			case 'PUT':
				$task = 'save';
				break;

			// DELETE results in a record being deleted, as long as there is an ID
			case 'DELETE':
				if ($id)
				{
					$task = 'remove';
				}
				break;

			// GET results in browse, edit or add depending on the ID
			case 'GET':
			default:
				// If it's an edit without an ID or ID=0, it's really an add
				if (($task == 'edit') && ($id == 0))
				{
					$task = 'add';
				}
				break;
		}

		return $task;
	}

	/**
	 * Checks if the current user has enough privileges for the requested ACL area. This overridden method supports
	 * asset tracking as well.
	 *
	 * @param   string  $area  The ACL area, e.g. core.manage
	 *
	 * @return  boolean  True if the user has the ACL privilege specified
	 */
	protected function checkACL($area)
	{
		$area = $this->getACLRuleFor($area);

		$result = parent::checkACL($area);

		// Check if we're dealing with ids
		$ids = null;

		// First, check if there is an asset for this record
		/** @var DataModel $model */
		$model = $this->getModel();

		$ids = null;

		if (is_object($model) && ($model instanceof DataModel) && $model->isAssetsTracked())
		{
			$ids = $this->getIDsFromRequest($model, false);
		}

		// No IDs tracked, return parent's result
		if (empty($ids))
		{
			return $result;
		}

		// Asset tracking
		if (!is_array($ids))
		{
			$ids = [$ids];
		}

		$resource    = $this->container->inflector->singularize($this->view);
		$isEditState = ($area == 'core.edit.state');

		foreach ($ids as $id)
		{
			$asset = $this->container->componentName . '.' . $resource . '.' . $id;

			// Dedicated permission found, check it!
			$platform = $this->container->platform;

			if ($platform->authorise($area, $asset))
			{
				return true;
			}

			// Fallback on edit.own, if not edit.state. First test if the permission is available.

			$editOwn = $this->getACLRuleFor('@*editown');

			if ((!$isEditState) && ($platform->authorise($editOwn, $asset)))
			{
				$model->load($id);

				if (!$model->hasField('created_by'))
				{
					return false;
				}

				// Now test the owner is the user.
				$owner_id = (int) $model->getFieldValue('created_by', null);

				// If the owner matches 'me' then do the test.
				if ($owner_id == $platform->getUser()->id)
				{
					return true;
				}

				return false;
			}
		}

		// No result found? Not authorised.
		return false;
	}

	protected function deleteOrTrash($forceDelete = false)
	{
		// CSRF prevention
		$this->csrfProtection();

		$model = $this->getModel()->savestate(false);
		$ids   = $this->getIDsFromRequest($model, false);
		$error = null;

		try
		{
			$status = true;

			foreach ($ids as $id)
			{
				$model->find($id);

				$userId = $this->container->platform->getUser()->id;

				if ($model->isLocked($userId))
				{
					$model->checkIn($userId);
				}

				if ($forceDelete)
				{
					$model->forceDelete();
				}
				else
				{
					$model->delete();
				}
			}
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();
		}

		// Redirect
		if ($customURL = $this->input->getBase64('returnurl', ''))
		{
			$customURL = base64_decode($customURL);
		}

		$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();

		if (!$status)
		{
			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$textKey = strtoupper($this->container->componentName . '_LBL_' . $this->container->inflector->singularize($this->view) . '_DELETED');
			$this->setRedirect($url, Text::_($textKey));
		}
	}

	/**
	 * Common method to handle apply and save tasks
	 *
	 * @return  bool True on success
	 */
	protected function applySave()
	{
		// Load the model
		$model = $this->getModel()->savestate(false);

		if (!$model->getId())
		{
			$this->getIDsFromRequest($model, true);
		}

		$userId = $this->container->platform->getUser()->id;
		$id     = $model->getId();
		$data   = $this->input->getData();

		if ($model->isLocked($userId))
		{
			try
			{
				$model->checkIn($userId);
			}
			catch (LockedRecord $e)
			{
				// Redirect to the display task
				if ($customURL = $this->input->getBase64('returnurl', ''))
				{
					$customURL = base64_decode($customURL);
				}

				$eventName = 'onAfterApplySaveError';
				$result    = $this->triggerEvent($eventName, [&$data, $id, $e]);

				$url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->container->componentName . '&view=' . $this->container->inflector->pluralize($this->view) . $this->getItemidURLSuffix();
				$this->setRedirect($url, $e->getMessage(), 'error');

				return false;
			}
		}

		// Set the layout to form, if it's not set in the URL
		if (is_null($this->layout))
		{
			$this->layout = 'form';
		}

		// Save the data
		$status = true;
		$error  = null;

		try
		{
			$eventName = 'onBeforeApplySave';
			$result    = $this->triggerEvent($eventName, [&$data]);

			if ($id != 0)
			{
				// Try to check-in the record if it's not a new one
				$model->unlock();
			}

			// Save the data
			$model->save($data);

			$eventName = 'onAfterApplySave';
			$result    = $this->triggerEvent($eventName, [&$data, $model->getId()]);

			$this->input->set('id', $model->getId());
		}
		catch (\Exception $e)
		{
			$status = false;
			$error  = $e->getMessage();

			$eventName = 'onAfterApplySaveError';
			$result    = $this->triggerEvent($eventName, [&$data, $model->getId(), $e]);
		}

		if (!$status)
		{
			// Cache the item data in the session. We may need to reuse them if the save fails.
			$itemData = $model->getData();

			$sessionKey = $this->viewName . '.savedata';
			$this->container->platform->setSessionVar($sessionKey, $itemData, $this->container->componentName);

			// Redirect on error
			$id = $model->getId();

			if ($customURL = $this->input->getBase64('returnurl', ''))
			{
				$customURL = base64_decode($customURL);
			}

			if (!empty($customURL))
			{
				$url = $customURL;
			}
			elseif ($id != 0)
			{
				$url = 'index.php?option=' . $this->container->componentName . '&view=' . $this->view . '&task=edit&id=' . $id . $this->getItemidURLSuffix();
			}
			else
			{
				$url = 'index.php?option=' . $this->container->componentName . '&view=' . $this->view . '&task=add' . $this->getItemidURLSuffix();
			}

			$this->setRedirect($url, $error, 'error');
		}
		else
		{
			$sessionKey = $this->viewName . '.savedata';
			$this->container->platform->setSessionVar($sessionKey, null, $this->container->componentName);
		}

		return $status;
	}

	/**
	 * Gets the applicable ACL privilege for the apply and save tasks. The value returned is:
	 * - @add if the record's ID is empty / record doesn't exist
	 * - True if the ACL privilege of the edit task (@edit) is allowed
	 * - @editown if the owner of the record (field user_id, userid or user) is the same as the logged in user
	 * - False if the record is not owned by the logged in user and the user doesn't have the @edit privilege
	 *
	 * @return bool|string
	 */
	protected function getACLForApplySave()
	{
		$model = $this->getModel();

		if (!$model->getId())
		{
			$this->getIDsFromRequest($model, true);
		}

		$id = $model->getId();

		if (!$id)
		{
			return '@add';
		}

		if ($this->checkACL('@edit'))
		{
			return true;
		}

		$user = $this->container->platform->getUser();
		$uid  = 0;

		if ($model->hasField('user_id'))
		{
			$uid = $model->getFieldValue('user_id');
		}
		elseif ($model->hasField('userid'))
		{
			$uid = $model->getFieldValue('userid');
		}
		elseif ($model->hasField('user'))
		{
			$uid = $model->getFieldValue('user');
		}

		if (!empty($uid) && !$user->guest && ($user->id == $uid))
		{
			return '@editown';
		}

		return false;
	}
}
Controller/Exception/ItemNotFound.php000060400000000556152455514240013722 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller\Exception;

defined('_JEXEC') || die;

/**
 * Exception thrown when we can't find the requested item in a read task
 */
class ItemNotFound extends \RuntimeException
{
}
Controller/Exception/CannotGetName.php000060400000000540152455514240014023 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller\Exception;

defined('_JEXEC') || die;

/**
 * Exception thrown when we can't get a Controller's name
 */
class CannotGetName extends \RuntimeException
{
}
Controller/Exception/LockedRecord.php000060400000001213152455514240013676 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;

/**
 * Exception thrown when the provided Model is locked for writing by another user
 */
class LockedRecord extends \RuntimeException
{
	public function __construct($message = "", $code = 403, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('LIB_FOF_CONTROLLER_ERR_LOCKED');
		}

		parent::__construct($message, $code, $previous);
	}
}
Controller/Exception/NotADataModel.php000060400000000555152455514240013762 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller\Exception;

defined('_JEXEC') || die;

/**
 * Exception thrown when the provided Model is not a DataModel
 */
class NotADataModel extends \InvalidArgumentException
{
}
Controller/Exception/TaskNotFound.php000060400000000603152455514240013717 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Controller\Exception;

defined('_JEXEC') || die;

/**
 * Exception thrown when we can't find a suitable method to handle the requested task
 */
class TaskNotFound extends \InvalidArgumentException
{
}
Update/Collection.php000060400000023604152455514240010602 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Update;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Download\Download;
use SimpleXMLElement;

class Collection
{
	/**
	 * Reads a "collection" XML update source and returns the complete tree of categories
	 * and extensions applicable for platform version $jVersion
	 *
	 * @param   string  $url       The collection XML update source URL to read from
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  array  A list of update sources applicable to $jVersion
	 */
	public function getAllUpdates($url, $jVersion = null)
	{
		// Get the target platform
		if (is_null($jVersion))
		{
			$jVersion = JVERSION;
		}

		// Initialise return value
		$updates = [
			'metadata'   => [
				'name'        => '',
				'description' => '',
			],
			'categories' => [],
			'extensions' => [],
		];

		// Download and parse the XML file
		$container  = Container::getInstance('com_foobar');
		$downloader = new Download($container);
		$xmlSource  = $downloader->getFromURL($url);

		try
		{
			$xml = new SimpleXMLElement($xmlSource, LIBXML_NONET);
		}
		catch (Exception $e)
		{
			return $updates;
		}

		// Sanity check
		if (($xml->getName() != 'extensionset'))
		{
			unset($xml);

			return $updates;
		}

		// Initialise return value with the stream metadata (name, description)
		$rootAttributes = $xml->attributes();
		foreach ($rootAttributes as $k => $v)
		{
			$updates['metadata'][$k] = (string) $v;
		}

		// Initialise the raw list of updates
		$rawUpdates = [
			'categories' => [],
			'extensions' => [],
		];

		// Segregate the raw list to a hierarchy of extension and category entries
		/** @var SimpleXMLElement $extension */
		foreach ($xml->children() as $extension)
		{
			switch ($extension->getName())
			{
				case 'category':
					// These are the parameters we expect in a category
					$params = [
						'name'                  => '',
						'description'           => '',
						'category'              => '',
						'ref'                   => '',
						'targetplatformversion' => $jVersion,
					];

					// These are the attributes of the element
					$attributes = $extension->attributes();

					// Merge them all
					foreach ($attributes as $k => $v)
					{
						$params[$k] = (string) $v;
					}

					// We can't have a category with an empty category name
					if (empty($params['category']))
					{
						continue;
					}

					// We can't have a category with an empty ref
					if (empty($params['ref']))
					{
						continue;
					}

					if (empty($params['description']))
					{
						$params['description'] = $params['category'];
					}

					if (!array_key_exists($params['category'], $rawUpdates['categories']))
					{
						$rawUpdates['categories'][$params['category']] = [];
					}

					$rawUpdates['categories'][$params['category']][] = $params;

					break;

				case 'extension':
					// These are the parameters we expect in a category
					$params = [
						'element'               => '',
						'type'                  => '',
						'version'               => '',
						'name'                  => '',
						'detailsurl'            => '',
						'targetplatformversion' => $jVersion,
					];

					// These are the attributes of the element
					$attributes = $extension->attributes();

					// Merge them all
					foreach ($attributes as $k => $v)
					{
						$params[$k] = (string) $v;
					}

					// We can't have an extension with an empty element
					if (empty($params['element']))
					{
						continue;
					}

					// We can't have an extension with an empty type
					if (empty($params['type']))
					{
						continue;
					}

					// We can't have an extension with an empty version
					if (empty($params['version']))
					{
						continue;
					}

					if (empty($params['name']))
					{
						$params['name'] = $params['element'] . ' ' . $params['version'];
					}

					if (!array_key_exists($params['type'], $rawUpdates['extensions']))
					{
						$rawUpdates['extensions'][$params['type']] = [];
					}

					if (!array_key_exists($params['element'], $rawUpdates['extensions'][$params['type']]))
					{
						$rawUpdates['extensions'][$params['type']][$params['element']] = [];
					}

					$rawUpdates['extensions'][$params['type']][$params['element']][] = $params;
					break;

				default:
					break;
			}
		}

		unset($xml);

		if (!empty($rawUpdates['categories']))
		{
			foreach ($rawUpdates['categories'] as $category => $entries)
			{
				$update                           = $this->filterListByPlatform($entries, $jVersion);
				$updates['categories'][$category] = $update;
			}
		}

		if (!empty($rawUpdates['extensions']))
		{
			foreach ($rawUpdates['extensions'] as $type => $extensions)
			{
				$updates['extensions'][$type] = [];

				if (!empty($extensions))
				{
					foreach ($extensions as $element => $entries)
					{
						$update                                 = $this->filterListByPlatform($entries, $jVersion);
						$updates['extensions'][$type][$element] = $update;
					}
				}
			}
		}

		return $updates;
	}

	/**
	 * Returns only the category definitions of a collection
	 *
	 * @param   string  $url       The URL of the collection update source
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  array  An array of category update definitions
	 */
	public function getCategories($url, $jVersion = null)
	{
		$allUpdates = $this->getAllUpdates($url, $jVersion);

		return $allUpdates['categories'];
	}

	/**
	 * Returns the update source for a specific category
	 *
	 * @param   string  $url       The URL of the collection update source
	 * @param   string  $category  The category name you want to get the update source URL of
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  string|null  The update stream URL, or null if it's not found
	 */
	public function getCategoryUpdateSource($url, $category, $jVersion = null)
	{
		$allUpdates = $this->getAllUpdates($url, $jVersion);

		if (array_key_exists($category, $allUpdates['categories']))
		{
			return $allUpdates['categories'][$category]['ref'];
		}
		else
		{
			return null;
		}
	}

	/**
	 * Get a list of updates for extensions only, optionally of a specific type
	 *
	 * @param   string  $url       The URL of the collection update source
	 * @param   string  $type      The extension type you want to get the update source URL of, empty to get all
	 *                             extension types
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  array|null  An array of extension update definitions or null if none is found
	 */
	public function getExtensions($url, $type = null, $jVersion = null)
	{
		$allUpdates = $this->getAllUpdates($url, $jVersion);

		if (empty($type))
		{
			return $allUpdates['extensions'];
		}
		elseif (array_key_exists($type, $allUpdates['extensions']))
		{
			return $allUpdates['extensions'][$type];
		}
		else
		{
			return null;
		}
	}

	/**
	 * Get the update source URL for a specific extension, based on the type and element, e.g.
	 * type=file and element=joomla is Joomla! itself.
	 *
	 * @param   string  $url       The URL of the collection update source
	 * @param   string  $type      The extension type you want to get the update source URL of
	 * @param   string  $element   The extension element you want to get the update source URL of
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  string|null  The update source URL or null if the extension is not found
	 */
	public function getExtensionUpdateSource($url, $type, $element, $jVersion = null)
	{
		$allUpdates = $this->getExtensions($url, $type, $jVersion);

		if (empty($allUpdates))
		{
			return null;
		}
		elseif (array_key_exists($element, $allUpdates))
		{
			return $allUpdates[$element]['detailsurl'];
		}
		else
		{
			return null;
		}
	}

	/**
	 * Filters a list of updates, returning only those available for the
	 * specified platform version $jVersion
	 *
	 * @param   array   $updates   An array containing update definitions (categories or extensions)
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  array|null  The update definition that is compatible, or null if none is compatible
	 */
	private function filterListByPlatform($updates, $jVersion = null)
	{
		// Get the target platform
		if (is_null($jVersion))
		{
			$jVersion = JVERSION;
		}

		$versionParts          = explode('.', $jVersion, 4);
		$platformVersionMajor  = $versionParts[0];
		$platformVersionMinor  = (count($versionParts) > 1) ? $platformVersionMajor . '.' . $versionParts[1] : $platformVersionMajor;
		$platformVersionNormal = (count($versionParts) > 2) ? $platformVersionMinor . '.' . $versionParts[2] : $platformVersionMinor;
		$platformVersionFull   = (count($versionParts) > 3) ? $platformVersionNormal . '.' . $versionParts[3] : $platformVersionNormal;

		$pickedExtension   = null;
		$pickedSpecificity = -1;

		foreach ($updates as $update)
		{
			// Test the target platform
			$targetPlatform = (string) $update['targetplatformversion'];

			if ($targetPlatform === $platformVersionFull)
			{
				$pickedExtension   = $update;
				$pickedSpecificity = 4;
			}
			elseif (($targetPlatform === $platformVersionNormal) && ($pickedSpecificity <= 3))
			{
				$pickedExtension   = $update;
				$pickedSpecificity = 3;
			}
			elseif (($targetPlatform === $platformVersionMinor) && ($pickedSpecificity <= 2))
			{
				$pickedExtension   = $update;
				$pickedSpecificity = 2;
			}
			elseif (($targetPlatform === $platformVersionMajor) && ($pickedSpecificity <= 1))
			{
				$pickedExtension   = $update;
				$pickedSpecificity = 1;
			}
		}

		return $pickedExtension;
	}
}
Update/Extension.php000060400000006654152455514240010471 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Update;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Download\Download;
use SimpleXMLElement;

/**
 * A helper class to read and parse "extension" update XML files over the web
 */
class Extension
{
	/**
	 * Reads an "extension" XML update source and returns all listed update entries.
	 *
	 * If you have a "collection" XML update source you should do something like this:
	 * $collection = new F0FUtilsUpdateCollection();
	 * $extensionUpdateURL = $collection->getExtensionUpdateSource($url, 'component', 'com_foobar', JVERSION);
	 * $extension = new F0FUtilsUpdateExtension();
	 * $updates = $extension->getUpdatesFromExtension($extensionUpdateURL);
	 *
	 * @param   string  $url  The extension XML update source URL to read from
	 *
	 * @return  array  An array of update entries
	 */
	public function getUpdatesFromExtension($url)
	{
		// Initialise
		$ret = [];

		// Get and parse the XML source
		$container  = Container::getInstance('com_FOOBAR');
		$downloader = new Download($container);
		$xmlSource  = $downloader->getFromURL($url);

		try
		{
			$xml = new SimpleXMLElement($xmlSource, LIBXML_NONET);
		}
		catch (Exception $e)
		{
			return $ret;
		}

		// Sanity check
		if (($xml->getName() != 'updates'))
		{
			unset($xml);

			return $ret;
		}

		// Let's populate the list of updates
		/** @var SimpleXMLElement $update */
		foreach ($xml->children() as $update)
		{
			// Sanity check
			if ($update->getName() != 'update')
			{
				continue;
			}

			$entry = [
				'infourl'        => ['title' => '', 'url' => ''],
				'downloads'      => [],
				'tags'           => [],
				'targetplatform' => [],
			];

			$properties = get_object_vars($update);

			foreach ($properties as $nodeName => $nodeContent)
			{
				switch ($nodeName)
				{
					default:
						$entry[$nodeName] = $nodeContent;
						break;

					case 'infourl':
					case 'downloads':
					case 'tags':
					case 'targetplatform':
						break;
				}
			}

			$infourlNode               = $update->xpath('infourl');
			$entry['infourl']['title'] = (string) $infourlNode[0]['title'];
			$entry['infourl']['url']   = (string) $infourlNode[0];

			$downloadNodes = $update->xpath('downloads/downloadurl');
			foreach ($downloadNodes as $downloadNode)
			{
				$entry['downloads'][] = [
					'type'   => (string) $downloadNode['type'],
					'format' => (string) $downloadNode['format'],
					'url'    => (string) $downloadNode,
				];
			}

			$tagNodes = $update->xpath('tags/tag');
			foreach ($tagNodes as $tagNode)
			{
				$entry['tags'][] = (string) $tagNode;
			}

			/** @var SimpleXMLElement[] $targetPlatformNode */
			$targetPlatformNode = $update->xpath('targetplatform');

			$entry['targetplatform']['name']    = (string) $targetPlatformNode[0]['name'];
			$entry['targetplatform']['version'] = (string) $targetPlatformNode[0]['version'];
			$client                             = $targetPlatformNode[0]->xpath('client');
			$entry['targetplatform']['client']  = (is_array($client) && count($client)) ? (string) $client[0] : '';
			$folder                             = $targetPlatformNode[0]->xpath('folder');
			$entry['targetplatform']['folder']  = is_array($folder) && count($folder) ? (string) $folder[0] : '';

			$ret[] = $entry;
		}

		unset($xml);

		return $ret;
	}
}
Update/Update.php000060400000061073152455514240007733 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Update;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Model\Model;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Updater\Updater;
use SimpleXMLElement;

/**
 * A helper Model to interact with Joomla!'s extensions update feature
 */
class Update extends Model
{
	/** @var Updater The Joomla! updater object */
	protected $updater = null;

	/** @var int The extension_id of this component */
	protected $extension_id = 0;

	/** @var string The currently installed version, as reported by the #__extensions table */
	protected $version = 'dev';

	/** @var string The name of the component e.g. com_something */
	protected $component = 'com_foobar';

	/** @var string The URL to the component's update XML stream */
	protected $updateSite = null;

	/** @var string The name to the component's update site (description of the update XML stream) */
	protected $updateSiteName = null;

	/** @var string The extra query to append to (commercial) components' download URLs */
	protected $extraQuery = null;

	/** @var string The component Options key which stores a copy of the Download ID */
	protected $paramsKey = 'update_dlid';

	/**
	 * Public constructor. Initialises the protected members as well. Useful $config keys:
	 * update_component        The component name, e.g. com_foobar
	 * update_version        The default version if the manifest cache is unreadable
	 * update_site            The URL to the component's update XML stream
	 * update_extraquery    The extra query to append to (commercial) components' download URLs
	 * update_sitename        The update site's name (description)
	 * update_paramskey        The component parameters key which holds the license key in J3 (and a copy of it in J4)
	 *
	 * @param   array  $config
	 */
	public function __construct($config = [])
	{
		$container = Container::getInstance('com_FOOBAR');

		if (isset($config['update_container']) && is_object($config['update_container']) && ($config['update_container'] instanceof Container))
		{
			$container = $config['update_container'];
		}

		parent::__construct($container);

		// Get an instance of the updater class
		$this->updater = Updater::getInstance();

		// Get the component name
		if (isset($config['update_component']))
		{
			$this->component = $config['update_component'];
		}
		else
		{
			$this->component = $this->input->getCmd('option', '');
		}

		// Get the component version
		if (isset($config['update_version']))
		{
			$this->version = $config['update_version'];
		}

		// Get the update site
		if (isset($config['update_site']))
		{
			$this->updateSite = $config['update_site'];
		}

		// Get the extra query
		if (isset($config['update_extraquery']))
		{
			$this->extraQuery = $config['update_extraquery'];
		}

		// Get the extra query
		if (isset($config['update_sitename']))
		{
			$this->updateSiteName = $config['update_sitename'];
		}

		// Get the extra query
		if (isset($config['update_paramskey']))
		{
			$this->paramsKey = $config['update_paramskey'];
		}

		// Get the extension type
		$extension = $this->getExtensionObject();

		if (is_object($extension))
		{
			$this->extension_id = $extension->extension_id;

			if (empty($this->version) || ($this->version == 'dev'))
			{
				$data = json_decode($extension->manifest_cache, true);

				if (isset($data['version']))
				{
					$this->version = $data['version'];
				}
			}

		}
	}

	/**
	 * Gets the license key for a paid extension.
	 *
	 * On Joomla! 3 or when $forceLegacy is true we look in the component Options.
	 *
	 * On Joomla! 4 we use the information in the dlid element of the extension's XML manifest to parse the extra_query
	 * fields of all configured update sites of the extension. This is the same thing Joomla does when it tries to
	 * determine the license key of our extension when installing updates. If the extension is missing, it has no
	 * associated update sites, the update sites are missing / rebuilt / disassociated from the extension or the
	 * extra_query of all update site records is empty we parse the $extraQuery set in the constructor, if any. Also
	 * note that on Joomla 4 mode if the extension does not exist, does not have a manifest or does not have a valid
	 * dlid element in its manifest we will end up returning an empty string, just like Joomla! itself would have done
	 * when installing updates.
	 *
	 * @param   bool  $forceLegacy  Should I always retrieve the legacy license key, even in J4?
	 *
	 * @return  string
	 */
	public function getLicenseKey($forceLegacy = false)
	{
		$legacyParamsKey = $this->getLegacyParamsKey();

		// Joomla 3 (Legacy): Download ID stored in the component options
		if ($forceLegacy || !version_compare(JVERSION, '3.999.999', 'gt'))
		{
			return $this->container->params->get($legacyParamsKey, '');
		}

		// Joomla! 4. We need to parse the extra_query of the update sites to get the correct Download ID.
		$updateSites = $this->getUpdateSites();
		$extra_query = array_reduce($updateSites, function ($extra_query, $updateSite) {
			if (!empty($extra_query))
			{
				return $extra_query;
			}

			return $updateSite['extra_query'];
		}, '');

		// Fall back to legacy extra query
		if (empty($extra_query))
		{
			$extra_query = $this->extraQuery;
		}

		// Return the parsed results.
		return $this->getLicenseKeyFromExtraQuery($extra_query);
	}

	/**
	 * Get the contents of all the update sites of the configured extension
	 *
	 * @return  array
	 */
	public function getUpdateSites()
	{
		$updateSiteIDs = $this->getUpdateSiteIds();
		$db            = $this->container->db;
		$query         = $db->getQuery(true)
			->select('*')
			->from($db->qn('#__update_sites'))
			->where($db->qn('update_site_id') . ' IN (' . implode(', ', $updateSiteIDs) . ')');

		try
		{
			$db->setQuery($query);

			$ret = $db->loadAssocList('update_site_id');
		}
		catch (Exception $e)
		{
			$ret = null;
		}

		return empty($ret) ? [] : $ret;
	}

	public function setLicenseKey($licenseKey)
	{
		$legacyParamsKey = $this->getLegacyParamsKey();

		// Sanitize and validate the license key. If it's not valid we set an empty license key.
		$licenseKey = $this->sanitizeLicenseKey($licenseKey);

		if (!$this->isValidLicenseKey($licenseKey))
		{
			$licenseKey = '';
		}

		// Update $this->extraQuery.
		$this->extraQuery = $this->getExtraQueryString($licenseKey);

		// Save the license key in the component options ($legacyParamsKey)
		$this->container->params->set($legacyParamsKey, $licenseKey);
		$this->container->params->save();

		// Apply the new extra_query to the update site
		$this->refreshUpdateSite();
	}

	/**
	 * Copies a Joomla 3 license key from the Options storage to Joomla 4 download key storage (the extra_query column
	 * of the #__update_sites table).
	 *
	 * This method does nothing on Joomla 3.
	 *
	 * @return  void
	 */
	public function upgradeLicenseKey()
	{
		// Only applies to Joomla! 4
		if (!version_compare(JVERSION, '3.999.999', 'gt'))
		{
			return;
		}

		// Make sure we DO have a legacy license key
		$legacyKey = $this->getLicenseKey(true);

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

		// Make sure we DO NOT have a J4 key. If we do, the J4 key wins and gets backported to legacy storage.
		$licenseKey = $this->getLicenseKey(false);

		if (!empty($licenseKey))
		{
			$this->backportLicenseKey();

			return;
		}

		// Save the legacy key as non-legacy. This updates the #__update_sites record, applying the license key.
		$this->setLicenseKey($legacyKey);
	}

	/**
	 * Copies a Joomla 4 license key from the download key storage (the extra_query column of the #__update_sites table)
	 * to the legacy Options storage.
	 *
	 * This method does nothing on Joomla 3.
	 *
	 * @return  void
	 */
	public function backportLicenseKey()
	{
		$legacyParamsKey = $this->getLegacyParamsKey();

		// Only applies to Joomla! 4
		if (!version_compare(JVERSION, '3.999.999', 'gt'))
		{
			return;
		}

		// Make sure we DO have a J4 key
		$licenseKey = $this->getLicenseKey(false);

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

		// Make sure that the legacy key is NOT the same as the J4 key
		$legacyKey = $this->getLicenseKey(true);

		if ($legacyKey == $licenseKey)
		{
			return;
		}

		// Save the license key to the legacy storage (component options)
		$this->container->params->set($legacyParamsKey, $licenseKey);
		$this->container->params->save();
	}

	/**
	 * Get an extra query string based on the dlid element of the XML manifest file of the extension.
	 *
	 * If the extension does not exist, the manifest does not exist or it does not have a dlid element we fall back to
	 * the legacy implementation of extra_query (getExtraQueryStringLegacy)
	 *
	 * @param   string  $licenseKey
	 *
	 * @return  string
	 */
	public function getExtraQueryString($licenseKey)
	{
		// Make sure the (sanitized) license key is valid. Otherwise we return an empty string.
		$licenseKey = $this->sanitizeLicenseKey($licenseKey);

		if (!$this->isValidLicenseKey($licenseKey))
		{
			return '';
		}

		// Get a fallback extra query using the legacy method
		$fallbackExtraQuery = $this->getExtraQueryStringLegacy($licenseKey);

		// Get the extension XML manifest. If the extension or the manifest don't exist use the fallback extra_query.
		$extension = $this->getExtensionObject();

		if (!$extension)
		{
			return $fallbackExtraQuery;
		}

		$installXmlFile = $this->getManifestXML(
			$extension->element,
			$extension->type,
			(int) $extension->client_id,
			$extension->folder
		);

		if (!$installXmlFile)
		{
			return $fallbackExtraQuery;
		}

		// If the manifest does not have the dlid element return the fallback extra_query.
		if (!isset($installXmlFile->dlid))
		{
			return $fallbackExtraQuery;
		}

		$prefix = (string) $installXmlFile->dlid['prefix'];
		$suffix = (string) $installXmlFile->dlid['suffix'];

		return $prefix . $this->sanitizeLicenseKey($licenseKey) . $suffix;
	}

	/**
	 * Retrieves the update information of the component, returning an array with the following keys:
	 *
	 * hasUpdate  True if an update is available
	 * version    The version of the available update
	 * infoURL    The URL to the download page of the update
	 *
	 * @param   bool  $force  Set to true if you want to forcibly reload the update information
	 *
	 * @return  array  See the method description for more information
	 */
	public function getUpdates($force = false)
	{
		$db = $this->container->db;

		// Default response (no update)
		$updateResponse = [
			'hasUpdate' => false,
			'version'   => '',
			'infoURL'   => '',
		];

		if (empty($this->extension_id))
		{
			return $updateResponse;
		}

		// If we had to update the version number stored in the database then we should force reload the updates
		if ($this->updatedCachedVersionNumber())
		{
			$force = true;
		}

		// If we are forcing the reload, set the last_check_timestamp to 0
		// and remove cached component update info in order to force a reload
		if ($force)
		{
			// Find the update site IDs
			$updateSiteIds = $this->getUpdateSiteIds();

			if (empty($updateSiteIds))
			{
				return $updateResponse;
			}

			// Set the last_check_timestamp to 0
			$query = $db->getQuery(true)
				->update($db->qn('#__update_sites'))
				->set($db->qn('last_check_timestamp') . ' = ' . $db->q('0'))
				->where($db->qn('update_site_id') . ' IN (' . implode(', ', $updateSiteIds) . ')');
			$db->setQuery($query);
			$db->execute();

			// Remove cached component update info from #__updates
			$query = $db->getQuery(true)
				->delete($db->qn('#__updates'))
				->where($db->qn('update_site_id') . ' IN (' . implode(', ', $updateSiteIds) . ')');
			$db->setQuery($query);
			$db->execute();
		}

		// Use the update cache timeout specified in com_installer
		$comInstallerParams = ComponentHelper::getParams('com_installer', false);
		$timeout            = 3600 * $comInstallerParams->get('cachetimeout', '6');

		// Load any updates from the network into the #__updates table
		$this->updater->findUpdates($this->extension_id, $timeout);

		// Get the update record from the database
		$query = $db->getQuery(true)
			->select('*')
			->from($db->qn('#__updates'))
			->where($db->qn('extension_id') . ' = ' . $db->q($this->extension_id));
		$db->setQuery($query);
		$updateRecord = $db->loadObject();

		// If we have an update record in the database return the information found there
		if (is_object($updateRecord))
		{
			$updateResponse = [
				'hasUpdate' => true,
				'version'   => $updateRecord->version,
				'infoURL'   => $updateRecord->infourl,
			];
		}

		return $updateResponse;
	}

	/**
	 * Gets the update site Ids for our extension.
	 *
	 * @return    array    An array of IDs
	 */
	public function getUpdateSiteIds()
	{
		$db    = $this->container->db;
		$query = $db->getQuery(true)
			->select($db->qn('update_site_id'))
			->from($db->qn('#__update_sites_extensions'))
			->where($db->qn('extension_id') . ' = ' . $db->q($this->extension_id));
		$db->setQuery($query);

		try
		{
			$ret = $db->loadColumn(0);
		}
		catch (Exception $e)
		{
			$ret = null;
		}

		return is_array($ret) ? $ret : [];
	}

	/**
	 * Get the currently installed version as reported by the #__extensions table
	 *
	 * @return  string
	 */
	public function getVersion()
	{
		return $this->version;
	}

	/**
	 * Override the currently installed version as reported by the #__extensions table
	 *
	 * @param   string  $version
	 */
	public function setVersion($version)
	{
		$this->version = $version;
	}

	/**
	 * Refreshes the Joomla! update sites for this extension as needed
	 *
	 * @return  void
	 */
	public function refreshUpdateSite()
	{
		if (empty($this->extension_id))
		{
			return;
		}

		// Create the update site definition we want to store to the database
		$update_site = [
			'name'                 => $this->updateSiteName,
			'type'                 => 'extension',
			'location'             => $this->updateSite,
			'enabled'              => 1,
			'last_check_timestamp' => 0,
			'extra_query'          => $this->extraQuery,
		];

		// Get a reference to the db driver
		$db = $this->container->db;

		// Get the #__update_sites columns
		$columns = $db->getTableColumns('#__update_sites', true);

		if (version_compare(JVERSION, '3.0.0', 'lt') || !array_key_exists('extra_query', $columns))
		{
			unset($update_site['extra_query']);
		}

		// Get the update sites for our extension
		$updateSiteIds = $this->getUpdateSiteIds();

		if (empty($updateSiteIds))
		{
			$updateSiteIds = [];
		}

		/** @var boolean $needNewUpdateSite Do I need to create a new update site? */
		$needNewUpdateSite = true;

		/** @var int[] $deleteOldSites Old Site IDs to delete */
		$deleteOldSites = [];

		// Loop through all update sites
		foreach ($updateSiteIds as $id)
		{
			$query = $db->getQuery(true)
				->select('*')
				->from($db->qn('#__update_sites'))
				->where($db->qn('update_site_id') . ' = ' . $db->q($id));
			$db->setQuery($query);
			$aSite = $db->loadObject();

			if (empty($aSite))
			{
				// Update site is now up-to-date, don't need to refresh it anymore.
				continue;
			}

			// We have an update site that looks like ours
			if ($needNewUpdateSite && ($aSite->name == $update_site['name']) && ($aSite->location == $update_site['location']))
			{
				$needNewUpdateSite = false;
				$mustUpdate        = false;

				// Is it enabled? If not, enable it.
				if (!$aSite->enabled)
				{
					$mustUpdate     = true;
					$aSite->enabled = 1;
				}

				// Do we have the extra_query property (J 3.2+) and does it match?
				if (property_exists($aSite, 'extra_query') && isset($update_site['extra_query'])
					&& ($aSite->extra_query != $update_site['extra_query']))
				{
					$mustUpdate         = true;
					$aSite->extra_query = $update_site['extra_query'];
				}

				// Update the update site if necessary
				if ($mustUpdate)
				{
					$db->updateObject('#__update_sites', $aSite, 'update_site_id', true);
				}

				continue;
			}

			// In any other case we need to delete this update site, it's obsolete
			$deleteOldSites[] = $aSite->update_site_id;
		}

		if (!empty($deleteOldSites))
		{
			try
			{
				$obsoleteIDsQuoted = array_map([$db, 'quote'], $deleteOldSites);

				// Delete update sites
				$query = $db->getQuery(true)
					->delete('#__update_sites')
					->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')');
				$db->setQuery($query)->execute();

				// Delete update sites to extension ID records
				$query = $db->getQuery(true)
					->delete('#__update_sites_extensions')
					->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')');
				$db->setQuery($query)->execute();
			}
			catch (Exception $e)
			{
				// Do nothing on failure
				return;
			}

		}

		// Do we still need to create a new update site?
		if ($needNewUpdateSite)
		{
			// No update sites defined. Create a new one.
			$newSite = (object) $update_site;
			$db->insertObject('#__update_sites', $newSite);

			$id                  = $db->insertid();
			$updateSiteExtension = (object) [
				'update_site_id' => $id,
				'extension_id'   => $this->extension_id,
			];
			$db->insertObject('#__update_sites_extensions', $updateSiteExtension);
		}
	}

	/**
	 * Removes any update sites which go by the same name or the same location as our update site but do not match the
	 * extension ID.
	 */
	public function removeObsoleteUpdateSites()
	{
		$db = $this->container->db;

		// Get update site IDs
		$updateSiteIDs = $this->getUpdateSiteIds();

		// Find update sites where the name OR the location matches BUT they are not one of the update site IDs
		$query = $db->getQuery(true)
			->select($db->qn('update_site_id'))
			->from($db->qn('#__update_sites'))
			->where(
				'((' . $db->qn('name') . ' = ' . $db->q($this->updateSiteName) . ') OR ' .
				'(' . $db->qn('location') . ' = ' . $db->q($this->updateSite) . '))'
			);

		if (!empty($updateSiteIDs))
		{
			$updateSitesQuoted = array_map([$db, 'quote'], $updateSiteIDs);
			$query->where($db->qn('update_site_id') . ' NOT IN (' . implode(',', $updateSitesQuoted) . ')');
		}

		try
		{
			$ids = $db->setQuery($query)->loadColumn();

			if (!empty($ids))
			{
				$obsoleteIDsQuoted = array_map([$db, 'quote'], $ids);

				// Delete update sites
				$query = $db->getQuery(true)
					->delete('#__update_sites')
					->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')');
				$db->setQuery($query)->execute();

				// Delete update sites to extension ID records
				$query = $db->getQuery(true)
					->delete('#__update_sites_extensions')
					->where($db->qn('update_site_id') . ' IN (' . implode(',', $obsoleteIDsQuoted) . ')');
				$db->setQuery($query)->execute();
			}
		}
		catch (Exception $e)
		{
			// Do nothing on failure
			return;
		}
	}

	/**
	 * Makes sure that the version number cached in the #__extensions table is consistent with the version number set in
	 * this model.
	 *
	 * @return  bool  True if we updated the version number cached in the #__extensions table.
	 *
	 * @since   3.1.2
	 */
	public function updatedCachedVersionNumber()
	{
		$extension = $this->getExtensionObject();

		if (!is_object($extension))
		{
			return false;
		}

		$data       = json_decode($extension->manifest_cache, true);
		$mustUpdate = true;

		if (isset($data['version']))
		{
			$mustUpdate = $this->version != $data['version'];
		}

		if (!$mustUpdate)
		{
			return false;
		}

		// The cached version is wrong; let's update it
		$data['version']           = $this->version;
		$extension->manifest_cache = json_encode($data);
		$db                        = $this->container->db;

		return $db->updateObject('#__extensions', $extension, ['extension_id']);
	}

	/**
	 * Returns an object with the #__extensions table record for the current extension.
	 *
	 * @return  mixed
	 */
	public function getExtensionObject()
	{
		[$extensionPrefix, $extensionName] = explode('_', $this->component);

		switch ($extensionPrefix)
		{
			default:
			case 'com':
				$type = 'component';
				$name = $this->component;
				break;

			case 'pkg':
				$type = 'package';
				$name = $this->component;
				break;
		}

		// Find the extension ID
		$db    = $this->container->db;
		$query = $db->getQuery(true)
			->select('*')
			->from($db->qn('#__extensions'))
			->where($db->qn('type') . ' = ' . $db->q($type))
			->where($db->qn('element') . ' = ' . $db->q($name));

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

		return $extension;
	}

	/**
	 * Is the provided string a valid license key?
	 *
	 * YOU SHOULD OVERRIDE THIS METHOD. The default implementation checks for valid Download IDs in the format used by
	 * Akeeba software.
	 *
	 * @param   string  $licenseKey
	 *
	 * @return  bool
	 */
	public function isValidLicenseKey($licenseKey)
	{
		return preg_match('/^([0-9]{1,}:)?[0-9a-f]{32}$/i', $licenseKey) === 1;
	}

	/**
	 * Sanitizes the license key.
	 *
	 * YOU SHOULD OVERRIDE THIS METHOD. The default implementation returns a lowercase string with all characters except
	 * letters, numbers and colons removed.
	 *
	 * @param   string  $licenseKey
	 *
	 * @return  string  The sanitized license key
	 */
	public function sanitizeLicenseKey($licenseKey)
	{
		return strtolower(preg_replace("/[^a-zA-Z0-9:]/", "", $licenseKey));
	}

	/**
	 * Returns the component Options key which holds a copy of the license key
	 *
	 * @return  string
	 */
	protected function getLegacyParamsKey()
	{
		if (!empty($this->paramsKey))
		{
			return $this->paramsKey;
		}

		$this->paramsKey = 'update_dlid';

		return $this->paramsKey;
	}

	/**
	 * Extract the download ID from an extra_query based on the prefix and suffix information stored in the dlid element
	 * of the extension's XML manifest file.
	 *
	 * @param   string  $extra_query
	 *
	 * @return string
	 */
	protected function getLicenseKeyFromExtraQuery($extra_query)
	{
		$extra_query = trim($extra_query);

		if (empty($extra_query))
		{
			return '';
		}

		// Get the extension XML manifest. If the extension or the manifest don't exist return an empty string.
		$extension = $this->getExtensionObject();

		if (!$extension)
		{
			return '';
		}

		$installXmlFile = $this->getManifestXML(
			$extension->element,
			$extension->type,
			(int) $extension->client_id,
			$extension->folder
		);

		if (!$installXmlFile)
		{
			return '';
		}

		// If the manifest does not have a dlid element return an empty string.
		if (!isset($installXmlFile->dlid))
		{
			return '';
		}

		// Naive parsing of the extra_query, the same way Joomla does.
		$prefix     = (string) $installXmlFile->dlid['prefix'];
		$suffix     = (string) $installXmlFile->dlid['suffix'];
		$licenseKey = substr($extra_query, strlen($prefix));

		if ($licenseKey === false)
		{
			return '';
		}

		if ($suffix)
		{
			$licenseKey = substr($licenseKey, 0, -strlen($suffix));
		}

		return ($licenseKey === false) ? '' : $licenseKey;
	}

	/**
	 * Get a legacy extra query string. Do NOT call this directly. Call getExtraQueryString() instead.
	 *
	 * YOU SHOULD OVERRIDE THIS METHOD. This returns dlid=SANITIZED_LICENSE_KEY which is what Akeeba Release System,
	 * used to deliver all Akeeba extensions, expects.
	 *
	 * @param   string  $licenseKey  The license key
	 *
	 * @return  string  The extra_query string to append to a downlaod URL to implement the license key
	 */
	protected function getExtraQueryStringLegacy($licenseKey)
	{
		if (empty($licenseKey) || !$this->isValidLicenseKey($licenseKey))
		{
			return '';
		}

		return 'dlid=' . $this->sanitizeLicenseKey($licenseKey);
	}

	/**
	 * Get the manifest XML file of a given extension.
	 *
	 * @param   string   $element    element of an extension
	 * @param   string   $type       type of an extension
	 * @param   integer  $client_id  client_id of an extension
	 * @param   string   $folder     folder of an extension
	 *
	 * @return  SimpleXMLElement
	 */
	protected function getManifestXML($element, $type, $client_id = 1, $folder = null)
	{
		$path = $client_id ? JPATH_ADMINISTRATOR : JPATH_ROOT;

		switch ($type)
		{
			case 'component':
				$path .= '/components/' . $element . '/' . substr($element, 4) . '.xml';
				break;
			case 'plugin':
				$path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml';
				break;
			case 'module':
				$path .= '/modules/' . $element . '/' . $element . '.xml';
				break;
			case 'template':
				$path .= '/templates/' . $element . '/templateDetails.xml';
				break;
			case 'library':
				$path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml';
				break;
			case 'file':
				$path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml';
				break;
			case 'package':
				$path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml';
		}

		return simplexml_load_file($path);
	}
}
Update/Joomla.php000060400000031220152455514240007721 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Update;

defined('_JEXEC') || die;

class Joomla extends Extension
{
	/**
	 * The source for LTS updates
	 *
	 * @var  string
	 */
	protected static $lts_url = 'http://update.joomla.org/core/list.xml';

	/**
	 * The source for STS updates
	 *
	 * @var  string
	 */
	protected static $sts_url = 'http://update.joomla.org/core/sts/list_sts.xml';

	/**
	 * The source for test release updates
	 *
	 * @var  string
	 */
	protected static $test_url = 'http://update.joomla.org/core/test/list_test.xml';

	/**
	 * Reads a "collection" XML update source and picks the correct source URL
	 * for the extension update source.
	 *
	 * @param   string  $url       The collection XML update source URL to read from
	 * @param   string  $jVersion  Joomla! version to fetch updates for, or null to use JVERSION
	 *
	 * @return  string  The URL of the extension update source, or empty if no updates are provided / fetching failed
	 */
	public function getUpdateSourceFromCollection($url, $jVersion = null)
	{
		$provider = new Collection();

		return $provider->getExtensionUpdateSource($url, 'file', 'joomla', $jVersion);
	}

	/**
	 * Determines the properties of a version: STS/LTS, normal or testing
	 *
	 * @param   string  $jVersion        The version number to check
	 * @param   string  $currentVersion  The current Joomla! version number
	 *
	 * @return  array  The properties analysis
	 */
	public function getVersionProperties($jVersion, $currentVersion = null)
	{
		// Initialise
		$ret = [
			'lts'     => true,
			// Is this an LTS release? False means STS.
			'current' => false,
			// Is this a release in the $currentVersion branch?
			'upgrade' => 'none',
			// Upgrade relation of $jVersion to $currentVersion: 'none' (can't upgrade), 'lts' (next or current LTS), 'sts' (next or current STS) or 'current' (same release, no upgrade available)
			'testing' => false,
			// Is this a testing (alpha, beta, RC) release?
		];

		// Get the current version if none is defined
		if (is_null($currentVersion))
		{
			$currentVersion = JVERSION;
		}

		// Sanitise version numbers
		$sameVersion    = $jVersion == $currentVersion;
		$jVersion       = $this->sanitiseVersion($jVersion);
		$currentVersion = $this->sanitiseVersion($currentVersion);
		$sameVersion    = $sameVersion || ($jVersion == $currentVersion);

		// Get the base version
		$baseVersion = substr($jVersion, 0, 3);

		// Get the minimum and maximum current version numbers
		$current_minimum = substr($currentVersion, 0, 3);
		$current_maximum = $current_minimum . '.9999';

		// Initialise STS/LTS version numbers
		$sts_minimum = false;
		$sts_maximum = false;
		$lts_minimum = false;

		// Is it an LTS or STS release?
		switch ($baseVersion)
		{
			case '1.5':
				$ret['lts'] = true;
				break;

			case '1.6':
				$ret['lts']  = false;
				$sts_minimum = '1.7';
				$sts_maximum = '1.7.999';
				$lts_minimum = '2.5';
				break;

			case '1.7':
				$ret['lts']  = false;
				$sts_minimum = false;
				$lts_minimum = '2.5';
				break;

			case '2.5':
				$ret['lts']  = true;
				$sts_minimum = false;
				$lts_minimum = '2.5';
				break;

			default:
				$majorVersion = (int) substr($jVersion, 0, 1);
				//$minorVersion = (int) substr($jVersion, 2, 1);

				$ret['lts']  = true;
				$sts_minimum = false;
				$lts_minimum = $majorVersion . '.0';
				break;
		}

		// Is it a current release?
		if (version_compare($jVersion, $current_minimum, 'ge') && version_compare($jVersion, $current_maximum, 'le'))
		{
			$ret['current'] = true;
		}

		// Is this a testing release?
		$versionParts    = explode('.', $jVersion);
		$lastVersionPart = array_pop($versionParts);

		if (in_array(substr($lastVersionPart, 0, 1), ['a', 'b']))
		{
			$ret['testing'] = true;
		}
		elseif (substr($lastVersionPart, 0, 2) == 'rc')
		{
			$ret['testing'] = true;
		}
		elseif (substr($lastVersionPart, 0, 3) == 'dev')
		{
			$ret['testing'] = true;
		}

		// Find the upgrade relation of $jVersion to $currentVersion
		if (version_compare($jVersion, $currentVersion, 'eq'))
		{
			$ret['upgrade'] = 'current';
		}
		elseif (($sts_minimum !== false) && version_compare($jVersion, $sts_minimum, 'ge') && version_compare($jVersion, $sts_maximum, 'le'))
		{
			$ret['upgrade'] = 'sts';
		}
		elseif (($lts_minimum !== false) && version_compare($jVersion, $lts_minimum, 'ge'))
		{
			$ret['upgrade'] = 'lts';
		}
		elseif ($baseVersion == $current_minimum)
		{
			$ret['upgrade'] = $ret['lts'] ? 'lts' : 'sts';
		}
		else
		{
			$ret['upgrade'] = 'none';
		}

		if ($sameVersion)
		{
			$ret['upgrade'] = 'none';
		}

		return $ret;
	}


	/**
	 * Filters a list of updates, making sure they apply to the specified CMS
	 * release.
	 *
	 * @param   array   $updates   A list of update records returned by the getUpdatesFromExtension method
	 * @param   string  $jVersion  The current Joomla! version number
	 *
	 * @return  array  A filtered list of updates. Each update record also includes version relevance information.
	 */
	public function filterApplicableUpdates($updates, $jVersion = null)
	{
		if (empty($jVersion))
		{
			$jVersion = JVERSION;
		}

		$versionParts         = explode('.', $jVersion, 4);
		$platformVersionMajor = $versionParts[0];
		$platformVersionMinor = $platformVersionMajor . '.' . $versionParts[1];
		//$platformVersionNormal = $platformVersionMinor . '.' . $versionParts[2];
		//$platformVersionFull   = (count($versionParts) > 3) ? $platformVersionNormal . '.' . $versionParts[3] : $platformVersionNormal;

		$ret = [];

		foreach ($updates as $update)
		{
			// Check each update for platform match
			if (strtolower($update['targetplatform']['name']) != 'joomla')
			{
				continue;
			}

			$targetPlatformVersion = $update['targetplatform']['version'];

			if (!preg_match('/' . $targetPlatformVersion . '/', $platformVersionMinor))
			{
				continue;
			}

			// Get some information from the version number
			$updateVersion     = $update['version'];
			$versionProperties = $this->getVersionProperties($updateVersion, $jVersion);

			if ($versionProperties['upgrade'] == 'none')
			{
				continue;
			}

			// The XML files are ill-maintained. Maybe we already have this update?
			if (!array_key_exists($updateVersion, $ret))
			{
				$ret[$updateVersion] = array_merge($update, $versionProperties);
			}
		}

		return $ret;
	}

	/**
	 * Joomla! has a lousy track record in naming its alpha, beta and release
	 * candidate releases. The convention used seems to be "what the hell the
	 * current package maintainer thinks looks better". This method tries to
	 * figure out what was in the mind of the maintainer and translate the
	 * funky version number to an actual PHP-format version string.
	 *
	 * @param   string  $version  The whatever-format version number
	 *
	 * @return  string  A standard formatted version number
	 */
	public function sanitiseVersion($version)
	{
		$test                   = strtolower($version);
		$alphaQualifierPosition = strpos($test, 'alpha-');
		$betaQualifierPosition  = strpos($test, 'beta-');
		$betaQualifierPosition2 = strpos($test, '-beta');
		$rcQualifierPosition    = strpos($test, 'rc-');
		$rcQualifierPosition2   = strpos($test, '-rc');
		$rcQualifierPosition3   = strpos($test, 'rc');
		$devQualifiedPosition   = strpos($test, 'dev');

		if ($alphaQualifierPosition !== false)
		{
			$betaRevision = substr($test, $alphaQualifierPosition + 6);
			if (!$betaRevision)
			{
				$betaRevision = 1;
			}
			$test = substr($test, 0, $alphaQualifierPosition) . '.a' . $betaRevision;
		}
		elseif ($betaQualifierPosition !== false)
		{
			$betaRevision = substr($test, $betaQualifierPosition + 5);
			if (!$betaRevision)
			{
				$betaRevision = 1;
			}
			$test = substr($test, 0, $betaQualifierPosition) . '.b' . $betaRevision;
		}
		elseif ($betaQualifierPosition2 !== false)
		{
			$betaRevision = substr($test, $betaQualifierPosition2 + 5);

			if (!$betaRevision)
			{
				$betaRevision = 1;
			}

			$test = substr($test, 0, $betaQualifierPosition2) . '.b' . $betaRevision;
		}
		elseif ($rcQualifierPosition !== false)
		{
			$betaRevision = substr($test, $rcQualifierPosition + 5);
			if (!$betaRevision)
			{
				$betaRevision = 1;
			}
			$test = substr($test, 0, $rcQualifierPosition) . '.rc' . $betaRevision;
		}
		elseif ($rcQualifierPosition2 !== false)
		{
			$betaRevision = substr($test, $rcQualifierPosition2 + 3);

			if (!$betaRevision)
			{
				$betaRevision = 1;
			}

			$test = substr($test, 0, $rcQualifierPosition2) . '.rc' . $betaRevision;
		}
		elseif ($rcQualifierPosition3 !== false)
		{
			$betaRevision = substr($test, $rcQualifierPosition3 + 5);

			if (!$betaRevision)
			{
				$betaRevision = 1;
			}

			$test = substr($test, 0, $rcQualifierPosition3) . '.rc' . $betaRevision;
		}
		elseif ($devQualifiedPosition !== false)
		{
			$betaRevision = substr($test, $devQualifiedPosition + 6);
			if (!$betaRevision)
			{
				$betaRevision = '';
			}
			$test = substr($test, 0, $devQualifiedPosition) . '.dev' . $betaRevision;
		}

		return $test;
	}

	/**
	 * Reloads the list of all updates available for the specified Joomla! version
	 * from the network.
	 *
	 * @param   array   $sources   The enabled sources to look into
	 * @param   string  $jVersion  The Joomla! version we are checking updates for
	 *
	 * @return   array  A list of updates for the installed, current, lts and sts versions
	 */
	public function getUpdates($sources = [], $jVersion = null)
	{
		// Make sure we have a valid list of sources
		if (empty($sources) || !is_array($sources))
		{
			$sources = [];
		}

		$defaultSources = ['lts' => true, 'sts' => true, 'test' => true, 'custom' => ''];

		$sources = array_merge($defaultSources, $sources);

		// Use the current JVERSION if none is specified
		if (empty($jVersion))
		{
			$jVersion = JVERSION;
		}

		// Get the current branch' min/max versions
		$versionParts      = explode('.', $jVersion, 4);
		$currentMinVersion = $versionParts[0] . '.' . $versionParts[1];
		$currentMaxVersion = $versionParts[0] . '.' . $versionParts[1] . '.9999';


		// Retrieve all updates
		$allUpdates = [];

		foreach ($sources as $source => $value)
		{
			if (($value === false) || empty($value))
			{
				continue;
			}

			switch ($source)
			{
				default:
				case 'lts':
					$url = self::$lts_url;
					break;

				case 'sts':
					$url = self::$sts_url;
					break;

				case 'test':
					$url = self::$test_url;
					break;

				case 'custom':
					$url = $value;
					break;
			}

			$url = $this->getUpdateSourceFromCollection($url, $jVersion);

			if (!empty($url))
			{
				$updates = $this->getUpdatesFromExtension($url);

				if (!empty($updates))
				{
					$applicableUpdates = $this->filterApplicableUpdates($updates, $jVersion);

					if (!empty($applicableUpdates))
					{
						$allUpdates = array_merge($allUpdates, $applicableUpdates);
					}
				}
			}
		}

		$ret = [
			// Currently installed version (used to reinstall, if available)
			'installed' => [
				'version' => '',
				'package' => '',
				'infourl' => '',
			],
			// Current branch
			'current'   => [
				'version' => '',
				'package' => '',
				'infourl' => '',
			],
			// Upgrade to STS release
			'sts'       => [
				'version' => '',
				'package' => '',
				'infourl' => '',
			],
			// Upgrade to LTS release
			'lts'       => [
				'version' => '',
				'package' => '',
				'infourl' => '',
			],
			// Upgrade to LTS release
			'test'      => [
				'version' => '',
				'package' => '',
				'infourl' => '',
			],
		];

		foreach ($allUpdates as $update)
		{
			$sections = [];

			if ($update['upgrade'] == 'current')
			{
				$sections[0] = 'installed';
			}
			elseif (version_compare($update['version'], $currentMinVersion, 'ge') && version_compare($update['version'], $currentMaxVersion, 'le'))
			{
				$sections[0] = 'current';
			}
			else
			{
				$sections[0] = '';
			}

			$sections[1] = $update['lts'] ? 'lts' : 'sts';

			if ($update['testing'])
			{
				$sections = ['test'];
			}

			foreach ($sections as $section)
			{
				if (empty($section))
				{
					continue;
				}

				$existingVersionForSection = $ret[$section]['version'];

				if (empty($existingVersionForSection))
				{
					$existingVersionForSection = '0.0.0';
				}

				if (version_compare($update['version'], $existingVersionForSection, 'ge'))
				{
					$ret[$section]['version'] = $update['version'];
					$ret[$section]['package'] = $update['downloads'][0]['url'];
					$ret[$section]['infourl'] = $update['infourl']['url'];
				}
			}
		}

		// Catch the case when the latest current branch version is the installed version (up to date site)
		if (empty($ret['current']['version']) && !empty($ret['installed']['version']))
		{
			$ret['current'] = $ret['installed'];
		}

		return $ret;
	}
}
language/index.html000060400000000340152455514240010324 0ustar00<!--
  ~ @package   FOF
  ~ @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
  ~ @license   GNU General Public License version 2, or later
  -->

<html><head><title></title></head><body></body></html>language/en-GB/en-GB.lib_fof30.ini000060400000007721152455514240012364 0ustar00; @package     FOF
; @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
; @license     GNU GPL version 2 or later

; Download helper
LIB_FOF_DOWNLOAD_ERR_COULDNOTDOWNLOADFROMURL="Could not download from %s"
LIB_FOF_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE="Local file %s is not writeable"
LIB_FOF_DOWNLOAD_ERR_CURL_ERROR="The download failed: cURL error %s: %s"
LIB_FOF_DOWNLOAD_ERR_FOPEN_ERROR="The download failed: fopen returned no header information"
LIB_FOF_DOWNLOAD_ERR_HTTPERROR="Unexpected HTTP status %s"

LIB_FOF_MODEL_ERR_GET_NAME="FOF: Model: Cannot get Model's name"
LIB_FOF_CONTROLLER_ERR_GET_NAME="FOF: Controller: Cannot get Controller's name"
LIB_FOF_VIEW_ERR_GET_NAME="FOF: Controller: Cannot get View's name"

LIB_FOF_TRANSPARENTAUTH_ERR_NOT_FOUND="FOF: TransparentAuthentication %s not found"
LIB_FOF_DISPATCHER_ERR_NOT_FOUND="FOF: Dispatcher %s not found"
LIB_FOF_TOOLBAR_ERR_NOT_FOUND="FOF: Toolbar %s not found"
LIB_FOF_CONTROLLER_ERR_NOT_FOUND="FOF: Controller %s not found"
LIB_FOF_MODEL_ERR_NOT_FOUND="FOF: Model %s not found"
LIB_FOF_VIEW_ERR_NOT_FOUND="FOF: View %s not found"

LIB_FOF_CONTROLLER_ERR_LOCKED="This item is already checked out by another user"

LIB_FOF_FORM_ERR_GETSTATIC_NOT_ALLOWED="FOF: %s cannot be used in single item display forms"
LIB_FOF_FORM_ERR_DATAMODEL_REQUIRED="FOF: %s requires a DataModel to act upon"
LIB_FOF_FORM_ERR_GETOPTIONS_INVALID_GROUP_CONTENTS="FOF: %s invalid group contents in getOptions"
LIB_FOF_FORM_ERR_GETINPUT_NOT_ALLOWED="FOF: %s cannot be used in input forms"

LIB_FOF_HAL_ERR_INVALIDLINK="A HAL link must always have a non-empty href"
LIB_FOF_HAL_ERR_INVALIDRENDERFORMAT="Unsupported HAL Document format &quot;%s&quot;. Render aborted."

LIB_FOF_MODEL_ERR_BIND="FOF: %s::bind() argument is %s, not an object or array"
LIB_FOF_MODEL_ERR_COULDNOTLOAD="Could not load record"
LIB_FOF_MODEL_ERR_NOITEMSFOUND="No items found in %s"
LIB_FOF_MODEL_ERR_CANNOTLOCKNOTLOADEDRECORD="Cannot lock a record which has not been loaded"
LIB_FOF_MODEL_ERR_NOASSETKEY="Table must have an asset key defined and a value for the table id in order to track assets"
LIB_FOF_MODEL_ERR_NOCONTENTTYPE="Content type for %s is not set."

LIB_FOF_MODEL_ERR_TREE_INCOMPATIBLETABLE="Database table %s is not compatible with TreeModel: it does not have lft/rgt columns"
LIB_FOF_MODEL_ERR_TREE_UNEXPECTEDPK="No primary key provided or deleting this record is not allowed"
LIB_FOF_MODEL_ERR_TREE_UNSUPPORTEDMETHOD="Method %s() is not supported by TreeModel"
LIB_FOF_MODEL_ERR_TREE_ONLYINROOT="Method %s() is only allowed for root nodes"
LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_PARENT="Invalid lft/rgt values in parent node"
LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_SIBLING="Invalid lft/rgt values in sibling node"
LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_OTHER="Invalid lft/rgt values in current node"
LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_CURRENT="Invalid lft/rgt values in other node"
LIB_FOF_MODEL_ERR_TREE_ROOTNOTFOUND="No root found for table %s, node lft=%s"

LIB_FOF_MODEL_ERR_FILTER_INVALIDFIELD="Invalid field object"
LIB_FOF_MODEL_ERR_FILTER_NODBOBJECT="Database object unspecified creating a %s filter"

LIB_FOF_TOOLBAR_ERR_MISSINGARGUMENT="The '%s' attribute is required for the '%s' button type"
LIB_FOF_TOOLBAR_ERR_UNKNOWNBUTTONTYPE="Unknown button type %s"

LIB_FOF_VIEW_MODELNOTINVIEW="The model %s does not exist in view %s"
LIB_FOF_VIEW_UNRECOGNISEDEXTENSION="FOF: Unrecognised extension in view template %s"
LIB_FOF_VIEW_POSSIBLYSUHOSIN="FOF: Could not write to your cache directory. Please make your cache directories (cache and administrator/cache under your site's root) writeable to PHP by changing the permissions. Alternatively, ask your host to make sure that they have not disabled the stream_wrapper_register() function in PHP. Moreover, if your host is using the Suhosin patch for PHP ask them to whitelist the fof:// stream wrapper in their server's php.ini file. If you do not understand what this means please contact your host and paste this entire message to them."
Cli/Application.php000060400000022272152455514240010237 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

// Do not put the JEXEC or die check on this file

/**
 * FOF-powered Joomla! CLI application implementation.
 *
 * Get all the power of Joomla in CLI without all the awkward decisions which make CLI scripts fail on many common,
 * commercial hosting environments. We've been doing that in our software before Joomla got CLI support. We know of all
 * the pitfalls and this little gem here will work around most of them (or at least fail gracefully).
 *
 * Your CLI script must begin with the following boilerplate code:
 *
 * // Boilerplate -- START
 * define('_JEXEC', 1);
 *
 * foreach ([__DIR__, getcwd()] as $curdir)
 * {
 *   if (file_exists($curdir . '/defines.php'))
 *     {
 *        define('JPATH_BASE', realpath($curdir . '/..'));
 *        require_once $curdir . '/defines.php';
 *
 *      break;
 *     }
 *
 *   if (file_exists($curdir . '/../includes/defines.php'))
 *     {
 *       define('JPATH_BASE', realpath($curdir . '/..'));
 *       require_once $curdir . '/../includes/defines.php';
 *
 *     break;
 *     }
 * }
 *
 * defined('JPATH_LIBRARIES') || die ('This script must be placed in or run from the cli folder of your site.');
 *
 * require_once JPATH_LIBRARIES . '/fof30/Cli/Application.php';
 * // Boilerplate -- END
 *
 * Create a class which extends FOFCliApplication and implements doExecute, e.g.
 *
 * // Class definition -- START
 * class YourClassName extends FOFCliApplication
 * {
 *   protected function doExecute()
 *   {
 *     // Do something useful
 *   }
 * }
 * // Class definition -- END
 *
 * Finally, execute your script with:
 *
 * // Execute script -- START
 * FOFCliApplication::getInstance('YourClassName')->execute();
 * // Execute script -- END
 *
 * You can optionally define $minphp before the boilerplate code to enforce a different minimum PHP version.
 */

// Abort immediately when this file is executed from a web SAPI
if (array_key_exists('REQUEST_METHOD', $_SERVER))
{
	die('This is a command line script. You are not allowed to access it over the web.');
}

// Work around some misconfigured servers which print out notices
if (function_exists('error_reporting'))
{
	$oldLevel = error_reporting(E_ERROR | E_NOTICE | E_DEPRECATED);
}

// Minimum PHP version check
if (!isset($minphp))
{
	$minphp = '5.6.0';
}

if (version_compare(PHP_VERSION, $minphp, 'lt'))
{
	require_once __DIR__ . '/wrong_php.php';

	die;
}

// Required by scripts written for old Joomla! versions.
defined('DS') || define('DS', DIRECTORY_SEPARATOR);

/**
 * Timezone fix
 *
 * This piece of code was originally put here because some PHP 5.3 servers forgot to declare a default timezone.
 * Unfortunately it's still required because some hosts STILL forget to provide a timezone in their php.ini files or,
 * worse, use invalid timezone names.
 */
if (function_exists('date_default_timezone_get') && function_exists('date_default_timezone_set'))
{
	$serverTimezone = @date_default_timezone_get();

	// Do I have no timezone set?
	if (empty($serverTimezone) || !is_string($serverTimezone))
	{
		$serverTimezone = 'UTC';
	}

	// Do I have an invalid timezone?
	try
	{
		$testTimeZone = new DateTimeZone($serverTimezone);
	}
	catch (\Exception $e)
	{
		$serverTimezone = 'UTC';
	}

	// Set the default timezone to a correct thing
	@date_default_timezone_set($serverTimezone);
}

// This is not necessary if you have used the boilerplate code.
if (!isset($curdir) && !defined('JPATH_ROOT'))
{
	foreach ([__DIR__ . '/../../../cli', getcwd()] as $curdir)
	{
		if (file_exists($curdir . '/defines.php'))
		{
			define('JPATH_BASE', realpath($curdir . '/..'));
			require_once $curdir . '/defines.php';

			break;
		}

		if (file_exists($curdir . '/../includes/defines.php'))
		{
			define('JPATH_BASE', realpath($curdir . '/..'));
			require_once $curdir . '/../includes/defines.php';

			break;
		}
	}

	defined('JPATH_LIBRARIES') || die ('This script must be placed in or run from the cli folder of your site.');
}

// Restore the error reporting before importing Joomla core code
if (function_exists('error_reporting'))
{
	error_reporting($oldLevel);
}

// Awkward Joomla version detection before we can actually load Joomla! itself
$joomlaMajorVersion = 3;
$joomlaMinorVersion = 0;
$jVersionFile       = JPATH_LIBRARIES . '/src/Version.php';

if ($versionFileContents = @file_get_contents($jVersionFile))
{
	preg_match("/MAJOR_VERSION\s*=\s*(\d*)\s*;/", $versionFileContents, $versionMatches);
	$joomlaMajorVersion = (int) $versionMatches[1];
	preg_match("/MINOR_VERSION\s*=\s*(\d*)\s*;/", $versionFileContents, $versionMatches);
	$joomlaMinorVersion = (int) $versionMatches[1];
}

// Load the Trait files
include_once __DIR__ . '/Traits/CGIModeAware.php';
include_once __DIR__ . '/Traits/CustomOptionsAware.php';
include_once __DIR__ . '/Traits/JoomlaConfigAware.php';
include_once __DIR__ . '/Traits/MemStatsAware.php';
include_once __DIR__ . '/Traits/MessageAware.php';
include_once __DIR__ . '/Traits/TimeAgoAware.php';

// The actual implementation of the CliApplication depends on the Joomla version we're running under
switch ($joomlaMajorVersion)
{
	case 3:
	default:
		require_once __DIR__ . '/Joomla3.php';

		abstract class FOFApplicationCLI extends FOFCliApplicationJoomla3
		{
		}

		;

		break;

	case 4:
		require_once __DIR__ . '/Joomla4.php';

		abstract class FOFApplicationCLI extends FOFCliApplicationJoomla4
		{
		}

		;

		break;
}

/**
 * A default exception handler. Catches all unhandled exceptions, displays debug information about them and sets the
 * error level to 254.
 *
 * @param   Throwable  $ex  The Exception / Error being handled
 */
function FOFCliExceptionHandler($ex)
{
	echo "\n\n";
	echo "********** ERROR! **********\n\n";
	echo $ex->getMessage();
	echo "\n\nTechnical information:\n\n";
	echo "Code: " . $ex->getCode() . "\n";
	echo "File: " . $ex->getFile() . "\n";
	echo "Line: " . $ex->getLine() . "\n";
	echo "\nStack Trace:\n\n" . $ex->getTraceAsString();
	echo "\n\n";
	exit(254);
}

/**
 * Timeout handler
 *
 * This function is registered as a shutdown script. If a catchable timeout occurs it will detect it and print a helpful
 * error message instead of just dying cold. The error level is set to 253 in this case.
 *
 * @return  void
 */
function FOFCliTimeoutHandler()
{
	$connection_status = connection_status();

	if ($connection_status == 0)
	{
		// Normal script termination, do not report an error.
		return;
	}

	echo "\n\n";
	echo "********** ERROR! **********\n\n";

	if ($connection_status == 1)
	{
		echo <<< END
The process was aborted on user's request.

This usually means that you pressed CTRL-C to terminate the script (if you're
running it from a terminal / SSH session), or that your host's CRON daemon
aborted the execution of this script.

If you are running this script through a CRON job and saw this message, please
contact your host and request an increase in the timeout limit for CRON jobs.
Moreover you need to ask them to increase the max_execution_time in the
php.ini file or, even better, set it to 0.
END;
	}
	else
	{
		echo <<< END
This script has timed out. As a result, the process has FAILED to complete.

Your host applies a maximum execution time for CRON jobs which is too low for
this script to work properly. Please contact your host and request an increase
in the timeout limit for CRON jobs. Moreover you need to ask them to increase
the max_execution_time in the php.ini file or, even better, set it to 0.
END;


		if (!function_exists('php_ini_loaded_file'))
		{
			echo "\n\n";

			return;
		}

		$ini_location = php_ini_loaded_file();

		echo <<<END
The php.ini file your host will need to modify is located at:
$ini_location
Info for the host: the location above is reported by PHP's php_ini_loaded_file() method.

END;

		echo "\n\n";
		exit(253);
	}
}

/**
 * Error handler. It tries to catch fatal errors and report them in a meaningful way. Obviously it only works for
 * catchable fatal errors. It sets the error level to 252.
 *
 * IMPORTANT! Under PHP 7 the default exception handler will be called instead, including when there is a non-catchable
 *            fatal error.
 *
 * @param   int     $errno    Error number
 * @param   string  $errstr   Error string, tells us what went wrong
 * @param   string  $errfile  Full path to file where the error occurred
 * @param   int     $errline  Line number where the error occurred
 *
 * @return  void
 */
function FOFCliErrorHandler($errno, $errstr, $errfile, $errline)
{
	switch ($errno)
	{
		case E_ERROR:
		case E_USER_ERROR:
			echo "\n\n";
			echo "********** ERROR! **********\n\n";
			echo "PHP Fatal Error: $errstr";
			echo "\n\nTechnical information:\n\n";
			echo "File: " . $errfile . "\n";
			echo "Line: " . $errline . "\n";
			echo "\nStack Trace:\n\n" . debug_backtrace();
			echo "\n\n";

			exit(252);
			break;

		default:
			break;
	}
}

/**
 * Custom default handlers for otherwise unhandled exceptions and PHP catchable errors.
 *
 * Moreover, we register a shutdown function to catch timeouts and SIGTERM signals, because some hosts *are* monsters.
 */
set_exception_handler('FOFCliExceptionHandler');
set_error_handler('FOFCliErrorHandler', E_ERROR | E_USER_ERROR);
register_shutdown_function('FOFCliTimeoutHandler');Cli/Traits/CGIModeAware.php000060400000003256152455514240011432 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Cli\Traits;

defined('_JEXEC') || die;

/**
 * CGI Mode detection and workaround
 *
 * Some hosts only give access to the PHP CGI binary, even for running CLI scripts. While problematic, it mostly works.
 * This trait detects PHP-CGI and manipulates $_GET in such a way that we populate the $argv and $argc global variables
 * in the same way that PHP-CLI would set them. This allows the CLI input object to work. Moreover, we unset the PHP
 * execution time limit, if possible, to prevent accidental timeouts.
 *
 * @package FOF30\Cli\Traits
 */
trait CGIModeAware
{
	/**
	 * Detect if we are running under CGI mode. In this case it populates the global $argv and $argc parameters off the
	 * CGI input ($_GET superglobal).
	 */
	private function detectAndWorkAroundCGIMode()
	{
		// This code only executes when running under CGI. So let's detect it first.
		$cgiMode = (!defined('STDOUT') || !defined('STDIN') || !isset($_SERVER['argv']));

		if (!$cgiMode)
		{
			return;
		}

		// CGI mode has a time limit. Unset it to prevent timeouts.
		if (function_exists('set_time_limit'))
		{
			set_time_limit(0);
		}

		// Convert $_GET into the appropriate $argv representation. This allows Input\Cli to work under PHP-CGI.
		$query = "";

		if (!empty($_GET))
		{
			foreach ($_GET as $k => $v)
			{
				$query .= " $k";
				if ($v != "")
				{
					$query .= "=$v";
				}
			}
		}

		$query = ltrim($query);

		global $argv, $argc;

		$argv = explode(' ', $query);
		$argc = count($argv);

		$_SERVER['argv'] = $argv;
	}

}
Cli/Traits/MemStatsAware.php000060400000002640152455514240011754 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Cli\Traits;

defined('_JEXEC') || die;

/**
 * Memory statistics
 *
 * This is an optional trait which allows the developer to print memory usage statistics and format byte sizes into
 * human-readable strings.
 *
 * @package FOF30\Cli\Traits
 */
trait MemStatsAware
{
	/**
	 * Formats a number of bytes in human readable format
	 *
	 * @param   int  $size  The size in bytes to format, e.g. 8254862
	 *
	 * @return  string  The human-readable representation of the byte size, e.g. "7.87 Mb"
	 */
	protected function formatByteSize($size)
	{
		$unit = ['b', 'KB', 'MB', 'GB', 'TB', 'PB'];

		return @round($size / 1024 ** ($i = floor(log($size, 1024))), 2) . ' ' . $unit[$i];
	}

	/**
	 * Returns the current memory usage, formatted
	 *
	 * @return  string
	 */
	protected function memUsage()
	{
		if (function_exists('memory_get_usage'))
		{
			$size = memory_get_usage();

			return $this->formatByteSize($size);
		}
		else
		{
			return "(unknown)";
		}
	}

	/**
	 * Returns the peak memory usage, formatted
	 *
	 * @return  string
	 */
	protected function peakMemUsage()
	{
		if (function_exists('memory_get_peak_usage'))
		{
			$size = memory_get_peak_usage();

			return $this->formatByteSize($size);
		}
		else
		{
			return "(unknown)";
		}
	}
}
Cli/Traits/TimeAgoAware.php000060400000004424152455514240011546 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Cli\Traits;

defined('_JEXEC') || die;

/**
 * Allows the developer to show the relative time difference between two timestamps.
 *
 * @package FOF30\Cli\Traits
 */
trait TimeAgoAware
{
	/**
	 * Returns the relative time difference between two timestamps in a human readable format
	 *
	 * @param   int       $referenceTimestamp  Timestamp of the reference date/time
	 * @param   int|null  $currentTimestamp    Timestamp of the current date/time. Null for time().
	 * @param   string    $timeUnit            Time unit. One of s, m, h, d, or y.
	 * @param   bool      $autoSuffix          Add "ago" / "from now" suffix?
	 *
	 * @return  string  For example, "10 seconds ago"
	 */
	protected function timeAgo($referenceTimestamp = 0, $currentTimestamp = null, $timeUnit = '', $autoSuffix = true)
	{
		if (is_null($currentTimestamp))
		{
			$currentTimestamp = time();
		}

		// Raw time difference
		$raw   = $currentTimestamp - $referenceTimestamp;
		$clean = abs($raw);

		$calcNum = [
			['s', 60],
			['m', 60 * 60],
			['h', 60 * 60 * 60],
			['d', 60 * 60 * 60 * 24],
			['y', 60 * 60 * 60 * 24 * 365],
		];

		$calc = [
			's' => [1, 'second'],
			'm' => [60, 'minute'],
			'h' => [60 * 60, 'hour'],
			'd' => [60 * 60 * 24, 'day'],
			'y' => [60 * 60 * 24 * 365, 'year'],
		];

		$effectiveTimeUnit = $timeUnit;

		if ($timeUnit == '')
		{
			$effectiveTimeUnit = 's';

			for ($i = 0; $i < count($calcNum); $i++)
			{
				if ($clean <= $calcNum[$i][1])
				{
					$effectiveTimeUnit = $calcNum[$i][0];
					$i                 = count($calcNum);
				}
			}
		}

		$timeDifference = floor($clean / $calc[$effectiveTimeUnit][0]);
		$textSuffix     = '';

		if ($autoSuffix == true && ($currentTimestamp == time()))
		{
			if ($raw < 0)
			{
				$textSuffix = ' from now';
			}
			else
			{
				$textSuffix = ' ago';
			}
		}

		if ($referenceTimestamp != 0)
		{
			if ($timeDifference == 1)
			{
				return $timeDifference . ' ' . $calc[$effectiveTimeUnit][1] . ' ' . $textSuffix;
			}

			return $timeDifference . ' ' . $calc[$effectiveTimeUnit][1] . 's ' . $textSuffix;
		}

		return '(no reference timestamp was provided).';
	}

}
Cli/Traits/JoomlaConfigAware.php000060400000002577152455514240012577 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Cli\Traits;

defined('_JEXEC') || die;

/**
 * Allows the CLI application to use the Joomla Global Configuration parameters as its own configuration.
 *
 * @package FOF30\Cli\Traits
 */
trait JoomlaConfigAware
{
	/**
	 * Method to load the application configuration, returning it as an object or array
	 *
	 * This can be overridden in subclasses if you don't want to fetch config from a PHP class file.
	 *
	 * @param   string|null  $file       The filepath to the file containing the configuration class. Default: Joomla's
	 *                                   configuration.php
	 * @param   string       $className  The name of the PHP class holding the configuration. Default: JConfig
	 *
	 * @return  mixed  Either an array or object to be loaded into the configuration object.
	 */
	protected function fetchConfigurationData($file = null, $className = 'JConfig')
	{
		// Set the configuration file name.
		if (empty($file))
		{
			$file = JPATH_BASE . '/configuration.php';
		}

		// Import the configuration file.
		if (!is_file($file))
		{
			return [];
		}

		include_once $file;

		// Instantiate the configuration object.
		if (!class_exists('JConfig'))
		{
			return [];
		}

		return new $className();
	}

}
Cli/Traits/CustomOptionsAware.php000060400000010004152455514240013036 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Cli\Traits;

defined('_JEXEC') || die;

use Joomla\CMS\Filter\InputFilter;

/**
 * Implements a simpler, more straightforward options parser than the Joomla CLI input object. It supports short options
 * when the Joomla CLI input object doesn't. Eventually this will go away and we can use something like Symfony Console
 * instead.
 *
 * @package FOF30\Cli\Traits
 */
trait CustomOptionsAware
{
	/**
	 * POSIX-style CLI options. Access them with through the getOption method.
	 *
	 * @var   array
	 */
	protected static $cliOptions = [];

	/**
	 * Filter object to use for custom options parsing.
	 *
	 * @var    InputFilter
	 */
	protected $filter = null;

	/**
	 * Initializes the custom CLI options parsing
	 *
	 * @return void
	 */
	protected function initialiseCustomOptions()
	{
		// Create a new InputFilter
		$this->filter = InputFilter::getInstance();

		// Parse the POSIX options
		$this->parseOptions();
	}

	/**
	 * Parses POSIX command line options and sets the self::$cliOptions associative array. Each array item contains
	 * a single dimensional array of values. Arguments without a dash are silently ignored.
	 *
	 * This works much better than JInputCli since it allows you to use all valid POSIX ways of defining CLI parameters.
	 *
	 * @return  void
	 */
	protected function parseOptions()
	{
		global $argc, $argv;

		// Workaround for PHP-CGI
		if (!isset($argc) && !isset($argv))
		{
			$query = "";

			if (!empty($_GET))
			{
				foreach ($_GET as $k => $v)
				{
					$query .= " $k";

					if ($v != "")
					{
						$query .= "=$v";
					}
				}
			}

			$query = ltrim($query);
			$argv  = explode(' ', $query);
			$argc  = count($argv);
		}

		$currentName = "";
		$options     = [];

		for ($i = 1; $i < $argc; $i++)
		{
			$argument = $argv[$i];

			$value = $argument;

			if (strpos($argument, "-") === 0)
			{
				$argument = ltrim($argument, '-');

				$name  = $argument;
				$value = null;

				if (strstr($argument, '='))
				{
					[$name, $value] = explode('=', $argument, 2);
				}

				$currentName = $name;

				if (!isset($options[$currentName]) || ($options[$currentName] == null))
				{
					$options[$currentName] = [];
				}
			}

			if ((!is_null($value)) && (!is_null($currentName)))
			{
				$key = null;

				if (strstr($value, '='))
				{
					$parts = explode('=', $value, 2);
					$key   = $parts[0];
					$value = $parts[1];
				}

				$values = $options[$currentName];

				if (is_null($values))
				{
					$values = [];
				}

				if (is_null($key))
				{
					array_push($values, $value);
				}
				else
				{
					$values[$key] = $value;
				}

				$options[$currentName] = $values;
			}
		}

		self::$cliOptions = $options;
	}

	/**
	 * Returns the value of a command line option. This does NOT use JInputCLI. You MUST run parseOptions before.
	 *
	 * @param   string  $key      The full name of the option, e.g. "foobar"
	 * @param   mixed   $default  The default value to return
	 * @param   string  $type     Joomla! filter type, e.g. cmd, int, bool and so on.
	 *
	 * @return  mixed  The value of the option
	 */
	protected function getOption($key, $default = null, $type = 'raw')
	{
		// If the key doesn't exist set it to the default value
		if (!array_key_exists($key, self::$cliOptions))
		{
			self::$cliOptions[$key] = is_array($default) ? $default : [$default];
		}

		$type = strtolower($type);

		if ($type == 'array')
		{
			return self::$cliOptions[$key];
		}

		$value = null;

		if (!empty(self::$cliOptions[$key]))
		{
			$value = self::$cliOptions[$key][0];
		}

		return $this->filterVariable($value, $type);
	}

	/**
	 * Filter a variable using JInputFilter
	 *
	 * @param   mixed   $var   The variable to filter
	 * @param   string  $type  The filter type, default 'cmd'
	 *
	 * @return  mixed  The filtered value
	 */
	protected function filterVariable($var, $type = 'cmd')
	{
		return $this->filter->clean($var, $type);
	}

}
Cli/Traits/MessageAware.php000060400000002331152455514240011600 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Cli\Traits;

defined('_JEXEC') || die;

/**
 * Sometimes other extensions will try to enqueue messages to the application. Methods for those tasks only exists in
 * web applications, so we have to replicate their behavior in CLI environment or fatal errors will occur
 *
 * @package FOF30\Cli\Traits
 */
trait MessageAware
{
	/** @var array Queue holding all messages */
	protected $messageQueue = [];

	/**
	 * @param $msg
	 * @param $type
	 *
	 * @return null
	 */
	public function enqueueMessage($msg, $type)
	{
		// Don't add empty messages.
		if (trim($msg) === '')
		{
			return;
		}

		$message = ['message' => $msg, 'type' => strtolower($type)];

		if (!in_array($message, $this->messageQueue))
		{
			// Enqueue the message.
			$this->messageQueue[] = $message;
		}
	}

	/**
	 * Loosely based on Joomla getMessageQueue
	 *
	 * @param bool $clear
	 *
	 * @return array
	 */
	public function getMessageQueue($clear = false)
	{
		$messageQueue = $this->messageQueue;

		if ($clear)
		{
			$this->messageQueue = [];
		}

		return $messageQueue;
	}
}
Cli/wrong_php.php000060400000005353152455514240010000 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

/** @var string $minphp */
?>

================================================================================
WARNING! Incompatible PHP version <?php echo PHP_VERSION ?> (required: <?php echo $minphp ?> or later)
================================================================================

This script must be run using PHP version <?php echo $minphp ?> or later. Your server is
currently using a much older version which would cause this script to crash. As
a result we have aborted execution of the script. Please contact your host and
ask them for the correct path to the PHP CLI binary for PHP <?php echo $minphp ?> or later, then
edit your CRON job and replace your current path to PHP with the one your host
gave you.

For your information, the current PHP version information is as follows.

PATH:    <?php echo PHP_BINDIR ?>
VERSION: <?php echo PHP_VERSION ?>

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
IMPORTANT!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PHP version numbers are NOT decimals! Trailing zeros do matter. For example,
PHP 5.3.28 is twenty four versions newer (greater than) than PHP 5.3.4.
Please consult https://www.akeeba.com/how-do-version-numbers-work.html


Further clarifications:

1. There is no possible way that you are receiving this message in error. We
are using the PHP_VERSION constant to detect the PHP version you are
currently using. This is what PHP itself reports as its own version. It
simply cannot lie.

2. Even though your *site* may be running in a higher PHP version that the one
reported above, your CRON scripts will most likely not be running under it.
This has to do with the fact that your site DOES NOT run under the command
line and there are different executable files (binaries) for the web and
command line versions of PHP.

3. Please note that we cannot provide support about this error as the solution
depends only on your server setup. The only people who know how your server
is set up are your host's technicians. Therefore we can only advise you to
contact your host and request them the correct path to the PHP CLI binary.
Let us stress out that only your host knows and can give this information
to you.

4. The latest published versions of PHP can be found at http://www.php.net/
Any older version is considered insecure and must not be used on a
production site. If your server uses a much older version of PHP than those
published in the URL above please notify your host that their servers are
insecure and in need of an update.

This script will now terminate. Goodbye.

Cli/Joomla4.php000060400000013546152455514240007305 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

// Do not put the JEXEC or die check on this file

use FOF30\Cli\Traits\CGIModeAware;
use FOF30\Cli\Traits\CustomOptionsAware;
use FOF30\Cli\Traits\JoomlaConfigAware;
use FOF30\Cli\Traits\MemStatsAware;
use FOF30\Cli\Traits\TimeAgoAware;
use Joomla\CMS\Application\CliApplication;
use Joomla\CMS\Application\ExtensionNamespaceMapper;
use Joomla\CMS\Factory;
use Joomla\Event\Dispatcher;
use Joomla\Registry\Registry;
use Joomla\Session\SessionInterface;

/**
 * Load the legacy Joomla! include files
 *
 * Despite Joomla complaining about it with an E_DEPRECATED notice, if you use bootstrap.php instead of
 * import.legacy.php you get an HTML error page (yes, under CLI!) which is kinda daft.
 */
if (function_exists('error_reporting'))
{
	$oldErrorReporting = @error_reporting(E_ERROR | E_NOTICE | E_DEPRECATED);
}

include_once JPATH_LIBRARIES . '/import.legacy.php';

if (function_exists('error_reporting'))
{
	@error_reporting($oldErrorReporting);
}

// Load the Framework (J4 beta 1 and later) or CMS import file (J4 a12 and lower)
$cmsImportFilePath = JPATH_BASE . '/includes/framework.php';
$cmsImportFilePathOld = JPATH_LIBRARIES . '/cms.php';

if (@file_exists($cmsImportFilePath))
{
	@include_once $cmsImportFilePath;

	// Boot the DI container
	$container = \Joomla\CMS\Factory::getContainer();

	/*
	 * Alias the session service keys to the CLI session service as that is the primary session backend for this application
	 *
	 * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects
	 * is supported.  This includes aliases for aliased class names, and the keys for aliased class names should be considered
	 * deprecated to be removed when the class name alias is removed as well.
	 */
	$container->alias('session', 'session.cli')
		->alias('JSession', 'session.cli')
		->alias(\Joomla\CMS\Session\Session::class, 'session.cli')
		->alias(\Joomla\Session\Session::class, 'session.cli')
		->alias(\Joomla\Session\SessionInterface::class, 'session.cli');
}
elseif (@file_exists($cmsImportFilePathOld))
{
	@include_once $cmsImportFilePathOld;
}

/**
 * Base class for a Joomla! command line application. Adapted from JCli / JApplicationCli
 */
abstract class FOFCliApplicationJoomla4 extends CliApplication
{
	use ExtensionNamespaceMapper;

	use CGIModeAware, CustomOptionsAware, JoomlaConfigAware, MemStatsAware, TimeAgoAware;

	private $allowedToClose = false;

	public static function getInstance($name = null)
	{
		$instance = parent::getInstance($name);

		Factory::$application = $instance;

		/**
		 * Load FOF.
		 *
		 * In Joomla 4 this must happen after we have set up the application in the factory because Factory::getLanguage
		 * goes through the application object to retrieve the configuration.
		 */
		if (!defined('FOF30_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof30/include.php'))
		{
			throw new RuntimeException('Cannot load FOF', 500);
		}

		return $instance;
	}

	public function __construct(\Joomla\Input\Input $input = null, Registry $config = null, \Joomla\CMS\Application\CLI\CliOutput $output = null, \Joomla\CMS\Application\CLI\CliInput $cliInput = null, \Joomla\Event\DispatcherInterface $dispatcher = null, \Joomla\DI\Container $container = null)
	{
		// Some servers only provide a CGI executable. While not ideal for running CLI applications we can make do.
		$this->detectAndWorkAroundCGIMode();

		// We need to tell Joomla to register its default namespace conventions
		$this->createExtensionNamespaceMap();

		// Initialize custom options handling which is a bit more straightforward than Input\Cli.
		$this->initialiseCustomOptions();

		// Default configuration: Joomla Global Configuration
		if (empty($config))
		{
			$config = new Registry($this->fetchConfigurationData());
		}

		if (empty($dispatcher))
		{
			$dispatcher = new Dispatcher();
		}

		parent::__construct($input, $config, $output, $cliInput, $dispatcher, $container);

		/**
		 * Allow the application to close.
		 *
		 * This is required to allow CliApplication to execute under CGI mode. The checks performed in the parent
		 * constructor will call close() if the application does not run pure CLI mode. However, some hosts only provide
		 * the PHP CGI binary for executing CLI scripts. While wrong it will work in most cases. By default close() will
		 * do nothing, thereby allowing the parent constructor to call it without a problem. Finally, we set this flag
		 * to true to allow doExecute() to call close() and actually close the application properly. Yeehaw!
		 */
		$this->allowedToClose = true;
	}

	/**
	 * Method to close the application.
	 *
	 * See the constructor for details on why it works the way it works.
	 *
	 * @param   integer  $code  The exit code (optional; default is 0).
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 * @since   1.0
	 */
	public function close($code = 0)
	{
		// See the constructor for details
		if (!$this->allowedToClose)
		{
			return;
		}

		exit($code);
	}

	/**
	 * Gets the name of the current running application.
	 *
	 * @return  string  The name of the application.
	 *
	 * @since   4.0.0
	 */
	public function getName()
	{
		return get_class($this);
	}

	/**
	 * Get the menu object.
	 *
	 * @param   string  $name     The application name for the menu
	 * @param   array   $options  An array of options to initialise the menu with
	 *
	 * @return  \Joomla\CMS\Menu\AbstractMenu|null  A AbstractMenu object or null if not set.
	 *
	 * @since   4.0.0
	 */
	public function getMenu($name = null, $options = [])
	{
		return null;
	}

	/**
	 * Method to get the application session object.
	 *
	 * @return  SessionInterface  The session object
	 *
	 * @since   4.0.0
	 */
	public function getSession()
	{
		return $this->getContainer()->get('session.cli');
	}

}
Cli/Joomla3.php000060400000007471152455514240007304 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

// Do not put the JEXEC or die check on this file

use FOF30\Cli\Traits\CGIModeAware;
use FOF30\Cli\Traits\CustomOptionsAware;
use FOF30\Cli\Traits\JoomlaConfigAware;
use FOF30\Cli\Traits\MemStatsAware;
use FOF30\Cli\Traits\MessageAware;
use FOF30\Cli\Traits\TimeAgoAware;
use FOF30\Utils\CliSessionHandler;
use Joomla\CMS\Application\CliApplication;
use Joomla\CMS\Input\Cli;

// Load the legacy Joomla! include files (Joomla! 3 only)
include_once JPATH_LIBRARIES . '/import.legacy.php';

// Load the CMS import file if it exists (newer Joomla! 3 versions and Joomla! 4)
$cmsImportFilePath = JPATH_LIBRARIES . '/cms.php';

if (@file_exists($cmsImportFilePath))
{
	@include_once $cmsImportFilePath;
}

/**
 * Base class for a Joomla! command line application. Adapted from JCli / JApplicationCli
 */
abstract class FOFCliApplicationJoomla3 extends CliApplication
{
	use CGIModeAware, CustomOptionsAware, JoomlaConfigAware, MemStatsAware, TimeAgoAware, MessageAware;

	private $allowedToClose = false;

	public static function getInstance($name = null)
	{
		// Load the Joomla global configuration in JFactory. This must happen BEFORE loading FOF.
		JFactory::getConfig(JPATH_CONFIGURATION . '/configuration.php');

		// Load FOF
		if (!defined('FOF30_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof30/include.php'))
		{
			throw new RuntimeException('Cannot load FOF', 500);
		}

		// Create a CLI-specific session
		JFactory::$session = JSession::getInstance('none', [
			'expire' => 84400,
		], new CliSessionHandler());

		$instance = parent::getInstance($name);

		JFactory::$application = $instance;

		return $instance;
	}

	public function __construct(Cli $input = null, \Joomla\Registry\Registry $config = null, \JEventDispatcher $dispatcher = null)
	{
		// Some servers only provide a CGI executable. While not ideal for running CLI applications we can make do.
		$this->detectAndWorkAroundCGIMode();

		// Initialize custom options handling which is a bit more straightforward than Input\Cli.
		$this->initialiseCustomOptions();

		parent::__construct($input, $config, $dispatcher);

		/**
		 * Allow the application to close.
		 *
		 * This is required to allow CliApplication to execute under CGI mode. The checks performed in the parent
		 * constructor will call close() if the application does not run pure CLI mode. However, some hosts only provide
		 * the PHP CGI binary for executing CLI scripts. While wrong it will work in most cases. By default close() will
		 * do nothing, thereby allowing the parent constructor to call it without a problem. Finally, we set this flag
		 * to true to allow doExecute() to call close() and actually close the application properly. Yeehaw!
		 */
		$this->allowedToClose = true;
	}

	/**
	 * Method to close the application.
	 *
	 * See the constructor for details on why it works the way it works.
	 *
	 * @param   integer  $code  The exit code (optional; default is 0).
	 *
	 * @return  void
	 *
	 * @codeCoverageIgnore
	 * @since   1.0
	 */
	public function close($code = 0)
	{
		// See the constructor for details
		if (!$this->allowedToClose)
		{
			return;
		}

		exit($code);
	}

	/**
	 * Gets the name of the current running application.
	 *
	 * @return  string  The name of the application.
	 *
	 * @since   4.0.0
	 */
	public function getName()
	{
		return get_class($this);
	}

	/**
	 * Get the menu object.
	 *
	 * @param   string  $name     The application name for the menu
	 * @param   array   $options  An array of options to initialise the menu with
	 *
	 * @return  \Joomla\CMS\Menu\AbstractMenu|null  A AbstractMenu object or null if not set.
	 *
	 * @since   4.0.0
	 */
	public function getMenu($name = null, $options = [])
	{
		return null;
	}
}
Dispatcher/Exception/AccessForbidden.php000060400000001313152455514240014320 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Dispatcher\Exception;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use RuntimeException;

/**
 * Exception thrown when the access to the requested resource is forbidden under the current execution context
 */
class AccessForbidden extends RuntimeException
{
	public function __construct($message = "", $code = 403, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN');
		}

		parent::__construct($message, $code, $previous);
	}

}
Dispatcher/Dispatcher.php000060400000023013152455514240011433 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Dispatcher;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use FOF30\Controller\Controller;
use FOF30\Dispatcher\Exception\AccessForbidden;
use FOF30\TransparentAuthentication\TransparentAuthentication;

/**
 * A generic MVC dispatcher
 *
 * @property-read  \FOF30\Input\Input $input  The input object (magic __get returns the Input from the Container)
 */
class Dispatcher
{
	/** @var   string  The name of the default view, in case none is specified */
	public $defaultView = null;

	/** @var  array  Local cache of the dispatcher configuration */
	protected $config = [];

	/** @var  Container  The container we belong to */
	protected $container = null;

	/** @var  string  The view which will be rendered by the dispatcher */
	protected $view = null;

	/** @var  string  The layout for rendering the view */
	protected $layout = null;

	/** @var  Controller  The controller which will be used */
	protected $controller = null;

	/** @var  bool  Is this user transparently logged in? */
	protected $isTransparentlyLoggedIn = false;

	/**
	 * Public constructor
	 *
	 * The $config array can contain the following optional values:
	 * defaultView  string  The view to render if none is specified in $input
	 *
	 * Do note that $config is passed to the Controller and through it to the Model and View. Please see these classes
	 * for more information on the configuration variables they accept.
	 *
	 * @param   \FOF30\Container\Container  $container
	 * @param   array                       $config
	 */
	public function __construct(Container $container, array $config = [])
	{
		$this->container = $container;

		$this->config = $config;

		$this->defaultView = $container->appConfig->get('dispatcher.defaultView', $this->defaultView);

		if (isset($config['defaultView']))
		{
			$this->defaultView = $config['defaultView'];
		}

		$this->supportCustomViewAndTaskParameters();

		// Get the default values for the view and layout names
		$this->view   = $this->input->getCmd('view', null);
		$this->layout = $this->input->getCmd('layout', null);

		// Not redundant; you may pass an empty but non-null view which is invalid, so we need the fallback
		if (empty($this->view))
		{
			$this->view = $this->defaultView;
			$this->container->input->set('view', $this->view);
		}
	}

	/**
	 * Magic get method. Handles magic properties:
	 * $this->input  mapped to $this->container->input
	 *
	 * @param   string  $name  The property to fetch
	 *
	 * @return  mixed|null
	 */
	public function __get($name)
	{
		// Handle $this->input
		if ($name == 'input')
		{
			return $this->container->input;
		}

		// Property not found; raise error
		$trace = debug_backtrace();
		trigger_error(
			'Undefined property via __get(): ' . $name .
			' in ' . $trace[0]['file'] .
			' on line ' . $trace[0]['line'],
			E_USER_NOTICE);

		return null;
	}

	/**
	 * The main code of the Dispatcher. It spawns the necessary controller and
	 * runs it.
	 *
	 * @return  void
	 *
	 * @throws  AccessForbidden  When the access is forbidden
	 */
	public function dispatch()
	{
		// Load the translations for this component;
		$this->container->platform->loadTranslations($this->container->componentName);

		// Perform transparent authentication
		if ($this->container->platform->getUser()->guest)
		{
			$this->transparentAuthenticationLogin();
		}

		// Get the event names (different for CLI)
		$onBeforeEventName = 'onBeforeDispatch';
		$onAfterEventName  = 'onAfterDispatch';

		if ($this->container->platform->isCli())
		{
			$onBeforeEventName = 'onBeforeDispatchCLI';
			$onAfterEventName  = 'onAfterDispatchCLI';
		}

		try
		{
			$result = $this->triggerEvent($onBeforeEventName);
			$error  = '';
		}
		catch (\Exception $e)
		{
			$result = false;
			$error  = $e->getMessage();
		}

		if ($result === false)
		{
			if ($this->container->platform->isCli())
			{
				$this->container->platform->setHeader('Status', '403 Forbidden', true);
			}

			$this->transparentAuthenticationLogout();

			$this->container->platform->showErrorPage(new AccessForbidden);
		}

		// Get and execute the controller
		$view = $this->input->getCmd('view', $this->defaultView);
		$task = $this->input->getCmd('task', 'default');

		if (empty($task))
		{
			$task = 'default';
			$this->input->set('task', $task);
		}

		try
		{
			$this->controller = $this->container->factory->controller($view, $this->config);
			$status           = $this->controller->execute($task);
		}
		catch (Exception $e)
		{
			$this->container->platform->showErrorPage($e);

			// Redundant; just to make code sniffers happy
			return;
		}

		if ($status !== false)
		{
			try
			{
				$this->triggerEvent($onAfterEventName);
			}
			catch (\Exception $e)
			{
				$status = false;
			}
		}

		if (($status === false))
		{
			if ($this->container->platform->isCli())
			{
				$this->container->platform->setHeader('Status', '403 Forbidden', true);
			}

			$this->transparentAuthenticationLogout();

			$this->container->platform->showErrorPage(new AccessForbidden);
		}

		$this->transparentAuthenticationLogout();

		$this->controller->redirect();
	}

	/**
	 * Returns a reference to the Controller object currently in use by the dispatcher
	 *
	 * @return Controller
	 */
	public function &getController()
	{
		return $this->controller;
	}

	/**
	 * Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
	 * Joomla! plugin system. A true/false return value is expected. The first false return cancels the event.
	 *
	 * EXAMPLE
	 * Component: com_foobar, Object name: item, Event: onBeforeDispatch, Arguments: array(123, 456)
	 * The event calls:
	 * 1. $this->onBeforeDispatch(123, 456)
	 * 2. Joomla! plugin event onComFoobarDispatcherBeforeDispatch($this, 123, 456)
	 *
	 * @param   string  $event      The name of the event, typically named onPredicateVerb e.g. onBeforeKick
	 * @param   array   $arguments  The arguments to pass to the event handlers
	 *
	 * @return  bool
	 */
	protected function triggerEvent($event, array $arguments = [])
	{
		$result = true;

		// If there is an object method for this event, call it
		if (method_exists($this, $event))
		{
			switch (count($arguments))
			{
				case 0:
					$result = $this->{$event}();
					break;
				case 1:
					$result = $this->{$event}($arguments[0]);
					break;
				case 2:
					$result = $this->{$event}($arguments[0], $arguments[1]);
					break;
				case 3:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2]);
					break;
				case 4:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
					break;
				case 5:
					$result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
					break;
				default:
					$result = call_user_func_array([$this, $event], $arguments);
					break;
			}
		}

		if ($result === false)
		{
			return false;
		}

		// All other event handlers live outside this object, therefore they need to be passed a reference to this
		// objects as the first argument.
		array_unshift($arguments, $this);

		// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
		$prefix = '';

		if (substr($event, 0, 2) == 'on')
		{
			$prefix = 'on';
			$event  = substr($event, 2);
		}

		// Get the component/model prefix for the event
		$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Dispatcher';

		// The event name will be something like onComFoobarItemsBeforeSomething
		$event = $prefix . $event;

		// Call the Joomla! plugins
		$results = $this->container->platform->runPlugins($event, $arguments);

		if (!empty($results))
		{
			foreach ($results as $result)
			{
				if ($result === false)
				{
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Handles the transparent authentication log in
	 */
	protected function transparentAuthenticationLogin()
	{
		/** @var TransparentAuthentication $transparentAuth */
		$transparentAuth = $this->container->transparentAuth;
		$authInfo        = $transparentAuth->getTransparentAuthenticationCredentials();

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

		$this->isTransparentlyLoggedIn = $this->container->platform->loginUser($authInfo);
	}

	/**
	 * Handles the transparent authentication log out
	 */
	protected function transparentAuthenticationLogout()
	{
		if (!$this->isTransparentlyLoggedIn)
		{
			return;
		}

		/** @var TransparentAuthentication $transparentAuth */
		$transparentAuth = $this->container->transparentAuth;

		if (!$transparentAuth->getLogoutOnExit())
		{
			return;
		}

		$this->container->platform->logoutUser();
	}

	/**
	 * Adds support for akview/aktask in lieu of view and task.
	 *
	 * This is for future-proofing FOF in case Joomla assigns special meaning to view and task, e.g. by trying to find a
	 * specific controller / task class instead of letting the component's front-end router handle it. If that happens
	 * FOF components can have a single Joomla-compatible view/task which launches the Dispatcher and perform internal
	 * routing using akview/aktask.
	 *
	 * @return  void
	 * @since   3.6.3
	 */
	private function supportCustomViewAndTaskParameters()
	{
		$view = $this->input->getCmd('akview', null);
		$task = $this->input->getCmd('aktask', null);

		if (!is_null($view))
		{
			$this->input->remove('akview');
			$this->input->set('view', $view);
		}

		if (!is_null($task))
		{
			$this->input->remove('aktask');
			$this->input->set('task', $task);
		}
	}
}
Dispatcher/Mixin/ViewAliases.php000060400000005115152455514240012650 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Dispatcher\Mixin;

defined('_JEXEC') || die;

use Joomla\CMS\Uri\Uri;

/**
 * Lets you create view aliases. When you access a view alias the real view is loaded instead. You can optionally have
 * an HTTPS 301 redirection for GET requests to URLs that use the view name alias.
 *
 * IMPORTANT: This is a mixin (or, as we call it in PHP, a trait). Traits require PHP 5.4 or later. If you opt to use
 * this trait your component will no longer work under PHP 5.3.
 *
 * Usage:
 *
 * • Override $viewNameAliases with your view names map.
 * • If you want to issue HTTP 301 for GET requests set $permanentAliasRedirectionOnGET to true.
 * • If you have an onBeforeDispatch method remember to alias and call this traits' onBeforeDispatch method at the top.
 *
 * Regarding the last point, if you've never used traits before, the code looks like this. Top of the class:
 *   use ViewAliases {
 *       onBeforeDispatch as onBeforeDispatchViewAliases;
 *   }
 * and inside your custom onBeforeDispatch method, the first statement should be:
 *   $this->onBeforeDispatchViewAliases();
 * Simple!
 */
trait ViewAliases
{
	/**
	 * Maps view name aliases to actual views. The format is 'alias' => 'RealView'.
	 *
	 * @var  array
	 */
	protected $viewNameAliases = [];

	/**
	 * If set to true, any GET request to the alias view will result in an HTTP 301 permanent redirection to the real
	 * view name.
	 *
	 * This does NOT apply to POST, PUT, DELETE etc URLs. When you submit form data you cannot have a redirection. The
	 * browser will _typically_ not resend the submitted data.
	 *
	 * @var  bool
	 */
	protected $permanentAliasRedirectionOnGET = false;

	/**
	 * Transparently replaces old view names with their counterparts.
	 *
	 * If you are overriding this method in your component remember to alias it and call it from your overridden method.
	 */
	protected function onBeforeDispatch()
	{
		if (!array_key_exists($this->view, $this->viewNameAliases))
		{
			return;
		}

		$this->view = $this->viewNameAliases[$this->view];
		$this->container->input->set('view', $this->view);

		// Perform HTTP 301 Moved permanently redirection on GET requests if requested to do so
		if ($this->permanentAliasRedirectionOnGET && isset($_SERVER['REQUEST_METHOD'])
			&& (strtoupper($_SERVER['REQUEST_METHOD']) == 'GET')
		)
		{
			$url = Uri::getInstance();
			$url->setVar('view', $this->view);

			$this->container->platform->redirect($url, 301);
		}
	}
}
Event/Observable.php000060400000001712152455514240010426 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Event;

defined('_JEXEC') || die;

/**
 * Interface Observable
 *
 * @codeCoverageIgnore
 */
interface Observable
{
	/**
	 * Attaches an observer to the object
	 *
	 * @param   Observer  $observer  The observer to attach
	 *
	 * @return  Observable  Ourselves, for chaining
	 */
	public function attach(Observer $observer);

	/**
	 * Detaches an observer from the object
	 *
	 * @param   Observer  $observer  The observer to detach
	 *
	 * @return  Observable  Ourselves, for chaining
	 */
	public function detach(Observer $observer);

	/**
	 * Triggers an event in the attached observers
	 *
	 * @param   string  $event  The event to attach
	 * @param   array   $args   Arguments to the event handler
	 *
	 * @return  array
	 */
	public function trigger($event, array $args = []);
}
Event/Observer.php000060400000002764152455514240010141 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Event;

defined('_JEXEC') || die;

use ReflectionMethod;
use ReflectionObject;

class Observer
{
	/** @var   Observable  The object to observe */
	protected $subject = null;

	protected $events = null;

	/**
	 * Creates the observer and attaches it to the observable subject object
	 *
	 * @param   Observable  $subject  The observable object to attach the observer to
	 */
	function __construct(Observable &$subject)
	{
		// Attach this observer to the subject
		$subject->attach($this);

		// Store a reference to the subject object
		$this->subject = $subject;
	}

	/**
	 * Returns the list of events observable by this observer. Set the $this->events array manually for faster
	 * processing, or let this method use reflection to return a list of all public methods.
	 *
	 * @return  array
	 */
	public function getObservableEvents()
	{
		if (is_null($this->events))
		{
			// Assign an empty array to protect us from behaviours without any valid method
			$this->events = [];

			$reflection = new ReflectionObject($this);
			$methods    = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);

			foreach ($methods as $m)
			{
				if ($m->name == 'getObservableEvents')
				{
					continue;
				}

				if ($m->name == '__construct')
				{
					continue;
				}

				$this->events[] = $m->name;
			}
		}

		return $this->events;
	}
}
Event/Dispatcher.php000060400000016042152455514240010432 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Event;

defined('_JEXEC') || die;

use FOF30\Container\Container;

class Dispatcher implements Observable
{
	/** @var   Container  The container this event dispatcher is attached to */
	protected $container = null;

	/** @var   array  The observers attached to the dispatcher */
	protected $observers = [];

	/** @var   array  Maps events to observers */
	protected $events = [];

	/**
	 * Public constructor
	 *
	 * @param   Container  $container  The container this event dispatcher is attached to
	 */
	public function __construct(Container $container)
	{
		$this->container = $container;
	}

	/**
	 * Returns the container this event dispatcher is attached to
	 *
	 * @return  Container
	 */
	public function getContainer()
	{
		return $this->container;
	}

	/**
	 * Attaches an observer to the object
	 *
	 * @param   Observer  $observer  The observer to attach
	 *
	 * @return  Dispatcher  Ourselves, for chaining
	 */
	public function attach(Observer $observer)
	{
		$className = get_class($observer);

		// Make sure this observer is not already registered
		if (isset($this->observers[$className]))
		{
			return $this;
		}

		// Attach observer
		$this->observers[$className] = $observer;

		// Register the observable events
		$events = $observer->getObservableEvents();

		foreach ($events as $event)
		{
			$event = strtolower($event);

			if (!isset($this->events[$event]))
			{
				$this->events[$event] = [$className];
			}
			else
			{
				$this->events[$event][] = $className;
			}
		}

		return $this;
	}

	/**
	 * Detaches an observer from the object
	 *
	 * @param   Observer  $observer  The observer to detach
	 *
	 * @return  Dispatcher  Ourselves, for chaining
	 */
	public function detach(Observer $observer)
	{
		$className = get_class($observer);

		// Make sure this observer is already registered
		if (!isset($this->observers[$className]))
		{
			return $this;
		}

		// Unregister the observable events
		$events = $observer->getObservableEvents();

		foreach ($events as $event)
		{
			$event = strtolower($event);

			if (isset($this->events[$event]))
			{
				$key = array_search($className, $this->events[$event]);

				if ($key !== false)
				{
					unset($this->events[$event][$key]);

					if (empty($this->events[$event]))
					{
						unset ($this->events[$event]);
					}
				}
			}
		}

		// Detach observer
		unset($this->observers[$className]);

		return $this;
	}

	/**
	 * Is an observer object already registered with this dispatcher?
	 *
	 * @param   Observer  $observer  The observer to check if it's attached
	 *
	 * @return  boolean
	 */
	public function hasObserver(Observer $observer)
	{
		$className = get_class($observer);

		return $this->hasObserverClass($className);
	}

	/**
	 * Is there an observer of the specified class already registered with this dispatcher?
	 *
	 * @param   string  $className  The observer class name to check if it's attached
	 *
	 * @return  boolean
	 */
	public function hasObserverClass($className)
	{
		return isset($this->observers[$className]);
	}

	/**
	 * Returns an observer attached to this behaviours dispatcher by its class name
	 *
	 * @param   string  $className  The class name of the observer object to return
	 *
	 * @return  null|Observer
	 */
	public function getObserverByClass($className)
	{
		if (!$this->hasObserverClass($className))
		{
			return null;
		}

		return $this->observers[$className];
	}

	/**
	 * Triggers an event in the attached observers
	 *
	 * @param   string  $event  The event to attach
	 * @param   array   $args   Arguments to the event handler
	 *
	 * @return  array
	 */
	public function trigger($event, array $args = [])
	{
		$event = strtolower($event);

		$result = [];

		// Make sure the event is known to us, otherwise return an empty array
		if (!isset($this->events[$event]) || empty($this->events[$event]))
		{
			return $result;
		}

		foreach ($this->events[$event] as $className)
		{
			// Make sure the observer exists.
			if (!isset($this->observers[$className]))
			{
				continue;
			}

			// Get the observer
			$observer = $this->observers[$className];

			// Make sure the method exists
			if (!method_exists($observer, $event))
			{
				continue;
			}

			// Call the event handler and add its output to the return value. The switch allows for execution up to 2x
			// faster than using call_user_func_array
			switch (count($args))
			{
				case 0:
					$result[] = $observer->{$event}();
					break;
				case 1:
					$result[] = $observer->{$event}($args[0]);
					break;
				case 2:
					$result[] = $observer->{$event}($args[0], $args[1]);
					break;
				case 3:
					$result[] = $observer->{$event}($args[0], $args[1], $args[2]);
					break;
				case 4:
					$result[] = $observer->{$event}($args[0], $args[1], $args[2], $args[3]);
					break;
				case 5:
					$result[] = $observer->{$event}($args[0], $args[1], $args[2], $args[3], $args[4]);
					break;
				default:
					$result[] = call_user_func_array([$observer, $event], $args);
					break;
			}
		}

		// Return the observers' result in an array
		return $result;
	}

	/**
	 * Asks each observer to handle an event based on the provided arguments. The first observer to return a non-null
	 * result wins. This is a *very* simplistic implementation of the Chain of Command pattern.
	 *
	 * @param   string  $event  The event name to handle
	 * @param   array   $args   The arguments to the event
	 *
	 * @return  mixed  Null if the event can't be handled by any observer
	 */
	public function chainHandle($event, $args = [])
	{
		$event = strtolower($event);

		$result = null;

		// Make sure the event is known to us, otherwise return an empty array
		if (!isset($this->events[$event]) || empty($this->events[$event]))
		{
			return $result;
		}

		foreach ($this->events[$event] as $className)
		{
			// Make sure the observer exists.
			if (!isset($this->observers[$className]))
			{
				continue;
			}

			// Get the observer
			$observer = $this->observers[$className];

			// Make sure the method exists
			if (!method_exists($observer, $event))
			{
				continue;
			}

			// Call the event handler and add its output to the return value. The switch allows for execution up to 2x
			// faster than using call_user_func_array
			switch (count($args))
			{
				case 0:
					$result = $observer->{$event}();
					break;
				case 1:
					$result = $observer->{$event}($args[0]);
					break;
				case 2:
					$result = $observer->{$event}($args[0], $args[1]);
					break;
				case 3:
					$result = $observer->{$event}($args[0], $args[1], $args[2]);
					break;
				case 4:
					$result = $observer->{$event}($args[0], $args[1], $args[2], $args[3]);
					break;
				case 5:
					$result = $observer->{$event}($args[0], $args[1], $args[2], $args[3], $args[4]);
					break;
				default:
					$result = call_user_func_array([$observer, $event], $args);
					break;
			}

			if (!is_null($result))
			{
				return $result;
			}
		}

		// Return the observers' result in an array
		return $result;
	}
}
Template/Template.php000060400000045464152455514240010623 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Template;

defined('_JEXEC') || die;

use Exception;
use FOF30\Container\Container;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use stdClass;

/**
 * A utility class to load view templates, media files and modules.
 *
 * @since    1.0
 */
class Template
{
	/**
	 * The component container
	 *
	 * @var  Container
	 */
	protected $container = null;

	/**
	 * Public constructor
	 *
	 * @param   Container  $container  The component container
	 */
	function __construct(Container $container)
	{
		$this->container = $container;
	}

	/**
	 * Add a CSS file to the page generated by the CMS
	 *
	 * @param   string  $uri      A path definition understood by parsePath, e.g. media://com_example/css/foo.css
	 * @param   string  $version  (optional) Version string to be added to the URL
	 * @param   string  $type     MIME type of the stylesheeet
	 * @param   string  $media    Media target definition of the style sheet, e.g. "screen"
	 * @param   array   $attribs  Array of attributes
	 *
	 * @see self::parsePath
	 *
	 * @return  void
	 */
	public function addCSS($uri, $version = null, $type = 'text/css', $media = null, $attribs = [])
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		// Make sure we have attributes
		if (empty($attribs) || !is_array($attribs))
		{
			$attribs = [];
		}

		$url      = $this->parsePath($uri);
		$document = $this->container->platform->getDocument();

		// Joomla! 3.7+ uses a single method for everything
		if (version_compare(JVERSION, '3.6.999', 'ge'))
		{
			$options = [
				'version' => is_null($version) ? null : ((string) $version),
			];

			$attribs['type'] = $type;

			if (!empty($media))
			{
				$attribs['media'] = $media;
			}

			$document->addStyleSheet($url, $options, $attribs);

			return;
		}

		// Joomla! 3.6 and lower have separate methods for versioned and non-versioned files
		$method = 'addStyleSheet' . (!empty($version) ? 'Version' : '');

		if (!method_exists($document, $method))
		{
			return;
		}


		if (empty($version))
		{
			$document->addStyleSheet($url, $type, $media, $attribs);

			return;
		}

		$document->addStyleSheetVersion($url, (string) $version, $type, $media, $attribs);
	}

	/**
	 * Add a JS script file to the page generated by the CMS.
	 *
	 * There are three combinations of defer and async (see http://www.w3schools.com/tags/att_script_defer.asp):
	 * * $defer false, $async true: The script is executed asynchronously with the rest of the page
	 *   (the script will be executed while the page continues the parsing)
	 * * $defer true, $async false: The script is executed when the page has finished parsing.
	 * * $defer false, $async false. (default) The script is loaded and executed immediately. When it finishes
	 *   loading the browser continues parsing the rest of the page.
	 *
	 * When you are using $async = true there is no guarantee about the load order of the scripts. Whichever
	 * script loads first will be executed first. The order they appear on the page is completely irrelevant.
	 *
	 * If you set both async and defer to true it's the same as setting only async to true. That is to say, scripts will
	 * be loaded and executed in an order that is NOT guaranteed. Only set async for completely standalone scripts.
	 *
	 * If you are using defer you should pass script options using $this->container->platform->addScriptOptions(). This
	 * will add the script options as a JSON document in the document head. You can retrieve these options client-side
	 * with Joomla.getOptions() as long as the Joomla core JS has already been loaded, i.e. your deferred script is
	 * added AFTER Joomla's core code.
	 *
	 * @param   string   $uri      A path definition understood by parsePath, e.g. media://com_example/js/foo.js
	 * @param   boolean  $defer    Adds the defer attribute, see above
	 * @param   boolean  $async    Adds the async attribute, see above
	 * @param   string   $version  (optional) Version string to be added to the URL
	 * @param   string   $type     MIME type of the script
	 *
	 * @see self::parsePath
	 *
	 * @return  void
	 */
	public function addJS($uri, $defer = false, $async = false, $version = null, $type = 'text/javascript')
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$url      = $this->container->template->parsePath($uri);
		$document = $this->container->platform->getDocument();

		// Setting both defer and async is nonsense. Only async makes sense in this case.
		if ($defer && $async)
		{
			$defer = false;
		}

		// Joomla! 3.7+ uses a single method for everything
		if (version_compare(JVERSION, '3.6.999', 'ge'))
		{
			$options = [
				'version' => is_null($version) ? null : ((string) $version),
			];

			$attribs['defer'] = $defer;
			$attribs['async'] = $async;

			if (!empty($media))
			{
				$attribs['mime'] = $type;
			}

			$document->addScript($url, $options, $attribs);

			return;
		}

		// Joomla! 3.6 and lower have separate methods for versioned and non-versioned files
		$method = 'addScript' . (!empty($version) ? 'Version' : '');

		if (!method_exists($document, $method))
		{
			return;
		}


		if (empty($version))
		{
			$document->addScript($url, $type, $defer, $async);

			return;
		}

		$document->addScriptVersion($url, (string) $version, $type, $defer, $async);
	}

	/**
	 * Adds an inline JavaScript script to the page header
	 *
	 * @param   string  $script  The script content to add
	 * @param   string  $type    The MIME type of the script
	 */
	public function addJSInline($script, $type = 'text/javascript')
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$document = $this->container->platform->getDocument();

		if (!method_exists($document, 'addScriptDeclaration'))
		{
			return;
		}

		$document->addScriptDeclaration($script, $type);
	}

	/**
	 * Adds an inline stylesheet (inline CSS) to the page header
	 *
	 * @param   string  $css   The stylesheet content to add
	 * @param   string  $type  The MIME type of the script
	 */
	public function addCSSInline($css, $type = 'text/css')
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$document = $this->container->platform->getDocument();

		if (!method_exists($document, 'addStyleDeclaration'))
		{
			return;
		}

		$document->addStyleDeclaration($css, $type);
	}

	/**
	 * Creates a SEF compatible sort header. Standard Joomla function will add a href="#" tag, so with SEF
	 * enabled, the browser will follow the fake link instead of processing the onSubmit event; so we
	 * need a fix.
	 *
	 * @param   string    $text   Header text
	 * @param   string    $field  Field used for sorting
	 * @param   stdClass  $list   Object holding the direction and the ordering field
	 *
	 * @return  string  HTML code for sorting
	 */
	public function sefSort($text, $field, $list)
	{
		$sort = HTMLHelper::_('grid.sort', Text::_(strtoupper($text)) . '&nbsp;', $field, $list->order_Dir, $list->order);

		return str_replace('href="#"', 'href="javascript:void(0);"', $sort);
	}

	/**
	 * Parse a fancy path definition into a path relative to the site's root,
	 * respecting template overrides, suitable for inclusion of media files.
	 * For example, media://com_foobar/css/test.css is parsed into
	 * media/com_foobar/css/test.css if no override is found, or
	 * templates/mytemplate/media/com_foobar/css/test.css if the current
	 * template is called mytemplate and there's a media override for it.
	 *
	 * Regarding plugins, templates are searched inside the plugin's tmpl directory and the template's html directory.
	 * For instance considering plugin://system/example/something the files will be looked for in:
	 * plugins/system/example/tmpl/something.php
	 * templates/yourTemplate/html/plg_system_example/something.php
	 * Template paths for plugins are uncommon and not standard Joomla! practice. They make sense when you are
	 * implementing features of your component as plugins and they need to provide HTML output, e.g. some of the
	 * integration plugins we use in Akeeba Subscriptions.
	 *
	 * The valid protocols are ( @param   string  $path  Fancy path
	 *
	 * @param   boolean  $localFile  When true, it returns the local path, not the URL
	 *
	 * @return  string  Parsed path
	 * @see self::getAltPaths ):
	 *      media://        The media directory or a media override
	 *      plugin://    Given as plugin://pluginType/pluginName/template, e.g. plugin://system/example/something
	 *      admin://        Path relative to administrator directory (no overrides)
	 *      site://        Path relative to site's root (no overrides)
	 *      auto://      Automatically guess if it should be site:// or admin://
	 *      module://    The module directory or a template override (must be module://moduleName/templateName)
	 *
	 */
	public function parsePath($path, $localFile = false)
	{
		// Make sure the path has a protocol we can parse. Otherwise just return the raw path.
		$protocol          = '';
		$separatorPosition = strpos($path, '://');

		if ($separatorPosition === false)
		{
			return $path;
		}

		$protocol = substr($path, 0, $separatorPosition);

		switch ($protocol)
		{
			case 'media':
			case 'plugin':
			case 'module':
			case 'auto':
			case 'admin':
			case 'site':
				break;

			default:
				return $path;
		}

		// Get the platform directories through the container
		$platformDirs = $this->container->platform->getPlatformBaseDirs();

		if ($localFile)
		{
			$url = rtrim($platformDirs['root'], DIRECTORY_SEPARATOR) . '/';
		}
		else
		{
			$url = $this->container->platform->URIroot();
		}

		$altPaths = $this->getAltPaths($path);
		$filePath = $altPaths['normal'];

		// If JDEBUG is enabled, prefer the debug path, else prefer an alternate path if present
		if (defined('JDEBUG') && JDEBUG && isset($altPaths['debug']))
		{
			if (file_exists($platformDirs['public'] . '/' . $altPaths['debug']))
			{
				$filePath = $altPaths['debug'];
			}
		}
		elseif (isset($altPaths['alternate']))
		{
			if (file_exists($platformDirs['public'] . '/' . $altPaths['alternate']))
			{
				$filePath = $altPaths['alternate'];
			}
		}

		$url .= $filePath;

		return $url;
	}

	/**
	 * Parse a fancy path definition into a path relative to the site's root.
	 * It returns both the normal and alternative (template media override) path.
	 * For example, media://com_foobar/css/test.css is parsed into
	 * array(
	 *   'normal' => 'media/com_foobar/css/test.css',
	 *   'alternate' => 'templates/mytemplate/media/com_foobar/css//test.css'
	 * );
	 *
	 * The valid protocols are:
	 * media://        The media directory or a media override
	 * admin://        Path relative to administrator directory (no alternate)
	 * site://        Path relative to site's root (no alternate)
	 * auto://      Automatically guess if it should be site:// or admin://
	 * plugin://    The plugin directory or a template override (must be plugin://pluginType/pluginName/templateName)
	 * module://    The module directory or a template override (must be module://moduleName/templateName)
	 *
	 * @param   string  $path  Fancy path
	 *
	 * @return  array  Array of normal and alternate parsed path
	 */
	public function getAltPaths($path)
	{
		$protoAndPath = explode('://', $path, 2);

		if (count($protoAndPath) < 2)
		{
			$protocol = 'media';
		}
		else
		{
			$protocol = $protoAndPath[0];
			$path     = $protoAndPath[1];
		}

		if ($protocol == 'auto')
		{
			$protocol = $this->container->platform->isBackend() ? 'admin' : 'site';
		}

		$path = ltrim($path, '/' . DIRECTORY_SEPARATOR);

		switch ($protocol)
		{
			case 'media':
				// Do we have a media override in the template?
				$pathAndParams = explode('?', $path, 2);

				$ret = [
					'normal'    => 'media/' . $pathAndParams[0],
					'alternate' => $this->container->platform->getTemplateOverridePath('media:/' . $pathAndParams[0], false),
				];
				break;

			case 'plugin':
				// The path is pluginType/pluginName/viewTemplate
				$pathInfo     = explode('/', $path);
				$pluginType   = $pathInfo[0] ?? 'system';
				$pluginName   = $pathInfo[1] ?? 'foobar';
				$viewTemplate = $pathInfo[2] ?? 'default';

				$pluginSystemName = 'plg_' . $pluginType . '_' . $pluginName;

				$ret = [
					'normal'    => 'plugins/' . $pluginType . '/' . $pluginName . '/tmpl/' . $viewTemplate,
					'alternate' => $this->container->platform->getTemplateOverridePath($pluginSystemName . '/' . $viewTemplate, false),
				];

				break;

			case 'module':
				// The path is moduleName/viewTemplate
				$pathInfo     = explode('/', $path, 2);
				$moduleName   = $pathInfo[0] ?? 'foobar';
				$viewTemplate = $pathInfo[1] ?? 'default';

				$moduleSystemName = 'mod_' . $moduleName;
				$basePath         = $this->container->platform->isBackend() ? 'administrator/' : '';

				$ret = [
					'normal'    => $basePath . 'modules/' . $moduleSystemName . '/tmpl/' . $viewTemplate,
					'alternate' => $this->container->platform->getTemplateOverridePath($moduleSystemName . '/' . $viewTemplate, false),
				];

				break;

			case 'admin':
				$ret = [
					'normal' => 'administrator/' . $path,
				];
				break;

			default:
			case 'site':
				$ret = [
					'normal' => $path,
				];
				break;
		}

		// For CSS and JS files, add a debug path if the supplied file is compressed
		$filesystem = $this->container->filesystem;
		$ext        = $filesystem->getExt($ret['normal']);

		if (in_array($ext, ['css', 'js']))
		{
			$file = basename($filesystem->stripExt($ret['normal']));

			/*
			 * Detect if we received a file in the format name.min.ext
			 * If so, strip the .min part out, otherwise append -uncompressed
			 */

			if (strlen($file) > 4 && strrpos($file, '.min', '-4'))
			{
				$position = strrpos($file, '.min', '-4');
				$filename = str_replace('.min', '.', $file, $position) . $ext;
			}
			else
			{
				$filename = $file . '-uncompressed.' . $ext;
			}

			// Clone the $ret array so we can manipulate the 'normal' path a bit
			$t1   = (object) $ret;
			$temp = clone $t1;
			unset($t1);
			$temp       = (array) $temp;
			$normalPath = explode('/', $temp['normal']);
			array_pop($normalPath);
			$normalPath[] = $filename;
			$ret['debug'] = implode('/', $normalPath);
		}

		return $ret;
	}

	/**
	 * Returns the contents of a module position
	 *
	 * @param   string  $position  The position name, e.g. "position-1"
	 * @param   int     $style     Rendering style, see JDocumentRendererModule::render
	 *
	 * @return  string  The contents of the module position
	 */
	public function loadPosition($position, $style = -2)
	{
		$document = $this->container->platform->getDocument();

		if (!($document instanceof Document))
		{
			return '';
		}

		if (!method_exists($document, 'loadRenderer'))
		{
			return '';
		}

		try
		{
			$renderer = $document->loadRenderer('module');
		}
		catch (Exception $exc)
		{
			return '';
		}

		$params = ['style' => $style];

		$contents = '';

		foreach (ModuleHelper::getModules($position) as $mod)
		{
			$contents .= $renderer->render($mod, $params);
		}

		return $contents;
	}

	/**
	 * Render a module by name
	 *
	 * @param   string  $moduleName  The name of the module (real, eg 'Breadcrumbs' or folder, eg 'mod_breadcrumbs')
	 * @param   int     $style       The rendering style, see JDocumentRendererModule::render
	 *
	 * @return string  The rendered module
	 */
	public function loadModule($moduleName, $style = -2)
	{
		$document = $this->container->platform->getDocument();

		if (!($document instanceof Document))
		{
			return '';
		}

		if (!method_exists($document, 'loadRenderer'))
		{
			return '';
		}

		try
		{
			$renderer = $document->loadRenderer('module');
		}
		catch (Exception $exc)
		{
			return '';
		}

		$params = ['style' => $style];

		$mod = ModuleHelper::getModule($moduleName);

		if (empty($mod))
		{
			return '';
		}

		return $renderer->render($mod, $params);
	}

	/**
	 * Performs SEF routing on a non-SEF URL, returning the SEF URL.
	 *
	 * When the $merge option is set to true the option, view, layout and format parameters of the current URL and the
	 * requested route will be merged. For example, assuming that current url is
	 * http://example.com/index.php?option=com_foo&view=cpanel
	 * then
	 * $template->route('view=categories&layout=tree');
	 * will result to
	 * http://example.com/index.php?option=com_foo&view=categories&layout=tree
	 *
	 * If $merge is unspecified (null) we will auto-detect the intended behavior. If you haven't specified option and
	 * one of view or task we will merge. Otherwise no merging takes place. This covers most use cases of FOF 3.0.
	 *
	 * @param   string  $route  The parameters string
	 * @param   bool    $merge  Should I perform parameter merging?
	 *
	 * @return  string  The human readable, complete url
	 */
	public function route($route = '', $merge = null)
	{
		$route = trim($route);

		/**
		 * Backwards compatibility with FOF 3.0: if the merge option is unspecified we will auto-detect the behaviour.
		 * If option and either one of view or task are present we won't merge.
		 */
		if (is_null($merge))
		{
			$hasOption = (strpos($route, 'option=') !== false);
			$hasView   = (strpos($route, 'view=') !== false);
			$hasTask   = (strpos($route, 'task=') !== false);

			$merge = !($hasOption && ($hasView || $hasTask));
		}

		if ($merge)
		{
			// Handle special cases before trying to merge
			if ($route == 'index.php' || $route == 'index.php?')
			{
				$result = 'index.php';
			}
			elseif (substr($route, 0, 1) == '&')
			{
				$url  = Uri::getInstance();
				$vars = [];
				parse_str($route, $vars);

				$url->setQuery(array_merge($url->getQuery(true), $vars));

				$result = 'index.php?' . $url->getQuery();
			}
			else
			{
				$url   = Uri::getInstance();
				$props = $url->getQuery(true);

				// Strip 'index.php?'
				if (substr($route, 0, 10) == 'index.php?')
				{
					$route = substr($route, 10);
				}

				// Parse route
				$parts = [];
				parse_str($route, $parts);
				$result = [];

				// Check to see if there is component information in the route if not add it

				if (!isset($parts['option']) && isset($props['option']))
				{
					$result[] = 'option=' . $props['option'];
				}

				// Add the layout information to the route only if it's not 'default'

				if (!isset($parts['view']) && isset($props['view']))
				{
					$result[] = 'view=' . $props['view'];

					if (!isset($parts['layout']) && isset($props['layout']))
					{
						$result[] = 'layout=' . $props['layout'];
					}
				}

				// Add the format information to the URL only if it's not 'html'

				if (!isset($parts['format']) && isset($props['format']) && $props['format'] != 'html')
				{
					$result[] = 'format=' . $props['format'];
				}

				// Reconstruct the route

				if (!empty($route))
				{
					$result[] = $route;
				}

				$result = 'index.php?' . implode('&', $result);
			}
		}
		else
		{
			$result = $route;
		}

		return Route::_($result);
	}
}
Timer/Timer.php000060400000003355152455514240007426 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Timer;

defined('_JEXEC') || die;

/**
 * Timeout prevention timer
 */
class Timer
{
	/**
	 * Maximum execution time allowance per step
	 *
	 * @var  integer
	 */
	private $max_exec_time = null;

	/**
	 * Timestamp of execution start
	 *
	 * @var  integer
	 */
	private $start_time = null;

	/**
	 * Public constructor, creates the timer object and calculates the execution
	 * time limits.
	 *
	 * @param   integer  $max_exec_time  Maximum execution time, in seconds
	 * @param   integer  $runtime_bias   Runtime bias factor, as percent points of the max execution time
	 *
	 * @return  Timer
	 */
	public function __construct($max_exec_time = 5, $runtime_bias = 75)
	{
		// Initialize start time
		$this->start_time = microtime(true);

		$this->max_exec_time = $max_exec_time * $runtime_bias / 100;
	}

	/**
	 * Wake-up function to reset internal timer when we get unserialized
	 *
	 * @return  void
	 */
	public function __wakeup()
	{
		// Re-initialize start time on wake-up
		$this->start_time = microtime(true);
	}

	/**
	 * Gets the number of seconds left, before we hit the "must break" threshold
	 *
	 * @return  float
	 */
	public function getTimeLeft()
	{
		return $this->max_exec_time - $this->getRunningTime();
	}

	/**
	 * Gets the time elapsed since object creation/unserialization, effectively
	 * how long this step is running
	 *
	 * @return  float
	 */
	public function getRunningTime()
	{
		return microtime(true) - $this->start_time;
	}

	/**
	 * Reset the timer
	 *
	 * @return  void
	 */
	public function resetTime()
	{
		$this->start_time = microtime(true);
	}
}
Autoloader/Autoloader.php000060400000014604152455514240011463 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Autoloader;

defined('_JEXEC') || die;

use InvalidArgumentException;

/**
 * A PSR-4 class autoloader. This is a modified version of Composer's ClassLoader class
 *
 * @codeCoverageIgnore
 */
class Autoloader
{
	/** @var   Autoloader  The static instance of this autoloader */
	private static $instance;
	/** @var   array  Lengths of PSR-4 prefixes */
	private $prefixLengths = [];
	/** @var   array  Prefix to directory map */
	private $prefixDirs = [];
	/** @var   array  Fall-back directories */
	private $fallbackDirs = [];

	/**
	 * @return Autoloader
	 */
	public static function getInstance()
	{
		if (!is_object(self::$instance))
		{
			self::$instance = new Autoloader();
		}

		return self::$instance;
	}

	/**
	 * Returns the prefix to directory map
	 *
	 * @return  array
	 *
	 * @noinspection PhpUnused
	 */
	public function getPrefixes()
	{
		return $this->prefixDirs;
	}

	/**
	 * Returns the list of fall=back directories
	 *
	 * @return  array
	 *
	 * @noinspection PhpUnused
	 */
	public function getFallbackDirs()
	{
		return $this->fallbackDirs;
	}

	/**
	 * Registers a set of PSR-4 directories for a given namespace, either
	 * appending or prefixing to the ones previously set for this namespace.
	 *
	 * @param   string        $prefix   The prefix/namespace, with trailing '\\'
	 * @param   array|string  $paths    The PSR-0 base directories
	 * @param   boolean       $prepend  Whether to prefix the directories
	 *
	 * @return  $this for chaining
	 *
	 * @throws  InvalidArgumentException  When the prefix is invalid
	 */
	public function addMap($prefix, $paths, $prepend = false)
	{
		if ($prefix)
		{
			$prefix = ltrim($prefix, '\\');
		}

		if (!$prefix)
		{
			// Register directories for the root namespace.
			if ($prepend)
			{
				$this->fallbackDirs = array_merge(
					(array) $paths,
					$this->fallbackDirs
				);

				$this->fallbackDirs = array_unique($this->fallbackDirs);
			}
			else
			{
				$this->fallbackDirs = array_merge(
					$this->fallbackDirs,
					(array) $paths
				);

				$this->fallbackDirs = array_unique($this->fallbackDirs);
			}
		}
		elseif (!isset($this->prefixDirs[$prefix]))
		{
			// Register directories for a new namespace.
			$length = strlen($prefix);
			if ('\\' !== $prefix[$length - 1])
			{
				throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
			}
			$this->prefixLengths[$prefix[0]][$prefix] = $length;
			$this->prefixDirs[$prefix]                = (array) $paths;
		}
		elseif ($prepend)
		{
			// Prepend directories for an already registered namespace.
			$this->prefixDirs[$prefix] = array_merge(
				(array) $paths,
				$this->prefixDirs[$prefix]
			);

			$this->prefixDirs[$prefix] = array_unique($this->prefixDirs[$prefix]);
		}
		else
		{
			// Append directories for an already registered namespace.
			$this->prefixDirs[$prefix] = array_merge(
				$this->prefixDirs[$prefix],
				(array) $paths
			);

			$this->prefixDirs[$prefix] = array_unique($this->prefixDirs[$prefix]);
		}

		return $this;
	}

	/**
	 * Does the autoloader have a map for the specified prefix?
	 *
	 * @param   string  $prefix
	 *
	 * @return  bool
	 */
	public function hasMap($prefix)
	{
		return isset($this->prefixDirs[$prefix]);
	}

	/**
	 * Registers a set of PSR-4 directories for a given namespace,
	 * replacing any others previously set for this namespace.
	 *
	 * @param   string        $prefix  The prefix/namespace, with trailing '\\'
	 * @param   array|string  $paths   The PSR-4 base directories
	 *
	 * @return  void
	 *
	 * @throws  InvalidArgumentException  When the prefix is invalid
	 *
	 * @noinspection PhpUnused
	 */
	public function setMap($prefix, $paths)
	{
		if ($prefix)
		{
			$prefix = ltrim($prefix, '\\');
		}

		if (!$prefix)
		{
			$this->fallbackDirs = (array) $paths;
		}
		else
		{
			$length = strlen($prefix);
			if ('\\' !== $prefix[$length - 1])
			{
				throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
			}
			$this->prefixLengths[$prefix[0]][$prefix] = $length;
			$this->prefixDirs[$prefix]                = (array) $paths;
		}
	}

	/**
	 * Registers this instance as an autoloader.
	 *
	 * @param   boolean  $prepend  Whether to prepend the autoloader or not
	 *
	 * @return  void
	 */
	public function register($prepend = false)
	{
		spl_autoload_register([$this, 'loadClass'], true, $prepend);
	}

	/**
	 * Unregisters this instance as an autoloader.
	 *
	 * @return  void
	 */
	public function unregister()
	{
		spl_autoload_unregister([$this, 'loadClass']);
	}

	/**
	 * Loads the given class or interface.
	 *
	 * @param   string  $class  The name of the class
	 *
	 * @return  bool True if loaded, null otherwise
	 */
	public function loadClass($class)
	{
		if ($file = $this->findFile($class))
		{
			/** @noinspection PhpIncludeInspection */
			include $file;

			return true;
		}

		return false;
	}

	/**
	 * Finds the path to the file where the class is defined.
	 *
	 * @param   string  $class  The name of the class
	 *
	 * @return  string|false  The path if found, false otherwise
	 */
	public function findFile($class)
	{
		// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
		if ('\\' == $class[0])
		{
			$class = substr($class, 1);
		}

		// FEFHelper lookup
		if (substr($class, 0, 9) == 'FEFHelper')
		{

			if (file_exists($file = realpath(__DIR__ . '/..') . '/Utils/FEFHelper/' . strtolower(substr($class, 9)) . '.php'))
			{
				return $file;
			}
		}

		// PSR-4 lookup
		$logicalPath = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';

		$first = $class[0];

		if (isset($this->prefixLengths[$first]))
		{
			foreach ($this->prefixLengths[$first] as $prefix => $length)
			{
				if (0 === strpos($class, $prefix))
				{
					foreach ($this->prefixDirs[$prefix] as $dir)
					{
						if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPath, $length)))
						{
							return $file;
						}
					}
				}
			}
		}

		// PSR-4 fallback dirs
		foreach ($this->fallbackDirs as $dir)
		{
			if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPath))
			{
				return $file;
			}
		}

		return false;
	}
}

// Register the current namespace with the autoloader
Autoloader::getInstance()->addMap('FOF30\\', [realpath(__DIR__ . '/..')]);
Autoloader::getInstance()->register();
Layout/LayoutFile.php000060400000004261152455514240010615 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Layout;

defined('_JEXEC') || die;

use FOF30\Container\Container;
use Joomla\CMS\Layout\FileLayout;

/**
 * Base class for rendering a display layout
 * loaded from from a layout file
 *
 * This class searches for Joomla! version override Layouts. For example,
 * if you have run this under Joomla! 3.0 and you try to load
 * mylayout.default it will automatically search for the
 * layout files default.j30.php, default.j3.php and default.php, in this
 * order.
 *
 * @package  FrameworkOnFramework
 */
class LayoutFile extends FileLayout
{
	/** @var  Container  The component container */
	public $container = null;

	/**
	 * Method to finds the full real file path, checking possible overrides
	 *
	 * @return  string  The full path to the layout file
	 */
	protected function getPath()
	{
		if (is_null($this->container))
		{
			$component       = $this->options->get('component');
			$this->container = Container::getInstance($component);
		}

		$filesystem = $this->container->filesystem;

		if (is_null($this->fullPath) && !empty($this->layoutId))
		{
			$parts = explode('.', $this->layoutId);
			$file  = array_pop($parts);

			$filePath = implode('/', $parts);
			$suffixes = $this->container->platform->getTemplateSuffixes();

			foreach ($suffixes as $suffix)
			{
				$files[] = $file . $suffix . '.php';
			}

			$files[] = $file . '.php';

			$platformDirs = $this->container->platform->getPlatformBaseDirs();
			$prefix       = $this->container->platform->isBackend() ? $platformDirs['admin'] : $platformDirs['root'];

			$possiblePaths = [
				$prefix . '/templates/' . $this->container->platform->getTemplate() . '/html/layouts/' . $filePath,
				$this->basePath . '/' . $filePath,
				$platformDirs['root'] . '/layouts/' . $filePath,
			];

			reset($files);

			foreach ($files as $fileName)
			{
				if (!is_null($this->fullPath))
				{
					break;
				}

				$r              = $filesystem->pathFind($possiblePaths, $fileName);
				$this->fullPath = $r === false ? null : $r;
			}
		}

		return $this->fullPath;
	}
}
Layout/LayoutHelper.php000060400000002742152455514240011157 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Layout;

defined('_JEXEC') || die;

use FOF30\Container\Container;

class LayoutHelper
{
	/**
	 * A default base path that will be used if none is provided when calling the render method.
	 * Note that JLayoutFile itself will defaults to JPATH_ROOT . '/layouts' if no basePath is supplied at all
	 *
	 * @var    string
	 */
	public static $defaultBasePath = '';

	/**
	 * Method to render the layout.
	 *
	 * @param   Container  $container    The container of your component
	 * @param   string     $layoutFile   Dot separated path to the layout file, relative to base path
	 * @param   object     $displayData  Object which properties are used inside the layout file to build displayed
	 *                                   output
	 * @param   string     $basePath     Base path to use when loading layout files
	 *
	 * @return  string
	 */
	public static function render(Container $container, $layoutFile, $displayData = null, $basePath = '')
	{
		$basePath = empty($basePath) ? self::$defaultBasePath : $basePath;

		// Make sure we send null to LayoutFile if no path set
		$basePath          = empty($basePath) ? null : $basePath;
		$layout            = new LayoutFile($layoutFile, $basePath);
		$layout->container = $container;
		$renderedLayout    = $layout->render($displayData);

		return $renderedLayout;
	}

}
Configuration/Configuration.php000060400000012165152455514240012703 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration;

defined('_JEXEC') || die;

use FOF30\Container\Container;

/**
 * Reads and parses the fof.xml file in the back-end of a FOF-powered component,
 * provisioning the data to the rest of the FOF framework
 *
 * @since    2.1
 */
class Configuration
{
	/**
	 * Cache of FOF components' configuration variables
	 *
	 * @var array
	 */
	public static $configurations = [];
	/**
	 * The component's container
	 *
	 * @var  Container
	 */
	protected $container = null;

	function __construct(Container $c)
	{
		$this->container = $c;

		$this->parseComponent();
	}

	/**
	 * Returns the value of a variable. Variables use a dot notation, e.g.
	 * view.config.whatever where the first part is the domain, the rest of the
	 * parts specify the path to the variable.
	 *
	 * @param   string  $variable  The variable name
	 * @param   mixed   $default   The default value, or null if not specified
	 *
	 * @return  mixed  The value of the variable
	 */
	public function get($variable, $default = null)
	{
		static $domains = null;

		if (is_null($domains))
		{
			$domains = $this->getDomains();
		}

		[$domain, $var] = explode('.', $variable, 2);

		if (!in_array(ucfirst($domain), $domains))
		{
			return $default;
		}

		$class = '\\FOF30\\Configuration\\Domain\\' . ucfirst($domain);
		/** @var   \FOF30\Configuration\Domain\DomainInterface $o */
		$o = new $class;

		return $o->get(self::$configurations[$this->container->componentName], $var, $default);
	}

	/**
	 * Parses the configuration of the specified component
	 *
	 * @return  void
	 */
	protected function parseComponent()
	{
		if ($this->container->platform->isCli())
		{
			$order = ['cli', 'backend'];
		}
		elseif ($this->container->platform->isBackend())
		{
			$order = ['backend'];
		}
		else
		{
			$order = ['frontend'];
		}

		$order[] = 'common';

		$order                                                 = array_reverse($order);
		self::$configurations[$this->container->componentName] = [];

		foreach ([false, true] as $userConfig)
		{
			foreach ($order as $area)
			{
				$config                                                = $this->parseComponentArea($area, $userConfig);
				self::$configurations[$this->container->componentName] = array_replace_recursive(self::$configurations[$this->container->componentName], $config);
			}
		}
	}

	/**
	 * Parses the configuration options of a specific component area
	 *
	 * @param   string  $area        Which area to parse (frontend, backend, cli)
	 * @param   bool    $userConfig  When true the user configuration (fof.user.xml) file will be read
	 *
	 * @return  array  A hash array with the configuration data
	 */
	protected function parseComponentArea($area, $userConfig = false)
	{
		$component = $this->container->componentName;

		// Initialise the return array
		$ret = [];

		// Get the folders of the component
		$componentPaths = $this->container->platform->getComponentBaseDirs($component);
		$filesystem     = $this->container->filesystem;
		$path           = $componentPaths['admin'];

		if (isset($this->container['backEndPath']))
		{
			$path = $this->container['backEndPath'];
		}

		// This line unfortunately doesn't work with Unit Tests because JPath depends on the JPATH_SITE constant :(
		// $path = $filesystem->pathCheck($path);

		// Check that the path exists
		if (!$filesystem->folderExists($path))
		{
			return $ret;
		}

		// Read the filename if it exists
		$filename = $path . '/fof.xml';

		if ($userConfig)
		{
			$filename = $path . '/fof.user.xml';
		}

		if (!$filesystem->fileExists($filename) && !file_exists($filename))
		{
			return $ret;
		}

		$data = file_get_contents($filename);

		// Load the XML data in a SimpleXMLElement object
		$xml = simplexml_load_string($data);

		if (!($xml instanceof \SimpleXMLElement))
		{
			return $ret;
		}

		// Get this area's data
		$areaData = $xml->xpath('//' . $area);

		if (empty($areaData))
		{
			return $ret;
		}

		$xml = array_shift($areaData);

		// Parse individual configuration domains
		$domains = $this->getDomains();

		foreach ($domains as $dom)
		{
			$class = '\\FOF30\\Configuration\\Domain\\' . ucfirst($dom);

			if (class_exists($class, true))
			{
				/** @var   \FOF30\Configuration\Domain\DomainInterface $o */
				$o = new $class;
				$o->parseDomain($xml, $ret);
			}
		}

		// Finally, return the result
		return $ret;
	}

	/**
	 * Gets a list of the available configuration domain adapters
	 *
	 * @return  array  A list of the available domains
	 */
	protected function getDomains()
	{
		static $domains = [];

		if (empty($domains))
		{
			$filesystem = $this->container->filesystem;

			$files = $filesystem->folderFiles(__DIR__ . '/Domain', '.php');

			if (!empty($files))
			{
				foreach ($files as $file)
				{
					$domain = basename($file, '.php');

					if ($domain == 'DomainInterface')
					{
						continue;
					}

					$domain    = preg_replace('/[^A-Za-z0-9]/', '', $domain);
					$domains[] = $domain;
				}

				$domains = array_unique($domains);
			}
		}

		return $domains;
	}

}
Configuration/Domain/Models.php000060400000022565152455514240012533 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration\Domain;

defined('_JEXEC') || die;

use SimpleXMLElement;

/**
 * Configuration parser for the models-specific settings
 *
 * @since    2.1
 */
class Models implements DomainInterface
{
	/**
	 * Parse the XML data, adding them to the $ret array
	 *
	 * @param   SimpleXMLElement  $xml  The XML data of the component's configuration area
	 * @param   array            &$ret  The parsed data, in the form of a hash array
	 *
	 * @return  void
	 */
	public function parseDomain(SimpleXMLElement $xml, array &$ret)
	{
		// Initialise
		$ret['models'] = [];

		// Parse model configuration
		$modelsData = $xml->xpath('model');

		// Sanity check
		if (empty($modelsData))
		{
			return;
		}

		foreach ($modelsData as $aModel)
		{
			$key = (string) $aModel['name'];

			$ret['models'][$key]['behaviors']      = [];
			$ret['models'][$key]['behaviorsMerge'] = false;
			$ret['models'][$key]['tablealias']     = $aModel->xpath('tablealias');
			$ret['models'][$key]['fields']         = [];
			$ret['models'][$key]['relations']      = [];
			$ret['models'][$key]['config']         = [];


			// Parse configuration
			$optionData = $aModel->xpath('config/option');

			if (!empty($optionData))
			{
				foreach ($optionData as $option)
				{
					$k                                 = (string) $option['name'];
					$ret['models'][$key]['config'][$k] = (string) $option;
				}
			}

			// Parse field aliases
			$fieldData = $aModel->xpath('field');

			if (!empty($fieldData))
			{
				foreach ($fieldData as $field)
				{
					$k                                 = (string) $field['name'];
					$ret['models'][$key]['fields'][$k] = (string) $field;
				}
			}

			// Parse behaviours
			$behaviorsData  = (string) $aModel->behaviors;
			$behaviorsMerge = (string) $aModel->behaviors['merge'];

			if (!empty($behaviorsMerge))
			{
				$behaviorsMerge = trim($behaviorsMerge);
				$behaviorsMerge = strtoupper($behaviorsMerge);

				if (in_array($behaviorsMerge, ['1', 'YES', 'ON', 'TRUE']))
				{
					$ret['models'][$key]['behaviorsMerge'] = true;
				}
			}

			if (!empty($behaviorsData))
			{
				$behaviorsData = explode(',', $behaviorsData);

				foreach ($behaviorsData as $behavior)
				{
					$behavior = trim($behavior);

					if (empty($behavior))
					{
						continue;
					}

					$ret['models'][$key]['behaviors'][] = $behavior;
				}
			}

			// Parse relations
			$relationsData = $aModel->xpath('relation');

			if (!empty($relationsData))
			{
				foreach ($relationsData as $relationData)
				{
					$type     = (string) $relationData['type'];
					$itemName = (string) $relationData['name'];

					if (empty($type) || empty($itemName))
					{
						continue;
					}

					$modelClass    = (string) $relationData['foreignModelClass'];
					$localKey      = (string) $relationData['localKey'];
					$foreignKey    = (string) $relationData['foreignKey'];
					$pivotTable    = (string) $relationData['pivotTable'];
					$ourPivotKey   = (string) $relationData['pivotLocalKey'];
					$theirPivotKey = (string) $relationData['pivotForeignKey'];

					$relation = [
						'type'              => $type,
						'itemName'          => $itemName,
						'foreignModelClass' => empty($modelClass) ? null : $modelClass,
						'localKey'          => empty($localKey) ? null : $localKey,
						'foreignKey'        => empty($foreignKey) ? null : $foreignKey,
					];

					if (!empty($ourPivotKey) || !empty($theirPivotKey) || !empty($pivotTable))
					{
						$relation['pivotLocalKey']   = empty($ourPivotKey) ? null : $ourPivotKey;
						$relation['pivotForeignKey'] = empty($theirPivotKey) ? null : $theirPivotKey;
						$relation['pivotTable']      = empty($pivotTable) ? null : $pivotTable;
					}

					$ret['models'][$key]['relations'][] = $relation;
				}
			}
		}
	}

	/**
	 * Return a configuration variable
	 *
	 * @param   string &$configuration  Configuration variables (hashed array)
	 * @param   string  $var            The variable we want to fetch
	 * @param   mixed   $default        Default value
	 *
	 * @return  mixed  The variable's value
	 */
	public function get(&$configuration, $var, $default)
	{
		$parts = explode('.', $var);

		$view   = $parts[0];
		$method = 'get' . ucfirst($parts[1]);

		if (!method_exists($this, $method))
		{
			return $default;
		}

		array_shift($parts);
		array_shift($parts);

		$ret = $this->$method($view, $configuration, $parts, $default);

		return $ret;
	}

	/**
	 * Internal method to return the magic field mapping
	 *
	 * @param   string  $model          The model for which we will be fetching a field map
	 * @param   array  &$configuration  The configuration parameters hash array
	 * @param   array   $params         Extra options
	 * @param   string  $default        Default magic field mapping; empty if not defined
	 *
	 * @return  array   Field map
	 */
	protected function getField($model, &$configuration, $params, $default = '')
	{
		$fieldmap = [];

		if (isset($configuration['models']['*']) && isset($configuration['models']['*']['fields']))
		{
			$fieldmap = $configuration['models']['*']['fields'];
		}

		if (isset($configuration['models'][$model]) && isset($configuration['models'][$model]['fields']))
		{
			$fieldmap = array_merge($fieldmap, $configuration['models'][$model]['fields']);
		}

		$map = $default;

		if (empty($params[0]) || ($params[0] == '*'))
		{
			$map = $fieldmap;
		}
		elseif (isset($fieldmap[$params[0]]))
		{
			$map = $fieldmap[$params[0]];
		}

		return $map;
	}

	/**
	 * Internal method to get model alias
	 *
	 * @param   string  $model          The model for which we will be fetching table alias
	 * @param   array  &$configuration  The configuration parameters hash array
	 * @param   array   $params         Ignored
	 * @param   string  $default        Default table alias
	 *
	 * @return  string  Table alias
	 */
	protected function getTablealias($model, &$configuration, $params, $default = '')
	{
		$tableMap = [];

		if (isset($configuration['models']['*']['tablealias']))
		{
			$tableMap = $configuration['models']['*']['tablealias'];
		}

		if (isset($configuration['models'][$model]['tablealias']))
		{
			$tableMap = array_merge($tableMap, $configuration['models'][$model]['tablealias']);
		}

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

		return $tableMap[0];
	}

	/**
	 * Internal method to get model behaviours
	 *
	 * @param   string  $model          The model for which we will be fetching behaviours
	 * @param   array  &$configuration  The configuration parameters hash array
	 * @param   array   $params         Unused
	 * @param   string  $default        Default behaviour
	 *
	 * @return  string  Model behaviours
	 */
	protected function getBehaviors($model, &$configuration, $params, $default = '')
	{
		$behaviors = $default;

		if (isset($configuration['models']['*'])
			&& isset($configuration['models']['*']['behaviors'])
		)
		{
			$behaviors = $configuration['models']['*']['behaviors'];
		}

		if (isset($configuration['models'][$model])
			&& isset($configuration['models'][$model]['behaviors'])
		)
		{
			$merge = false;

			if (isset($configuration['models'][$model])
				&& isset($configuration['models'][$model]['behaviorsMerge'])
			)
			{
				$merge = (bool) $configuration['models'][$model]['behaviorsMerge'];
			}

			if ($merge)
			{
				$behaviors = array_merge($behaviors, $configuration['models'][$model]['behaviors']);
			}
			else
			{
				$behaviors = $configuration['models'][$model]['behaviors'];
			}
		}

		return $behaviors;
	}

	/**
	 * Internal method to get model relations
	 *
	 * @param   string  $model          The model for which we will be fetching relations
	 * @param   array  &$configuration  The configuration parameters hash array
	 * @param   array   $params         Unused
	 * @param   string  $default        Default relations
	 *
	 * @return  array   Model relations
	 */
	protected function getRelations($model, &$configuration, $params, $default = '')
	{
		$relations = $default;

		if (isset($configuration['models']['*'])
			&& isset($configuration['models']['*']['relations'])
		)
		{
			$relations = $configuration['models']['*']['relations'];
		}

		if (isset($configuration['models'][$model])
			&& isset($configuration['models'][$model]['relations'])
		)
		{
			$relations = $configuration['models'][$model]['relations'];
		}

		return $relations;
	}

	/**
	 * Internal method to return the a configuration option for the Model.
	 *
	 * @param   string  $model          The view for which we will be fetching a task map
	 * @param   array  &$configuration  The configuration parameters hash array
	 * @param   array   $params         Extra options; key 0 defines the option variable we want to fetch
	 * @param   mixed   $default        Default option; null if not defined
	 *
	 * @return  string  The setting for the requested option
	 */
	protected function getConfig($model, &$configuration, $params, $default = null)
	{
		$ret = $default;

		$config = [];

		if (isset($configuration['models']['*']['config']))
		{
			$config = $configuration['models']['*']['config'];
		}

		if (isset($configuration['models'][$model]['config']))
		{
			$config = array_merge($config, $configuration['models'][$model]['config']);
		}

		if (empty($params) || empty($params[0]))
		{
			return $config;
		}

		if (isset($config[$params[0]]))
		{
			$ret = $config[$params[0]];
		}

		return $ret;
	}
}
Configuration/Domain/Authentication.php000060400000003335152455514240014261 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration\Domain;

defined('_JEXEC') || die;

use SimpleXMLElement;

/**
 * Configuration parser for the authentication-specific settings
 *
 * @since    2.1
 */
class Authentication implements DomainInterface
{
	/**
	 * Parse the XML data, adding them to the $ret array
	 *
	 * @param   SimpleXMLElement   $xml  The XML data of the component's configuration area
	 * @param   array             &$ret  The parsed data, in the form of a hash array
	 *
	 * @return  void
	 */
	public function parseDomain(SimpleXMLElement $xml, array &$ret)
	{
		// Initialise
		$ret['authentication'] = [];

		// Parse the dispatcher configuration
		$authenticationData = $xml->authentication;

		// Sanity check

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

		$options = $xml->xpath('authentication/option');

		if (!empty($options))
		{
			foreach ($options as $option)
			{
				$key                         = (string) $option['name'];
				$ret['authentication'][$key] = (string) $option;
			}
		}
	}

	/**
	 * Return a configuration variable
	 *
	 * @param   string  &$configuration  Configuration variables (hashed array)
	 * @param   string   $var            The variable we want to fetch
	 * @param   mixed    $default        Default value
	 *
	 * @return  mixed  The variable's value
	 */
	public function get(&$configuration, $var, $default)
	{
		if ($var == '*')
		{
			return $configuration['authentication'];
		}

		if (isset($configuration['authentication'][$var]))
		{
			return $configuration['authentication'][$var];
		}
		else
		{
			return $default;
		}
	}
}
Configuration/Domain/DomainInterface.php000060400000002256152455514240014333 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration\Domain;

defined('_JEXEC') || die;

use SimpleXMLElement;

/**
 * The Interface of a FOF configuration domain class. The methods are used to parse and
 * provision sensible information to consumers. The Configuration class acts as an
 * adapter to the domain classes.
 *
 * @since    2.1
 */
interface DomainInterface
{
	/**
	 * Parse the XML data, adding them to the $ret array
	 *
	 * @param   SimpleXMLElement   $xml  The XML data of the component's configuration area
	 * @param   array             &$ret  The parsed data, in the form of a hash array
	 *
	 * @return  void
	 */
	public function parseDomain(SimpleXMLElement $xml, array &$ret);

	/**
	 * Return a configuration variable
	 *
	 * @param   string  &$configuration  Configuration variables (hashed array)
	 * @param   string   $var            The variable we want to fetch
	 * @param   mixed    $default        Default value
	 *
	 * @return  mixed  The variable's value
	 */
	public function get(&$configuration, $var, $default);
}
Configuration/Domain/Container.php000060400000003241152455514240013220 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration\Domain;

defined('_JEXEC') || die;

use SimpleXMLElement;

/**
 * Configuration parser for the Container-specific settings
 *
 * @since    2.1
 */
class Container implements DomainInterface
{
	/**
	 * Parse the XML data, adding them to the $ret array
	 *
	 * @param   SimpleXMLElement   $xml  The XML data of the component's configuration area
	 * @param   array             &$ret  The parsed data, in the form of a hash array
	 *
	 * @return  void
	 */
	public function parseDomain(SimpleXMLElement $xml, array &$ret)
	{
		// Initialise
		$ret['container'] = [];

		// Parse the dispatcher configuration
		$containerData = $xml->container;

		// Sanity check

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

		$options = $xml->xpath('container/option');

		if (!empty($options))
		{
			foreach ($options as $option)
			{
				$key                    = (string) $option['name'];
				$ret['container'][$key] = (string) $option;
			}
		}
	}

	/**
	 * Return a configuration variable
	 *
	 * @param   string  &$configuration  Configuration variables (hashed array)
	 * @param   string   $var            The variable we want to fetch
	 * @param   mixed    $default        Default value
	 *
	 * @return  mixed  The variable's value
	 */
	public function get(&$configuration, $var, $default)
	{
		if ($var == '*')
		{
			return $configuration['container'];
		}

		if (isset($configuration['container'][$var]))
		{
			return $configuration['container'][$var];
		}
		else
		{
			return $default;
		}
	}
}
Configuration/Domain/Dispatcher.php000060400000003255152455514240013371 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration\Domain;

defined('_JEXEC') || die;

use SimpleXMLElement;

/**
 * Configuration parser for the dispatcher-specific settings
 *
 * @since    2.1
 */
class Dispatcher implements DomainInterface
{
	/**
	 * Parse the XML data, adding them to the $ret array
	 *
	 * @param   SimpleXMLElement   $xml  The XML data of the component's configuration area
	 * @param   array             &$ret  The parsed data, in the form of a hash array
	 *
	 * @return  void
	 */
	public function parseDomain(SimpleXMLElement $xml, array &$ret)
	{
		// Initialise
		$ret['dispatcher'] = [];

		// Parse the dispatcher configuration
		$dispatcherData = $xml->dispatcher;

		// Sanity check

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

		$options = $xml->xpath('dispatcher/option');

		if (!empty($options))
		{
			foreach ($options as $option)
			{
				$key                     = (string) $option['name'];
				$ret['dispatcher'][$key] = (string) $option;
			}
		}
	}

	/**
	 * Return a configuration variable
	 *
	 * @param   string  &$configuration  Configuration variables (hashed array)
	 * @param   string   $var            The variable we want to fetch
	 * @param   mixed    $default        Default value
	 *
	 * @return  mixed  The variable's value
	 */
	public function get(&$configuration, $var, $default)
	{
		if ($var == '*')
		{
			return $configuration['dispatcher'];
		}

		if (isset($configuration['dispatcher'][$var]))
		{
			return $configuration['dispatcher'][$var];
		}
		else
		{
			return $default;
		}
	}
}
Configuration/Domain/Views.php000060400000020426152455514240012377 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 2, or later
 */

namespace FOF30\Configuration\Domain;

defined('_JEXEC') || die;

use SimpleXMLElement;

/**
 * Configuration parser for the view-specific settings
 *
 * @since    2.1
 */
class Views implements DomainInterface
{
	/**
	 * Parse the XML data, adding them to the $ret array
	 *
	 * @param   SimpleXMLElement   $xml  The XML data of the component's configuration area
	 * @param   array             &$ret  The parsed data, in the form of a hash array
	 *
	 * @return  void
	 */
	public function parseDomain(SimpleXMLElement $xml, array &$ret)
	{
		// Initialise
		$ret['views'] = [];

		// Parse view configuration
		$viewData = $xml->xpath('view');

		// Sanity check

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

		foreach ($viewData as $aView)
		{
			$key = (string) $aView['name'];

			// Parse ACL options
			$ret['views'][$key]['acl'] = [];
			$aclData                   = $aView->xpath('acl/task');

			if (!empty($aclData))
			{
				foreach ($aclData as $acl)
				{
					$k                             = (string) $acl['name'];
					$ret['views'][$key]['acl'][$k] = (string) $acl;
				}
			}

			// Parse taskmap
			$ret['views'][$key]['taskmap'] = [];
			$taskmapData                   = $aView->xpath('taskmap/task');

			if (!empty($taskmapData))
			{
				foreach ($taskmapData as $map)
				{
					$k                                 = (string) $map['name'];
					$ret['views'][$key]['taskmap'][$k] = (string) $map;
				}
			}

			// Parse controller configuration
			$ret['views'][$key]['config'] = [];
			$optionData                   = $aView->xpath('config/option');

			if (!empty($optionData))
			{
				foreach ($optionData as $option)
				{
					$k                                = (string) $option['name'];
					$ret['views'][$key]['config'][$k] = (string) $option;
				}
			}

			// Parse the toolbar
			$ret['views'][$key]['toolbar'] = [];
			$toolBars                      = $aView->xpath('toolbar');

			if (!empty($toolBars))
			{
				foreach ($toolBars as $toolBar)
				{
					$taskName = isset($toolBar['task']) ? (string) $toolBar['task'] : '*';

					// If a toolbar title is specified, create a title element.
					if (isset($toolBar['title']))
					{
						$ret['views'][$key]['toolbar'][$taskName]['title'] = [
							'value' => (string) $toolBar['title'],
						];
					}

					// Parse the toolbar buttons data
					$toolbarData = $toolBar->xpath('button');

					if (!empty($toolbarData))
					{
						foreach ($toolbarData as $button)
						{
							$k                                                     = (string) $button['type'];
							$ret['views'][$key]['toolbar'][$taskName][$k]          = current($button->attributes());
							$ret['views'][$key]['toolbar'][$taskName][$k]['value'] = (string) $button;
						}
					}
				}
			}
		}
	}

	/**
	 * Return a configuration variable
	 *
	 * @param   string  &$configuration  Configuration variables (hashed array)
	 * @param   string   $var            The variable we want to fetch
	 * @param   mixed    $default        Default value
	 *
	 * @return  mixed  The variable's value
	 */
	public function get(&$configuration, $var, $default)
	{
		$parts = explode('.', $var);

		$view   = $parts[0];
		$method = 'get' . ucfirst($parts[1]);

		if (!method_exists($this, $method))
		{
			return $default;
		}

		array_shift($parts);
		array_shift($parts);

		$ret = $this->$method($view, $configuration, $parts, $default);

		return $ret;
	}

	/**
	 * Internal function to return the task map for a view
	 *
	 * @param   string   $view           The view for which we will be fetching a task map
	 * @param   array   &$configuration  The configuration parameters hash array
	 * @param   array    $params         Extra options (not used)
	 * @param   array    $default        ßDefault task map; empty array if not provided
	 *
	 * @return  array  The task map as a hash array in the format task => method
	 */
	protected function getTaskmap($view, &$configuration, $params, $default = [])
	{
		$taskmap = [];

		if (isset($configuration['views']['*']) && isset($configuration['views']['*']['taskmap']))
		{
			$taskmap = $configuration['views']['*']['taskmap'];
		}

		if (isset($configuration['views'][$view]) && isset($configuration['views'][$view]['taskmap']))
		{
			$taskmap = array_merge($taskmap, $configuration['views'][$view]['taskmap']);
		}

		if (empty($taskmap))
		{
			return $default;
		}

		return $taskmap;
	}

	/**
	 * Internal method to return the ACL mapping (privilege required to access
	 * a specific task) for the given view's tasks
	 *
	 * @param   string   $view           The view for which we will be fetching a task map
	 * @param   array   &$configuration  The configuration parameters hash array
	 * @param   array    $params         Extra options; key 0 defines the task we want to fetch
	 * @param   string   $default        Default ACL option; empty (no ACL check) if not defined
	 *
	 * @return  string  The privilege required to access this view
	 */
	protected function getAcl($view, &$configuration, $params, $default = '')
	{
		$aclmap = [];

		if (isset($configuration['views']['*']) && isset($configuration['views']['*']['acl']))
		{
			$aclmap = $configuration['views']['*']['acl'];
		}

		if (isset($configuration['views'][$view]) && isset($configuration['views'][$view]['acl']))
		{
			$aclmap = array_merge($aclmap, $configuration['views'][$view]['acl']);
		}

		$acl = $default;

		if (empty($params) || empty($params[0]))
		{
			return $aclmap;
		}

		if (isset($aclmap['*']))
		{
			$acl = $aclmap['*'];
		}

		if (isset($aclmap[$params[0]]))
		{
			$acl = $aclmap[$params[0]];
		}

		return $acl;
	}

	/**
	 * Internal method to return the a configuration option for the view. These
	 * are equivalent to $config array options passed to the Controller
	 *
	 * @param   string   $view           The view for which we will be fetching a task map
	 * @param   array   &$configuration  The configuration parameters hash array
	 * @param   array    $params         Extra options; key 0 defines the option variable we want to fetch
	 * @param   mixed    $default        Default option; null if not defined
	 *
	 * @return  string  The setting for the requested option
	 */
	protected function getConfig($view, &$configuration, $params, $default = null)
	{
		$ret = $default;

		$config = [];

		if (isset($configuration['views']['*']['config']))
		{
			$config = $configuration['views']['*']['config'];
		}

		if (isset($configuration['views'][$view]['config']))
		{
			$config = array_merge($config, $configuration['views'][$view]['config']);
		}

		if (empty($params) || empty($params[0]))
		{
			return $config;
		}

		if (isset($config[$params[0]]))
		{
			$ret = $config[$params[0]];
		}

		return $ret;
	}

	/**
	 * Internal method to return the toolbar infos.
	 *
	 * @param   string   $view           The view for which we will be fetching buttons
	 * @param   array   &$configuration  The configuration parameters hash array
	 * @param   array    $params         Extra options
	 * @param   string   $default        Default option
	 *
	 * @return  string  The toolbar data for this view
	 */
	protected function getToolbar($view, &$configuration, $params, $default = '')
	{
		$toolbar = [];

		if (isset($configuration['views']['*'])
			&& isset($configuration['views']['*']['toolbar'])
			&& isset($configuration['views']['*']['toolbar']['*']))
		{
			$toolbar = $configuration['views']['*']['toolbar']['*'];
		}

		if (isset($configuration['views']['*'])
			&& isset($configuration['views']['*']['toolbar'])
			&& isset($configuration['views']['*']['toolbar'][$params[0]]))
		{
			$toolbar = array_merge($toolbar, $configuration['views']['*']['toolbar'][$params[0]]);
		}

		if (isset($configuration['views'][$view])
			&& isset($configuration['views'][$view]['toolbar'])
			&& isset($configuration['views'][$view]['toolbar']['*']))
		{
			$toolbar = array_merge($toolbar, $configuration['views'][$view]['toolbar']['*']);
		}

		if (isset($configuration['views'][$view])
			&& isset($configuration['views'][$view]['toolbar'])
			&& isset($configuration['views'][$view]['toolbar'][$params[0]]))
		{
			$toolbar = array_merge($toolbar, $configuration['views'][$view]['toolbar'][$params[0]]);
		}

		if (empty($toolbar))
		{
			return $default;
		}

		return $toolbar;
	}
}