Your IP : 216.73.216.190


Current Path : /proc/1908984/root/proc/2603263/cwd/
Upload File :
Current File : //proc/1908984/root/proc/2603263/cwd/Html.tar

FEFHelper/BrowseView.php000064400000070464152346061040011125 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace FOF40\Html\FEFHelper;

defined('_JEXEC') || die;

use FOF40\Container\Container;
use FOF40\Html\SelectOptions;
use FOF40\Model\DataModel;
use FOF40\Utils\ArrayHelper;
use FOF40\View\DataView\DataViewInterface;
use FOF40\View\View;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;

/**
 * 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(string $fieldName): string
	{
		$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(string $fieldName): string
	{
		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  string
	 */
	public static function sortgrid(string $field, ?string $langKey = null): string
	{
		/** @var DataViewInterface $view */
		$view = self::getViewFromBacktrace();

		if (is_null($langKey))
		{
			$langKey = self::fieldLabelKey($field);
		}

		return HTMLHelper::_('FEFHelp.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(string $localField, string $modelTitleField = 'title', ?string $modelName = null,
	                                   ?string $placeholder = null, array $params = []): string
	{
		/** @var DataModel $model */
		$model = self::getViewFromBacktrace()->getModel();

		if (empty($modelName))
		{
			$modelName = $model->getForeignModelNameFor($localField);
		}

		if (is_null($placeholder))
		{
			$placeholder = self::fieldLabelKey($localField);
		}

		$params = array_merge([
			'list.none'      => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
			'value_field'    => $modelTitleField,
			'fof.autosubmit' => true,
		], $params);

		return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
	}

	/**
	 * Display a text filter (search box)
	 *
	 * @param   string  $localField   The name of the model field. Used when getting the filter state.
	 * @param   string  $searchField  The INPUT element's name. Default: "filter_$localField".
	 * @param   string  $placeholder  The Text 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(string $localField, ?string $searchField = null, ?string $placeholder = null,
	                                    array $attributes = []): string
	{
		/** @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['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder'];
		$attributes['title']       = $attributes['title'] ?? $attributes['placeholder'];
		$attributes['value']       = $view->escape($model->getState($localField));

		if (!isset($attributes['onchange']))
		{
			$attributes['class'] = trim(($attributes['class'] ?? '') . ' akeebaCommonEventsOnChangeSubmit');
			$attributes['data-akeebasubmittarget'] = $attributes['data-akeebasubmittarget'] ?? 'adminForm';
		}

		// 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 HTMLHelper 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(string $localField, array $options, ?string $placeholder = null,
	                                    array $params = []): string
	{
		/** @var DataModel $model */
		$model = self::getViewFromBacktrace()->getModel();

		if (is_null($placeholder))
		{
			$placeholder = self::fieldLabelKey($localField);
		}

		$params = array_merge([
			'list.none'      => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
			'fof.autosubmit' => true,
		], $params);

		return self::genericSelect($localField, $options, $model->getState($localField), $params);
	}

	/**
	 * View access dropdown filter
	 *
	 * @param   string  $localField   Field name
	 * @param   string  $placeholder  Placeholder for no selection
	 * @param   array   $params       Generic select display parameters
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function accessFilter(string $localField, ?string $placeholder = null, array $params = []): string
	{
		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(string $localField, ?string $placeholder = null, array $params = []): string
	{
		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   mixed   $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 HTMLHelper 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(string $name, string $modelName, $currentValue, array $params = [],
	                                   array $modelState = [], array $options = []): string
	{
		$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 HTMLHelper 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(string $value, ?string $modelName = null, array $params = [],
	                                       array $modelState = [], array $options = []): ?string
	{
		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 HTMLHelper options
	 *
	 * @param   mixed   $selected     The currently selected value
	 * @param   array   $data         The HTMLHelper 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, array $data, string $optKey = 'value', string $optText = 'text', bool $selectFirst = true): ?string
	{
		$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 Text::_(). 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   string  $name
	 * @param   array   $options
	 * @param   mixed   $currentValue
	 * @param   array   $params
	 *
	 * @return string
	 *
	 * @since 3.3.0
	 */
	public static function genericSelect(string $name, array $options, $currentValue, array $params = []): string
	{
		$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'];

		$classes = $params['class'] ?? '';
		$classes = is_array($classes) ? implode(' ', $classes) : $classes;

		// 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';
			$classes                           .= ' akeebaCommonEventsOnChangeSubmit';
			$params['data-akeebasubmittarget'] = $formName;
		}

		// Construct SELECT element's attributes
		$attr = [
			'class'         => trim($classes) ?: null,
			'size'          => ($params['size'] ?? null) ?: null,
			'multiple'      => ($params['multiple'] ?? null) ?: null,
			'required'      => ($params['required'] ?? false) ?: null,
			'aria-required' => ($params['required'] ?? false) ? 'true' : null,
			'autofocus'     => ($params['autofocus'] ?? false) ?: null,
			'disabled'      => (($params['disabled'] ?? false) || ($params['readonly'] ?? false)) ?: null,
			'onchange'      => $params['onchange'] ?? null,
		];

		$attr = array_filter($attr, function ($x) {
			return !is_null($x);
		});

		// We merge the constructed SELECT element's attributes with the 'list.attr' array, if it was provided
		$params['list.attr'] = array_merge($attr, (($params['list.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::_('FEFHelp.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::_('FEFHelp.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) === 0)
				{
					$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::_('FEFHelp.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(string $text, DataModel $item): string
	{
		$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(): View
	{
		// In case we are on a brain-dead 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;
		}
	}

	/**
	 * Get HTMLHelper 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 Text? Default: true source_format        Set
	 * to "optionsobject" if you're returning an array of HTMLHelper options. Ignored otherwise.
	 *
	 * @param   array  $attribs
	 *
	 * @return array
	 *
	 * @since 3.3.0
	 */
	private static function getOptionsFromSource(array $attribs = []): array
	{
		$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
			// ...and so does the option
			if (class_exists($source_class, true) && 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::_('FEFHelp.select.option', $key, $value, 'value', 'text');
					}
				}
			}
		}

		reset($options);

		return $options;
	}

	/**
	 * Get HTMLHelper 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 Text? 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 HTMLHelper select options you want to add in front of the model's returned
	 *                               values
	 *
	 * @return mixed
	 *
	 * @since 3.3.0
	 */
	private static function getOptionsFromModel(string $modelName, array $params = [], array $modelState = [],
	                                            array $options = []): array
	{
		// Let's find the FOF DI container from the call stack
		$container = self::getContainerFromBacktrace();

		// Explode model name into component name and prefix
		$componentName = $container->componentName;
		$mName         = $modelName;

		if (strpos($modelName, '.') !== false)
		{
			[$componentName, $mName] = explode('.', $mName, 2);
		}

		if ($componentName !== $container->componentName)
		{
			$container = Container::getInstance($componentName);
		}

		/** @var DataModel $model */
		$model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false);

		$defaultParams = [
			'key_field'    => $model->getKeyName(),
			'value_field'  => 'title',
			'apply_access' => false,
			'none'         => null,
			'none_as_zero' => false,
			'translate'    => true,
			'with'         => [],
		];

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

		$cache    = isset($params['cache']) && $params['cache'];
		$cacheKey = null;

		if ($cache)
		{
			$cacheKey = sha1(print_r([
				$model->getContainer()->componentName,
				$model->getName(),
				$params['key_field'],
				$params['value_field'],
				$params['apply_access'],
				$params['none'],
				$params['translate'],
				$params['with'],
				$modelState,
			], true));
		}

		if ($cache && isset(self::$cacheModelOptions[$cacheKey]))
		{
			return self::$cacheModelOptions[$cacheKey];
		}

		if (empty($params['none']) && !is_null($params['none']))
		{
			$langKey     = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName());
			$placeholder = Text::_($langKey);

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

		if (!empty($params['none']))
		{
			$options[] = HTMLHelper::_('FEFHelp.select.option', null, Text::_($params['none']));

			if ($params['none_as_zero'])
			{
				$options[] = HTMLHelper::_('FEFHelp.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);

		foreach ($items as $item)
		{
			$value = $item->{$params['value_field']};

			if ($params['translate'])
			{
				$value = Text::_($value);
			}

			$options[] = HTMLHelper::_('FEFHelp.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(): Container
	{
		// In case we are on a brain-dead 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 !== 0)
			{
				$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;
		}
	}

}
FEFHelper/browse.php000064400000074702152346061040010331 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

defined('_JEXEC') || die;

use FOF40\Html\FEFHelper\BrowseView;
use FOF40\Model\DataModel;
use FOF40\Utils\ArrayHelper;
use FOF40\View\DataView\DataViewInterface;
use FOF40\View\DataView\Raw as DataViewRaw;
use Joomla\CMS\Factory as JoomlaFactory;
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 HTMLHelper::_('FEFHelp.browse.methodName', $parameter1, $parameter2, ...)
 *
 * @noinspection PhpIllegalPsrClassPathInspection
 */
abstract class FEFHelpBrowse
{
	/**
	 * 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(int $i, string $task, $prefix = '', string $active_title = '',
	                              string $inactive_title = '', bool $tip = false,
	                              string $active_class = '', string $inactive_class = '',
	                              bool $enabled = true, bool $translate = true, string $checkbox = 'cb'): string
	{
		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);
		}

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

			if (substr($active_class, 0, 2) == '--')
			{
				[$btnColor, $active_class] = explode(' ', $active_class, 2);
				$btnColor = ltrim($btnColor, '-');
			}

			$html   = [];
			$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini ' . ($active_class === 'publish' ? ' active' : '') . ($tip ? ' hasTooltip' : '') . '"';
			$html[] = ' href="javascript:void(0);" onclick="return Joomla.listItemTask(\'' . $checkbox . $i . '\',\'' . $prefix . $task . '\')"';
			$html[] = $tip ? ' title="' . $title . '"' : '';
			$html[] = '>';
			$html[] = '<span class="akion-' . $active_class . '" aria-hidden="true"></span>&ensp;';
			$html[] = '</a>';

			return implode($html);
		}

		$btnColor = 'grey';

		if (substr($inactive_class, 0, 2) == '--')
		{
			[$btnColor, $inactive_class] = explode(' ', $inactive_class, 2);
			$btnColor = ltrim($btnColor, '-');
		}

		$html   = [];
		$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini disabled akeebagrid' . ($tip ? ' hasTooltip' : '') . '"';
		$html[] = $tip ? ' title="' . $title . '"' : '';
		$html[] = '>';

		if ($active_class === 'protected')
		{
			$inactive_class = 'locked';
		}

		$html[] = '<span class="akion-' . $inactive_class . '"></span>&ensp;';
		$html[] = '</a>';

		return implode($html);
	}

	/**
	 * Returns a state change button on the browse view's table
	 *
	 * @param   array         $states     array of value/state. Each state is an array of the form
	 *                                    (task, text, active title, inactive title, tip (boolean), HTML active class,
	 *                                    HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active
	 *                                    title,
	 *                                    'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active
	 *                                    class,
	 *                                    'inactive_class'=>html inactive class)
	 * @param   integer       $value      The state value.
	 * @param   integer       $i          The row index
	 * @param   string|array  $prefix     An optional task prefix or an array of options
	 * @param   boolean       $enabled    An optional setting for access control on the action.
	 * @param   boolean       $translate  An optional setting for translation.
	 * @param   string        $checkbox   An optional prefix for checkboxes.
	 *
	 * @return  string  The HTML markup
	 *
	 * @since   3.3.0
	 */
	public static function state(array $states, int $value, int $i, $prefix = '', bool $enabled = true,
	                             bool $translate = true, string $checkbox = 'cb'): string
	{
		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(int $value, int $i, $prefix = '', bool $enabled = true, string $checkbox = 'cb',
	                                 ?string $publish_up = null, ?string $publish_down = null): string
	{
		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 = JoomlaFactory::getDbo()->getNullDate();
			$nowDate  = JoomlaFactory::getDate()->toUnix();

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

			$publish_up   = (!empty($publish_up) && ($publish_up != $nullDate)) ? JoomlaFactory::getDate($publish_up, 'UTC')->setTimeZone($tz) : false;
			$publish_down = (!empty($publish_down) && ($publish_down != $nullDate)) ? JoomlaFactory::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 (array_keys($states) as $key)
			{
				// 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(int $value, int $i, $prefix = '', bool $enabled = true, string $checkbox = 'cb'): string
	{
		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(int $i, string $editorName, string $time, $prefix = '', bool $enabled = false,
	                                  string $checkbox = 'cb'): string
	{
		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(string $orderingField, ?string $order, string $class = 'input-sm',
	                             string $icon = 'akion-android-more-vertical',
	                             string $inactiveIcon = 'akion-android-more-vertical',
	                             DataViewInterface $view = null): string
	{
		$order = $order ?? 'asc';

		/** @var \FOF40\View\DataView\Html $view */
		if (is_null($view))
		{
			$view = BrowseView::getViewFromBacktrace();
		}
		$dndOrderingActive = $view->getLists()->order == $orderingField;

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

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

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

			if (!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)
			{
				$html .= '<input type="text" name="order[]" style="display: none" 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(string $orderingField = 'ordering', string $icon = 'akion-stats-bars'): string
	{
		$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(int $i, string $task = 'orderup', $prefix = '', string $text = 'JLIB_HTML_MOVE_UP',
	                               bool $enabled = true, string $checkbox = 'cb'): string
	{
		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(int $i, string $task = 'orderdown', string $prefix = '',
	                                 string $text = 'JLIB_HTML_MOVE_DOWN', bool $enabled = true,
	                                 string $checkbox = 'cb'): string
	{
		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(string $title, string $order, ?string $direction = 'asc', string $selected = '',
	                            ?string $task = null, string $new_direction = 'asc', string $tip = '',
	                            ?string $form = null): string
	{
		HTMLHelper::_('behavior.core');
		HTMLHelper::_('bootstrap.popover');

		$direction = strtolower($direction ?? 'asc');
		$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>';
		}

		return $html . '</a>';
	}

	/**
	 * 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(string $name = 'checkall-toggle', string $tip = 'JGLOBAL_CHECK_ALL',
	                                string $action = 'Joomla.checkAll(this)'): string
	{
		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   mixed    $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, empty if checked out.
	 *
	 * @since   3.3.0
	 */
	public static function id(int $rowNum, $recId, bool $checkedOut = false, string $name = 'cid',
	                          string $stub = 'cb'): string
	{
		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(string $orderBy, bool $return = false): ?string
	{
		$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
		{
			JoomlaFactory::getApplication()->getDocument()->addScriptDeclaration($js);
		}
		catch (Exception $e)
		{
			// If we have no application, well, not having table sorting JS is the least of your worries...
		}

		return null;
	}

	/**
	 * 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,
	                                   ?string $sortBy = null, ?string $order_Dir = null): string
	{
		if (is_null($view))
		{
			$view = BrowseView::getViewFromBacktrace();
		}

		$showBrowsePagination = $view->showBrowsePagination ?? true;
		$showBrowseOrdering   = $view->showBrowseOrdering ?? true;
		$showBrowseOrderBy    = $view->showBrowseOrderBy ?? true;

		if (!$showBrowsePagination && !$showBrowseOrdering && !$showBrowseOrderBy)
		{
			return '';
		}

		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');
		$orderingDecr = Text::_('JFIELD_ORDERING_DESC');
		$sortByLabel  = Text::_('JGLOBAL_SORT_BY');

		// Order direction dropdown
		$directionSelect = HTMLHelper::_('FEFHelp.select.genericlist', [
			''     => $orderingDecr,
			'asc'  => Text::_('JGLOBAL_ORDER_ASCENDING'),
			'desc' => Text::_('JGLOBAL_ORDER_DESCENDING'),
		], 'directionTable', [
			'id'          => 'directionTable',
			'list.select' => $order_Dir,
			'list.attr'   => [
				'class'    => 'input-medium custom-select akeebaCommonEventsOnChangeOrderTable',
			],
		]);

		// Sort by field dropdown

		$sortTable = HTMLHelper::_('FEFHelp.select.genericlist', array_merge([
			'' => Text::_('JGLOBAL_SORT_BY'),
		], $sortFields), 'sortTable', [
			'id'          => 'sortTable',
			'list.select' => $sortBy,
			'list.attr'   => [
				'class'    => 'input-medium custom-select akeebaCommonEventsOnChangeOrderTable',
			],
		]);

		$html = '';

		if ($showBrowsePagination)
		{
			$html .= <<< HTML
		<div class="akeeba-filter-element akeeba-form-group">
			<label for="limit" class="element-invisible">
				$limitLabel
			</label>
			{$pagination->getLimitBox()}
		</div>

HTML;
		}

		if ($showBrowseOrdering)
		{
			$html .= <<< HTML
		<div class="akeeba-filter-element akeeba-form-group">
			<label for="directionTable" class="element-invisible">
				$orderingDecr
			</label>
			$directionSelect
		</div>

HTML;
		}

		if ($showBrowseOrderBy)
		{
			$html .= <<< HTML
		<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): array
	{
		$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 (array_keys($model->getFields()) as $field)
		{
			$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;
	}
}
FEFHelper/edit.php000064400000002753152346061040007752 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

defined('_JEXEC') || die;

use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory as JoomlaFactory;

/**
 * Custom JHtml (HTMLHelper) class. Offers edit (form) view controls compatible with Akeeba Frontend
 * Framework (FEF).
 *
 * Call these methods as HTMLHelper::_('FEFHelp.edit.methodName', $parameter1, $parameter2, ...)
 */
abstract class FEFHelpEdit
{
	public static function editor(string $fieldName, ?string $value, array $params = []): string
	{
		$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 = JoomlaFactory::getConfig()->get('editor');
			$user   = JoomlaFactory::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);
	}
}
FEFHelper/select.php000064400000075642152346061040010313 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

defined('_JEXEC') || die;

use FOF40\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 HTMLHelper::_('FEFHelp.select.methodName', $parameter1, $parameter2, ...)
 *
 * @noinspection PhpIllegalPsrClassPathInspection
 */
abstract class FEFHelpSelect
{
	/**
	 * 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   mixed   $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
	 *
	 * @see     JFormFieldRadio
	 */
	public static function booleanlist($name, $attribs = [], $selected = null, $yes = 'JYES', $no = 'JNO', $id = false)
	{
		$options = [
			\Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.option', '0', \Joomla\CMS\Language\Text::_($no)),
			\Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.option', '1', \Joomla\CMS\Language\Text::_($yes)),
		];
		$attribs = array_merge(['forSelect' => 1], $attribs);

		return \Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.radiolist', $options, $name, $attribs, 'value', 'text', (int) $selected, $id);
	}

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

		// Joomla 3: Use Chosen
		if (version_compare(JVERSION, '3.999.999', 'le'))
		{
			HTMLHelper::_('formbehavior.chosen');

			return $innerList;
		}

		// Joomla 4: Use the joomla-field-fancy-select using choices.js
		try
		{
			\Joomla\CMS\Factory::getApplication()->getDocument()->getWebAssetManager()
				->usePreset('choicesjs')
				->useScript('webcomponent.field-fancy-select');
		}
		catch (Exception $e)
		{
			return $innerList;
		}

		$j4Attr = array_filter([
			'class'       => $attribs['class'] ?? null,
			'placeholder' => $attribs['placeholder'] ?? null,
		], function ($x) {
			return !empty($x);
		});

		$dataAttribute = '';

		if (isset($attribs['dataAttribute']))
		{
			$dataAttribute = is_string($attribs['dataAttribute']) ? $attribs['dataAttribute'] : '';
		}

		if ((bool) ($attribs['allowCustom'] ?? false))
		{
			$dataAttribute .= ' allow-custom new-item-prefix="#new#"';
		}

		$remoteSearchUrl = $attribs['remoteSearchURL'] ?? null;
		$remoteSearch    = ((bool) ($attribs['remoteSearch'] ?? false)) && !empty($remoteSearchUrl);
		$termKey         = $attribs['termKey'] ?? 'like';
		$minTermLength   = $attribs['minTermLength'] ?? 3;

		if ($remoteSearch)
		{
			$dataAttribute             .= ' remote-search';
			$j4Attr['url']             = $remoteSearchUrl;
			$j4Attr['term-key']        = $termKey;
			$j4Attr['min-term-length'] = $minTermLength;
		}

		if (isset($attribs['required']))
		{
			$j4Attr['class'] = ($j4Attr['class'] ?? '') . ' required';
			$dataAttribute .= ' required';
		}

		if (isset($attribs['readonly']))
		{
			return $innerList;
		}

		return sprintf("<joomla-field-fancy-select %s %s>%s</joomla-field-fancy-select>", ArrayHelper::toString($j4Attr), $dataAttribute, $innerList);
	}

	/**
	 * Generates an HTML selection list.
	 *
	 * @param   array    $data       An array of objects, arrays, or scalars.
	 * @param   string   $name       The value of the HTML name attribute.
	 * @param   mixed    $attribs    Additional HTML attributes for the `<select>` tag. This
	 *                               can be an array of attributes, or an array of options. Treated as options
	 *                               if it is the last argument passed. Valid options are:
	 *                               Format options, see {@see HTMLHelper::$formatOptions}.
	 *                               Selection options, see {@see HTMLHelper::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.
	 *
	 */
	public static function genericlist(array $data, string $name, ?array $attribs = null, string $optKey = 'value',
	                                   string $optText = 'text', $selected = null, $idtag = false,
	                                   bool $translate = false): string
	{
		// 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']++);

		return $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol']
			. static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol'];
	}

	/**
	 * 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 HTMLHelper::$formatOptions}.
	 *                            Selection options. See {@see HTMLHelper::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
	 *                            Text::_().
	 *
	 * @return  string  HTML for the select list
	 *
	 * @throws  RuntimeException If a group has contents that cannot be processed.
	 */
	public static function groupedlist(array $data, string $name, array $options = []): string
	{
		// 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'];
			}
		}

		return $html . ($baseIndent . '</select>' . $options['format.eol']);
	}

	/**
	 * 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(int $start, int $end, int $inc, string $name, ?array $attribs = null,
	                                   $selected = null, string $format = ''): string
	{
		// 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::_('FEFHelp.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(?string $value, string $text = '', $optKey = 'value', string $optText = 'text',
	                              bool $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 HTMLHelper::$formatOptions}.
	 *                                -groups: Boolean. If set, looks for keys with the value
	 *                                "&lt;optgroup>" and synthesizes groups from them. Deprecated. Defaults
	 *                                true for backwards compatibility.
	 *                                -list.select: either the value of one selected option or an array
	 *                                of selected options. Default: none.
	 *                                -list.translate: Boolean. If set, text and labels are translated via
	 *                                Text::_(). 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(array $arr, $optKey = 'value', string $optText = 'text',
	                               ?string $selected = null, bool $translate = false): string
	{
		$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   mixed    $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 ? \Joomla\CMS\Language\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>';

			}
		}

		return $html . "\n";
	}

	/**
	 * Creates two radio buttons styled with FEF to appear as a YES/NO switch
	 *
	 * @param   string  $name      Name of the field
	 * @param   mixed   $selected  Selected field
	 * @param   array   $attribs   Additional attributes to add to the switch
	 *
	 * @return    string    The HTML for the switch
	 */
	public static function booleanswitch(string $name, $selected, array $attribs = []): string
	{
		if (empty($attribs))
		{
			$attribs = ['class' => 'akeeba-toggle'];
		}
		elseif (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;
	}
}
Fields/fancyradio.php000064400000002550152346061040010605 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

defined('_JEXEC') || die();

use Joomla\CMS\Form\FormHelper;

// Prevent PHP fatal errors if this somehow gets accidentally loaded multiple times
if (class_exists('JFormFieldFancyradio'))
{
	return;
}

// Load the base form field class
FormHelper::loadFieldClass('radio');

/**
 * Yes/No switcher, compatible with Joomla 3 and 4
 *
 * ## How to use
 *
 * 1. Create a folder in your project for custom Joomla form fields, e.g. components/com_example/fields
 * 2. Create a new file called `fancyradio.php` with the content
 *    ```php
 *    defined('_JEXEC') || die();
 *    require_once JPATH_LIBRARIES . '/fof40/Html/Fields/fancyradio.php';
 *    ```
 *
 * @package      Joomla\CMS\Form\Field
 *
 * @since        1.0.0
 * @noinspection PhpUnused
 * @noinspection PhpIllegalPsrClassPathInspection
 */
class JFormFieldFancyradio extends JFormFieldRadio
{
	public function __construct($form = null)
	{
		if (version_compare(JVERSION, '3.999.999', 'gt'))
		{
			// Joomla 4.0 and later.
			$this->layout = 'joomla.form.field.radio.switcher';
		}
		else
		{
			// Joomla 3.x. Yes, 3.10 does have the layout but I am playing it safe.
			$this->layout = 'joomla.form.field.radio';
		}

		parent::__construct($form);
	}
}SelectOptions.php000064400000027324152346061040010061 0ustar00<?php
/**
 * @package   FOF
 * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace FOF40\Html;

defined('_JEXEC') || die;

use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory as JoomlaFactory;
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 HTMLHelper select options for Joomla-specific information such as access levels.
 *
 * @method static array access() access(array $params)
 * @method static array usergroups() usergroups(array $params)
 * @method static array cachehandlers() cachehandlers(array $params)
 * @method static array components() components(array $params)
 * @method static array languages() languages(array $params)
 * @method static array published() published(array $params)
 */
class SelectOptions
{
	private static $cache = [];

	/**
	 * Magic method to handle static calls
	 *
	 * @param   string  $name       The name of the static method being called
	 * @param   array   $arguments  Optional arguments, if they are supported by the options type.
	 *
	 * @return  mixed
	 * @since   3.3.0
	 */
	public static function __callStatic(string $name, array $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
	 * @api
	 */
	public static function getOptions(string $type, array $params = []): array
	{
		if ((substr($type, 0, 1) == '_') || !method_exists(__CLASS__, '_api_' . $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]));
		$ret      = [];

		if ($fetchNew)
		{
			$ret = forward_static_call_array([__CLASS__, '_api_' . $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
	 * @internal
	 *
	 * @see     \Joomla\CMS\HTML\Helpers\Access::level()
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_access(array $params = []): array
	{
		$db    = JoomlaFactory::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
	 * @internal
	 *
	 * @see     \Joomla\CMS\HTML\Helpers\Access::usergroup()
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_usergroups(array $params = []): array
	{
		$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
	 *
	 * @param   array  $params  Ignored
	 *
	 * @return  stdClass[]
	 * @since   3.3.0
	 * @internal
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_cachehandlers(array $params = []): array
	{
		$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
	 * @internal
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_components(array $params = []): array
	{
		$db = JoomlaFactory::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 Text::_()
		$aComponents = [];
		$user        = JoomlaFactory::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 Text::_() 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
	 *
	 * @param   array  $params
	 *
	 * @return  object[]  Languages for the specified client
	 * @since   3.3.0
	 * @internal
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_languages(array $params): array
	{
		$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   array  $params
	 *
	 * @return  array
	 * @since   3.3.0
	 * @internal
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_published(array $params = []): array
	{
		$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   array  $params
	 *
	 * @return  array
	 * @since   3.3.0
	 * @internal
	 *
	 * @noinspection PhpUnusedPrivateMethodInspection
	 */
	private static function _api_boolean(array $params = []): array
	{
		$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   object  $item  The component object
	 *
	 * @return  string  $text  The translated name of the extension
	 * @since   3.3.0
	 * @internal
	 *
	 * @see     /administrator/com_installer/models/extension.php
	 */
	private static function _translateComponentName(object $item): string
	{
		// 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 Text::_() 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   = JoomlaFactory::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);
	}
}
MessageRenderer.php000064400000004076152357725760010362 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Document\Renderer\Html;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Document\DocumentRenderer;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Layout\LayoutHelper;

/**
 * HTML document renderer for the system message queue
 *
 * @since  3.5
 */
class MessageRenderer extends DocumentRenderer
{
	/**
	 * Renders the error stack and returns the results as a string
	 *
	 * @param   string  $name     Not used.
	 * @param   array   $params   Associative array of values
	 * @param   string  $content  Not used.
	 *
	 * @return  string  The output of the script
	 *
	 * @since   3.5
	 */
	public function render($name, $params = array(), $content = null)
	{
		$msgList     = $this->getData();
		$displayData = array(
			'msgList' => $msgList,
			'name'    => $name,
			'params'  => $params,
			'content' => $content,
		);

		$app        = \JFactory::getApplication();
		$chromePath = JPATH_THEMES . '/' . $app->getTemplate() . '/html/message.php';

		if (file_exists($chromePath))
		{
			include_once $chromePath;
		}

		if (function_exists('renderMessage'))
		{
			Log::add('renderMessage() is deprecated. Override system message rendering with layouts instead.', Log::WARNING, 'deprecated');

			return renderMessage($msgList);
		}

		return LayoutHelper::render('joomla.system.message', $displayData);
	}

	/**
	 * Get and prepare system message data for output
	 *
	 * @return  array  An array contains system message
	 *
	 * @since   3.5
	 */
	private function getData()
	{
		// Initialise variables.
		$lists = array();

		// Get the message queue
		$messages = \JFactory::getApplication()->getMessageQueue();

		// Build the sorted message list
		if (is_array($messages) && !empty($messages))
		{
			foreach ($messages as $msg)
			{
				if (isset($msg['type']) && isset($msg['message']))
				{
					$lists[$msg['type']][] = $msg['message'];
				}
			}
		}

		return $lists;
	}
}
ComponentRenderer.php000064400000001600152357725760010726 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Document\Renderer\Html;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Document\DocumentRenderer;

/**
 * HTML document renderer for the component output
 *
 * @since  3.5
 */
class ComponentRenderer extends DocumentRenderer
{
	/**
	 * Renders a component script and returns the results as a string
	 *
	 * @param   string  $component  The name of the component to render
	 * @param   array   $params     Associative array of values
	 * @param   string  $content    Content script
	 *
	 * @return  string  The output of the script
	 *
	 * @since   3.5
	 */
	public function render($component = null, $params = array(), $content = null)
	{
		return $content;
	}
}
ModulesRenderer.php000064400000003563152357725760010406 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Document\Renderer\Html;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Document\DocumentRenderer;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Layout\LayoutHelper;

/**
 * HTML document renderer for a module position
 *
 * @since  3.5
 */
class ModulesRenderer extends DocumentRenderer
{
	/**
	 * Renders multiple modules script and returns the results as a string
	 *
	 * @param   string  $position  The position of the modules to render
	 * @param   array   $params    Associative array of values
	 * @param   string  $content   Module content
	 *
	 * @return  string  The output of the script
	 *
	 * @since   3.5
	 */
	public function render($position, $params = array(), $content = null)
	{
		$renderer = $this->_doc->loadRenderer('module');
		$buffer   = '';

		$app          = \JFactory::getApplication();
		$user         = \JFactory::getUser();
		$frontediting = ($app->isClient('site') && $app->get('frontediting', 1) && !$user->guest);
		$menusEditing = ($app->get('frontediting', 1) == 2) && $user->authorise('core.edit', 'com_menus');

		foreach (ModuleHelper::getModules($position) as $mod)
		{
			$moduleHtml = $renderer->render($mod, $params, $content);

			if ($frontediting && trim($moduleHtml) != '' && $user->authorise('module.edit.frontend', 'com_modules.module.' . $mod->id))
			{
				$displayData = array('moduleHtml' => &$moduleHtml, 'module' => $mod, 'position' => $position, 'menusediting' => $menusEditing);
				LayoutHelper::render('joomla.edit.frontediting_modules', $displayData);
			}

			$buffer .= $moduleHtml;
		}

		\JEventDispatcher::getInstance()->trigger('onAfterRenderModules', array(&$buffer, &$params));

		return $buffer;
	}
}
HeadRenderer.php000064400000025201152357725760007630 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Document\Renderer\Html;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Document\DocumentRenderer;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\Utilities\ArrayHelper;

/**
 * HTML document renderer for the document `<head>` element
 *
 * @since  3.5
 */
class HeadRenderer extends DocumentRenderer
{
	/**
	 * Renders the document head and returns the results as a string
	 *
	 * @param   string  $head     (unused)
	 * @param   array   $params   Associative array of values
	 * @param   string  $content  The script
	 *
	 * @return  string  The output of the script
	 *
	 * @since   3.5
	 */
	public function render($head, $params = array(), $content = null)
	{
		return $this->fetchHead($this->_doc);
	}

	/**
	 * Generates the head HTML and return the results as a string
	 *
	 * @param   JDocumentHtml  $document  The document for which the head will be created
	 *
	 * @return  string  The head hTML
	 *
	 * @since   3.5
	 * @deprecated  4.0  Method code will be moved into the render method
	 */
	public function fetchHead($document)
	{
		// Convert the tagids to titles
		if (isset($document->_metaTags['name']['tags']))
		{
			$tagsHelper = new TagsHelper;
			$document->_metaTags['name']['tags'] = implode(', ', $tagsHelper->getTagNames($document->_metaTags['name']['tags']));
		}

		if ($document->getScriptOptions())
		{
			\JHtml::_('behavior.core');
		}

		// Trigger the onBeforeCompileHead event
		$app = \JFactory::getApplication();
		$app->triggerEvent('onBeforeCompileHead');

		// Get line endings
		$lnEnd        = $document->_getLineEnd();
		$tab          = $document->_getTab();
		$tagEnd       = ' />';
		$buffer       = '';
		$mediaVersion = $document->getMediaVersion();

		// Generate charset when using HTML5 (should happen first)
		if ($document->isHtml5())
		{
			$buffer .= $tab . '<meta charset="' . $document->getCharset() . '" />' . $lnEnd;
		}

		// Generate base tag (need to happen early)
		$base = $document->getBase();

		if (!empty($base))
		{
			$buffer .= $tab . '<base href="' . $base . '" />' . $lnEnd;
		}

		// Generate META tags (needs to happen as early as possible in the head)
		foreach ($document->_metaTags as $type => $tag)
		{
			foreach ($tag as $name => $content)
			{
				if ($type == 'http-equiv' && !($document->isHtml5() && $name == 'content-type'))
				{
					$buffer .= $tab . '<meta http-equiv="' . $name . '" content="' . htmlspecialchars($content, ENT_COMPAT, 'UTF-8') . '" />' . $lnEnd;
				}
				elseif ($type != 'http-equiv' && !empty($content))
				{
					if (is_array($content))
					{
						foreach ($content as $value)
						{
							$buffer .= $tab . '<meta ' . $type . '="' . $name . '" content="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '" />' . $lnEnd;
						}
					}
					else
					{
						$buffer .= $tab . '<meta ' . $type . '="' . $name . '" content="' . htmlspecialchars($content, ENT_COMPAT, 'UTF-8') . '" />' . $lnEnd;
					}
				}
			}
		}

		// Don't add empty descriptions
		$documentDescription = $document->getDescription();

		if ($documentDescription)
		{
			$buffer .= $tab . '<meta name="description" content="' . htmlspecialchars($documentDescription, ENT_COMPAT, 'UTF-8') . '" />' . $lnEnd;
		}

		// Don't add empty generators
		$generator = $document->getGenerator();

		if ($generator)
		{
			$buffer .= $tab . '<meta name="generator" content="' . htmlspecialchars($generator, ENT_COMPAT, 'UTF-8') . '" />' . $lnEnd;
		}

		$buffer .= $tab . '<title>' . htmlspecialchars($document->getTitle(), ENT_COMPAT, 'UTF-8') . '</title>' . $lnEnd;

		// Generate link declarations
		foreach ($document->_links as $link => $linkAtrr)
		{
			$buffer .= $tab . '<link href="' . $link . '" ' . $linkAtrr['relType'] . '="' . $linkAtrr['relation'] . '"';

			if (is_array($linkAtrr['attribs']))
			{
				if ($temp = ArrayHelper::toString($linkAtrr['attribs']))
				{
					$buffer .= ' ' . $temp;
				}
			}

			$buffer .= ' />' . $lnEnd;
		}

		$defaultCssMimes = array('text/css');

		// Generate stylesheet links
		foreach ($document->_styleSheets as $src => $attribs)
		{
			// Check if stylesheet uses IE conditional statements.
			$conditional = isset($attribs['options']) && isset($attribs['options']['conditional']) ? $attribs['options']['conditional'] : null;

			// Check if script uses media version.
			if (isset($attribs['options']['version']) && $attribs['options']['version'] && strpos($src, '?') === false
				&& ($mediaVersion || $attribs['options']['version'] !== 'auto'))
			{
				$src .= '?' . ($attribs['options']['version'] === 'auto' ? $mediaVersion : $attribs['options']['version']);
			}

			$buffer .= $tab;

			// This is for IE conditional statements support.
			if (!is_null($conditional))
			{
				$buffer .= '<!--[if ' . $conditional . ']>';
			}

			$relation = isset($attribs['rel']) ? $attribs['rel'] : 'stylesheet';
			$buffer .= '<link href="' . $src . '" rel="' . $relation . '"';

			// Add script tag attributes.
			foreach ($attribs as $attrib => $value)
			{
				// Don't add the 'options' attribute. This attribute is for internal use (version, conditional, etc).
				if ($attrib === 'options')
				{
					continue;
				}

				// Don't add type attribute if document is HTML5 and it's a default mime type. 'mime' is for B/C.
				if (in_array($attrib, array('type', 'mime')) && $document->isHtml5() && in_array($value, $defaultCssMimes))
				{
					continue;
				}

				// Don't add type attribute if document is HTML5 and it's a default mime type. 'mime' is for B/C.
				if ($attrib === 'mime')
				{
					$attrib = 'type';
				}

				// Add attribute to script tag output.
				$buffer .= ' ' . htmlspecialchars($attrib, ENT_COMPAT, 'UTF-8');

				// Json encode value if it's an array.
				$value = !is_scalar($value) ? json_encode($value) : $value;

				$buffer .= '="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"';
			}

			$buffer .= $tagEnd;

			// This is for IE conditional statements support.
			if (!is_null($conditional))
			{
				$buffer .= '<![endif]-->';
			}

			$buffer .= $lnEnd;
		}

		// Generate stylesheet declarations
		foreach ($document->_style as $type => $content)
		{
			$buffer .= $tab . '<style';

			if (!is_null($type) && (!$document->isHtml5() || !in_array($type, $defaultCssMimes)))
			{
				$buffer .= ' type="' . $type . '"';
			}

			$buffer .= '>' . $lnEnd;

			// This is for full XHTML support.
			if ($document->_mime != 'text/html')
			{
				$buffer .= $tab . $tab . '/*<![CDATA[*/' . $lnEnd;
			}

			$buffer .= $content . $lnEnd;

			// See above note
			if ($document->_mime != 'text/html')
			{
				$buffer .= $tab . $tab . '/*]]>*/' . $lnEnd;
			}

			$buffer .= $tab . '</style>' . $lnEnd;
		}

		// Generate scripts options
		$scriptOptions = $document->getScriptOptions();

		if (!empty($scriptOptions))
		{
			$buffer .= $tab . '<script type="application/json" class="joomla-script-options new">';

			$prettyPrint = (JDEBUG && defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : false);
			$jsonOptions = json_encode($scriptOptions, $prettyPrint);
			$jsonOptions = $jsonOptions ? $jsonOptions : '{}';

			$buffer .= $jsonOptions;
			$buffer .= '</script>' . $lnEnd;
		}

		$defaultJsMimes         = array('text/javascript', 'application/javascript', 'text/x-javascript', 'application/x-javascript');
		$html5NoValueAttributes = array('defer', 'async');

		// Generate script file links
		foreach ($document->_scripts as $src => $attribs)
		{
			// Check if script uses IE conditional statements.
			$conditional = isset($attribs['options']) && isset($attribs['options']['conditional']) ? $attribs['options']['conditional'] : null;

			// Check if script uses media version.
			if (isset($attribs['options']['version']) && $attribs['options']['version'] && strpos($src, '?') === false
				&& ($mediaVersion || $attribs['options']['version'] !== 'auto'))
			{
				$src .= '?' . ($attribs['options']['version'] === 'auto' ? $mediaVersion : $attribs['options']['version']);
			}

			$buffer .= $tab;

			// This is for IE conditional statements support.
			if (!is_null($conditional))
			{
				$buffer .= '<!--[if ' . $conditional . ']>';
			}

			$buffer .= '<script src="' . $src . '"';

			// Add script tag attributes.
			foreach ($attribs as $attrib => $value)
			{
				// Don't add the 'options' attribute. This attribute is for internal use (version, conditional, etc).
				if ($attrib === 'options')
				{
					continue;
				}

				// Don't add type attribute if document is HTML5 and it's a default mime type. 'mime' is for B/C.
				if (in_array($attrib, array('type', 'mime')) && $document->isHtml5() && in_array($value, $defaultJsMimes))
				{
					continue;
				}

				// B/C: If defer and async is false or empty don't render the attribute.
				if (in_array($attrib, array('defer', 'async')) && !$value)
				{
					continue;
				}

				// Don't add type attribute if document is HTML5 and it's a default mime type. 'mime' is for B/C.
				if ($attrib === 'mime')
				{
					$attrib = 'type';
				}
				// B/C defer and async can be set to yes when using the old method.
				elseif (in_array($attrib, array('defer', 'async')) && $value === true)
				{
					$value = $attrib;
				}

				// Add attribute to script tag output.
				$buffer .= ' ' . htmlspecialchars($attrib, ENT_COMPAT, 'UTF-8');

				if (!($document->isHtml5() && in_array($attrib, $html5NoValueAttributes)))
				{
					// Json encode value if it's an array.
					$value = !is_scalar($value) ? json_encode($value) : $value;

					$buffer .= '="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"';
				}
			}

			$buffer .= '></script>';

			// This is for IE conditional statements support.
			if (!is_null($conditional))
			{
				$buffer .= '<![endif]-->';
			}

			$buffer .= $lnEnd;
		}

		// Generate script declarations
		foreach ($document->_script as $type => $content)
		{
			$buffer .= $tab . '<script';

			if (!is_null($type) && (!$document->isHtml5() || !in_array($type, $defaultJsMimes)))
			{
				$buffer .= ' type="' . $type . '"';
			}

			$buffer .= '>' . $lnEnd;

			// This is for full XHTML support.
			if ($document->_mime != 'text/html')
			{
				$buffer .= $tab . $tab . '//<![CDATA[' . $lnEnd;
			}

			$buffer .= $content . $lnEnd;

			// See above note
			if ($document->_mime != 'text/html')
			{
				$buffer .= $tab . $tab . '//]]>' . $lnEnd;
			}

			$buffer .= $tab . '</script>' . $lnEnd;
		}

		// Output the custom tags - array_unique makes sure that we don't output the same tags twice
		foreach (array_unique($document->_custom) as $custom)
		{
			$buffer .= $tab . $custom . $lnEnd;
		}

		return ltrim($buffer, $tab);
	}
}
ModuleRenderer.php000064400000005240152357725760010215 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Document\Renderer\Html;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Document\DocumentRenderer;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\Registry\Registry;

/**
 * HTML document renderer for a single module
 *
 * @since  3.5
 */
class ModuleRenderer extends DocumentRenderer
{
	/**
	 * Renders a module script and returns the results as a string
	 *
	 * @param   string  $module   The name of the module to render
	 * @param   array   $attribs  Associative array of values
	 * @param   string  $content  If present, module information from the buffer will be used
	 *
	 * @return  string  The output of the script
	 *
	 * @since   3.5
	 */
	public function render($module, $attribs = array(), $content = null)
	{
		if (!is_object($module))
		{
			$title = isset($attribs['title']) ? $attribs['title'] : null;

			$module = ModuleHelper::getModule($module, $title);

			if (!is_object($module))
			{
				if (is_null($content))
				{
					return '';
				}

				/**
				 * If module isn't found in the database but data has been pushed in the buffer
				 * we want to render it
				 */
				$tmp = $module;
				$module = new \stdClass;
				$module->params = null;
				$module->module = $tmp;
				$module->id = 0;
				$module->user = 0;
			}
		}

		// Set the module content
		if (!is_null($content))
		{
			$module->content = $content;
		}

		// Get module parameters
		$params = new Registry($module->params);

		// Use parameters from template
		if (isset($attribs['params']))
		{
			$template_params = new Registry(html_entity_decode($attribs['params'], ENT_COMPAT, 'UTF-8'));
			$params->merge($template_params);
			$module = clone $module;
			$module->params = (string) $params;
		}

		// Default for compatibility purposes. Set cachemode parameter or use JModuleHelper::moduleCache from within the module instead
		$cachemode = $params->get('cachemode', 'oldstatic');

		if ($params->get('cache', 0) == 1 && \JFactory::getConfig()->get('caching') >= 1 && $cachemode != 'id' && $cachemode != 'safeuri')
		{
			// Default to itemid creating method and workarounds on
			$cacheparams = new \stdClass;
			$cacheparams->cachemode = $cachemode;
			$cacheparams->class = 'JModuleHelper';
			$cacheparams->method = 'renderModule';
			$cacheparams->methodparams = array($module, $attribs);

			return ModuleHelper::ModuleCache($module, $params, $cacheparams);
		}

		return ModuleHelper::renderModule($module, $attribs);
	}
}