| Current Path : /home/w/u/e/wuectly/www/03cbe/ |
| Current File : /home/w/u/e/wuectly/www/03cbe/fof30.tar |
Inflector/Inflector.php 0000604 00000031164 15245551424 0011137 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000016316 15245551424 0011660 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000012237 15245551424 0010300 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000000571 15245551424 0012117 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Encrypt;
defined('_JEXEC') || die;
interface RandvalInterface
{
/**
*
* Returns a cryptographically secure random value.
*
* @return string
*
*/
public function generate();
}
Encrypt/Totp.php 0000604 00000011452 15245551424 0007635 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000011017 15245551424 0007723 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000016270 15245551424 0007422 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000005015 15245551424 0014117 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000010231 15245551424 0012175 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000003450 15245551424 0013763 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000012507 15245551424 0007463 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000012232 15245551424 0010511 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000025571 15245551424 0007732 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000010520 15245551424 0007075 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000002413 15245551424 0010004 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000002521 15245551424 0010003 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000004341 15245551424 0011541 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000032254 15245551424 0007343 0 ustar 00 <?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.php 0000604 00000026027 15245551424 0010605 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000000463 15245551424 0013551 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Download\Exception;
defined('_JEXEC') || die;
use RuntimeException;
class DownloadError extends RuntimeException
{
}
Download/Adapter/Curl.php 0000604 00000015542 15245551424 0011323 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000006547 15245551424 0013467 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000010010 15245551424 0011446 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000005136 15245551424 0012424 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\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.php 0000604 00000004066 15245551424 0010774 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use JLoader;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
abstract class StringHelper
{
/**
* Convert a string into a slug (alias), suitable for use in URLs. Please
* note that transliteration support is rudimentary at this stage.
*
* @param string $value A string to convert to slug
*
* @return string The slug
*
* @deprecated 3.0 Use \JApplicationHelper::stringURLSafe instead
*
* @codeCoverageIgnore
*/
public static function toSlug($value)
{
if (class_exists('\JLog'))
{
Log::add('FOF30\\Utils\\StringHelper::toSlug is deprecated. Use \\JApplicationHelper::stringURLSafe instead', Log::WARNING, 'deprecated');
}
return ApplicationHelper::stringURLSafe($value);
}
/**
* Convert common northern European languages' letters into plain ASCII. This
* is a rudimentary transliteration.
*
* @param string $value The value to convert to ASCII
*
* @return string The converted string
*
* @deprecated 3.0 Use JFactory::getLanguage()->transliterate instead
*
* @codeCoverageIgnore
*/
public static function toASCII($value)
{
if (class_exists('\JLog'))
{
Log::add('FOF30\\Utils\\StringHelper::toASCII is deprecated. Use JFactory::getLanguage()->transliterate instead', Log::WARNING, 'deprecated');
}
$lang = Factory::getLanguage();
return $lang->transliterate($value);
}
/**
* Convert a string to a boolean.
*
* @param string $string The string.
*
* @return boolean The converted string
*/
public static function toBool($string)
{
$string = trim((string) $string);
$string = strtolower($string);
if (in_array($string, [1, 'true', 'yes', 'on', 'enabled'], true))
{
return true;
}
if (in_array($string, [0, 'false', 'no', 'off', 'disabled'], true))
{
return false;
}
return (bool) $string;
}
}
Utils/ComponentVersion.php 0000604 00000006523 15245551424 0011676 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Factory;
use SimpleXMLElement;
/**
* Retrieve the version of a component from the cached XML manifest or, if it's not present, the version recorded in the
* database.
*/
abstract class ComponentVersion
{
/**
* A cache with the version numbers of components
*
* @var array
*
* @since 3.1.5
*/
private static $version = [];
/**
* Get a component's version. The XML manifest on disk will be tried first. If it's not there or does not have a
* version string the manifest cache in the database is tried. If that fails a fake version number will be returned.
*
* @param string $component The name of the component, e.g. com_foobar
*
* @return string The version string
*
* @since 3.1.5
*/
public static function getFor($component)
{
if (!isset(self::$version[$component]))
{
self::$version[$component] = null;
}
if (is_null(self::$version[$component]))
{
self::$version[$component] = self::getVersionFromManifest($component);
}
if (is_null(self::$version[$component]))
{
self::$version[$component] = self::getVersionFromDatabase($component);
}
if (is_null(self::$version[$component]))
{
self::$version[$component] = 'dev-' . str_replace(' ', '_', microtime(false));
}
return self::$version[$component];
}
/**
* Get a component's version from the manifest cache in the database
*
* @param string $component The component's name
*
* @return string The component version or null if none is defined
*
* @since 3.1.5
*/
private static function getVersionFromDatabase($component)
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->qn('manifest_cache'))
->from($db->qn('#__extensions'))
->where($db->qn('element') . ' = ' . $db->q($component))
->where($db->qn('type') . ' = ' . $db->q('component'));
try
{
$json = $db->setQuery($query)->loadResult();
}
catch (Exception $e)
{
return null;
}
if (empty($json))
{
return null;
}
$options = json_decode($json, true);
if (empty($options))
{
return null;
}
if (!isset($options['version']))
{
return null;
}
return $options['version'];
}
/**
* Get a component's version from the manifest file on disk. IMPORTANT! The manifest for com_something must be named
* something.xml.
*
* @param string $component The component's name
*
* @return string The component version or null if none is defined
*
* @since 1.2.0
*/
private static function getVersionFromManifest($component)
{
$bareComponent = str_replace('com_', '', $component);
$file = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . $bareComponent . '.xml';
if (!is_file($file) || !is_readable($file))
{
return null;
}
$data = @file_get_contents($file);
if (empty($data))
{
return null;
}
try
{
$xml = new SimpleXMLElement($data, LIBXML_COMPACT | LIBXML_NONET | LIBXML_ERR_NONE);
}
catch (Exception $e)
{
return null;
}
$versionNode = $xml->xpath('/extension/version');
if (empty($versionNode))
{
return null;
}
return (string) ($versionNode[0]);
}
}
Utils/Phpfunc.php 0000604 00000001424 15245551424 0007764 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
/**
* Intercept calls to PHP functions.
*
* Based on the Session package of Aura for PHP – https://github.com/auraphp/Aura.Session
*
* @method function_exists(string $function)
* @method hash_algos()
*/
class Phpfunc
{
/**
*
* Magic call to intercept any function pass to it.
*
* @param string $func The function to call.
*
* @param array $args Arguments passed to the function.
*
* @return mixed The result of the function call.
*
*/
public function __call($func, $args)
{
return call_user_func_array($func, $args);
}
}
Utils/InstallScript/Plugin.php 0000604 00000017263 15245551424 0012422 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils\InstallScript;
defined('_JEXEC') || die;
use Exception;
use FOF30\Database\Installer;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Log\Log;
// In case FOF's autoloader is not present yet, e.g. new installation
if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true))
{
require_once __DIR__ . '/BaseInstaller.php';
}
/**
* A helper class which you can use to create plugin installation scripts.
*
* Example usage: class PlgSystemExampleInstallerScript extends FOF30\Utils\InstallScript\Module
*
* NB: The class name is always Plg<Plugin Folder><Plugin Name>InstallerScript per Joomla's conventions.
*
* This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
* Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
*/
class Plugin extends BaseInstaller
{
/**
* The plugins's name, e.g. foobar (for plg_system_foobar). Auto-filled from the class name.
*
* @var string
*/
protected $pluginName = '';
/**
* The plugins's folder, e.g. system (for plg_system_foobar). Auto-filled from the class name.
*
* @var string
*/
protected $pluginFolder = '';
/**
* The path where the schema XML files are stored. The path is relative to the folder which contains the extension's
* files.
*
* @var string
*/
protected $schemaXmlPath = 'sql/xml';
/**
* Plugin installer script constructor.
*/
public function __construct()
{
// Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary.
if (empty($this->pluginFolder) || empty($this->pluginName))
{
$class = get_class($this);
$words = preg_replace('/(\s)+/', '_', $class);
$words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words));
$classParts = explode('_', $words);
if (empty($this->pluginFolder))
{
$this->pluginFolder = $classParts[1];
}
if (empty($this->pluginName))
{
$this->pluginName = $classParts[2];
}
}
}
/**
* Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to
* tell Joomla! if it should abort the installation.
*
* @param string $type Installation type (install, update,
* discover_install)
* @param ComponentAdapter $parent Parent object
*
* @return boolean True to let the installation proceed, false to halt the installation
*/
public function preflight($type, $parent)
{
// Check the minimum PHP version
if (!$this->checkPHPVersion())
{
return false;
}
// Check the minimum Joomla! version
if (!$this->checkJoomlaVersion())
{
return false;
}
// Clear op-code caches to prevent any cached code issues
$this->clearOpcodeCaches();
return true;
}
/**
* Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
* or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
* database updates and similar housekeeping functions.
*
* @param string $type install, update or discover_update
* @param ComponentAdapter $parent Parent object
*
* @return void
* @throws Exception
*
*/
public function postflight($type, $parent)
{
/**
* We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
* works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
* uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
* uninstall).
*/
// Add ourselves to the list of extensions depending on FOF30
// $this->addDependency('fof30', $this->getDependencyName());
// Install or update database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
if (@is_dir($schemaPath))
{
$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
$dbInstaller->updateSchema();
}
// Make sure everything is copied properly
$this->bugfixFilesNotCopiedOnUpdate($parent);
// Add post-installation messages on Joomla! 3.2 and later
$this->_applyPostInstallationMessages();
// Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded.
$this->clearOpcodeCaches();
}
/**
* Runs on uninstallation
*
* @param ComponentAdapter $parent The parent object
*/
public function uninstall($parent)
{
// Uninstall database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
// Uninstall database
if (@is_dir($schemaPath))
{
$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
$dbInstaller->removeSchema();
}
// Uninstall post-installation messages on Joomla! 3.2 and later
$this->uninstallPostInstallationMessages();
/**
* We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
* works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
* uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
* uninstall).
*/
// Remove ourselves from the list of extensions depending on FOF30
// $this->removeDependency('fof30', $this->getDependencyName());
}
/**
* Fix for Joomla bug: sometimes files are not copied on update.
*
* We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files /
* folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more
* added / modified files and folders you have. We are trying to work around it by retrying the copy operation
* ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's
* extensions.
*
* @param ComponentAdapter $parent
*/
protected function bugfixFilesNotCopiedOnUpdate($parent)
{
Log::add("Joomla! extension update workaround for $this->pluginFolder plugin $this->pluginName", Log::INFO, 'fof3_extension_installation');
$temporarySource = $parent->getParent()->getPath('source');
$copyMap = [
// Plugin files
$temporarySource => JPATH_ROOT . '/plugins/' . $this->pluginFolder . '/' . $this->pluginName,
// Language (always stored in administrator for plugins)
$temporarySource . '/language' => JPATH_ADMINISTRATOR . '/language',
// Media files, e.g. /media/plg_system_foobar
$temporarySource . '/media' => JPATH_ROOT . '/media/' . $this->getDependencyName(),
];
foreach ($copyMap as $source => $target)
{
Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation');
$ignored = [];
if ($source == $temporarySource)
{
$ignored = [
'index.html', 'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md',
'script.php', 'language', 'media',
];
}
$this->recursiveConditionalCopy($source, $target, $ignored);
}
}
/**
* Get the extension name for FOF dependency tracking, e.g. plg_system_foobar
*
* @return string
*/
protected function getDependencyName()
{
return 'plg_' . strtolower($this->pluginFolder) . '_' . $this->pluginName;
}
}
Utils/InstallScript/BaseInstaller.php 0000604 00000050102 15245551424 0013701 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils\InstallScript;
defined('_JEXEC') || die;
use DirectoryIterator;
use Exception;
use FOFTemplateUtils;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;
class BaseInstaller
{
/**
* The minimum PHP version required to install this extension
*
* @var string
*/
protected $minimumPHPVersion = '7.2.0';
/**
* The minimum Joomla! version required to install this extension
*
* @var string
*/
protected $minimumJoomlaVersion = '3.3.0';
/**
* The maximum Joomla! version this extension can be installed on
*
* @var string
*/
protected $maximumJoomlaVersion = '4.0.99';
/**
* Post-installation message definitions for Joomla! 3.2 or later.
*
* This array contains the message definitions for the Post-installation Messages component added in Joomla! 3.2 and
* later versions. Each element is also a hashed array. For the keys used in these message definitions please see
* addPostInstallationMessage
*
* @var array
*/
protected $postInstallationMessages = [];
/**
* Recursively copy a bunch of files, but only if the source and target file have a different size.
*
* @param string $source Path to copy FROM
* @param string $dest Path to copy TO
* @param array $ignored List of entries to ignore (first level entries are taken into account)
*
* @return void
*/
protected function recursiveConditionalCopy($source, $dest, $ignored = [])
{
// Make sure source and destination exist
if (!@is_dir($source))
{
return;
}
if (!@is_dir($dest))
{
if (!@mkdir($dest, 0755))
{
Folder::create($dest, 0755);
}
}
if (!@is_dir($dest))
{
$this->log(__CLASS__ . ": Cannot create folder $dest");
return;
}
// List the contents of the source folder
try
{
$di = new DirectoryIterator($source);
}
catch (Exception $e)
{
return;
}
// Process each entry
foreach ($di as $entry)
{
// Ignore dot dirs (. and ..)
if ($entry->isDot())
{
continue;
}
$sourcePath = $entry->getPathname();
$fileName = $entry->getFilename();
// Do not copy ignored files
if (!empty($ignored) && in_array($fileName, $ignored))
{
continue;
}
// If it's a directory do a recursive copy
if ($entry->isDir())
{
$this->recursiveConditionalCopy($sourcePath, $dest . DIRECTORY_SEPARATOR . $fileName);
continue;
}
// If it's a file check if it's missing or identical
$mustCopy = false;
$targetPath = $dest . DIRECTORY_SEPARATOR . $fileName;
if (!@is_file($targetPath))
{
$mustCopy = true;
}
else
{
$sourceSize = @filesize($sourcePath);
$targetSize = @filesize($targetPath);
$mustCopy = $sourceSize != $targetSize;
}
if (!$mustCopy)
{
continue;
}
if (!@copy($sourcePath, $targetPath))
{
if (!File::copy($sourcePath, $targetPath))
{
$this->log(__CLASS__ . ": Cannot copy $sourcePath to $targetPath");
}
}
}
}
/**
* Try to log a warning / error with Joomla
*
* @param string $message The message to write to the log
* @param bool $error Is this an error? If not, it's a warning. (default: false)
* @param string $category Log category, default jerror
*
* @return void
*/
protected function log($message, $error = false, $category = 'jerror')
{
// Just in case...
if (!class_exists('JLog', true))
{
return;
}
$priority = $error ? Log::ERROR : Log::WARNING;
try
{
Log::add($message, $priority, $category);
}
catch (Exception $e)
{
// Swallow the exception.
}
}
/**
* Check that the server meets the minimum PHP version requirements.
*
* @return bool
*/
protected function checkPHPVersion()
{
if (!empty($this->minimumPHPVersion))
{
if (defined('PHP_VERSION'))
{
$version = PHP_VERSION;
}
elseif (function_exists('phpversion'))
{
$version = phpversion();
}
else
{
$version = '5.0.0'; // all bets are off!
}
if (!version_compare($version, $this->minimumPHPVersion, 'ge'))
{
$msg = "<p>You need PHP $this->minimumPHPVersion or later to install this extension</p>";
$this->log($msg);
return false;
}
}
return true;
}
/**
* Check the minimum and maximum Joomla! versions for this extension
*
* @return bool
*/
protected function checkJoomlaVersion()
{
if (!empty($this->minimumJoomlaVersion) && !version_compare(JVERSION, $this->minimumJoomlaVersion, 'ge'))
{
$msg = "<p>You need Joomla! $this->minimumJoomlaVersion or later to install this extension</p>";
$this->log($msg);
return false;
}
// Check the maximum Joomla! version
if (!empty($this->maximumJoomlaVersion) && !version_compare(JVERSION, $this->maximumJoomlaVersion, 'le'))
{
$msg = "<p>You need Joomla! $this->maximumJoomlaVersion or earlier to install this extension</p>";
$this->log($msg);
return false;
}
return true;
}
/**
* Clear PHP opcode caches
*
* @return void
*/
protected function clearOpcodeCaches()
{
// Always reset the OPcache if it's enabled. Otherwise there's a good chance the server will not know we are
// replacing .php scripts. This is a major concern since PHP 5.5 included and enabled OPcache by default.
if (function_exists('opcache_reset'))
{
opcache_reset();
}
// Also do that for APC cache
elseif (function_exists('apc_clear_cache'))
{
@apc_clear_cache();
}
}
/**
* Get the dependencies for a package from the #__akeeba_common table
*
* @param string $package The package
*
* @return array The dependencies
*/
protected function getDependencies($package)
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->qn('value'))
->from($db->qn('#__akeeba_common'))
->where($db->qn('key') . ' = ' . $db->q($package));
try
{
$dependencies = $db->setQuery($query)->loadResult();
$dependencies = json_decode($dependencies, true);
if (empty($dependencies))
{
$dependencies = [];
}
}
catch (Exception $e)
{
$dependencies = [];
}
return $dependencies;
}
/**
* Sets the dependencies for a package into the #__akeeba_common table
*
* @param string $package The package
* @param array $dependencies The dependencies list
*/
protected function setDependencies($package, array $dependencies)
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->delete('#__akeeba_common')
->where($db->qn('key') . ' = ' . $db->q($package));
try
{
$db->setQuery($query)->execute();
}
catch (Exception $e)
{
// Do nothing if the old key wasn't found
}
$object = (object) [
'key' => $package,
'value' => json_encode($dependencies),
];
try
{
$db->insertObject('#__akeeba_common', $object, 'key');
}
catch (Exception $e)
{
// Do nothing if the old key wasn't found
}
}
/**
* Adds a package dependency to #__akeeba_common
*
* @param string $package The package
* @param string $dependency The dependency to add
*/
protected function addDependency($package, $dependency)
{
$dependencies = $this->getDependencies($package);
if (!in_array($dependency, $dependencies))
{
$dependencies[] = $dependency;
$this->setDependencies($package, $dependencies);
}
}
/**
* Removes a package dependency from #__akeeba_common
*
* @param string $package The package
* @param string $dependency The dependency to remove
*/
protected function removeDependency($package, $dependency)
{
$dependencies = $this->getDependencies($package);
if (in_array($dependency, $dependencies))
{
$index = array_search($dependency, $dependencies);
unset($dependencies[$index]);
$this->setDependencies($package, $dependencies);
}
}
/**
* Do I have a dependency for a package in #__akeeba_common
*
* @param string $package The package
* @param string $dependency The dependency to check for
*
* @return bool
*/
protected function hasDependency($package, $dependency)
{
$dependencies = $this->getDependencies($package);
return in_array($dependency, $dependencies);
}
/**
* Adds or updates a post-installation message (PIM) definition for Joomla! 3.2 or later. You can use this in your
* post-installation script using this code:
*
* The $options array contains the following mandatory keys:
*
* extension_id The numeric ID of the extension this message is for (see the #__extensions table)
*
* type One of message, link or action. Their meaning is:
* message Informative message. The user can dismiss it.
* link The action button links to a URL. The URL is defined in the action parameter.
* action A PHP action takes place when the action button is clicked. You need to specify the
* action_file (RAD path to the PHP file) and action (PHP function name) keys. See
* below for more information.
*
* title_key The JText language key for the title of this PIM
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE
*
* description_key The JText language key for the main body (description) of this PIM
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION
*
* action_key The JText language key for the action button. Ignored and not required when type=message
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION
*
* language_extension The extension name which holds the language keys used above. For example, com_foobar,
* mod_something, plg_system_whatever, tpl_mytemplate
*
* language_client_id Should we load the front-end (0) or back-end (1) language keys?
*
* version_introduced Which was the version of your extension where this message appeared for the first time?
* Example: 3.2.1
*
* enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1.
*
* condition_file The RAD path to a PHP file containing a PHP function which determines whether this message
* should be shown to the user. @param array $options See description
*
* @return void
*
* @throws Exception
* @see Template::parsePath() for RAD path format. Joomla! will include this file
* before calling the function defined in the action key below.
* Example: admin://components/com_foobar/helpers/postinstall.php
*
* action The name of a PHP function which will be used to run the action of this PIM. This must be
* a
* simple PHP user function (not a class method, static method etc) which returns no result.
* Example: com_foobar_postinstall_messageone_action
*
* @see Template::parsePath() for RAD path format. Joomla!
* will include this file before calling the condition_method.
* Example: admin://components/com_foobar/helpers/postinstall.php
*
* condition_method The name of a PHP function which will be used to determine whether to show this message to
* the user. This must be a simple PHP user function (not a class method, static method etc)
* which returns true to show the message and false to hide it. This function is defined in
* the condition_file. Example: com_foobar_postinstall_messageone_condition
*
* When type=message no additional keys are required.
*
* When type=link the following additional keys are required:
*
* action The URL which will open when the user clicks on the PIM's action button
* Example: index.php?option=com_foobar&view=tools&task=installSampleData
*
* Then type=action the following additional keys are required:
*
* action_file The RAD path to a PHP file containing a PHP function which performs the action of this
* PIM.
*
*/
protected function addPostInstallationMessage(array $options)
{
// Make sure there are options set
if (!is_array($options))
{
throw new Exception('Post-installation message definitions must be of type array', 500);
}
// Initialise array keys
$defaultOptions = [
'extension_id' => '',
'type' => '',
'title_key' => '',
'description_key' => '',
'action_key' => '',
'language_extension' => '',
'language_client_id' => '',
'action_file' => '',
'action' => '',
'condition_file' => '',
'condition_method' => '',
'version_introduced' => '',
'enabled' => '1',
];
$options = array_merge($defaultOptions, $options);
// Array normalisation. Removes array keys not belonging to a definition.
$defaultKeys = array_keys($defaultOptions);
$allKeys = array_keys($options);
$extraKeys = array_diff($allKeys, $defaultKeys);
if (!empty($extraKeys))
{
foreach ($extraKeys as $key)
{
unset($options[$key]);
}
}
// Normalisation of integer values
$options['extension_id'] = (int) $options['extension_id'];
$options['language_client_id'] = (int) $options['language_client_id'];
$options['enabled'] = (int) $options['enabled'];
// Normalisation of 0/1 values
foreach (['language_client_id', 'enabled'] as $key)
{
$options[$key] = $options[$key] ? 1 : 0;
}
// Make sure there's an extension_id
if (!(int) $options['extension_id'])
{
throw new Exception('Post-installation message definitions need an extension_id', 500);
}
// Make sure there's a valid type
if (!in_array($options['type'], ['message', 'link', 'action']))
{
throw new Exception('Post-installation message definitions need to declare a type of message, link or action', 500);
}
// Make sure there's a title key
if (empty($options['title_key']))
{
throw new Exception('Post-installation message definitions need a title key', 500);
}
// Make sure there's a description key
if (empty($options['description_key']))
{
throw new Exception('Post-installation message definitions need a description key', 500);
}
// If the type is anything other than message you need an action key
if (($options['type'] != 'message') && empty($options['action_key']))
{
throw new Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500);
}
// You must specify the language extension
if (empty($options['language_extension']))
{
throw new Exception('Post-installation message definitions need to specify which extension contains their language keys', 500);
}
// The action file and method are only required for the "action" type
if ($options['type'] == 'action')
{
if (empty($options['action_file']))
{
throw new Exception('Post-installation message definitions need an action file when they are of type "action"', 500);
}
$file_path = FOFTemplateUtils::parsePath($options['action_file'], true);
if (!@is_file($file_path))
{
throw new Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500);
}
if (empty($options['action']))
{
throw new Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500);
}
}
if ($options['type'] == 'link')
{
if (empty($options['link']))
{
throw new Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500);
}
}
// The condition file and method are only required when the type is not "message"
if ($options['type'] != 'message')
{
if (empty($options['condition_file']))
{
throw new Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500);
}
$file_path = FOFTemplateUtils::parsePath($options['condition_file'], true);
if (!@is_file($file_path))
{
throw new Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500);
}
if (empty($options['condition_method']))
{
throw new Exception('Post-installation message definitions need a condition method (function name) when they are of type "' . $options['type'] . '"', 500);
}
}
// Check if the definition exists
$tableName = '#__postinstall_messages';
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->qn($tableName))
->where($db->qn('extension_id') . ' = ' . $db->q($options['extension_id']))
->where($db->qn('type') . ' = ' . $db->q($options['type']))
->where($db->qn('title_key') . ' = ' . $db->q($options['title_key']));
$existingRow = $db->setQuery($query)->loadAssoc();
// Is the existing definition the same as the one we're trying to save (ignore the enabled flag)?
if (!empty($existingRow))
{
$same = true;
foreach ($options as $k => $v)
{
if ($k == 'enabled')
{
continue;
}
if ($existingRow[$k] != $v)
{
$same = false;
break;
}
}
// Trying to add the same row as the existing one; quit
if ($same)
{
return;
}
// Otherwise it's not the same row. Remove the old row before insert a new one.
$query = $db->getQuery(true)
->delete($db->qn($tableName))
->where($db->q('extension_id') . ' = ' . $db->q($options['extension_id']))
->where($db->q('type') . ' = ' . $db->q($options['type']))
->where($db->q('title_key') . ' = ' . $db->q($options['title_key']));
$db->setQuery($query)->execute();
}
// Insert the new row
$options = (object) $options;
$db->insertObject($tableName, $options);
}
/**
* Applies the post-installation messages for Joomla! 3.2 or later
*
* @return void
*/
protected function _applyPostInstallationMessages()
{
// Make sure it's Joomla! 3.2.0 or later
if (!version_compare(JVERSION, '3.2.0', 'ge'))
{
return;
}
// Make sure there are post-installation messages
if (empty($this->postInstallationMessages))
{
return;
}
// Get the extension ID for our component
$db = Factory::getDbo();
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
try
{
$ids = $db->loadColumn();
}
catch (Exception $exc)
{
return;
}
if (empty($ids))
{
return;
}
$extension_id = array_shift($ids);
foreach ($this->postInstallationMessages as $message)
{
$message['extension_id'] = $extension_id;
$this->addPostInstallationMessage($message);
}
}
/**
* Uninstalls the post-installation messages for Joomla! 3.2 or later
*
* @return void
*/
protected function uninstallPostInstallationMessages()
{
// Make sure it's Joomla! 3.2.0 or later
if (!version_compare(JVERSION, '3.2.0', 'ge'))
{
return;
}
// Make sure there are post-installation messages
if (empty($this->postInstallationMessages))
{
return;
}
// Get the extension ID for our component
$db = Factory::getDbo();
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
try
{
$ids = $db->loadColumn();
}
catch (Exception $exc)
{
return;
}
if (empty($ids))
{
return;
}
$extension_id = array_shift($ids);
$query = $db->getQuery(true)
->delete($db->qn('#__postinstall_messages'))
->where($db->qn('extension_id') . ' = ' . $db->q($extension_id));
try
{
$db->setQuery($query)->execute();
}
catch (Exception $e)
{
return;
}
}
}
Utils/InstallScript/Module.php 0000604 00000016574 15245551424 0012415 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils\InstallScript;
defined('_JEXEC') || die;
use Exception;
use FOF30\Database\Installer;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Log\Log;
// In case FOF's autoloader is not present yet, e.g. new installation
if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true))
{
require_once __DIR__ . '/BaseInstaller.php';
}
/**
* A helper class which you can use to create module installation scripts.
*
* Example usage: class Mod_ExampleInstallerScript extends FOF30\Utils\InstallScript\Module
*
* This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
* Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
*/
class Module extends BaseInstaller
{
/**
* Which side of the site is this module installed in? Use 'site' or 'administrator'.
*
* @var string
*/
protected $moduleClient = 'site';
/**
* The modules's name, e.g. mod_foobar. Auto-filled from the class name.
*
* @var string
*/
protected $moduleName = '';
/**
* The path where the schema XML files are stored. The path is relative to the folder which contains the extension's
* files.
*
* @var string
*/
protected $schemaXmlPath = 'sql/xml';
/**
* Module installer script constructor.
*/
public function __construct()
{
// Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary.
if (empty($this->moduleName))
{
$class = get_class($this);
$words = preg_replace('/(\s)+/', '_', $class);
$words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words));
$classParts = explode('_', $words);
$this->moduleName = 'mod_' . $classParts[2];
}
}
/**
* Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to
* tell Joomla! if it should abort the installation.
*
* @param string $type Installation type (install, update,
* discover_install)
* @param ComponentAdapter $parent Parent object
*
* @return boolean True to let the installation proceed, false to halt the installation
*/
public function preflight($type, $parent)
{
// Check the minimum PHP version
if (!$this->checkPHPVersion())
{
return false;
}
// Check the minimum Joomla! version
if (!$this->checkJoomlaVersion())
{
return false;
}
// Clear op-code caches to prevent any cached code issues
$this->clearOpcodeCaches();
return true;
}
/**
* Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
* or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
* database updates and similar housekeeping functions.
*
* @param string $type install, update or discover_update
* @param ComponentAdapter $parent Parent object
*
* @return void
* @throws Exception
*
*/
public function postflight($type, $parent)
{
/**
* We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
* works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
* uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
* uninstall).
*/
// Add ourselves to the list of extensions depending on FOF30
// $this->addDependency('fof30', $this->getDependencyName());
// Install or update database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
if (@is_dir($schemaPath))
{
$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
$dbInstaller->updateSchema();
}
// Make sure everything is copied properly
$this->bugfixFilesNotCopiedOnUpdate($parent);
// Add post-installation messages on Joomla! 3.2 and later
$this->_applyPostInstallationMessages();
// Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded.
$this->clearOpcodeCaches();
}
/**
* Runs on uninstallation
*
* @param ComponentAdapter $parent The parent object
*/
public function uninstall($parent)
{
// Uninstall database
$schemaPath = $parent->getParent()->getPath('source') . '/' . $this->schemaXmlPath;
// Uninstall database
if (@is_dir($schemaPath))
{
$dbInstaller = new Installer(Factory::getDbo(), $schemaPath);
$dbInstaller->removeSchema();
}
// Uninstall post-installation messages on Joomla! 3.2 and later
$this->uninstallPostInstallationMessages();
/**
* We are not doing dependency tracking for modules and plugins because of the way Joomla package uninstallation
* works. FOF's uninstall() method would get called before the extensions are uninstalled, therefore its
* uninstallation would fail and make the entire package uninstallation to fail (the package is impossible to
* uninstall).
*/
// Remove ourselves from the list of extensions depending on FOF30
// $this->removeDependency('fof30', $this->getDependencyName());
}
/**
* Fix for Joomla bug: sometimes files are not copied on update.
*
* We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files /
* folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more
* added / modified files and folders you have. We are trying to work around it by retrying the copy operation
* ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's
* extensions.
*
* @param ComponentAdapter $parent
*/
protected function bugfixFilesNotCopiedOnUpdate($parent)
{
Log::add("Joomla! extension update workaround for $this->moduleClient module $this->moduleName", Log::INFO, 'fof3_extension_installation');
$temporarySource = $parent->getParent()->getPath('source');
$rootFolder = ($this->moduleClient == 'site') ? JPATH_SITE : JPATH_ADMINISTRATOR;
$copyMap = [
// Module files
$temporarySource => $rootFolder . '/modules/' . $this->moduleName,
// Language
$temporarySource . '/language' => $rootFolder . '/language',
// Media files
$temporarySource . '/media' => JPATH_ROOT . '/media/' . $this->moduleName,
];
foreach ($copyMap as $source => $target)
{
Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation');
$ignored = [];
if ($source == $temporarySource)
{
$ignored = [
'index.html', 'index.htm', 'LICENSE.txt', 'license.txt', 'readme.htm', 'readme.html', 'README.md',
'script.php', 'language', 'media',
];
}
$this->recursiveConditionalCopy($source, $target, $ignored);
}
}
/**
* Get the extension name for FOF dependency tracking, e.g. mod_site_foobar
*
* @return string
*/
protected function getDependencyName()
{
return 'mod_' . strtolower($this->moduleClient) . '_' . substr($this->moduleName, 4);
}
}
Utils/InstallScript/Component.php 0000604 00000104610 15245551424 0013117 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils\InstallScript;
defined('_JEXEC') || die;
use Exception;
use FOF30\Database\Installer as DatabaseInstaller;
use InvalidArgumentException;
use JDatabaseDriver;
use JError;
use JLoader;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Table\Menu;
use Joomla\CMS\Table\Table;
use SimpleXMLElement;
use stdClass;
// In case FOF's autoloader is not present yet, e.g. new installation
if (!class_exists('FOF30\\Utils\\InstallScript\\BaseInstaller', true))
{
require_once __DIR__ . '/BaseInstaller.php';
}
/**
* A helper class which you can use to create component installation scripts.
*
* Example usage: class Com_ExampleInstallerScript extends FOF30\Utils\InstallScript\Component
*
* This namespace contains more classes for creating installation scripts for other kinds of Joomla! extensions as well.
* Do keep in mind that only components, modules and plugins could have post-installation scripts before Joomla! 3.3.
*/
class Component extends BaseInstaller
{
/**
* The component's name. Auto-filled from the class name.
*
* @var string
*/
protected $componentName = '';
/**
* The title of the component (printed on installation and uninstallation messages)
*
* @var string
*/
protected $componentTitle = 'Foobar Component';
/**
* The list of obsolete extra modules and plugins to uninstall on component upgrade / installation.
*
* @var array
*/
protected $uninstallation_queue = [
// modules => { (folder) => { (module) }* }*
'modules' => [
'admin' => [],
'site' => [],
],
// plugins => { (folder) => { (element) }* }*
'plugins' => [
'system' => [],
],
];
/**
* Obsolete files and folders to remove from the free version only. This is used when you move a feature from the
* free version of your extension to its paid version. If you don't have such a distinction you can ignore this.
*
* @var array
*/
protected $removeFilesFree = [
'files' => [
// Use pathnames relative to your site's root, e.g.
// 'administrator/components/com_foobar/helpers/whatever.php'
],
'folders' => [
// Use pathnames relative to your site's root, e.g.
// 'administrator/components/com_foobar/baz'
],
];
/**
* Obsolete files and folders to remove from both paid and free releases. This is used when you refactor code and
* some files inevitably become obsolete and need to be removed.
*
* @var array
*/
protected $removeFilesAllVersions = [
'files' => [
// Use pathnames relative to your site's root, e.g.
// 'administrator/components/com_foobar/helpers/whatever.php'
],
'folders' => [
// Use pathnames relative to your site's root, e.g.
// 'administrator/components/com_foobar/baz'
],
];
/**
* A list of scripts to be copied to the "cli" directory of the site
*
* @var array
*/
protected $cliScriptFiles = [
// Use just the filename, e.g.
// 'my-cron-script.php'
];
/**
* The path inside your package where cli scripts are stored
*
* @var string
*/
protected $cliSourcePath = 'cli';
/**
* Is the schemaXmlPath class variable a relative path? If set to true the schemaXmlPath variable contains a path
* relative to the component's back-end directory. If set to false the schemaXmlPath variable contains an absolute
* filesystem path.
*
* @var boolean
*/
protected $schemaXmlPathRelative = true;
/**
* The path where the schema XML files are stored. Its contents depend on the schemaXmlPathRelative variable above
* true => schemaXmlPath contains a path relative to the component's back-end directory
* false => schemaXmlPath contains an absolute filesystem path
*
* @var string
*/
protected $schemaXmlPath = 'sql/xml';
/**
* Is this the paid version of the extension? This only determines which files / extensions will be removed.
*
* @var boolean
*/
protected $isPaid = false;
/**
* Module installer script constructor.
*/
public function __construct()
{
// Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary.
if (empty($this->componentName))
{
$class = get_class($this);
$words = preg_replace('/(\s)+/', '_', $class);
$words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words));
$classParts = explode('_', $words);
$this->componentName = 'com_' . $classParts[2];
}
}
/**
* Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to
* tell Joomla! if it should abort the installation.
*
* @param string $type Installation type (install, update,
* discover_install)
* @param ComponentAdapter $parent Parent object
*
* @return boolean True to let the installation proceed, false to halt the installation
*/
public function preflight($type, $parent)
{
// Check the minimum PHP version
if (!$this->checkPHPVersion())
{
return false;
}
// Check the minimum Joomla! version
if (!$this->checkJoomlaVersion())
{
return false;
}
// Clear op-code caches to prevent any cached code issues
$this->clearOpcodeCaches();
// Workarounds for JInstaller issues.
if (in_array($type, ['install', 'discover_install']))
{
// Bug fix for "Database function returned no error"
$this->bugfixDBFunctionReturnedNoError();
}
else
{
// Bug fix for "Can not build admin menus"
$this->bugfixCantBuildAdminMenus();
}
return true;
}
/**
* Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing
* or updating your component. This is the last chance you've got to perform any additional installations, clean-up,
* database updates and similar housekeeping functions.
*
* @param string $type install, update or discover_update
* @param ComponentAdapter $parent Parent object
*
* @return void
* @throws Exception
*
*/
public function postflight($type, $parent)
{
// Add ourselves to the list of extensions depending on FOF30
$this->addDependency('fof30', $this->componentName);
// Install or update database
$dbInstaller = new DatabaseInstaller(Factory::getDbo(),
($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' .
$this->schemaXmlPath
);
$dbInstaller->updateSchema();
// These workarounds are only needed, and only work, on Joomla! 3.x
if (strpos(JVERSION, '3.') === 0)
{
// Make sure menu items are installed
$this->_createAdminMenus($parent);
// Make sure menu items are published
$this->_reallyPublishAdminMenuItems($parent);
}
// Which files should I remove?
if ($this->isPaid)
{
// This is the paid version, only remove the removeFilesAllVersions files
$removeFiles = $this->removeFilesAllVersions;
}
else
{
// This is the free version, remove the removeFilesAllVersions and removeFilesFree files
$removeFiles = ['files' => [], 'folders' => []];
if (isset($this->removeFilesAllVersions['files']))
{
if (isset($this->removeFilesFree['files']))
{
$removeFiles['files'] = array_merge($this->removeFilesAllVersions['files'], $this->removeFilesFree['files']);
}
else
{
$removeFiles['files'] = $this->removeFilesAllVersions['files'];
}
}
elseif (isset($this->removeFilesFree['files']))
{
$removeFiles['files'] = $this->removeFilesFree['files'];
}
if (isset($this->removeFilesAllVersions['folders']))
{
if (isset($this->removeFilesFree['folders']))
{
$removeFiles['folders'] = array_merge($this->removeFilesAllVersions['folders'], $this->removeFilesFree['folders']);
}
else
{
$removeFiles['folders'] = $this->removeFilesAllVersions['folders'];
}
}
elseif (isset($this->removeFilesFree['folders']))
{
$removeFiles['folders'] = $this->removeFilesFree['folders'];
}
}
// Remove obsolete files and folders
$this->removeFilesAndFolders($removeFiles);
// Make sure everything is copied properly
$this->bugfixFilesNotCopiedOnUpdate($parent);
// Copy the CLI files (if any)
$this->copyCliFiles($parent);
// Show the post-installation page
$this->renderPostInstallation($parent);
// Uninstall obsolete sub-extensions
$this->uninstallObsoleteSubextensions($parent);
// Clear the FOF cache
$false = false;
$cache = Factory::getCache('fof', '');
$cache->store($false, 'cache', 'fof');
// Make sure the Joomla! menu structure is correct
$this->_rebuildMenu();
// Add post-installation messages on Joomla! 3.2 and later
$this->_applyPostInstallationMessages();
// Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded.
$this->clearOpcodeCaches();
}
/**
* Runs on uninstallation
*
* @param ComponentAdapter $parent The parent object
*/
public function uninstall($parent)
{
// Uninstall database
$dbInstaller = new DatabaseInstaller(Factory::getDbo(),
($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' .
$this->schemaXmlPath
);
$dbInstaller->removeSchema();
// Uninstall post-installation messages on Joomla! 3.2 and later
$this->uninstallPostInstallationMessages();
// Remove ourselves from the list of extensions depending on FOF30
$this->removeDependency('fof30', $this->componentName);
// Show the post-uninstallation page
$this->renderPostUninstallation($parent);
}
/**
* Copies the CLI scripts into Joomla!'s cli directory
*
* @param ComponentAdapter $parent
*/
protected function copyCliFiles($parent)
{
$src = $parent->getParent()->getPath('source');
foreach ($this->cliScriptFiles as $script)
{
if (is_file(JPATH_ROOT . '/cli/' . $script))
{
File::delete(JPATH_ROOT . '/cli/' . $script);
}
if (is_file($src . '/' . $this->cliSourcePath . '/' . $script))
{
File::copy($src . '/' . $this->cliSourcePath . '/' . $script, JPATH_ROOT . '/cli/' . $script);
}
}
}
/**
* Fix for Joomla bug: sometimes files are not copied on update.
*
* We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files /
* folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more
* added / modified files and folders you have. We are trying to work around it by retrying the copy operation
* ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's
* extensions.
*
* @param ComponentAdapter $parent
*/
protected function bugfixFilesNotCopiedOnUpdate($parent)
{
Log::add("Joomla! extension update workaround for component $this->componentName", Log::INFO, 'fof3_extension_installation');
$temporarySource = $parent->getParent()->getPath('source');
$copyMap = [
// Backend component files
'backend' => JPATH_ADMINISTRATOR . '/components/' . $this->componentName,
'admin' => JPATH_ADMINISTRATOR . '/components/' . $this->componentName,
// Frontend component files
'frontend' => JPATH_SITE . '/components/' . $this->componentName,
'site' => JPATH_SITE . '/components/' . $this->componentName,
// Backend language
'language/backend' => JPATH_ADMINISTRATOR . '/language',
'language/admin' => JPATH_ADMINISTRATOR . '/language',
// Frontend language
'language/frontend' => JPATH_SITE . '/language',
'language/site' => JPATH_SITE . '/language',
// Media files
'media' => JPATH_ROOT . '/media/' . $this->componentName,
];
foreach ($copyMap as $partialSource => $target)
{
$source = $temporarySource . '/' . $partialSource;
Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation');
$this->recursiveConditionalCopy($source, $target);
}
}
/**
* Override this method to display a custom component installation message if you so wish
*
* @param ComponentAdapter $parent Parent class calling us
*/
protected function renderPostInstallation($parent)
{
echo "<h3>$this->componentName has been installed</h3>";
}
/**
* Override this method to display a custom component uninstallation message if you so wish
*
* @param ComponentAdapter $parent Parent class calling us
*/
protected function renderPostUninstallation($parent)
{
echo "<h3>$this->componentName has been uninstalled</h3>";
}
/**
* Bug fix for "DB function returned no error"
*/
protected function bugfixDBFunctionReturnedNoError()
{
$db = Factory::getDbo();
try
{
// Fix broken #__assets records
$this->deleteComponentAssetRecords($db);
// Fix broken #__extensions records
$this->deleteComponentExtensionRecord($db);
/**
* Fix broken #__menu records
*
* Only run on Joomla! versions lower than 3.7. Joomla! 3.7 introduced a backend menu manager which
* lets the user create missing menu items. Moreover, it lets them create custom links to the component
* which means that our menu deleting code would break them! So we don't run this code in newer Joomla!
* versions any more.
*/
if (version_compare(JVERSION, '3.6.9999', 'le'))
{
$this->deleteComponentMenuRecord($db);
}
}
catch (Exception $exc)
{
return;
}
}
/**
* Joomla! 1.6+ bug fix for "Can not build admin menus"
*/
protected function bugfixCantBuildAdminMenus()
{
$db = Factory::getDbo();
// If there are multiple #__extensions record, keep one of them
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
try
{
$ids = $db->loadColumn();
}
catch (Exception $exc)
{
return;
}
if ((is_array($ids) || $ids instanceof \Countable ? count($ids) : 0) > 1)
{
asort($ids);
$extension_id = array_shift($ids); // Keep the oldest id
foreach ($ids as $id)
{
$query = $db->getQuery(true);
$query->delete('#__extensions')
->where($db->qn('extension_id') . ' = ' . $db->q($id));
$db->setQuery($query);
try
{
$db->execute();
}
catch (Exception $exc)
{
// Nothing
}
}
}
// If there are multiple assets records, delete all except the oldest one
$query = $db->getQuery(true);
$query->select('id')
->from('#__assets')
->where($db->qn('name') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
$ids = $db->loadObjectList();
if ((is_array($ids) || $ids instanceof \Countable ? count($ids) : 0) > 1)
{
asort($ids);
$asset_id = array_shift($ids); // Keep the oldest id
foreach ($ids as $id)
{
$query = $db->getQuery(true);
$query->delete('#__assets')
->where($db->qn('id') . ' = ' . $db->q($id));
$db->setQuery($query);
try
{
$db->execute();
}
catch (Exception $exc)
{
// Nothing
}
}
}
// Remove #__menu records for good measure! –– I think this is not necessary and causes the menu item to
// disappear on extension update.
/**
* $query = $db->getQuery(true);
* $query->select('id')
* ->from('#__menu')
* ->where($db->qn('type') . ' = ' . $db->q('component'))
* ->where($db->qn('menutype') . ' = ' . $db->q('main'))
* ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName));
* $db->setQuery($query);
*
* try
* {
* $ids1 = $db->loadColumn();
* }
* catch (Exception $exc)
* {
* $ids1 = array();
* }
*
* if (empty($ids1))
* {
* $ids1 = array();
* }
*
* $query = $db->getQuery(true);
* $query->select('id')
* ->from('#__menu')
* ->where($db->qn('type') . ' = ' . $db->q('component'))
* ->where($db->qn('menutype') . ' = ' . $db->q('main'))
* ->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName . '&%'));
* $db->setQuery($query);
*
* try
* {
* $ids2 = $db->loadColumn();
* }
* catch (Exception $exc)
* {
* $ids2 = array();
* }
*
* if (empty($ids2))
* {
* $ids2 = array();
* }
*
* $ids = array_merge($ids1, $ids2);
*
* if (!empty($ids))
* {
* foreach ($ids as $id)
* {
* $query = $db->getQuery(true);
* $query->delete('#__menu')
* ->where($db->qn('id') . ' = ' . $db->q($id));
* $db->setQuery($query);
*
* try
* {
* $db->execute();
* }
* catch (Exception $exc)
* {
* // Nothing
* }
* }
* }
* /**/
}
/**
* Removes obsolete files and folders
*
* @param array $removeList The files and directories to remove
*/
protected function removeFilesAndFolders($removeList)
{
// Remove files
if (isset($removeList['files']) && !empty($removeList['files']))
{
foreach ($removeList['files'] as $file)
{
$f = JPATH_ROOT . '/' . $file;
if (!is_file($f))
{
continue;
}
File::delete($f);
}
}
// Remove folders
if (isset($removeList['folders']) && !empty($removeList['folders']))
{
foreach ($removeList['folders'] as $folder)
{
$f = JPATH_ROOT . '/' . $folder;
if (!is_dir($f))
{
continue;
}
Folder::delete($f);
}
}
}
/**
* Uninstalls obsolete sub-extensions (modules, plugins) bundled with the main extension
*
* @param ComponentAdapter $parent The parent object
*
* @return stdClass The sub-extension uninstallation status
*/
protected function uninstallObsoleteSubextensions($parent)
{
$db = Factory::getDBO();
$status = new stdClass();
$status->modules = [];
$status->plugins = [];
// Modules uninstallation
if (isset($this->uninstallation_queue['modules']) && count($this->uninstallation_queue['modules']))
{
foreach ($this->uninstallation_queue['modules'] as $folder => $modules)
{
if (is_array($modules) || $modules instanceof \Countable ? count($modules) : 0)
{
foreach ($modules as $module)
{
// Find the module ID
$sql = $db->getQuery(true)
->select($db->qn('extension_id'))
->from($db->qn('#__extensions'))
->where($db->qn('element') . ' = ' . $db->q('mod_' . $module))
->where($db->qn('type') . ' = ' . $db->q('module'));
$db->setQuery($sql);
$id = $db->loadResult();
// Uninstall the module
if ($id)
{
$installer = new Installer;
$result = $installer->uninstall('module', $id, 1);
$status->modules[] = [
'name' => 'mod_' . $module,
'client' => $folder,
'result' => $result,
];
}
}
}
}
}
// Plugins uninstallation
if (isset($this->uninstallation_queue['plugins']) && count($this->uninstallation_queue['plugins']))
{
foreach ($this->uninstallation_queue['plugins'] as $folder => $plugins)
{
if (is_array($plugins) || $plugins instanceof \Countable ? count($plugins) : 0)
{
foreach ($plugins as $plugin)
{
$sql = $db->getQuery(true)
->select($db->qn('extension_id'))
->from($db->qn('#__extensions'))
->where($db->qn('type') . ' = ' . $db->q('plugin'))
->where($db->qn('element') . ' = ' . $db->q($plugin))
->where($db->qn('folder') . ' = ' . $db->q($folder));
$db->setQuery($sql);
$id = $db->loadResult();
if ($id)
{
$installer = new Installer;
$result = $installer->uninstall('plugin', $id, 1);
$status->plugins[] = [
'name' => 'plg_' . $plugin,
'group' => $folder,
'result' => $result,
];
}
}
}
}
}
return $status;
}
/**
* @param ComponentAdapter $parent
*
* @return bool
*
* @throws Exception When the Joomla! menu is FUBAR
*/
private function _createAdminMenus($parent)
{
$db = $parent->getParent()->getDbo();
/** @var Menu $table */
$table = Table::getInstance('menu');
$option = $parent->get('element');
// If a component exists with this option in the table then we don't need to add menus
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->qn('#__menu') . ' AS ' . $db->qn('m'))
->leftJoin($db->qn('#__extensions', 'e') . ' ON ' .
$db->qn('m.component_id') . ' = ' . $db->qn('e.extension_id'))
->where($db->qn('m.parent_id') . ' = ' . $db->q(1))
->where($db->qn('m.client_id') . ' = ' . $db->q(1))
->where($db->qn('e.type') . ' = ' . $db->q('component'))
->where($db->qn('e.element') . ' = ' . $db->q($option));
$db->setQuery($query);
$existingMenus = $db->loadResult();
if ($existingMenus)
{
return true;
}
// Let's find the extension id
$query->clear()
->select($db->qn('e.extension_id'))
->from($db->qn('#__extensions', 'e'))
->where($db->qn('e.type') . ' = ' . $db->q('component'))
->where($db->qn('e.element') . ' = ' . $db->q($option));
$db->setQuery($query);
$componentId = $db->loadResult();
// Ok, now its time to handle the menus. Start with the component root menu, then handle submenus.
if (method_exists($parent, 'getManifest'))
{
$menuElement = $parent->getManifest()->administration->menu;
}
else
{
$menuElement = $parent->get('manifest')->administration->menu;
}
// We need to insert the menu item as the last child of Joomla!'s menu root node. First let's make sure that
// it exists. Normally it should be the menu item with ID = 1.
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('id') . ' = ' . $db->q(1));
$rootItemId = $db->setQuery($query)->loadResult();
// If we didn't find the item with ID=1 something has screwed up the menu table, e.g. a bad upgrade script. In
// this case we can try to find the root node by title.
if (is_null($rootItemId))
{
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('title') . ' = ' . $db->q('Menu_Item_Root'));
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
// So, someone changed the title of the menu item too?! Let's find it by alias.
if (is_null($rootItemId))
{
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('alias') . ' = ' . $db->q('root'));
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
// For crying out loud, they changed the alias too? Fine! Find it by component ID.
if (is_null($rootItemId))
{
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('component_id') . ' = ' . $db->q('0'));
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
// Um, OK. Still no go. Let's try with minimum lft value.
if (is_null($rootItemId))
{
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->order($db->qn('lft') . ' ASC');
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
// I quit. Your site's menu structure is broken. I'll just throw an error.
if (is_null($rootItemId))
{
throw new Exception("Your site is broken. There is no root menu item. As a result it is impossible to create menu items. The installation of this component has failed. Please fix your database and retry!", 500);
}
/** @var SimpleXMLElement $menuElement */
if ($menuElement)
{
$data = [];
$data['menutype'] = 'main';
$data['client_id'] = 1;
$data['title'] = (string) trim($menuElement);
$data['alias'] = (string) $menuElement;
$data['link'] = 'index.php?option=' . $option;
$data['type'] = 'component';
$data['published'] = 0;
$data['parent_id'] = 1;
$data['component_id'] = $componentId;
$data['img'] = (string) $menuElement->attributes()->img ?: 'class:component';
$data['home'] = 0;
$data['path'] = '';
$data['params'] = '';
}
// No menu element was specified, Let's make a generic menu item
else
{
$data = [];
$data['menutype'] = 'main';
$data['client_id'] = 1;
$data['title'] = $option;
$data['alias'] = $option;
$data['link'] = 'index.php?option=' . $option;
$data['type'] = 'component';
$data['published'] = 0;
$data['parent_id'] = 1;
$data['component_id'] = $componentId;
$data['img'] = 'class:component';
$data['home'] = 0;
$data['path'] = '';
$data['params'] = '';
}
try
{
$table->setLocation($rootItemId, 'last-child');
}
catch (InvalidArgumentException $e)
{
$this->log($e->getMessage());
return false;
}
if (!$table->bind($data) || !$table->check() || !$table->store())
{
// The menu item already exists. Delete it and retry instead of throwing an error.
$query->clear()
->select('id')
->from('#__menu')
->where('menutype = ' . $db->quote('main'))
->where('client_id = 1')
->where('link = ' . $db->quote('index.php?option=' . $option))
->where('type = ' . $db->quote('component'))
->where('parent_id = 1')
->where('home = 0');
$db->setQuery($query);
$menu_ids_level1 = $db->loadColumn();
if (empty($menu_ids_level1))
{
// Oops! Could not get the menu ID. Go back and rollback changes.
JError::raiseWarning(1, $table->getError());
return false;
}
else
{
$ids = implode(',', $menu_ids_level1);
$query->clear()
->select('id')
->from('#__menu')
->where('menutype = ' . $db->quote('main'))
->where('client_id = 1')
->where('type = ' . $db->quote('component'))
->where('parent_id in (' . $ids . ')')
->where('level = 2')
->where('home = 0');
$db->setQuery($query);
$menu_ids_level2 = $db->loadColumn();
$ids = implode(',', array_merge($menu_ids_level1, $menu_ids_level2));
// Remove the old menu item
$query->clear()
->delete('#__menu')
->where('id in (' . $ids . ')');
$db->setQuery($query);
$db->execute();
// Retry creating the menu item
$table->setLocation($rootItemId, 'last-child');
if (!$table->bind($data) || !$table->check() || !$table->store())
{
// Install failed, warn user and rollback changes
JError::raiseWarning(1, $table->getError());
return false;
}
}
}
/*
* Since we have created a menu item, we add it to the installation step stack
* so that if we have to rollback the changes we can undo it.
*/
$parent->getParent()->pushStep(['type' => 'menu', 'id' => $componentId]);
/*
* Process SubMenus
*/
if (method_exists($parent, 'getManifest'))
{
$submenu = $parent->getManifest()->administration->submenu;
}
else
{
$submenu = $parent->get('manifest')->administration->submenu;
}
if (!$submenu)
{
return true;
}
$parent_id = $table->id;
/** @var SimpleXMLElement $child */
foreach ($submenu->menu as $child)
{
$data = [];
$data['menutype'] = 'main';
$data['client_id'] = 1;
$data['title'] = (string) trim($child);
$data['alias'] = (string) $child;
$data['type'] = 'component';
$data['published'] = 0;
$data['parent_id'] = $parent_id;
$data['component_id'] = $componentId;
$data['img'] = (string) $child->attributes()->img ?: 'class:component';
$data['home'] = 0;
// Set the sub menu link
if ((string) $child->attributes()->link)
{
$data['link'] = 'index.php?' . $child->attributes()->link;
}
else
{
$request = [];
if ((string) $child->attributes()->act)
{
$request[] = 'act=' . $child->attributes()->act;
}
if ((string) $child->attributes()->task)
{
$request[] = 'task=' . $child->attributes()->task;
}
if ((string) $child->attributes()->controller)
{
$request[] = 'controller=' . $child->attributes()->controller;
}
if ((string) $child->attributes()->view)
{
$request[] = 'view=' . $child->attributes()->view;
}
if ((string) $child->attributes()->layout)
{
$request[] = 'layout=' . $child->attributes()->layout;
}
if ((string) $child->attributes()->sub)
{
$request[] = 'sub=' . $child->attributes()->sub;
}
$qstring = (count($request)) ? '&' . implode('&', $request) : '';
$data['link'] = 'index.php?option=' . $option . $qstring;
}
$table = Table::getInstance('menu');
try
{
$table->setLocation($parent_id, 'last-child');
}
catch (InvalidArgumentException $e)
{
return false;
}
if (!$table->bind($data) || !$table->check() || !$table->store())
{
// Install failed, rollback changes
return false;
}
/*
* Since we have created a menu item, we add it to the installation step stack
* so that if we have to rollback the changes we can undo it.
*/
$parent->getParent()->pushStep(['type' => 'menu', 'id' => $componentId]);
}
return true;
}
/**
* Make sure the Component menu items are really published!
*
* @param ComponentAdapter $parent
*
* @return bool
*/
private function _reallyPublishAdminMenuItems($parent)
{
$db = $parent->getParent()->getDbo();
$option = $parent->get('element');
$query = $db->getQuery(true)
->update('#__menu AS m')
->join('LEFT', '#__extensions AS e ON m.component_id = e.extension_id')
->set($db->qn('published') . ' = ' . $db->q(1))
->where('m.parent_id = 1')
->where('m.client_id = 1')
->where('e.type = ' . $db->quote('component'))
->where('e.element = ' . $db->quote($option));
$db->setQuery($query);
try
{
$db->execute();
}
catch (Exception $e)
{
// If it fails, it fails. Who cares.
}
}
/**
* Tells Joomla! to rebuild its menu structure to make triple-sure that the Components menu items really do exist
* in the correct place and can really be rendered.
*/
private function _rebuildMenu()
{
/** @var Menu $table */
$table = Table::getInstance('menu');
$db = $table->getDbo();
// We need to rebuild the menu based on its root item. By default this is the menu item with ID=1. However, some
// crappy upgrade scripts enjoy screwing it up. Hey, ho, the workaround way I go.
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('id') . ' = ' . $db->q(1));
$rootItemId = $db->setQuery($query)->loadResult();
if (is_null($rootItemId))
{
// Guess what? The Problem has happened. Let's find the root node by title.
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('title') . ' = ' . $db->q('Menu_Item_Root'));
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
if (is_null($rootItemId))
{
// Did they change the title too?! Let's find it by alias.
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('alias') . ' = ' . $db->q('root'));
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
if (is_null($rootItemId))
{
// Woah! The alias is borked, too?! Find it by component ID.
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->where($db->qn('component_id') . ' = ' . $db->q('0'));
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
if (is_null($rootItemId))
{
// Your site is more of a "shite" than a "site". Let's try with minimum lft value.
$rootItemId = null;
$query = $db->getQuery(true)
->select($db->qn('id'))
->from($db->qn('#__menu'))
->order($db->qn('lft') . ' ASC');
$rootItemId = $db->setQuery($query, 0, 1)->loadResult();
}
if (is_null($rootItemId))
{
// I quit. Your site is broken.
return false;
}
$table->rebuild($rootItemId);
}
/**
* Deletes the assets table records for the component
*
* @param JDatabaseDriver $db
*
* @return void
*
* @since 3.0.18
*/
private function deleteComponentAssetRecords($db)
{
$query = $db->getQuery(true);
$query->select('id')
->from('#__assets')
->where($db->qn('name') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
$ids = $db->loadColumn();
if (empty($ids))
{
return;
}
foreach ($ids as $id)
{
$query = $db->getQuery(true);
$query->delete('#__assets')
->where($db->qn('id') . ' = ' . $db->q($id));
$db->setQuery($query);
try
{
$db->execute();
}
catch (Exception $exc)
{
// Nothing
}
}
}
/**
* Deletes the extensions table records for the component
*
* @param JDatabaseDriver $db
*
* @return void
*
* @since 3.0.18
*/
private function deleteComponentExtensionRecord($db)
{
$query = $db->getQuery(true);
$query->select('extension_id')
->from('#__extensions')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
$db->setQuery($query);
$ids = $db->loadColumn();
if (empty($ids))
{
return;
}
foreach ($ids as $id)
{
$query = $db->getQuery(true);
$query->delete('#__extensions')
->where($db->qn('extension_id') . ' = ' . $db->q($id));
$db->setQuery($query);
try
{
$db->execute();
}
catch (Exception $exc)
{
// Nothing
}
}
}
/**
* Deletes the menu table records for the component
*
* @param JDatabaseDriver $db
*
* @return void
*
* @since 3.0.18
*/
private function deleteComponentMenuRecord($db)
{
$query = $db->getQuery(true);
$query->select('id')
->from('#__menu')
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('menutype') . ' = ' . $db->q('main'))
->where($db->qn('link') . ' LIKE ' . $db->q('index.php?option=' . $this->componentName));
$db->setQuery($query);
$ids = $db->loadColumn();
if (empty($ids))
{
return;
}
foreach ($ids as $id)
{
$query = $db->getQuery(true);
$query->delete('#__menu')
->where($db->qn('id') . ' = ' . $db->q($id));
$db->setQuery($query);
try
{
$db->execute();
}
catch (Exception $exc)
{
// Nothing
}
}
}
}
Utils/helpers.php 0000604 00000024233 15245551424 0010026 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
defined('_JEXEC') || die;
/**
* 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.php 0000604 00000016427 15245551424 0010372 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\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.php 0000604 00000006276 15245551424 0011744 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\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.php 0000604 00000005145 15245551424 0011031 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils\FEFHelper;
defined('_JEXEC') || die;
use FOF30\View\DataView\DataViewInterface;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Pagination\Pagination;
/**
* Interim FEF helper which was used in FOF 3.2. This is deprecated. Please use the FEFHelper.browse JHtml helper
* instead. The implementation of this class should be a good hint on how you can do that.
*
* @deprecated 4.0
*/
abstract class Html
{
/**
* Helper function to create Javascript code required for table ordering
*
* @param string $order Current order
*
* @return string Javascript to add to the page
*/
public static function jsOrderingBackend($order)
{
return HTMLHelper::_('FEFHelper.browse.orderjs', $order, true);
}
/**
* Creates the required HTML code for backend pagination and sorting
*
* @param Pagination $pagination Pagination object
* @param array $sortFields Fields allowed to be sorted
* @param string $order Ordering field
* @param string $order_Dir Ordering direction (ASC, DESC)
*
* @return string
*/
public static function selectOrderingBackend($pagination, $sortFields, $order, $order_Dir)
{
if (is_null($sortFields))
{
$sortFields = [];
}
if (is_string($sortFields))
{
$sortFields = [$sortFields];
}
if (!is_array($sortFields))
{
$sortFields = [];
}
return
'<div class="akeeba-filter-bar akeeba-filter-bar--right">' .
HTMLHelper::_('FEFHelper.browse.orderheader', null, $sortFields, $pagination, $order, $order_Dir) .
'</div>';
}
/**
* Returns the drag'n'drop reordering field for Browse views
*
* @param DataViewInterface $view The DataView you're rendering against
* @param string $orderingField The name of the field you're ordering by
* @param string $order The order value of the current row
* @param string $class CSS class for the ordering value INPUT field
* @param string $icon CSS class for the d'n'd handle icon
* @param string $inactiveIcon CSS class for the d'n'd disabled icon
*
* @return string
*/
public static function dragDropReordering(DataViewInterface $view, $orderingField, $order, $class = 'input-sm', $icon = 'akion-drag', $inactiveIcon = 'akion-android-more-vertical')
{
return HTMLHelper::_('FEFHelper.browse.order', $orderingField, $order, $class, $icon, $inactiveIcon, $view);
}
}
Utils/FEFHelper/BrowseView.php 0000604 00000067172 15245551424 0012231 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils\FEFHelper;
defined('_JEXEC') || die;
use Exception;
use FOF30\Container\Container;
use FOF30\Model\DataModel;
use FOF30\Utils\ArrayHelper;
use FOF30\Utils\SelectOptions;
use FOF30\View\DataView\DataViewInterface;
use FOF30\View\View;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use RuntimeException;
/**
* An HTML helper for Browse views.
*
* It reintroduces a FEF-friendly of some of the functionality found in FOF 3's Header and Field classes. These
* helpers are also accessible through Blade, making the transition from XML forms to Blade templates easier.
*
* @since 3.3.0
*/
abstract class BrowseView
{
/**
* Caches the results of getOptionsFromModel keyed by a hash. The hash is computed by the model
* name, the model state and the options passed to getOptionsFromModel.
*
* @var array
*/
private static $cacheModelOptions = [];
/**
* Get the translation key for a field's label
*
* @param string $fieldName The field name
*
* @return string
*
* @since 3.3.0
*/
public static function fieldLabelKey($fieldName)
{
$view = self::getViewFromBacktrace();
try
{
$inflector = $view->getContainer()->inflector;
$viewName = $inflector->singularize($view->getName());
$altViewName = $inflector->pluralize($view->getName());
$componentName = $view->getContainer()->componentName;
$keys = [
strtoupper($componentName . '_' . $viewName . '_FIELD_' . $fieldName),
strtoupper($componentName . '_' . $altViewName . '_FIELD_' . $fieldName),
strtoupper($componentName . '_' . $viewName . '_' . $fieldName),
strtoupper($componentName . '_' . $altViewName . '_' . $fieldName),
];
foreach ($keys as $key)
{
if (Text::_($key) != $key)
{
return $key;
}
}
return $keys[0];
}
catch (Exception $e)
{
return ucfirst($fieldName);
}
}
/**
* Returns the label for a field (translated)
*
* @param string $fieldName The field name
*
* @return string
*/
public static function fieldLabel($fieldName)
{
return Text::_(self::fieldLabelKey($fieldName));
}
/**
* Return a table field header which sorts the table by that field upon clicking
*
* @param string $field The name of the field
* @param string|null $langKey (optional) The language key for the header to be displayed
*
* @return mixed
*/
public static function sortgrid($field, $langKey = null)
{
/** @var DataViewInterface $view */
$view = self::getViewFromBacktrace();
if (is_null($langKey))
{
$langKey = self::fieldLabelKey($field);
}
return HTMLHelper::_('FEFHelper.browse.sort', $langKey, $field, $view->getLists()->order_Dir, $view->getLists()->order, $view->getTask());
}
/**
* Create a browse view filter from values returned by a model
*
* @param string $localField Field name
* @param string $modelTitleField Foreign model field for drop-down display values
* @param null $modelName Foreign model name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function modelFilter($localField, $modelTitleField = 'title', $modelName = null, $placeholder = null, array $params = [])
{
/** @var DataModel $model */
$model = self::getViewFromBacktrace()->getModel();
if (empty($modelName))
{
$modelName = $model->getForeignModelNameFor($localField);
}
if (is_null($placeholder))
{
$placeholder = self::fieldLabelKey($localField);
}
$params = array_merge([
'list.none' => '— ' . Text::_($placeholder) . ' —',
'value_field' => $modelTitleField,
'fof.autosubmit' => true,
], $params);
return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
}
/**
* Display a text filter (search box)
*
* @param string $localField The name of the model field. Used when getting the filter state.
* @param string $searchField The INPUT element's name. Default: "filter_$localField".
* @param string $placeholder The JText language key for the placeholder. Default: extrapolate from $localField.
* @param array $attributes HTML attributes for the INPUT element.
*
* @return string
*
* @since 3.3.0
*/
public static function searchFilter($localField, $searchField = null, $placeholder = null, array $attributes = [])
{
/** @var DataModel $model */
$view = self::getViewFromBacktrace();
$model = $view->getModel();
$searchField = empty($searchField) ? $localField : $searchField;
$placeholder = empty($placeholder) ? self::fieldLabelKey($localField) : $placeholder;
$attributes['type'] = $attributes['type'] ?? 'text';
$attributes['name'] = $searchField;
$attributes['id'] = !isset($attributes['id']) ? "filter_$localField" : $attributes['id'];
$attributes['onchange'] = !isset($attributes['onchange']) ? 'document.adminForm.submit()' : null;
$attributes['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder'];
$attributes['title'] = $attributes['title'] ?? $attributes['placeholder'];
$attributes['value'] = $view->escape($model->getState($localField));
// Remove null attributes and collapse into a string
$attributes = array_filter($attributes, function ($v) {
return !is_null($v);
});
$attributes = ArrayHelper::toString($attributes);
return "<input $attributes />";
}
/**
* Create a browse view filter with dropdown values
*
* @param string $localField Field name
* @param array $options The JHtml options list to use
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function selectFilter($localField, array $options, $placeholder = null, array $params = [])
{
/** @var DataModel $model */
$model = self::getViewFromBacktrace()->getModel();
if (is_null($placeholder))
{
$placeholder = self::fieldLabelKey($localField);
}
$params = array_merge([
'list.none' => '— ' . Text::_($placeholder) . ' —',
'fof.autosubmit' => true,
], $params);
return self::genericSelect($localField, $options, $model->getState($localField), $params);
}
/**
* View access dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function accessFilter($localField, $placeholder = null, array $params = [])
{
return self::selectFilter($localField, SelectOptions::getOptions('access', $params), $placeholder, $params);
}
/**
* Published state dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function publishedFilter($localField, $placeholder = null, array $params = [])
{
return self::selectFilter($localField, SelectOptions::getOptions('published', $params), $placeholder, $params);
}
/**
* Create a select box from the values returned by a model
*
* @param string $name Field name
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param string $currentValue The currently selected value
* @param array $params Passed to optionsFromModel and genericSelect
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any JHtml select options you want to add in front of the model's returned values
*
* @return string
*
* @see self::getOptionsFromModel
* @see self::getOptionsFromSource
* @see self::genericSelect
*
* @since 3.3.0
*/
public static function modelSelect($name, $modelName, $currentValue, array $params = [], array $modelState = [], array $options = [])
{
$params = array_merge([
'fof.autosubmit' => true,
], $params);
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
return self::genericSelect($name, $options, $currentValue, $params);
}
/**
* Get a (human readable) title from a (typically numeric, foreign key) key value using the data
* returned by a DataModel.
*
* @param string $value The key value
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param array $params Passed to getOptionsFromModel
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any JHtml select options you want to add in front of the model's returned values
*
* @return string
*
* @see self::getOptionsFromModel
* @see self::getOptionsFromSource
* @see self::genericSelect
*
* @since 3.3.0
*/
public static function modelOptionName($value, $modelName = null, array $params = [], array $modelState = [], array $options = [])
{
if (!isset($params['cache']))
{
$params['cache'] = true;
}
if (!isset($params['none_as_zero']))
{
$params['none_as_zero'] = true;
}
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
return self::getOptionName($value, $options);
}
/**
* Gets the active option's label given an array of JHtml options
*
* @param mixed $selected The currently selected value
* @param array $data The JHtml options to parse
* @param string $optKey Key name, default: value
* @param string $optText Value name, default: text
* @param bool $selectFirst Should I automatically select the first option? Default: true
*
* @return mixed The label of the currently selected option
*/
public static function getOptionName($selected, $data, $optKey = 'value', $optText = 'text', $selectFirst = true)
{
$ret = null;
foreach ($data as $elementKey => &$element)
{
if (is_array($element))
{
$key = $optKey === null ? $elementKey : $element[$optKey];
$text = $element[$optText];
}
elseif (is_object($element))
{
$key = $optKey === null ? $elementKey : $element->$optKey;
$text = $element->$optText;
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
if (is_null($ret) && $selectFirst && ($selected == $key))
{
$ret = $text;
}
elseif ($selected == $key)
{
$ret = $text;
}
}
return $ret;
}
/**
* Create a generic select list based on a bunch of options. Option sources will be merged into the provided
* options automatically.
*
* Parameters:
* - format.depth The current indent depth.
* - format.eol The end of line string, default is linefeed.
* - format.indent The string to use for indentation, default is tab.
* - groups If set, looks for keys with the value "<optgroup>" and synthesizes groups from them. Deprecated.
* Default: true.
* - list.select Either the value of one selected option or an array of selected options. Default: $currentValue.
* - list.translate If true, text and labels are translated via JText::_(). Default is false.
* - list.attr HTML element attributes (key/value array or string)
* - list.none Placeholder for no selection (creates an option with an empty string key)
* - option.id The property in each option array to use as the selection id attribute. Defaults: null.
* - option.key The property in each option array to use as the Default: "value". If set to null, the index of the
* option array is used.
* - option.label The property in each option array to use as the selection label attribute. Default: null
* - option.text The property in each option array to use as the displayed text. Default: "text". If set to null,
* the option array is assumed to be a list of displayable scalars.
* - option.attr The property in each option array to use for additional selection attributes. Defaults: null.
* - option.disable: The property that will hold the disabled state. Defaults to "disable".
* - fof.autosubmit Should I auto-submit the form on change? Default: true
* - fof.formname Form to auto-submit. Default: adminForm
* - class CSS class to apply
* - size Size attribute for the input
* - multiple Is this a multiple select? Default: false.
* - required Is this a required field? Default: false.
* - autofocus Should I focus this field automatically? Default: false
* - disabled Is this a disabled field? Default: false
* - readonly Render as a readonly field with hidden inputs? Overrides 'disabled'. Default: false
* - onchange Custom onchange handler. Overrides fof.autosubmit. Default: NULL (use fof.autosubmit).
*
* @param $name
* @param array $options
* @param $currentValue
* @param array $params
*
* @return string
*
* @since 3.3.0
*/
public static function genericSelect($name, array $options, $currentValue, array $params = [])
{
$params = array_merge([
'format.depth' => 0,
'format.eol' => "\n",
'format.indent' => "\t",
'groups' => true,
'list.select' => $currentValue,
'list.translate' => false,
'option.id' => null,
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
'option.attr' => null,
'option.disable' => 'disable',
'list.attr' => '',
'list.none' => '',
'id' => null,
'fof.autosubmit' => true,
'fof.formname' => 'adminForm',
'class' => '',
'size' => '',
'multiple' => false,
'required' => false,
'autofocus' => false,
'disabled' => false,
'onchange' => null,
'readonly' => false,
], $params);
$currentValue = $params['list.select'];
// If fof.autosubmit is enabled and onchange is not set we will add our own handler
if ($params['fof.autosubmit'] && is_null($params['onchange']))
{
$formName = $params['fof.formname'] ?: 'adminForm';
$params['onchange'] = "document.{$formName}.submit()";
}
// Construct SELECT element's attributes
$attr = '';
$attr .= $params['class'] ? ' class="' . $params['class'] . '"' : '';
$attr .= !empty($params['size']) ? ' size="' . $params['size'] . '"' : '';
$attr .= $params['multiple'] ? ' multiple' : '';
$attr .= $params['required'] ? ' required aria-required="true"' : '';
$attr .= $params['autofocus'] ? ' autofocus' : '';
$attr .= ($params['disabled'] || $params['readonly']) ? ' disabled="disabled"' : '';
$attr .= $params['onchange'] ? ' onchange="' . $params['onchange'] . '"' : '';
// We use the constructed SELECT element's attributes only if no 'attr' key was provided
if (empty($params['list.attr']))
{
$params['list.attr'] = $attr;
}
// Merge the options with those fetched from a source (e.g. another Helper object)
$options = array_merge($options, self::getOptionsFromSource($params));
if (!empty($params['list.none']))
{
array_unshift($options, HTMLHelper::_('FEFHelper.select.option', '', Text::_($params['list.none'])));
}
$html = [];
// Create a read-only list (no name) with hidden input(s) to store the value(s).
if ($params['readonly'])
{
$html[] = HTMLHelper::_('FEFHelper.select.genericlist', $options, $name, $params);
// E.g. form field type tag sends $this->value as array
if ($params['multiple'] && is_array($currentValue))
{
if (!count($currentValue))
{
$currentValue[] = '';
}
foreach ($currentValue as $value)
{
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
}
}
else
{
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
}
}
else
// Create a regular list.
{
$html[] = HTMLHelper::_('FEFHelper.select.genericlist', $options, $name, $params);
}
return implode($html);
}
/**
* Replace tags that reference fields with their values
*
* @param string $text Text to process
* @param DataModel $item The DataModel instance to get values from
*
* @return string Text with tags replace
*
* @since 3.3.0
*/
public static function parseFieldTags($text, DataModel $item)
{
$ret = $text;
if (empty($item))
{
return $ret;
}
/**
* Replace [ITEM:ID] in the URL with the item's key value (usually: the auto-incrementing numeric ID)
*/
$replace = $item->getId();
$ret = str_replace('[ITEM:ID]', $replace, $ret);
// Replace the [ITEMID] in the URL with the current Itemid parameter
$ret = str_replace('[ITEMID]', $item->getContainer()->input->getInt('Itemid', 0), $ret);
// Replace the [TOKEN] in the URL with the Joomla! form token
$ret = str_replace('[TOKEN]', $item->getContainer()->platform->getToken(true), $ret);
// Replace other field variables in the URL
$data = $item->getData();
foreach ($data as $field => $value)
{
// Skip non-processable values
if (is_array($value) || is_object($value))
{
continue;
}
$search = '[ITEM:' . strtoupper($field) . ']';
$ret = str_replace($search, $value, $ret);
}
return $ret;
}
/**
* Get the FOF View from the backtrace of the static call. MAGIC!
*
* @return View
*
* @since 3.3.0
*/
public static function getViewFromBacktrace()
{
// In case we are on a braindead host
if (!function_exists('debug_backtrace'))
{
throw new RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
}
/**
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
* will expand my search by another 2 entries and so on until I either find a container or I stop
* finding new call stack entries.
*/
$lastNumberOfEntries = 0;
$limit = 4;
$skip = 0;
$container = null;
while (true)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
if (count($backtrace) == $lastNumberOfEntries)
{
throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
$lastNumberOfEntries = count($backtrace);
if ($skip)
{
$backtrace = array_slice($backtrace, $skip);
}
foreach ($backtrace as $bt)
{
if (!isset($bt['object']))
{
continue;
}
if ($bt['object'] instanceof View)
{
return $bt['object'];
}
}
$skip = $limit;
$limit += 2;
}
throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
/**
* Get JHtml options from an alternate source, e.g. a helper. This is useful for adding arbitrary options
* which are either dynamic or you do not want to inline to your view, e.g. reusable options across
* different views.
*
* The attribs can be:
* source_file The file to load. You can use FOF's URIs such as 'admin:com_foobar/foo/bar'
* source_class The class to use
* source_method The static method to use on source_class
* source_key Use * if you're returning a key/value array. Otherwise the array key for the key (ID)
* value.
* source_value Use * if you're returning a key/value array. Otherwise the array key for the displayed
* value. source_translate Should I pass the value field through JText? Default: true source_format Set
* to "optionsobject" if you're returning an array of JHtml options. Ignored otherwise.
*
* @param array $attribs
*
* @return array
*
* @since 3.3.0
*/
private static function getOptionsFromSource(array $attribs = [])
{
$options = [];
$container = self::getContainerFromBacktrace();
$attribs = array_merge([
'source_file' => '',
'source_class' => '',
'source_method' => '',
'source_key' => '*',
'source_value' => '*',
'source_translate' => true,
'source_format' => '',
], $attribs);
$source_file = $attribs['source_file'];
$source_class = $attribs['source_class'];
$source_method = $attribs['source_method'];
$source_key = $attribs['source_key'];
$source_value = $attribs['source_value'];
$source_translate = $attribs['source_translate'];
$source_format = $attribs['source_format'];
if ($source_class && $source_method)
{
// Maybe we have to load a file?
if (!empty($source_file))
{
$source_file = $container->template->parsePath($source_file, true);
if ($container->filesystem->fileExists($source_file))
{
include $source_file;
}
}
// Make sure the class exists
if (class_exists($source_class, true))
{
// ...and so does the option
if (in_array($source_method, get_class_methods($source_class)))
{
// Get the data from the class
if ($source_format == 'optionsobject')
{
$options = array_merge($options, $source_class::$source_method());
}
else
{
$source_data = $source_class::$source_method();
// Loop through the data and prime the $options array
foreach ($source_data as $k => $v)
{
$key = (empty($source_key) || ($source_key == '*')) ? $k : @$v[$source_key];
$value = (empty($source_value) || ($source_value == '*')) ? $v : @$v[$source_value];
if ($source_translate)
{
$value = Text::_($value);
}
$options[] = HTMLHelper::_('FEFHelper.select.option', $key, $value, 'value', 'text');
}
}
}
}
}
reset($options);
return $options;
}
/**
* Get JHtml options from the values returned by a model.
*
* The params can be:
* key_field The model field used for the OPTION's key. Default: the model's ID field.
* value_field The model field used for the OPTION's displayed value. You must provide it.
* apply_access Should I apply Joomla ACLs to the model? Default: FALSE.
* none Placeholder for no selection. Default: NULL (no placeholder).
* none_as_zero When true, the 'none' placeholder applies to values '' **AND** '0' (empty string and zero)
* translate Should I pass the values through JText? Default: TRUE.
* with Array of relation names for eager loading.
* cache Cache the results for faster reuse
*
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param array $params Parameters which define which options to get from the model
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any JHtml select options you want to add in front of the model's returned values
*
* @return mixed
*
* @since 3.3.0
*/
private static function getOptionsFromModel($modelName, array $params = [], array $modelState = [], array $options = [])
{
// Let's find the FOF DI container from the call stack
$container = self::getContainerFromBacktrace();
// Explode model name into component name and prefix
$componentName = $container->componentName;
$mName = $modelName;
if (strpos($modelName, '.') !== false)
{
[$componentName, $mName] = explode('.', $mName, 2);
}
if ($componentName != $container->componentName)
{
$container = Container::getInstance($componentName);
}
/** @var DataModel $model */
$model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false);
$defaultParams = [
'key_field' => $model->getKeyName(),
'value_field' => 'title',
'apply_access' => false,
'none' => null,
'none_as_zero' => false,
'translate' => true,
'with' => [],
];
$params = array_merge($defaultParams, $params);
$cache = isset($params['cache']) && $params['cache'];
$cacheKey = null;
if ($cache)
{
$cacheKey = sha1(print_r([
$model->getContainer()->componentName,
$model->getName(),
$params['key_field'],
$params['value_field'],
$params['apply_access'],
$params['none'],
$params['translate'],
$params['with'],
$modelState,
], true));
}
if ($cache && isset(self::$cacheModelOptions[$cacheKey]))
{
return self::$cacheModelOptions[$cacheKey];
}
if (empty($params['none']) && !is_null($params['none']))
{
$langKey = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName());
$placeholder = Text::_($langKey);
if ($langKey != $placeholder)
{
$params['none'] = '— ' . $placeholder . ' —';
}
}
if (!empty($params['none']))
{
$options[] = HTMLHelper::_('FEFHelper.select.option', null, Text::_($params['none']));
if ($params['none_as_zero'])
{
$options[] = HTMLHelper::_('FEFHelper.select.option', 0, Text::_($params['none']));
}
}
if ($params['apply_access'])
{
$model->applyAccessFiltering();
}
if (!is_null($params['with']))
{
$model->with($params['with']);
}
// Set the model's state, if applicable
foreach ($modelState as $stateKey => $stateValue)
{
$model->setState($stateKey, $stateValue);
}
// Set the query and get the result list.
$items = $model->get(true);
// Build the field options.
if (!empty($items))
{
foreach ($items as $item)
{
$value = $item->{$params['value_field']};
if ($params['translate'])
{
$value = Text::_($value);
}
$options[] = HTMLHelper::_('FEFHelper.select.option', $item->{$params['key_field']}, $value);
}
}
if ($cache)
{
self::$cacheModelOptions[$cacheKey] = $options;
}
return $options;
}
/**
* Get the FOF DI container from the backtrace of the static call. MAGIC!
*
* @return Container
*
* @since 3.3.0
*/
private static function getContainerFromBacktrace()
{
// In case we are on a braindead host
if (!function_exists('debug_backtrace'))
{
throw new RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
}
/**
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
* will expand my search by another 2 entries and so on until I either find a container or I stop
* finding new call stack entries.
*/
$lastNumberOfEntries = 0;
$limit = 4;
$skip = 0;
$container = null;
while (true)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
if (count($backtrace) == $lastNumberOfEntries)
{
throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
$lastNumberOfEntries = count($backtrace);
if ($skip)
{
$backtrace = array_slice($backtrace, $skip);
}
foreach ($backtrace as $bt)
{
if (!isset($bt['object']))
{
continue;
}
if (!method_exists($bt['object'], 'getContainer'))
{
continue;
}
return $bt['object']->getContainer();
}
$skip = $limit;
$limit += 2;
}
throw new RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
}
Utils/FEFHelper/edit.php 0000604 00000002671 15245551424 0011053 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
defined('_JEXEC') || die;
use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory;
/**
* Custom JHtml (HTMLHelper) class. Offers edit (form) view controls compatible with Akeeba Frontend
* Framework (FEF).
*
* Call these methods as JHtml::_('FEFHelper.edit.methodName', $parameter1, $parameter2, ...)
*/
abstract class FEFHelperEdit
{
public static function editor($fieldName, $value, array $params = [])
{
$params = array_merge([
'id' => null,
'editor' => null,
'width' => '100%',
'height' => 500,
'columns' => 50,
'rows' => 20,
'created_by' => null,
'asset_id' => null,
'buttons' => true,
'hide' => false,
], $params);
$editorType = $params['editor'];
if (is_null($editorType))
{
$editorType = Factory::getConfig()->get('editor');
$user = Factory::getUser();
if (!$user->guest)
{
$editorType = $user->getParam('editor', $editorType);
}
}
if (is_null($params['id']))
{
$params['id'] = $fieldName;
}
$editor = Editor::getInstance($editorType);
return $editor->display($fieldName, $value, $params['width'], $params['height'],
$params['columns'], $params['rows'], $params['buttons'], $params['id'],
$params['asset_id'], $params['created_by'], $params);
}
}
Utils/FEFHelper/select.php 0000604 00000065565 15245551424 0011420 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
defined('_JEXEC') || die;
use FOF30\Utils\ArrayHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
/**
* Custom JHtml (HTMLHelper) class. Offers selects compatible with Akeeba Frontend Framework (FEF)
*
* Call these methods as JHtml::_('FEFHelper.select.methodName', $parameter1, $parameter2, ...)
*/
abstract class FEFHelperSelect
{
/**
* Default values for options. Organized by option group.
*
* @var array
*/
protected static $optionDefaults = [
'option' => [
'option.attr' => null,
'option.disable' => 'disable',
'option.id' => null,
'option.key' => 'value',
'option.key.toHtml' => true,
'option.label' => null,
'option.label.toHtml' => true,
'option.text' => 'text',
'option.text.toHtml' => true,
'option.class' => 'class',
'option.onclick' => 'onclick',
],
];
/**
* Generates a yes/no radio list.
*
* @param string $name The value of the HTML name attribute
* @param array $attribs Additional HTML attributes for the `<select>` tag
* @param string $selected The key that is selected
* @param string $yes Language key for Yes
* @param string $no Language key for no
* @param mixed $id The id for the field or false for no id
*
* @return string HTML for the radio list
*
* @since 1.5
* @see JFormFieldRadio
*/
public static function booleanlist($name, $attribs = [], $selected = null, $yes = 'JYES', $no = 'JNO', $id = false)
{
$options = [
HTMLHelper::_('FEFHelper.select.option', '0', Text::_($no)),
HTMLHelper::_('FEFHelper.select.option', '1', Text::_($yes)),
];
$attribs = array_merge(['forSelect' => 1], $attribs);
return HTMLHelper::_('FEFHelper.select.radiolist', $options, $name, $attribs, 'value', 'text', (int) $selected, $id);
}
/**
* Generates an HTML selection list.
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see JHtml::$formatOptions}.
* Selection options, see {@see JHtmlSelect::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
* @since 1.5
*/
public static function genericlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
$translate = false)
{
// Set default options
$options = array_merge(HTMLHelper::$formatOptions, ['format.depth' => 0, 'id' => false]);
if (is_array($attribs) && func_num_args() === 3)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
}
else
{
// Get options from the parameters
$options['id'] = $idtag;
$options['list.attr'] = $attribs;
$options['list.translate'] = $translate;
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(['[', ']', ' '], '', $id);
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
$html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol']
. static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol'];
return $html;
}
/**
* Generates a grouped HTML selection list from nested arrays.
*
* @param array $data An array of groups, each of which is an array of options.
* @param string $name The value of the HTML name attribute
* @param array $options Options, an array of key/value pairs. Valid options are:
* Format options, {@see JHtml::$formatOptions}.
* Selection options. See {@see JHtmlSelect::options()}.
* group.id: The property in each group to use as the group id
* attribute. Defaults to none.
* group.label: The property in each group to use as the group
* label. Defaults to "text". If set to null, the data array index key is
* used.
* group.items: The property in each group to use as the array of
* items in the group. Defaults to "items". If set to null, group.id and
* group. label are forced to null and the data element is assumed to be a
* list of selections.
* id: Value to use as the select element id attribute. Defaults to
* the same as the name.
* list.attr: Attributes for the select element. Can be a string or
* an array of key/value pairs. Defaults to none.
* list.select: either the value of one selected option or an array
* of selected options. Default: none.
* list.translate: Boolean. If set, text and labels are translated via
* JText::_().
*
* @return string HTML for the select list
*
* @throws RuntimeException If a group has contents that cannot be processed.
*/
public static function groupedlist($data, $name, $options = [])
{
// Set default options and overwrite with anything passed in
$options = array_merge(
HTMLHelper::$formatOptions,
[
'format.depth' => 0, 'group.items' => 'items', 'group.label' => 'text', 'group.label.toHtml' => true,
'id' => false,
],
$options
);
// Apply option rules
if ($options['group.items'] === null)
{
$options['group.label'] = null;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(['[', ']', ' '], '', $id);
// Disable groups in the options.
$options['groups'] = false;
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
$html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol'];
$groupIndent = str_repeat($options['format.indent'], $options['format.depth']++);
foreach ($data as $dataKey => $group)
{
$label = $dataKey;
$id = '';
$noGroup = is_int($dataKey);
if ($options['group.items'] == null)
{
// Sub-list is an associative array
$subList = $group;
}
elseif (is_array($group))
{
// Sub-list is in an element of an array.
$subList = $group[$options['group.items']];
if (isset($group[$options['group.label']]))
{
$label = $group[$options['group.label']];
$noGroup = false;
}
if (isset($options['group.id']) && isset($group[$options['group.id']]))
{
$id = $group[$options['group.id']];
$noGroup = false;
}
}
elseif (is_object($group))
{
// Sub-list is in a property of an object
$subList = $group->{$options['group.items']};
if (isset($group->{$options['group.label']}))
{
$label = $group->{$options['group.label']};
$noGroup = false;
}
if (isset($options['group.id']) && isset($group->{$options['group.id']}))
{
$id = $group->{$options['group.id']};
$noGroup = false;
}
}
else
{
throw new RuntimeException('Invalid group contents.', 1);
}
if ($noGroup)
{
$html .= static::options($subList, $options);
}
else
{
$html .= $groupIndent . '<optgroup' . (empty($id) ? '' : ' id="' . $id . '"') . ' label="'
. ($options['group.label.toHtml'] ? htmlspecialchars($label, ENT_COMPAT, 'UTF-8') : $label) . '">' . $options['format.eol']
. static::options($subList, $options) . $groupIndent . '</optgroup>' . $options['format.eol'];
}
}
$html .= $baseIndent . '</select>' . $options['format.eol'];
return $html;
}
/**
* Generates a selection list of integers.
*
* @param integer $start The start integer
* @param integer $end The end integer
* @param integer $inc The increment
* @param string $name The value of the HTML name attribute
* @param mixed $attribs Additional HTML attributes for the `<select>` tag, an array of
* attributes, or an array of options. Treated as options if it is the last
* argument passed.
* @param mixed $selected The key that is selected
* @param string $format The printf format to be applied to the number
*
* @return string HTML for the select list
*/
public static function integerlist($start, $end, $inc, $name, $attribs = null, $selected = null, $format = '')
{
// Set default options
$options = array_merge(HTMLHelper::$formatOptions, [
'format.depth' => 0, 'option.format' => '', 'id' => null,
]);
if (is_array($attribs) && func_num_args() === 5)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
// Extract the format and remove it from downstream options
$format = $options['option.format'];
unset($options['option.format']);
}
else
{
// Get options from the parameters
$options['list.attr'] = $attribs;
$options['list.select'] = $selected;
}
$start = (int) $start;
$end = (int) $end;
$inc = (int) $inc;
$data = [];
for ($i = $start; $i <= $end; $i += $inc)
{
$data[$i] = $format ? sprintf($format, $i) : $i;
}
// Tell genericlist() to use array keys
$options['option.key'] = null;
return HTMLHelper::_('FEFHelper.select.genericlist', $data, $name, $options);
}
/**
* Create an object that represents an option in an option list.
*
* @param string $value The value of the option
* @param string $text The text for the option
* @param mixed $optKey If a string, the returned object property name for
* the value. If an array, options. Valid options are:
* attr: String|array. Additional attributes for this option.
* Defaults to none.
* disable: Boolean. If set, this option is disabled.
* label: String. The value for the option label.
* option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* option.key: The property that will hold the selection value.
* Defaults to "value".
* option.label: The property in each option array to use as the
* selection label attribute. If a "label" option is provided, defaults to
* "label", if no label is given, defaults to null (none).
* option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The property that will hold the the displayed text. This
* parameter is ignored if an options array is passed.
* @param boolean $disable Not used.
*
* @return stdClass
*/
public static function option($value, $text = '', $optKey = 'value', $optText = 'text', $disable = false)
{
$options = [
'attr' => null,
'disable' => false,
'option.attr' => null,
'option.disable' => 'disable',
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
];
if (is_array($optKey))
{
// Merge in caller's options
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['disable'] = $disable;
}
$obj = new stdClass;
$obj->{$options['option.key']} = $value;
$obj->{$options['option.text']} = trim($text) ? $text : $value;
/*
* If a label is provided, save it. If no label is provided and there is
* a label name, initialise to an empty string.
*/
$hasProperty = $options['option.label'] !== null;
if (isset($options['label']))
{
$labelProperty = $hasProperty ? $options['option.label'] : 'label';
$obj->$labelProperty = $options['label'];
}
elseif ($hasProperty)
{
$obj->{$options['option.label']} = '';
}
// Set attributes only if there is a property and a value
if ($options['attr'] !== null)
{
$obj->{$options['option.attr']} = $options['attr'];
}
// Set disable only if it has a property and a value
if ($options['disable'] !== null)
{
$obj->{$options['option.disable']} = $options['disable'];
}
return $obj;
}
/**
* Generates the option tags for an HTML select list (with no select tag
* surrounding the options).
*
* @param array $arr An array of objects, arrays, or values.
* @param mixed $optKey If a string, this is the name of the object variable for
* the option value. If null, the index of the array of objects is used. If
* an array, this is a set of options, as key/value pairs. Valid options are:
* -Format options, {@see JHtml::$formatOptions}.
* -groups: Boolean. If set, looks for keys with the value
* "<optgroup>" and synthesizes groups from them. Deprecated. Defaults
* true for backwards compatibility.
* -list.select: either the value of one selected option or an array
* of selected options. Default: none.
* -list.translate: Boolean. If set, text and labels are translated via
* JText::_(). Default is false.
* -option.id: The property in each option array to use as the
* selection id attribute. Defaults to none.
* -option.key: The property in each option array to use as the
* selection value. Defaults to "value". If set to null, the index of the
* option array is used.
* -option.label: The property in each option array to use as the
* selection label attribute. Defaults to null (none).
* -option.text: The property in each option array to use as the
* displayed text. Defaults to "text". If set to null, the option array is
* assumed to be a list of displayable scalars.
* -option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* -option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* -option.key: The property that will hold the selection value.
* Defaults to "value".
* -option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string)
* @param boolean $translate Translate the option values.
*
* @return string HTML for the select list
*/
public static function options($arr, $optKey = 'value', $optText = 'text', $selected = null, $translate = false)
{
$options = array_merge(
HTMLHelper::$formatOptions,
static::$optionDefaults['option'],
['format.depth' => 0, 'groups' => true, 'list.select' => null, 'list.translate' => false]
);
if (is_array($optKey))
{
// Set default options and overwrite with anything passed in
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
$options['list.translate'] = $translate;
}
$html = '';
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']);
foreach ($arr as $elementKey => &$element)
{
$attr = '';
$extra = '';
$label = '';
$id = '';
if (is_array($element))
{
$key = $options['option.key'] === null ? $elementKey : $element[$options['option.key']];
$text = $element[$options['option.text']];
if (isset($element[$options['option.attr']]))
{
$attr = $element[$options['option.attr']];
}
if (isset($element[$options['option.id']]))
{
$id = $element[$options['option.id']];
}
if (isset($element[$options['option.label']]))
{
$label = $element[$options['option.label']];
}
if (isset($element[$options['option.disable']]) && $element[$options['option.disable']])
{
$extra .= ' disabled="disabled"';
}
}
elseif (is_object($element))
{
$key = $options['option.key'] === null ? $elementKey : $element->{$options['option.key']};
$text = $element->{$options['option.text']};
if (isset($element->{$options['option.attr']}))
{
$attr = $element->{$options['option.attr']};
}
if (isset($element->{$options['option.id']}))
{
$id = $element->{$options['option.id']};
}
if (isset($element->{$options['option.label']}))
{
$label = $element->{$options['option.label']};
}
if (isset($element->{$options['option.disable']}) && $element->{$options['option.disable']})
{
$extra .= ' disabled="disabled"';
}
if (isset($element->{$options['option.class']}) && $element->{$options['option.class']})
{
$extra .= ' class="' . $element->{$options['option.class']} . '"';
}
if (isset($element->{$options['option.onclick']}) && $element->{$options['option.onclick']})
{
$extra .= ' onclick="' . $element->{$options['option.onclick']} . '"';
}
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
/*
* The use of options that contain optgroup HTML elements was
* somewhat hacked for J1.5. J1.6 introduces the grouplist() method
* to handle this better. The old solution is retained through the
* "groups" option, which defaults true in J1.6, but should be
* deprecated at some point in the future.
*/
$key = (string) $key;
if ($key === '<OPTGROUP>' && $options['groups'])
{
$html .= $baseIndent . '<optgroup label="' . ($options['list.translate'] ? Text::_($text) : $text) . '">' . $options['format.eol'];
$baseIndent = str_repeat($options['format.indent'], ++$options['format.depth']);
}
elseif ($key === '</OPTGROUP>' && $options['groups'])
{
$baseIndent = str_repeat($options['format.indent'], --$options['format.depth']);
$html .= $baseIndent . '</optgroup>' . $options['format.eol'];
}
else
{
// If no string after hyphen - take hyphen out
$splitText = explode(' - ', $text, 2);
$text = $splitText[0];
if (isset($splitText[1]) && $splitText[1] !== '' && !preg_match('/^[\s]+$/', $splitText[1]))
{
$text .= ' - ' . $splitText[1];
}
if (!empty($label) && $options['list.translate'])
{
$label = Text::_($label);
}
if ($options['option.label.toHtml'])
{
$label = htmlentities($label);
}
if (is_array($attr))
{
$attr = ArrayHelper::toString($attr);
}
else
{
$attr = trim($attr);
}
$extra = ($id ? ' id="' . $id . '"' : '') . ($label ? ' label="' . $label . '"' : '') . ($attr ? ' ' . $attr : '') . $extra;
if (is_array($options['list.select']))
{
foreach ($options['list.select'] as $val)
{
$key2 = is_object($val) ? $val->{$options['option.key']} : $val;
if ($key == $key2)
{
$extra .= ' selected="selected"';
break;
}
}
}
elseif ((string) $key === (string) $options['list.select'])
{
$extra .= ' selected="selected"';
}
if ($options['list.translate'])
{
$text = Text::_($text);
}
// Generate the option, encoding as required
$html .= $baseIndent . '<option value="' . ($options['option.key.toHtml'] ? htmlspecialchars($key, ENT_COMPAT, 'UTF-8') : $key) . '"'
. $extra . '>';
$html .= $options['option.text.toHtml'] ? htmlentities(html_entity_decode($text, ENT_COMPAT, 'UTF-8'), ENT_COMPAT, 'UTF-8') : $text;
$html .= '</option>' . $options['format.eol'];
}
}
return $html;
}
/**
* Generates an HTML radio list.
*
* @param array $data An array of objects
* @param string $name The value of the HTML name attribute
* @param string $attribs Additional HTML attributes for the `<select>` tag
* @param mixed $optKey The key that is selected
* @param string $optText The name of the object variable for the option value
* @param string $selected The name of the object variable for the option text
* @param boolean $idtag Value of the field id or null by default
* @param boolean $translate True if options will be translated
*
* @return string HTML for the select list
*/
public static function radiolist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
$translate = false)
{
$forSelect = false;
if (isset($attribs['forSelect']))
{
$forSelect = (bool) ($attribs['forSelect']);
unset($attribs['forSelect']);
}
if (is_array($attribs))
{
$attribs = ArrayHelper::toString($attribs);
}
$id_text = empty($idtag) ? $name : $idtag;
$html = '';
foreach ($data as $optionObject)
{
$optionValue = $optionObject->$optKey;
$labelText = $translate ? Text::_($optionObject->$optText) : $optionObject->$optText;
$id = ($optionObject->id ?? null);
$extra = '';
$id = $id ? $optionObject->id : $id_text . $optionValue;
if (is_array($selected))
{
foreach ($selected as $val)
{
$k2 = is_object($val) ? $val->$optKey : $val;
if ($optionValue == $k2)
{
$extra .= ' selected="selected" ';
break;
}
}
}
else
{
$extra .= ((string) $optionValue === (string) $selected ? ' checked="checked" ' : '');
}
if ($forSelect)
{
$html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />';
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">' . $labelText . '</label>';
}
else
{
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">';
$html .= "\n\t\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />' . $labelText;
$html .= "\n\t" . '</label>';
}
}
$html .= "\n";
return $html;
}
/**
* Creates two radio buttons styled with FEF to appear as a YES/NO switch
*
* @param string $name Name of the field
* @param string $selected Selected field
* @param array $attribs Additional attributes to add to the switch
*
* @return string The HTML for the switch
*/
public static function booleanswitch($name, $selected, array $attribs = [])
{
if (empty($attribs))
{
$attribs = ['class' => 'akeeba-toggle'];
}
else
{
if (isset($attribs['class']))
{
$attribs['class'] .= ' akeeba-toggle';
}
else
{
$attribs['class'] = 'akeeba-toggle';
}
}
$temp = '';
foreach ($attribs as $key => $value)
{
$temp .= $key . ' = "' . $value . '"';
}
$attribs = $temp;
$checked_1 = $selected ? '' : 'checked ';
$checked_2 = $selected ? 'checked ' : '';
$html = '<div ' . $attribs . '>';
$html .= '<input type="radio" class="radio-yes" name="' . $name . '" ' . $checked_2 . 'id="' . $name . '-2" value="1">';
$html .= '<label for="' . $name . '-2" class="green">' . Text::_('JYES') . '</label>';
$html .= '<input type="radio" class="radio-no" name="' . $name . '" ' . $checked_1 . 'id="' . $name . '-1" value="0">';
$html .= '<label for="' . $name . '-1" class="red">' . Text::_('JNO') . '</label>';
$html .= '</div>';
return $html;
}
}
Utils/FEFHelper/browse.php 0000604 00000072505 15245551424 0011432 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
defined('_JEXEC') || die;
use FOF30\Model\DataModel;
use FOF30\Utils\ArrayHelper;
use FOF30\Utils\FEFHelper\BrowseView;
use FOF30\View\DataView\DataViewInterface;
use FOF30\View\DataView\Html;
use FOF30\View\DataView\Raw as DataViewRaw;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Pagination\Pagination;
/**
* Custom JHtml (HTMLHelper) class. Offers browse view controls compatible with Akeeba Frontend
* Framework (FEF).
*
* Call these methods as JHtml::_('FEFHelper.browse.methodName', $parameter1, $parameter2, ...)
*/
abstract class FEFHelperBrowse
{
/**
* Returns an action button on the browse view's table
*
* @param integer $i The row index
* @param string $task The task to fire when the button is clicked
* @param string|array $prefix An optional task prefix or an array of options
* @param string $active_title An optional active tooltip to display if $enable is true
* @param string $inactive_title An optional inactive tooltip to display if $enable is true
* @param boolean $tip An optional setting for tooltip
* @param string $active_class An optional active HTML class
* @param string $inactive_class An optional inactive HTML class
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function action($i, $task, $prefix = '', $active_title = '', $inactive_title = '', $tip = false, $active_class = '',
$inactive_class = '', $enabled = true, $translate = true, $checkbox = 'cb')
{
if (is_array($prefix))
{
$options = $prefix;
$active_title = array_key_exists('active_title', $options) ? $options['active_title'] : $active_title;
$inactive_title = array_key_exists('inactive_title', $options) ? $options['inactive_title'] : $inactive_title;
$tip = array_key_exists('tip', $options) ? $options['tip'] : $tip;
$active_class = array_key_exists('active_class', $options) ? $options['active_class'] : $active_class;
$inactive_class = array_key_exists('inactive_class', $options) ? $options['inactive_class'] : $inactive_class;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
if ($tip)
{
$title = $enabled ? $active_title : $inactive_title;
$title = $translate ? Text::_($title) : $title;
$title = HTMLHelper::_('tooltipText', $title, '', 0);
}
$html = [];
if ($enabled)
{
$btnColor = 'grey';
if (substr($active_class, 0, 2) == '--')
{
[$btnColor, $active_class] = explode(' ', $active_class, 2);
$btnColor = ltrim($btnColor, '-');
}
$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini ' . ($active_class === 'publish' ? ' active' : '') . ($tip ? ' hasTooltip' : '') . '"';
$html[] = ' href="javascript:void(0);" onclick="return Joomla.listItemTask(\'' . $checkbox . $i . '\',\'' . $prefix . $task . '\')"';
$html[] = $tip ? ' title="' . $title . '"' : '';
$html[] = '>';
$html[] = '<span class="akion-' . $active_class . '" aria-hidden="true"></span> ';
$html[] = '</a>';
}
else
{
$btnColor = 'grey';
if (substr($inactive_class, 0, 2) == '--')
{
[$btnColor, $inactive_class] = explode(' ', $inactive_class, 2);
$btnColor = ltrim($btnColor, '-');
}
$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini disabled akeebagrid' . ($tip ? ' hasTooltip' : '') . '"';
$html[] = $tip ? ' title="' . $title . '"' : '';
$html[] = '>';
if ($active_class === 'protected')
{
$inactive_class = 'locked';
}
$html[] = '<span class="akion-' . $inactive_class . '"></span> ';
$html[] = '</a>';
}
return implode($html);
}
/**
* Returns a state change button on the browse view's table
*
* @param array $states array of value/state. Each state is an array of the form
* (task, text, active title, inactive title, tip (boolean), HTML active class,
* HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active
* title,
* 'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active
* class,
* 'inactive_class'=>html inactive class)
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function state($states, $value, $i, $prefix = '', $enabled = true, $translate = true, $checkbox = 'cb')
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$state = ArrayHelper::getValue($states, (int) $value, $states[0]);
$task = array_key_exists('task', $state) ? $state['task'] : $state[0];
$text = array_key_exists('text', $state) ? $state['text'] : (array_key_exists(1, $state) ? $state[1] : '');
$active_title = array_key_exists('active_title', $state) ? $state['active_title'] : (array_key_exists(2, $state) ? $state[2] : '');
$inactive_title = array_key_exists('inactive_title', $state) ? $state['inactive_title'] : (array_key_exists(3, $state) ? $state[3] : '');
$tip = array_key_exists('tip', $state) ? $state['tip'] : (array_key_exists(4, $state) ? $state[4] : false);
$active_class = array_key_exists('active_class', $state) ? $state['active_class'] : (array_key_exists(5, $state) ? $state[5] : '');
$inactive_class = array_key_exists('inactive_class', $state) ? $state['inactive_class'] : (array_key_exists(6, $state) ? $state[6] : '');
return static::action(
$i, $task, $prefix, $active_title, $inactive_title, $tip,
$active_class, $inactive_class, $enabled, $translate, $checkbox
);
}
/**
* Returns a published state on the browse view's table
*
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
* @param string $publish_up An optional start publishing date.
* @param string $publish_down An optional finish publishing date.
*
* @return string The HTML markup
*
* @see self::state()
*
* @since 3.3.0
*/
public static function published($value, $i, $prefix = '', $enabled = true, $checkbox = 'cb', $publish_up = null, $publish_down = null)
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
/**
* Format:
*
* (task, text, active title, inactive title, tip (boolean), active icon class (without akion-), inactive icon class (without akion-))
*/
$states = [
1 => [
'unpublish', 'JPUBLISHED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JPUBLISHED', true, '--green checkmark',
'--green checkmark',
],
0 => [
'publish', 'JUNPUBLISHED', 'JLIB_HTML_PUBLISH_ITEM', 'JUNPUBLISHED', true, '--red close', '--red close',
],
2 => [
'unpublish', 'JARCHIVED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JARCHIVED', true, '--orange ion-ios-box',
'--orange ion-ios-box',
],
-2 => [
'publish', 'JTRASHED', 'JLIB_HTML_PUBLISH_ITEM', 'JTRASHED', true, '--dark trash-a', '--dark trash-a',
],
];
// Special state for dates
if ($publish_up || $publish_down)
{
$nullDate = Factory::getDbo()->getNullDate();
$nowDate = Factory::getDate()->toUnix();
$tz = Factory::getUser()->getTimezone();
$publish_up = (!empty($publish_up) && ($publish_up != $nullDate)) ? Factory::getDate($publish_up, 'UTC')->setTimeZone($tz) : false;
$publish_down = (!empty($publish_down) && ($publish_down != $nullDate)) ? Factory::getDate($publish_down, 'UTC')->setTimeZone($tz) : false;
// Create tip text, only we have publish up or down settings
$tips = [];
if ($publish_up)
{
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publish_up, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
if ($publish_down)
{
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publish_down, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
$tip = empty($tips) ? false : implode('<br />', $tips);
// Add tips and special titles
foreach ($states as $key => $state)
{
// Create special titles for published items
if ($key == 1)
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_ITEM';
if (!empty($publish_up) && ($publish_up != $nullDate) && $nowDate < $publish_up->toUnix())
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_PENDING_ITEM';
$states[$key][5] = $states[$key][6] = 'android-time';
}
if (!empty($publish_down) && ($publish_down != $nullDate) && $nowDate > $publish_down->toUnix())
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_EXPIRED_ITEM';
$states[$key][5] = $states[$key][6] = 'alert';
}
}
// Add tips to titles
if ($tip)
{
$states[$key][1] = Text::_($states[$key][1]);
$states[$key][2] = Text::_($states[$key][2]) . '<br />' . $tip;
$states[$key][3] = Text::_($states[$key][3]) . '<br />' . $tip;
$states[$key][4] = true;
}
}
return static::state($states, $value, $i, [
'prefix' => $prefix, 'translate' => !$tip,
], $enabled, true, $checkbox);
}
return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
}
/**
* Returns an isDefault state on the browse view's table
*
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @see self::state()
* @since 3.3.0
*/
public static function isdefault($value, $i, $prefix = '', $enabled = true, $checkbox = 'cb')
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$states = [
0 => ['setDefault', '', 'JLIB_HTML_SETDEFAULT_ITEM', '', 1, 'android-star-outline', 'android-star-outline'],
1 => [
'unsetDefault', 'JDEFAULT', 'JLIB_HTML_UNSETDEFAULT_ITEM', 'JDEFAULT', 1, 'android-star',
'android-star',
],
];
return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
}
/**
* Returns a checked-out icon
*
* @param integer $i The row index.
* @param string $editorName The name of the editor.
* @param string $time The time that the object was checked out.
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled True to enable the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function checkedout($i, $editorName, $time, $prefix = '', $enabled = false, $checkbox = 'cb')
{
HTMLHelper::_('bootstrap.tooltip');
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$text = $editorName . '<br />' . HTMLHelper::_('date', $time, Text::_('DATE_FORMAT_LC')) . '<br />' . HTMLHelper::_('date', $time, 'H:i');
$active_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKIN'), $text, 0);
$inactive_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKED_OUT'), $text, 0);
return static::action(
$i, 'checkin', $prefix, html_entity_decode($active_title, ENT_QUOTES, 'UTF-8'),
html_entity_decode($inactive_title, ENT_QUOTES, 'UTF-8'), true, 'locked', 'locked', $enabled, false, $checkbox
);
}
/**
* Returns the drag'n'drop reordering field for Browse views
*
* @param string $orderingField The name of the field you're ordering by
* @param string $order The order value of the current row
* @param string $class CSS class for the ordering value INPUT field
* @param string $icon CSS class for the d'n'd handle icon
* @param string $inactiveIcon CSS class for the d'n'd disabled icon
* @param DataViewInterface $view The view you're rendering against. Leave null for auto-detection.
*
* @return string
*/
public static function order($orderingField, $order, $class = 'input-sm', $icon = 'akion-android-more-vertical', $inactiveIcon = 'akion-android-more-vertical', DataViewInterface $view = null)
{
/** @var Html $view */
if (is_null($view))
{
$view = BrowseView::getViewFromBacktrace();
}
$dndOrderingActive = $view->getLists()->order == $orderingField;
// Default inactive ordering
$html = '<span class="sortable-handler inactive" >';
$html .= '<span class="' . $icon . '"></span>';
$html .= '</span>';
// The modern drag'n'drop method
if ($view->getPerms()->editstate)
{
$disableClassName = '';
$disabledLabel = '';
// DO NOT REMOVE! It will initialize Joomla libraries and javascript functions
$hasAjaxOrderingSupport = $view->hasAjaxOrderingSupport();
if (!is_array($hasAjaxOrderingSupport) || !$hasAjaxOrderingSupport['saveOrder'])
{
$disabledLabel = Text::_('JORDERINGDISABLED');
$disableClassName = 'inactive tip-top hasTooltip';
}
$orderClass = $dndOrderingActive ? 'order-enabled' : 'order-disabled';
$html = '<div class="' . $orderClass . '">';
$html .= '<span class="sortable-handler ' . $disableClassName . '" title="' . $disabledLabel . '">';
$html .= '<span class="' . ($disableClassName ? $inactiveIcon : $icon) . '"></span>';
$html .= '</span>';
if ($dndOrderingActive)
{
$joomla35IsBroken = version_compare(JVERSION, '3.5.0', 'ge') ? 'style="display: none"' : '';
$html .= '<input type="text" name="order[]" ' . $joomla35IsBroken . ' size="5" class="' . $class . ' text-area-order" value="' . $order . '" />';
}
$html .= '</div>';
}
return $html;
}
/**
* Returns the drag'n'drop reordering table header for Browse views
*
* @param string $orderingField The name of the field you're ordering by
* @param string $icon CSS class for the d'n'd handle icon
*
* @return string
*/
public static function orderfield($orderingField = 'ordering', $icon = 'akion-stats-bars')
{
$title = Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN');
$orderingLabel = Text::_('JFIELD_ORDERING_LABEL');
return <<< HTML
<a href="#"
onclick="Joomla.tableOrdering('{$orderingField}','asc','');return false;"
class="hasPopover"
title="{$orderingLabel}"
data-content="{$title}"
data-placement="top"
>
<span class="{$icon}"></span>
</a>
HTML;
}
/**
* Creates an order-up action icon.
*
* @param integer $i The row index.
* @param string $task An optional task to fire.
* @param string|array $prefix An optional task prefix or an array of options
* @param string $text An optional text to display
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function orderUp($i, $task = 'orderup', $prefix = '', $text = 'JLIB_HTML_MOVE_UP', $enabled = true, $checkbox = 'cb')
{
if (is_array($prefix))
{
$options = $prefix;
$text = array_key_exists('text', $options) ? $options['text'] : $text;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
return static::action($i, $task, $prefix, $text, $text, false, 'arrow-up-b', 'arrow-up-b', $enabled, true, $checkbox);
}
/**
* Creates an order-down action icon.
*
* @param integer $i The row index.
* @param string $task An optional task to fire.
* @param string|array $prefix An optional task prefix or an array of options
* @param string $text An optional text to display
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function orderDown($i, $task = 'orderdown', $prefix = '', $text = 'JLIB_HTML_MOVE_DOWN', $enabled = true, $checkbox = 'cb')
{
if (is_array($prefix))
{
$options = $prefix;
$text = array_key_exists('text', $options) ? $options['text'] : $text;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
return static::action($i, $task, $prefix, $text, $text, false, 'arrow-down-b', 'arrow-down-b', $enabled, true, $checkbox);
}
/**
* Table header for a field which changes the sort order when clicked
*
* @param string $title The link title
* @param string $order The order field for the column
* @param string $direction The current direction
* @param string $selected The selected ordering
* @param string $task An optional task override
* @param string $new_direction An optional direction for the new column
* @param string $tip An optional text shown as tooltip title instead of $title
* @param string $form An optional form selector
*
* @return string
*
* @since 3.3.0
*/
public static function sort($title, $order, $direction = 'asc', $selected = '', $task = null, $new_direction = 'asc', $tip = '', $form = null)
{
HTMLHelper::_('behavior.core');
HTMLHelper::_('bootstrap.popover');
$direction = strtolower($direction);
$icon = ['akion-android-arrow-dropup', 'akion-android-arrow-dropdown'];
$index = (int) ($direction === 'desc');
if ($order != $selected)
{
$direction = $new_direction;
}
else
{
$direction = $direction === 'desc' ? 'asc' : 'desc';
}
if ($form)
{
$form = ', document.getElementById(\'' . $form . '\')';
}
$html = '<a href="#" onclick="Joomla.tableOrdering(\'' . $order . '\',\'' . $direction . '\',\'' . $task . '\'' . $form . ');return false;"'
. ' class="hasPopover" title="' . htmlspecialchars(Text::_($tip ?: $title)) . '"'
. ' data-content="' . htmlspecialchars(Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN')) . '" data-placement="top">';
if (isset($title['0']) && $title['0'] === '<')
{
$html .= $title;
}
else
{
$html .= Text::_($title);
}
if ($order == $selected)
{
$html .= '<span class="' . $icon[$index] . '"></span>';
}
$html .= '</a>';
return $html;
}
/**
* Method to check all checkboxes on the browse view's table
*
* @param string $name The name of the form element
* @param string $tip The text shown as tooltip title instead of $tip
* @param string $action The action to perform on clicking the checkbox
*
* @return string
*
* @since 3.3.0
*/
public static function checkall($name = 'checkall-toggle', $tip = 'JGLOBAL_CHECK_ALL', $action = 'Joomla.checkAll(this)')
{
HTMLHelper::_('behavior.core');
HTMLHelper::_('bootstrap.tooltip');
return '<input type="checkbox" name="' . $name . '" value="" class="hasTooltip" title="' . HTMLHelper::_('tooltipText', $tip)
. '" onclick="' . $action . '" />';
}
/**
* Method to create a checkbox for a grid row.
*
* @param integer $rowNum The row index
* @param integer $recId The record id
* @param boolean $checkedOut True if item is checked out
* @param string $name The name of the form element
* @param string $stub The name of stub identifier
*
* @return mixed String of html with a checkbox if item is not checked out, null if checked out.
*
* @since 3.3.0
*/
public static function id($rowNum, $recId, $checkedOut = false, $name = 'cid', $stub = 'cb')
{
return $checkedOut ? '' : '<input type="checkbox" id="' . $stub . $rowNum . '" name="' . $name . '[]" value="' . $recId
. '" onclick="Joomla.isChecked(this.checked);" />';
}
/**
* Include the necessary JavaScript for the browse view's table order feature
*
* @param string $orderBy Filed by which we are currently sorting the table.
* @param bool $return Should I return the JS? Default: false (= add to the page's head)
*
* @return string
*/
public static function orderjs($orderBy, $return = false)
{
$js = <<< JS
Joomla.orderTable = function()
{
var table = document.getElementById("sortTable");
var direction = document.getElementById("directionTable");
var order = table.options[table.selectedIndex].value;
var dirn = 'asc';
if (order !== '$orderBy')
{
dirn = 'asc';
}
else {
dirn = direction.options[direction.selectedIndex].value;
}
Joomla.tableOrdering(order, dirn);
};
JS;
if ($return)
{
return $js;
}
try
{
Factory::getApplication()->getDocument()->addScriptDeclaration($js);
}
catch (Exception $e)
{
// If we have no application, well, not having table sorting JS is the least of your worries...
}
}
/**
* Returns the table ordering / pagination header for a browse view: number of records to display, order direction,
* order by field.
*
* @param DataViewRaw $view The view you're rendering against. If not provided we
* will guess it using MAGIC.
* @param array $sortFields Array of field name => description for the ordering
* fields in the dropdown. If not provided we will use all
* the fields available in the model.
* @param Pagination $pagination The Joomla pagination object. If not provided we fetch
* it from the view.
* @param string $sortBy Order by field name. If not provided we fetch it from
* the view.
* @param string $order_Dir Order direction. If not provided we fetch it from the
* view.
*
* @return string
*
* @since 3.3.0
*/
public static function orderheader(DataViewRaw $view = null, array $sortFields = [], Pagination $pagination = null, $sortBy = null, $order_Dir = null)
{
if (is_null($view))
{
$view = BrowseView::getViewFromBacktrace();
}
if (empty($sortFields))
{
/** @var DataModel $model */
$model = $view->getModel();
$sortFields = $view->getLists()->sortFields ?? [];
$sortFields = empty($sortFields) ? self::getSortFields($model) : $sortFields;
}
if (empty($pagination))
{
$pagination = $view->getPagination();
}
if (empty($sortBy))
{
$sortBy = $view->getLists()->order;
}
if (empty($order_Dir))
{
$order_Dir = $view->getLists()->order_Dir;
if (empty($order_Dir))
{
$order_Dir = 'asc';
}
}
// Static hidden text labels
$limitLabel = Text::_('JFIELD_PLG_SEARCH_SEARCHLIMIT_DESC');
$orderingDescr = Text::_('JFIELD_ORDERING_DESC');
$sortByLabel = Text::_('JGLOBAL_SORT_BY');
// Order direction dropdown
$directionSelect = HTMLHelper::_('FEFHelper.select.genericlist', [
'' => $orderingDescr,
'asc' => Text::_('JGLOBAL_ORDER_ASCENDING'),
'desc' => Text::_('JGLOBAL_ORDER_DESCENDING'),
], 'directionTable', [
'id' => 'directionTable',
'list.select' => $order_Dir,
'list.attr' => [
'class' => 'input-medium custom-select',
'onchange' => 'Joomla.orderTable()',
],
]);
// Sort by field dropdown
$sortTable = HTMLHelper::_('FEFHelper.select.genericlist', array_merge([
'' => Text::_('JGLOBAL_SORT_BY'),
], $sortFields), 'sortTable', [
'id' => 'sortTable',
'list.select' => $sortBy,
'list.attr' => [
'class' => 'input-medium custom-select',
'onchange' => 'Joomla.orderTable()',
],
]);
$html = <<<HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="limit" class="element-invisible">
$limitLabel
</label>
{$pagination->getLimitBox()}
</div>
<div class="akeeba-filter-element akeeba-form-group">
<label for="directionTable" class="element-invisible">
$orderingDescr
</label>
$directionSelect
</div>
<div class="akeeba-filter-element akeeba-form-group">
<label for="sortTable" class="element-invisible">
{$sortByLabel}
</label>
$sortTable
</div>
HTML;
return $html;
}
/**
* Get the default sort fields from a model. It creates a hash array where the keys are the model's field names and
* the values are the translation keys for their names, following FOF's naming conventions.
*
* @param DataModel $model The model for which we get the sort fields
*
* @return array
*
* @since 3.3.0
*/
private static function getSortFields(DataModel $model)
{
$sortFields = [];
$idField = $model->getIdFieldName() ?: 'id';
$defaultFieldLabels = [
'publish_up' => 'JGLOBAL_FIELD_PUBLISH_UP_LABEL',
'publish_down' => 'JGLOBAL_FIELD_PUBLISH_DOWN_LABEL',
'created_by' => 'JGLOBAL_FIELD_CREATED_BY_LABEL',
'created_on' => 'JGLOBAL_FIELD_CREATED_LABEL',
'modified_by' => 'JGLOBAL_FIELD_MODIFIED_BY_LABEL',
'modified_on' => 'JGLOBAL_FIELD_MODIFIED_LABEL',
'ordering' => 'JGLOBAL_FIELD_FIELD_ORDERING_LABEL',
'id' => 'JGLOBAL_FIELD_ID_LABEL',
'hits' => 'JGLOBAL_HITS',
'title' => 'JGLOBAL_TITLE',
'user_id' => 'JGLOBAL_USERNAME',
'username' => 'JGLOBAL_USERNAME',
];
$componentName = $model->getContainer()->componentName;
$viewNameSingular = $model->getContainer()->inflector->singularize($model->getName());
$viewNamePlural = $model->getContainer()->inflector->pluralize($model->getName());
foreach ($model->getFields() as $field => $fieldDescriptor)
{
$possibleKeys = [
$componentName . '_' . $viewNamePlural . '_FIELD_' . $field,
$componentName . '_' . $viewNamePlural . '_' . $field,
$componentName . '_' . $viewNameSingular . '_FIELD_' . $field,
$componentName . '_' . $viewNameSingular . '_' . $field,
];
if (array_key_exists($field, $defaultFieldLabels))
{
$possibleKeys[] = $defaultFieldLabels[$field];
}
if ($field === $idField)
{
$possibleKeys[] = $defaultFieldLabels['id'];
}
$fieldLabel = '';
foreach ($possibleKeys as $langKey)
{
$langKey = strtoupper($langKey);
$fieldLabel = Text::_($langKey);
if ($fieldLabel !== $langKey)
{
break;
}
$fieldLabel = '';
}
if (!empty($fieldLabel))
{
$sortFields[$field] = (new Joomla\Filter\InputFilter())->clean($fieldLabel);
}
}
return $sortFields;
}
}
Utils/Collection.php 0000604 00000032507 15245551424 0010462 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use 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.php 0000604 00000006260 15245551424 0010661 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
/**
* A utility class to help you quickly clean the Joomla! cache
*/
class CacheCleaner
{
/**
* Clears the com_modules and com_plugins cache. You need to call this whenever you alter the publish state or
* parameters of a module or plugin from your code.
*
* @return void
*/
public static function clearPluginsAndModulesCache()
{
self::clearPluginsCache();
self::clearModulesCache();
}
/**
* Clears the com_plugins cache. You need to call this whenever you alter the publish state or parameters of a
* plugin from your code.
*
* @return void
*/
public static function clearPluginsCache()
{
self::clearCacheGroups(['com_plugins'], [0, 1]);
}
/**
* Clears the com_modules cache. You need to call this whenever you alter the publish state or parameters of a
* module from your code.
*
* @return void
*/
public static function clearModulesCache()
{
self::clearCacheGroups(['com_modules'], [0, 1]);
}
/**
* Clears the specified cache groups.
*
* @param array $clearGroups Which cache groups to clear. Usually this is com_yourcomponent to clear
* your component's cache.
* @param array $cacheClients Which cache clients to clear. 0 is the back-end, 1 is the front-end. If you
* do not specify anything, both cache clients will be cleared.
* @param string|null $event An event to run upon trying to clear the cache. Empty string to disable. If
* NULL and the group is "com_content" I will trigger onContentCleanCache.
*
* @return void
*/
public static function clearCacheGroups(array $clearGroups, array $cacheClients = [0, 1], $event = null)
{
try
{
$app = Factory::getApplication();
}
catch (Exception $e)
{
return;
}
foreach ($clearGroups as $group)
{
foreach ($cacheClients as $client_id)
{
$options = [
'defaultgroup' => $group,
'cachebase' => ($client_id) ? JPATH_ADMINISTRATOR . '/cache' : $app->get('cache_path', JPATH_SITE . '/cache'),
'result' => true,
];
try
{
$cache = Cache::getInstance('callback', $options);
/** @noinspection PhpUndefinedMethodInspection Available via __call(), not tagged in Joomla core */
$cache->clean();
}
catch (Exception $e)
{
$options['result'] = false;
}
/**
* Call Joomla's cache cleaning plugin event (e.g. onContentCleanCache) as well.
*
* @see BaseDatabaseModel::cleanCache()
*/
if (!empty($event))
{
$app->triggerEvent($event, $options);
}
/**
* If you're cleaning com_content and you have passed no event name I will use onContentCleanCache.
*/
elseif (($group == 'com_content') && ($event !== ''))
{
$app->triggerEvent('onContentCleanCache', $options);
}
}
}
}
}
Utils/DynamicGroups.php 0000604 00000013433 15245551424 0011150 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\Container\Container;
use ReflectionClass;
use ReflectionException;
use ReflectionObject;
/**
* Dynamic user to user group assignment.
*
* This class allows you to add / remove the currently logged in user to a user group without writing the information to
* the database. This is useful when you want to allow core and third party code to allow or prohibit display of
* information and / or taking actions based on a condition controlled in your code.
*/
class DynamicGroups
{
/**
* Add the current user to a user group just for this page load.
*
* @param int $groupID The group ID to add the current user into.
*
* @return void
*/
public static function addGroup($groupID)
{
self::addRemoveGroup($groupID, true);
self::cleanUpUserObjectCache();
}
/**
* Remove the current user from a user group just for this page load.
*
* @param int $groupID The group ID to remove the current user from.
*
* @return void
*/
public static function removeGroup($groupID)
{
self::addRemoveGroup($groupID, false);
self::cleanUpUserObjectCache();
}
/**
* Internal function to add or remove the current user from a user group just for this page load.
*
* @param int $groupID The group ID to add / remove the current user from.
* @param bool $add Add (true) or remove (false) the user?
*
* @return void
*/
protected static function addRemoveGroup($groupID, $add)
{
// Get a fake container (we need it for its platform interface)
$container = Container::getInstance('com_FOOBAR');
/**
* Make sure that Joomla has retrieved the user's groups from the database.
*
* By going through the User object's getAuthorisedGroups we force Joomla to go through Access::getGroupsByUser
* which retrieves the information from the database and caches it into the Access helper class.
*/
$container->platform->getUser()->getAuthorisedGroups();
$container->platform->getUser($container->platform->getUser()->id)->getAuthorisedGroups();
/**
* Now we can get a Reflection object into Joomla's Access helper class and manipulate its groupsByUser cache.
*/
$className = class_exists('Joomla\\CMS\\Access\\Access') ? 'Joomla\\CMS\\Access\\Access' : 'JAccess';
try
{
$reflectedAccess = new ReflectionClass($className);
}
catch (ReflectionException $e)
{
// This should never happen!
$container->platform->logDebug('Cannot locate the Joomla\\CMS\\Access\\Access or JAccess class. Is your Joomla installation broken or too old / too new?');
return;
}
$groupsByUser = $reflectedAccess->getProperty('groupsByUser');
$groupsByUser->setAccessible(true);
$rawGroupsByUser = $groupsByUser->getValue();
/**
* Next up, we need to manipulate the keys of the cache which contain user to user group assignments.
*
* $rawGroupsByUser (JAccess::$groupsByUser) stored the group ownership as userID:recursive e.g. 0:1 for the
* default user, recursive. We need to deal with four keys: 0:1, 0:0, myID:1 and myID:0
*/
$user = $container->platform->getUser();
$keys = ['0:1', '0:0', $user->id . ':1', $user->id . ':0'];
foreach ($keys as $key)
{
if (!array_key_exists($key, $rawGroupsByUser))
{
continue;
}
$groups = $rawGroupsByUser[$key];
if ($add)
{
if (in_array($groupID, $groups))
{
continue;
}
$groups[] = $groupID;
}
else
{
if (!in_array($groupID, $groups))
{
continue;
}
$removeKey = array_search($groupID, $groups);
unset($groups[$removeKey]);
}
$rawGroupsByUser[$key] = $groups;
}
// We can commit our changes back to the cache property and make it publicly inaccessible again.
$groupsByUser->setValue(null, $rawGroupsByUser);
$groupsByUser->setAccessible(false);
/**
* We are not done. Caching user groups is only one aspect of Joomla access management. Joomla also caches the
* identities, i.e. the user group assignment per user, in a different cache. We need to reset it to for our
* user.
*
* Do note that we CAN NOT use clearStatics since that also clears the user group assignment which we assigned
* dynamically. Therefore calling it would destroy our work so far.
*/
$refProperty = $reflectedAccess->getProperty('identities');
$refProperty->setAccessible(true);
$identities = $refProperty->getValue();
$keys = [$user->id, 0];
foreach ($keys as $key)
{
if (!array_key_exists($key, $identities))
{
continue;
}
unset($identities[$key]);
}
$refProperty->setValue(null, $identities);
$refProperty->setAccessible(false);
}
/**
* Clean up the current user's authenticated groups cache.
*
* @return void
*/
protected static function cleanUpUserObjectCache()
{
// Get a fake container (we need it for its platform interface)
$container = Container::getInstance('com_FOOBAR');
$user = $container->platform->getUser();
$reflectedUser = new ReflectionObject($user);
// Clear the user group cache
$refProperty = $reflectedUser->getProperty('_authGroups');
$refProperty->setAccessible(true);
$refProperty->setValue($user, []);
$refProperty->setAccessible(false);
// Clear the view access level cache
$refProperty = $reflectedUser->getProperty('_authLevels');
$refProperty->setAccessible(true);
$refProperty->setValue($user, []);
$refProperty->setAccessible(false);
// Clear the authenticated actions cache. I haven't seen it used anywhere but it's there, so...
$refProperty = $reflectedUser->getProperty('_authActions');
$refProperty->setAccessible(true);
$refProperty->setValue($user, []);
$refProperty->setAccessible(false);
}
}
Utils/Ip.php 0000604 00000033306 15245551424 0006735 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
/**
* IP address helper
*
* Makes sure that we get the real IP of the user
*/
class Ip
{
/**
* The IP address of the current visitor
*
* @var string
*/
protected static $ip = null;
/**
* Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers?
*
* @var bool
*/
protected static $allowIpOverrides = true;
/**
* See self::detectAndCleanIP and setUseFirstIpInChain
*
* If this is enabled (default) self::detectAndCleanIP will return the FIRST IP in case there is an IP chain coming
* for example from an X-Forwarded-For HTTP header. When set to false it will simulate the old behavior in FOF up to
* and including 3.1.1 which returned the LAST IP in the list.
*
* @var bool
*/
protected static $useFirstIpInChain = true;
/**
* List of headers we should check to know if the user is behind a proxy. PLEASE NOTE: ORDER MATTERS!
*
* @var array
*/
protected static $proxyHeaders = [
'HTTP_CF_CONNECTING_IP', // CloudFlare
'HTTP_X_FORWARDED_FOR', // Standard for transparent proxy (e.g. NginX)
'HTTP_X_SUCURI_CLIENTIP', // Sucuri firewall uses its own header
];
/**
* Set the $useFirstIpInChain flag. See above.
*
* @param bool $value
*/
public static function setUseFirstIpInChain($value = true)
{
self::$useFirstIpInChain = $value;
}
/**
* Getter for the list of proxy headers we can check
*
* @return array
*/
public static function getProxyHeaders()
{
return static::$proxyHeaders;
}
/**
* Get the current visitor's IP address
*
* @return string
*/
public static function getIp()
{
if (is_null(static::$ip))
{
$ip = self::detectAndCleanIP();
if (!empty($ip) && ($ip != '0.0.0.0') && function_exists('inet_pton') && function_exists('inet_ntop'))
{
$myIP = @inet_pton($ip);
if ($myIP !== false)
{
$ip = inet_ntop($myIP);
}
}
static::setIp($ip);
}
return static::$ip;
}
/**
* Set the IP address of the current visitor
*
* @param string $ip
*
* @return void
*/
public static function setIp($ip)
{
static::$ip = $ip;
}
/**
* Checks if an IP is contained in a list of IPs or IP expressions
*
* @param string $ip The IPv4/IPv6 address to check
* @param array|string $ipTable An IP expression (or a comma-separated or array list of IP expressions) to
* check against
*
* @return null|boolean True if it's in the list
*/
public static function IPinList($ip, $ipTable = '')
{
// No point proceeding with an empty IP list
if (empty($ipTable))
{
return false;
}
// If the IP list is not an array, convert it to an array
if (!is_array($ipTable))
{
if (strpos($ipTable, ',') !== false)
{
$ipTable = explode(',', $ipTable);
$ipTable = array_map(function ($x) {
return trim($x);
}, $ipTable);
}
else
{
$ipTable = trim($ipTable);
$ipTable = [$ipTable];
}
}
// If no IP address is found, return false
if ($ip == '0.0.0.0')
{
return false;
}
// If no IP is given, return false
if (empty($ip))
{
return false;
}
// Sanity check
if (!function_exists('inet_pton'))
{
return false;
}
// Get the IP's in_adds representation
$myIP = @inet_pton($ip);
// If the IP is in an unrecognisable format, quite
if ($myIP === false)
{
return false;
}
$ipv6 = self::isIPv6($ip);
foreach ($ipTable as $ipExpression)
{
$ipExpression = trim($ipExpression);
// Inclusive IP range, i.e. 123.123.123.123-124.125.126.127
if (strstr($ipExpression, '-'))
{
[$from, $to] = explode('-', $ipExpression, 2);
if ($ipv6 && (!self::isIPv6($from) || !self::isIPv6($to)))
{
// Do not apply IPv4 filtering on an IPv6 address
continue;
}
elseif (!$ipv6 && (self::isIPv6($from) || self::isIPv6($to)))
{
// Do not apply IPv6 filtering on an IPv4 address
continue;
}
$from = @inet_pton(trim($from));
$to = @inet_pton(trim($to));
// Sanity check
if (($from === false) || ($to === false))
{
continue;
}
// Swap from/to if they're in the wrong order
if ($from > $to)
{
[$from, $to] = [$to, $from];
}
if (($myIP >= $from) && ($myIP <= $to))
{
return true;
}
}
// Netmask or CIDR provided
elseif (strstr($ipExpression, '/'))
{
$binaryip = self::inet_to_bits($myIP);
[$net, $maskbits] = explode('/', $ipExpression, 2);
if ($ipv6 && !self::isIPv6($net))
{
// Do not apply IPv4 filtering on an IPv6 address
continue;
}
elseif (!$ipv6 && self::isIPv6($net))
{
// Do not apply IPv6 filtering on an IPv4 address
continue;
}
elseif ($ipv6 && strstr($maskbits, ':'))
{
// Perform an IPv6 CIDR check
if (self::checkIPv6CIDR($myIP, $ipExpression))
{
return true;
}
// If we didn't match it proceed to the next expression
continue;
}
elseif (!$ipv6 && strstr($maskbits, '.'))
{
// Convert IPv4 netmask to CIDR
$long = ip2long($maskbits);
$base = ip2long('255.255.255.255');
$maskbits = 32 - log(($long ^ $base) + 1, 2);
}
// Convert network IP to in_addr representation
$net = @inet_pton($net);
// Sanity check
if ($net === false)
{
continue;
}
// Get the network's binary representation
$binarynet = self::inet_to_bits($net);
$expectedNumberOfBits = $ipv6 ? 128 : 24;
$binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);
// Check the corresponding bits of the IP and the network
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
if ($ip_net_bits == $net_bits)
{
return true;
}
}
else
{
// IPv6: Only single IPs are supported
if ($ipv6)
{
$ipExpression = trim($ipExpression);
if (!self::isIPv6($ipExpression))
{
continue;
}
$ipCheck = @inet_pton($ipExpression);
if ($ipCheck === false)
{
continue;
}
if ($ipCheck == $myIP)
{
return true;
}
}
else
{
// Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123]
$dots = 0;
if (substr($ipExpression, -1) == '.')
{
// Partial IP address. Convert to CIDR and re-match
foreach (count_chars($ipExpression, 1) as $i => $val)
{
if ($i == 46)
{
$dots = $val;
}
}
$netmask = '255.255.255.255';
switch ($dots)
{
case 1:
$netmask = '255.0.0.0';
$ipExpression .= '0.0.0';
break;
case 2:
$netmask = '255.255.0.0';
$ipExpression .= '0.0';
break;
case 3:
$netmask = '255.255.255.0';
$ipExpression .= '0';
break;
default:
$dots = 0;
}
if ($dots)
{
$binaryip = self::inet_to_bits($myIP);
// Convert netmask to CIDR
$long = ip2long($netmask);
$base = ip2long('255.255.255.255');
$maskbits = 32 - log(($long ^ $base) + 1, 2);
$net = @inet_pton($ipExpression);
// Sanity check
if ($net === false)
{
continue;
}
// Get the network's binary representation
$binarynet = self::inet_to_bits($net);
$expectedNumberOfBits = $ipv6 ? 128 : 24;
$binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);
// Check the corresponding bits of the IP and the network
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
if ($ip_net_bits == $net_bits)
{
return true;
}
}
}
if (!$dots)
{
$ip = @inet_pton(trim($ipExpression));
if ($ip == $myIP)
{
return true;
}
}
}
}
}
return false;
}
/**
* Works around the REMOTE_ADDR not containing the user's IP
*/
public static function workaroundIPIssues()
{
$ip = self::getIp();
if (array_key_exists('REMOTE_ADDR', $_SERVER) && ($_SERVER['REMOTE_ADDR'] == $ip))
{
return;
}
if (array_key_exists('REMOTE_ADDR', $_SERVER))
{
$_SERVER['FOF_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
}
elseif (function_exists('getenv'))
{
if (getenv('REMOTE_ADDR'))
{
$_SERVER['FOF_REMOTE_ADDR'] = getenv('REMOTE_ADDR');
}
}
$_SERVER['REMOTE_ADDR'] = $ip;
}
/**
* Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header?
*
* @param bool $newState True to allow the override
*
* @return void
*/
public static function setAllowIpOverrides($newState)
{
self::$allowIpOverrides = $newState ? true : false;
}
/**
* Is it an IPv6 IP address?
*
* @param string $ip An IPv4 or IPv6 address
*
* @return boolean True if it's IPv6
*/
protected static function isIPv6($ip)
{
if (strstr($ip, ':'))
{
return true;
}
return false;
}
/**
* Gets the visitor's IP address. Automatically handles reverse proxies
* reporting the IPs of intermediate devices, like load balancers. Examples:
* https://www.akeeba.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html
* http://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips
* The solution used is assuming that the first IP address is the external one (unless $useFirstIpInChain is set to
* false)
*
* @return string
*/
protected static function detectAndCleanIP()
{
$ip = static::detectIP();
if ((strstr($ip, ',') !== false) || (strstr($ip, ' ') !== false))
{
$ip = str_replace(' ', ',', $ip);
$ip = str_replace(',,', ',', $ip);
$ips = explode(',', $ip);
$ip = '';
// Loop until we're running out of parts or we have a hit
while ($ips)
{
$ip = array_shift($ips);
$ip = trim($ip);
if (self::$useFirstIpInChain)
{
return self::cleanIP($ip);
}
}
}
return self::cleanIP($ip);
}
protected static function cleanIP($ip)
{
$ip = trim($ip);
$ip = strtoupper($ip);
/**
* Work around IPv4-mapped addresses.
*
* IPv4 addresses may be embedded in an IPv6 address. This is always 80 zeroes, 16 ones and the IPv4 address.
* In all possible IPv6 notations this is:
* 0:0:0:0:0:FFFF:192.168.1.1
* ::FFFF:192.168.1.1
* ::FFFF:C0A8:0101
*
* @see http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding-2.htm
*/
if ((strpos($ip, '::FFFF:') === 0) || (strpos($ip, '0:0:0:0:0:FFFF:') === 0))
{
// Fast path: the embedded IPv4 is in decimal notation.
if (strstr($ip, '.') !== false)
{
return substr($ip, strrpos($ip, ':') + 1);
}
// Get the embedded IPv4 (in hex notation)
$ip = substr($ip, strpos($ip, ':FFFF:') + 6);
// Convert each 16-bit WORD to decimal
[$word1, $word2] = explode(':', $ip);
$word1 = hexdec($word1);
$word2 = hexdec($word2);
$longIp = $word1 * 65536 + $word2;
return long2ip($longIp);
}
return $ip;
}
/**
* Gets the visitor's IP address
*
* @return string
*/
protected static function detectIP()
{
// Normally the $_SERVER superglobal is set
if (isset($_SERVER))
{
// Do we have IP overrides enabled?
if (static::$allowIpOverrides)
{
// If so, check for every proxy header
foreach (static::$proxyHeaders as $header)
{
if (array_key_exists($header, $_SERVER))
{
return $_SERVER[$header];
}
}
}
// CLI applications
if (!array_key_exists('REMOTE_ADDR', $_SERVER))
{
return '';
}
// Normal, non-proxied server or server behind a transparent proxy
return $_SERVER['REMOTE_ADDR'];
}
// This part is executed on PHP running as CGI, or on SAPIs which do
// not set the $_SERVER superglobal
// If getenv() is disabled, you're screwed
if (!function_exists('getenv'))
{
return '';
}
// Do we have IP overrides enabled?
if (static::$allowIpOverrides)
{
// If so, check for every proxy header
foreach (static::$proxyHeaders as $header)
{
if (getenv($header))
{
return getenv($header);
}
}
}
// Normal, non-proxied server or server behind a transparent proxy
if (getenv('REMOTE_ADDR'))
{
return getenv('REMOTE_ADDR');
}
// Catch-all case for broken servers and CLI applications
return '';
}
/**
* Converts inet_pton output to bits string
*
* @param string $inet The in_addr representation of an IPv4 or IPv6 address
*
* @return string
*/
protected static function inet_to_bits($inet)
{
if (strlen($inet) == 4)
{
$unpacked = unpack('C4', $inet);
}
else
{
$unpacked = unpack('C16', $inet);
}
$binaryip = '';
foreach ($unpacked as $byte)
{
$binaryip .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
}
/**
* Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet
*
* @param string $ip The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A
* @param string $cidrnet The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64
*
* @return bool
*/
protected static function checkIPv6CIDR($ip, $cidrnet)
{
$ip = inet_pton($ip);
$binaryip = self::inet_to_bits($ip);
[$net, $maskbits] = explode('/', $cidrnet);
$net = inet_pton($net);
$binarynet = self::inet_to_bits($net);
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
return $ip_net_bits === $net_bits;
}
}
Utils/SelectOptions.php 0000604 00000024746 15245551424 0011170 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use InvalidArgumentException;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use stdClass;
/**
* Returns arrays of JHtml select options for Joomla-specific information such as access levels.
*/
class SelectOptions
{
private static $cache = [];
/**
* Magic method to handle static calls
*
* @param string $name The name of the static method being called
* @param string $arguments Ignored.
*
* @return mixed
* @since 3.3.0
*
*/
public static function __callStatic($name, $arguments)
{
return self::getOptions($name, $arguments);
}
/**
* Get a list of Joomla options of the type you specify. Supported types
* - access View access levels
* - usergroups User groups
* - cachehandlers Cache handlers
* - components Installed components accessible by the current user
* - languages Site or administrator languages
* - published Published status
*
* Global params:
* - cache Should I returned cached data? Default: true.
*
* See the private static methods of this class for more information on params.
*
* @param string $type The options type to get
* @param array $params Optional arguments, if they are supported by the options type.
*
* @return stdClass[]
* @since 3.3.0
*
*/
public static function getOptions($type, array $params = [])
{
if ((substr($type, 0, 1) == '_') || !method_exists(__CLASS__, $type))
{
throw new InvalidArgumentException(__CLASS__ . "does not support option type '$type'.");
}
$useCache = true;
if (isset($params['cache']))
{
$useCache = isset($params['cache']);
unset($params['cache']);
}
$cacheKey = sha1($type . '--' . print_r($params, true));
$fetchNew = !$useCache || ($useCache && !isset(self::$cache[$cacheKey]));
if ($fetchNew)
{
$ret = forward_static_call_array([__CLASS__, $type], [$params]);
}
if (!$useCache)
{
return $ret;
}
if ($fetchNew)
{
self::$cache[$cacheKey] = $ret;
}
return self::$cache[$cacheKey];
}
/**
* Joomla! Access Levels (previously: view access levels)
*
* Available params:
* - allLevels: Show an option for all levels (default: false)
*
* @param array $params Parameters
*
* @return stdClass[]
*
* @since 3.3.0
*
* @see \JHtmlAccess::level()
*/
private static function access(array $params = [])
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('a.id', 'value') . ', ' . $db->quoteName('a.title', 'text'))
->from($db->quoteName('#__viewlevels', 'a'))
->group($db->quoteName(['a.id', 'a.title', 'a.ordering']))
->order($db->quoteName('a.ordering') . ' ASC')
->order($db->quoteName('title') . ' ASC');
// Get the options.
$db->setQuery($query);
$options = $db->loadObjectList();
if (isset($params['allLevels']) && $params['allLevels'])
{
array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_LEVELS')));
}
return $options;
}
/**
* Joomla! User Groups
*
* Available params:
* - allGroups: Show an option for all groups (default: false)
*
* @param array $params Parameters
*
* @return stdClass[]
*
* @since 3.3.0
*
* @see \JHtmlAccess::usergroup()
*/
private static function usergroups(array $params = [])
{
$options = array_values(UserGroupsHelper::getInstance()->getAll());
for ($i = 0, $n = count($options); $i < $n; $i++)
{
$options[$i]->value = $options[$i]->id;
$options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title;
}
// If all usergroups is allowed, push it into the array.
if (isset($params['allGroups']) && $params['allGroups'])
{
array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_GROUPS')));
}
return $options;
}
/**
* Joomla cache handlers
*
* @return stdClass[]
* @since 3.3.0
*
*/
private static function cachehandlers()
{
$options = [];
// Convert to name => name array.
foreach (Cache::getStores() as $store)
{
$options[] = HTMLHelper::_('select.option', $store, Text::_('JLIB_FORM_VALUE_CACHE_' . $store), 'value', 'text');
}
return $options;
}
/**
* Get a list of all installed components and also translates them.
*
* Available params:
* - client_ids Array of Joomla application client IDs
*
* @param array $params
*
* @return stdClass[]
* @since 3.3.0
*
*/
private static function components(array $params)
{
$db = Factory::getDbo();
// Check for client_ids override
$client_ids = $params['client_ids'] ?? [0, 1];
if (is_string($client_ids))
{
$client_ids = explode(',', $client_ids);
}
// Calculate client_ids where clause
$client_ids = array_map(function ($client_id) use ($db) {
return $db->q((int) trim($client_id));
}, $client_ids);
$query = $db->getQuery(true)
->select(
[
$db->qn('name'),
$db->qn('element'),
$db->qn('client_id'),
$db->qn('manifest_cache'),
]
)
->from($db->qn('#__extensions'))
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('client_id') . ' IN (' . implode(',', $client_ids) . ')');
$components = $db->setQuery($query)->loadObjectList('element');
// Convert to array of objects, so we can use sortObjects()
// Also translate component names with JText::_()
$aComponents = [];
$user = Factory::getUser();
foreach ($components as $component)
{
// Don't show components in the list where the user doesn't have access for
// TODO: perhaps add an option for this
if (!$user->authorise('core.manage', $component->element))
{
continue;
}
$aComponents[$component->element] = (object) [
'value' => $component->element,
'text' => self::_translateComponentName($component),
];
}
// Reorder the components array, because the alphabetical
// ordering changed due to the JText::_() translation
uasort(
$aComponents,
function ($a, $b) {
return strcasecmp($a->text, $b->text);
}
);
return $aComponents;
}
/**
* Method to get the field options.
*
* Available params:
* - client 'site' (default) or 'administrator'
* - none Text to show for "all languages" option, use empty string to remove it
*
* @return array Languages for the specified client
* @since 3.3.0
*
*/
private static function languages($params)
{
$db = Factory::getDbo();
$client = $params['client'] ?? 'site';
if (!in_array($client, ['site', 'administrator']))
{
$client = 'site';
}
// Make sure the languages are sorted base on locale instead of random sorting
$options = LanguageHelper::createLanguageList(null, constant('JPATH_' . strtoupper($client)), true, true);
if (count($options) > 1)
{
usort(
$options,
function ($a, $b) {
return strcmp($a['value'], $b['value']);
}
);
}
$none = $params['none'] ?? '*';
if (!empty($none))
{
array_unshift($options, HTMLHelper::_('select.option', '*', Text::_($none)));
}
return $options;
}
/**
* Options for a Published field
*
* Params:
* - none Placeholder for no selection (empty key). Default: null.
* - published Show "Published"? Default: true
* - unpublished Show "Unpublished"? Default: true
* - archived Show "Archived"? Default: false
* - trash Show "Trashed"? Default: false
* - all Show "All" option? This is different than none, the key is '*'. Default: false
*
* @param $params
*
* @return array
*/
private static function published(array $params = [])
{
$config = array_merge([
'none' => '',
'published' => true,
'unpublished' => true,
'archived' => false,
'trash' => false,
'all' => false,
], $params);
$options = [];
if (!empty($config['none']))
{
$options[] = HTMLHelper::_('select.option', '', Text::_($config['none']));
}
if ($config['published'])
{
$options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED'));
}
if ($config['unpublished'])
{
$options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED'));
}
if ($config['archived'])
{
$options[] = HTMLHelper::_('select.option', '2', Text::_('JARCHIVED'));
}
if ($config['trash'])
{
$options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED'));
}
if ($config['all'])
{
$options[] = HTMLHelper::_('select.option', '*', Text::_('JALL'));
}
return $options;
}
/**
* Options for a Published field
*
* Params:
* - none Placeholder for no selection (empty key). Default: null.
*
* @param $params
*
* @return array
*/
private static function boolean(array $params = [])
{
$config = array_merge([
'none' => '',
], $params);
$options = [];
if (!empty($config['none']))
{
$options[] = HTMLHelper::_('select.option', '', Text::_($config['none']));
}
$options[] = HTMLHelper::_('select.option', '1', Text::_('JYES'));
$options[] = HTMLHelper::_('select.option', '0', Text::_('JNO'));
return $options;
}
/**
* Translate a component name
*
* @param stdClass $item The component object
*
* @return string $text The translated name of the extension
*
* @since 3.3.0
*
* @see administrator/com_installer/models/extension.php
*/
private static function _translateComponentName($item)
{
// Map the manifest cache to $item. This is needed to get the name from the
// manifest_cache and NOT from the name column, else some JText::_() translations fails.
$mData = json_decode($item->manifest_cache);
if ($mData)
{
foreach ($mData as $key => $value)
{
if ($key == 'type')
{
// Ignore the type field
continue;
}
$item->$key = $value;
}
}
$lang = Factory::getLanguage();
$source = JPATH_ADMINISTRATOR . '/components/' . $item->element;
$lang->load("$item->element.sys", JPATH_ADMINISTRATOR, null, false, false)
|| $lang->load("$item->element.sys", $source, null, false, false)
|| $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, $lang->getDefault(), false, false)
|| $lang->load("$item->element.sys", $source, $lang->getDefault(), false, false);
return Text::_($item->name);
}
}
Utils/InstallScript.php 0000604 00000001360 15245551424 0011153 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\Utils\InstallScript\Component;
// Make sure the new class can be loaded
if (!class_exists('FOF30\\Utils\\InstallScript\\Component', true))
{
require_once __DIR__ . '/InstallScript/Component.php';
}
/**
* A helper class which you can use to create component installation scripts.
*
* This is the old location of the installation script class, maintained for backwards compatibility with FOF 3.0. Please
* use the new class FOF30\Utils\InstallScript\Component instead.
*/
class InstallScript extends Component
{
}
Utils/ArrayHelper.php 0000604 00000031320 15245551424 0010575 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use 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.php 0000604 00000014700 15245551424 0007573 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
/**
* 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.php 0000604 00000014115 15245551424 0011272 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use FOF30\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.php 0000604 00000023103 15245551424 0011653 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use DateTime;
use DateTimeZone;
use Exception;
use FOF30\Container\Container;
use FOF30\Date\Date;
use Joomla\CMS\User\User;
/**
* A helper class to wrangle timezones, as used by Joomla!.
*
* @package FOF30\Utils
*
* @since 3.1.3
*/
class TimezoneWrangler
{
/**
* The default timestamp format string to use when one is not provided
*
* @var string
*/
protected $defaultFormat = 'Y-m-d H:i:s T';
/**
* When set, this timezone will be used instead of the Joomla! applicable timezone for the user.
*
* @var DateTimeZone
*/
protected $forcedTimezone = null;
/**
* Cache of user IDs to applicable timezones
*
* @var array
*/
protected $userToTimezone = [];
/**
* The component container for which we are created
*
* @var Container
*/
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Get the default timestamp format to use when one is not provided
*
* @return string
*/
public function getDefaultFormat()
{
return $this->defaultFormat;
}
/**
* Set the default timestamp format to use when one is not provided
*
* @param string $defaultFormat
*
* @return void
*/
public function setDefaultFormat($defaultFormat)
{
$this->defaultFormat = $defaultFormat;
}
/**
* Returns the forced timezone which is used instead of the applicable Joomla! timezone.
*
* @return DateTimeZone
*/
public function getForcedTimezone()
{
return $this->forcedTimezone;
}
/**
* Sets the forced timezone which is used instead of the applicable Joomla! timezone. If the new timezone is
* different than the existing one we will also reset the user to timezone cache.
*
* @param DateTimeZone|string $forcedTimezone
*
* @return void
*/
public function setForcedTimezone($forcedTimezone)
{
// Are we unsetting the forced TZ?
if (empty($forcedTimezone))
{
$this->forcedTimezone = null;
$this->resetCache();
return;
}
// If the new TZ is a string we have to create an object
if (is_string($forcedTimezone))
{
$forcedTimezone = new DateTimeZone($forcedTimezone);
}
$oldTZ = '';
if (is_object($this->forcedTimezone) && ($this->forcedTimezone instanceof DateTimeZone))
{
$oldTZ = $this->forcedTimezone->getName();
}
if ($oldTZ == $forcedTimezone->getName())
{
return;
}
$this->forcedTimezone = $forcedTimezone;
$this->resetCache();
}
/**
* Reset the user to timezone cache. This is done automatically every time you change the forced timezone.
*/
public function resetCache()
{
$this->userToTimezone = [];
}
/**
* Get the applicable timezone for a user. If the user is not a guest and they have a timezone set up in their
* profile it will be used. Otherwise we fall back to the Server Timezone as set up in Global Configuration. If that
* fails, we use GMT. However, if you have used a non-blank forced timezone that will be used instead, circumventing
* this calculation. Therefore the returned timezone is one of the following, by descending order of priority:
* - Forced timezone
* - User's timezone (explicitly set in their user profile)
* - Server Timezone (from Joomla's Global Configuration)
* - GMT
*
* @param User|null $user
*
* @return DateTimeZone
*/
public function getApplicableTimezone($user = null)
{
// If we have a forced timezone use it instead of trying to figure anything out.
if (is_object($this->forcedTimezone))
{
return $this->forcedTimezone;
}
// No user? Get the current user.
if (is_null($user))
{
$user = $this->container->platform->getUser();
}
// If there is a cached timezone return that instead.
if (isset($this->userToTimezone[$user->id]))
{
return $this->userToTimezone[$user->id];
}
// Prefer the user timezone if it's set.
if (!$user->guest)
{
$tz = $user->getParam('timezone', null);
if (!empty($tz))
{
try
{
$this->userToTimezone[$user->id] = new DateTimeZone($tz);
return $this->userToTimezone[$user->id];
}
catch (Exception $e)
{
}
}
}
// Get the Server Timezone from Global Configuration with a fallback to GMT
$tz = $this->container->platform->getConfig()->get('offset', 'GMT');
try
{
$this->userToTimezone[$user->id] = new DateTimeZone($tz);
}
catch (Exception $e)
{
// If an invalid timezone was set we get to use GMT
$this->userToTimezone[$user->id] = new DateTimeZone('GMT');
}
return $this->userToTimezone[$user->id];
}
/**
* Returns a FOF Date object with its timezone set to the user's applicable timezone.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
* timezone is specified in a date string we assume it's GMT.
*
* @param User $user Applicable user for timezone calculation. Null = current user.
* @param mixed $time Source time. Leave blank for current date/time.
*
* @return Date
*/
public function getLocalDateTime($user, $time = null)
{
$time = empty($time) ? 'now' : $time;
$date = new Date($time);
$tz = $this->getApplicableTimezone($user);
$date->setTimezone($tz);
return $date;
}
/**
* Returns a FOF Date object with its timezone set to GMT.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
* timezone is specified in a date string we assume it's the user's applicable timezone.
*
* @param User $user
* @param mixed $time
*
* @return Date
*/
public function getGMTDateTime($user, $time)
{
$time = empty($time) ? 'now' : $time;
$tz = $this->getApplicableTimezone($user);
$date = new Date($time, $tz);
$gmtTimezone = new DateTimeZone('GMT');
$date->setTimezone($gmtTimezone);
return $date;
}
/**
* Returns a formatted date string in the user's applicable timezone.
*
* If no format is specified we will use $defaultFormat.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
* timezone is specified in a date string we assume it's GMT.
*
* $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). JApplicationCms does that
* for you automatically. If you're under CLI, a custom JApplicationWeb etc you will probably have to load this
* file
* manually.
*
* @param string|null $format Timestamp format. If empty $defaultFormat is used.
* @param User|null $user Applicable user for timezone calculation. Null = current
* user.
* @param DateTime|Date|string|int|null $time Source time. Leave blank for current date/time.
* @param bool $translate Translate day of week and month names?
*
* @return string
*/
public function getLocalTimeStamp($format = null, $user = null, $time = null, $translate = false)
{
$date = $this->getLocalDateTime($user, $time);
$format = empty($format) ? $this->defaultFormat : $format;
return $date->format($format, true, $translate);
}
/**
* Returns a formatted date string in the GMT timezone.
*
* If no format is specified we will use $defaultFormat.
*
* If no user is specified the current user will be used.
*
* $time can be a DateTime object (including Date and JDate), an integer (UNIX timestamp) or a date string. If no
* timezone is specified in a date string we assume it's the user's applicable timezone.
*
* $translate requires you to have loaded the relevant translation file (e.g. en-GB.ini). JApplicationCms does that
* for you automatically. If you're under CLI, a custom JApplicationWeb etc you will probably have to load this
* file
* manually.
*
* @param string|null $format Timestamp format. If empty $defaultFormat is used.
* @param User|null $user Applicable user for timezone calculation. Null = current
* user.
* @param DateTime|Date|string|int|null $time Source time. Leave blank for current date/time.
* @param bool $translate Translate day of week and month names?
*
* @return string
*/
public function getGMTTimeStamp($format = null, $user = null, $time = null, $translate = false)
{
$date = $this->localToGMT($user, $time);
$format = empty($format) ? $this->defaultFormat : $format;
return $date->format($format, true, $translate);
}
/**
* Convert a local time back to GMT. Returns a Date object with its timezone set to GMT.
*
* This is an alias to getGMTDateTime
*
* @param string|Date $time
* @param User|null $user
*
* @return Date
*/
public function localToGMT($time, $user = null)
{
return $this->getGMTDateTime($user, $time);
}
/**
* Convert a GMT time to local timezone. Returns a Date object with its timezone set to the applicable user's TZ.
*
* This is an alias to getLocalDateTime
*
* @param string|Date $time
* @param User|null $user
*
* @return Date
*/
public function GMTToLocal($time, $user = null)
{
return $this->getLocalDateTime($user, $time);
}
}
Utils/MediaVersion.php 0000604 00000011527 15245551424 0010753 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Utils;
defined('_JEXEC') || die;
use Exception;
use 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.xml 0000604 00000011167 15245551424 0007213 0 ustar 00 <?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>