DBTypes/Filters

* filters should test string input during initialization, so errors
   can be passed on to the PageTemplate.
 * have strings be compared for identity by default and (NOT) LIKE as
   optional operator
 * note: if a string criterium gets processed via callback function
   it will not get prechecked
This commit is contained in:
Sarjuuk 2026-02-12 00:25:54 +01:00
parent 9d187e8d4c
commit 2216b664fa
18 changed files with 226 additions and 199 deletions

View file

@ -938,7 +938,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
// tab: see also
$seeAlso = new QuestList(array(['name_loc'.Lang::getLocale()->value, '%'.Util::htmlEscape($this->subject->getField('name', true)).'%'], ['id', $this->typeId, '!']));
$seeAlso = new QuestList(array(['name_loc'.Lang::getLocale()->value, Util::htmlEscape($this->subject->getField('name', true))], ['id', $this->typeId, '!']));
if (!$seeAlso->error)
{
$this->extendGlobalData($seeAlso->getJSGlobals());

View file

@ -29,13 +29,14 @@ abstract class DBTypeList
* expression: str - must match fieldname;
* int - 1: select everything; 0: select nothing
* array - another condition array
* value: str - operator defaults to: LIKE <val>
* int - operator defaults to: = <val>
* value: str - operator defaults to: = <val>
* num - operator defaults to: = <val>
* array - operator defaults to: IN (<val>)
* null - operator defaults to: IS [NULL]
* operator: modifies/overrides default
* ! - negated default value (NOT LIKE; <>; NOT IN)
* MATCH - creates fulltext search ('value' must be array; column must have fulltext index)
* LIKE / NOT LIKE - partial string matching ('value' must be string (*d'uh*))
* condition as str
* defines linking (AND || OR)
* condition as int
@ -167,37 +168,50 @@ abstract class DBTypeList
else
return null;
$c[2] ??= '';
if (is_array($c[1]) && !empty($c[1]))
{
if (($c[2] ?? '') === 'MATCH')
if ($c[2] === 'MATCH')
return 'MATCH('.$field.') AGAINST(\''.implode(' ', $c[1]).'\' IN BOOLEAN MODE)';
array_walk($c[1], fn(&$x) => $x = Util::checkNumeric($x) ? $x : DB::Aowow()->escape($x));
$op = (isset($c[2]) && $c[2] == '!') ? 'NOT IN' : 'IN';
$op = $c[2] == '!' ? 'NOT IN' : 'IN';
$val = '('.implode(', ', $c[1]).')';
}
else if (Util::checkNumeric($c[1])) // Note: should this be a NUM_REQ_* check?
{
$op = (isset($c[2]) && $c[2] == '!') ? '<>' : '=';
$val = $c[1];
$op = $c[2] == '!' ? '<>' : ($c[2] ?: '=');
}
else if (is_string($c[1]))
{
$op = (isset($c[2]) && $c[2] == '!') ? 'NOT LIKE' : 'LIKE';
$val = DB::Aowow()->escape($c[1]);
$val = mysqli_real_escape_string(DB::Aowow()->link, $c[1]);
if ($c[2] == 'LIKE')
{
$op = 'LIKE';
$val = '"%'.$val.'%"';
}
else if ($c[2] == 'NOT LIKE')
{
$op = 'NOT LIKE';
$val = '"%'.$val.'%"';
}
else
{
$op = $c[2] == '!' ? '<>' : '=';
$val = '"'.$val.'"';
}
}
else if (count($c) > 1 && $c[1] === null) // specifficly check for NULL
{
$op = (isset($c[2]) && $c[2] == '!') ? 'IS NOT' : 'IS';
$op = $c[2] == '!' ? 'IS NOT' : 'IS';
$val = 'NULL';
}
else // null for example
return null;
if (isset($c[2]) && $c[2] != '!')
$op = $c[2];
return '('.$field.' '.$op.' '.$val.')';
}
};

View file

@ -56,6 +56,7 @@ abstract class Filter
public const V_LIST = 10;
public const V_CALLBACK = 11;
public const V_REGEX = 12;
public const V_NAME = 13;
protected const ENUM_ANY = -2323;
protected const ENUM_NONE = -2324;
@ -102,7 +103,13 @@ abstract class Filter
private array $cndSet = []; // db type query storage
private array $rawData = [];
/* genericFilter: [FILTER_TYPE, colOrFnName, param1, param2]
protected string $type = ''; // set by child
protected array $parentCats = []; // used to validate ty-filter
protected array $inTokens = []; // text search includes
protected array $exTokens = []; // text search excludes
protected array $ftTokens = []; // fulltext search
/* $genericFilter: [FILTER_TYPE, colOrFnName, param1, param2]
[self::CR_BOOLEAN, <string:colName>, <bool:isString>, null]
[self::CR_FLAG, <string:colName>, <int:testBit>, <bool:matchAny>] # default param2: matchExact
[self::CR_NUMERIC, <string:colName>, <int:NUM_FLAGS>, <bool:addExtraCol>]
@ -111,12 +118,17 @@ abstract class Filter
[self::CR_STAFFFLAG, <string:colName>, null, null]
[self::CR_CALLBACK, <string:fnName>, <mixed:param1>, <mixed:param2>]
[self::CR_NYI_PH, null, <int:returnVal>, param2] # mostly 1: to ignore this criterium; 0: to fail the whole query
*/
protected string $type = ''; // set by child
protected array $parentCats = []; // used to validate ty-filter
$inputFields: fieldName => [VALUE_TYPE, checkInfo, fieldIsArray]
[self::V_EQUAL, <mixed:exactValue>, <bool:isArray>]
[self::V_RANGE, <array:minMaxInt>, <bool:isArray>]
[self::V_LIST, <array:validInts>, <bool:isArray>]
[self::V_CALLBACK, <string:fnName>, <bool:isArray>]
[self::V_REGEX, <string:regexp>, <bool:isArray>]
[self::V_NAME, <bool:matchExact>, <bool:isArray>]
*/
protected static array $genericFilter = [];
protected static array $inputFields = []; // list of input fields defined per page - fieldName => [checkType, checkValue[, fieldIsArray]]
protected static array $inputFields = []; // list of input fields defined per page
protected static array $enums = []; // validation for opt lists per page - criteriumID => [validOptionList]
// express Filters in template
@ -441,6 +453,12 @@ abstract class Filter
continue 2;
break;
case self::CR_STRING:
if ($param1 & STR_LOCALIZED)
$colOrFn .= '_loc'.Lang::getLocale()->value;
if ($this->tokenizeString($colOrFn, $_crv[$i], $param1 & STR_MATCH_EXACT, $param1 & STR_ALLOW_SHORT))
continue 2;
break;
case self::CR_CALLBACK:
case self::CR_NYI_PH:
continue 2;
@ -497,19 +515,19 @@ abstract class Filter
$this->fiSetWeights = [$_wt, $_wtv];
}
protected function checkInput(int $type, mixed $valid, mixed &$val, bool $recursive = false) : bool
protected function checkInput(int $type, mixed $checkInfo, mixed &$val, bool $recursive = false) : bool
{
switch ($type)
{
case self::V_EQUAL:
if (gettype($valid) == 'integer')
if (gettype($checkInfo) == 'integer')
$val = intval($val);
else if (gettype($valid) == 'double')
else if (gettype($checkInfo) == 'double')
$val = floatval($val);
else /* if (gettype($valid) == 'string') */
else /* if (gettype($checkInfo) == 'string') */
$val = strval($val);
if ($valid == $val)
if ($checkInfo == $val)
return true;
break;
@ -517,10 +535,10 @@ abstract class Filter
if (!Util::checkNumeric($val, NUM_CAST_INT))
return false;
if (in_array($val, $valid))
if (in_array($val, $checkInfo))
return true;
foreach ($valid as $v)
foreach ($checkInfo as $v)
{
if (gettype($v) != 'array')
continue;
@ -531,142 +549,144 @@ abstract class Filter
break;
case self::V_RANGE:
if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $valid[0] && $val <= $valid[1])
if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $checkInfo[0] && $val <= $checkInfo[1])
return true;
break;
case self::V_CALLBACK:
if ($this->$valid($val))
if ($this->$checkInfo($val))
return true;
break;
case self::V_REGEX:
if (!preg_match($valid, $val))
if (!preg_match($checkInfo, $val))
return true;
break;
case self::V_NAME:
if (preg_match(self::PATTERN_NAME, $val))
break;
if (!$this->tokenizeString('na', $val, $checkInfo && $this->values['ex']))
return false; // quit without logging more errors
return true;
}
if (!$recursive)
{
trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.Util::toString($valid).' val: '.((string)$val).']', E_USER_NOTICE);
trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.Util::toString($checkInfo).' val: '.((string)$val).']', E_USER_NOTICE);
$this->error = true;
}
return false;
}
protected function transformToken(string $string, bool $exact) : string
public static function transformToken(string &$string, bool $allowShort = false) : ?array
{
$lenTest = fn($x) => $x !== '' && (mb_strlen($x) > 2 || $allowShort || Lang::getLocale()->isLogographic());
$string = trim($string);
if ($string === '')
return null;
// invalid chars for both LIKE and MATCH
$str = str_replace(['\\', '%'], '', $string);
if ($neg = ($str[0] === '-'))
$str = mb_substr($str, 1);
if (!$lenTest($str))
return null;
// if the fulltext token contains invalid chars, should it be sub-tokenized (current behavior) or should the chars just be stripped
$ft = array_filter(explode(' ', preg_replace(self::PATTERN_FT, ' ', $str)), $lenTest);
// escape manually entered _; entering % should be prohibited
$string = str_replace('_', '\\_', $string);
// then replace search wildcards with sql wildcards
$lk = strtr(str_replace('_', '\\_', $str), self::$wCards);
// now replace search wildcards with sql wildcards
$string = strtr($string, self::$wCards);
return sprintf($exact ? '%s' : '%%%s%%', $string);
return [$lk, $ft, $neg];
}
protected function tokenizeString(array $fields, string $string = '', bool $exact = false, bool $allowShort = false) : array
protected function tokenizeString(string $field, string $string, bool $exact = false, bool $allowShort = false) : bool
{
if (!$string && $this->values['na'])
$string = $this->values['na'];
// always allow sub 3 chars for logographic locales
if (Lang::getLocale()->isLogographic())
$allowShort = true;
$qry = [];
foreach ($fields as $f)
{
$sub = [];
$tokens = $exact ? [$string] : array_filter(explode(' ', $string));
foreach ($tokens as $t)
{
if ($t[0] == '-' && (mb_strlen($t) > 3 || $allowShort))
$sub[] = [$f, $this->transformToken(mb_substr($t, 1), $exact), '!'];
else if ($t[0] != '-' && (mb_strlen($t) > 2 || $allowShort))
$sub[] = [$f, $this->transformToken($t, $exact)];
}
// single cnd?
if (!$sub)
continue;
else if (count($sub) > 1)
array_unshift($sub, 'AND');
else
$sub = $sub[0];
$qry[] = $sub;
}
// single cnd?
if (!$qry)
{
trigger_error('Filter::tokenizeString - could not tokenize string: '.$string, E_USER_NOTICE);
$this->error = true;
}
else if (count($qry) > 1)
array_unshift($qry, 'OR');
else
$qry = $qry[0];
return $qry;
}
protected function buildMatchLookup(array $fields, string $string = '', bool $exact = false, bool $allowShort = false) : array
{
if (!$string && $this->values['na'])
$string = $this->values['na'];
if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH'))
return $this->tokenizeString($fields, $string, $exact, $allowShort);
$ftString = trim(preg_replace(self::PATTERN_FT, ' ', $string));
if (!$ftString)
return [];
// always allow sub 3 chars for logographic locales
if (Lang::getLocale()->isLogographic())
$allowShort = true;
$sub = [];
$tokens = $exact ? [$ftString] : array_filter(explode(' ', $ftString));
$tokens = $exact ? [$string] : explode(' ', $string);
foreach ($tokens as $t)
{
$ex = $t[0] === '-';
if ($ex)
$t = mb_substr($t, 1);
if ([$like, $fulltext, $ex] = self::transformToken($t, $allowShort))
{
if ($like)
$this->{$ex ? 'exTokens' : 'inTokens'}[$field][] = $like;
// cant have trailing/leading dashes. FT confuses them for additional modifiers and dies with a syntax error
// would be an issue for all modifiers, but Filter::PATTERN_FT only allows for - at this point
$t = preg_replace('/^-+|-+$/', '', $t);
// don't bother with fulltext search if exact is specified
if ($exact)
continue;
if ($allowShort || mb_strlen($t) > 2)
$sub[] = ($ex ? '-' : '+') . $t . '*';
// note: a fulltext search purely from exclude tokens will return no result
foreach ($fulltext as $ft)
{
// cant have trailing/leading dashes. FT confuses them for additional modifiers and dies with a syntax error
// would be an issue for all modifiers, but Filter::PATTERN_FT only allows for - at this point
$this->ftTokens[$field][] = ($ex ? '-' : '+') . preg_replace('/^-+|-+$/', '', $ft) . '*';
}
}
}
if (!$sub)
if (empty($this->inTokens[$field]))
{
trigger_error('Filter::buildMatchLookup - could not build MATCH AGAINST from: "'.$ftString.'"', E_USER_NOTICE);
trigger_error('Filter::tokenizeString - could not tokenize string: "'.$string.'" for input: '.$field, E_USER_NOTICE);
$this->error = true;
return false;
}
return true;
}
protected function buildLikeLookup(array $fields, bool $exact = false) : array
{
$qry = [];
foreach ($fields as $field => $col)
{
$sub = [];
if (!empty($this->inTokens[$field]))
$sub = array_merge($sub, array_map(fn($x) => [$col, $x, $exact ? null : 'LIKE'], $this->inTokens[$field]));
if (!empty($this->exTokens[$field]))
$sub = array_merge($sub, array_map(fn($x) => [$col, $x, $exact ? null : 'NOT LIKE'], $this->exTokens[$field]));
if (count($sub) > 1)
array_unshift($sub, 'AND');
else if ($sub)
$sub = $sub[0];
if ($sub)
$qry[] = $sub;
}
return $qry ? ['OR', ...$qry] : [];
}
protected function buildMatchLookup(array $fields, bool $exact = false) : array
{
if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH'))
return $this->buildLikeLookup($fields, $exact);
$qry = [];
foreach ($fields as $f)
foreach ($fields as $field => $col)
{
$qry[] = [$f, trim($string)];
if ($sub)
$qry[] = [$f, $sub, 'MATCH'];
if (!empty($this->ftTokens[$field]))
$qry[] = [$col, $this->ftTokens[$field], 'MATCH'];
$tok = $this->values[$field];
if (self::transformToken($tok))
$qry[] = [$col, $tok];
}
// single cnd?
if (count($qry) > 1)
array_unshift($qry, 'OR');
else
$qry = $qry[0];
return $qry;
return $qry ? ['OR', ...$qry] : [];
}
protected function int2Op(mixed &$op) : bool
@ -738,14 +758,14 @@ abstract class Filter
return [[$field, $value, '&'], $value];
}
private function genericString(string $field, string $value, ?int $strFlags) : ?array
private function genericString(string $field, ?int $strFlags) : ?array
{
$strFlags ??= 0x0;
if ($strFlags & STR_LOCALIZED)
$field .= '_loc'.Lang::getLocale()->value;
return $this->tokenizeString([$field], $value, $strFlags & STR_MATCH_EXACT, $strFlags & STR_ALLOW_SHORT);
return $this->buildLikeLookup([$field => $field]);
}
private function genericNumeric(string $field, int|float $value, int $op, int $typeCast) : ?array
@ -821,7 +841,7 @@ abstract class Filter
self::CR_FLAG => $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2),
self::CR_STAFFFLAG => $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true),
self::CR_BOOLEAN => $this->genericBoolean($colOrFn, $crs, !empty($param1)),
self::CR_STRING => $this->genericString($colOrFn, $crv, $param1),
self::CR_STRING => $this->genericString($colOrFn, $param1),
self::CR_CALLBACK => $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2),
self::CR_ENUM => $handleEnum($cr, $crs, $colOrFn, $param1, $param2),
self::CR_NYI_PH => $handleNYIPH($crs, $crv, $param1),

