<?php
/**
* @package Joomla.Administrator
* @subpackage com_fields
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Fields\Administrator\Helper;
\defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Fields\FieldsServiceInterface;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormHelper;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Component\Fields\Administrator\Model\FieldsModel;
use Joomla\Database\ParameterType;
/**
* FieldsHelper
*
* @since 3.7.0
*/
class FieldsHelper
{
/**
* @var FieldsModel
*/
private static $fieldsCache = null;
/**
* @var FieldsModel
*/
private static $fieldCache = null;
/**
* Extracts the component and section from the context string which has to
* be in the format component.context.
*
* @param string $contextString contextString
* @param object $item optional item object
*
* @return array|null
*
* @since 3.7.0
*/
public static function extract($contextString, $item = null)
{
$parts = explode('.', $contextString, 2);
if (count($parts) < 2)
{
return null;
}
$newSection = '';
$component = Factory::getApplication()->bootComponent($parts[0]);
if ($component instanceof FieldsServiceInterface)
{
$newSection = $component->validateSection($parts[1], $item);
}
if ($newSection)
{
$parts[1] = $newSection;
}
return $parts;
}
/**
* Returns the fields for the given context.
* If the item is an object the returned fields do have an additional field
* "value" which represents the value for the given item. If the item has an
* assigned_cat_ids field, then additionally fields which belong to that
* category will be returned.
* Should the value being prepared to be shown in an HTML context then
* prepareValue must be set to true. No further escaping needs to be done.
* The values of the fields can be overridden by an associative array where the keys
* have to be a name and its corresponding value.
*
* @param string $context The context of the content passed to the helper
* @param \stdClass $item item
* @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF
* @param array $valuesToOverride The values to override
*
* @return array
*
* @since 3.7.0
*/
public static function getFields($context, $item = null, $prepareValue = false, array $valuesToOverride = null)
{
if (self::$fieldsCache === null)
{
// Load the model
self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields')
->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]);
self::$fieldsCache->setState('filter.state', 1);
self::$fieldsCache->setState('list.limit', 0);
}
if (is_array($item))
{
$item = (object) $item;
}
if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*')
{
self::$fieldsCache->setState('filter.language', array('*', $item->language));
}
self::$fieldsCache->setState('filter.context', $context);
self::$fieldsCache->setState('filter.assigned_cat_ids', array());
/*
* If item has assigned_cat_ids parameter display only fields which
* belong to the category
*/
if ($item && (isset($item->catid) || isset($item->fieldscatid)))
{
$assignedCatIds = $item->catid ?? $item->fieldscatid;
if (!is_array($assignedCatIds))
{
$assignedCatIds = explode(',', $assignedCatIds);
}
// Fields without any category assigned should show as well
$assignedCatIds[] = 0;
self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds);
}
$fields = self::$fieldsCache->getItems();
if ($fields === false)
{
return array();
}
if ($item && isset($item->id))
{
if (self::$fieldCache === null)
{
self::$fieldCache = Factory::getApplication()->bootComponent('com_fields')
->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]);
}
$fieldIds = array_map(
function ($f)
{
return $f->id;
},
$fields
);
$fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id);
$new = array();
foreach ($fields as $key => $original)
{
/*
* Doing a clone, otherwise fields for different items will
* always reference to the same object
*/
$field = clone $original;
if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride))
{
$field->value = $valuesToOverride[$field->name];
}
elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride))
{
$field->value = $valuesToOverride[$field->id];
}
elseif (array_key_exists($field->id, $fieldValues))
{
$field->value = $fieldValues[$field->id];
}
if (!isset($field->value) || $field->value === '')
{
$field->value = $field->default_value;
}
$field->rawvalue = $field->value;
// If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display, 3 - After Display, 0 - Do not prepare
if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2')))
{
PluginHelper::importPlugin('fields');
/*
* On before field prepare
* Event allow plugins to modify the output of the field before it is prepared
*/
Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field));
// Gathering the value for the field
$value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field));
if (is_array($value))
{
$value = implode(' ', $value);
}
/*
* On after field render
* Event allows plugins to modify the output of the prepared field
*/
Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value));
// Assign the value
$field->value = $value;
}
$new[$key] = $field;
}
$fields = $new;
}
return $fields;
}
/**
* Renders the layout file and data on the context and does a fall back to
* Fields afterwards.
*
* @param string $context The context of the content passed to the helper
* @param string $layoutFile layoutFile
* @param array $displayData displayData
*
* @return NULL|string
*
* @since 3.7.0
*/
public static function render($context, $layoutFile, $displayData)
{
$value = '';
/*
* Because the layout refreshes the paths before the render function is
* called, so there is no way to load the layout overrides in the order
* template -> context -> fields.
* If there is no override in the context then we need to call the
* layout from Fields.
*/
if ($parts = self::extract($context))
{
// Trying to render the layout on the component from the context
$value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0));
}
if ($value == '')
{
// Trying to render the layout on Fields itself
$value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0));
}
return $value;
}
/**
* PrepareForm
*
* @param string $context The context of the content passed to the helper
* @param Form $form form
* @param object $data data.
*
* @return boolean
*
* @since 3.7.0
*/
public static function prepareForm($context, Form $form, $data)
{
// Extracting the component and section
$parts = self::extract($context);
if (! $parts)
{
return true;
}
$context = $parts[0] . '.' . $parts[1];
// When no fields available return here
$fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject);
if (! $fields)
{
return true;
}
$component = $parts[0];
$section = $parts[1];
$assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid');
// Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category
$assignedCatids = is_array($assignedCatids)
? (int) reset($assignedCatids)
: (int) $assignedCatids;
if (!$assignedCatids && $formField = $form->getField('catid'))
{
$assignedCatids = $formField->getAttribute('default', null);
if (!$assignedCatids)
{
// Choose the first category available
$catOptions = $formField->options;
if ($catOptions && !empty($catOptions[0]->value))
{
$assignedCatids = (int) $catOptions[0]->value;
}
}
$data->fieldscatid = $assignedCatids;
}
/*
* If there is a catid field we need to reload the page when the catid
* is changed
*/
if ($form->getField('catid') && $parts[0] != 'com_fields')
{
/*
* Setting some parameters for the category field
*/
$form->setFieldAttribute('catid', 'refresh-enabled', true);
$form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids);
$form->setFieldAttribute('catid', 'refresh-section', $section);
}
// Getting the fields
$fields = self::getFields($parts[0] . '.' . $parts[1], $data);
if (!$fields)
{
return true;
}
$fieldTypes = self::getFieldTypes();
// Creating the dom
$xml = new \DOMDocument('1.0', 'UTF-8');
$fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields'));
$fieldsNode->setAttribute('name', 'com_fields');
// Organizing the fields according to their group
$fieldsPerGroup = array(0 => array());
foreach ($fields as $field)
{
if (!array_key_exists($field->type, $fieldTypes))
{
// Field type is not available
continue;
}
if (!array_key_exists($field->group_id, $fieldsPerGroup))
{
$fieldsPerGroup[$field->group_id] = array();
}
if ($path = $fieldTypes[$field->type]['path'])
{
// Add the lookup path for the field
FormHelper::addFieldPath($path);
}
if ($path = $fieldTypes[$field->type]['rules'])
{
// Add the lookup path for the rule
FormHelper::addRulePath($path);
}
$fieldsPerGroup[$field->group_id][] = $field;
}
$model = Factory::getApplication()->bootComponent('com_fields')
->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]);
$model->setState('filter.context', $context);
/**
* $model->getItems() would only return existing groups, but we also
* have the 'default' group with id 0 which is not in the database,
* so we create it virtually here.
*/
$defaultGroup = new \stdClass;
$defaultGroup->id = 0;
$defaultGroup->title = '';
$defaultGroup->description = '';
$iterateGroups = array_merge(array($defaultGroup), $model->getItems());
// Looping through the groups
foreach ($iterateGroups as $group)
{
if (empty($fieldsPerGroup[$group->id]))
{
continue;
}
// Defining the field set
/** @var \DOMElement $fieldset */
$fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset'));
$fieldset->setAttribute('name', 'fields-' . $group->id);
$fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields');
$fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules');
$label = $group->title;
$description = $group->description;
if (!$label)
{
$key = strtoupper($component . '_FIELDS_' . $section . '_LABEL');
if (!Factory::getLanguage()->hasKey($key))
{
$key = 'JGLOBAL_FIELDS';
}
$label = $key;
}
if (!$description)
{
$key = strtoupper($component . '_FIELDS_' . $section . '_DESC');
if (Factory::getLanguage()->hasKey($key))
{
$description = $key;
}
}
$fieldset->setAttribute('label', $label);
$fieldset->setAttribute('description', strip_tags($description));
// Looping through the fields for that context
foreach ($fieldsPerGroup[$group->id] as $field)
{
try
{
Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form));
/*
* If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data
* is not known, set the required flag to false on any circumstance.
*/
if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name))
{
$form->setFieldAttribute($field->name, 'required', 'false');
}
}
catch (\Exception $e)
{
Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
}
}
// When the field set is empty, then remove it
if (!$fieldset->hasChildNodes())
{
$fieldsNode->removeChild($fieldset);
}
}
// Loading the XML fields string into the form
$form->load($xml->saveXML());
$model = Factory::getApplication()->bootComponent('com_fields')
->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]);
if ((!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules'
&& Factory::getApplication()->isClient('site'))
{
// Modules on front end editing don't have data and an id set
$data->id = Factory::getApplication()->input->getInt('id');
}
// Looping through the fields again to set the value
if (!isset($data->id) || !$data->id)
{
return true;
}
foreach ($fields as $field)
{
$value = $model->getFieldValue($field->id, $data->id);
if ($value === null)
{
continue;
}
if (!is_array($value) && $value !== '')
{
// Function getField doesn't cache the fields, so we try to do it only when necessary
$formField = $form->getField($field->name, 'com_fields');
if ($formField && $formField->forceMultiple)
{
$value = (array) $value;
}
}
// Setting the value on the field
$form->setValue($field->name, 'com_fields', $value);
}
return true;
}
/**
* Return a boolean if the actual logged in user can edit the given field value.
*
* @param \stdClass $field The field
*
* @return boolean
*
* @since 3.7.0
*/
public static function canEditFieldValue($field)
{
$parts = self::extract($field->context);
return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id);
}
/**
* Return a boolean based on field (and field group) display / show_on settings
*
* @param \stdClass $field The field
*
* @return boolean
*
* @since 3.8.7
*/
public static function displayFieldOnForm($field)
{
$app = Factory::getApplication();
// Detect if the field should be shown at all
if ($field->params->get('show_on') == 1 && $app->isClient('administrator'))
{
return false;
}
elseif ($field->params->get('show_on') == 2 && $app->isClient('site'))
{
return false;
}
if (!self::canEditFieldValue($field))
{
$fieldDisplayReadOnly = $field->params->get('display_readonly', '2');
if ($fieldDisplayReadOnly == '2')
{
// Inherit from field group display read-only setting
$groupModel = $app->bootComponent('com_fields')
->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]);
$groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1');
$fieldDisplayReadOnly = $groupDisplayReadOnly;
}
if ($fieldDisplayReadOnly == '0')
{
// Do not display field on form when field is read-only
return false;
}
}
// Display field on form
return true;
}
/**
* Gets assigned categories ids for a field
*
* @param \stdClass[] $fieldId The field ID
*
* @return array Array with the assigned category ids
*
* @since 4.0.0
*/
public static function getAssignedCategoriesIds($fieldId)
{
$fieldId = (int) $fieldId;
if (!$fieldId)
{
return array();
}
$db = Factory::getDbo();
$query = $db->getQuery(true);
$query->select($db->quoteName('a.category_id'))
->from($db->quoteName('#__fields_categories', 'a'))
->where('a.field_id = ' . $fieldId);
$db->setQuery($query);
return $db->loadColumn();
}
/**
* Gets assigned categories titles for a field
*
* @param \stdClass[] $fieldId The field ID
*
* @return array Array with the assigned categories
*
* @since 3.7.0
*/
public static function getAssignedCategoriesTitles($fieldId)
{
$fieldId = (int) $fieldId;
if (!$fieldId)
{
return [];
}
$db = Factory::getDbo();
$query = $db->getQuery(true);
$query->select($db->quoteName('c.title'))
->from($db->quoteName('#__fields_categories', 'a'))
->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id')
->where($db->quoteName('field_id') . ' = :fieldid')
->bind(':fieldid', $fieldId, ParameterType::INTEGER);
$db->setQuery($query);
return $db->loadColumn();
}
/**
* Gets the fields system plugin extension id.
*
* @return integer The fields system plugin extension id.
*
* @since 3.7.0
*/
public static function getFieldsPluginId()
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('fields'));
$db->setQuery($query);
try
{
$result = (int) $db->loadResult();
}
catch (\RuntimeException $e)
{
Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
$result = 0;
}
return $result;
}
/**
* Loads the fields plugins and returns an array of field types from the plugins.
*
* The returned array contains arrays with the following keys:
* - label: The label of the field
* - type: The type of the field
* - path: The path of the folder where the field can be found
*
* @return array
*
* @since 3.7.0
*/
public static function getFieldTypes()
{
PluginHelper::importPlugin('fields');
$eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes');
$data = array();
foreach ($eventData as $fields)
{
foreach ($fields as $fieldDescription)
{
if (!array_key_exists('path', $fieldDescription))
{
$fieldDescription['path'] = null;
}
if (!array_key_exists('rules', $fieldDescription))
{
$fieldDescription['rules'] = null;
}
$data[$fieldDescription['type']] = $fieldDescription;
}
}
return $data;
}
/**
* Clears the internal cache for the custom fields.
*
* @return void
*
* @since 3.8.0
*/
public static function clearFieldsCache()
{
self::$fieldCache = null;
self::$fieldsCache = null;
}
}