Template/Update (Part 4)

* convert search into separate endpoints
 * move shared functionalty to components
 * NOTE: acceptance of opensearch has waned over the last decade and
         the script should be updated
This commit is contained in:
Sarjuuk
2025-08-06 15:19:28 +02:00
parent 81d9248541
commit 1f5152c871
13 changed files with 1995 additions and 1471 deletions

113
endpoints/search/search.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
=> Templated Page /w Listviews
*/
class SearchBaseResponse extends TemplateResponse implements ICache
{
use TrCache, TrSearch;
private const SEARCH_MODS_ALL = 0x0FFFFFFF; // yeah im lazy, now what?
protected int $cacheType = CACHE_TYPE_SEARCH;
protected string $template = 'search';
protected string $pageName = 'search';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']];
protected array $expectedGET = array(
'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']]
);
public string $invalidTerms = '';
public function __construct(string $pageParam)
{
parent::__construct($pageParam); // just to set g_user and g_locale
$this->query = $this->_get['search']; // technically pageParam, but prepared
if ($limit = Cfg::get('SQL_LIMIT_SEARCH'))
$this->maxResults = $limit;
$this->searchMask = Search::TYPE_REGULAR | self::SEARCH_MODS_ALL;
$this->searchObj = new Search($this->query, $this->searchMask, $this->maxResults);
}
protected function generate() : void
{
if (!$this->query) // empty search > goto home page
$this->forward();
$this->search = $this->query; // escaped by TemplateResponse
if ($iv = $this->searchObj->invalid)
$this->invalidTerms = implode(', ', Util::htmlEscape($iv));
array_unshift($this->title, $this->search, Lang::main('search'));
$this->redButtons[BUTTON_WOWHEAD] = true;
$this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'search=', Util::htmlEscape($this->query));
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true);
$canRedirect = true;
$redirectTo = '';
foreach ($this->searchObj->perform() as $lvData)
{
if ($lvData[1] == 'npc' || $lvData[1] == 'object')
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview(...$lvData));
// we already have a target > can't have more targets > no redirects
if ($canRedirect && $redirectTo)
$canRedirect = false;
if ($canRedirect) // note - we are very lucky that in case of searches $template is identical to the typeString
$redirectTo = '?'.$lvData[1].'='.key($lvData[0]['data']);
}
$this->extendGlobalData($this->searchObj->getJSGlobals());
parent::generate();
$this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']);
// this one stings..
// we have to manually call saveCache, beacause normally it would be called AFTER the page is rendered..
// .. which will not happen if we forward to somewhere
// also we have to set a postCacheHook in this case that handles future forwards (gets called in display() so the currenct call is also covered)
if ($canRedirect && $redirectTo)
{
$this->setOnCacheLoaded([self::class, 'onBeforeDisplay'], $redirectTo);
$this->saveCache($this->result);
}
}
// update dates to now()
public static function tabsHook(Template\PageTemplate $pt, Tabs &$lvTabs) : void
{
foreach ($lvTabs->iterate() as &$listview)
if ($listview instanceof Listview && $listview->getTemplate() == 'holiday')
WorldEventList::updateListview($listview);
}
public static function onBeforeDisplay(Template\PageTemplate $pt, string $url) : never
{
header('Location: '.$url, true, 302); // we no longer have access to BaseResponse .. so thats fun
exit();
}
}
?>

View File

