<?php /** * @package Joomla.Administrator * @subpackage com_fields * * @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; JLoader::register('JFolder', JPATH_LIBRARIES . '/joomla/filesystem/folder.php'); /** * FieldsHelper * * @since 3.7.0 */ class FieldsHelper { private static $fieldsCache = null; 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; } $component = $parts[0]; $eName = str_replace('com_', '', $component); $path = JPath::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); if (file_exists($path)) { $cName = ucfirst($eName) . 'Helper'; JLoader::register($cName, $path); if (class_exists($cName) && is_callable(array($cName, 'validateSection'))) { $section = call_user_func_array(array($cName, 'validateSection'), array($parts[1], $item)); if ($section) { $parts[1] = $section; } } } 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 JModelLegacy::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_fields/models', 'FieldsModel'); self::$fieldsCache = JModelLegacy::getInstance('Fields', 'FieldsModel', array( 'ignore_request' => true) ); self::$fieldsCache->setState('filter.state', 1); self::$fieldsCache->setState('list.limit', 0); } if (is_array($item)) { $item = (object) $item; } if (JLanguageMultilang::isEnabled() && isset($item->language) && $item->language != '*') { self::$fieldsCache->setState('filter.language', array('*', $item->language)); } self::$fieldsCache->setState('filter.context', $context); /* * If item has assigned_cat_ids parameter display only fields which * belong to the category */ if ($item && (isset($item->catid) || isset($item->fieldscatid))) { $assignedCatIds = isset($item->catid) ? $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 = JModelLegacy::getInstance('Field', 'FieldsModel', array('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 && key_exists($field->name, $valuesToOverride)) { $field->value = $valuesToOverride[$field->name]; } elseif ($valuesToOverride && key_exists($field->id, $valuesToOverride)) { $field->value = $valuesToOverride[$field->id]; } elseif (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'))) { JPluginHelper::importPlugin('fields'); $dispatcher = JEventDispatcher::getInstance(); // Event allow plugins to modify the output of the field before it is prepared $dispatcher->trigger('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); // Gathering the value for the field $value = $dispatcher->trigger('onCustomFieldsPrepareField', array($context, $item, &$field)); if (is_array($value)) { $value = implode(' ', $value); } // Event allow plugins to modify the output of the prepared field $dispatcher->trigger('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 = JLayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); } if ($value == '') { // Trying to render the layout on Fields itself $value = JLayoutHelper::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 JForm $form form * @param object $data data. * * @return boolean * * @since 3.7.0 */ public static function prepareForm($context, JForm $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 JObject); if (! $fields) { return true; } $component = $parts[0]; $section = $parts[1]; $assignedCatids = isset($data->catid) ? $data->catid : (isset($data->fieldscatid) ? $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); // Choose the first category available $xml = new DOMDocument; $xml->loadHTML($formField->__get('input')); $options = $xml->getElementsByTagName('option'); if (!$assignedCatids && $firstChoice = $options->item(0)) { $assignedCatids = $firstChoice->getAttribute('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 the onchange event to reload the page when the category * has changed */ $form->setFieldAttribute('catid', 'onchange', 'categoryHasChanged(this);'); // Preload spindle-wheel when we need to submit form due to category selector changed JFactory::getDocument()->addScriptDeclaration(" function categoryHasChanged(element) { var cat = jQuery(element); if (cat.val() == '" . $assignedCatids . "')return; Joomla.loadingLayer('show'); jQuery('input[name=task]').val('" . $section . ".reload'); element.form.submit(); } jQuery( document ).ready(function() { Joomla.loadingLayer('load'); var formControl = '#" . $form->getFormControl() . "_catid'; if (!jQuery(formControl).val() != '" . $assignedCatids . "'){jQuery(formControl).val('" . $assignedCatids . "');} });" ); } // 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 JFormHelper::addFieldPath($path); } if ($path = $fieldTypes[$field->type]['rules']) { // Add the lookup path for the rule JFormHelper::addRulePath($path); } $fieldsPerGroup[$field->group_id][] = $field; } // On the front, sometimes the admin fields path is not included JTable::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_fields/tables'); $model = JModelLegacy::getInstance('Groups', 'FieldsModel', array('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 (!JFactory::getLanguage()->hasKey($key)) { $key = 'JGLOBAL_FIELDS'; } $label = $key; } if (!$description) { $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); if (JFactory::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 { JFactory::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) { JFactory::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 = JModelLegacy::getInstance('Field', 'FieldsModel', array('ignore_request' => true)); if ((!isset($data->id) || !$data->id) && JFactory::getApplication()->input->getCmd('controller') == 'config.display.modules' && JFactory::getApplication()->isClient('site')) { // Modules on front end editing don't have data and an id set $data->id = JFactory::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 JFactory::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 = JFactory::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 = JModelLegacy::getInstance('Group', 'FieldsModel', array('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 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 array(); } $db = JFactory::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('field_id = ' . $fieldId); $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 = JFactory::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) { JError::raiseWarning(500, $e->getMessage()); $result = 0; } return $result; } /** * Configure the Linkbar. * * @param string $context The context the fields are used for * @param string $vName The view currently active * * @return void * * @since 3.7.0 */ public static function addSubmenu($context, $vName) { $parts = self::extract($context); if (!$parts) { return; } $component = $parts[0]; // Avoid nonsense situation. if ($component == 'com_fields') { return; } // Try to find the component helper. $eName = str_replace('com_', '', $component); $file = JPath::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); if (!file_exists($file)) { return; } require_once $file; $cName = ucfirst($eName) . 'Helper'; if (class_exists($cName) && is_callable(array($cName, 'addSubmenu'))) { $lang = JFactory::getLanguage(); $lang->load($component, JPATH_ADMINISTRATOR) || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); $cName::addSubmenu('fields.' . $vName); } } /** * 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() { JPluginHelper::importPlugin('fields'); $eventData = JEventDispatcher::getInstance()->trigger('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; } }