diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 289a2528..a17b0992 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -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()); diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php index f2aaa4d7..6e34833f 100644 --- a/includes/components/dbtypelist.class.php +++ b/includes/components/dbtypelist.class.php @@ -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 - * int - operator defaults to: = + * value: str - operator defaults to: = + * num - operator defaults to: = * array - operator defaults to: IN () * 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.')'; } }; diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 3208a862..b877ce3e 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -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, , , null] [self::CR_FLAG, , , ] # default param2: matchExact [self::CR_NUMERIC, , , ] @@ -111,12 +118,17 @@ abstract class Filter [self::CR_STAFFFLAG, , null, null] [self::CR_CALLBACK, , , ] [self::CR_NYI_PH, null, , 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, , ] + [self::V_RANGE, , ] + [self::V_LIST, , ] + [self::V_CALLBACK, , ] + [self::V_REGEX, , ] + [self::V_NAME, , ] + */ 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), diff --git a/includes/components/search.class.php b/includes/components/search.class.php index bc8c9182..5818178c 100644 --- a/includes/components/search.class.php +++ b/includes/components/search.class.php @@ -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; diff --git a/includes/dbtypes/achievement.class.php b/includes/dbtypes/achievement.class.php index 89c19b1a..247aabb0 100644 --- a/includes/dbtypes/achievement.class.php +++ b/includes/dbtypes/achievement.class.php @@ -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[] = $_; diff --git a/includes/dbtypes/areatrigger.class.php b/includes/dbtypes/areatrigger.class.php index 57fd4e59..de1fbc19 100644 --- a/includes/dbtypes/areatrigger.class.php +++ b/includes/dbtypes/areatrigger.class.php @@ -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] diff --git a/includes/dbtypes/arenateam.class.php b/includes/dbtypes/arenateam.class.php index 545e8332..a9294093 100644 --- a/includes/dbtypes/arenateam.class.php +++ b/includes/dbtypes/arenateam.class.php @@ -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] diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index f6a871b9..9dcbe7d2 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -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]; diff --git a/includes/dbtypes/enchantment.class.php b/includes/dbtypes/enchantment.class.php index 4770910a..821e08e8 100644 --- a/includes/dbtypes/enchantment.class.php +++ b/includes/dbtypes/enchantment.class.php @@ -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 diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php index 7d5337f1..b15f8393 100644 --- a/includes/dbtypes/gameobject.class.php +++ b/includes/dbtypes/gameobject.class.php @@ -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; diff --git a/includes/dbtypes/guild.class.php b/includes/dbtypes/guild.class.php index 623f6ee5..0732a404 100644 --- a/includes/dbtypes/guild.class.php +++ b/includes/dbtypes/guild.class.php @@ -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] diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php index 173cac23..455bd303 100644 --- a/includes/dbtypes/icon.class.php +++ b/includes/dbtypes/icon.class.php @@ -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; diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 21336935..50869817 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -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) { diff --git a/includes/dbtypes/itemset.class.php b/includes/dbtypes/itemset.class.php index 27a83663..618f2128 100644 --- a/includes/dbtypes/itemset.class.php +++ b/includes/dbtypes/itemset.class.php @@ -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] diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index f3a4b1f8..df4da3a7 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -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; } diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index 1f39c2d1..e6afef09 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -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][] = $_; diff --git a/includes/dbtypes/sound.class.php b/includes/dbtypes/sound.class.php index 577611f8..0f65f855 100644 --- a/includes/dbtypes/sound.class.php +++ b/includes/dbtypes/sound.class.php @@ -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] diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 852fcde8..1336eeca 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -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][] = $_;