@@ -0,0 +1,94 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
=> search by compare or profiler (only items + itemsets)
array:[
searchString,
[itemData],
[itemsetData]
]
*/
class SearchJsonResponse extends TextResponse implements ICache
{
use TrCache, TrSearch;
protected int $cacheType = CACHE_TYPE_SEARCH;
protected array $expectedGET = array(
'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'wt' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ],
'wtv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ],
'slots' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ],
'type' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => Type::ITEM, 'max_range' => Type::ITEMSET]]
);
private array $extraOpts = []; // for weighted search
private array $extraCnd = []; // for weighted search
public function __construct(string $pageParam)
{
parent::__construct($pageParam);
$this->query = $this->_get['search']; // technically pageParam, but prepared
if ($this->_get['wt'] && $this->_get['wtv']) // slots and type should get ignored
{
$itemFilter = new ItemListFilter($this->_get);
if ($_ = $itemFilter->createConditionsForWeights())
{
$this->extraOpts = $itemFilter->extraOpts;
$this->extraCnd[] = $_;
}
}
if ($_ = array_filter($this->_get['slots'] ?? []))
$this->extraCnd[] = ['slot', $_];
if ($limit = Cfg::get('SQL_LIMIT_SEARCH'))
$this->maxResults = $limit;
$this->searchMask = Search::TYPE_JSON;
if ($this->_get['slots'] || $this->_get['type'] == Type::ITEM)
$this->searchMask |= 1 << Search::MOD_ITEM;
else if ($this->_get['type'] == Type::ITEMSET)
$this->searchMask |= 1 << Search::MOD_ITEM | 1 << Search::MOD_ITEMSET;
$this->searchObj = new Search($this->query, $this->searchMask, $this->maxResults, $this->extraCnd, $this->extraOpts);
}
// !note! dear reader, if you ever try to generate a string, that is to be evaled by JS, NEVER EVER terminate with a \n ..... $totalHoursWasted +=2;
protected function generate() : void
{
$outItems = [];
$outSets = [];
// invalid conditions: not enough characters to search OR no types to search
if (!$this->searchObj->canPerform())
$this->generate404($this->query);
foreach ($this->searchObj->perform() as $modId => $data)
{
if ($modId == Search::MOD_ITEM)
$outItems = $data;
else if ($modId == Search::MOD_ITEMSET)
$outSets = $data;
}
$this->result = Util::toJSON([$this->query, $outItems, $outSets]);
}
public function generate404(?string $search = ''): never
{
parent::generate404(Util::toJSON([$search, [], []]));
}
}
?>

View File

@@ -0,0 +1,129 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/* ugh .. badly documented standards...
so, Opensearch 1.1 _mentions_ support for results returned as json, but does not describe a format
GPT-4.1 described it as
[
'searchTerm',
['text match 1', .., 'text match N'],
['description 1', .., ' description N'],
['url 1', .., 'url N']
]
but was unable to provide sources (or rather the sources it linked 404ed or where unhelpful)
though https://en.wikipedia.org/w/api.php?action=opensearch&namespace=0&search=term supports this claim
Firefox today only evaluates index 1
Edge/Chrome do not support suggestions from manual installs and refuse auto-discovery (would require policy or plugin)
- for pre-installed search engines (like wikipedia) Edge/Chrome also only evaluates index 1
- original useage by WH
=> suggestions when typing into searchboxes
array:[
str, // search
str[10], // found
[], // unused - description for found result?
str[10], // url to found result
[], // unused
[], // unused
[], // unused
str[10][4] // type, typeId, param1 (4:quality, 3,6,9,10,17:icon, 5,11:faction), param2 (3:quality, 6:rank)
]
WH walked away from this hybrid approach and has separate endpoints for internal search suggestions and opensearch, with the latter only providing found text (index 1)
we move the appendix of ' (TypeName)' on found text to descriptions as it fucks over Firefox users when they apply the suggestion
*/
class SearchOpenResponse extends TextResponse implements ICache
{
use TrCache, TrSearch;
private const /* int */ SEARCH_MODS_OPEN =
1 << Search::MOD_CLASS | 1 << Search::MOD_RACE | 1 << Search::MOD_TITLE | 1 << Search::MOD_WORLDEVENT |
1 << Search::MOD_CURRENCY | 1 << Search::MOD_ITEMSET | 1 << Search::MOD_ITEM | 1 << Search::MOD_ABILITY |
1 << Search::MOD_TALENT | 1 << Search::MOD_CREATURE | 1 << Search::MOD_QUEST | 1 << Search::MOD_ACHIEVEMENT |
1 << Search::MOD_ZONE | 1 << Search::MOD_OBJECT | 1 << Search::MOD_FACTION | 1 << Search::MOD_SKILL |
1 << Search::MOD_PET;
protected string $contentType = MIME_TYPE_OPENSEARCH;
protected int $cacheType = CACHE_TYPE_SEARCH;
protected array $expectedGET = array(
'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']]
);
public function __construct(string $pageParam)
{
parent::__construct($pageParam); // just to set g_user and g_locale
$this->query = $this->_get['search']; // technically pageParam, but prepared
if ($limit = Cfg::get('SQL_LIMIT_QUICKSEARCH'))
$this->maxResults = $limit;
$this->searchMask = Search::TYPE_OPEN | self::SEARCH_MODS_OPEN;
$this->searchObj = new Search($this->query, $this->searchMask, $this->maxResults);
}
protected function generate() : void
{
// this one is funny: we want 10 results, ideally equally distributed over each type
$foundTotal = 0;
$result = array( // 0:query, 1:[names], 3:[links]; 7:[extraInfo]
$this->query,
[], [], [], [], [], [], []
);
// invalid conditions: not enough characters to search OR no types to search
if (!$this->searchObj->canPerform())
$this->generate404($this->query);
foreach ($this->searchObj->perform() as [, , $nMatches, , , ])
$foundTotal += $nMatches;
foreach ($this->searchObj->perform() as [$data, $type, $nMatches, $param1, $param2, $desc])
{
$max = max(1, intVal($this->maxResults * $nMatches / $foundTotal));
$i = 0;
foreach ($data as $id => $name)
{
if (++$i > $max)
break;
if (count($result[1]) >= $this->maxResults)
break 2;
$result[1][] = $name; // originally - $name . ' ('.$desc.')'
$result[2][] = $desc; // .. and here empty
$result[3][] = Cfg::get('HOST_URL').'/?'.Type::getFileString($type).'='.$id;
$extra = [$type, $id]; // type, typeId
if (isset($param1[$id]))
$extra[] = $param1[$id]; // param1
if (isset($param2[$id]))
$extra[] = $param2[$id]; // param2
$result[7][] = $extra;
}
}
$this->result = Util::toJSON($result);
}
public function generate404(?string $search = null) : never
{
parent::generate404(Util::toJSON([$search, [], [], [], [], [], [], []]));
}
}
?>

