diff --git a/system/modules/listing/dca/tl_module.php b/system/modules/listing/dca/tl_module.php index 79dcf0dbc4..4893de6fb3 100644 --- a/system/modules/listing/dca/tl_module.php +++ b/system/modules/listing/dca/tl_module.php @@ -71,7 +71,7 @@ 'label' => &$GLOBALS['TL_LANG']['tl_module']['list_info'], 'exclude' => true, 'inputType' => 'text', - 'eval' => array('maxlength'=>255, 'tl_class'=>'w50'), + 'eval' => array('maxlength'=>255, 'decodeEntities'=>true, 'tl_class'=>'w50'), 'sql' => "varchar(255) NOT NULL default ''" ); diff --git a/system/modules/listing/modules/ModuleListing.php b/system/modules/listing/modules/ModuleListing.php index a45d9ab454..ad706cd920 100644 --- a/system/modules/listing/modules/ModuleListing.php +++ b/system/modules/listing/modules/ModuleListing.php @@ -80,6 +80,7 @@ public function generate() $this->strTemplate = $this->list_layout; $this->list_where = $this->replaceInsertTags($this->list_where); + $this->list_info_where = $this->replaceInsertTags($this->list_info_where); return parent::generate(); } @@ -105,9 +106,16 @@ protected function compile() * Add the search menu */ $strWhere = ''; + $strOuterWhere = ''; $varKeyword = ''; $strOptions = ''; + // moved in front of search args, added () to protect OR, see #6337 + if ($this->list_where) + { + $strWhere .= " WHERE (" . $this->list_where . ")"; + } + $this->Template->searchable = false; $arrSearchFields = trimsplit(',', $this->list_search); @@ -117,8 +125,31 @@ protected function compile() if (\Input::get('search') && \Input::get('for')) { + $searchCol = \Input::get('search'); $varKeyword = '%' . \Input::get('for') . '%'; - $strWhere = (!$this->list_where ? " WHERE " : " AND ") . \Input::get('search') . " LIKE ?"; + + // try to determine if search column is virtual, see #6337 + // algorithm is a bit daring but worked for many test cases + $strCols = ',' . $this->list_fields . ','; + // eliminate (...), instead of a risky loop do 3x: + $strCols = preg_replace('/[a-zA-Z0-9_]*\([^()]*\)/', 'X', $strCols); + $strCols = preg_replace('/[a-zA-Z0-9_]*\([^()]*\)/', 'X', $strCols); + $strCols = preg_replace('/[a-zA-Z0-9_]*\([^()]*\)/', 'X', $strCols); + $strCols = preg_replace('/,\s*/', ',', strrev($strCols)); + $strCols = preg_replace('/,"?([a-z0-9_]+)"? +/i', ',!\1!', $strCols); + $strCols = utf8_strtolower(strrev($strCols)); + $searchCol = utf8_strtolower($searchCol); + if (preg_match_all('/!([^!]+)!,/', $strCols, $arrVcols) > 0) + { + if (in_array($searchCol, $arrVcols[1])) + { + $strOuterWhere = " WHERE " . \Input::get('search') . " LIKE ?"; + } + } + if (!$strOuterWhere) + { + $strWhere .= (!$strWhere ? " WHERE " : " AND ") . \Input::get('search') . " LIKE ?"; + } } foreach ($arrSearchFields as $field) @@ -133,14 +164,15 @@ protected function compile() /** * Get the total number of records */ - $strQuery = "SELECT COUNT(*) AS count FROM " . $this->list_table; - - if ($this->list_where) + $strQuery = "SELECT COUNT(*) AS count FROM "; + if ($strOuterWhere) { - $strQuery .= " WHERE " . $this->list_where; + $strQuery .= "(SELECT " . $this->list_fields . " FROM " . $this->list_table . $strWhere . ") t " . $strOuterWhere; + } + else + { + $strQuery .= $this->list_table . $strWhere; } - - $strQuery .= $strWhere; $objTotal = $this->Database->prepare($strQuery)->execute($varKeyword); @@ -173,14 +205,24 @@ protected function compile() /** * Get the selected records */ - $strQuery = "SELECT " . $this->strPk . "," . $this->list_fields . " FROM " . $this->list_table; + // help detect PK in column list - may fail if PK is used in function argument + // in simple SELECT, double ID col is tolerated, not so if subselect is used + $tmpFields = trimsplit(',', $this->list_fields); + $blnPkInList = in_array($this->strPk, $tmpFields) && $strOuterWhere; - if ($this->list_where) + $strDetailHint = ''; + + if ($this->list_info_where && $this->list_info) { - $strQuery .= " WHERE " . $this->list_where; + // add virtual column to tell if detail link needed + $strDetailHint = ",(SELECT 1 FROM " . $this->list_table . " b WHERE b.id = a.id AND " . $this->list_info_where . ") as _detail_hint"; + } + $strQuery = "SELECT " . ($blnPkInList ? "" : $this->strPk . ",") . $this->list_fields . $strDetailHint . " FROM " . $this->list_table ." a" . $strWhere; + // take care of search for virtual column (see #6337) + if ($strOuterWhere) + { + $strQuery = "SELECT * FROM (" . $strQuery .") t " . $strOuterWhere; } - - $strQuery .= $strWhere; // Order by if (\Input::get('order_by')) @@ -231,50 +273,23 @@ protected function compile() */ $arrTh = array(); $arrTd = array(); - $arrFields = trimsplit(',', $this->list_fields); - - // THEAD - for ($i=0, $c=count($arrFields); $i<$c; $i++) - { - // Never show passwords - if ($GLOBALS['TL_DCA'][$this->list_table]['fields'][$arrFields[$i]]['inputType'] == 'password') - { - continue; - } - - $class = ''; - $sort = 'asc'; - $strField = strlen($label = $GLOBALS['TL_DCA'][$this->list_table]['fields'][$arrFields[$i]]['label'][0]) ? $label : $arrFields[$i]; - - // Add a CSS class to the order_by column - if (\Input::get('order_by') == $arrFields[$i]) - { - $sort = (\Input::get('sort') == 'asc') ? 'desc' : 'asc'; - $class = ' sorted ' . \Input::get('sort'); - } - - $arrTh[] = array - ( - 'link' => $strField, - 'href' => (ampersand($strUrl) . $strVarConnector . 'order_by=' . $arrFields[$i]) . '&sort=' . $sort, - 'title' => specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['list_orderBy'], $strField)), - 'class' => $class . (($i == 0) ? ' col_first' : '') //. ((($i + 1) == count($arrFields)) ? ' col_last' : '') - ); - } + $arrFields = array(); // will collect the column names/aliases, see #6337 $j = 0; $arrRows = $objData->fetchAllAssoc(); - // TBODY + // TBODY. for ($i=0, $c=count($arrRows); $i<$c; $i++) { $j = 0; $class = 'row_' . $i . (($i == 0) ? ' row_first' : '') . ((($i + 1) == count($arrRows)) ? ' row_last' : '') . ((($i % 2) == 0) ? ' even' : ' odd'); + $blnDetailHint = empty($strDetailHint) || !empty($arrRows[$i]['_detail_hint']); + foreach ($arrRows[$i] as $k=>$v) { // Skip the primary key - if ($k == $this->strPk && !in_array($this->strPk, $arrFields)) + if ($k == $this->strPk && !in_array($this->strPk, $tmpFields)) { continue; } @@ -285,24 +300,59 @@ protected function compile() continue; } + // skip the detail hint (see #6332) + if ($k == '_detail_hint') + { + continue; + } + + // collect column names for header (see #6337) + if ($i == 0) + { + $arrFields[] =$k; + } + $value = $this->formatValue($k, $v); $arrTd[$class][$k] = array ( 'raw' => $v, 'content' => ($value ? $value : ' '), - 'class' => 'col_' . $j . (($j++ == 0) ? ' col_first' : '') . ($this->list_info ? '' : (($j >= (count($arrRows[$i]) - 1)) ? ' col_last' : '')), + 'class' => 'col_' . $j . (($j++ == 0) ? ' col_first' : '') . ($this->list_info ? '' : (($j >= (count($arrRows[$i]) - 1)) ? ' col_last' : '')) . (is_numeric($v)? ' numeric' : ''), 'id' => $arrRows[$i][$this->strPk], 'field' => $k, - 'url' => $strUrl . $strVarConnector . 'show=' . $arrRows[$i][$this->strPk] + 'url' => $blnDetailHint ? $strUrl . $strVarConnector . 'show=' . $arrRows[$i][$this->strPk] : '' ); } } + // THEAD + // uses collected column names from TBODY + for ($i=0, $c=count($arrFields); $i<$c; $i++) + { + $class = ''; + $sort = 'asc'; + $strField = strlen($label = $GLOBALS['TL_DCA'][$this->list_table]['fields'][$arrFields[$i]]['label'][0]) ? $label : ucfirst($arrFields[$i]); + + // Add a CSS class to the order_by column + if (\Input::get('order_by') == $arrFields[$i]) + { + $sort = (\Input::get('sort') == 'asc') ? 'desc' : 'asc'; + $class = ' sorted ' . \Input::get('sort'); + } + + $arrTh[] = array + ( + 'link' => $strField, + 'href' => (ampersand($strUrl) . $strVarConnector . 'order_by=' . $arrFields[$i]) . '&sort=' . $sort, + 'title' => specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['list_orderBy'], $strField)), + 'class' => $class . (($i == 0) ? ' col_first' : '') //. ((($i + 1) == count($arrFields)) ? ' col_last' : '') + ); + } + $this->Template->thead = $arrTh; $this->Template->tbody = $arrTd; - /** * Pagination */ @@ -347,9 +397,13 @@ protected function listSingleRecord($id) $this->Template->referer = 'javascript:history.go(-1)'; $this->Template->back = $GLOBALS['TL_LANG']['MSC']['goBack']; $this->list_info = deserialize($this->list_info); - $this->list_info_where = $this->replaceInsertTags($this->list_info_where); + ##$this->list_info_where = $this->replaceInsertTags($this->list_info_where); - $objRecord = $this->Database->prepare("SELECT " . $this->list_info . " FROM " . $this->list_table . " WHERE " . (($this->list_info_where != '') ? $this->list_info_where . " AND " : "") . $this->strPk . "=?") + $objRecord = $this->Database->prepare(" SELECT " . $this->list_info + . " FROM " . $this->list_table + . " WHERE " . (($this->list_info_where != '') + ? "(" . $this->list_info_where . ") AND " + : "") . $this->strPk . "=?") ->limit(1) ->execute($id); diff --git a/system/modules/listing/templates/listing/list_default.html5 b/system/modules/listing/templates/listing/list_default.html5 index 5f95a65dd8..f43a463220 100644 --- a/system/modules/listing/templates/listing/list_default.html5 +++ b/system/modules/listing/templates/listing/list_default.html5 @@ -66,7 +66,7 @@