View file

@ -112,40 +112,20 @@ class Search
foreach (explode(' ', $this->query) as $raw)
{
// invalid chars for both LIKE and MATCH
$clean = str_replace(['\\', '%'], '', $raw);
if ($clean === '')
continue;
$ex = ($clean[0] == '-');
if ($ex)
if ([$like, $fulltext, $ex] = Filter::transformToken($raw, $allowShort))
{
$clean = mb_substr($clean, 1);
$raw = mb_substr($raw, 1);
}
$this->{$ex ? 'excluded' : 'included'}[] = $like;
if (mb_strlen($clean) < 3 && !$allowShort)
{
$this->invalid[] = $raw;
continue;
}
$this->{$ex ? 'excluded' : 'included'}[] = str_replace('_', '\\_', $clean);
// note: a fulltext search purely with exclude tokens will return no result
if (($tokens = trim(preg_replace(Filter::PATTERN_FT, ' ', $clean))) !== '')
{
foreach (array_filter(explode(' ', $tokens)) as $t)
// note: a fulltext search purely from exclude tokens will return no result
foreach ($fulltext as $ft)
{
// cant have trailing/leading dashes. FT confuses them for additional modifiers and dies with a syntax error
// would be an issue for all modifiers, but Filter::PATTERN_FT only allows for - at this point
$t = preg_replace('/^-+|-+$/', '', $t);
if ($allowShort || mb_strlen($t) > 2)
$this->fulltext[] = ($ex ? '-' : '+') . $t . '*';
$this->fulltext[] = ($ex ? '-' : '+') . preg_replace('/^-+|-+$/', '', $ft) . '*';
}
}
else
$this->invalid[] = $raw;
}
}
@ -165,11 +145,8 @@ class Search
foreach ($fields as $f)
{
$sub = [];
foreach ($this->included as $i)
$sub[] = [$f, '%'.$i.'%'];
foreach ($this->excluded as $x)
$sub[] = [$f, '%'.$x.'%', '!'];
$sub = array_merge($sub, array_map(fn($x) => [$f, $x, 'LIKE'], $this->included));
$sub = array_merge($sub, array_map(fn($x) => [$f, $x, 'NOT LIKE'], $this->excluded));
// single cnd?
if (count($sub) > 1)
@ -202,17 +179,17 @@ class Search
$fields[] = 'name_loc'.Lang::getLocale()->value;
$qry = [];
foreach ($fields as $f)
{
$qry[] = [$f, $this->query];
if ($this->fulltext)
$qry[] = [$f, $this->fulltext, 'MATCH'];
}
if ($this->fulltext)
$qry = array_map(fn($x) => [$x, $this->fulltext, 'MATCH'], $fields);
$strBak = trim($this->query);
if (mb_strlen($strBak) > 2 || Lang::getLocale()->isLogographic())
$qry = array_merge($qry, array_map(fn($x) => [$x, $strBak], $fields));
// single cnd?
if (count($qry) > 1)
array_unshift($qry, 'OR');
else
else if (count($qry) == 1)
$qry = $qry[0];
return $qry;

View file

@ -310,7 +310,7 @@ class AchievementListFilter extends Filter
'cr' => [parent::V_RANGE, [2, 18], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name / description - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // extended name search
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH, -SIDE_ALLIANCE, -SIDE_HORDE], false], // side
@ -328,9 +328,9 @@ class AchievementListFilter extends Filter
{
$_ = [];
if ($_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'reward_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]);
$_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value, 'na' => 'reward_loc'.Lang::getLocale()->value, 'na' => 'description_loc'.Lang::getLocale()->value]);
else
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]);
$_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]);
if ($_)
$parts[] = $_;