File diff suppressed because it is too large Load Diff

View File

@@ -79,41 +79,38 @@ class WorldEventList extends DBTypeList
return $row ? new LocString($row) : null;
}
public static function updateDates($date = null)
public static function updateDates(?array $date = null, ?int &$start = null, ?int &$end = null, ?int &$rec = null) : bool
{
if (!$date || empty($date['firstDate']) || empty($date['length']))
{
return array(
'start' => 0,
'end' => 0,
'rec' => 0
);
return false;
$start = $date['firstDate'];
$end = $date['firstDate'] + $date['length'];
$rec = $date['rec'] ?: -1; // interval
if ($rec < 0 || $date['lastDate'] < time())
return true;
$nIntervals = ceil((time() - $start) / $rec);
$start += $nIntervals * $rec;
$end += $nIntervals * $rec;
return true;
}
// Convert everything to seconds
$firstDate = intVal($date['firstDate']);
$lastDate = !empty($date['lastDate']) ? intVal($date['lastDate']) : 5000000000; // in the far far FAR future..;
$interval = !empty($date['rec']) ? intVal($date['rec']) : -1;
$length = intVal($date['length']);
$curStart = $firstDate;
$curEnd = $firstDate + $length;
$nextStart = $curStart + $interval;
$nextEnd = $curEnd + $interval;
while ($interval > 0 && $nextEnd <= $lastDate && $curEnd < time())
public static function updateListview(Listview &$listview) : void
{
$curStart = $nextStart;
$curEnd = $nextEnd;
$nextStart = $curStart + $interval;
$nextEnd = $curEnd + $interval;
}
foreach ($listview->iterate() as &$row)
{
WorldEventList::updateDates($row['_date'] ?? null, $start, $end, $rec);
return array(
'start' => $curStart,
'end' => $curEnd,
'rec' => $interval
);
$row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null;
$row['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : null;
$row['rec'] = $rec;
unset($row['_date']);
}
}
public function getListviewData(bool $forNow = false) : array
@@ -139,11 +136,8 @@ class WorldEventList extends DBTypeList
{
foreach ($data as &$d)
{
$u = self::updateDates($d['_date']);
self::updateDates($d['_date'], $d['startDate'], $d['endDate'], $d['rec']);
unset($d['_date']);
$d['startDate'] = $u['start'];
$d['endDate'] = $u['end'];
$d['rec'] = $u['rec'];
}
}

View File

@@ -55,12 +55,6 @@ define ('SC_FLAG_APPEND_LOCALE', 0x04);
define ('SC_FLAG_LOCALIZED', 0x08);
define('SEARCH_TYPE_REGULAR', 0x10000000);
define('SEARCH_TYPE_OPEN', 0x20000000);
define('SEARCH_TYPE_JSON', 0x40000000);
define('SEARCH_MASK_OPEN', 0x007DC1FF); // open search
define('SEARCH_MASK_ALL', 0x0FFFFFFF); // normal search
// Databases
define('DB_AOWOW', 0);
define('DB_WORLD', 1);

View File

@@ -304,13 +304,13 @@ class EventPage extends GenericPage
protected function postCache()
{
// update dates to now()
$updated = WorldEventList::updateDates($this->dates);
WorldEventList::updateDates($this->dates, $start, $end, $rec);
if ($this->mode == CACHE_TYPE_TOOLTIP)
{
return array(
date(Lang::main('dateFmtLong'), $updated['start']),
date(Lang::main('dateFmtLong'), $updated['end'])
date(Lang::main('dateFmtLong'), $start),
date(Lang::main('dateFmtLong'), $end)
);
}
else
@@ -323,19 +323,19 @@ class EventPage extends GenericPage
/********************/
// start
if ($updated['start'])
array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['start']));
if ($start)
array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $start));
// end
if ($updated['end'])
array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['end']));
if ($end)
array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $end));
// occurence
if ($updated['rec'] > 0)
array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($updated['rec'] * 1000));
if ($rec > 0)
array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($rec * 1000));
// in progress
if ($updated['start'] < time() && $updated['end'] > time())
if ($start < time() && $end > time())
array_push($this->infobox, '[span class=q2]'.Lang::event('inProgress').'[/span]');
$this->infobox = '[ul][li]'.implode('[/li][li]', $this->infobox).'[/li][/ul]';
@@ -351,11 +351,11 @@ class EventPage extends GenericPage
foreach ($view[1]['data'] as &$data)
{
$updated = WorldEventList::updateDates($data['_date']);
WorldEventList::updateDates($data['_date'], $start, $end, $rec);
unset($data['_date']);
$data['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false;
$data['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false;
$data['rec'] = $updated['rec'];
$data['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : false;
$data['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : false;
$data['rec'] = $rec;
}
}
}

View File

@@ -96,11 +96,11 @@ class EventsPage extends GenericPage
continue;
}
$updated = WorldEventList::updateDates($data['_date']);
WorldEventList::updateDates($data['_date'], $start, $end, $rec);
unset($data['_date']);
$data['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false;
$data['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false;
$data['rec'] = $updated['rec'];
$data['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : false;
$data['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : false;
$data['rec'] = $rec;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -18804,7 +18804,9 @@ var LiveSearch = new function() {
function highlight(match, $1) {
// $1 containts % in matches with %s, which we don't want to replace
return ($1 ? match : '<b><u>' + match + '</u></b>');
return ($1 ? '<b><u>' + match + '</u></b>' : match);
// aowow - why was the ternary reversed? Also how can it not match .. we explicitly searched for it.
// return ($1 ? match : '<b><u>' + match + '</u></b>');
}
function display(textbox, search, suggz, dataz) {

View File

@@ -1,7 +1,10 @@
<?php namespace Aowow; ?>
<?php
namespace Aowow\Template;
<?php $this->brick('header'); ?>
use \Aowow\Lang;
$this->brick('header');
?>
<div class="main" id="main">
<div class="main-precontents" id="main-precontents"></div>
<div class="main-contents" id="main-contents">
@@ -13,12 +16,12 @@ $this->brick('pageTemplate');
?>
<div class="text">
<a href="<?=$this->wowheadLink; ?>" class="button-red"><em><b><i>Wowhead</i></b><span>Wowhead</span></em></a>
<?php
if ($this->lvTabs):
echo ' <h1>'.Lang::main('foundResult').' <i>'.Util::htmlEscape($this->search).'</i>';
if ($this->invalid):
echo '<span class="sub">'.sprintf(Lang::main('ignoredTerms'), implode(', ', $this->invalid)).'</span>';
$this->brick('redButtons');
if (count($this->lvTabs)):
echo ' <h1>'.Lang::main('foundResult').' <i>'.$this->search.'</i>';
if ($this->invalidTerms):
echo '<span class="sub">'.Lang::main('ignoredTerms', [$this->invalidTerms]).'</span>';
endif;
echo "</h1>\n";
?>
@@ -27,9 +30,9 @@ if ($this->lvTabs):
$this->brick('lvTabs');
else:
echo ' <h1>'.Lang::main('noResult').' <i>'.Util::htmlEscape($this->search).'</i>';
if ($this->invalid):
echo '<span class="sub">'.sprintf(Lang::main('ignoredTerms'), implode(', ', $this->invalid)).'</span>';
echo ' <h1>'.Lang::main('noResult').' <i>'.$this->search.'</i>';
if ($this->invalidTerms):
echo '<span class="sub">'.Lang::main('ignoredTerms', [$this->invalidTerms]).'</span>';
endif;
echo "</h1>\n";
?>