Файловый менеджер - Редактировать - /home/lmsyaran/public_html/joomla5/components/com_fabrik/models/element.php
Назад
<?php /** * Fabrik Element Model * * @package Joomla * @subpackage Fabrik * @copyright Copyright (C) 2005-2023 Media A-Team, Inc. - All rights reserved. * @license GNU/GPL http://www.gnu.org/copyleft/gpl.html */ // No direct access defined('_JEXEC') or die('Restricted access'); use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Table\Table; use Joomla\CMS\Language\Text; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Layout\FileLayout; use Joomla\CMS\Layout\LayoutInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Profiler\Profiler; use Joomla\CMS\Session\Session; use Joomla\CMS\Form\FormHelper; use Joomla\CMS\Factory; use Joomla\CMS\Object\CMSObject; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Filesystem\Folder; use Fabrik\Helpers\LayoutFile; use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; use Joomla\String\StringHelper; use Joomla\CMS\Date\Date; use Joomla\CMS\HTML\HTMLHelper; use Fabrik\Helpers\Php; jimport('joomla.application.component.model'); jimport('joomla.filesystem.file'); /** * Fabrik Element Model * * @package Fabrik * @since 3.0 */ #[\AllowDynamicProperties] class PlgFabrik_Element extends FabrikPlugin { /** * Element id * * @var int */ protected $id = null; /** * Javascript actions to attach to element * * @var array */ protected $jsActions = null; /** * Editable * * @var bool */ public $editable = null; /** * Is an upload element * * @var bool */ protected $is_upload = false; /** * Does the element's data get recorded in the db * * @var bool */ protected $recordInDatabase = true; /** * Contain access rights * * @var object */ protected $access = null; /** * Validation error * * @var string */ protected $validationError = null; /** * Stores possible element names to avoid repeat db queries * * @var array */ public $fullNames = array(); /** * Group model * * @var object */ protected $group = null; /** * Form model * * @var object */ protected $form = null; /** * List model * * @var object */ protected $list = null; /** * Element object * * @var FabrikTableElement */ public $element = null; /** * If the element 'Include in search all' option is set to 'default' then this states if the * element should be ignored from search all. * * @var bool True, ignore in extended search all. */ protected $ignoreSearchAllDefault = false; /** * Does the element have a label * * @var bool */ private $hasLabel = true; /** * Does the element contain sub elements e.g checkboxes radiobuttons * * @var bool */ public $hasSubElements = false; /** * Valid image extensions * * @var array */ protected $imageExtensions = array('jpg', 'jpeg', 'gif', 'bmp', 'png'); /** * Is the element in a detailed view? * * @var bool */ public $inDetailedView = false; /** * Default values * * @var array */ public $defaults = array(); /** * The element's HTML ids based on $repeatCounter * * @var array */ public $HTMLids = array(); /** * Is the element in a repeat group * * @var bool */ public $inRepeatGroup = null; /** * Is the element in a new group. * * Used by pre rendering / getGroupView so elements can set a default on new repeat groups, * even if it isn't a new record. * * @var bool */ public $newGroup = null; /** * Default value * * @var string */ protected $default = null; /** * Join model * * @var object */ protected $joinModel = null; /** * Has the icon been set * * @var bool */ protected $iconsSet = false; /** * Parent element row - if no parent returns element * * @var object */ protected $parent = null; /** * Actual table name (table or joined tables db table name) * * @var string */ protected $actualTable = null; /** * Ensures the query values are only escaped once * * @var bool */ protected $escapedQueryValue = false; /** * Db table field type * * @var string */ protected $fieldDesc = 'VARCHAR(%s)'; /** * Db table field size * * @var string */ protected $fieldSize = '255'; /** * Element error msg * * @var string */ protected $elementError = ''; /** * Multi-db join option - can we add duplicate options (set to false in tags element) * * @var bool */ protected $allowDuplicates = true; /** * Wraps 'foo' AS 'bar' as statements with SUM('foo') = 'bar' * Used in nv3d viz to alter query statements * * @var string */ public $calcSelectModifier = null; public static $fxAdded = array(); /** * @var FabrikFEModelElementValidator */ public $validator; /** * Selected filter value labels * * @var array */ public $filterDisplayValues = array(); /** * Cache for eval'ed options for dropdowns * * @var array */ protected $phpOptions = array(); /** * * Cache for suboptions * * @var null|array * * @since 3.7 */ protected $subOptionValues = null; /** * Cache for subpoption labels * * @var null|array */ protected $subOptionLabels = null; /* dynamic properties to make php8.2 happy */ public $tmpl = null; public $_foreignKey = null; public $_repeatGroupTotal = null; public $_inJoin = null; public $elementHTMLName = null; public $_default = null; /** * Constructor * * @param object &$subject The object to observe * @param array $config An array that holds the plugin configuration * * @since 1.5 */ public function __construct(&$subject, $config = array()) { parent::__construct($subject, $config); $this->validator = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('ElementValidator', 'FabrikFEModel'); $this->validator->setElementModel($this); $this->access = new stdClass; } /** * Weed out any non-serializable properties. We only ever get serialized by J!'s cache handler, * to create the cache ID, so we don't really care about __wake() or not saving all state. We just * want to avoid the dreaded "serialization of a closure is not allowed", and provide enough propeties * to guarrantee a unique hash for the cache ID. */ public function __sleep() { $serializable = array(); foreach ($this as $paramName => $paramValue) { //if (!is_string($paramValue) && !is_array($paramValue) && is_callable($paramValue)) if (!is_numeric($paramValue) && !is_string($paramValue) && !is_array($paramValue)) { continue; } $serializable[] = $paramName; } return $serializable; } /** * Method to set the element id * * @param int $id element ID number * * @return void */ public function setId($id) { // Set new element ID $this->id = $id; } /** * Get the element id * * @return int element id */ public function getId() { return $this->id; } /** * Get the element table object * * @param bool $force default false - force load the element * * @return FabrikTableElement element table */ public function &getElement($force = false) { if (!$this->element || $force) { Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_fabrik/tables'); $row = FabTable::getInstance('Element', 'FabrikTable'); $row->load($this->id); $this->element = $row; // 3.1 reset the params at the same time. Seems to be required for ajax autocomplete if ($force) { unset($this->params); $this->getParams(); } } return $this->element; } /** * Get parent element * * @return object element table */ public function getParent() { if (!isset($this->parent)) { $element = $this->getElement(); if ((int) $element->parent_id !== 0) { $this->parent = FabTable::getInstance('element', 'FabrikTable'); $this->parent->load($element->parent_id); } else { $this->parent = $element; } } return $this->parent; } /** * Bind data to the _element variable - if possible we should run one query to get all the forms * element data and then iterate over that, creating an element plugin for each row * and bind each record to that plugins _element. This is instead of using getElement() which * reloads in the element increasing the number of queries run * * @param mixed &$row (object or assoc array) * * @return object element table */ public function bindToElement(&$row) { if (!$this->element) { Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_fabrik/tables'); $this->element = FabTable::getInstance('Element', 'FabrikTable'); } if (is_object($row)) { $row = ArrayHelper::fromObject($row); } $this->element->bind($row); return $this->element; } /** * Set the context in which the element occurs * * @param FabrikFEModelGroup &$groupModel group model * @param FabrikFEModelForm &$formModel form model * @param FabrikFEModelList &$listModel list model * * @return void */ public function setContext(&$groupModel, &$formModel, &$listModel) { // Don't assign these with &= as they already are when passed into the func $this->group = $groupModel; $this->form = $formModel; $this->list = $listModel; } /** * Get the element's fabrik list model * * @return FabrikFEModelList list model */ public function getListModel() { if (is_null($this->list)) { $groupModel = $this->getGroup(); $this->list = $groupModel->getListModel(); } return $this->list; } /** * load in the group model * * @param int $groupId group id * * @return FabrikFEModelGroup group model */ public function &getGroup($groupId = null) { if (is_null($groupId)) { $element = $this->getElement(); $groupId = $element->group_id; } if (is_null($this->group) || $this->group->getId() != $groupId) { $model = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('Group', 'FabrikFEModel'); $model->setId($groupId); $model->getGroup(); $this->group = $model; } return $this->group; } /** * Get the elements group model * * @param int $group_id If not set uses elements default group id * * @return FabrikFEModelGroup group model */ public function getGroupModel($group_id = null) { return $this->getGroup($group_id); } /** * Set the group model * * @param FabrikFEModelGroup $group group model * * @since 3.0.6 * * @return null */ public function setGroupModel($group) { $this->group = $group; } /** * get the elements form model * * @deprecated use getFormModel * * @return FabrikFEModelForm Form model */ public function getForm() { return $this->getFormModel(); } /** * get the element's form model * * @return FabrikFEModelForm Form model */ public function getFormModel() { if (is_null($this->form)) { $listModel = $this->getListModel(); $table = $listModel->getTable(); $this->form = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('Form', 'FabrikFEModel'); $this->form->setId($table->form_id); $this->form->getForm(); } return $this->form; } /** * Set form model * * @param FabrikFEModelForm $model form model * * @return void */ public function setFormModel($model) { $this->form = $model; } /** * Shows the RAW list data - can be overwritten in plugin class * * @param string $data element data * @param object $thisRow all the data in the tables current row * * @return string formatted value */ public function renderRawListData($data, $thisRow) { return $data; } /** * replace labels shown in table view with icons (if found) * * @param string $data data * @param string $view list/details * @param string $tmpl template * * @since 3.9 - icon_folder is a three way - search through template folders for icons or use class * * @deprecated use replaceWithIcons() * @return string data */ protected function _replaceWithIcons($data, $view = 'list', $tmpl = null) { return $this->replaceWithIcons($data, $view, $tmpl); } /** * Replace labels shown in list view with icons (if found) * * @param string $data Data * @param string $view List/details * @param string $tmpl Template * * @since 3.9 - icon_folder is a three way - search through template folders for icons or use class * * @return string data */ protected function replaceWithIcons($data, $view = 'list', $tmpl = null) { if ($data == '') { $this->iconsSet = false; return $data; } $params = $this->getParams(); $listModel = $this->getListModel(); $iconFile = (string) $params->get('icon_file', ''); $iconFolder = (int) $params->get('icon_folder', ''); if ($iconFile === '{extension}') { $iconFileInfo = pathinfo(trim(strip_tags($data))); $iconFile = FArrayHelper::getValue($iconFileInfo, 'extension', ''); } if ((int) $params->get('icon_folder', 0) === 0 && $iconFile === '') { $this->iconsSet = false; return $data; } if (in_array($listModel->getOutPutFormat(), array('csv', 'rss'))) { $this->iconsSet = false; return $data; } $cleanData = empty($iconFile) ? FabrikString::clean(strip_tags($data)) : $iconFile; $cleanDatas = array($this->getElement()->name . '_' . $cleanData, $cleanData); $opts = array('forceImage' => true); //If subdir is set prepend file name with subdirectory (so first search through [template folders]/subdir for icons, e.g. images/subdir) $iconSubDir = $params->get('icon_subdir', ''); if ($iconSubDir != '') { $iconSubDir = rtrim($iconSubDir, '/') . '/'; $iconSubDir = ltrim($iconSubDir, '/'); array_unshift($cleanDatas, $iconSubDir . $cleanData); //search subdir first } foreach ($cleanDatas as $cleanData) { foreach ($this->imageExtensions as $ex) { $img = FabrikHelperHTML::image($cleanData . '.' . $ex, $view, $tmpl, array(), false, $opts); if (!empty($img)) { break; } } if (empty($img) && $iconFolder === 2) { $opts = array('forceImage' => false); $img = FabrikHelperHTML::image($cleanData, $view, $tmpl, array(), false, $opts); } if (!empty($img)) { $this->iconsSet = true; $opts = new stdClass; $opts->position = 'top'; $opts = json_encode($opts); $data = '<span>' . $data . '</span>'; // See if data has an <a> tag $html = FabrikHelperHTML::loadDOMDocument($data); $as = $html->getElementsBytagName('a'); if ($params->get('icon_hovertext', true)) { //$aHref = 'javascript:void(0)'; $aHref = '#'; $target = ''; if ($as->length) { // Data already has an <a href="foo"> lets get that for use in hover text $a = $as->item(0); $aHref = $a->getAttribute('href'); $target = $a->getAttribute('target'); $target = 'target="' . $target . '"'; } $data = htmlspecialchars($data, ENT_QUOTES); $layout = FabrikHelperHTML::getLayout('element.fabrik-element-listicon-tip'); $displayData = new stdClass; $displayData->img = $img; $displayData->title = $data; $displayData->href = $aHref; $displayData->target = $target; $displayData->opts = $opts; $img = $layout->render($displayData); //$img = '<a class="fabrikTip" ' . $target . ' href="' . $aHref . '" opts=\'' . $opts . '\' title="' . $data . '">' . $img . '</a>'; } elseif (!empty($iconFile)) { /** * $$$ hugh - kind of a hack, but ... if this is an upload element, it may already be a link, and * we'll need to replace the text in the link with the image * After ages dicking around with a regex to do this, decided to use DOMDocument instead! */ if ($as->length) { $img = $html->createElement('img'); $src = FabrikHelperHTML::image($cleanData . '.' . $ex, $view, $tmpl, array(), true, array('forceImage' => true)); $img->setAttribute('src', $src); $as->item(0)->nodeValue = ''; $as->item(0)->appendChild($img); return $html->saveHTML(); } } return $img; } } return $data; } /** * Build the sub query which is used when merging in in repeat element * records from their joined table into the one field. * Overwritten in database join element to allow for building the join * to the table containing the stored values required labels * * @param string $jKey key * @param bool $addAs add 'AS' to select sub query * * @return string sub query */ public function buildQueryElementConcat($jKey, $addAs = true) { $joinTable = $this->getJoinModel()->getJoin()->table_join; $dbTable = $this->actualTableName(); // Jaanus: joined group pk? set in groupConcactJoinKey() $pkField = $this->groupConcactJoinKey(); $fullElName = $this->_db->qn($dbTable . '___' . $this->element->name); $sql = '(SELECT GROUP_CONCAT(' . $jKey . ' SEPARATOR \'' . GROUPSPLITTER . '\') FROM ' . $joinTable . ' WHERE parent_id = ' . $pkField . ')'; if ($addAs) { $sql .= ' AS ' . $fullElName; } return $sql; } /** * Build the sub query which is used when merging in * repeat element records from their joined table into the one field. * Overwritten in database join element to allow for building * the join to the table containing the stored values required ids * * @since 2.1.1 * * @return string sub query */ protected function buildQueryElementConcatRaw() { $joinTable = $this->getJoinModel()->getJoin()->table_join; $dbTable = $this->actualTableName(); $fullElName = $this->_db->qn($dbTable . '___' . $this->element->name . '_raw'); $pkField = $this->groupConcactJoinKey(); return '(SELECT GROUP_CONCAT(id SEPARATOR \'' . GROUPSPLITTER . '\') FROM ' . $joinTable . ' WHERE parent_id = ' . $pkField . ') AS ' . $fullElName; } /** * Build the sub query which is used when merging in * repeat element records from their joined table into the one field. * Overwritten in database join element to allow for building * the join to the table containing the stored values required ids * * @since 2.1.1 * * @return string sub query */ protected function buildQueryElementConcatId() { return ''; } /** * Used in form model setJoinData. * * @since 2.1.1 * * @return array Element names to search data in to create join data array */ public function getJoinDataNames() { $name = $this->getFullName(true, false); $rawName = $name . '_raw'; return array($name, $rawName); } /** * Create the SQL select 'name AS alias' segment for list/form queries * * @param array &$aFields array of element names * @param array &$aAsFields array of 'name AS alias' fields * @param array $opts options : alias - replace the fullelement name in asfields "name AS * tablename___elementname" * * @return void */ public function getAsField_html(&$aFields, &$aAsFields, $opts = array()) { $dbTable = $this->actualTableName(); $db = FabrikWorker::getDbo(); $table = $this->getListModel()->getTable(); $fullElName = FArrayHelper::getValue($opts, 'alias', $db->qn($dbTable . '___' . $this->element->name)); $fName = $dbTable . '.' . $this->element->name; $k = $db->qn($fName); $secret = $this->config->get('secret'); if ($this->encryptMe()) { $k = 'AES_DECRYPT(' . $k . ', ' . $db->q($secret) . ')'; } if ($this->isJoin()) { $jKey = $this->element->name; if ($this->encryptMe()) { $jKey = 'AES_DECRYPT(' . $jKey . ', ' . $db->q($secret) . ')'; } $joinTable = $this->getJoinModel()->getJoin()->table_join; //$fullElName = FArrayHelper::getValue($opts, 'alias', $k); $fullElName = FArrayHelper::getValue($opts, 'alias', $fullElName); $str = $this->buildQueryElementConcat($jKey); } else { if ($this->calcSelectModifier) { $k = $this->calcSelectModifier . '(' . $k . ')'; } $str = $k . ' AS ' . $fullElName; } if ($table->db_primary_key == $fullElName) { array_unshift($aFields, $fullElName); array_unshift($aAsFields, $fullElName); } else { if (!in_array($str, $aFields)) { $aFields[] = $str; $aAsFields[] = $fullElName; } $k = $db->qn($dbTable . '.' . $this->element->name); if ($this->encryptMe()) { $k = 'AES_DECRYPT(' . $k . ', ' . $db->q($secret) . ')'; } if ($this->isJoin()) { $pkField = $this->groupConcactJoinKey(); $str = $this->buildQueryElementConcatRaw(); $aFields[] = $str; $as = $db->qn($dbTable . '___' . $this->element->name . '_raw'); $aAsFields[] = $as; $str = $this->buildQueryElementConcatId(); $aFields[] = $str; $as = $db->qn($dbTable . '___' . $this->element->name . '_id'); $aAsFields[] = $as; $as = $db->qn($dbTable . '___' . $this->element->name . '___params'); $str = '(SELECT GROUP_CONCAT(params SEPARATOR \'' . GROUPSPLITTER . '\') FROM ' . $joinTable . ' WHERE parent_id = ' . $pkField . ') AS ' . $as; // Jaanus: joined group pk set in groupConcactJoinKey() $aFields[] = $str; $aAsFields[] = $as; } else { $fullElName = $db->qn($dbTable . '___' . $this->element->name . '_raw'); if ($this->calcSelectModifier) { $k = $this->calcSelectModifier . '(' . $k . ')'; } $str = $k . ' AS ' . $fullElName; } if (!in_array($str, $aFields)) { $aFields[] = $str; $aAsFields[] = $fullElName; } } } /** * OMG! If repeat element inside a repeat group then the group_concat subquery needs to change the key * it selected on - so it could either be the table pk or the joined groups pk.... :D * * @since 3.1rc1 * * @return string */ protected function groupConcactJoinKey() { $table = $this->getListModel()->getTable(); if ($this->getGroupModel()->isJoin() && $this->isJoin()) { $groupJoin = $this->getGroupModel()->getJoinModel()->getJoin(); $pkField = $groupJoin->params->get('pk'); } else { $pkField = $table->db_primary_key; } return $pkField; } /** * Get raw column name * * @param bool $useStep Use step in name * * @return string */ public function getRawColumn($useStep = true) { $n = $this->getFullName($useStep, false); $n .= '_raw`'; return $n; } /** * Is the element editable - wrapper for _editable property as 3.1 uses editable * * @since 3.0.7 * * @return bool */ public function isEditable() { return $this->editable; } /** * Set the element edit state - wrapper for _editable property as 3.1 uses editable * * @param bool $editable Is the element editable * * @since 3.0.7 * * @return void */ public function setEditable($editable) { $this->editable = $editable; } /** * Check user can view the read only element OR view in list view * * @param string $view View list/form @since 3.0.7 * * @return bool can view or not */ public function canView($view = 'form') { $default = 1; $key = $view == 'form' ? 'view' : 'listview'; $prop = $view == 'form' ? 'view_access' : 'list_view_access'; $params = $this->getParams(); if (!is_object($this->access) || !isset($this->access->{$key})) { $groups = $this->user->getAuthorisedViewLevels(); $this->access->$key = in_array($params->get($prop, $default), $groups); } // If no group access, can override with check on lookup element's value = logged in user id. if (!$this->access->$key && $params->get('view_access_user', '') !== '' && $view == 'form') { $formModel = $this->getFormModel(); $data = $formModel->getData(); if (!empty($data) && $this->user->get('id') !== 0) { $lookUpId = $params->get('view_access_user', ''); $lookUp = $formModel->getElement($lookUpId, true); // Could be a linked parent element in which case the form doesn't contain the element whose id is $lookUpId if (!$lookUp) { $lookUp = FabrikWorker::getPluginManager()->getElementPlugin($lookUpId); } if ($lookUp) { $fullName = $lookUp->getFullName(true, true); $value = $formModel->getElementData($fullName, true); $this->access->$key = ($this->user->get('id') == $value) ? true : false; } else { FabrikWorker::logError('Did not load element ' . $lookUpId . ' for element::canView()', 'error'); } } } else if ($this->access->$key && $view == 'form') { $formModel = $this->getFormModel(); $pluginManager = FabrikWorker::getPluginManager(); if (in_array(false, $pluginManager->runPlugins('onElementCanView', $formModel, 'form', $this))) { $this->access->$key = false; } } else if ($this->access->$key && $view == 'list') { $listModel = $this->getListModel(); $pluginManager = FabrikWorker::getPluginManager(); if (in_array(false, $pluginManager->runPlugins('onElementCanViewList', $listModel, 'list', $this))) { $this->access->$key = false; } } return $this->access->$key; } /** * Check if the user can use the active element * If location is 'list' then we don't check the group canEdit() option - causes inline edit plugin not to work * when followed by a update_col plugin. * * @param string $location To trigger plugin on form/list for elements * @param string $event To trigger plugin on * * @return bool can use or not */ public function canUse($location = 'form', $event = null) { // Odd! even though defined in initialize() for confirmation plugin access was not set. if (!isset($this->access)) { $this->access = new stdClass; } if (!is_object($this->access) || !isset($this->access->use)) { /** * $$$ hugh - testing new "Option 5" for group show, "Always show read only" * So if element's group show is type 5, then element is de-facto read only. */ if ($location !== 'list' && !$this->getGroupModel()->canEdit()) { $this->access->use = false; } else { $viewLevel = $this->getElement()->access; if (!$this->getFormModel()->isNewRecord()) { $editViewLevel = $this->getParams()->get('edit_access'); if ($editViewLevel) { $viewLevel = $editViewLevel; } } $groups = $this->user->getAuthorisedViewLevels(); $this->access->use = in_array($viewLevel, $groups); // Override with check on lookup element's value = logged in user id. $params = $this->getParams(); if (!$this->access->use && $params->get('edit_access_user', '') !== '' && $location == 'form') { $formModel = $this->getFormModel(); $data = $formModel->getData(); if (!empty($data) && $this->user->get('id') !== 0) { $lookUpId = $params->get('edit_access_user', ''); $lookUp = $formModel->getElement($lookUpId, true); // Could be a linked parent element in which case the form doesn't contain the element whose id is $lookUpId if (!$lookUp) { $lookUp = FabrikWorker::getPluginManager()->getElementPlugin($lookUpId); } if ($lookUp) { $fullName = $lookUp->getFullName(true, true); $value = (array) $formModel->getElementData($fullName, true); $this->access->use = in_array($this->user->get('id'), $value); } else { FabrikWorker::logError('Did not load element ' . $lookUpId . ' for element::canUse()', 'error'); } } } else if ($this->access->use && $location == 'form') { $formModel = $this->getFormModel(); $pluginManager = FabrikWorker::getPluginManager(); if (in_array(false, $pluginManager->runPlugins('onElementCanUse', $formModel, 'form', $this))) { $this->access->use = false; } } } } return $this->access->use; } /** * Defines if the user can use the filter related to the element * * @return bool true if you can use */ public function canUseFilter() { if (!is_object($this->access) || !isset($this->access->filter)) { $groups = $this->user->getAuthorisedViewLevels(); // $$$ hugh - fix for where certain elements got created with 0 as the // the default for filter_access, which isn't a legal value, should be 1 $filterAccess = $this->getParams()->get('filter_access'); $filterAccess = $filterAccess == '0' ? '1' : $filterAccess; $this->access->filter = in_array($filterAccess, $groups); } return $this->access->filter; } /** * Set/get if element should record its data in the database * * @deprecated - not used * * @return bool */ public function setIsRecordedInDatabase() { return true; } /** * Internal element validation * * @param array $data Form data * @param int $repeatCounter Repeat group counter * * @return bool */ public function validate($data, $repeatCounter = 0) { return true; } /** * Get validation error - run through Text * * @return string */ public function getValidationErr() { return Text::_($this->validationError); } /** * Is the element consider to be empty for purposes of rendering on the form, * i.e. for assigning classes, etc. Can be overridden by individual elements. * * NOTE - this was originally intended for validation, but wound up being used for both validation * AND rendering. Which doesn't really work, because the $data can be entirely different. Tried * adding dataConsideredEmptyForValidation() below, but that causes issues where elements don't have * one, we'd need to go through in one swoop and split them out in every element. So for now, leave this * as the default which is called in both contexts, BUT the notempty validation checks to see if an * element model has a dataCOnsideredEmptyForValidation() method and calls that in preference to this * if it does. We can come back and revisit this issue, as we gradually split out the funcitonality in each * element type. * * @param array $data Data to test against * @param int $repeatCounter Repeat group # * * @return bool */ public function dataConsideredEmpty($data, $repeatCounter) { return ($data == '') ? true : false; } /** * is the element consider to be empty for validation purposes, on form submit * Used in isempty validation rule. Split out from dataConsideredEmpty in 3.2 * * NOTE - see comments on dataConsideredEmpty(), have to hold off on putting this in the main model. * * @param array $data Data to test against * @param int $repeatCounter Repeat group # * * @return bool * * @since 3.2 */ /* public function dataConsideredEmptyForValidation($data, $repeatCounter) { return ($data == '') ? true : false; } */ /** * Get an array of element html ids and their corresponding * js events which trigger a validation. * Examples of where this would be overwritten include timedate element with time field enabled * * @param int $repeatCounter Repeat group counter * * @return array html ids to watch for validation */ public function getValidationWatchElements($repeatCounter) { $id = $this->getHTMLId($repeatCounter); $ar = array('id' => $id, 'triggerEvent' => $this->getChangeEvent()); return array($ar); } /** * Manipulates posted form data for insertion into database * * @param mixed $val This elements posted form data * @param array $data Posted form data * * @return mixed */ public function storeDatabaseFormat($val, $data) { if (is_array($val) && count($val) === 1) { $val = array_shift($val); } if (is_array($val) || is_object($val)) { return json_encode($val); } else { return $val; } } /** * When importing csv data you can run this function on all the data to * format it into the format that the form would have submitted the date * * @param array &$data To prepare * @param string $key List column heading * @param bool $isRaw Data is raw * * @return array data */ public function prepareCSVData(&$data, $key, $isRaw = false) { return $data; } /** * Determines if the data in the form element is used when updating a record * * @param mixed $val Element form data * * @return bool true if ignored on update, default = false */ public function ignoreOnUpdate($val) { return false; } /** * can be overwritten in add-on class * * checks the posted form data against elements INTERNAL validation rule - e.g. file upload size / type * * @param array $aErrors Existing errors * @param object &$groupModel Group model * @param object &$formModel Form model * @param array $data Posted data * * @deprecated - not used * * @return array updated errors */ public function validateData($aErrors, &$groupModel, &$formModel, $data) { return $aErrors; } /** * Determines the label used for the browser title * in the form/detail views * * @param array $data Form data * @param int $repeatCounter When repeating joined groups we need to know what part of the array to access * @param array $opts Options * * @return string Text to add to the browser's title */ public function getTitlePart($data, $repeatCounter = 0, $opts = array()) { $titlePart = $this->getValue($data, $repeatCounter, $opts); return is_array($titlePart) ? implode(', ', $titlePart) : $titlePart; } /** * Should the element ignore a form or list row copy, and use the default regardless * * @return bool */ public function defaultOnCopy() { $params = $this->getParams(); return $params->get('default_on_copy', '0') === '1'; } /** * This really does get just the default value (as defined in the element's settings) * * @param array $data Form data * * @return mixed */ public function getDefaultValue($data = array()) { if (!isset($this->default)) { $w = new FabrikWorker; $element = $this->getElement(); $default = $w->parseMessageForPlaceHolder($element->default, $data); if ($element->eval == "1" && is_string($default)) { /** * Inline edit with a default eval'd "return FabrikHelperElement::filterValue(290);" * was causing the default to be eval'd twice (no idea y) - add in check for 'return' into eval string * see http://fabrikar.com/forums/showthread.php?t=30859 */ if (!stristr($default, 'return')) { $this->_default = $default; } else { FabrikHelperHTML::debug($default, 'element eval default:' . $element->label); FabrikWorker::clearEval(); $default = Php::Eval(['code' => $default, 'vars'=>['data'=>$data]]); FabrikWorker::logEval($default, 'Caught exception on eval of ' . $element->name . ': %s'); // Test this does stop error $this->_default = $default === false ? '' : $default; } } if (is_array($default)) { foreach ($default as &$d) { $d = Text::_($d); } $this->default = $default; } else { $this->default = Text::_($default); } } return $this->default; } /** * Called by form model to build an array of values to encrypt * * @param array &$values Previously encrypted values * @param array $data Form data * @param int $c Repeat group counter * * @return void */ public function getValuesToEncrypt(&$values, $data, $c) { $name = $this->getFullName(true, false); $opts = array('raw' => true); $group = $this->getGroup(); if ($group->canRepeat()) { if (!array_key_exists($name, $values)) { $values[$name]['data'] = array(); } $values[$name]['data'][$c] = $this->getValue($data, $c, $opts); } else { $values[$name]['data'] = $this->getValue($data, $c, $opts); } } /** * Element plugin specific method for setting unencrypted values back into post data * * @param array &$post Data passed by ref * @param string $key Key * @param string $data Elements unencrypted data * * @return void */ public function setValuesFromEncryt(&$post, $key, $data) { FArrayHelper::setValue($post, $key, $data); FArrayHelper::setValue($_REQUEST, $key, $data); // $$$rob even though $post is passed by reference - by adding in the value // we aren't actually modifying the $_POST var that post was created from $this->app->getInput()->set($key, $data); } /** * Determines the value for the element in the form view * * @param array $data Form data * @param int $repeatCounter When repeating joined groups we need to know what part of the array to access * * @return string value */ public function getROValue($data, $repeatCounter = 0) { return $this->getValue($data, $repeatCounter); } /** * Helper method to get the default value used in getValue() * For readonly elements: * If the form is new we need to get the default value * If the form is being edited we don't want to get the default value * Otherwise use the 'use_default' value in $opts, defaulting to true * * @param array $data Form data * @param array $opts Options * * @since 3.0.7 * * @return mixed value */ protected function getDefaultOnACL($data, $opts) { // Rob - 31/10/2012 - if readonly and editing an existing record we don't want to show the default label if (!$this->isEditable() && (int)FArrayHelper::getValue($data, 'rowid') != 0) { $opts['use_default'] = false; } /** * $$$rob - if no search form data submitted for the search element then the default * selection was being applied instead * otherwise get the default value so if we don't find the element's value in $data we fall back on this value */ return FArrayHelper::getValue($opts, 'use_default', true) == false ? '' : $this->getDefaultValue($data); } /** * Use in list model storeRow() to determine if data should be stored. * Currently only supported for db join elements whose values are default values * avoids casing '' into 0 for int fields * * @param array $data Data being inserted * @param mixed $val Element value to insert into table * * @since 3.0.7 * * @return boolean */ public function dataIsNull($data, $val) { return false; } /** * Determines the value for the element in the form view * * @param array $data Form data * @param int $repeatCounter When repeating joined groups we need to know what part of the array to access * @param array $opts Options, 'raw' = 1/0 use raw value * * @return string value */ public function getValue($data, $repeatCounter = 0, $opts = array()) { $input = $this->app->getInput(); if (!isset($this->defaults)) { $this->defaults = array(); } $key = $repeatCounter . '.' . serialize($opts); if (!array_key_exists($key, $this->defaults)) { $groupRepeat = $this->getGroupModel()->canRepeat(); $default = $this->getDefaultOnACL($data, $opts); $name = $this->getFullName(true, false); if (FArrayHelper::getValue($opts, 'raw', 0) == 1) { $name .= '_raw'; } /** * @FIXME - if an element is NULL in the table, we will be applying the default even if this * isn't a new form. Probaby needs to be a global option, although not entirely sure what * we would set it to ... */ //Fix: don't apply the default if a rowid is set and >=1 (= existing record) if (array_key_exists('rowid',$data) && !empty($data['rowid'])) { $values = FArrayHelper::getValue($data, $name); } else { $values = FArrayHelper::getValue($data, $name, $default); } // Querystring override (seems on http://fabrikar.com/subscribe/form/22 querystring var was not being set into $data) if (FArrayHelper::getValue($opts, 'use_querystring', false)) { if ((is_array($values) && empty($values)) || $values === '') { // Trying to avoid errors if value is an array $values = $input->get($name, null, 'array'); if (is_null($values) || (count($values) === 1 && $values[0] == '')) { $values = $input->get($name, '', 'string'); } } } if ($groupRepeat) { // Weird bug where stdClass with key 0, when cast to (array) you couldn't access values[0] if (is_object($values)) { $values = ArrayHelper::fromObject($values); } if (!is_array($values)) { $values = (array) $values; } $values = FArrayHelper::getValue($values, $repeatCounter, ''); } if (FArrayHelper::getValue($opts, 'runplugins', false)) { $formModel = $this->getFormModel(); FabrikWorker::getPluginManager()->runPlugins('onGetElementDefault', $formModel, 'form', $this); } $this->defaults[$key] = $values; } return $this->defaults[$key]; } /** * Is the element hidden or not - if not set then return false * * @return bool */ public function isHidden() { $element = $this->getElement(); return ($element->hidden == true) ? true : false; } /** * Used in things like date when its id is suffixed with _cal * called from getLabel(); * * @param string &$id Initial id * * @return void */ protected function modHTMLId(&$id) { } /** * Should the element be tipped? * * @param string $mode Form/list render context * @param array $data data array * * @since 3.0.6 * * @return bool */ private function isTipped($mode = 'form', $data = array()) { $formModel = $this->getFormModel(); if ($formModel->getParams()->get('tiplocation', 'tip') !== 'tip' && $mode === 'form') { return false; } $params = $this->getParams(); if (empty($this->getTipText($data))) { return false; } if ($mode == 'form' && (!$formModel->isEditable() && $params->get('labelindetails', true) == false)) { return false; } if ($mode === 'list' && $params->get('labelinlist', false) == false) { return false; } return true; } /** * Get list heading label * * @return string */ public function getListHeading() { $params = $this->getParams(); $element = $this->getElement(); $label = $params->get('alt_list_heading') == '' ? $element->label : $params->get('alt_list_heading'); return Text::_($label); } /** * Get the element's HTML label * * @param int $repeatCounter Group repeat counter * @param string $tmpl Form template * * @return string label */ public function getLabel($repeatCounter, $tmpl = '') { $element = $this->getElement(); $this->modHTMLId($elementHTMLId); $model = $this->getFormModel(); $displayData = new stdClass; $displayData->canView = $this->canView(); $displayData->id = $this->getHTMLId($repeatCounter); $displayData->canUse = $this->canUse(); // $displayData->j3 = FabrikWorker::j3(); $displayData->j3 = true; $displayData->hidden = $this->isHidden(); $displayData->label = Text::_($element->label); $displayData->altLabel = $this->getListHeading(); $displayData->hasLabel = $this->hasLabel; $displayData->view = $this->app->getInput()->get('view', 'form'); $displayData->tip = $this->tipHtml($model->data); $displayData->tipText = $this->tipTextAndValidations('form', $model->data); $displayData->rollOver = $this->isTipped('form', $model->data); $displayData->isEditable = $this->isEditable(); $displayData->tipOpts = $this->tipOpts(); $displayData->labelPosition = $this->group->labelPosition(); $labelClass = 'form-label'; if ($displayData->canView || $displayData->canUse) { $labelClass .= ' fabrikLabel '; if (empty($displayData->label)) { $labelClass .= ' fabrikEmptyLabel'; } if ($displayData->rollOver) { $labelClass .= ' fabrikHover'; } if ($displayData->hasLabel && !$displayData->hidden) { if ($displayData->tip !== '') { //trob: this triggers FabrikJS, use layout components\com_fabrik\layouts\element\fabrik-element-label.php //$labelClass .= ' fabrikTip'; } } } $displayData->icons = ''; $iconOpts = array('icon-class' => 'small'); if ($displayData->rollOver) { $displayData->icons .= FabrikHelperHTML::image('question-sign', 'form', $tmpl, $iconOpts) . ' '; } if ($displayData->isEditable) { $displayData->icons .= $this->validator->labelIcons(); } $displayData->labelClass = $labelClass; $layout = FabrikHelperHTML::getLayout('fabrik-element-label', $this->labelPaths()); $str = $layout->render($displayData); return $str; } /** * Get an array of paths to look for the element template. * * @return array */ protected function labelPaths() { // base built-in layouts $basePath = COM_FABRIK_BASE . 'components/com_fabrik/layouts/element'; $pluginPath = COM_FABRIK_BASE . '/plugins/fabrik_element/' . $this->getPluginName() . '/layouts'; // Custom per template layouts $view = $this->getFormModel()->isEditable() ? 'form' : 'details'; $tmplPath = COM_FABRIK_FRONTEND . '/views/'. $view . '/tmpl/' . $this->getFormModel()->getTmpl() . '/layouts/element/'; $tmplElPath = COM_FABRIK_FRONTEND . '/views/'. $view . '/tmpl/' . $this->getFormModel()->getTmpl() . '/layouts/element/' . $this->getFullName(true, false); // Custom per theme layouts $perThemePath = JPATH_THEMES . '/' . $this->app->getTemplate() . '/html/layouts/com_fabrik/element'; $perElementPath = JPATH_THEMES . '/' . $this->app->getTemplate() . '/html/layouts/com_fabrik/element/' . $this->getFullName(true, false); return array($basePath, $pluginPath, $tmplPath, $tmplElPath, $perThemePath, $perElementPath); } /** * Set fabrikErrorMessage div with potential error messages * * @param int $repeatCounter repeat counter * @param string $tmpl template * * @return string */ protected function addErrorHTML($repeatCounter, $tmpl = '') { $err = $this->getErrorMsg($repeatCounter); $err = htmlspecialchars($err, ENT_QUOTES); $layout = FabrikHelperHTML::getLayout('element.fabrik-element-error'); $displayData = new stdClass; $displayData->err = $err; $displayData->tmpl = $tmpl; return $layout->render($displayData); } /** * Add tips on element labels * does ACL check on element's label in details setting * * @param string $txt Label * @param array $data Row data * @param string $mode Form/list render context * * @return string Label with tip */ protected function rollover_old($txt, $data = array(), $mode = 'form') { if (is_object($data)) { $data = ArrayHelper::fromObject($data); } $rollOver = $this->tipHtml($data, $mode); return $rollOver !== '' ? '<span class="fabrikTip" ' . $rollOver . '>' . $txt . '</span>' : $txt; } /** * Add tips on element labels * does ACL check on element's label in details setting * * @param string $txt Label * @param array $data Row data * @param string $mode Form/list render context * * @return string Label with tip */ protected function rollover($txt, $data = array(), $mode = 'form') { if (is_object($data)) { $data = ArrayHelper::fromObject($data); } $title = $this->tipTextAndValidations($mode, $data); // $$$ hugh - only run the layout if there's a title, save row rendering time if (!empty($title)) { $layout = FabrikHelperHTML::getLayout('element.fabrik-element-tip'); $displayData = new stdClass; $displayData->tipTitle = $title; $displayData->tipText = $txt; $displayData->rollOver = $this->isTipped('form', $data); $displayData->isEditable = $this->isEditable(); $displayData->tipOpts = $this->tipOpts(); $rollOver = $layout->render($displayData); } else { // defensive coding for corner case of calcs with JSON data $rollOver = is_scalar($txt) ? (string)$txt : ''; } return $rollOver; } /** * Get the hover tip options * * @return stdClass */ protected function tipOpts() { $params = $this->getParams(); $opts = new stdClass; $pos = $params->get('tiplocation', 'top'); $opts->formTip = true; $opts->position = $pos; $opts->trigger = 'hover'; $opts->notice = true; if ($this->editable) { if ($this->validator->hasValidations()) { $opts->heading = Text::_('COM_FABRIK_VALIDATION'); } } return $opts; } /** * Get Hover tip text and validation text * * @param string $mode View mode form/list * @param array $data Model data * * @return string */ protected function tipTextAndValidations($mode, $data = array()) { $lines = array(); $tmpl = $this->getFormModel()->getTmpl(); if (($mode === 'list' || !$this->validator->hasValidations()) && !$this->isTipped($mode, $data)) { return ''; } if ($this->isTipped($mode, $data)) { $lines[] = '<li>' . FabrikHelperHTML::image('question-sign', 'form', $tmpl) . ' ' . $this->getTipText($data) . '</li>'; } if ($mode === 'form') { $validationTexts = $this->validator->hoverTexts(); foreach ($validationTexts as $validationText) { $lines[] = '<li>' . $validationText . '</li>'; } } if (count($lines) > 0) { array_unshift($lines,'<ul class="validation-notices" style="list-style:none">'); $lines[] = '</ul>'; $lines = array_unique($lines); $rollOver = implode('', $lines); // $$$ rob - looks like htmlspecialchars is needed otherwise invalid markup created and pdf output issues. $rollOver = htmlspecialchars($rollOver, ENT_QUOTES); } else { $rollOver = ''; } return $rollOver; } /** * Get the element tip HTML * * @param array $data to use in parse holders - defaults to form's data * * @return string tip HTML */ protected function getTipText($data = null) { if (is_null($data)) { $data = $this->getFormModel()->data; } $model = $this->getFormModel(); $params = $this->getParams(); if (!$model->isEditable() && !$params->get('labelindetails')) { return ''; } $w = new FabrikWorker; $tip = $w->parseMessageForPlaceHolder($params->get('rollover'), $data); if ($params->get('tipseval')) { FabrikWorker::clearEval(); $res = Php::Eval(['code' => $tip, 'vars' => ['data'=>$data]]); FabrikWorker::logEval($res, 'Caught exception (%s) on eval of ' . $this->getElement()->name . ' tip: ' . str_replace('%','%',$tip)); $tip = $res; } $tip = Text::_($tip); return $tip; } /** * Used for the name of the filter fields * For element this is an alias of getFullName() * Overridden currently only in databasejoin class * * @return string element filter name */ public function getFilterFullName() { return FabrikString::safeColName($this->getFullName(true, false)); } /** * Get the field name to use in the list's slug url * * @param bool $raw raw * * @since 3.0.6 * * @return string element slug name */ public function getSlugName($raw = false) { return $this->getFilterFullName(); } /** * Set and override element full name (used in pw element) * * @param string $name Element name * @param bool $useStep Concat name with form's step element (true) or with '.' (false) default true * @param bool $incRepeatGroup Include '[]' at the end of the name (used for repeat group elements) default true * * @return void */ public function setFullName($name = '', $useStep = true, $incRepeatGroup = true) { $groupModel = $this->getGroup(); $formModel = $this->getFormModel(); $element = $this->getElement(); $key = $element->id . '.' . $groupModel->get('id') . '_' . $formModel->getId() . '_' . $useStep . '_' . $incRepeatGroup; $this->fullNames[$key] = $name; } /** * If already run then stored value returned * * @param bool $useStep Concat name with form's step element (true) or with '.' (false) default true * @param bool $incRepeatGroup Include '[]' at the end of the name (used for repeat group elements) default true * * @return string element full name */ public function getFullName($useStep = true, $incRepeatGroup = true) { $groupModel = $this->getGroup(); $formModel = $this->getFormModel(); $listModel = $this->getListModel(); $element = $this->getElement(); $key = $element->id . '.' . $groupModel->get('id') . '_' . $formModel->getId() . '_' . $useStep . '_' . $incRepeatGroup; if (isset($this->fullNames[$key])) { return $this->fullNames[$key]; } $table = $listModel->getTable(); $db_table_name = $table->db_table_name; $thisStep = ($useStep) ? $formModel->joinTableElementStep : '.'; if ($groupModel->isJoin()) { $joinModel = $groupModel->getJoinModel(); $join = $joinModel->getJoin(); $fullName = $join->table_join . $thisStep . $element->name; } else { $fullName = $db_table_name . $thisStep . $element->name; } if ($groupModel->canRepeat() == 1 && $incRepeatGroup) { $fullName .= '[]'; } $this->fullNames[$key] = $fullName; return $fullName; } /** * Get order by full name * * @param bool $useStep Concat name with form's step element (true) or with '.' (false) default true * * @return string Order by full name */ public function getOrderbyFullName($useStep = true) { return $this->getFullName($useStep); } /** * When copying elements from an existing table * once a copy of all elements has been made run them through this method * to ensure that things like watched element id's are updated * * @param array $newElements copied element ids (keyed on original element id) * * @return void */ public function finalCopyCheck($newElements) { // Overwritten in element class } /** * Copy an element table row * * @param int $id Element id to copy * @param string $copyText Feedback msg * @param int $groupId Group model id * @param string $name New element name * * @return mixed Error or new row */ public function copyRow($id, $copyText = 'Copy of %s', $groupId = null, $name = null) { /** @var FabrikTableElement $rule */ $rule = FabTable::getInstance('Element', 'FabrikTable'); $rule->load((int) $id); $rule->id = null; $rule->label = sprintf($copyText, $rule->label); if (!is_null($groupId)) { $rule->group_id = $groupId; } if (!is_null($name)) { $rule->name = $name; } $groupModel = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('Group', 'FabrikFEModel'); $groupModel->setId($groupId); $groupListModel = $groupModel->getListModel(); // $$$ rob - if its a joined group then it can have the same element names if ((int) $groupModel->getGroup()->is_join === 0) { if ($groupListModel->fieldExists($rule->name, array(), $groupModel)) { $this->app->enqueueMessage(Text::_('COM_FABRIK_ELEMENT_NAME_IN_USE'), 'error'); return false; } } $date = $this->date; $tz = new \DateTimeZone($this->app->get('offset')); $date->setTimezone($tz); $rule->created = $date->toSql(); $params = $rule->params == '' ? new stdClass : json_decode($rule->params); $params->parent_linked = 1; $rule->params = json_encode($params); $rule->parent_id = $id; $config = ComponentHelper::getParams('com_fabrik'); if ($config->get('unpublish_clones', false)) { $rule->published = 0; } $rule->store(); /** * I thought we did this in an overridden element model method, like onCopy? * if its a database join then add in a new join record */ if (is_a($this, 'PlgFabrik_ElementDatabasejoin')) { $join = FabTable::getInstance('Join', 'FabrikTable'); $join->load(array('element_id' => $id)); $join->id = null; $join->element_id = $rule->id; $join->group_id = $rule->group_id; $join->store(); } // Copy js events $db = FabrikWorker::getDbo(true); $query = $db->getQuery(true); $query->select('id')->from('#__fabrik_jsactions')->where('element_id = ' . (int) $id); $db->setQuery($query); $actions = $db->loadColumn(); foreach ($actions as $id) { $jsCode = FabTable::getInstance('Jsaction', 'FabrikTable'); $jsCode->load($id); $jsCode->id = 0; $jsCode->element_id = $rule->id; $jsCode->store(); } return $rule; } /** * Get the element's raw label (used for details view, not wrapped in <label> tags * * @return string Label */ protected function getRawLabel() { return $this->element->label; } /** * This was in the views display and _getElement code but seeing as its used * by multiple views its safer to have it here * * @param int $c Repeat group counter * @param int $elCount Order in which the element is shown in the form * @param string $tmpl Template * * @return mixed - false if you shouldn't continue to render the element */ public function preRender($c, $elCount, $tmpl) { $model = $this->getFormModel(); $groupModel = $this->getGroup(); $group = $groupModel->getGroupProperties($model); if (!$this->canUse() && !$this->canView()) { return false; } if (!$this->canUse()) { $this->setEditable(false); } else { $editable = $model->isEditable() ? true : false; $this->setEditable($editable); } // Force reload? $this->HTMLids = null; $elementTable = $this->getElement(); $element = new stdClass; $element->startRow = 0; $element->endRow = 0; $elHTMLName = $this->getFullName(); // If the element is in a join AND is the join's foreign key then we don't show the element if ($elementTable->name == $this->_foreignKey) { $element->label = ''; $element->error = ''; $this->element->hidden = true; } else { $element->error = $this->getErrorMsg($c); } $element->plugin = $elementTable->plugin; $element->hidden = $this->isHidden(); $element->id = $this->getHTMLId($c); $element->className = 'fb_el_' . $element->id; //$element->containerClass = $this->containerClass($element); $element->element = $this->preRenderElement($model->data, $c); $element->bsClass = $this->getBsClass(); // Ensure that view data property contains the same html as the group's element $model->tmplData[$elHTMLName] = $element->element; $element->label_raw = Text::_($this->getRawLabel()); // GetLabel needs to know if the element is editable if ($elementTable->name != $this->_foreignKey) { $l = $this->getLabel($c, $tmpl); $w = new FabrikWorker; $element->label = $w->parseMessageForPlaceHolder($l, $model->data); } $element->errorTag = $this->addErrorHTML($c, $tmpl); $element->element_ro = $this->getROElement($model->data, $c); $element->value = $this->getValue($model->data, $c); $element->containerClass = $this->containerClass($element); $elName = $this->getFullName(true, false); if (array_key_exists($elName . '_raw', $model->data)) { $element->element_raw = $model->data[$elName . '_raw']; } else { $element->element_raw = array_key_exists($elName, $model->data) ? $model->data[$elName] : $element->value; } if ($this->dataConsideredEmpty($element->element_ro, $c)) { $element->containerClass .= ' fabrikDataEmpty'; $element->dataEmpty = true; } else { $element->dataEmpty = false; } // Tips (if not rendered as hovers) $tip = $this->getTipText(); if ($tip !== '') { $tip = FabrikHelperHTML::image('question-sign', 'form', $tmpl) . ' ' . $tip; } $element->labels = $groupModel->labelPosition('form'); $element->dlabels = $groupModel->labelPosition('details'); switch ($model->getParams()->get('tiplocation')) { default: case 'tip': $element->tipAbove = ''; $element->tipBelow = ''; $element->tipSide = ''; break; case 'above': $element->tipAbove = $tip; $element->tipBelow = ''; $element->tipSide = ''; break; case 'below': $element->tipAbove = ''; $element->tipBelow = $tip; $element->tipSide = ''; break; case 'side': $element->tipAbove = ''; $element->tipBelow = ''; $element->tipSide = $tip; break; } return $element; } /** * Build the tip HTML * * @param array $data Data * @param string $mode Mode Form/List * * @return string */ protected function tipHtml($data = array(), $mode = 'form') { $title = $this->tipTextAndValidations($mode, $data); $opts = $this->tipOpts(); $opts = json_encode($opts); return $title !== '' ? 'title="' . $title . '" opts=\'' . $opts . '\'' : ''; } /** * Get the class name for the element wrapping dom object * * @param object $element element row * * @since 3.0 * * @return string class names */ protected function containerClass($element) { $item = $this->getElement(); $c = array('fabrikElementContainer', 'plg-' . $item->plugin, $element->className); if ($element->hidden) { $c[] = 'fabrikHide'; } else { /** * $$$ hugh - adding a class name for repeat groups, as per: * http://fabrikar.com/forums/showthread.php?p=165128#post165128 * But as per my response on that thread, if this turns out to be a performance * hit, may take it out. That said, I think having this class will make things * easier for custom styling when the element ID isn't constant. */ $groupModel = $this->getGroupModel(); if ($groupModel->canRepeat()) { $c[] = 'fabrikRepeatGroup___' . $this->getFullName(true, false); } } if ($element->error != '') { $c[] = 'fabrikError'; } $c[] = $this->getParams()->get('containerclass', ''); $c[] = $this->getRowClassRO($element); // first run plugins, which may set row class $formModel = $this->getFormModel(); $args = new stdClass; $args->rowClass = []; $args->data = $formModel->data; $pluginResults = \Fabrik\Helpers\Worker::getPluginManager()->runPlugins( 'onElementContainerClass', $formModel, 'form', $this, $args ); $c = array_merge($c, $args->rowClass); return implode(' ', $c); } /** * Merge the rendered element into the views element storage arrays * * @param object $element to merge * @param array &$aElements element array * @param array &$namedData Form data * @param array &$aSubGroupElements sub group element array * * @return void */ public function stockResults($element, &$aElements, &$namedData, &$aSubGroupElements) { $elHTMLName = $this->getFullName(); $aElements[$this->getElement()->name] = $element; /** * $$$ rob 12/10/2012 - $namedData is the formModels data - commenting out as the form data needs to be consistent * as we loop over elements - this was setting from a string to an object ?!!!???!! * $namedData[$elHTMLName] = $element; */ if ($elHTMLName) { // $$$ rob was keyed on int but that's not very useful for templating $aSubGroupElements[$this->getElement()->name] = $element; } } /** * Pre-render just the element (no labels etc.) * Was _getElement but this was ambiguous with getElement() and method is public * * @param array $data data * @param int $repeatCounter repeat group counter * * @return string */ public function preRenderElement($data, $repeatCounter = 0) { $groupModel = $this->getGroupModel(); if (!$this->canView() && !$this->canUse()) { return ''; } // Used for working out if the element should behave as if it was in a new form (joined grouped) even when editing a record $this->inRepeatGroup = $groupModel->canRepeat(); $this->_inJoin = $groupModel->isJoin(); $opts = array('runplugins' => 1); $this->getValue($data, $repeatCounter, $opts); if ($this->isEditable()) { return $this->render($data, $repeatCounter); } else { $htmlId = $this->getHTMLId($repeatCounter); // $$$ rob even when not in ajax mode the element update() method may be called in which case we need the span // $$$ rob changed from span wrapper to div wrapper as element's content may contain divs which give html error // Placeholder to be updated by ajax code // @TODO the entity decode causes problems on RO with tooltips $v = $this->getROElement($data, $repeatCounter); $v = html_entity_decode($v); //$v = $v == '' ? ' ' : $v; return '<div class="fabrikElementReadOnly" id="' . $htmlId . '">' . $v . '</div>'; } } /** * Get read-only element * Was _getROElement() but is a public method * * @param array $data data * @param int $repeatCounter repeat group counter * * @return string */ public function getROElement($data, $repeatCounter = 0) { if (!$this->canView() && !$this->canUse()) { return ''; } $editable = $this->isEditable(); $this->setEditable(false); $v = $this->render($data, $repeatCounter); $this->addCustomLink($v, $data, $repeatCounter); $this->setEditable($editable); return $v; } /** * Add custom link to element - must be uneditable for link to be added * * @param string &$v value * @param array $data row data * @param int $repeatCounter repeat counter * * @return string */ protected function addCustomLink(&$v, $data, $repeatCounter = 0) { if ($this->isEditable()) { return $v; } $params = $this->getParams(); $customLink = $params->get('custom_link', ''); if ($customLink !== '' && $this->getElement()->link_to_detail == '1' && $params->get('custom_link_indetails', true)) { $w = new FabrikWorker; /** * $$$ hugh - this should really happen elsewhere, but I needed a quick fix for handling * {slug} in detail view links, which for some reason are not 'stringURLSafe' at this point, * so they are like "4:A Page Title" instead of 4-a-page-title. */ if (strstr($customLink, '{slug}') && array_key_exists('slug', $data)) { $slug = str_replace(':', '-', $data['slug']); $slug = ApplicationHelper::stringURLSafe($slug); $customLink = str_replace('{slug}', $slug, $customLink); } /** * Testing new parseMessageForRepeats(), see comments on the function itself. */ $customLink = $w->parseMessageForRepeats($customLink, $data, $this, $repeatCounter); $customLink = $w->parseMessageForPlaceHolder($customLink, $data); $customLink = $this->getListModel()->parseMessageForRowHolder($customLink, $data); if (trim($customLink) !== '') { $v = '<a href="' . $customLink . '" data-iscustom="1">' . $v . '</a>'; } } return $v; } /** * Get any html error messages * * @param int $repeatCount group repeat count * * @return string error messages */ protected function getErrorMsg($repeatCount = 0) { $arErrors = $this->getFormModel()->errors; $parsed_name = $this->getFullName(); $err_msg = ''; $parsed_name = FabrikString::rtrimword($parsed_name, '[]'); if (isset($arErrors[$parsed_name])) { if (array_key_exists($repeatCount, $arErrors[$parsed_name])) { if (is_array($arErrors[$parsed_name][$repeatCount])) { $err_msg = implode('<br />', $arErrors[$parsed_name][$repeatCount]); } else { $err_msg .= $arErrors[$parsed_name][$repeatCount]; } } } return $err_msg; } /** * Draws the html form element * * @param array $data To pre-populate element with * @param int $repeatCounter Repeat group counter * * @return string elements html */ public function render($data, $repeatCounter = 0) { return 'need to overwrite in element plugin class'; } /** * Format the read only output for the page * * @param string $value Initial value * @param string $label Label * * @return string Read only value */ protected function getReadOnlyOutput($value, $label) { $params = $this->getParams(); if ($params->get('icon_folder') != -1 && $params->get('icon_folder') != '') { $icon = $this->replaceWithIcons($value); if ($this->iconsSet) { $label = $icon; } } return $label; } /** * Get hidden field * * @param string $name Element name * @param string $value Element value * @param string $id Element id * @param string $class Class name * * @return string */ protected function getHiddenField($name, $value, $id = '', $class = 'fabrikinput inputbox hidden') { $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); $opts = array('class' => $class, 'type' => 'hidden', 'name' => $name, 'value' => $value, 'id' => $id); return $this->buildInput('input', $opts); } /** * Helper method to build an input field * * @deprecated use LayoutInterfaces instead * * @param string $node Input type default 'input' * @param array $bits Input property => value * @param bool $shortTag Is $node a <node/> or <node></node> tag * * @return string input */ protected function buildInput($node = 'input', $bits = array(), $shortTag = true) { $str = '<' . $node . ' '; $value = ''; foreach ($bits as $key => $val) { if ($node === 'textarea' && $key === 'value') { $value = $val; continue; } $str .= $key . '="' . $val . '" '; } $str .= $shortTag ? '' : '>'; if (!$shortTag && $value !== '') { $str .= $value; } $str .= $shortTag ? '/>' : '</' . $node . '>'; return $str; } /** * Helper function to build the property array used in buildInput() * * @param int $repeatCounter Repeat group counter * @param mixed $type Null/string $type property (if null then password/text applied as default) * * @return array input properties key/value */ protected function inputProperties($repeatCounter, $type = null) { $bits = array(); $element = $this->getElement(); $params = $this->getParams(); $size = (int) $element->width < 0 ? 1 : (int) $element->width; if (!isset($type)) { // Changes by JF Questiaux - info@betterliving.be switch ($params->get('password')) // Kept the name 'password' for backward compatibility { case '1' : $type = 'password'; break; case '2' : $type = 'tel'; break; case '3' : $type = 'email'; break; case '4' : $type = 'search'; break; case '5' : $type = 'url'; break; case '6' : $type = 'number'; break; default : $type = 'text'; } // End of changes } $maxLength = $params->get('maxlength'); if ($maxLength == '0' or $maxLength == '') { $maxLength = $size; } $class = array(); // Bootstrap 3 $class[] = 'form-control'; if ($this->elementError != '') { $class[] = ' elementErrorHighlight'; } if ($element->hidden == '1') { $class[] = ' hidden'; $type = 'hidden'; } $bits['type'] = $type; $bits['id'] = $this->getHTMLId($repeatCounter); $bits['name'] = $this->getHTMLName($repeatCounter); if (!$element->hidden) { $bits['size'] = $size; $bits['maxlength'] = $maxLength; } $class[] = 'fabrikinput inputbox'; $bits['class'] = implode(' ', $class); if ($params->get('placeholder', '') !== '') { $bits['placeholder'] = Text::_($params->get('placeholder')); } if ($params->get('autocomplete', 1) == 0) { $bits['autocomplete'] = 'off'; } // Cant be used with hidden element types if ($element->hidden != '1') { if ($params->get('readonly')) { $bits['readonly'] = "readonly"; $bits['class'] .= " readonly"; } if ($params->get('disable')) { $bits['class'] .= " disabled"; $bits['disabled'] = 'disabled'; } } return $bits; } /** * get the id used in the html element * * @param int $repeatCounter group counter * * @return string */ public function getHTMLId($repeatCounter = 0) { if (!is_array($this->HTMLids)) { $this->HTMLids = array(); } if (!array_key_exists((int) $repeatCounter, $this->HTMLids)) { $groupModel = $this->getGroup(); $listModel = $this->getListModel(); $table = $listModel->getTable(); $element = $this->getElement(); if ($groupModel->isJoin()) { $joinModel = $groupModel->getJoinModel(); $joinTable = $joinModel->getJoin(); $fullName = $joinTable->table_join . '___' . $element->name; } else { $fullName = $table->db_table_name . '___' . $element->name; } // Change the id for detailed view elements if ($this->inDetailedView) { $fullName .= '_ro'; } if ($groupModel->canRepeat()) { $fullName .= '_' . $repeatCounter; } $this->HTMLids[$repeatCounter] = $fullName; } return $this->HTMLids[$repeatCounter]; } /** * get the element html name * * @param int $repeatCounter group counter * * @return string */ public function getHTMLName($repeatCounter = 0) { $groupModel = $this->getGroup(); $table = $this->getListModel()->getTable(); $element = $this->getElement(); if ($groupModel->isJoin()) { $joinModel = $groupModel->getJoinModel(); $joinTable = $joinModel->getJoin(); $fullName = $joinTable->table_join . '___' . $element->name; } else { $fullName = $table->db_table_name . '___' . $element->name; } if ($groupModel->canRepeat()) { // $$$ rob - always use repeatCounter in html names - avoids ajax post issues with mootools1.1 $fullName .= '[' . $repeatCounter . ']'; } if ($this->hasSubElements) { $fullName .= '[]'; } // @TODO: check this - repeated elements do need to have something applied to their id based on their order in the repeated groups $this->elementHTMLName = $fullName; return $this->elementHTMLName; } /** * Load element params * * @return Registry default element params */ public function getParams() { if (!isset($this->params)) { $this->params = new Registry($this->getElement()->params); } return $this->params; } /** * Not used * * @deprecated * * @return mixed */ protected function loadPluginParams() { if (isset($this->xmlPath)) { $element = $this->getElement(); $pluginParams = new Registry($element->params); return $pluginParams; } return false; } /** * Loads in elements validation objects * * @deprecated use $this->validator->findAll() * * @return array validation objects */ public function getValidations() { return $this->validator->findAll(); } /** * get javascript actions * * @deprecated ? * * @return array js actions */ public function getJSActions() { if (!isset($this->jsActions)) { $query = $this->_db->getQuery(); $query->select('*')->from('#__fabrik_jsactions')->where('element_id = ' . (int) $this->id); $this->_db->setQuery($query); $this->jsActions = $this->_db->loadObjectList(); } return $this->jsActions; } /** *Create the js code to observe the elements js actions * * @param string $jsControllerKey Either form_ or _details * @param int $repeatCount Counter * * @return string js events */ public function getFormattedJSActions($jsControllerKey, $repeatCount) { $jsStr = ''; $allJsActions = $this->getFormModel()->getJsActions(); /** * hugh - only needed getParent when we weren't saving changes to parent params to child * which we should now be doing ... and getParent() causes an extra table lookup for every child * element on the form. * $element = $this->getParent(); */ $element = $this->getElement(); $w = new FabrikWorker; if (array_key_exists($element->id, $allJsActions)) { $elId = $this->getHTMLId($repeatCount); foreach ($allJsActions[$element->id] as $jsAct) { $js = $jsAct->code; $js = str_replace(array("\n", "\r"), "", $js); // Don't think we need to do this any more, although removing it will break bc /* if ($jsAct->action == 'load') { // JS code is already stored in the db as htmlspecialchars() 09/08/2013 $quote = '''; $js = preg_replace('#\bthis\b#', 'document.id(' . $quote . $elId . $quote . ')', $js); } */ if ($jsAct->action != '' && $js !== '') { $jsSlashes = addslashes($js); $jsStr .= $jsControllerKey . ".dispatchEvent('$element->plugin', '$elId', '$jsAct->action', '$jsSlashes');\n"; } else { // Build wysiwyg code if (isset($jsAct->js_e_event) && $jsAct->js_e_event != '') { // $$$ rob get the correct element id based on the repeat counter $triggerEl = $this->getFormModel()->getElement(str_replace('fabrik_trigger_element_', '', $jsAct->js_e_trigger)); $triggerid = is_object($triggerEl) ? 'element_' . $triggerEl->getHTMLId($repeatCount) : $jsAct->js_e_trigger; $key = $elId . serialize($jsAct); if (array_key_exists($key, self::$fxAdded)) { // Avoid duplicate events continue; } $jsStr .= $jsControllerKey . ".addElementFX('$triggerid', '$jsAct->js_e_event');\n"; self::$fxAdded[$key] = true; $f = InputFilter::getInstance(); $post = $f->clean($_POST, 'array'); $jsAct->js_e_value = $w->parseMessageForPlaceHolder(htmlspecialchars_decode($jsAct->js_e_value), $post); if ($jsAct->js_e_condition == 'hidden') { $js = "if (this.getContainer().getStyle('display') === 'none') {"; } elseif ($jsAct->js_e_condition == 'shown') { $js = "if (this.getContainer().getStyle('display') !== 'none') {"; } elseif ($jsAct->js_e_condition == 'CONTAINS') { $js = "if (this.get('value') !== null "; $js .= " && (Array.from(this.get('value')).contains('$jsAct->js_e_value')"; $js .= " || this.get('value').contains('$jsAct->js_e_value'))"; $js .= ") {"; } elseif ($jsAct->js_e_condition == '!CONTAINS') { $js = "if (this.get('value') === null "; $js .= " || (!Array.from(this.get('value')).contains('$jsAct->js_e_value')"; $js .= " || !this.get('value').contains('$jsAct->js_e_value'))"; $js .= ") {"; } // $$$ hugh if we always quote the js_e_value, numeric comparison doesn't work, as '100' < '3'. // So let's assume if they use <, <=, > or >= they mean numbers. elseif (in_array($jsAct->js_e_condition, array('<', '<=', '>', '>='))) { $js .= "if(this.get('value').toFloat() $jsAct->js_e_condition '$jsAct->js_e_value'.toFloat()) {"; } elseif ($jsAct->js_e_condition == 'regex') { if (preg_match('#^/.+/\w*#', $jsAct->js_e_value)) { $js .= "if (this.get('value').toString().test(%%REGEX%%)) {"; } else { $js .= "if (this.get('value').toString().test(/%%REGEX%%/)) {"; } } elseif ($jsAct->js_e_condition == '!regex') { if (preg_match('#^/.+/\w*#', $jsAct->js_e_value)) { $js .= "if (this.get('value').toString().test(%%REGEX%%)) {"; } else { $js .= "if (!this.get('value').toString().test(/%%REGEX%%/)) {"; } } else { $js = "if (this.get('value') $jsAct->js_e_condition '$jsAct->js_e_value') {"; } // Need to use corrected triggerid here as well if (preg_match('#^fabrik_trigger#', $triggerid)) { $js .= "Fabrik.getBlock('" . $jsControllerKey . "').doElementFX('" . $triggerid . "', '$jsAct->js_e_event', this)"; } else { $js .= "Fabrik.getBlock('" . $jsControllerKey . "').doElementFX('fabrik_trigger_" . $triggerid . "', '$jsAct->js_e_event', this)"; } $js .= "}"; $js = addslashes($js); $js = str_replace('%%REGEX%%', $jsAct->js_e_value, $js); $js = str_replace(array("\n", "\r"), "", $js); $jsStr .= $jsControllerKey . ".dispatchEvent('$element->plugin', '$elId', '$jsAct->action', '$js');\n"; } } } } return $jsStr; } /** * Get the default value for the list filter * * @param bool $normal is the filter a normal or advanced filter * @param int $counter filter order * * @return string */ protected function getDefaultFilterVal($normal = true, $counter = 0) { $input = $this->app->getInput(); // Used for update col list plugin - we don't want a default value filled if ($input->get('fabrikIngoreDefaultFilterVal', false)) { return ''; } $listModel = $this->getListModel(); $filters = $listModel->getFilterArray(); // $$$ rob test for db join fields //$elName = $this->getFilterFullName(); $elName = $this->getFullName(true, false); $elid = $this->getElement()->id; $f = InputFilter::getInstance(); $data = $f->clean($_REQUEST, 'array'); // See if the data is in the request array - can use tablename___elementname=filterval in query string $default = ''; if (array_key_exists($elName, $data)) { if (is_array($data[$elName])) { $default = @$data[$elName]['value']; } } $context = 'com_' . $this->package . '.list' . $listModel->getRenderContext() . '.filter.' . $elid; $context .= $normal ? '.normal' : '.advanced'; // We didn't find anything - lets check the filters if ($default == '') { if (empty($filters)) { return ''; } if (array_key_exists('elementid', $filters)) { /** * $$$ hugh - if we have one or more pre-filters on the same element that has a normal filter, * the following line doesn't work. So in 'normal' mode we need to get all the keys, * and find the 'normal' one. * $k = $normal == true ? array_search($elid, $filters['elementid']) : $counter; */ $k = false; if ($normal) { $keys = array_keys($filters['elementid'], $elid); foreach ($keys as $key) { /** * $$$ rob 05/09/2011 - just testing for 'normal' is not enough as there are several search_types * i.e. I've added a test for Querystring filters as without that the search values were * not being shown in ranged filter fields */ if (in_array($filters['search_type'][$key], array('normal', 'querystring', 'jpluginfilters'))) { $k = $key; continue; } } } else { $k = $counter; } // Is there a filter with this elements name if ($k !== false) { $searchType = FArrayHelper::getValue($filters['search_type'], $k); // Check element name is the same as the filter (could occur in advanced search when swapping element type) if ($searchType <> 'advanced' || $filters['key'][$k] === $input->getString('element')) { /** * if its a search all filter don't use its value. * if we did the next time the filter form is submitted its value is turned * from a search all filter into an element filter */ if (!is_null($searchType) && $searchType != 'searchall') { if ($searchType != 'prefilter') { $default = FArrayHelper::getValue($filters['origvalue'], $k); } } } } } } $default = $this->app->getUserStateFromRequest($context, $elid, $default); $fType = $this->getElement()->filter_type; if ($this->multiOptionFilter()) { $default = (is_array($default) && array_key_exists('value', $default)) ? $default['value'] : $default; if (is_array($default)) { // Hidden querystring filters can be using ranged valued though if (!in_array($fType, array('hidden', 'checkbox', 'multiselect', 'range'))) { // Weird thing on meow where when you first load the task list the id element had a date range filter applied to it???? $default = ''; } } else { $default = stripslashes($default); } } return $default; } /** * Is the element filter type a multi-select * * @return boolean */ protected function multiOptionFilter() { $fType = $this->getElement()->filter_type; return in_array($fType, array('range', 'checkbox', 'multiselect')); } /** * If the search value isn't what is stored in the database, but rather what the user * sees then switch from the search string to the db value here * overwritten in things like checkbox and radio plugins * * @param string $value FilterVal * * @return string */ protected function prepareFilterVal($value) { return $value; } /** * Get the filter name * * @param int $counter Filter order * @param bool $normal Do we render as a normal filter or as an advanced search filter * * @return string */ protected function filterName($counter = 0, $normal = true) { $listModel = $this->getListModel(); $v = 'fabrik___filter[list_' . $listModel->getRenderContext() . '][value]'; $v .= $normal ? '[' . $counter . ']' : '[]'; return $v; } /** * Get the list filter for the element * * @param int $counter Filter order * @param bool $normal Do we render as a normal filter or as an advanced search filter * if normal include the hidden fields as well (default true, use false for advanced filter * rendering) * * @return string Filter html */ public function getFilter($counter = 0, $normal = true, $container = '') { $listModel = $this->getListModel(); $formModel = $listModel->getFormModel(); $dbElName = $this->getFullName(false, false); if (!$formModel->hasElement($dbElName)) { return ''; } $element = $this->getElement(); $elName = $this->getFullName(true, false); $v = $this->filterName($counter, $normal); // Correct default got $default = $this->getDefaultFilterVal($normal, $counter); $this->filterDisplayValues = array($default); $return = array(); if (in_array($element->filter_type, array('range', 'dropdown', 'checkbox', 'multiselect'))) { $rows = $this->filterValueList($normal); $this->unmergeFilterSplits($rows); if (!in_array($element->filter_type, array('checkbox', 'multiselect'))) { array_unshift($rows, HTMLHelper::_('select.option', '', $this->filterSelectLabel())); } $this->getFilterDisplayValues($default, $rows); foreach ($rows as &$r) { // translate $r->text = Text::_($r->text); // decode first, to decode all hex entities (like ') $r->text = html_entity_decode($r->text, ENT_QUOTES | ENT_XML1, 'UTF-8'); // Encode if necessary if (!in_array($element->get('filter_type'), array('checkbox'))) { /** * Special case, pick out alt="foo" string if the text is an img tag in a dropdown */ $m = []; if ($element->get('filter_type') === 'dropdown' && preg_match('/<img\s+.*?alt="(.*?)".*>/', $r->text, $m)) { $r->text = $m[1]; } else { $r->text = strip_tags($r->text); } $r->text = htmlspecialchars($r->text, ENT_NOQUOTES, 'UTF-8', false); } } } switch ($element->filter_type) { case 'range': $this->rangedFilterFields($default, $return, $rows, $v, 'list'); break; case 'checkbox': $return[] = $this->checkboxFilter($rows, $default, $v); break; case 'dropdown': case 'multiselect': $return[] = $this->selectFilter($rows, $default, $v); break; case 'field': default: $return[] = $this->singleFilter($default, $v); break; case 'hidden': if (is_array($default)) { $this->rangedFilterFields($default, $return, null, $v, 'hidden'); } else { $return[] = $this->singleFilter($default, $v, 'hidden'); } break; case 'auto-complete': $autoComplete = $this->autoCompleteFilter($default, $v, null, $normal, $container); $return = array_merge($return, $autoComplete); break; } $return[] = $normal ? $this->getFilterHiddenFields($counter, $elName, false, $normal) : $this->getAdvancedFilterHiddenFields(); return implode("\n", $return); } /** * Build a select list filter * * @param $rows * @param $default * @param $v * * @return mixed */ protected function selectFilter($rows, $default, $v) { $class = $this->filterClass(); $element = $this->getElement(); $id = $this->getHTMLId() . 'value'; if ($element->filter_type === 'dropdown' || $element->filter_type === 'multiselect') { $advancedClass = $this->getAdvancedSelectClass(); $class .= !empty($advancedClass) ? ' ' . $advancedClass : ''; } $max = count($rows) < 7 ? count($rows) : 7; $size = $element->filter_type === 'multiselect' ? 'multiple="multiple" size="' . $max . '"' : 'size="1"'; $v = $element->filter_type === 'multiselect' ? $v . '[]' : $v; $data = 'data-filter-name="' . $this->getFullName(true, false) . '"'; return HTMLHelper::_('select.genericlist', $rows, $v, 'class="form-select-sm ' . $class . '" ' . $size . ' ' . $data, 'value', 'text', $default, $id); } /** * Get the labels for the filter values * * @param array|string $default * @param array $rows * * @return array */ protected function getFilterDisplayValues($default, $rows) { $default = (array) $default; $this->filterDisplayValues = array(); foreach ($rows as $row) { if (in_array($row->value, $default) && $row->value != '') { $this->filterDisplayValues[] = $row->text; } } return $this->filterDisplayValues; } /** * Get filter classes * * @since 3.1b * * @return string */ protected function filterClass() { $params = $this->getParams(); $classes = array('fabrik_filter'); $bootstrapClass = trim($this->getBsClass($params->get('filter_class', 'col-sm-4'))); $classes[] = $bootstrapClass; $classes[] = $params->get('filter_responsive_class', ''); return implode(' ', $classes); } /** * Checkbox filter * * @param array $rows Filter list options * @param array $default Selected filter values * @param string $v Filter name * * @since 3.0.7 * * @return string Checkbox filter LayoutInterface HTML */ protected function checkboxFilter($rows, $default, $v) { $values = array(); $labels = array(); foreach ($rows as $row) { $values[] = $row->value; $labels[] = $row->text; } $default = (array) $default; $layout = $this->getLayout('list-filter-checkbox'); $displayData = new stdClass; $displayData->values = $values; $displayData->labels = $labels; $displayData->default = $default; $displayData->elementName = $this->getFullName(true, false); $displayData->name = $v; $res = $layout->render($displayData); // If no custom list layout found revert to the default list.filter.fabrik-filter-checkbox renderer if ($res === '') { //$basePath = COM_FABRIK_FRONTEND . '/layouts/'; //$layout = new FileLayout('list.filter.fabrik-filter-checkbox', $basePath, array('debug' => false, 'component' => 'com_fabrik', 'client' => 'site')); //$layout = $this->getLayout('list.filter.fabrik-filter-checkbox'); $layout = $this->getListModel()->getLayout('list.filter.fabrik-filter-checkbox'); $res = $layout->render($displayData); } return $res; } /** * Build ranged filter fields either as two drop-downs or two hidden fields * * @param array $default Filter values * @param array &$return HTML to return * @param array $rows Filter list options * @param string $v Filter name * @param string $type Show ranged values as a list or hidden * * @since 3.0.7 * * @return void */ protected function rangedFilterFields($default, &$return, $rows, $v, $type = 'list') { $element = $this->getElement(); $class = $this->filterClass(); $attributes = 'class="' . $class . '" size="1" '; $attributes .= 'data-filter-name="' . $this->getFullName(true, false) . '"'; $default = (array) $default; if (count($default) === 1) { $default[1] = ''; } $def0 = array_key_exists('value', $default) ? $default['value'][0] : $default[0]; $def1 = array_key_exists('value', $default) ? $default['value'][1] : $default[1]; if ($type === 'list') { $return[] = '<span class="fabrikFilterRangeLabel">' . Text::_('COM_FABRIK_BETWEEN') . '</span>'; $return[] = HTMLHelper::_('select.genericlist', $rows, $v . '[0]', $attributes, 'value', 'text', $def0, $element->name . '_filter_range_0'); $return[] = '<br />'; $return[] = '<span class="fabrikFilterRangeLabel">' . Text::_('COM_FABRIK_AND') . '</span>'; $return[] = HTMLHelper::_('select.genericlist', $rows, $v . '[1]', $attributes, 'value', 'text', $def1, $element->name . '_filter_range_1'); } else { $return[] = '<input type="hidden" data-filter-name="' . $this->getFullName(true, false) . '" class="' . $class . '" name="' . $v . '[0]" value="' . $def0 . '" id="' . $element->name . '_filter_range_0" />'; $return[] = '<input type="hidden" data-filter-name="' . $this->getFullName(true, false) . '" class="' . $class . '" name="' . $v . '[1]" value="' . $def1 . '" id="' . $element->name . '_filter_range_1" />'; } } /** * Create a input type text/hidden filter * * @param string $default Value * @param string $v Filter name * @param string $type Type: 'text' or 'hidden' * * @return string filter */ protected function singleFilter($default, $v, $type = 'text') { // $$$ hugh - for "reasons", sometimes it's an array with one value. No clue why. Sod it. if (is_array($default)) { $default = array_shift($default); } // $$$ rob - if searching on "O'Fallon" from querystring filter the string has slashes added regardless $default = (string) $default; $default = stripslashes($default); $default = htmlspecialchars($default); $size = (int) $this->getParams()->get('filter_length', 20); $class = $this->filterClass(); $id = $this->getHTMLId() . 'value'; return '<input type="' . $type . '" data-filter-name="' . $this->getFullName(true, false) . '" name="' . $v . '" class="' . $class . '" size="' . $size . '" value="' . $default . '" id="' . $id . '" />'; } /** * Build the HTML ////for the auto-complete filter * * @param string $default Label * @param string $v Field name * @param string $labelValue Label value * @param bool $normal Do we render as a normal filter or as an advanced search filter * if normal include the hidden fields as well (default true, use false for advanced * filter rendering) * * @return array HTML bits */ protected function autoCompleteFilter($default, $v, $labelValue = null, $normal = true, $container = null) { if (is_null($labelValue)) { $labelValue = $default; } $listModel = $this->getListModel(); $default = stripslashes($default); $default = htmlspecialchars($default); $id = $this->getHTMLId() . 'value'; $class = $this->filterClass(); $size = (int) $this->getParams()->get('filter_length', 20); /** * $$$ rob 28/10/2011 using selector rather than element id so we can have n modules with the same filters * showing and not produce invalid html & duplicate js calls */ $return = array(); $return[] = '<input type="hidden" " data-filter-name="' . $this->getFullName(true, false) . '" name="' . $v . '" class="' . $class . ' ' . $id . '" value="' . $default . '" />'; $return[] = '<input type="text" name="auto-complete' . $this->getElement()->id . '" class="' . $class . ' autocomplete-trigger ' . $id . '-auto-complete" size="' . $size . '" value="' . $labelValue . '" />'; $opts = array(); if ($normal) { $opts['menuclass'] = 'auto-complete-container'; if (empty($container)) { $container = 'listform_' . $listModel->getRenderContext(); } $selector = '#' . $container . ' .' . $id; } else { $selector = '.advancedSearch_' . $listModel->getRenderContext() . ' .' . $id; $opts['menuclass'] = 'auto-complete-container advanced'; } $element = $this->getElement(); $formId = $this->getFormModel()->getId(); FabrikHelperHTML::autoComplete($selector, $element->id, $formId, $element->plugin, $opts); return $return; } /** * Get drop-down filter select label * * @return string */ protected function filterSelectLabel() { $params = $this->getParams(); return $params->get('filter_required') == 1 ? Text::_('COM_FABRIK_PLEASE_SELECT') : Text::_('COM_FABRIK_FILTER_PLEASE_SELECT'); } /** * Checks if filter option values are in json format * if so explode those values into new options * * @param array &$rows Filter options * * @return null */ protected function unmergeFilterSplits(&$rows) { /* * takes rows which may be in format : * * [0] => stdClass Object ( [text] => ["1"] [value] => ["1"] ) and converts them into [0] => CMSObject Object ( [_errors:protected] => Array ( ) [value] => 1 [text] => 1 [disable] => ) */ $allValues = array(); foreach ($rows as $row) { $allValues[] = $row->value; } $c = count($rows) - 1; for ($j = $c; $j >= 0; $j--) { $vals = FabrikWorker::JSONtoData($rows[$j]->value, true); $txt = FabrikWorker::JSONtoData($rows[$j]->text, true); if (is_array($vals)) { for ($i = 0; $i < count($vals); $i++) { $vals2 = FabrikWorker::JSONtoData($vals[$i], true); $txt2 = FabrikWorker::JSONtoData(FArrayHelper::getValue($txt, $i), true); for ($jj = 0; $jj < count($vals2); $jj++) { if (!in_array($vals2[$jj], $allValues)) { $allValues[] = $vals2[$jj]; $rows[] = HTMLHelper::_('select.option', $vals2[$jj], $txt2[$jj]); } } } if (FabrikWorker::isJSON($rows[$j]->value, false)) { // $$$ rob 01/10/2012 - if not unset then you could get json values in standard dd filter (checkbox) unset($rows[$j]); } } if (count($vals) > 1) { unset($rows[$j]); } } } /** * Run after unmergeFilterSplits to ensure filter dropdown labels are correct * * @param array &$rows filter options * * @return null */ protected function reapplyFilterLabels(&$rows) { $values = $this->getSubOptionValues(); $labels = $this->getSubOptionLabels(); foreach ($rows as &$row) { $k = array_search($row->value, $values); if ($k !== false) { $row->text = $labels[$k]; } } $rows = array_values($rows); } /** * Get sub option values * * @param array $data Form data. If submitting a form, we want to use that form's data and not * re-query the form Model for its data as with multiple plugins of the same type * this was getting the plugin params out of sync. * * @return array */ protected function getSubOptionValues($data = array()) { $phpOpts = $this->getPhpOptions($data); if (!$phpOpts) { // cache, as even just fetching the params can eat up time in list with lots of rows if (!isset($this->subOptionValues)) { $params = $this->getParams(); $opts = $params->get('sub_options', ''); $opts = $opts == '' ? array() : (array) @$opts->sub_values; $this->subOptionValues = $opts; } return $this->subOptionValues; } else { /** * Paul - According to tooltip, $phpOpts should be of form "array(JHTML: :_('select.option', '1', 'one'))" * This is an array of objects with properties text and value. * If user has mis-specified this we should tell them. * * @FIXME - $$$ hugh - seems like an empty array should be valid as well? **/ if (!is_array($phpOpts) || !$phpOpts[0] || !is_object($phpOpts[0]) || !isset($phpOpts[0]->value) || !isset($phpOpts[0]->text)) { FabrikWorker::logError(sprintf(Text::_('COM_FABRIK_ELEMENT_SUBOPTION_ERROR'), $this->element->name, var_export($phpOpts, true)), 'error'); return array(); } $opts = array(); foreach ($phpOpts as $phpOpt) { $opts[] = $phpOpt->value; } } return $opts; } /** * Get sub option labels * * @param array $data Form data. If submitting a form, we want to use that form's data and not * re-query the form Model for its data as with multiple plugins of the same type * this was getting the plugin params out of sync. * * @return array */ protected function getSubOptionLabels($data = array()) { $phpOpts = $this->getPhpOptions($data); if (!$phpOpts) { // cache, as running Text::() can eat up time with lots of options if (!isset($this->subOptionLabels)) { $params = $this->getParams(); $opts = $params->get('sub_options', ''); $opts = $opts == '' ? array() : (array) @$opts->sub_labels; foreach ($opts as &$opt) { $opt = Text::_($opt); } $this->subOptionLabels = $opts; } return $this->subOptionLabels; } else { /** * Paul - According to tooltip, $phpOpts should be of form "array(HTMLHelper::_('select.option', '1', 'one'))" * This is an array of objects with properties text and value. * If user has mis-specified this we should tell them. * * @FIXME - $$$ hugh - seems like an empty array should be valid as well? **/ if (!is_array($phpOpts) || !$phpOpts[0] || !is_object($phpOpts[0]) || !isset($phpOpts[0]->value) || !isset($phpOpts[0]->text)) { FabrikWorker::logError(sprintf(Text::_('COM_FABRIK_ELEMENT_SUBOPTION_ERROR'), $this->element->name, var_export($phpOpts, true)), 'error'); return array(); } $opts = array(); foreach ($phpOpts as $phpOpt) { $opts[] = $phpOpt->text; } } foreach ($opts as &$opt) { $opt = Text::_($opt); } return $opts; } /** * Get sub option enabled/disabled state * * @return array */ protected function getSubOptionEnDis() { $opts = array(); $phpOpts = $this->getPhpOptions(); if ($phpOpts) { foreach ($phpOpts as $phpOpt) { $opts[] = isset($phpOpt->disable) ? $phpOpt->disable : false; } } return $opts; } /** * Should we get the elements sub options via the use of eval'd parameter setting * * @param array $data Form data. If submitting a form, we want to use that form's data and not * re-query the form Model for its data as with multiple plugins of the same type * this was getting the plugin params out of sync. * * @since 3.0.7 * * @return mixed false if no, otherwise needs to return array of HTMLHelperoptions */ protected function getPhpOptions($data = array()) { $params = $this->getParams(); $pop = $params->get('dropdown_populate', ''); if ($pop !== '') { $w = new FabrikWorker; //$data = empty($data) ? $this->getFormModel()->getData() : $data; $pop = $w->parseMessageForPlaceHolder($pop, $data, false); $key = md5($pop) . '-' . md5(serialize($data)); if (isset($this->phpOptions[$key])) { return $this->phpOptions[$key]; } FabrikWorker::clearEval(); $res = Php::Eval(['code' => $pop, 'vars' => ["data" => $data]]); FabrikWorker::logEval($res, 'Eval exception : ' . $this->element->name . '::getPhpOptions() : ' . $pop . ' : %s'); $this->phpOptions[$key] = $res; return $res; } return false; } /** * Get the radio buttons possible values * needed for inline edit list plugin * * @return array of radio button values */ public function getOptionValues() { return $this->getSubOptionValues(); } /** * get the radio buttons possible labels * needed for inline edit list plugin * * @return array of radio button labels */ protected function getOptionLabels() { return $this->getSubOptionLabels(); } /** * Get the filter build method - all (2) or recorded data (1) * * @since 3.0.7 * * @return int */ protected function getFilterBuildMethod() { $usersConfig = ComponentHelper::getParams('com_fabrik'); $params = $this->getParams(); $filterBuild = $params->get('filter_build_method', 0); if ($filterBuild == 0) { $filterBuild = $usersConfig->get('filter_build_method'); } return $filterBuild; } /** * Used by radio and drop-down elements to get a drop-down list of their unique * unique values OR all options - based on filter_build_method * * @param bool $normal do we render as a normal filter or as an advanced search filter * @param string $tableName table name to use - defaults to element's current table * @param string $label field to use, defaults to element name * @param string $id field to use, defaults to element name * @param bool $incJoin include join * * @return array text/value objects */ public function filterValueList($normal, $tableName = '', $label = '', $id = '', $incJoin = true) { $filterBuild = $this->getFilterBuildMethod(); if ($filterBuild == 2 && $this->hasSubElements) { return $this->filterValueList_All($normal, $tableName, $label, $id, $incJoin); } else { return $this->filterValueList_Exact($normal, $tableName, $label, $id, $incJoin); } } /** * If filterValueList_Exact incjoin value = false, then this method is called * to ensure that the query produced in filterValueList_Exact contains at least the database join element's * join * * @return string required join text to ensure exact filter list code produces a valid query. */ protected function buildFilterJoin() { return ''; } /** * Create an array of label/values which will be used to populate the elements filter dropdown * returns only data found in the table you are filtering on * * @param bool $normal do we render as a normal filter or as an advanced search filter * @param string $tableName table name to use - defaults to element's current table * @param string $label field to use, defaults to element name * @param string $id field to use, defaults to element name * @param bool $incJoin include join * * @throws ErrorException * * @return array filter value and labels */ protected function filterValueList_Exact($normal, $tableName = '', $label = '', $id = '', $incJoin = true) { $listModel = $this->getListModel(); $fbConfig = ComponentHelper::getParams('com_fabrik'); $fabrikDb = $listModel->getDb(); $table = $listModel->getTable(); $element = $this->getElement(); $origTable = $table->db_table_name; $elName = $this->getFullName(true, false); $elName2 = $this->getFullName(false, false); if (!$this->isJoin()) { $ids = $listModel->getColumnData($elName2); // For ids that are text with apostrophes in for ($x = count($ids) - 1; $x >= 0; $x--) { if ($ids[$x] == '') { unset($ids[$x]); } else { $ids[$x] = addslashes($ids[$x]); } } } $incJoin = $this->isJoin() ? false : $incJoin; /** * filter the drop downs lists if the table_view_own_details option is on * other wise the lists contain data the user should not be able to see * note, this should now use the prefilter data to filter the list */ // Check if the elements group id is on of the table join groups if it is then we swap over the table name $fromTable = $this->isJoin() ? $this->getJoinModel()->getJoin()->table_join : $origTable; $joinStr = $incJoin ? $listModel->buildQueryJoin() : $this->buildFilterJoin(); // New option not to order elements - required if you want to use db joins 'Joins where and/or order by statement' $groupBy = $this->getOrderBy('filter'); foreach ($listModel->getJoins() as $aJoin) { // Not sure why the group id key wasn't found - but put here to remove error if (property_exists($aJoin, 'group_id')) { if ($aJoin->group_id == $element->group_id && $aJoin->element_id == 0) { $fromTable = $aJoin->table_join; $elName = preg_replace('/^' . $origTable . '\./', $fromTable . '.', $elName2); } } } $elName = FabrikString::safeColName($elName); if ($label == '') { $label = $this->isJoin() ? $this->getElement()->name : $elName; } if ($id == '') { $id = $this->isJoin() ? 'id' : $elName; } if ($this->encryptMe()) { $secret = $this->config->getValue('secret'); $label = 'AES_DECRYPT(' . $label . ', ' . $fabrikDb->quote($secret) . ')'; $id = 'AES_DECRYPT(' . $id . ', ' . $fabrikDb->quote($secret) . ')'; } $origTable = $tableName == '' ? $origTable : $tableName; /** * $$$ rob - 2nd sql was blowing up for me on my test table - why did we change to it? * http://localhost/fabrik2.0.x/index.php?option=com_fabrik&view=table&listid=12&calculations=0&resetfilters=0&Itemid=255&lang=en * so added test for initial fromtable in join str and if found use origtable */ if (strstr($joinStr, 'JOIN ' . $fabrikDb->qn($fromTable))) { $sql = 'SELECT DISTINCT(' . $label . ') AS ' . $fabrikDb->qn('text') . ', ' . $id . ' AS ' . $fabrikDb->qn('value') . ' FROM ' . $fabrikDb->qn($origTable) . ' ' . $joinStr . "\n"; } else { $sql = 'SELECT DISTINCT(' . $label . ') AS ' . $fabrikDb->qn('text') . ', ' . $id . ' AS ' . $fabrikDb->qn('value') . ' FROM ' . $fabrikDb->qn($fromTable) . ' ' . $joinStr . "\n"; } if (!$this->isJoin()) { $sql .= 'WHERE ' . $id . ' IN (\'' . implode("','", $ids) . '\')'; } // Apply element where/order by statements to the filter (e.g. dbjoins 'Joins where and/or order by statement') $elementWhere = $this->buildQueryWhere(array(), true, null, array('mode' => 'filter')); if (StringHelper::stristr($sql, 'WHERE ') && StringHelper::stristr($elementWhere??'', 'WHERE ')) { // $$$ hugh - only replace the WHERE with AND if it's the first word, so we don't munge sub-queries // $elementWhere = StringHelper::str_ireplace('WHERE ', 'AND ', $elementWhere); $elementWhere = preg_replace("#^(\s*)(WHERE)(.*)#i", "$1AND$3", $elementWhere); } else if (StringHelper::stristr($sql, 'WHERE ') && !empty($elementWhere) && !StringHelper::stristr($elementWhere, 'WHERE ')) { // if we have a WHERE in the main query, and the element clause isn't empty but doesn't start with WHERE ... $elementWhere = 'AND ' . $elementWhere; } $sql .= ' ' . $elementWhere; $sql .= "\n" . $groupBy; $sql = $listModel->pluginQuery($sql); $fabrikDb->setQuery($sql, 0, $fbConfig->get('filter_list_max', 100)); FabrikHelperHTML::debug((string) $fabrikDb->getQuery(), 'element filterValueList_Exact:'); try { $rows = $fabrikDb->loadObjectList(); } catch (RuntimeException $e) { throw new ErrorException('filter query error: ' . $this->getElement()->name . ' ' . $fabrikDb->getErrorMsg(), 500); } return $rows; } /** * Get a readonly value for a filter, uses getROElement() to ascertain value, adds between x & y if ranged values * * @param mixed $data String or array of filter value(s) * * @since 3.0.7 * * @return string */ public function getFilterRO($data) { if (in_array($this->getFilterType(), array('range', 'range-hidden'))) { $return = array(); foreach ($data as $d) { $return[] = $this->getROElement($d); } return Text::_('COM_FABRIK_BETWEEN') . '<br />' . implode('<br />' . Text::_('COM_FABRIK_AND') . "<br />", $return); } return $this->getROElement($data); } /** * Get options order by * * @param string $view View mode '' or 'filter' * @param JDatabaseQuery|bool $query Set to false to return a string * * @return string order by statement */ protected function getOrderBy($view = '', $query = false) { if (isset($this->orderBy)) { return $this->orderBy; } else { return "ORDER BY text ASC "; } } /** * Create an array of label/values which will be used to populate the elements filter dropdown * returns all possible options * * @param bool $normal do we render as a normal filter or as an advanced search filter * @param string $tableName table name to use - defaults to element's current table * @param string $label field to use, defaults to element name * @param string $id field to use, defaults to element name * @param bool $incJoin include join * * @return array filter value and labels */ protected function filterValueList_All($normal, $tableName = '', $label = '', $id = '', $incJoin = true) { $values = $this->getSubOptionValues(); $labels = $this->getSubOptionLabels(); $return = array(); for ($i = 0; $i < count($values); $i++) { $return[] = HTMLHelper::_('select.option', $values[$i], $labels[$i]); } return $return; } /** * Get the hidden fields for a normal filter * * @param int $counter Filter counter * @param string $elName Full element name will be converted to tablename.elementname format * @param bool $hidden Has the filter been added due to a search form value with no corresponding filter set * up in the table if it has we need to know so that when we do a search from a * 'fabrik_list_filter_all' field that search term takes precedence * @param bool $normal do we render as a normal filter or as an advanced search filter * * * @return string html Hidden fields */ protected function getFilterHiddenFields($counter, $elName, $hidden = false, $normal = true) { $params = $this->getParams(); $element = $this->getElement(); $class = $this->filterClass(); $filters = $this->getListModel()->getFilterArray(); // $$$ needs to apply to CDD's as well, so just making this an override-able method. if ($this->quoteLabel()) { $elName = FabrikString::safeColName($elName); } // If querying via the querystring - then the condition and eval should be looked up against that key $elementIds = FArrayHelper::getValue($filters, 'elementid', array()); // Check that there is an element filter for this element in the element ids. $filterIndex = array_search($this->getId(), $elementIds); $hidden = $hidden ? 1 : 0; $match = $this->isExactMatch(array('match' => $element->filter_exact_match)); $return = array(); $eval = FArrayHelper::getValue($filters, 'eval', array()); $joins = FArrayHelper::getValue($filters, 'join', array()); $condition = FArrayHelper::getValue($filters, 'condition', array()); $groupedTo = FArrayHelper::getValue($filters, 'grouped_to_previous', array()); /* * Element filter not found (could be a prefilter instead) so use element default options * see http://fabrikar.com/forums/index.php?threads/major-filter-issues.37360/ */ if ($filterIndex === false || $normal) { $condition = $this->getFilterCondition(); $eval = FABRIKFILTER_TEXT; } else { $condition = FArrayHelper::getValue($condition, $filterIndex, $this->getFilterCondition()); $eval = FArrayHelper::getValue($eval, $filterIndex, FABRIKFILTER_TEXT); } $searchTypes = FArrayHelper::getValue($filters, 'search_type', array()); $searchType = FArrayHelper::getValue($searchTypes, $filterIndex, 'normal'); // If our previous filter was a pre-filter we never want to use its join value as it could result in the pre-filter being ignored // Thus in this instance we should always use 'AND' $join = $searchType === 'prefilter' ? 'AND' : FArrayHelper::getValue($joins, $filterIndex, 'AND'); $groupedToPrevious = FArrayHelper::getValue($groupedTo, $filterIndex, '0'); // Need to include class other wise csv export produces incorrect results when exporting $prefix = '<input type="hidden" class="' . $class . '" name="fabrik___filter[list_' . $this->getListModel()->getRenderContext() . ']'; $return[] = $prefix . '[condition][' . $counter . ']" value="' . $condition . '" />'; $return[] = $prefix . '[join][' . $counter . ']" value="' . $join . '" />'; $return[] = $prefix . '[key][' . $counter . ']" value="' . $elName . '" />'; $return[] = $prefix . '[search_type][' . $counter . ']" value="normal" />'; $return[] = $prefix . '[match][' . $counter . ']" value="' . $match . '" />'; $return[] = $prefix . '[full_words_only][' . $counter . ']" value="' . $params->get('full_words_only', '0') . '" />'; $return[] = $prefix . '[eval][' . $counter . ']" value="' . $eval . '" />'; $return[] = $prefix . '[grouped_to_previous][' . $counter . ']" value="' . $groupedToPrevious . '" />'; $return[] = $prefix . '[hidden][' . $counter . ']" value="' . $hidden . '" />'; $return[] = $prefix . '[elementid][' . $counter . ']" value="' . $element->id . '" />'; return implode("\n", $return); } /** * Get the condition statement to use in the filters hidden field * * @return string =, begins or contains */ protected function getFilterCondition() { if ($this->getElement()->filter_type == 'auto-complete') { $cond = 'contains'; } else { $match = $this->isExactMatch(array('match' => $this->getElement()->filter_exact_match)); $cond = ($match == 1) ? '=' : 'contains'; } return $cond; } /** * Get the filter type: the element filter_type property unless a ranged querystring is used * * @since 3.0.7 * * @return string */ protected function getFilterType() { $element = $this->getElement(); $type = $element->filter_type; $name = $this->getFullName(true, false); $qsFilter = $this->app->getInput()->get($name, array(), 'array'); $qsValues = FArrayHelper::getValue($qsFilter, 'value', array()); if (is_array($qsValues) && count($qsValues) > 1) { $type = $type === 'hidden' ? 'range-hidden' : 'range'; } return $type; } /** * Get the hidden fields for an advanced filter * * @return string html hidden fields */ protected function getAdvancedFilterHiddenFields() { $element = $this->getElement(); $return = array(); $prefix = '<input type="hidden" name="fabrik___filter[list_' . $this->getListModel()->getRenderContext() . ']'; $return[] = $prefix . '[elementid][]" value="' . $element->id . '" />'; return implode("\n", $return); } /** * This builds an array containing the filters value and condition * when using a ranged search * * @param array $value Initial values * @param string $condition Filter condition e.g. BETWEEN * * @return array (value condition) */ protected function getRangedFilterValue($value, $condition = '') { $db = FabrikWorker::getDbo(); $element = $this->getElement(); if ($element->filter_type === 'range' || strtoupper($condition) === 'BETWEEN') { if (is_numeric($value[0]) && is_numeric($value[1])) { $value = $value[0] . ' AND ' . $value[1]; } else { $value = $db->q($value[0]) . ' AND ' . $db->q($value[1]); } $condition = 'BETWEEN'; } else { if (is_array($value) && !empty($value)) { foreach ($value as &$v) { $v = $db->q($v); } $value = ' (' . implode(',', $value) . ')'; } $condition = 'IN'; } return array($value, $condition); } /** * Escapes a SINGLE query search string * * @param string $condition filter condition * @param value &$value value to escape * * @since 3.0.7 * * @return null */ private function escapeOneQueryValue($condition, &$value) { if ($condition == 'REGEXP') { $value = preg_quote($value); } /** * If doing a search via a querystring for O'Fallon then the ' is backslashed * in FabrikModelListfilter::getQuerystringFilters() * but the MySQL regexp needs it to be back-quoted three times */ // If searching on '\' then don't double up \'s if (strlen(str_replace('\\', '', $value)) > 0) { $value = str_replace("\\", "\\\\\\", $value); // $$$rob check things haven't been double quoted twice (occurs now that we are doing preg_quote() above to fix searches on '*' $value = str_replace("\\\\\\\\\\\\", "\\\\\\", $value); } } /** * Escapes the a query search string * * @param string $condition filter condition * @param value &$value value to escape * * @return null */ private function escapeQueryValue($condition, &$value) { // $$$ rob 30/06/2011 only escape once ! if ($this->escapedQueryValue) { return; } $this->escapedQueryValue = true; if (is_array($value)) { foreach ($value as &$val) { $this->escapeOneQueryValue($condition, $val); } } else { $this->escapeOneQueryValue($condition, $value); } } /** * Builds an array containing the filters value and condition * * @param string $value Initial value * @param string $condition Initial condition e.g. LIKE, = * @param string $eval How the value should be handled * * @return array (value condition) */ public function getFilterValue($value, $condition, $eval) { $condition = StringHelper::strtolower($condition); $this->escapeQueryValue($condition, $value); $db = FabrikWorker::getDbo(); if (is_array($value)) { // Ranged search list($value, $condition) = $this->getRangedFilterValue($value, $condition); } else { switch ($condition) { case 'notequals': case '<>': $condition = "<>"; // 2 = sub-query so don't quote $value = ($eval == FABRIKFILTER_QUERY) ? '(' . $value . ')' : $db->q($value); break; case 'equals': case '=': $condition = "="; $value = ($eval == FABRIKFILTER_QUERY) ? '(' . $value . ')' : $db->q($value); break; case 'begins': case 'begins with': $condition = "LIKE"; $value = $eval == FABRIKFILTER_QUERY ? '(' . $value . ')' : $db->q($value . '%'); break; case 'ends': case 'ends with': // @TODO test this with subquery $condition = "LIKE"; $value = $eval == FABRIKFILTER_QUERY ? '(' . $value . ')' : $db->q('%' . $value); break; case 'contains': case 'like': // @TODO test this with subquery $condition = "LIKE"; //$value = $eval == FABRIKFILTER_QUERY ? '(' . $value . ')' : $db->q('%' . $value . '%'); // if they want NOQUOTES on a LIKE, assume they are building their own CONCAT or whatever with %'s switch ($eval) { case FABRIKFILTER_QUERY: $value = '(' . $value . ')'; break; case FABRIKFILTER_NOQUOTES: $value = $value; break; default: $value = $db->q('%' . $value . '%'); break; } break; case '>': case '>': case 'greaterthan': $condition = '>'; break; case '<': case '<': case 'lessthan': $condition = '<'; break; case '>=': case '>=': case 'greaterthanequals': $condition = '>='; break; case '<=': case '<=': case 'lessthanequals': $condition = '<='; break; case 'in': $condition = 'IN'; if ($eval != FABRIKFILTER_QUERY) { $value = FabrikString::safeQuote($value, true); } $value = '(' . $value . ')'; break; case 'not_in': $condition = 'NOT IN'; if ($eval != FABRIKFILTER_QUERY) { $value = FabrikString::safeQuote($value, true); } $value = '(' . $value . ')'; break; } switch ($condition) { case '>': case '<': case '>=': case '<=': if ($eval == FABRIKFILTER_QUERY) { $value = '(' . $value . ')'; } else { if (!is_numeric($value)) { $value = $db->q($value); } } break; } // $$$ hugh - if 'noquotes' (3) selected, strip off the quotes again! if ($eval == FABRIKFILTER_NOQUOTES) { // $$$ hugh - darn, this is stripping the ' of the end of things like "select & from foo where bar = '123'" $value = StringHelper::ltrim($value, "'"); $value = StringHelper::rtrim($value, "'"); } if ($condition == '=' && $value == "'_null_'") { $condition = " IS NULL "; $value = ''; } } return array($value, $condition); } /** * Build the filter query for the given element. * Can be overwritten in plugin - e.g. see checkbox element which checks for partial matches * * @param string $key element name in format `tablename`.`elementname` * @param string $condition =/like etc. * @param string $value search string - already quoted if specified in filter array options * @param string $originalValue original filter value without quotes or %'s applied * @param string $type filter type advanced/normal/prefilter/search/querystring/sea* @ * @param string $filterEval eval the filter value * @return string sql query part e,g, "key = value" */ public function getFilterQuery($key, $condition, $value, $originalValue, $type = 'normal', $filterEval = '0') { $this->encryptFieldName($key); switch ($condition) { case 'thisyear': $query = ' YEAR(' . $key . ') = YEAR(NOW()) '; break; case 'earlierthisyear': $query = ' (DAYOFYEAR(' . $key . ') <= DAYOFYEAR(NOW()) AND YEAR(' . $key . ') = YEAR(NOW())) '; break; case 'laterthisyear': $query = ' (DAYOFYEAR(' . $key . ') >= DAYOFYEAR(NOW()) AND YEAR(' . $key . ') = YEAR(NOW())) '; break; case 'today': $query = ' (' . $key . ' >= CURDATE() AND ' . $key . ' < CURDATE() + INTERVAL 1 DAY) '; break; case 'yesterday': $query = ' (' . $key . ' >= CURDATE() - INTERVAL 1 DAY AND ' . $key . ' < CURDATE()) '; break; case 'tomorrow': $query = ' (' . $key . ' >= CURDATE() + INTERVAL 1 DAY AND ' . $key . ' < CURDATE() + INTERVAL 2 DAY ) '; break; case 'thismonth': $query = ' (' . $key . ' >= DATE_ADD(LAST_DAY(DATE_SUB(now(), INTERVAL 1 MONTH)), INTERVAL 1 DAY) AND ' . $key . ' <= LAST_DAY(NOW()) ) '; break; case 'lastmonth': $query = ' (' . $key . ' >= DATE_ADD(LAST_DAY(DATE_SUB(now(), INTERVAL 2 MONTH)), INTERVAL 1 DAY) AND ' . $key . ' <= LAST_DAY(DATE_SUB(NOW(), INTERVAL 1 MONTH)) ) '; break; case 'nextmonth': $query = ' (' . $key . ' >= DATE_ADD(LAST_DAY(now()), INTERVAL 1 DAY) AND ' . $key . ' <= DATE_ADD(LAST_DAY(NOW()), INTERVAL 1 MONTH) ) '; break; case 'nextweek1': $query = ' (YEARWEEK(' . $key . ',1) = YEARWEEK(DATE_ADD(NOW(), INTERVAL 1 WEEK), 1))'; break; case 'birthday': $query = '(MONTH(' . $key . ') = MONTH(CURDATE()) AND DAY(' . $key . ') = DAY(CURDATE())) '; break; default: if ($this->isJoin()) { // Query the joined table concatenating into one field $joinTable = $this->getJoinModel()->getJoin()->table_join; // Jaanus: joined group pk set in groupConcactJoinKey() $pk = $this->groupConcactJoinKey(); $key = "(SELECT GROUP_CONCAT(id SEPARATOR '" . GROUPSPLITTER . "') FROM $joinTable WHERE parent_id = $pk)"; $value = str_replace("'", '', $value); $query = "($key = '$value' OR $key LIKE '$value" . GROUPSPLITTER . "%' OR $key LIKE '" . GROUPSPLITTER . "$value" . GROUPSPLITTER . "%' OR $key LIKE '%" . GROUPSPLITTER . "$value')"; } else { $query = " $key $condition $value "; } break; } return $query; } /** * Get the AES decrypt sql segment for the element * * @param string &$key field name * * @return void */ public function encryptFieldName(&$key) { if ($this->encryptMe()) { $db = FabrikWorker::getDbo(); $secret = $this->config->get('secret'); $matches = array(); if (preg_match('/LOWER\((.*)\)/', $key, $matches)) { $key = 'LOWER(CONVERT(AES_DECRYPT(' . $matches[1] . ', ' . $db->q($secret) . ') USING utf8))'; } else { $key = 'AES_DECRYPT(' . $key . ', ' . $db->q($secret) . ')'; } } } /** * If no filter condition supplied (either via querystring or in posted filter data * return the most appropriate filter option for the element. * * @return string default filter condition ('=', 'REGEXP' etc.) */ public function getDefaultFilterCondition() { $fieldDesc = $this->getFieldDescription(); if (StringHelper::stristr($fieldDesc, 'INT') || $this->getElement()->filter_exact_match == 1) { return '='; } return 'REGEXP'; } /** * $$$ rob testing not using this as elements can only be in one group * $$$ hugh - still called from import.php * when adding a new element this will ensure its added to all tables that the * elements group is associated with * * @param string $origColName original column name leave null to ignore * * @TODO Fabrik 3 - loadFromFormId() might need to pass in a package id * * @deprecated * * @return null */ public function addToDBTable($origColName = null) { } /** * called from admin element controller when element saved * * @param array $data posted element save data * * @return bool save ok or not */ public function onSave($data) { $params = $this->getParams(); if (!$this->canEncrypt() && $params->get('encrypt')) { throw new RuntimeException('The encryption option is only available for field and text area plugins'); } // Overridden in element plugin if needed return true; } /** * Called from admin element controller when element is removed * * @return bool save ok or not */ public function onRemove() { // Delete js actions $db = FabrikWorker::getDbo(true); $query = $db->getQuery(true); $id = (int) $this->getElement()->id; $query->delete()->from('#__fabrik_jsactions')->where('element_id =' . $id); $db->setQuery($query); try { $db->execute(); } catch (Exception $e) { throw new RuntimeException('Didn\'t delete js actions for element ' . $id); } return true; } /** * States if the element contains data which is recorded in the database * some elements (e.g. buttons) don't * * @param array $data posted data * * @return bool */ public function recordInDatabase($data = null) { return $this->getParams()->get('store_in_db', $this->recordInDatabase); } /** * Used by elements with sub-options, given a value, return its label * * @param string $v Value * @param string $defaultLabel Default label * @param bool $forceCheck Force check even if $v === $defaultLabel * * @return string Label */ public function getLabelForValue($v, $defaultLabel = null, $forceCheck = false) { /** * $$$ hugh - only needed getParent when we weren't saving changes to parent params to child * which we should now be doing ... and getParent() causes an extra table lookup for every child * element on the form. * $element = $this->getParent(); */ $params = $this->getParams(); $values = $this->getSubOptionValues(); $labels = $this->getSubOptionLabels(); //F4: Don't use strict search, in F4 values may be integer or string $key = array_search($v, $values, false); /** * $$$ rob if we allow adding to the dropdown but not recording * then there will be no $key set to revert to the $val instead */ if ($v === $params->get('sub_default_value')) { $v = $params->get('sub_default_label'); } return ($key === false) ? $v : FArrayHelper::getValue($labels, $key, $defaultLabel); } /** * Build the query for the avg calculation * * @param FabrikFEModelList &$listModel list model * @param array $labels Labels * * @return string sql statement */ protected function getAvgQuery(&$listModel, $labels = array()) { $label = count($labels) == 0 ? "'calc' AS label" : 'CONCAT(' . implode(', " & " , ', $labels) . ') AS label'; $item = $listModel->getTable(); $joinSQL = $listModel->buildQueryJoin(); $whereSQL = $listModel->buildQueryWhere(); $name = $this->getFullName(false, false); $groupModel = $this->getGroup(); $roundTo = (int) $this->getParams()->get('avg_round'); if ($groupModel->isJoin()) { // Element is in a joined column - lets presume the user wants to sum all cols, rather than reducing down to the main cols totals $sql = "SELECT ROUND(AVG($name), $roundTo) AS value, $label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL " . $this->additionalElementCalcJoin('avg_split'); } else { /* * Need to do first query to get distinct records as if we are doing left joins the sum is too large * However, views may not have a primary key which is unique so set to '' if no join */ $distinct = $this->getListModel()->isView() && trim($joinSQL) == '' ? '' : 'DISTINCT'; $sql = "SELECT ROUND(AVG(value), $roundTo) AS value, label FROM (SELECT " . $distinct . " $item->db_primary_key, $name AS value, $label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL " . $this->additionalElementCalcJoin('avg_split') . ") AS t"; } return $sql; } /** * Get sum query * * @param FabrikFEModelList &$listModel List model * @param array $labels Label * * @return string */ protected function getSumQuery(&$listModel, $labels = array()) { $label = count($labels) == 0 ? "'calc' AS label" : 'CONCAT(' . implode(', " & " , ', $labels) . ') AS label'; $item = $listModel->getTable(); $joinSQL = $listModel->buildQueryJoin(); $whereSQL = $listModel->buildQueryWhere(); $name = $this->getFullName(false, false); $groupModel = $this->getGroup(); if ($groupModel->isJoin()) { // Element is in a joined column - lets presume the user wants to sum all cols, rather than reducing down to the main cols totals $sql = "SELECT SUM($name) AS value, $label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL " . $this->additionalElementCalcJoin('sum_split'); } else { /* * Need to do first query to get distinct records as if we are doing left joins the sum is too large * However, views may not have a primary key which is unique so set to '' if no join */ $distinct = $this->getListModel()->isView() && trim($joinSQL) == '' ? '' : 'DISTINCT'; $sql = "SELECT SUM(value) AS value, label FROM (SELECT " . $distinct . " $item->db_primary_key, $name AS value, $label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL " . $this->additionalElementCalcJoin('sum_split') . ") AS t"; } return $sql; } /** * If split then the split element could require an additional join to get the sum query to work * * @param string $splitParam Name of calculation split param. Loads up split calculation element * * @return string Sql */ private function additionalElementCalcJoin($splitParam) { $sql = ''; $elementId = $this->getParams()->get($splitParam, null); if (!is_null($elementId)) { $pluginManager = FabrikWorker::getPluginManager(); $plugin = $pluginManager->getElementPlugin($elementId); /** * If the join table_join_alias is set, it has already been joined in the buildQueryJoin * so we don't need to add it (it'll blow up with a "Not unique table/alias" if we do) */ $join = $plugin->getJoin(); if (!(isset($join->table_join_alias) && !empty($join->table_join_alias))) { $sql = ' ' . $plugin->buildFilterJoin(); } } return $sql; } /** * Get a custom query * * @param FabrikFEModelList &$listModel list * @param string $label label * * @return string */ protected function getCustomQuery(&$listModel, $label = "'calc'") { $params = $this->getParams(); $custom_query = $params->get('custom_calc_query', ''); $item = $listModel->getTable(); $joinSQL = $listModel->buildQueryJoin(); $whereSQL = $listModel->buildQueryWhere(); $name = $this->getFullName(false, false); $groupModel = $this->getGroup(); if ($groupModel->isJoin()) { // Element is in a joined column - lets presume the user wants to sum all cols, rather than reducing down to the main cols totals // $custom_query = sprintf($custom_query, $name); $custom_query = str_replace('%s', $name, $custom_query); return "SELECT $custom_query AS value, $label AS label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL"; } else { // Need to do first query to get distinct records as if we are doing left joins the sum is too large // $custom_query = sprintf($custom_query, 'value'); $custom_query = str_replace('%s', 'value', $custom_query); return 'SELECT ' . $custom_query . ' AS value, label FROM (SELECT DISTINCT ' . FabrikString::safeColName($item->db_table_name) . '.*, ' . $name . ' AS value, ' . $label . ' AS label FROM ' . FabrikString::safeColName($item->db_table_name) . ' ' . $joinSQL . ' ' . $whereSQL . ') AS t'; } } /** * Get a query for our median query * * @param FabrikFEModelList &$listModel List * @param array $labels Label * * @return string */ protected function getMedianQuery(&$listModel, $labels = array()) { $label = count($labels) == 0 ? "'calc' AS label" : 'CONCAT(' . implode(', " & " , ', $labels) . ') AS label'; $item = $listModel->getTable(); $joinSQL = $listModel->buildQueryJoin(); $whereSQL = $listModel->buildQueryWhere(); $sql = 'SELECT ' . $this->getFullName(false, false) . ' AS value, ' . $label . ' FROM ' . FabrikString::safeColName($item->db_table_name) . ' ' . $joinSQL . ' ' . $whereSQL . ' ' . $this->additionalElementCalcJoin('median_split'); return $sql; } /** * Get a query for our count method * * @param FabrikFEModelList &$listModel List * @param array $labels Labels * * @return string */ protected function getCountQuery(&$listModel, $labels = array()) { $label = count($labels) == 0 ? "'calc' AS label" : 'CONCAT(' . implode(', " & " , ', $labels) . ') AS label'; $db = FabrikWorker::getDbo(); $item = $listModel->getTable(); $joinSQL = $listModel->buildQueryJoin(); $whereSQL = $listModel->buildQueryWhere(); $name = $this->getFullName(false, false); // $$$ hugh - need to account for 'count value' here! $params = $this->getParams(); $count_condition = $params->get('count_condition', ''); if (!empty($count_condition)) { if (!empty($whereSQL)) { $whereSQL .= " AND $name = " . $db->q($count_condition); } else { $whereSQL = "WHERE $name = " . $db->q($count_condition); } } $groupModel = $this->getGroup(); if ($groupModel->isJoin()) { // Element is in a joined column - lets presume the user wants to sum all cols, rather than reducing down to the main cols totals $sql = "SELECT COUNT($name) AS value, $label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL " . $this->additionalElementCalcJoin('count_split'); } else { // Need to do first query to get distinct records as if we are doing left joins the sum is too large $sql = "SELECT COUNT(value) AS value, label FROM (SELECT DISTINCT $item->db_primary_key, $name AS value, $label FROM " . FabrikString::safeColName($item->db_table_name) . " $joinSQL $whereSQL " . $this->additionalElementCalcJoin('count_split') . ") AS t"; } return $sql; } /** * Work out the calculation group-bys to apply: * * - If group_by is assigned in the app input * - If no group_by request then check the list models group by and add that * * @param string $splitParam Element parameter name containing the calculation split option * @param object $listModel List model * * @since 3.0.8 * * @return array (Group by element names, group by element labels) */ protected function calcGroupBys($splitParam, $listModel) { $pluginManager = FabrikWorker::getPluginManager(); $requestGroupBy = $this->app->getInput()->get('group_by', ''); $groupByLabels = array(); if ($requestGroupBy == '0') { $requestGroupBy = ''; } $groupBys = array(); if ($requestGroupBy !== '') { $formModel = $this->getFormModel(); $requestGroupBy = $formModel->getElement($requestGroupBy)->getElement()->id; $groupBys[] = $requestGroupBy; } else { $listGroupBy = $listModel->getTable()->group_by; if ($listGroupBy !== '') { $groupBys[] = $listGroupBy; } } $params = $this->getParams(); $splitSum = $params->get($splitParam, null); if (!is_null($splitSum)) { $groupBys[] = $splitSum; } foreach ($groupBys as &$gById) { $plugin = $pluginManager->getElementPlugin($gById); /** * In the calculation rendering, in default.php, we are keying by values not labels for join elements, * so there's no point keying by label for joins. So I think we need to change this chunk so it just tests to see if * there is a getJoinValueColumn method, and if so, use that. I'm leaving the original code intact though, * while I wait to see if this causes Other Horrible Things To Happen. */ /* $sName = method_exists($plugin, 'getJoinLabelColumn') ? $plugin->getJoinLabelColumn() : $plugin->getFullName(false, false, false); if (!stristr($sName, 'CONCAT')) { $gById = FabrikString::safeColName($sName); } else { if (method_exists($plugin, 'getJoinValueColumn')) { $sName = $plugin->getJoinValueColumn(); $gById = FabrikString::safeColName($sName); } } */ if (method_exists($plugin, 'getFullLabelOrConcat')) { $sName = $plugin->getJoinValueColumn(); $sLabel = $plugin->getFullLabelOrConcat(); } else { $sName = $sLabel = $plugin->getFullName(false, false, false); } $gById = FabrikString::safeColName($sName); $groupByLabels[] = $sLabel; } return array($groupBys, $groupByLabels); } /** * If the calculation query has had to convert the data to a machine format, use * this function to convert back to human readable format. E.g. time element * calcs in seconds but we'd want to convert back into h:m:s * * @param array &$rows Calculation values * * @return void */ protected function formatCalValues(&$rows) { } /** * Calculation: sum * can be overridden in element class * * @param FabrikFEModelList &$listModel List model * * @return array */ public function sum(&$listModel) { $db = $listModel->getDb(); $params = $this->getParams(); $splitSum = $params->get('sum_split', ''); list($groupBys, $groupByLabels) = $this->calcGroupBys('sum_split', $listModel); $split = empty($groupBys) ? false : true; $calcLabel = $params->get('sum_label', Text::_('COM_FABRIK_SUM')); if ($split) { $pluginManager = FabrikWorker::getPluginManager(); $plugin = $pluginManager->getElementPlugin($splitSum); $sql = $this->getSumQuery($listModel, $groupBys) . ' GROUP BY label'; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results2 = $db->loadObjectList('label'); $this->formatCalValues($results2); $uberTotal = 0; foreach ($results2 as $pair) { $uberTotal += $pair->value; } $uberObject = new stdClass; $uberObject->value = $uberTotal; $uberObject->label = Text::_('COM_FABRIK_TOTAL'); $uberObject->class = 'splittotal'; $uberObject->special = true; $results2[] = $uberObject; $results = $this->formatCalcSplitLabels($results2, $plugin, 'sum'); } else { // Need to add a group by here as well as if the ONLY_FULL_GROUP_BY SQL mode is enabled an error is produced $sql = $this->getSumQuery($listModel) . ' GROUP BY label'; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results = $db->loadObjectList('label'); $this->formatCalValues($results); } $res = $this->formatCalcs($results, $calcLabel, $split); return array($res, $results); } /** * calculation: average * can be overridden in element class * * @param FabrikFEModelList &$listModel list model * * @return string result */ public function avg(&$listModel) { $db = $listModel->getDb(); $params = $this->getParams(); $splitAvg = $params->get('avg_split', ''); $item = $listModel->getTable(); $calcLabel = $params->get('avg_label', Text::_('COM_FABRIK_AVERAGE')); list($groupBys, $groupByLabels) = $this->calcGroupBys('avg_split', $listModel); $split = empty($groupBys) ? false : true; if ($split) { $pluginManager = FabrikWorker::getPluginManager(); $plugin = $pluginManager->getElementPlugin($splitAvg); $sql = $this->getAvgQuery($listModel, $groupBys) . " GROUP BY label"; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results2 = $db->loadObjectList('label'); $this->formatCalValues($results2); $uberTotal = 0; foreach ($results2 as $pair) { $uberTotal += $pair->value; } $uberObject = new stdClass; $uberObject->value = $uberTotal / count($results2); $uberObject->label = Text::_('COM_FABRIK_AVERAGE'); $uberObject->special = true; $uberObject->class = 'splittotal'; $results2[] = $uberObject; $results = $this->formatCalcSplitLabels($results2, $plugin, 'avg'); } else { // Need to add a group by here as well as if the ONLY_FULL_GROUP_BY SQL mode is enabled an error is produced $sql = $this->getAvgQuery($listModel) . " GROUP BY label"; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results = $db->loadObjectList('label'); $this->formatCalValues($results); } $res = $this->formatCalcs($results, $calcLabel, $split); return array($res, $results); } /** * Get the sprintf format string * * @since 3.0.4 * * @return string */ public function getFormatString() { $params = $this->getParams(); return $params->get('text_format_string'); } /** * calculation: median * can be overridden in element class * * @param FabrikDEModelList &$listModel list model * * @return string result */ public function median(&$listModel) { $db = $listModel->getDb(); $item = $listModel->getTable(); $element = $this->getElement(); $joinSQL = $listModel->buildQueryJoin(); $whereSQL = $listModel->buildQueryWhere(); $params = $this->getParams(); $splitMedian = $params->get('median_split', ''); list($groupBys, $groupByLabels) = $this->calcGroupBys('sum_split', $listModel); $split = empty($groupBys) ? false : true; $format = $this->getFormatString(); $res = ''; $calcLabel = $params->get('median_label', Text::_('COM_FABRIK_MEDIAN')); $results = array(); if ($split) { $pluginManager = FabrikWorker::getPluginManager(); $plugin = $pluginManager->getElementPlugin($splitMedian); $sql = $this->getMedianQuery($listModel, $groupBys) . ' GROUP BY label '; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results2 = $db->loadObjectList(); $results = $this->formatCalcSplitLabels($results2, $plugin, 'median'); } else { $sql = $this->getMedianQuery($listModel); $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $res = $this->_median($db->loadColumn()); $o = new stdClass; if ($format != '') { $res = sprintf($format, $res); } $o->value = $res; $label = $this->getListHeading(); $o->elLabel = $label; $o->calLabel = $calcLabel; $o->label = 'calc'; $results = array('calc' => $o); } $res = $this->formatCalcs($results, $calcLabel, $split, true, false); return array($res, $results); } /** * calculation: count * can be overridden in element class * * @param FabrikFEModelList &$listModel List model * * @return string result */ public function count(&$listModel) { $db = $listModel->getDb(); $listModel->clearTable(); $item = $listModel->getTable(); $element = $this->getElement(); $params = $this->getParams(); $calcLabel = $params->get('count_label', Text::_('COM_FABRIK_COUNT')); $splitCount = $params->get('count_split', ''); list($groupBys, $groupByLabels) = $this->calcGroupBys('count_split', $listModel); $split = empty($groupBys) ? false : true; if ($split) { $pluginManager = FabrikWorker::getPluginManager(); $plugin = $pluginManager->getElementPlugin($splitCount); $sql = $this->getCountQuery($listModel, $groupBys) . " GROUP BY label "; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results2 = $db->loadObjectList('label'); $uberTotal = 0; /* * Removes values from display when split on used: * see http://www.fabrikar.com/forums/index.php?threads/calculation-split-on-problem.32035/ foreach ($results2 as $k => &$r) { if ($k == '') { unset($results2[$k]); } } */ foreach ($results2 as $pair) { $uberTotal += $pair->value; } $uberObject = new stdClass; $uberObject->value = count($results2) == 0 ? 0 : $uberTotal; $uberObject->label = Text::_('COM_FABRIK_TOTAL'); $uberObject->class = 'splittotal'; $uberObject->special = true; $results = $this->formatCalcSplitLabels($results2, $plugin, 'count'); $results[Text::_('COM_FABRIK_TOTAL')] = $uberObject; } else { // Need to add a group by here as well as if the ONLY_FULL_GROUP_BY SQL mode is enabled an error is produced $sql = $this->getCountQuery($listModel) . ' GROUP BY label '; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results = $db->loadObjectList('label'); } $res = $this->formatCalcs($results, $calcLabel, $split, false, false); return array($res, $results); } /** * calculation: custom_calc * can be overridden in element class * * @param FabrikFEModelList &$listModel list model * * @return array */ public function custom_calc(&$listModel) { $db = $listModel->getDb(); $params = $this->getParams(); $item = $listModel->getTable(); $splitCustom = $params->get('custom_calc_split', ''); $split = $splitCustom == '' ? false : true; $calcLabel = $params->get('custom_calc_label', Text::_('COM_FABRIK_CUSTOM')); if ($split) { $pluginManager = FabrikWorker::getPluginManager(); $plugin = $pluginManager->getElementPlugin($splitCustom); $splitName = method_exists($plugin, 'getJoinLabelColumn') ? $plugin->getJoinLabelColumn() : $plugin->getFullName(false, false); $splitName = FabrikString::safeColName($splitName); $sql = $this->getCustomQuery($listModel, $splitName) . ' GROUP BY label'; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results2 = $db->loadObjectList('label'); $results = $this->formatCalcSplitLabels($results2, $plugin, 'custom_calc'); } else { // Need to add a group by here as well as if the ONLY_FULL_GROUP_BY SQL mode is enabled an error is produced $sql = $this->getCustomQuery($listModel) . ' GROUP BY label'; $sql = $listModel->pluginQuery($sql); $db->setQuery($sql); $results = $db->loadObjectList('label'); } $res = $this->formatCalcs($results, $calcLabel, $split); return array($res, $results); } /** * Format the labels for calculations when they are split * * @param array &$results2 calculation results * @param object &$plugin element that the data is SPLIT on * @param string $type of calculation * * @return array */ protected function formatCalcSplitLabels(&$results2, &$plugin, $type = '') { $results = array(); $toMerge = array(); $name = $plugin->getFullName(true, false); // $$$ hugh - avoid PHP warning if $results2 is NULL if (empty($results2)) { return $results; } foreach ($results2 as $key => $val) { if (isset($val->special) && $val->special) { // Don't include special values (ubers) in $toMerge, otherwise total sum added to first value $results[$val->label] = $val; continue; } if ($plugin->hasSubElements) { // http://fabrikar.com/forums/index.php?threads/calculation-split-on-problem.40122/ $labelParts = explode(' & ', $val->label??''); if (count($labelParts) > 1) { $label = $labelParts[1]; } else { $label = $labelParts[0]; } $label = FabrikWorker::JSONtoData($label); $ls = array(); if (is_array($label)) { foreach ($label as $l) { $ls[] = ($type != 'median') ? $plugin->getLabelForValue($l) : $plugin->getLabelForValue($key, $key); } } else { $ls[] = ($type != 'median') ? $plugin->getLabelForValue($label) : $plugin->getLabelForValue($key, $key); } if (count($labelParts) > 1) { $val->label = $labelParts[0] . ' & ' . implode(',', $ls); } else { $val->label = implode(',', $ls); } } else { $d = new stdClass; $d->$name = $val->label; $opts = array('rollover' => false, 'link' => false); $val->label = $plugin->renderListData($val->label, $d, $opts); } if (array_key_exists($val->label, $results)) { /** $$$ rob the $result data is keyed on the raw database result - however, we are interested in * keying on the formatted table result (e.g. allows us to group date entries by year) */ if ($results[$val->label] !== '') { $toMerge[$val->label][] = $results[$val->label]->value; } $results[$val->label] = ''; $toMerge[$val->label][] = $val->value; } else { $results[$val->label] = $val; } } foreach ($toMerge as $label => $data) { $o = new stdClass; switch ($type) { case 'avg': $o->value = $this->simpleAvg($data); break; case 'sum': $o->value = $this->simpleSum($data); break; case 'median': $o->value = $this->_median($data); break; case 'count': $o->value = count($data); break; case 'custom_calc': $params = $this->getParams(); $custom_calc_php = $params->get('custom_calc_php', ''); if (!empty($custom_calc_php)) { FabrikWorker::clearEval(); $o->value = Php::Eval(['code' => $custom_calc_php]); FabrikWorker::logEval($custom_calc_php, 'Caught exception on eval of ' . $name . ': %s'); } else { $o->value = $data; } break; default: $o->value = $data; break; } $o->label = $label; $results[$label] = $o; } return $results; } /** * find an average from a set of data * can be overwritten in plugin - see date for example of averaging dates * * @param array $data to average * * @return string average result */ public function simpleAvg($data) { return $this->simpleSum($data) / count($data); } /** * find the sum from a set of data * can be overwritten in plugin - see date for example of averaging dates * * @param array $data to sum * * @return string sum result */ public function simpleSum($data) { return array_sum($data); } /** * Take the results form a calc and create the string that can be used to summarize them * * @param array &$results calculation results * @param string $calcLabel calc label * @param bool $split is the data split * @param bool $numberFormat should we apply any number formatting * @param bool $sprintFFormat should we apply the text_format_string ? * * @return string */ protected function formatCalcs(&$results, $calcLabel, $split = false, $numberFormat = true, $sprintFFormat = true) { settype($results, 'array'); $res = array(); $res[] = $split ? '<dl>' : '<ul class="fabrikRepeatData">'; $l = '<span class="calclabel">' . $calcLabel . '</span>'; $res[] = $split ? '<dt>' . $l . '</dt>' : '<li>' . $l; $format = $this->getFormatString(); $label = $this->getListHeading(); foreach ($results as $key => $o) { $o->label = ($o->label == 'calc') ? '' : $o->label; $o->elLabel = $label . ' ' . $o->label; if ($numberFormat) { $o->value = $this->numberFormat($o->value); } if ($format != '' && $sprintFFormat) { $o->value = sprintf($format, $o->value); } $o->calLabel = $calcLabel; $class = isset($o->class) ? ' class="' . $o->class . '"' : ''; if ($split) { $res[] = '<dd' . $class . '><span class="calclabel">' . $o->label . ':</span> ' . $o->value . '</dd>'; } else { $res[] = $o->value . '</li>'; } } ksort($results); $res[] = $split ? '</dl>' : '</ul>'; return implode("\n", $res); } /** * Get median * * @param array $results set of results to get median from * * @return string median value */ private function _median($results) { $results = (array) $results; sort($results); if ((count($results) % 2) == 1) { /* odd */ $midKey = floor(count($results) / 2); return $results[$midKey]; } else { $midKey = floor(count($results) / 2) - 1; $midKey2 = floor(count($results) / 2); return $this->simpleAvg(array(FArrayHelper::getValue($results, $midKey), FArrayHelper::getValue($results, $midKey2))); } } /** * Returns javascript which creates an instance of the class defined in formJavascriptClass() * * @param int $repeatCounter Repeat group counter * * @return array */ public function elementJavascript($repeatCounter) { return array(); } /** * Get JS code for ini element list js * Overwritten in plugin classes * * @return string */ public function elementListJavascript() { return ''; } /** * Create a class for the elements default javascript options * * @param int $repeatCounter repeat group counter * * @return object options */ public function getElementJSOptions($repeatCounter) { $element = $this->getElement(); $opts = new stdClass; $data = $this->getFormModel()->data; $opts->repeatCounter = $repeatCounter; $opts->editable = ($this->canView() && !$this->canUse()) ? false : $this->isEditable(); $opts->value = $this->getValue($data, $repeatCounter); $opts->label = $element->label; $opts->defaultVal = $this->getDefaultValue($data); $opts->inRepeatGroup = $this->getGroup()->canRepeat() == 1; $opts->fullName = $this->getFullName(true, false); $opts->watchElements = $this->validator->jsWatchElements($repeatCounter); $groupModel = $this->getGroup(); $opts->canRepeat = (bool) $groupModel->canRepeat(); $opts->isGroupJoin = (bool) $groupModel->isJoin(); $validations = $this->validator->findAll(); $opts->mustValidate = false; foreach ($validations as $validation) { $validationParams = $validation->getParams(); if ($validationParams->get('must_validate', '0') === '1') { if ($validation->canValidate()) { $opts->mustValidate = true; break; } } } $opts->validations = empty($validations) ? false : true; if ($this->isJoin()) { $opts->joinid = (int) $this->getJoinModel()->getJoin()->id; } else { $opts->joinid = (int) $groupModel->getGroup()->join_id; } return $opts; } /** * Does the element use the WYSIWYG editor * * @return bool use wysiwyg editor */ public function useEditor() { return false; } /** * Processes uploaded data * * @return void */ public function processUpload() { } /** * Get the class to manage the form element * to ensure that the file is loaded only once * * @param array &$srcs Scripts previously loaded * @param string $script Script to load once class has loaded * @param array &$shim Dependant class names to load before loading the class - put in requirejs.config shim * * @return void */ public function formJavascriptClass(&$srcs, $script = '', &$shim = array()) { $name = $this->getElement()->plugin; $ext = FabrikHelperHTML::isDebug() ? '.js' : '-min.js'; $formId = $this->getFormModel()->getId(); static $elementClasses; if (!isset($elementClasses)) { $elementClasses = array(); } if (!array_key_exists($formId, $elementClasses)) { $elementClasses[$formId] = array(); } // Load up the default script if ($script == '') { $script = 'plugins/fabrik_element/' . $name . '/' . $name . $ext; } if (empty($elementClasses[$formId][$script])) { $srcs['Element' . ucfirst($name)] = $script; $elementClasses[$formId][$script] = 1; } } /** * load js file for element when in list view * * @param array &$srcs JS scripts to load * * @return null */ public function tableJavascriptClass(&$srcs) { $p = $this->getElement()->plugin; $src = 'plugins/fabrik_element/' . $p . '/list-' . $p . '.js'; if (File::exists(JPATH_SITE . '/' . $src)) { $className = 'Fb' . ucfirst($p) .'List'; $srcs[$className] = $src; } } /** * Can be overwritten in plugin classes * e.g. if changing from db join to field we need to remove the join * entry from the #__fabrik_joins table * * @param object &$row that is going to be updated * * @return null */ public function beforeSave(&$row) { // $safeHtmlFilter = InputFilter::getInstance(null, null, 1, 1); $safeHtmlFilter = InputFilter::getInstance(array(), array(), 1, 1); $post = $safeHtmlFilter->clean($_POST, 'array'); $post = $post['jform']; $dbjoinEl = (is_subclass_of($this, 'PlgFabrik_ElementDatabasejoin') || get_class($this) == 'PlgFabrik_ElementDatabasejoin'); /** * $$$ hugh - added test for empty id, i.e. new element, otherwise we try and delete a crap load of join table rows * we shouldn't be deleting! Also adding defensive code to deleteJoins() to test for empty ID. */ if (!empty($post['id']) && !$this->isJoin() && !$dbjoinEl) { $this->deleteJoins((int) $post['id']); } } /** * Delete joins * * @param int $id element id * * @return null */ protected function deleteJoins($id) { // $$$ hugh - bail if no $id specified if (empty($id)) { return; } $db = FabrikWorker::getDbo(true); $query = $db->getQuery(true); $query->delete('#__fabrik_joins')->where('element_id = ' . $id); $db->setQuery($query); $db->execute(); $query->clear(); $query->select('j.id AS jid')->from('#__fabrik_elements AS e')->join('INNER', ' #__fabrik_joins AS j ON j.element_id = e.id') ->where('e.parent_id = ' . $id); $db->setQuery($query); $join_ids = $db->loadColumn(); if (!empty($join_ids)) { $query->clear(); $query->delete('#__fabrik_joins')->where('id IN (' . implode(',', $join_ids) . ')'); $db->setQuery($query); $db->execute(); } } /** * If your element risks not to post anything in the form (e.g. check boxes with none checked) * the this function will insert a default value into the database * * @param array &$data form data * * @return array form data */ public function getEmptyDataValue(&$data) { } /** * Used to format the data when shown in the form's email * * @param mixed $value element's data * @param array $data form records data * @param int $repeatCounter repeat group counter * * @return string formatted value */ public function getEmailValue($value, $data = array(), $repeatCounter = 0) { if ($this->inRepeatGroup && is_array($value)) { $val = array(); foreach ($value as $v2) { $val[] = $this->getIndEmailValue($v2, $data, $repeatCounter); } } else { $val = $this->getIndEmailValue($value, $data, $repeatCounter); } return $val; } /** * Turn form value into email formatted value * * @param mixed $value Element value * @param array $data Form data * @param int $repeatCounter Group repeat counter * * @return string email formatted value */ protected function getIndEmailValue($value, $data = array(), $repeatCounter = 0) { return $value; } /** * Is the element an upload element * * @return boolean */ public function isUpload() { return $this->is_upload; } /** * If a database join element's value field points to the same db field as this element * then this element can, within modifyJoinQuery, update the query. * E.g. if the database join element points to a file upload element then you can replace * the file path that is the standard $val with the html to create the image * * @param string $val value * @param string $view form or list * * @deprecated - doesn't seem to be used * * @return string modified val */ protected function modifyJoinQuery($val, $view = 'form') { return $val; } /** * not used * * @deprecated * * @return void */ public function ajax_loadTableFields() { $db = FabrikWorker::getDbo(); $listModel = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('List', 'FabrikFEModel'); $input = $this->app->getInput(); $this->_cnnId = $input->getInt('cid', 0); $tbl = $db->qn($input->get('table')); $fieldDropDown = $listModel->getFieldsDropDown($this->_cnnId, $tbl, '-', false, 'params[join_val_column]'); $fieldDropDown2 = $listModel->getFieldsDropDown($this->_cnnId, $tbl, '-', false, 'params[join_key_column]'); echo "$('addJoinVal').innerHTML = '$fieldDropDown';"; echo "$('addJoinKey').innerHTML = '$fieldDropDown2';"; } /** * Get join row * * @return FabrikTableJoin join table or false if not loaded */ protected function getJoin() { if ($this->isJoin()) { return $this->getJoinModel()->getJoin(); } return false; } /** * Get database field description * * @return string db field type */ public function getFieldDescription() { $element = $this->getPluginName(); $plugin = PluginHelper::getPlugin('fabrik_element', $element); $fParams = new Registry($plugin->params); $p = $this->getParams(); if ($this->encryptMe()) { return 'BLOB'; } $group = $this->getGroup(); if ($group->isJoin() == 0 && $group->canRepeat()) { return "TEXT"; } else { $size = $p->get('maxlength', $this->fieldSize); $objType = sprintf($this->fieldDesc, $size); } $objType = $fParams->get('defaultFieldType', $objType); return $objType; } /** * Trigger called when a row is deleted, can be used to delete images previously uploaded * * @param array $groups grouped data of rows to delete * * @return void */ public function onDeleteRows($groups) { } /** * Get a value to use as an empty filter value * * @return string */ public function emptyFilterValue() { return ''; } /** * Trigger called when a form or group row is stored. * Ignores the element if it is a join chx/multi select * * @param array &$data Data to store * @param int $repeatCounter Repeat group index * * @return bool If false, data should not be added. */ public function onStoreRow(&$data, $repeatCounter = 0) { if ($this->isJoin()) { return false; } $element = $this->getElement(); // We should not process this element if it is unpublished // Unpublished elements may not be in a valid state and may cause an error (white-screen) if (!$element->published) { return false; } $shortName = $element->name; $listModel = $this->getListModel(); if ($this->encryptMe()) { $listModel->encrypt[] = $shortName; } $formModel = $this->getFormModel(); $name = $this->getFullName(true, false); /** * @TODO - fix this to use formData instead of formDataWithTableName, * which we need to deprecate. */ if (!array_key_exists($name, $formModel->formDataWithTableName)) //if ($this->dataConsideredEmpty(FArrayHelper::getValue($formModel->formDataWithTableName, $name, ''))) { $this->getEmptyDataValue($data); } $v = $this->getValue($formModel->formDataWithTableName, $repeatCounter); if (!$this->ignoreOnUpdate($v)) { $data[$shortName] = $v; } return true; } /** * Shows the data formatted for the list view * * @param string $data Elements data * @param stdClass &$thisRow All the data in the lists current row * @param array $opts Rendering options * * @return string formatted value */ public function renderListData($data, stdClass &$thisRow, $opts = array()) { $profiler = Profiler::getInstance('Application'); JDEBUG ? $profiler->mark("renderListData: parent: start: {$this->element->name}") : null; $params = $this->getParams(); $listModel = $this->getListModel(); if (!ArrayHelper::getValue($opts, 'json', false)) { $data = FabrikWorker::JSONtoData($data, true); } else { $data = (array) $data; } foreach ($data as $i => &$d) { /** * At this point we should have scalar data, but if something (like a textarea) had JSON as its value, * it will have gotten decoded by the JSONtoData, so if not scalar, re-encode it. */ if (!is_scalar($d)) { $d = json_encode($d); } if ((int)$params->get('icon_folder', 0) > 0 && ArrayHelper::getValue($opts, 'icon', 1)) { // $$$ rob was returning here but that stopped us being able to use links and icons together $d = $this->replaceWithIcons($d, 'list', $listModel->getTmpl()); } if (ArrayHelper::getValue($opts, 'rollover', 1)) { $d = $this->rollover($d, $thisRow, 'list'); } if (ArrayHelper::getValue($opts, 'link', 1)) { $d = $listModel->_addLink($d, $this, $thisRow, $i); } } $final = $this->renderListDataFinal($data, $opts); JDEBUG ? $profiler->mark("renderListData: parent: end: {$this->element->name}") : null; return $final; } /** * Final prepare data function called from renderListData(), converts data to string and if needed * encases in <ul> (for repeating data) * * @param array $data list cell data * $param array $opts list options * * @return string cell data */ protected function renderListDataFinal($data, $opts) { $profiler = Profiler::getInstance('Application'); JDEBUG ? $profiler->mark("renderListDataFinal: parent: start: {$this->element->name}") : null; if (is_array($data)) { if (count($data) > 1) { if (!array_key_exists(0, $data)) { // Occurs if we have created a list from an existing table whose data contains json objects (e.g. #__users.params) $obj = ArrayHelper::toObject($data); $data = array(); $data[0] = $obj; } // If we are storing info as json the data will contain an array of objects if (is_object($data[0])) { foreach ($data as &$o) { $this->convertDataToString($o); } } $r = '<ul class="fabrikRepeatData"><li>' . implode('</li><li>', $data) . '</li></ul>'; } else { $r = empty($data) ? '' : array_shift($data); } } else { $r = $data; } $displayData = new stdClass; $displayData->text = $r; if (ArrayHelper::getValue($opts, 'custom_layout', 1)) { $layout = $this->getLayout('list'); $res = $layout->render($displayData); } else { $res = ''; } // If no custom list layout found revert to the default list renderer if ($res === '') { $basePath = COM_FABRIK_FRONTEND . '/layouts/'; $layout = new FileLayout('fabrik-element-list', $basePath, array('debug' => false, 'component' => 'com_fabrik', 'client' => 'site')); $res = $layout->render($displayData); } JDEBUG ? $profiler->mark("renderListDataFinal: parent: end: {$this->element->name}") : null; return $res; } /** * Convert an object or array into a <ul> * * @param mixed &$o data to convert * * @return void */ protected function convertDataToString(&$o) { if (is_object($o)) { $s = '<ul>'; foreach ($o as $k => $v) { if (!is_string($v)) { $v = json_encode($v); } $s .= '<li>' . $v . '</li>'; } $s .= '</ul>'; $o = $s; } } /** * Prepares the element data for CSV export * * @param string $data Element data * @param object &$thisRow All the data in the lists current row * * @return string Formatted CSV export value */ public function renderListData_csv($data, &$thisRow) { return $data; } /** * Builds some html to allow certain elements to display the option to add in new options * e.g. picklists, dropdowns radiobuttons * * @param bool $repeatCounter repeat group counter * @param bool $onlylabel only show the label - overrides standard element settings * * @return string */ protected function getAddOptionFields($repeatCounter, $onlylabel = false) { $params = $this->getParams(); if (!$params->get('allow_frontend_addto')) { return; } $basePath = COM_FABRIK_BASE . '/components/com_fabrik/layouts/element'; $layout = new FileLayout('fabrik-element-addoptions', $basePath, array('debug' => false, 'component' => 'com_fabrik', 'client' => 'site')); $displayData = new stdClass; $displayData->id = $this->getHTMLId($repeatCounter); $displayData->add_image = FabrikHelperHTML::image('plus', 'form', @$this->tmpl, array('alt' => Text::_('COM_FABRIK_ADD'))); $displayData->allowadd_onlylabel = $params->get('allowadd-onlylabel'); $displayData->savenewadditions = $params->get('savenewadditions'); $displayData->onlylabel = $onlylabel; $displayData->hidden_field = $this->getHiddenField($displayData->id . '_additions', '', $displayData->id . '_additions'); return $layout->render($displayData); } /** * Does the element force the form to submit via AJAX * * @deprecated - not used * * @return bool true if the element type forces the form to */ public function requiresAJAXSubmit() { return false; } /** * Determine if the element should run its validation plugins on form submission * * @return bool default true */ public function mustValidate() { return true; } /** * Get the name of the field to order the table data by * can be overwritten in plugin class - but not currently done so * * @return string column to order by tablename___elementname and yes you can use aliases in the order by clause */ public function getOrderByName() { return $this->getFullName(true, false); } /** * Not used * * @param string $rawval raw value * * @deprecated - not used * * @return string */ public function getFilterLabel($rawval) { return $rawval; } /** * Store the element params * * @return bool * * @throws RuntimeException */ public function storeAttribs() { $element = $this->getElement(); if (!$element) { return false; } $db = FabrikWorker::getDbo(true); $element->params = $this->getParams()->toString(); /* * Trying to save JSON params larger than the params field totally breaks the backend. Original size * of params field was TEXT, and a large serialized calculation could blow that away. * * In 3.6.1 we increased the size of params to MEDIUMTEXT, but people only updating from github * may not get that update right away. So just to be on the safe side, we'll update it on the fly * if params are big. Leave this in till 3.7. */ if (strlen($element->params) > 65535) { $db->setQuery("ALTER TABLE `#__fabrik_elements` MODIFY `params` MEDIUMTEXT"); try { $db->execute(); } catch (RuntimeException $e) { // meh } } $query = $db->getQuery(true); $query->update('#__fabrik_elements')->set('params = ' . $db->q($element->params))->where('id = ' . (int) $element->id); $db->setQuery($query); $res = $db->execute(); return $res; } /** * load a new set of default properties and params for the element * can be overridden in plugin class * * @param array $properties Default props * * @return FabrikTableElement element (id = 0) */ public function getDefaultProperties($properties = array()) { $now = $this->date->toSql(); $this->setId(0); $item = $this->getElement(); $item->set('plugin', $this->_name); $item->set('params', $this->getDefaultAttribs()); $item->set('created', $now); $item->set('created_by', $this->user->get('id')); $item->set('created_by_alias', $this->user->get('username')); $item->set('published', '1'); $item->set('show_in_list_summary', '1'); $item->set('link_to_detail', '1'); $item->bind($properties); return $item; } /** * Get a json encoded string of the element default parameters * * @return string */ public function getDefaultAttribs() { $o = new stdClass; $o->rollover = ''; $o->comment = ''; $o->sub_default_value = ''; $o->sub_default_label = ''; $o->element_before_label = 1; $o->allow_frontend_addtocheckbox = 0; $o->database_join_display_type = 'dropdown'; $o->joinType = 'simple'; $o->join_conn_id = -1; $o->date_table_format = 'Y-m-d'; $o->date_form_format = 'Y-m-d H:i:s'; $o->date_showtime = 0; $o->date_time_format = 'H:i'; $o->date_defaulttotoday = 1; $o->date_firstday = 0; $o->multiple = 0; $o->allow_frontend_addtodropdown = 0; $o->password = 0; $o->maxlength = 255; $o->text_format = 'text'; $o->integer_length = 6; $o->decimal_length = 2; $o->guess_linktype = 0; $o->disable = 0; $o->readonly = 0; $o->ul_max_file_size = 16000; $o->ul_email_file = 0; $o->ul_file_increment = 0; $o->upload_allow_folderselect = 1; $o->fu_fancy_upload = 0; $o->upload_delete_image = 1; $o->make_link = 0; $o->fu_show_image_in_table = 0; $o->image_library = 'gd2'; $o->make_thumbnail = 0; $o->imagepath = '/'; $o->selectImage_root_folder = '/'; $o->image_front_end_select = 0; $o->show_image_in_table = 0; $o->image_float = 'none'; $o->link_target = '_self'; $o->radio_element_before_label = 0; $o->options_per_row = 4; $o->ck_options_per_row = 4; $o->allow_frontend_addtoradio = 0; $o->use_wysiwyg = 0; $o->my_table_data = 'id'; $o->update_on_edit = 0; $o->view_access = 1; $o->show_in_rss_feed = 0; $o->show_label_in_rss_feed = 0; $o->icon_folder = -1; $o->use_as_row_class = 0; $o->filter_access = 1; $o->full_words_only = 0; $o->inc_in_adv_search = 1; $o->sum_on = 0; $o->sum_access = 0; $o->avg_on = 0; $o->avg_access = 0; $o->median_on = 0; $o->median_access = 0; $o->count_on = 0; $o->count_access = 0; return json_encode($o); } /** * Do we need to include the light-box js code * * @return bool */ public function requiresLightBox() { return false; } /** * Do we need to include the slide-show js code * * @return bool */ public function requiresSlideshow() { return false; } /** * Get Joomfish options * * @deprecated - not supporting joomfish * * @return array key=>value options */ public function getJoomfishOptions() { return array(); } /** * When filtering a table determine if the element's filter should be an exact match * should take into account if the element is in a non-joined repeat group * * @param string $val element value * * @return bool */ public function isExactMatch($val) { $element = $this->getElement(); $filterExactMatch = isset($val['match']) ? $val['match'] : $element->filter_exact_match; $group = $this->getGroup(); if (!$group->isJoin() && $group->canRepeat()) { $filterExactMatch = false; } return $filterExactMatch; } /** * Not used * * @deprecated - not used * * @return boolean */ public function onAjax_getFolders() { $input = $this->app->getInput(); $rDir = $input->getString('dir'); $folders = Folder::folders($rDir); if ($folders === false) { // $$$ hugh - need to echo empty JSON array otherwise we break JS which assumes an array echo json_encode(array()); return false; } sort($folders); echo json_encode($folders); } /** * If used as a filter add in some JS code to watch observed filter element's changes * when it changes update the contents of this elements dd filter's options * * @param bool $normal is the filter a normal (true) or advanced filter * @param string $container container * * @return void */ public function filterJS($normal, $container) { // Overwritten in plugin } /** * Should the element's data be returned in the search all? * Looks at the lists selected options, if its there looks at what search mode the list is using * and determines if the selected element can be used. * * @param bool $advancedMode Is the elements' list is extended search all mode? * @param string $search Search string * * @return bool true */ public function includeInSearchAll($advancedMode = false, $search = '') { if ($this->isJoin() && $advancedMode) { return false; } $listModel = $this->getListModel(); $listParams = $listModel->getParams(); $searchElements = $listParams->get('list_search_elements', ''); if ($searchElements === '') { return false; } $searchElements = json_decode($searchElements); if (!isset($searchElements->search_elements) || !is_array($searchElements->search_elements)) { return false; } if (in_array($this->getId(), $searchElements->search_elements)) { $advancedMode = $listParams->get('search-mode-advanced'); return $this->canIncludeInSearchAll($advancedMode); } } /** * Is it possible to include the element in the Search all query? * true if basic search * true/false if advanced search * * @param bool $advancedMode Is the list using advanced search * * @since 3.1b * * @return boolean */ public function canIncludeInSearchAll($advancedMode) { $params = $this->getParams(); if (!$advancedMode) { return true; } if ($this->ignoreSearchAllDefault) { return false; } $format = $params->get('text_format'); if ($format == 'integer' || $format == 'decimal') { return false; } return true; } /** * Modify the label for admin list - filter elements. * Adds a '*' if the element is not available in advanced search * * @param string &$label Element label * * @return void */ public function availableInAdvancedSearchLabel(&$label) { $label = $this->canIncludeInSearchAll(true) ? $label : $label . '*'; } /** * Get the value to use for graph calculations * see timer which converts the value into seconds * * @param string $v standard value * * @return mixed calculation value */ public function getCalculationValue($v) { return (float) $v; } /** * run on formModel::setFormData() * * @param int $c repeat group counter * * @return void */ public function preProcess($c) { } /** * Called when copy row list plugin called * * @param mixed $val value to copy into new record * * @return mixed value to copy into new record */ public function onCopyRow($val) { return $this->defaultOnCopy() ? $this->default : $val; } /** * Called when save as copy form button clicked * * @param mixed $val value to copy into new record * * @return mixed value to copy into new record */ public function onSaveAsCopy($val) { return $this->defaultOnCopy() ? $this->default : $val; } /** * Ajax call to get auto complete options (now caches results) * * @return string json encoded options */ public function onAutocomplete_options() { // Needed for ajax update (since we are calling this method via dispatcher element is not set) $input = $this->app->getInput(); $o = new stdClass; $formModel = $this->getFormModel(); $this->setId($input->getInt('element_id')); $this->loadMeForAjax(); // Check for request forgeries if ($formModel->spoofCheck() && !Session::checkToken('request')) { $o->error = Text::_('JERROR_ALERTNOAUTHOR'); echo json_encode($o); return; } if (!$this->canUse()) { $o->error = Text::_('JERROR_ALERTNOAUTHOR'); echo json_encode($o); return; } $cache = FabrikWorker::getCache(); $search = $input->get('value', '', 'string'); // uh oh, can't serialize PDO db objects so no can cache, as J! serializes the args if ($this->config->get('dbtype') === 'pdomysql') { echo $this->cacheAutoCompleteOptions($this, $search); } else { echo $cache->get(array(get_class($this), 'cacheAutoCompleteOptions'), [$this, $search]); } } /** * Cache method to populate auto-complete options * * @param plgFabrik_Element $elementModel element model * @param string $search search string * @param array $opts options, 'label' => field to use for label (db join) * * @since 3.0.7 * * @return string json encoded search results */ public static function cacheAutoCompleteOptions($elementModel, $search, $opts = array()) { $name = $elementModel->getFullName(false, false); $elementModel->encryptFieldName($name); $listModel = $elementModel->getListModel(); $db = $listModel->getDb(); $query = $db->getQuery(true); $tableName = $listModel->getTable()->db_table_name; $query->select('DISTINCT(' . $name . ') AS value, ' . $name . ' AS text')->from($tableName); $query->where($name . ' LIKE ' . $db->q(addslashes('%' . $search . '%'))); $query = $listModel->buildQueryJoin($query); $query = $listModel->buildQueryWhere(false, $query); $query = $listModel->pluginQuery($query); $db->setQuery($query); $tmp = $db->loadObjectList(); foreach ($tmp as &$t) { $elementModel->toLabel($t->text); $t->text = strip_tags($t->text); } return json_encode($tmp); } /** * Get the table name that the element stores to * can be the main table name or the joined table name * * @return string */ protected function getTableName() { $listModel = $this->getListModel(); $table = $listModel->getTable(); $groupModel = $this->getGroup(); if ($groupModel->isJoin()) { $joinModel = $groupModel->getJoinModel(); $join = $joinModel->getJoin(); $name = $join->table_join; } else { $name = $table->db_table_name; } return $name; } /** * Converts a raw value into its label equivalent * * @param string &$v raw value * * @return void */ protected function toLabel(&$v) { } /** * Build group by query to append to list query * * @return string getGroupByQuery */ public function getGroupByQuery() { return ''; } /** * Append element where statement to lists where array * * @param array &$whereArray list models where statements * * @return void */ public function appendTableWhere(&$whereArray) { $params = $this->getParams(); if ($params->get('append_table_where', false)) { if (method_exists($this, 'buildQueryWhere')) { $where = trim($this->buildQueryWhere(array())); if ($where != '') { $where = StringHelper::substr($where, 5, StringHelper::strlen($where) - 5); if (!in_array($where, $whereArray)) { $whereArray[] = $where; } } } } } /** * Used by validations * * @param string $data this elements data * @param string $cond what condition to apply * @param string $compare data to compare element's data to * * @return bool */ public function greaterOrLessThan($data, $cond, $compare) { if ($cond == '>') { return $data > $compare; } elseif ($cond == '>=') { return $data >= $compare; } elseif ($cond == '<') { return $data < $compare; } elseif ($cond == '<=') { return $data <= $compare; } elseif ($cond == '==') { return $data == $compare; } return false; } /** * Can the element plugin encrypt data * * @return bool */ public function canEncrypt() { return false; } /** * Should the element's data be encrypted * * @return bool */ public function encryptMe() { $params = $this->getParams(); return ($this->canEncrypt() && $params->get('encrypt', false)); } /** * Format a number value * * @param mixed $data (double/int) * * @return string formatted number */ protected function numberFormat($data) { $params = $this->getParams(); if (!$params->get('field_use_number_format', false) || empty($data)) { return $data; } $decimalLength = (int) $params->get('decimal_length', 2); $decimalSep = $params->get('field_decimal_sep', '.'); $thousandSep = $params->get('field_thousand_sep', ','); // Workaround for params not letting us save just a space! if ($thousandSep == '#32') { $thousandSep = ' '; } else if ($thousandSep == '#00') { $thousandSep = ''; } return number_format((float) $data, $decimalLength, $decimalSep, $thousandSep); } /** * Strip number format from a number value * * @param mixed $val (double/int) * * @return string formatted number */ public function unNumberFormat($val) { $params = $this->getParams(); if (!$params->get('field_use_number_format', false) || is_null($val)) { return $val; } // if we're copying a row, format won't be applied $formModel = $this->getFormModel(); if (isset($formModel->formData) && array_key_exists('fabrik_copy_from_table', $formModel->formData)) { return $val; } // Might think about rounding to decimal_length, but for now let MySQL do it $decimalLength = (int) $params->get('decimal_length', 2); // Swap dec and thousand seps back to Normal People Decimal Format! $decimalSep = $params->get('field_decimal_sep', '.'); $thousandSep = $params->get('field_thousand_sep', ','); // Workaround for params not letting us save just a space! if ($thousandSep == '#32') { $thousandSep = ' '; } else if ($thousandSep == '#00') { $thousandSep = ''; } $val = str_replace($thousandSep, '', $val); $val = str_replace($decimalSep, '.', $val); return $val; } /** * Recursively get all linked children of an element * * @param int $id element id * * @return array */ public function getElementDescendents($id = 0) { if (empty($id)) { $id = $this->id; } $db = FabrikWorker::getDbo(true); $query = $db->getQuery(true); $query->select('id')->from('#__fabrik_elements')->where('parent_id = ' . (int) $id); $db->setQuery($query); $kids = $db->loadObjectList(); $all_kids = array(); foreach ($kids as $kid) { $all_kids[] = $kid->id; $all_kids = array_merge($this->getElementDescendents($kid->id), $all_kids); } return $all_kids; } /** * Get the actual table name to use when building select queries * so if in a joined group get the joined to table's name otherwise return the * table's db table name * * @return string */ protected function actualTableName() { if (isset($this->actualTable)) { return $this->actualTable; } $groupModel = $this->getGroup(); if ($groupModel->isJoin()) { $joinModel = $groupModel->getJoinModel(); return $joinModel->getJoin()->table_join; } $listModel = $this->getListModel(); $this->actualTable = $listModel->getTable()->db_table_name; return $this->actualTable; } /** * When creating crud query in tableModel::storeRow() each element has the chance * to alter the row id - used by sugarid plugin to fudge rowid * * @param string &$rowId row id * * @deprecated - not used * * @return void */ public function updateRowId(&$rowId) { } /** * Fabrik3: moved to Admin Element Model * * @deprecated - not used * * @return string table name */ protected function getRepeatElementTableName() { } /** * Is the element a repeating element * * @return bool */ public function isRepeatElement() { return $this->isJoin(); } /** * fabrik3: moved to Admin Element Model * if repeated element we need to make a joined db table to store repeated data in * * @depreciated * * @return void */ public function createRepeatElement() { } /** * get the element's associated join model * * @return FabrikFEModelJoin join model */ public function getJoinModel() { if (is_null($this->joinModel)) { $this->joinModel = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('Join', 'FabrikFEModel'); // $$$ rob ensure we load the join by asking for the parents id, but then ensure we set the element id back to this elements id $this->joinModel->getJoinFromKey('element_id', $this->getParent()->id); $this->joinModel->getJoin()->element_id = $this->getElement()->id; } return $this->joinModel; } /** * When saving an element pk we need to update any join which has the same params->pk * * @param string $oldName (previous element name) * @param string $newName (new element name) * * @since 3.0.6 * * @return void */ public function updateJoinedPks($oldName, $newName) { $db = FabrikWorker::getDbo(true); $item = $this->getListModel()->getTable(); $query = $db->getQuery(true); // Update linked lists id. $query->update('#__fabrik_joins')->set('table_key = ' . $db->q($newName)) ->where('join_from_table = ' . $db->q($item->db_table_name))->where('table_key = ' . $db->q($oldName)); $db->setQuery($query); $db->execute(); // Update join pk parameter $query->clear(); $query->select('id')->from('#__fabrik_joins')->where('table_join = ' . $db->q($item->db_table_name)); $db->setQuery($query); $ids = $db->loadColumn(); $testPk = $db->qn($item->db_table_name . '.' . $oldName); $newPk = $db->qn($item->db_table_name . '.' . $newName); foreach ($ids as $id) { $join = FabTable::getInstance('Join', 'FabrikTable'); $join->load($id); $params = new Registry($join->params); if ($params->get('pk') === $testPk) { $params->set('pk', $newPk); $join->params = (string) $params; $join->store(); } } } /** * Is the element a join * * @return bool */ public function isJoin() { return $this->getParams()->get('repeat', false); } /** * Used by inline edit table plugin * If returns yes then it means that there are only two possible options for the * ajax edit, so we should simply toggle to the alternative value and show the * element rendered with that new value (used for yes/no element) * * @deprecated - only called in a deprecated element method * * @return bool */ protected function canToggleValue() { return false; } /** * Encrypt an entire columns worth of data, used when updating an element to encrypted * with existing data in the column * * @return null */ public function encryptColumn() { $secret = $this->config->get('secret'); $listModel = $this->getListModel(); $db = $listModel->getDb(); $tbl = $this->actualTableName(); $name = $this->getElement()->name; $db->setQuery("UPDATE $tbl SET " . $name . " = AES_ENCRYPT(" . $name . ", " . $db->q($secret) . ")"); $db->execute(); } /** * Decrypt an entire columns worth of data, used when updating an element from encrypted to decrypted * with existing data in the column * * @return null */ public function decryptColumn() { // @TODO this query looks right but when going from encrypted blob to decrypted field the values are set to null $secret = $this->config->get('secret'); $listModel = $this->getListModel(); $db = $listModel->getDb(); $tbl = $this->actualTableName(); $name = $this->getElement()->name; $db->setQuery("UPDATE $tbl SET " . $name . " = AES_DECRYPT(" . $name . ", " . $db->q($secret) . ")"); $db->execute(); } /** * PN 19-Jun-11: Construct an element error string. * * @return string */ public function selfDiagnose() { $retStr = ''; $this->_db->setQuery("SELECT COUNT(*) FROM #__fabrik_groups " . "WHERE (id = " . $this->element->group_id . ");"); $group_id = $this->_db->loadResult(); if (!$group_id) { $retStr = 'No valid group assignment'; } elseif (!$this->element->plugin) { $retStr = 'No plugin'; } elseif (!$this->element->label) { $retStr = 'No element label'; } else { $retStr = ''; } return $retStr; } /** * Shortcut to get plugin manager * * @since 3.0b * * @deprecated * * @return object plugin manager */ public function getPluginManager() { return FabrikWorker::getPluginManager(); } /** * When the element is a repeatable join (e.g. db join checkbox) then figure out how many * records have been selected * * @param array $data data * @param object $oJoin join current join * * @since 3.0rc1 * * @return int number of records selected */ public function getJoinRepeatCount($data, $oJoin) { return count(FArrayHelper::getValue($data, $oJoin->table_join . '___id', array())); } /** * When we do ajax requests from the element - as the plugin controller uses the J dispatcher * the element hasn't loaded up itself, so any time you have a function onAjax_doSomething() call this * helper function first to load up the element. Otherwise things like parameters will not be loaded * * @since 3.0rc1 * * @return null */ protected function loadMeForAjax() { $input = $this->app->getInput(); $this->form = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('Form', 'FabrikFEModel'); $formId = $input->getInt('formid'); $this->form->setId($formId); $this->setId($input->getInt('element_id')); $this->list = Factory::getApplication()->bootComponent('com_fabrik')->getMVCFactory()->createModel('List', 'FabrikFEModel'); $this->list->loadFromFormId($formId); $table = $this->list->getTable(true); $table->form_id = $formId; $element = $this->getElement(true); $this->setEditable($this->canUse('form')); } /** * get the element's cell class * * @since 3.0.4 * * @return string css classes */ public function getCellClass() { $params = $this->getParams(); $classes = array(); $classes[] = $this->getFullName(true, false); $classes[] = 'fabrik_element'; $classes[] = 'fabrik_list_' . $this->getListModel()->getId() . '_group_' . $this->getGroupModel()->getId(); $c = $params->get('tablecss_cell_class', ''); if ($c !== '') { $classes[] = $c; } return implode(' ', $classes); } /** * get the elements list heading class * * @since 3.0.4 * * @return string css classes */ public function getHeadingClass() { $classes = array(); $classes[] = 'fabrik_ordercell'; $classes[] = $this->getFullName(true, false); $classes[] = $this->getElement()->id . '_order'; $classes[] = 'fabrik_list_' . $this->getListModel()->getId() . '_group_' . $this->getGroupModel()->getId(); $classes[] = $this->getParams()->get('tablecss_header_class'); return implode(' ', $classes); } /** * convert XML format data into fabrik data (used by web services) * * @param mixed $v data * * @return mixed data */ public function fromXMLFormat($v) { return $v; } /** * Allows the element to pre-process a rows data before and join merging of rows * occurs. Used in calc element to do calcs on actual row rather than merged row * * @param string $data elements data for the current row * @param object $row current row's data * * @since 3.0.5 * * @return string formatted value */ public function preFormatFormJoins($data, $row) { return $data; } /** * Return an array of parameter names which should not get updated if a linked element's parent is saved * notably any parameter which references another element id should be returned in this array * called from admin element model updateChildIds() * see cascadingdropdown element for example * * @return array parameter names to not alter */ public function getFixedChildParameters() { return array(); } /** * Set row class * * @param array &$data row data to set class for * * @return null */ public function setRowClass(&$data) { // first run plugins, which may set row class $listModel = $this->getListModel(); $args = new stdClass; $args->rowClass = ''; $args->data = $data; $pluginResults = \Fabrik\Helpers\Worker::getPluginManager()->runPlugins( 'onElementSetRowClass', $listModel, 'list', $this, $args ); // now check built in "use as row class" $rowClass = $this->getParams()->get('use_as_row_class'); $col = $this->getFullName(true, false); $rawCol = $col . '_raw'; foreach ($data as $groupKey => $group) { for ($i = 0; $i < count($group); $i++) { if ($rowClass == 1) { $c = false; if (isset($data[$groupKey][$i]->data->$rawCol)) { $c = $data[$groupKey][$i]->data->$rawCol; } elseif (isset($data[$groupKey][$i]->data->$col)) { $c = $data[$groupKey][$i]->data->$col; } if ($c !== false) { // added noRowClass and rowClass for use in div templates that need to split those out if (!isset($data[$groupKey][$i]->noRowClass)) { $data[$groupKey][$i]->noRowClass = $data[$groupKey][$i]->class; } if (!isset($data[$groupKey][$i]->rowClasses)) { $data[$groupKey][$i]->rowClasses = []; } $data[$groupKey][$i]->rowClasses[] = FabrikString::getRowClass($c, $this->element->name); // plugins may have already created a rowClass if (isset($data[$groupKey][$i]->pluginRowClass)) { $data[$groupKey][$i]->rowClass = $data[$groupKey][$i]->pluginRowClass . ' ' . implode(' ', $data[$groupKey][$i]->rowClasse); $data[$groupKey][$i]->class = $data[$groupKey][$i]->noRowClass . ' ' . $data[$groupKey][$i]->rowClass; } else { $data[$groupKey][$i]->rowClass = implode(' ', $data[$groupKey][$i]->rowClasses); $data[$groupKey][$i]->class = $data[$groupKey][$i]->noRowClass . ' ' . $data[$groupKey][$i]->rowClass; } } } else { // plugins may have already created a rowClass if (isset($data[$groupKey][$i]->pluginRowClass)) { // added noRowClass and rowClass for use in div templates that need to split those out if (!isset($data[$groupKey][$i]->noRowClass)) { $data[$groupKey][$i]->noRowClass = $data[$groupKey][$i]->class; } $data[$groupKey][$i]->class = $data[$groupKey][$i]->noRowClass . ' ' . $data[$groupKey][$i]->pluginRowClass; } } } } } /** * Set row class * * @param object $element element object * * @return null */ public function getRowClassRO($element) { $rowClass = $this->getParams()->get('use_as_row_class', '0'); if ($rowClass === '1') { return FabrikString::getRowClass($element->value, $this->element->name); } return ''; } /** * Unset the element models access * * @return null */ public function clearAccess() { unset($this->access); } /** * Forces reset of defaults, etc. * * @return null */ public function reset() { $this->defaults = null; } /** * Clear default values, need to call this if we change an elements value in any of the formData * arrays during submission process. * * @return null */ public function clearDefaults() { $this->defaults = null; } /** * Should the 'label' field be quoted. Overridden by databasejoin and extended classes, * which may use a CONCAT'ed label which mustn't be quoted. * * @since 3.0.6 * * @return boolean */ protected function quoteLabel() { return true; } /** * Create the where part for the query that selects the list options * * @param array $data Current row data to use in placeholder replacements * @param bool $incWhere Should the additional user defined WHERE statement be included * @param string $thisTableAlias Db table alias * @param array $opts Options * @param JDatabaseQuery|bool $query Append where to JDatabaseQuery object or return string (false) * * @return string|JDatabaseQuery */ protected function buildQueryWhere($data = array(), $incWhere = true, $thisTableAlias = null, $opts = array(), $query = false) { return ''; } /** * Is the element set to always render in list contexts * * @param bool $not_shown_only Not sure??? * * @return bool */ public function isAlwaysRender($not_shown_only = true) { $params = $this->getParams(); $element = $this->getElement(); $alwaysRender = $params->get('always_render', '0'); return $not_shown_only ? $element->show_in_list_summary == 0 && $alwaysRender == '1' : $alwaysRender == '1'; } /** * Called at end of form record save. Used for many-many join elements to save their data * * @param array &$data Form data * * @since 3.1rc1 * * @return void */ public function onFinalStoreRow(&$data) { if (!$this->isJoin()) { return; } $groupModel = $this->getGroupModel(); $listModel = $this->getListModel(); $db = $listModel->getDb(); $query = $db->getQuery(true); $formData =& $this->getFormModel()->formDataWithTableName; // I set this to raw for cdd. $name = $this->getFullName(true, false); $ajaxSubmit = $this->app->getInput()->get('fabrik_ajax'); $rawName = $name . '_raw'; $shortName = $this->getElement()->name; $join = $this->getJoin(); /* * The submitted element's values * * NOTE - if we are coming from a list row copy, the _raw data is actually the map table * id's, not the FK's, because we used form model getData to load it. So we need to stuff the * actual FK's (in the _id array) back into _raw */ if (array_key_exists('fabrik_copy_from_table', $formData)) { $idName = $name . '_id'; $idValues = FArrayHelper::getValue($formData, $idName, array()); if (!empty($idValues)) { $formData[$rawName] = $idValues; } } $d = FArrayHelper::getValue($formData, $rawName, FArrayHelper::getValue($formData, $name)); // set $emptyish to false so if no selection, we don't save a bogus empty row $allJoinValues = FabrikWorker::JSONtoData($d, true, false); if ($groupModel->isJoin()) { $groupJoinModel = $groupModel->getJoinModel(); $k = str_replace('`', '', str_replace('.', '___', $groupJoinModel->getJoin()->params->get('pk'))); $parentIds = (array) $formData[$k]; } else { $k = 'rowid'; $parentIds = empty($allJoinValues) ? array() : FArrayHelper::array_fill(0, count($allJoinValues), $formData[$k]); } $paramsKey = $this->getJoinParamsKey(); $allParams = (array) FArrayHelper::getValue($formData, $paramsKey, array()); $allParams = array_values($allParams); $i = 0; $idsToKeep = array(); foreach ($parentIds as $parentId) { if (!array_key_exists($parentId, $idsToKeep)) { $idsToKeep[$parentId] = array(); } if ($groupModel->canRepeat()) { $joinValues = FArrayHelper::getValue($allJoinValues, $i, array()); } else { $joinValues = $allJoinValues; } $joinValues = (array) $joinValues; // Get existing records if ($parentId == '') { $ids = array(); } else { $query->clear(); $query->select('id, ' . $shortName)->from($join->table_join)->where('parent_id = ' . $parentId); $db->setQuery($query); $ids = (array) $db->loadObjectList($shortName); } // If doing an ajax form submit and the element is an ajax file upload then its data is different. if (get_class($this) === 'PlgFabrik_ElementFileupload' && $ajaxSubmit) { $allParams = array_key_exists('crop', $joinValues) ? array_values($joinValues['crop']) : array(); $joinValues = array_key_exists('id', $joinValues) ? array_keys($joinValues['id']) : $joinValues; } foreach ($joinValues as $jIndex => $jid) { $record = new stdClass; $record->parent_id = $parentId; $fkVal = FArrayHelper::getValue($joinValues, $jIndex); $record->$shortName = $fkVal; $record->params = FArrayHelper::getValue($allParams, $jIndex); // Stop notice with file-upload where fkVal is an array if (array_key_exists($fkVal, $ids)) { $record->id = $ids[$fkVal]->id; $idsToKeep[$parentId][] = $record->id; } else { $record->id = 0; } if ($record->id == 0) { $ok = $listModel->insertObject($join->table_join, $record); $lastInsertId = $listModel->getDb()->insertid(); if (!$this->allowDuplicates) { $newId = new stdClass; $newId->id = $lastInsertId; $newId->$shortName = $record->$shortName; $ids[$record->$shortName] = $newId; } $idsToKeep[$parentId][] = $lastInsertId; } else { $ok = $listModel->updateObject($join->table_join, $record, 'id'); } if (!$ok) { throw new RuntimeException('Didn\'t save dbjoined repeat element'); } } $i++; } // Delete any records that were unselected. $this->deleteDeselectedItems($idsToKeep, $k); } /** * For joins: * Get the key which contains the linking tables primary key values. * * @param bool $step Use step '___' or '.' in full name * * @return boolean|string */ public function getJoinIdKey($step = true) { if (!$this->isJoin()) { return false; } if ($this->getGroupModel()->isJoin()) { $join = $this->getJoin(); $idKey = $join->table_join . '___id'; } else { $idKey = $this->getFullName($step, false) . '___id'; } return $idKey; } /** * For joins: * Get the key which contains the linking tables params values. * * @param bool $step Use step '___' or '.' in full name * * @return boolean|string */ public function getJoinParamsKey($step = true) { if (!$this->isJoin()) { return false; } if ($this->getGroupModel()->isJoin()) { $join = $this->getJoin(); $paramsKey = $join->table_join . '___params'; } else { $paramsKey = $this->getFullName($step, false) . '___params'; } return $paramsKey; } /** * Delete any deselected items from the cross-reference table * * @param array $idsToKeep List of ids to keep * @param string $k Parent record key name * * @return void */ protected function deleteDeselectedItems($idsToKeep, $k) { $listModel = $this->getListModel(); $join = $this->getJoin(); $db = $listModel->getDb(); $query = $db->getQuery(true); if (empty($idsToKeep)) { $formData = $this->getFormModel()->formDataWithTableName; $parentId = $formData[$k]; if (!empty($parentId)) { $query->delete($join->table_join)->where('parent_id = ' . $db->q($parentId)); $db->setQuery($query); $db->execute(); } } else { foreach ($idsToKeep as $parentId => $ids) { $query->clear(); $query->delete($join->table_join)->where($db->quoteName('parent_id') . ' = ' . $parentId); if (!empty($ids)) { $query->where($db->quoteName('id') . ' NOT IN ( ' . implode(',', $ids) . ')'); } $db->setQuery($query); $db->execute(); } } } /** * Return an internal validation icon - e.g. for Password element * * @return string */ public function internalValidationIcon() { return ''; } /** * Return internal validation hover text - e.g. for Password element * * @return string */ public function internalValidationText() { return ''; } /** * Return JS event required to trigger a 'change', usually 'change', * but some elements need a 'click' or a 'blur'. Used initially by CDD element. * NOTE - there is also a getChangeEvent() in element.js, which should return the same thing, Don't Ask. * * @return string */ public function getChangeEvent() { return 'change'; } /** * Returns class name to use for advanced select (no surrounding space), or empty string * * @return bool */ public function getAdvancedSelectClass() { $fbConfig = ComponentHelper::getParams('com_fabrik'); $params = $this->getParams(); $advancedClass = ''; $globalAdvanced = (int) $fbConfig->get('advanced_behavior', '0'); if ($globalAdvanced !== 0) { $advancedClass = $params->get('advanced_behavior', '0') == '1' || $globalAdvanced === 2 ? 'advancedSelect' : ''; } return $advancedClass; } /** * Get the element's LayoutInterface file * Its actually an instance of LayoutFile which inverses the ordering added include paths. * In LayoutFile the addedPath takes precedence over the default paths, which makes more sense! * * @param string $type form/details/list * @param array $paths Optional paths to add as includes * * @return LayoutFile */ public function getLayout($type, $paths = array(), $options = array()) { $defaultOptions = array('debug' => false, 'component' => 'com_fabrik', 'client' => 'site'); $options = array_merge($defaultOptions, $options); $basePath = $this->layoutBasePath(); $layout = new LayoutFile('fabrik-element-' . $this->getPluginName() . '-' . $type, $basePath, $options); foreach ($paths as $path) { $layout->addIncludePath($path); } $layout->addIncludePaths(JPATH_SITE . '/layouts'); $layout->addIncludePaths(JPATH_THEMES . '/' . $this->app->getTemplate() . '/html/layouts'); $layout->addIncludePaths(JPATH_THEMES . '/' . $this->app->getTemplate() . '/html/layouts/com_fabrik'); $layout->addIncludePaths(JPATH_THEMES . '/' . $this->app->getTemplate() . '/html/layouts/com_fabrik/element/'); // Custom per element layout... $layout->addIncludePaths(JPATH_THEMES . '/' . $this->app->getTemplate() . '/html/layouts/com_fabrik/element/' . $this->getFullName(true, false)); // Custom per template layout $view = $this->getFormModel()->isEditable() ? 'form' : 'details'; $layout->addIncludePaths(COM_FABRIK_FRONTEND . '/views/'. $view . '/tmpl/' . $this->getFormModel()->getTmpl() . '/layouts/element/'); $layout->addIncludePaths(COM_FABRIK_FRONTEND . '/views/'. $view . '/tmpl/' . $this->getFormModel()->getTmpl() . '/layouts/element/' . $this->getFullName(true, false)); return $layout; } /** * Get the LayoutInterface base path for the plugin's layout files. * * @return string */ protected function layoutBasePath() { return COM_FABRIK_BASE . '/plugins/fabrik_element/' . $this->getPluginName() . '/layouts'; } /** * Get lower case plugin name based off class name: * E.g. PlgFabrik_ElementDatabasejoin => databasejoin * * @return string */ protected function getPluginName() { $name = get_class($this); if (strstr($name, '\\')) { $name = explode('\\', $name); $name = array_pop($name); } return strtolower(StringHelper::str_ireplace('PlgFabrik_Element', '', $name)); } /** * Validate teh element against a Joomla form Rule * * @param string $type Rule type e.g. 'password' * @param mixed $value Value to validate * @param mixed $path Optional path to load teh rule from * * @throws Exception * * @return bool */ protected function validateJRule($type, $value, $path = null) { if (!is_null($path)) { FormHelper::addRulePath($path); } $rule = FormHelper::loadRuleType($type, true); $xml = new SimpleXMLElement('<xml></xml>'); $this->lang->load('com_users'); if (!$rule->test($xml, $value)) { $this->validationError = ''; foreach ($this->app->getMessageQueue() as $i => $msg) { if ($msg['type'] === 'warning') { $this->validationError .= $msg['message'] . '<br />'; } } FabrikWorker::killMessage($this->app, 'warning'); return false; } return true; } /** * Say an element is in a repeat group, this method gets that repeat groups primary * key value. * * @param int $repeatCounter * * @since 3.3.2 * * @return mixed False if not in a join */ protected function getJoinedGroupPkVal($repeatCounter = 0) { $groupModel = $this->getGroupModel(); if (!$groupModel->isJoin()) { return false; } $formModel = $this->getFormModel(); $joinModel = $groupModel->getJoinModel(); $elementModel = $formModel->getElement($joinModel->getForeignID()); return $elementModel->getValue($formModel->data, $repeatCounter); } /** * Is the element published. * * @return boolean */ public function isPublished() { return $this->getElement()->published === 1; } /** * Add any jsJLayout templates to Fabrik.jLayouts js object. * * @return void */ public function jsJLayout() { } /** * Give elements a chance to reset data after a failed validation. For instance, file upload element * needs to reset the values as they aren't submitted with the form * * @param $data array form data */ public function setValidationFailedData(&$data) { return; } /** * Swap values for labels * * @param array &$d Data * * @return void */ protected function swapValuesForLabels(&$d) { $groups = $this->getFormModel()->getGroupsHiarachy(); foreach (array_keys($groups) as $gkey) { $group = $groups[$gkey]; $elementModels = $group->getPublishedElements(); for ($j = 0; $j < count($elementModels); $j++) { $elementModel = $elementModels[$j]; $elKey = $elementModel->getFullName(true, false); $v = FArrayHelper::getValue($d, $elKey); if (is_array($v)) { $origData = FArrayHelper::getValue($d, $elKey, array()); if (!array_key_exists($elKey . '_raw', $d)) { $d[$elKey . '_raw'] = $origData; } foreach (array_keys($v) as $x) { $origVal = FArrayHelper::getValue($origData, $x); $d[$elKey][$x] = $elementModel->getLabelForValue($v[$x], $origVal, true); } } else { $origData = FArrayHelper::getValue($d, $elKey); if (!array_key_exists($elKey . '_raw', $d)) { $d[$elKey . '_raw'] = $origData; } $d[$elKey] = $elementModel->getLabelForValue($v, $origData, true); } } } } /** * When running parseMessageForPlaceholder on data we need to set the none-raw value of things like birthday/time * elements to that stored in the listModel::storeRow() method * * @param array &$data Form data * @param int $repeatCounter Repeat group counter * * @return void */ protected function setStoreDatabaseFormat(&$data, $repeatCounter = 0) { $formModel = $this->getFormModel(); $groups = $formModel->getGroupsHiarachy(); foreach ($groups as $groupModel) { $elementModels = $groupModel->getPublishedElements(); foreach ($elementModels as $elementModel) { $fullKey = $elementModel->getFullName(true, false); $value = $data[$fullKey]; if ($groupModel->canRepeat() && is_array($value)) { foreach ($value as $k => $v) { $data[$fullKey][$k] = $elementModel->storeDatabaseFormat($v, $data); } } else { $data[$fullKey] = $elementModel->storeDatabaseFormat($value, $data); } } } } /* Convert the older BS2 & BS3 column classes to BS5 * * @param string $oldClass class to convert * * @return string BS5 class */ protected function getBsClass($oldClass='') { $classList = [ 'input-mini' => 'col-sm-2', 'input-small' => 'col-sm-4', 'input-medium' => 'col-sm-6', 'input-large' => 'col-sm-8', 'input-xlarge' => 'col-sm-10', 'input-xxlarge' => 'col-sm-12', 'input-block-level' => 'col-sm-12', ]; // Some plugins have no format setting, but they should. If not then we use col-sm-10, 2 grids are used for the label $bsClass = $oldClass == '' ? $this->getParams()->get('bootstrap_class', 'col-sm-10') : $oldClass; // check for old col-sm and span classes $bsClass = str_replace(['col-md-', 'span'], 'col-sm-', $bsClass); if (array_key_exists($bsClass, $classList)) { $bsClass = $classList[$bsClass]; } if (strlen($bsClass) > 0) $bsClass = ' '.$bsClass; return $bsClass; } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0.21 |
proxy
|
phpinfo
|
Настройка