View file

@ -73,7 +73,7 @@ class AreaTriggerListFilter extends Filter
'cr' => [parent::V_LIST, [2], true ], // criteria ids
'crs' => [parent::V_RANGE, [1, 6], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - all criteria are numeric here
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ty' => [parent::V_RANGE, [0, 5], true ] // types
);
@ -85,7 +85,7 @@ class AreaTriggerListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
if ($_ = $this->buildLikeLookup(['na' => 'name']))
$parts[] = $_;
// type [list]

View file

@ -52,9 +52,9 @@ class ArenaTeamListFilter extends Filter
protected string $type = 'arenateams';
protected static array $genericFilter = [];
protected static array $inputFields = array(
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value
'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact
'si' => [parent::V_LIST, [1, 2], false], // side
'sz' => [parent::V_LIST, [2, 3, 5], false], // tema size
'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region
@ -73,7 +73,7 @@ class ArenaTeamListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->tokenizeString(['at.name'], $_v['na'], $_v['ex'] == 'on'))
if ($_ = $this->buildLikeLookup(['na' => 'at.name'], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]

View file

@ -343,7 +343,7 @@ class CreatureListFilter extends Filter
'cr' => [parent::V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 9999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiter
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / subname - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name / subname - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // also match subname
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'fa' => [parent::V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1
@ -365,10 +365,10 @@ class CreatureListFilter extends Filter
if ($_v['na'])
{
if ($_v['ex'] == 'on')
if ($_ = $this->tokenizeString(['subname_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup(['na' => 'subname_loc'.Lang::getLocale()->value]))
$parts[] = $_;
if ($_ = $this->buildMatchLookup(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
{
if ($parts)
$parts = ['OR', $_, ...$parts];

View file

@ -237,7 +237,7 @@ class EnchantmentListFilter extends Filter
'cr' => [parent::V_RANGE, [2, 123], true ], // criteria ids
'crs' => [parent::V_RANGE, [1, 15], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ty' => [parent::V_RANGE, [1, 8], true ] // types
);
@ -249,7 +249,7 @@ class EnchantmentListFilter extends Filter
//string
if ($_v['na'])
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
// type

View file

@ -167,7 +167,7 @@ class GameObjectListFilter extends Filter
'cr' => [parent::V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18, 50], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numeric input values expected
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
);
@ -180,7 +180,7 @@ class GameObjectListFilter extends Filter
// name
if ($_v['na'])
if ($_ = $this->buildMatchLookup(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
return $parts;

View file

@ -94,9 +94,9 @@ class GuildListFilter extends Filter
protected string $type = 'guilds';
protected static array $genericFilter = [];
protected static array $inputFields = array(
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value
'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact
'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side
'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region
'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template
@ -114,7 +114,7 @@ class GuildListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->tokenizeString(['g.name'], $_v['na'], $_v['ex'] == 'on'))
if ($_ = $this->buildLikeLookup(['na' => 'g.name'], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]

View file

@ -132,7 +132,7 @@ class IconListFilter extends Filter
'cr' => [parent::V_LIST, [1, 2, 3, 6, 9, 11, 13], true ], // criteria ids
'crs' => [parent::V_RANGE, [1, 6], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - all criteria are numeric here
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
);
@ -145,7 +145,7 @@ class IconListFilter extends Filter
//string
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
if ($_ = $this->buildLikeLookup(['na' => 'name']))
$parts[] = $_;
return $parts;

View file

@ -1991,7 +1991,7 @@ class ItemListFilter extends Filter
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
'upg' => [parent::V_REGEX, '/[^\d:]/ui', true ], // upgrade item ids
'gb' => [parent::V_LIST, [0, 1, 2, 3], false], // search result grouping
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ub' => [parent::V_LIST, [[1, 9], 11], false], // usable by classId
'qu' => [parent::V_RANGE, [0, 7], true ], // quality ids
@ -2097,7 +2097,7 @@ class ItemListFilter extends Filter
// name
if ($_v['na'])
if ($_ = $this->buildMatchLookup(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
// usable-by (not excluded by requiredClass && armor or weapons match mask from ?_classes)
@ -2218,9 +2218,16 @@ class ItemListFilter extends Filter
protected function cbHasRandEnchant(int $cr, int $crs, string $crv) : ?array
{
$n = preg_replace(parent::PATTERN_NAME, '', $crv);
$n = $this->transformToken($n, false);
if (!$this->tokenizeString($cr, $n))
return null;
$randIds = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, ABS(`id`) AS `id`, name_loc?d, `name_loc0` FROM ?_itemrandomenchant WHERE name_loc?d LIKE ?', Lang::getLocale()->value, Lang::getLocale()->value, $n);
$parts = [];
foreach ($this->inTokens[$cr] ?? [] as $tok)
$parts[] = sprintf('name_loc%d LIKE "%%%s%%"', Lang::getLocale()->value, mysqli_real_escape_string(DB::Aowow()->link, $tok));
foreach ($this->exTokens[$cr] ?? [] as $tok)
$parts[] = sprintf('name_loc%d NOT LIKE "%%%s%%"', Lang::getLocale()->value, mysqli_real_escape_string(DB::Aowow()->link, $tok));
$randIds = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, ABS(`id`) AS `id`, name_loc?d, `name_loc0` FROM ?_itemrandomenchant WHERE '.implode(' AND ', $parts), Lang::getLocale()->value);
$tplIds = $randIds ? DB::World()->select('SELECT `entry`, `ench` FROM item_enchantment_template WHERE `ench` IN (?a)', array_column($randIds, 'id')) : [];
foreach ($tplIds as &$set)
{

View file

@ -180,7 +180,7 @@ class ItemsetListFilter extends Filter
'cr' => [parent::V_RANGE, [2, 12], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 424]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name / description - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'qu' => [parent::V_RANGE, [0, 7], true ], // quality
'ty' => [parent::V_RANGE, [1, 12], true ], // set type
@ -199,7 +199,7 @@ class ItemsetListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
// quality [enum]

View file

@ -244,9 +244,9 @@ class ProfileListFilter extends Filter
'cr' => [parent::V_RANGE, [1, 36], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value
'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact
'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side
'ra' => [parent::V_LIST, [[1, 8], 10, 11], true ], // race
'cl' => [parent::V_LIST, [[1, 9], 11], true ], // class
@ -284,13 +284,20 @@ class ProfileListFilter extends Filter
// table key differs between remote and local :<
$k = $this->useLocalList ? 'p' : 'c';
// name [str] - the table is case sensitive. Since i don't want to destroy indizes, lets alter the search terms
// name [str]
if ($_v['na'])
{
$lower = $this->tokenizeString([$k.'.name'], Util::lower($_v['na']), $_v['ex'] == 'on', true);
$proper = $this->tokenizeString([$k.'.name'], Util::ucWords($_v['na']), $_v['ex'] == 'on', true);
// issue: the table is case sensitive. so we need to alter the tokens for multiple cases
foreach (['inTokens', 'exTokens'] as $prop)
{
if (empty($this->{$prop}['na']))
continue;
$parts[] = ['OR', $lower, $proper];
$this->{$prop}['na'] = array_map(Util::lower(...), $this->{$prop}['na']);
$this->{$prop}['_na'] = array_map(Util::ucWords(...), $this->{$prop}['na']);
};
$parts[] = $this->buildLikeLookup(['na' => $k.'.name', '_na' => $k.'.name'], $_v['ex'] == 'on');
}
// side [list]
@ -406,8 +413,10 @@ class ProfileListFilter extends Filter
protected function cbTeamName(int $cr, int $crs, string $crv, $size) : ?array
{
if ($_ = $this->tokenizeString(['at.name'], $crv))
return ['AND', ['at.type', $size], $_];
$n = preg_replace(parent::PATTERN_NAME, '', $crv);
if ($this->tokenizeString($cr, $n))
if ($_ = $this->buildLikeLookup([$cr => 'at.name']))
return ['AND', ['at.type', $size], $_];
return null;
}

View file

@ -485,7 +485,7 @@ class QuestListFilter extends Filter
'cr' => [parent::V_RANGE, [1, 45], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name / text - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // also match subname
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'minle' => [parent::V_RANGE, [0, 99], false], // min quest level
@ -507,10 +507,10 @@ class QuestListFilter extends Filter
if ($_v['na'])
{
if ($_v['ex'] == 'on')
if ($_ = $this->tokenizeString(['objectives_loc'.Lang::getLocale()->value, 'details_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup(['na' => 'objectives_loc'.Lang::getLocale()->value, 'na' => 'details_loc'.Lang::getLocale()->value]))
$parts[] = $_;
if ($_ = $this->buildMatchLookup(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
{
if ($parts)
$parts[0][] = $_;

View file

@ -104,7 +104,7 @@ class SoundListFilter extends Filter
{
protected string $type = 'sounds';
protected static array $inputFields = array(
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
'ty' => [parent::V_LIST, [[1, 4], 6, 9, 10, 12, 13, 14, 16, 17, [19, 31], 50, 52, 53], true ] // type
);
@ -115,7 +115,7 @@ class SoundListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
if ($_ = $this->buildLikeLookup(['na' => 'name']))
$parts[] = $_;
// type [list]

View file

@ -2575,7 +2575,7 @@ class SpellListFilter extends Filter
'cr' => [parent::V_RANGE, [1, 116], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter
'na' => [parent::V_NAME, false, false], // name / text - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // extended name search
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'minle' => [parent::V_RANGE, [0, 99], false], // spell level min
@ -2599,10 +2599,10 @@ class SpellListFilter extends Filter
if ($_v['na'])
{
if ($_v['ex'] == 'on')
if ($_ = $this->tokenizeString(['buff_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup(['na' => 'buff_loc'.Lang::getLocale()->value, 'na' => 'description_loc'.Lang::getLocale()->value]))
$parts[] = $_;
if ($_ = $this->buildMatchLookup(['name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
{
if ($parts)
$parts[0][] = $_;