diff --git a/htdocs/core/customreports.php b/htdocs/core/customreports.php index 496489d4a37..76891ad012e 100644 --- a/htdocs/core/customreports.php +++ b/htdocs/core/customreports.php @@ -133,6 +133,7 @@ if ($reshook < 0) { } } +// Load the main $object for statistics if ($objecttype) { try { if (!empty($arrayoftype[$objecttype]['ClassPath'])) { @@ -172,6 +173,83 @@ $search_component_params_input = trim(GETPOST('search_component_params_input', ' //var_dump($search_component_params_hidden); //var_dump($search_component_params_input); +// If string is not an universal filter string, we try to convert it into universal filter syntax string +$errorstr = ''; +forgeSQLFromUniversalSearchCriteria($search_component_params_input, $errorstr); // Try converstion UFS->SQL +//var_dump($errorstr); +if ($errorstr) { + $value = $search_component_params_input; + + $value = preg_replace('/([a-z\.]+)\s*([!<>=]+|in|notin|like|notlike)\s*/', '\1:\2:', $value); // Clean string 'x < 10' into 'x:<:10' so we can then explode on space to get all AND tests to do + $value = preg_replace('/\s*\|\s*/', '|', $value); + //var_dump($value); + + $crits = explode(' ', trim($value)); // the string after the name of the field. Explode on each AND + $res = ''; + + $i1 = 0; // count the nb of and criteria added (all fields / criteria) + foreach ($crits as $crit) { // Loop on each AND criteria + $crit = trim($crit); + + $i2 = 0; // count the nb of valid criteria added for this first criteria + $newres = ''; + $tmpcrits = explode('|', $crit); + $i3 = 0; // count the nb of valid criteria added for this current field + foreach ($tmpcrits as $tmpcrit) { + if ($tmpcrit !== '0' && empty($tmpcrit)) { + continue; + } + $tmpcrit = trim($tmpcrit); + //var_dump($tmpcrit); + + $errorstr = ''; + $parenthesislevel = 0; + $rescheckfilter = dolCheckFilters($tmpcrit, $errorstr, $parenthesislevel); + if ($rescheckfilter) { + while ($parenthesislevel > 0) { + $tmpcrit = preg_replace('/^\(/', '', preg_replace('/\)$/', '', $tmpcrit)); + $parenthesislevel--; + } + } + + $field = preg_replace('/(:[!<>=\s]+:|:in:|:notin:|:like:|:notlike:).*$/', '', $tmpcrit); // the name of the field + $tmpcrit = preg_replace('/^.*(:[!<>=\s]+:|:in:|:notin:|:like:|:notlike:)/', '\1', $tmpcrit); // the condition after the name of the field + //var_dump($field); var_dump($tmpcrit); var_dump($i3); + + $newres .= (($i2 > 0 || $i3 > 0) ? ' OR ' : ''); + + $operator = '='; + $newcrit = preg_replace('/(:[!<>=\s]+:|:in:|:notin:|:like:|:notlike:)/', '', $tmpcrit); + //var_dump($newcrit); + + $reg = array(); + preg_match('/:([!<>=\s]+|in|notin|like|notlike):/', $tmpcrit, $reg); + if (!empty($reg[1])) { + $operator = $reg[1]; + } + if ($newcrit != '') { + if (!preg_match('/^\'[^\']*\'$/', $newcrit)) { + $numnewcrit = price2num($newcrit); + $newres .= '('.$field.':'.$operator.':'.((float) $numnewcrit).')'; + } else { + $newres .= '('.$field.':'.$operator.":".((string) $newcrit).')'; + } + $i3++; // a criteria was added to string + } + } + $i2++; // a criteria for 1 more field was added to string + + if ($newres) { + $res = $res.($res ? ' AND ' : '').($i2 > 1 ? '(' : '').$newres.($i2 > 1 ? ')' : ''); + } + $i1++; + } + $res = "(".$res.")"; + + //var_dump($res);exit; + $search_component_params_input = $res; +} + $arrayofandtagshidden = dolForgeExplodeAnd($search_component_params_hidden); $arrayofandtagsinput = dolForgeExplodeAnd($search_component_params_input); @@ -811,7 +889,10 @@ if (!empty($search_measures) && !empty($search_xaxis)) { //print $sql; if ($errormessage) { + print '
'; print dol_escape_htmltag($errormessage); + //print '
'.dol_escape_htmltag('SQL is '.$sql); + print '
'; $sql = ''; } @@ -830,119 +911,122 @@ $data = array(); if ($sql) { $resql = $db->query($sql); if (!$resql) { - dol_print_error($db); - } + print '
'; + print dol_escape_htmltag($db->lasterror()); + //print '
'.dol_escape_htmltag('SQL is '.$sql); + print '
'; + } else { + $ifetch = 0; + $xi = 0; + $oldlabeltouse = ''; + while ($obj = $db->fetch_object($resql)) { + $ifetch++; + if ($useagroupby) { + $xval = $search_xaxis[0]; + $fieldforxkey = 'x_0'; + $xlabel = $obj->$fieldforxkey; + $xvalwithoutprefix = preg_replace('/^[a-z]+\./', '', $xval); - $ifetch = 0; - $xi = 0; - $oldlabeltouse = ''; - while ($obj = $db->fetch_object($resql)) { - $ifetch++; - if ($useagroupby) { - $xval = $search_xaxis[0]; - $fieldforxkey = 'x_0'; - $xlabel = $obj->$fieldforxkey; - $xvalwithoutprefix = preg_replace('/^[a-z]+\./', '', $xval); - - // Define $xlabel - if (!empty($object->fields[$xvalwithoutprefix]['arrayofkeyval'])) { - $xlabel = $object->fields[$xvalwithoutprefix]['arrayofkeyval'][$obj->$fieldforxkey]; - } - $labeltouse = (($xlabel || $xlabel == '0') ? dol_trunc($xlabel, 20, 'middle') : ($xlabel === '' ? $langs->transnoentitiesnoconv("Empty") : $langs->transnoentitiesnoconv("NotDefined"))); - - if ($oldlabeltouse && ($labeltouse != $oldlabeltouse)) { - $xi++; // Increase $xi - } - //var_dump($labeltouse.' '.$oldlabeltouse.' '.$xi); - $oldlabeltouse = $labeltouse; - - /* Example of value for $arrayofvaluesforgroupby - * array (size=1) - * 'g_0' => - * array (size=6) - * 0 => string '0' (length=1) - * '' => string 'Empty' (length=5) - * '__NULL__' => string 'Not defined' (length=11) - * 'done' => string 'done' (length=4) - * 'processing' => string 'processing' (length=10) - * 'undeployed' => string 'undeployed' (length=10) - */ - foreach ($search_measures as $key => $val) { - $gi = 0; - foreach ($search_groupby as $gkey) { - //var_dump('*** Fetch #'.$ifetch.' for labeltouse='.$labeltouse.' measure number '.$key.' and group g_'.$gi); - //var_dump($arrayofvaluesforgroupby); - foreach ($arrayofvaluesforgroupby['g_'.$gi] as $gvaluepossiblekey => $gvaluepossiblelabel) { - $ykeysuffix = $gvaluepossiblelabel; - $gvalwithoutprefix = preg_replace('/^[a-z]+\./', '', $gval); - - $fieldfory = 'y_'.$key; - $fieldforg = 'g_'.$gi; - $fieldforybis = 'y_'.$key.'_'.$ykeysuffix; - //var_dump('gvaluepossiblekey='.$gvaluepossiblekey.' gvaluepossiblelabel='.$gvaluepossiblelabel.' ykeysuffix='.$ykeysuffix.' gval='.$gval.' gvalwithoutsuffix='.$gvalwithoutprefix); - //var_dump('fieldforg='.$fieldforg.' obj->$fieldforg='.$obj->$fieldforg.' fieldfory='.$fieldfory.' obj->$fieldfory='.$obj->$fieldfory.' fieldforybis='.$fieldforybis); - - if (!is_array($data[$xi])) { - $data[$xi] = array(); - } - - if (!array_key_exists('label', $data[$xi])) { - $data[$xi] = array(); - $data[$xi]['label'] = $labeltouse; - } - - $objfieldforg = $obj->$fieldforg; - if (is_null($objfieldforg)) { - $objfieldforg = '__NULL__'; - } - - if ($gvaluepossiblekey == '0') { // $gvaluepossiblekey can have type int or string. So we create a special if, used when value is '0' - //var_dump($objfieldforg.' == \'0\' -> '.($objfieldforg == '0')); - if ($objfieldforg == '0') { - // The record we fetch is for this group - $data[$xi][$fieldforybis] = $obj->$fieldfory; - } elseif (!isset($data[$xi][$fieldforybis])) { - // The record we fetch is not for this group - $data[$xi][$fieldforybis] = '0'; - } - } else { - //var_dump((string) $objfieldforg.' === '.(string) $gvaluepossiblekey.' -> '.((string) $objfieldforg === (string) $gvaluepossiblekey)); - if ((string) $objfieldforg === (string) $gvaluepossiblekey) { - // The record we fetch is for this group - $data[$xi][$fieldforybis] = $obj->$fieldfory; - } elseif (!isset($data[$xi][$fieldforybis])) { - // The record we fetch is not for this group - $data[$xi][$fieldforybis] = '0'; - } - } - } - //var_dump($data[$xi]); - $gi++; + // Define $xlabel + if (!empty($object->fields[$xvalwithoutprefix]['arrayofkeyval'])) { + $xlabel = $object->fields[$xvalwithoutprefix]['arrayofkeyval'][$obj->$fieldforxkey]; } - } - } else { // No group by - $xval = $search_xaxis[0]; - $fieldforxkey = 'x_0'; - $xlabel = $obj->$fieldforxkey; - $xvalwithoutprefix = preg_replace('/^[a-z]+\./', '', $xval); + $labeltouse = (($xlabel || $xlabel == '0') ? dol_trunc($xlabel, 20, 'middle') : ($xlabel === '' ? $langs->transnoentitiesnoconv("Empty") : $langs->transnoentitiesnoconv("NotDefined"))); - // Define $xlabel - if (!empty($object->fields[$xvalwithoutprefix]['arrayofkeyval'])) { - $xlabel = $object->fields[$xvalwithoutprefix]['arrayofkeyval'][$obj->$fieldforxkey]; - } + if ($oldlabeltouse && ($labeltouse != $oldlabeltouse)) { + $xi++; // Increase $xi + } + //var_dump($labeltouse.' '.$oldlabeltouse.' '.$xi); + $oldlabeltouse = $labeltouse; - $labeltouse = (($xlabel || $xlabel == '0') ? dol_trunc($xlabel, 20, 'middle') : ($xlabel === '' ? $langs->transnoentitiesnoconv("Empty") : $langs->transnoentitiesnoconv("NotDefined"))); - $xarrayforallseries = array('label' => $labeltouse); - foreach ($search_measures as $key => $val) { - $fieldfory = 'y_'.$key; - $xarrayforallseries[$fieldfory] = $obj->$fieldfory; + /* Example of value for $arrayofvaluesforgroupby + * array (size=1) + * 'g_0' => + * array (size=6) + * 0 => string '0' (length=1) + * '' => string 'Empty' (length=5) + * '__NULL__' => string 'Not defined' (length=11) + * 'done' => string 'done' (length=4) + * 'processing' => string 'processing' (length=10) + * 'undeployed' => string 'undeployed' (length=10) + */ + foreach ($search_measures as $key => $val) { + $gi = 0; + foreach ($search_groupby as $gkey) { + //var_dump('*** Fetch #'.$ifetch.' for labeltouse='.$labeltouse.' measure number '.$key.' and group g_'.$gi); + //var_dump($arrayofvaluesforgroupby); + foreach ($arrayofvaluesforgroupby['g_'.$gi] as $gvaluepossiblekey => $gvaluepossiblelabel) { + $ykeysuffix = $gvaluepossiblelabel; + $gvalwithoutprefix = preg_replace('/^[a-z]+\./', '', $gval); + + $fieldfory = 'y_'.$key; + $fieldforg = 'g_'.$gi; + $fieldforybis = 'y_'.$key.'_'.$ykeysuffix; + //var_dump('gvaluepossiblekey='.$gvaluepossiblekey.' gvaluepossiblelabel='.$gvaluepossiblelabel.' ykeysuffix='.$ykeysuffix.' gval='.$gval.' gvalwithoutsuffix='.$gvalwithoutprefix); + //var_dump('fieldforg='.$fieldforg.' obj->$fieldforg='.$obj->$fieldforg.' fieldfory='.$fieldfory.' obj->$fieldfory='.$obj->$fieldfory.' fieldforybis='.$fieldforybis); + + if (!is_array($data[$xi])) { + $data[$xi] = array(); + } + + if (!array_key_exists('label', $data[$xi])) { + $data[$xi] = array(); + $data[$xi]['label'] = $labeltouse; + } + + $objfieldforg = $obj->$fieldforg; + if (is_null($objfieldforg)) { + $objfieldforg = '__NULL__'; + } + + if ($gvaluepossiblekey == '0') { // $gvaluepossiblekey can have type int or string. So we create a special if, used when value is '0' + //var_dump($objfieldforg.' == \'0\' -> '.($objfieldforg == '0')); + if ($objfieldforg == '0') { + // The record we fetch is for this group + $data[$xi][$fieldforybis] = $obj->$fieldfory; + } elseif (!isset($data[$xi][$fieldforybis])) { + // The record we fetch is not for this group + $data[$xi][$fieldforybis] = '0'; + } + } else { + //var_dump((string) $objfieldforg.' === '.(string) $gvaluepossiblekey.' -> '.((string) $objfieldforg === (string) $gvaluepossiblekey)); + if ((string) $objfieldforg === (string) $gvaluepossiblekey) { + // The record we fetch is for this group + $data[$xi][$fieldforybis] = $obj->$fieldfory; + } elseif (!isset($data[$xi][$fieldforybis])) { + // The record we fetch is not for this group + $data[$xi][$fieldforybis] = '0'; + } + } + } + //var_dump($data[$xi]); + $gi++; + } + } + } else { // No group by + $xval = $search_xaxis[0]; + $fieldforxkey = 'x_0'; + $xlabel = $obj->$fieldforxkey; + $xvalwithoutprefix = preg_replace('/^[a-z]+\./', '', $xval); + + // Define $xlabel + if (!empty($object->fields[$xvalwithoutprefix]['arrayofkeyval'])) { + $xlabel = $object->fields[$xvalwithoutprefix]['arrayofkeyval'][$obj->$fieldforxkey]; + } + + $labeltouse = (($xlabel || $xlabel == '0') ? dol_trunc($xlabel, 20, 'middle') : ($xlabel === '' ? $langs->transnoentitiesnoconv("Empty") : $langs->transnoentitiesnoconv("NotDefined"))); + $xarrayforallseries = array('label' => $labeltouse); + foreach ($search_measures as $key => $val) { + $fieldfory = 'y_'.$key; + $xarrayforallseries[$fieldfory] = $obj->$fieldfory; + } + $data[$xi] = $xarrayforallseries; + $xi++; } - $data[$xi] = $xarrayforallseries; - $xi++; } - } - $totalnbofrecord = count($data); + $totalnbofrecord = count($data); + } } //var_dump($data); diff --git a/htdocs/core/lib/date.lib.php b/htdocs/core/lib/date.lib.php index 40f5299905d..9239d78cee3 100644 --- a/htdocs/core/lib/date.lib.php +++ b/htdocs/core/lib/date.lib.php @@ -372,6 +372,7 @@ function convertDurationtoHour($duration_value, $duration_unit) * Note: In database, dates are always for the server TZ. * @return string $sqldate String with SQL filter * @see forgeSQLFromUniversalSearchCriteria() + * @see natural_search() */ function dolSqlDateFilter($datefield, $day_date, $month_date, $year_date, $excludefirstand = 0, $gm = false) { diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index d8a962ee89c..8035dcbf3b6 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -7356,6 +7356,7 @@ function dol_string_nohtmltag($stringtoclean, $removelinefeed = 1, $pagecodeto = $temp = dol_html_entity_decode($temp, ENT_COMPAT | ENT_HTML5, $pagecodeto); $temp = str_replace('< ', '__ltspace__', $temp); + $temp = str_replace('<:', '__lttwopoints__', $temp); if ($strip_tags) { $temp = strip_tags($temp); @@ -7394,6 +7395,7 @@ function dol_string_nohtmltag($stringtoclean, $removelinefeed = 1, $pagecodeto = } $temp = str_replace('__ltspace__', '< ', $temp); + $temp = str_replace('__lttwopoints__', '<:', $temp); return trim($temp); } @@ -10605,11 +10607,15 @@ function dol_getmypid() * If param $mode is 1, can contains an operator <, > or = like "<10" or ">=100.5 < -1000" * If param $mode is 2 or -2, can contains a list of int id separated by comma like "1,3,4" * If param $mode is 3 or -3, can contains a list of string separated by comma like "a,b,c". - * @param integer $mode 0=value is list of keyword strings, 1=value is a numeric test (Example ">5.5 <10"), 2=value is a list of ID separated with comma (Example '1,3,4'), -2 is for exclude list, - * 3=value is list of string separated with comma (Example 'text 1,text 2'), -3 if for exclude list, 4=value is a list of ID separated with comma (Example '2,7') to be used to search into a multiselect string '1,2,3,4' + * @param integer $mode 0=value is list of keyword strings, + * 1=value is a numeric test (Example ">5.5 <10"), + * 2=value is a list of ID separated with comma (Example '1,3,4'), -2 is for exclude list, + * 3=value is list of string separated with comma (Example 'text 1,text 2'), -3 if for exclude list, + * 4=value is a list of ID separated with comma (Example '2,7') to be used to search into a multiselect string '1,2,3,4' * @param integer $nofirstand 1=Do not output the first 'AND' * @return string $res The statement to append to the SQL query * @see dolSqlDateFilter() + * @see forgeSQLFromUniversalSearchCriteria() */ function natural_search($fields, $value, $mode = 0, $nofirstand = 0) { @@ -12529,6 +12535,7 @@ function jsonOrUnserialize($stringtodecode) * @param int $noerror 1=If search criteria is not valid, does not return an error string but invalidate the SQL * @return string Return forged SQL string * @see dolSqlDateFilter() + * @see natural_search() */ function forgeSQLFromUniversalSearchCriteria($filter, &$errorstr = '', $noand = 0, $nopar = 0, $noerror = 0) { @@ -12552,11 +12559,12 @@ function forgeSQLFromUniversalSearchCriteria($filter, &$errorstr = '', $noand = $t = str_replace(array('and','or','AND','OR',' '), '', $t); // Remove the only strings allowed between each () criteria // If the string result contains something else than '()', the syntax was wrong if (preg_match('/[^\(\)]/', $t)) { - $errorstr = 'Bad syntax of the search string'; + $tmperrorstr = 'Bad syntax of the search string'; + $errorstr = 'Bad syntax of the search string: '.$filter; if ($noerror) { return '1 = 2'; } else { - return 'Filter syntax error - '.$errorstr; // Bad syntax of the search string, we return an error message or force a SQL not found + return 'Filter error - '.$tmperrorstr; // Bad syntax of the search string, we return an error message or force a SQL not found } } @@ -12564,7 +12572,8 @@ function forgeSQLFromUniversalSearchCriteria($filter, &$errorstr = '', $noand = } /** - * Explode an universal search string with AND parts + * Explode an universal search string with AND parts. + * This is used to output the search criteria in an UFS (Universal Filter Syntax) input component. * * @param string $sqlfilters Universal SQL filter string. Must have been trimmed before. * @return array Array of AND @@ -12634,6 +12643,7 @@ function dolForgeExplodeAnd($sqlfilters) * @param string $error Returned error message * @param int $parenthesislevel Returned level of global parenthesis that we can remove/simplify, 0 if error or we can't simplify. * @return boolean True if valid, False if not valid ($error returned parameter is filled with the reason in such a case) + * @see forgeSQLFromUniversalSearchCriteria() */ function dolCheckFilters($sqlfilters, &$error = '', &$parenthesislevel = 0) { diff --git a/test/phpunit/ContratTest.php b/test/phpunit/ContratTest.php index 2851bb4ffb7..03db4ac7097 100644 --- a/test/phpunit/ContratTest.php +++ b/test/phpunit/ContratTest.php @@ -185,7 +185,7 @@ class ContratTest extends CommonClassTest /** * testContratOther * - * @param Contract $localobject Object contract + * @param Contrat $localobject Object contract * @return int * * @depends testContratFetch diff --git a/test/phpunit/SecurityTest.php b/test/phpunit/SecurityTest.php index 3dfd260c8b8..bf2c62a5743 100644 --- a/test/phpunit/SecurityTest.php +++ b/test/phpunit/SecurityTest.php @@ -409,6 +409,7 @@ class SecurityTest extends CommonClassTest $_POST["param12"]='aaa'; $_POST["param13"]='n n > < " XSS'; $_POST["param13b"]='n n > < " XSS'; + $_POST["param13c"]='aaa:<:bbb'; $_POST["param14"]="Text with ' encoded with the numeric html entity converted into text entity ' (like when submitted by CKEditor)"; $_POST["param15"]=" src=>0xbeefed"; //$_POST["param15b"]="Example HTML

This is a paragraph.