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- Item 1
- Item 2
";
@@ -537,6 +538,10 @@ class SecurityTest extends CommonClassTest
print __METHOD__." result=".$result."\n";
$this->assertEquals('n n > < XSS', $result, 'Test that html entities are decoded with alpha');
+ $result=GETPOST("param13c", 'alphanohtml');
+ print __METHOD__." result=".$result."\n";
+ $this->assertEquals('aaa:<:bbb', $result, 'Test 13c');
+
// Test with alphawithlgt
@@ -588,7 +593,6 @@ class SecurityTest extends CommonClassTest
print __METHOD__." result=".$result."\n";
$this->assertEquals('XSS', $result, 'Test 19');
-
// Test with restricthtml + MAIN_RESTRICTHTML_ONLY_VALID_HTML only to test disabling of bad attributes
$conf->global->MAIN_RESTRICTHTML_ONLY_VALID_HTML = 1;