diff --git a/includes/ajaxHandler/ajaxHandler.class.php b/includes/ajaxHandler/ajaxHandler.class.php deleted file mode 100644 index 5094e92f..00000000 --- a/includes/ajaxHandler/ajaxHandler.class.php +++ /dev/null @@ -1,71 +0,0 @@ -params = $params; - - $this->initRequestData(); - } - - public function handle(string &$out) : bool - { - if (!$this->handler) - return false; - - if ($this->validParams) - { - if (count($this->params) != 1) - return false; - - if (!in_array($this->params[0], $this->validParams)) - return false; - } - - $out = $this->{$this->handler}() ?? ''; - - return true; - } - - public function getContentType() : string - { - return $this->contentType; - } - - protected function reqPOST(string ...$keys) : bool - { - foreach ($keys as $k) - if (!isset($this->_post[$k]) || $this->_post[$k] === null || $this->_post[$k] === '') - return false; - - return true; - } - - protected function reqGET(string ...$keys) : bool - { - foreach ($keys as $k) - if (!isset($this->_get[$k]) || $this->_get[$k] === null || $this->_get[$k] === '') - return false; - - return true; - } -} - -?> diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php index 8286f114..8f307f40 100644 --- a/includes/components/dbtypelist.class.php +++ b/includes/components/dbtypelist.class.php @@ -632,10 +632,7 @@ trait spawnHelper $wpSum = []; $wpIdx = 0; $worldPos = []; - $spawns = DB::Aowow()->select("SELECT * FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0", self::$type, $this->getFoundIDs()); - - if (!$spawns) - return; + $spawns = DB::Aowow()->select("SELECT * FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0", self::$type, $this->getFoundIDs()) ?: []; if (!$skipAdmin && User::isInGroup(U_GROUP_MODERATOR)) if ($guids = array_column(array_filter($spawns, fn($x) => $x['guid'] > 0 || $x['type'] != Type::NPC), 'guid')) diff --git a/includes/components/frontend/announcement.class.php b/includes/components/frontend/announcement.class.php index 8823428f..16382009 100644 --- a/includes/components/frontend/announcement.class.php +++ b/includes/components/frontend/announcement.class.php @@ -43,7 +43,7 @@ class Announcement implements \JsonSerializable public function jsonSerialize() : array { $json = array( - 'parent' => 'announcement-' . abs($this->id), + 'parent' => 'announcement-' . $this->id, 'id' => $this->editable ? -$this->id : $this->id, 'mode' => $this->mode, 'status' => $this->status, diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php new file mode 100644 index 00000000..c53c58b5 --- /dev/null +++ b/includes/components/pagetemplate.class.php @@ -0,0 +1,559 @@ +locale = Lang::getLocale(); + $this->gStaticUrl = Cfg::get('STATIC_URL'); + $this->gHost = Cfg::get('HOST_URL'); + $this->analyticsTag = Cfg::get('GTAG_MEASUREMENT_ID'); + $this->gServerTime = sprintf("new Date('%s')", date(Util::$dateFormatInternal)); + $this->user = User::class; + } + + public function addDataLoader(string ...$dataFile) : void + { + foreach ($dataFile as $df) + $this->dataLoader[] = $df; + } + + public function addScript(int $type, string $str, int $flags = 0x0) : bool + { + $tpl = match ($type) + { + SC_CSS_FILE => '', + SC_CSS_STRING => '', + SC_JS_FILE => '', + SC_JS_STRING => '', + default => '' + }; + + if (!$tpl || !$str) + + if (!$str) + { + trigger_error('PageTemplate::addScript - content empty', E_USER_WARNING); + return false; + } + + if (!$tpl) + { + trigger_error('PageTemplate::addScript - unknown script type #'.$type, E_USER_WARNING); + return false; + } + + // insert locale string + if ($flags & SC_FLAG_LOCALIZED) + $str = sprintf($str, Lang::getLocale()->json()); + + $this->scripts[] = [$type, $str, $flags, $tpl]; + return true; + } + + /* (optional) set pre-render hooks */ + + public function registerDisplayHook(string $var, callable $fn) : void + { + $this->displayHooks[$var][] = $fn; + } + + private function getDisplayHooks(string $var) : array + { + return $this->displayHooks[$var] ?? []; + } + + /* 3) self test, ready to be cached now */ + + public function prepare() : bool + { + if (!self::test('template/pages/', $this->template)) + { + trigger_error('Error: nonexistent template requested: template/pages/'.$this->template.'.tpl.php', E_USER_ERROR); + return false; + } + + // TODO - more checks and preparations + + return true; + } + + /* 4) display */ + + public function render() : void + { + $this->update(); + + include('template/pages/'.$this->template.'.tpl.php'); + } + + + /***********/ + /* loaders */ + /***********/ + + // "template_exists" + public static function test(string $path, string $file) : bool + { + if (!preg_match('/^[\w\-_]+(\.tpl(\.php)?)?$/i', $file)) + return false; + + if ($path && preg_match('/\\{2,}|\/{2,}|\.{2,}|~/i', $path)) + return false; + + if (!is_file('template/'.$path.$file)) + return false; + + return true; + } + + // load brick + private function brick(string $file, array $localVars = []) : void + { + $file .= '.tpl.php'; + + if (!self::test('bricks/', $file)) + { + trigger_error('Nonexistent template requested: template/bricks/'.$file, E_USER_ERROR); + return; + } + + foreach ($localVars as $n => $v) + $$n = $v; + + include('template/bricks/'.$file); + } + + private function brickIf(mixed $boolish, string $file, array $localVars = []) : void + { + if ($boolish) + $this->brick($file, $localVars); + } + + // load brick with more text then vars + private function localizedBrick(string $file, array $localVars = []) : void + { + foreach ($localVars as $n => $v) + $$n = $v; + + $_file = $file.'_'.$this->locale->value.'.tpl.php'; + if (self::test('localized/', $_file)) + { + include('template/localized/'.$_file); + return; + } + + $_file = $file.'_'.$this->locale->getFallback()->value.'.tpl.php'; + if (self::test('localized/', $_file)) + { + include('template/localized/'.$_file); + return; + } + + trigger_error('Nonexistent template requested: template/localized/'.$_file, E_USER_ERROR); + } + + private function localizedBrickIf(mixed $boolish, string $file, array $localVars = []) : void + { + if ($boolish) + $this->localizedBrick($file, $localVars); + } + + + /****************/ + /* Util wrapper */ + /****************/ + + private function cfg(string $name) : mixed + { + return Cfg::get($name); + } + + private function json(mixed $var, int $jsonFlags = 0x0) : string + { + if (is_string($var) && $this->$var) + $var = $this->$var; + + return preg_replace('/script\s*\>/i', 'scr"+"ipt>', Util::toJSON($var, $jsonFlags)); + } + + private function escHTML(string $var) : string|array + { + return Util::htmlEscape($this->$var ?? $var); + } + + private function escJS(string $var) : string|array + { + return Util::jsEscape($this->$var ?? $var); + } + + private function ucFirst(string $var) : string + { + return Util::ucFirst($this->$var ?? $var); + } + + + /*****************/ + /* render helper */ + /*****************/ + + private function concat(string $arrVar, string $separator = '') : string + { + if (!is_array($this->$arrVar)) + return ''; + + return implode($separator, $this->$arrVar); + } + + private function renderArray(string|array $arrVar, int $lpad = 0) : string + { + $data = []; + if (is_string($arrVar) && isset($this->$arrVar) && is_array($this->$arrVar)) + $data = $this->$arrVar; + else if (is_array($arrVar)) + $data = $arrVar; + + $buff = ''; + foreach ($data as $x) + $buff .= str_repeat(' ', $lpad) . $x . "\n"; + + return $buff; + } + + // load jsGlobals + private function renderGlobalVars(int $lpad = 0) : string + { + $buff = ''; + + if ($this->guideRating) + $buff .= str_repeat(' ', $lpad).sprintf(self::GUIDE_RATING_TPL, ...$this->guideRating); + + foreach ($this->jsGlobals as [$jsVar, $data, $extraData]) + { + $buff .= str_repeat(' ', $lpad).'var _ = '.$jsVar.';'; + + foreach ($data as $key => $data) + $buff .= ' _['.(is_numeric($key) ? $key : "'".$key."'")."]=".Util::toJSON($data).';'; + + $buff .= "\n"; + + if (isset($this->gPageInfo['type']) && isset($this->gPageInfo['typeId']) && isset($extraData[$this->gPageInfo['typeId']])) + { + $buff .= "\n"; + foreach ($extraData[$this->gPageInfo['typeId']] as $k => $v) + if ($v) + $buff .= str_repeat(' ', $lpad).'_['.$this->gPageInfo['typeId'].'].'.$k.' = '.Util::toJSON($v).";\n"; + $buff .= "\n"; + } + } + + return $buff; + } + + private function renderSeriesItem(int $idx, array $list, int $lpad = 0) : string + { + $result = ''.($idx + 1).'
'; + + $end = array_key_last($list); + foreach ($list as $k => $i) // itemItr + { + $wrap = match ($i['side']) + { + SIDE_ALLIANCE => '%s', + SIDE_HORDE => '%s', + default => '%s' + }; + + if ($i['typeId'] == $this->typeId) + $result .= sprintf($wrap, ''.$i['name'].''); + else + $result .= sprintf($wrap, ''.$i['name'].''); + + if ($end != $k) + $result .= '
'; + + } + + return str_repeat(' ', $lpad) . $result . "
\n"; + } + + private function renderFilter(int $lpad = 0) : string + { + $result = []; + + // it's worth noting, that this only works on non-cached page calls. Luckily Profiler pages are not cached. + if ($this->context instanceof \Aowow\IProfilerList) + { + $result[] = "pr_setRegionRealm(\$WH.ge('fi').firstChild, '".$this->region."', '".$this->realm."');"; + + if ($this->filter->values['ra']) + $result[] = "pr_onChangeRace();"; + } + + if ($this->filter->fiInit) // str: filter template (and init html form) + $result[] = "fi_init('".$this->filter->fiInit."');"; + else if ($this->filter->fiType) // str: filter template (set without init) + $result[] = "var fi_type = '".$this->filter->fiType."'"; + + if ($this->filter->fiSetCriteria) // arr:criteria, arr:signs, arr:values + $result[] = 'fi_setCriteria('.mb_substr(Util::toJSON(array_values($this->filter->fiSetCriteria)), 1, -1).");"; + + /* + nt: don't try to match provided weights on predefined weight sets (preselects preset from opt list and ..?) + ids: weights are encoded as ids, not by their js name and need conversion before use + stealth: the ub-selector (items filter) will not visually change (so what..?) + */ + if ($this->filter->fiSetWeights) // arr:weights, bool:nt[0], bool:ids[1], bool:stealth[1] + $result[] = 'fi_setWeights('.Util::toJSON(array_values($this->filter->fiSetWeights)).', 0, 1, 1);'; + + if ($this->filter->fiExtraCols) // arr:extraCols + $result[] = 'fi_extraCols = '.Util::toJSON(array_values(array_unique($this->filter->fiExtraCols))).";"; + + return str_repeat(' ', $lpad)."\n"; + } + + private function makeOptionsList(array $data, mixed $selectedIdx = null, int $lpad = 0, ?callable $callback = null) : string + { + $callback ??= fn(&$v, &$k) => $v; // default callback: skip empty descriptors + $options = ''; + + foreach ($data as $idx => $str) + { + $extraAttributes = []; + if (!$callback($str, $idx, $extraAttributes)) + continue; + + if ($idx === '' || !$str) + continue; + + $options .= str_repeat(' ', max(0, $lpad)).' $v) + $options .= ' '.$k.'="'.$v.'"'; + + if (is_array($selectedIdx) && in_array($idx, $selectedIdx)) + $options .= ' selected="selected"'; + else if (!is_null($selectedIdx) && $selectedIdx == $idx) + $options .= ' selected="selected"'; + + $options .= ' value="'.$idx.'">'.$str.''.($lpad < 0 ? '' : "\n"); + } + + return $options; + } + + private function makeRadiosList(string $name, array $data, mixed $selectedIdx = null, int $lpad = 0, ?callable $callback = null) : string + { + $callback ??= fn(&$v, &$k) => $v; // default callback: skip empty descriptors + $options = ''; + + foreach ($data as $idx => [$title, $id]) + { + $extraAttributes = []; + if (!$callback($title, $idx, $extraAttributes)) + continue; + + if ($id === '' || !$title) + continue; + + $options .= str_repeat(' ', max(0, $lpad)).' $v) + $options .= ' '.$k.'="'.$v.'"'; + + $options .= '>'.$title.''.($lpad < 0 ? '' : "\n"); + } + + return $options; + } + + // unordered stuff + + private function prepareScripts() : void + { + $this->js = $this->css = []; + + foreach ($this->scripts as [$type, $str, $flags, $tpl]) + { + $app = []; + + if (($flags & SC_FLAG_APPEND_LOCALE) && $this->locale != \Aowow\Locale::EN) + $app[] = 'lang='.$this->locale->domain(); + + // append anti-cache timestamp + if (!($flags & SC_FLAG_NO_TIMESTAMP)) + if ($type == SC_JS_FILE || $type == SC_CSS_FILE) + $app[] = filemtime('static/'.$str) ?: 0; + + if ($app) + $appendix = '?'.implode('&', $app); + + if ($type == SC_JS_FILE || $type == SC_CSS_FILE) + $str = Cfg::get('STATIC_URL').'/'.$str; + + if ($flags & SC_FLAG_PREFIX) + { + if ($type == SC_JS_FILE || $type == SC_JS_STRING) + array_unshift($this->js, sprintf($tpl, $str, $appendix ?? '')); + else + array_unshift($this->css, sprintf($tpl, $str, $appendix ?? '')); + } + else + { + if ($type == SC_JS_FILE || $type == SC_JS_STRING) + array_push($this->js, sprintf($tpl, $str, $appendix ?? '')); + else + array_push($this->css, sprintf($tpl, $str, $appendix ?? '')); + } + } + + if ($data = array_unique($this->dataLoader)) + { + $args = array( + 'data' => implode('.', $data), + 'locale' => $this->locale->value, + 't' => $_SESSION['dataKey'] + ); + + array_push($this->js, ''); + } + } + + // refresh vars that shouldn't be cached + private function update() : void + { + // analytics + consent + if (!isset($_COOKIE['consent'])) + { + $this->addScript(SC_CSS_FILE, 'css/consent.css'); + $this->addScript(SC_JS_FILE, 'js/consent.js'); + + $this->consentFooter = true; + $this->analyticsTag = null; + } + else if ($this->analyticsTag && !$_COOKIE['consent']) + $this->analyticsTag = null; + + // js + css + $this->prepareScripts(); + + // db profiling + if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) + $this->dbProfiles = \Aowow\DB::getProfiles(); + } + + public function setListviewError() : void + { + if (!$this->lvTabs) + return; + + foreach ($this->lvTabs->iterate() as $lv) + if ($lv instanceof \Aowow\Listview) + $lv->setError(); + } + + // pre-serialization: if a var is relevant it was stored in $rawData + public function __sleep() : array + { + $this->context = null; // unlink from TemplateResponse + $this->pageData = []; // clear modified data + + $vars = []; + foreach ($this as $k => $_) + $vars[] = $k; + + return $vars; + } + + public function __wakeup() : void + { + $this->gStaticUrl = Cfg::get('STATIC_URL'); + $this->gHost = Cfg::get('HOST_URL'); + $this->analyticsTag = Cfg::get('GTAG_MEASUREMENT_ID'); + $this->gServerTime = sprintf("new Date('%s')", date(Util::$dateFormatInternal)); + } + + public function __set(string $var, mixed $value) : void + { + $this->pageData[$var] = $value; + } + + public function __get(string $var) : mixed + { + // modified data exists + if (isset($this->pageData[$var])) + return $this->pageData[$var]; + + if (!isset($this->rawData[$var])) + { + if (!$this->context) + return null; + + if (!property_exists($this->context, $var)) + return null; + + $this->rawData[$var] = $this->context->$var; + } + + if ($hooks = $this->getDisplayHooks($var)) + { + if (is_object($this->rawData[$var])) // is frontend component + $this->pageData[$var] = clone $this->rawData[$var]; + else + $this->pageData[$var] = $this->rawData[$var]; + + foreach ($hooks as $fn) + $fn($this, $this->pageData[$var]); + } + + return $this->pageData[$var] ?? $this->rawData[$var]; + } +} diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php new file mode 100644 index 00000000..2be3beb7 --- /dev/null +++ b/includes/components/response/baseresponse.class.php @@ -0,0 +1,657 @@ + ACC_STATUS_CHANGE_PASS) + return Lang::main('intError'); + + // check if already processing + if ($_ = DB::Aowow()->selectCell('SELECT `statusTimer` - UNIX_TIMESTAMP() FROM ?_account WHERE `email` = ? AND `status` > ?d AND `statusTimer` > UNIX_TIMESTAMP()', $email, ACC_STATUS_NEW)) + return sprintf(Lang::account('isRecovering'), Util::formatTime($_ * 1000)); + + // create new token and write to db + $token = Util::createHash(); + if (!DB::Aowow()->query('UPDATE ?_account SET `token` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d WHERE `email` = ?', $token, $newStatus, Cfg::get('ACC_RECOVERY_DECAY'), $email)) + return Lang::main('intError'); + + // send recovery mail + if (!Util::sendMail($email, $mailTemplate, [$token], Cfg::get('ACC_RECOVERY_DECAY'))) + return sprintf(Lang::main('intError2'), 'send mail'); + + return ''; + } +} + +trait TrGetNext +{ + private function getNext(bool $forHeader = false) : string + { + $next = ''; + if (!empty($this->_get['next'])) + $next = $this->_get['next']; + else if (isset($_SERVER['HTTP_REFERER']) && strstr($_SERVER['HTTP_REFERER'], '?')) + $next = explode('?', $_SERVER['HTTP_REFERER'])[1]; + else if ($forHeader) + return '.'; + + return ($forHeader ? '?' : '').$next; + } +} + + +Interface ICache +{ + public function saveCache(string|Template\PageTemplate $toCache) : void; + public function loadCache(bool|string|Template\PageTemplate &$fromCache) : bool; + public function setOnCacheLoaded(callable $callback, mixed $params = null) : void; + public function getCacheKeyComponents() : array; + public function applyOnCacheLoaded(mixed &$data) : mixed; +} + +trait TrCache +{ + private const STORE_METHOD_OBJECT = 0; + private const STORE_METHOD_STRING = 1; + + private int $_cacheType = CACHE_TYPE_NONE; + private int $skipCache = 0x0; + private ?int $decay = null; + private string $cacheDir = 'cache/template/'; + private bool $cacheInited = false; + private ?\Memcached $memcached = null; + private array $onCacheLoaded = [null, null]; // post-load updater + + public static array $cacheStats = []; // load info for page footer + + // visible properties or given strings are cached + public function saveCache(string|object $toCache) : void + { + $this->initCache(); + + if ($this->_cacheType == CACHE_TYPE_NONE) + return; + + if (!Cfg::get('CACHE_MODE') /* || Cfg::get('DEBUG') */) + return; + + if (!$this->decay) + return; + + $cKey = $this->formatCacheKey(); + $method = is_object($toCache) ? self::STORE_METHOD_OBJECT : self::STORE_METHOD_STRING; + + if ($method == self::STORE_METHOD_OBJECT) + $toCache = serialize($toCache); + else + $toCache = (string)$toCache; + + if (is_callable($this->onCacheLoaded[0])) + $postCache = serialize($this->onCacheLoaded); + + if (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED) + { + // on &refresh also clear related + if ($this->skipCache & CACHE_MODE_MEMCACHED) + $this->deleteCache(CACHE_MODE_MEMCACHED); + + $data = array( + 'timestamp' => time(), + 'lifetime' => $this->decay, + 'revision' => AOWOW_REVISION, + 'method' => $method, + 'postCache' => $postCache ?? null, + 'data' => $toCache + ); + + $this->memcached()?->set($cKey[2], $data); + } + + if (Cfg::get('CACHE_MODE') & CACHE_MODE_FILECACHE) + { + $data = time()." ".$this->decay." ".AOWOW_REVISION." ".$method."\n"; + $data .= ($postCache ?? '')."\n"; + $data .= gzcompress($toCache, 9); + + // on &refresh also clear related + if ($this->skipCache & CACHE_MODE_FILECACHE) + $this->deleteCache(CACHE_MODE_FILECACHE); + + if (Util::writeDir($this->cacheDir . implode(DIRECTORY_SEPARATOR, array_slice($cKey, 0, 2)))) + file_put_contents($this->cacheDir . implode(DIRECTORY_SEPARATOR, $cKey), $data); + } + } + + public function loadCache(mixed &$fromCache) : bool + { + $this->initCache(); + + if ($this->_cacheType == CACHE_TYPE_NONE) + return false; + + if (!Cfg::get('CACHE_MODE') /* || Cfg::get('DEBUG') */) + return false; + + $cKey = $this->formatCacheKey(); + $rev = $method = $data = $postCache = null; + + if ((Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED) && !($this->skipCache & CACHE_MODE_MEMCACHED)) + { + if ($cache = $this->memcached()?->get($cKey[2])) + { + $method = $cache['method']; + $data = $cache['data']; + $postCache = $cache['postCache']; + + if (($cache['timestamp'] + $cache['lifetime']) > time() && $cache['revision'] == AOWOW_REVISION) + self::$cacheStats = [CACHE_MODE_MEMCACHED, $cache['timestamp'], $cache['lifetime']]; + } + } + + if (!$data && (Cfg::get('CACHE_MODE') & CACHE_MODE_FILECACHE) && !($this->skipCache & CACHE_MODE_FILECACHE)) + { + $file = $this->cacheDir . implode(DIRECTORY_SEPARATOR, $cKey); + if (!file_exists($file)) + return false; + + $content = file_get_contents($file); + if (!$content) + return false; + + [$head, $postCache, $data] = explode("\n", $content, 3); + if (substr_count($head, ' ') != 3) + return false; + + [$time, $lifetime, $rev, $method] = explode(' ', $head); + + if (($time + $lifetime) < time() || $rev != AOWOW_REVISION) + return false; + + self::$cacheStats = [CACHE_MODE_FILECACHE, $time, $lifetime]; + $data = gzuncompress($data); + } + + if (!$data) + return false; + + if ($postCache) + $this->onCacheLoaded = unserialize($postCache); + + $fromCache = false; + if ($method == self::STORE_METHOD_OBJECT) + $fromCache = unserialize($data); + else if ($method == self::STORE_METHOD_STRING) + $fromCache = $data; + + return $fromCache !== false; + } + + public function deleteCache(int $modeMask = 0x3) : void + { + $this->initCache(); + + // type+typeId+catg; 3+6+10 + $cKey = $this->formatCacheKey(); + $cKey[2] = substr($cKey[2], 0, 19); + + if ($modeMask & CACHE_MODE_MEMCACHED) + foreach ($this->memcached()?->getAllKeys() ?? [] as $k) + if (strpos($k, $cKey[2]) === 0) + $this->memcached()?->delete($k); + + if ($modeMask & CACHE_MODE_FILECACHE) + foreach (glob(implode(DIRECTORY_SEPARATOR, $cKey).'*') as $file) + unlink($file); + } + + private function memcached() : ?\Memcached + { + if (!$this->memcached && (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED)) + { + $this->memcached = new \Memcached(); + $this->memcached->addServer('localhost', 11211); + } + + return $this->memcached; + } + + private function initCache() : void + { + // php's missing trait property conflict resolution is going to be the end of me + // also allow reevaluation even if inited. It may have changed in generate(), because of an error. + if (isset($this->cacheType)) + $this->_cacheType = $this->cacheType; + + if ($this->cacheInited) + return; + + // force refresh + if (isset($_GET['refresh']) && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV)) + { + if ($_GET['refresh'] == 'filecache') + $this->skipCache = CACHE_MODE_FILECACHE; + else if ($_GET['refresh'] == 'memcached') + $this->skipCache = CACHE_MODE_MEMCACHED; + else if ($_GET['refresh'] == '') + $this->skipCache = CACHE_MODE_FILECACHE | CACHE_MODE_MEMCACHED; + } + + $this->decay ??= Cfg::get('CACHE_DECAY'); + + $cacheDir = Cfg::get('CACHE_DIR'); + if ($cacheDir && Util::writeDir($cacheDir)) + $this->cacheDir = mb_substr($cacheDir, -1) != '/' ? $cacheDir.'/' : $cacheDir; + + $this->cacheInited = true; + } + + // https://stackoverflow.com/questions/466521 + private function formatCacheKey() : array + { + [$dbType, $dbTypeId, $category, $staffMask, $miscInfo] = $this->getCacheKeyComponents(); + + $fileKey = ''; + // DBType: 3 + $fileKey .= str_pad(dechex($dbType & 0xFFF), 3, 0, STR_PAD_LEFT); + // DBTypeId: 6 + $fileKey .= str_pad(dechex($dbTypeId & 0xFFFFFF), 6, 0, STR_PAD_LEFT); + // category: (2+4+4) + $fileKey .= str_pad(dechex($category & 0xFFFFFFFFFF), 2+4+4, 0, STR_PAD_LEFT); + // cacheType: 1 + $fileKey .= str_pad(dechex($this->_cacheType & 0xF), 1, 0, STR_PAD_LEFT); + // localeId: 2, + $fileKey .= str_pad(dechex(Lang::getLocale()->value & 0xFF), 2, 0, STR_PAD_LEFT); + // staff mask: 4 + $fileKey .= str_pad(dechex($staffMask & 0xFFFFFFFF), 4, 0, STR_PAD_LEFT); + // optioal: miscInfo + if ($miscInfo) + $fileKey .= '-'.$miscInfo; + + // topDir, 2ndDir, file + return array( + str_pad(dechex($dbType & 0xFF), 2, 0, STR_PAD_LEFT), + str_pad(dechex(($dbTypeId > 0 ? $dbTypeId : $category) & 0xFF), 2, 0, STR_PAD_LEFT), + $fileKey + ); + } + + public function setOnCacheLoaded(callable $callback, mixed $params = null) : void + { + $this->onCacheLoaded = [$callback, $params]; + } + + public function applyOnCacheLoaded(mixed &$data) : mixed + { + if (is_callable($this->onCacheLoaded[0])) + return $this->onCacheLoaded[0]($data, $this->onCacheLoaded[1]); + + return $data; + } + + public function setCacheDecay(int $seconds) : void + { + if ($seconds < 0) + return; + + $this->decay = $seconds; + } + + abstract public function getCacheKeyComponents() : array; +} + +trait TrSearch +{ + private int $maxResults = 500; + private string $query = ''; // sanitized search string + private int $searchMask = 0; // what to search for + private Search $searchObj; + + public function getCacheKeyComponents() : array + { + return array( + -1, // DBType + -1, // DBTypeId + $this->searchMask, // category + User::$groups, // staff mask + md5($this->query) // misc (here search query) + ); + } +} + +Interface IProfilerList +{ + public function getRegions() : void; +} + +trait TrProfiler +{ + protected int $realmId = 0; + protected string $battlegroup = ''; // not implemented, since no pserver supports it + + public string $region = ''; + public string $realm = ''; + + private function getSubjectFromUrl(string $pageParam) : void + { + if (!$pageParam) + return; + + // cat[0] is always region + // cat[1] is realm or bGroup (must be realm if cat[2] is set) + // cat[2] is arena-team, guild or character + $cat = explode('.', mb_strtolower($pageParam), 3); + + $cat = array_map('urldecode', $cat); + + if (array_search($cat[0], Util::$regions) === false) + return; + + $this->region = $cat[0]; + + // if ($cat[1] == Profiler::urlize(Cfg::get('BATTLEGROUP'))) + // $this->battlegroup = Cfg::get('BATTLEGROUP'); + if (isset($cat[1])) + { + foreach (Profiler::getRealms() as $rId => $r) + { + if (Profiler::urlize($r['name'], true) == $cat[1]) + { + $this->realm = $r['name']; + $this->realmId = $rId; + if (isset($cat[2]) && mb_strlen($cat[2]) >= 2) + $this->subjectName = mb_strtolower($cat[2]); // cannot reconstruct original name from urlized form; match against special name field + + break; + } + } + } + } + + private function followBreadcrumbPath() : void + { + if ($this->region) + { + $this->breadcrumb[] = $this->region; + + if ($this->realm) + $this->breadcrumb[] = Profiler::urlize($this->realm, true); + // else + // $this->breadcrumb[] = Profiler::urlize(Cfg::get('BATTLEGROUP')); + } + } +} + +trait TrProfilerDetail +{ + use TrProfiler { TrProfiler::getSubjectFromUrl as _getSubjectFromUrl; } + + protected string $subjectName = ''; + + public int $typeId = 0; + public ?array $doResync = null; + + private function getSubjectFromUrl(string $pageParam) : void + { + if (!$pageParam) + return; + + if (Util::checkNumeric($pageParam, NUM_CAST_INT)) + $this->typeId = $pageParam; + else + $this->_getSubjectFromUrl($pageParam); + } + + private function handleIncompleteData(int $type, int $guid) : void + { + // queue full fetch + if ($newId = Profiler::scheduleResync($type, $this->realmId, $guid)) + { + $this->template = 'text-page-generic'; + $this->doResync = [Type::getFileString($type), $newId]; + $this->inputbox = ['inputbox-status', ['head' => Lang::profiler('firstUseTitle', [Util::ucFirst($this->subjectName), $this->realm])]]; + + return; + } + + // todo: base info should have been created in __construct .. why are we here..? + $this->forward('?'.Type::getFileString($type).'s='.$this->region.'.'.Profiler::urlize($this->realm, true).'&filter=na='.Util::ucFirst($this->subjectName).';ex=on'); + } +} + +trait TrProfilerList +{ + use TrProfiler; + + public array $regions = []; + + public function getRegions() : void + { + $usedRegions = array_column(Profiler::getRealms(), 'region'); + foreach (Util::$regions as $idx => $id) + if (in_array($id, $usedRegions)) + $this->regions[$id] = Lang::profiler('regions', $id); + } +} + + +abstract class BaseResponse +{ + protected const PATTERN_TEXT_LINE = '/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; + protected const PATTERN_TEXT_BLOB = '/[\x00-\x09\x0B-\x1F\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; + + protected static array $sql = []; // debug: sql stats container + + protected array $expectedPOST = []; // fill with variables you that are going to be used; eg: + protected array $expectedGET = []; // 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList'] + protected array $expectedCOOKIE = []; + + protected array $_post = []; // the filtered variable result + protected array $_get = []; + protected array $_cookie = []; + + protected int $requiredUserGroup = U_GROUP_NONE; // by default accessible to everone + protected bool $requiresLogin = false; // normal users and guests are both U_GROUP_NONE, soooo..... + protected mixed $result = null; + + public function __construct() + { + $this->initRequestData(); + + if (!User::isInGroup($this->requiredUserGroup)) + $this->onUserGroupMismatch(); + + if ($this->requiresLogin && !User::isLoggedIn()) + $this->onUserGroupMismatch(); + } + + public function process() : void + { + $fromCache = false; + + if ($this instanceof ICache) + $fromCache = $this->loadCache($this->result); + + if (!$this->result) + $this->generate(); + + $this->display(); + + if ($this instanceof ICache && !$fromCache) + $this->saveCache($this->result); + } + + private function initRequestData() : void + { + // php bug? If INPUT_X is empty, filter_input_array returns null/fails + // only really relevant for INPUT_POST + // manuall set everything null in this case + + if ($this->expectedPOST) + { + if ($_POST) + $this->_post = filter_input_array(INPUT_POST, $this->expectedPOST); + else + $this->_post = array_fill_keys(array_keys($this->expectedPOST), null); + } + + if ($this->expectedGET) + { + if ($_GET) + $this->_get = filter_input_array(INPUT_GET, $this->expectedGET); + else + $this->_get = array_fill_keys(array_keys($this->expectedGET), null); + } + + if ($this->expectedCOOKIE) + { + if ($_COOKIE) + $this->_cookie = filter_input_array(INPUT_COOKIE, $this->expectedCOOKIE); + else + $this->_cookie = array_fill_keys(array_keys($this->expectedCOOKIE), null); + } + } + + protected function forward(string $url = '') : never + { + $this->sendNoCacheHeader(); + header('Location: '.($url ?: '.'), true, 302); + exit; + } + + protected function forwardToSignIn(string $next = '') : never + { + $this->forward('?account=signin'.($next ? '&next='.$next : '')); + } + + protected function sumSQLStats() : void + { + Util::arraySumByKey(self::$sql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); + foreach (Profiler::getRealms() as $rId => $_) + Util::arraySumByKey(self::$sql, DB::Characters($rId)->getStatistics()); + } + + protected function sendNoCacheHeader() + { + header('Expires: Sat, 01 Jan 2000 01:00:00 GMT'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); + } + + + /****************************/ + /* required Parameter tests */ + /****************************/ + + protected function assertPOST(string ...$keys) : bool + { + foreach ($keys as $k) // not sent by browser || empty text field sent || validation failed + if (!isset($this->_post[$k]) || $this->_post[$k] === null || $this->_post[$k] === '' || $this->_post[$k] === false) + return false; + + return true; + } + + protected function assertGET(string ...$keys) : bool + { + foreach ($keys as $k) + if (!isset($this->_get[$k]) || $this->_get[$k] === null || $this->_get[$k] === '' || $this->_get[$k] === false) + return false; + + return true; + } + + protected function assertCOOKIE(string ...$keys) : bool + { + foreach ($keys as $k) + if (!isset($this->_cookie[$k]) || $this->_cookie[$k] === null || $this->_cookie[$k] === '' || $this->_cookie[$k] === false) + return false; + + return true; + } + + + /*******************************/ + /* Parameter validation checks */ + /*******************************/ + + protected static function checkRememberMe(string $val) : bool + { + return $val === 'yes'; + } + + protected static function checkCheckbox(string $val) : bool + { + return $val === 'on'; + } + + protected static function checkEmptySet(string $val) : bool + { + return $val === ''; // parameter is set and expected to be empty + } + + protected static function checkIdList(string $val) : array + { + if (preg_match('/^-?\d+(,-?\d+)*$/', $val)) + return array_map('intVal', explode(',', $val)); + + return []; + } + + protected static function checkIntArray(string $val) : array + { + if (preg_match('/^-?\d+(:-?\d+)*$/', $val)) + return array_map('intVal', explode(':', $val)); + + return []; + } + + protected static function checkIdListUnsigned(string $val) : array + { + if (preg_match('/^\d+(,\d+)*$/', $val)) + return array_map('intVal', explode(',', $val)); + + return []; + } + + protected static function checkTextLine(string $val) : string + { + // trim non-printable chars + return preg_replace(self::PATTERN_TEXT_LINE, '', trim(urldecode($val))); + } + + protected static function checkTextBlob(string $val) : string + { + // trim non-printable chars + excessive whitespaces (pattern includes \r) + $str = preg_replace(self::PATTERN_TEXT_BLOB, '', trim($val)); + return preg_replace('/ +/', ' ', trim($str)); + } + + + /********************/ + /* child implements */ + /********************/ + + // calc response + abstract protected function generate() : void; + + // send response + abstract protected function display() : void; + + // handling differs by medium + abstract protected function onUserGroupMismatch() : never; +} + +?> diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php new file mode 100644 index 00000000..76fb8672 --- /dev/null +++ b/includes/components/response/templateresponse.class.php @@ -0,0 +1,687 @@ +type, // DBType + $this->typeId, // DBTypeId + -1, // category + User::$groups, // staff mask + '' // misc (here unused) + ); + } +} + + +trait TrListPage +{ + public ?string $subCat = null; + public ?Filter $filter = null; + + public function getCacheKeyComponents() : array + { + // max. 3 catgs + // catg max 65535 + if ($this->category) + { + $catg = 0x0; + for ($i = 0; $i < 3; $i++) + { + $catg <<= 4; + $catg |= ($this->category[$i] ?? 0) & 0xFFFF; + } + } + + if ($get = $this->filter?->buildGETParam()) + $misc = md5($get); + + return array( + $this->type, // DBType + -1, // DBTypeId + $catg ?? -1, // category + User::$groups, // staff mask + $misc ?? '' // misc (here filter) + ); + } +} + + +trait TrGuideEditor +{ + public int $typeId = 0; + + public int $editCategory = 0; + public int $editClassId = 0; + public int $editSpecId = 0; + public int $editRev = 0; + public int $editStatus = GUIDE_STATUS_DRAFT; + public string $editStatusColor = GuideMgr::STATUS_COLORS[GUIDE_STATUS_DRAFT]; + public string $editTitle = ''; + public string $editName = ''; + public string $editDescription = ''; + public string $editText = ''; + public Locale $editLocale = Locale::EN; +} + +class TemplateResponse extends BaseResponse +{ + final protected const /* int */ TAB_DATABASE = 0; + final protected const /* int */ TAB_TOOLS = 1; + final protected const /* int */ TAB_MORE = 2; + final protected const /* int */ TAB_COMMUNITY = 3; + final protected const /* int */ TAB_STAFF = 4; + final protected const /* int */ TAB_GUIDES = 6; + + private array $jsgBuffer = []; // throw any db type references in here to be processed later + private array $header = []; + private string $fullParams = ''; // effectively articleUrl + + protected string $template = ''; + protected array $breadcrumb = []; + protected ?int $activeTab = null; // [Database, Tools, More, Community, Staff, null, Guides] ?? none + protected string $pageName = ''; + protected array $category = []; + protected array $validCats = []; + protected ?string $articleUrl = null; + protected bool $filterError = false; // retroactively apply error notice to fixed filter result + + protected array $dataLoader = []; // ?data=x.y.z as javascript + protected array $scripts = array( + [SC_JS_FILE, 'js/jquery-3.7.0.min.js', SC_FLAG_NO_TIMESTAMP ], + [SC_JS_FILE, 'js/basic.js' ], + [SC_JS_FILE, 'widgets/power.js', SC_FLAG_NO_TIMESTAMP | SC_FLAG_APPEND_LOCALE], + [SC_JS_FILE, 'js/locale_%s.js', SC_FLAG_LOCALIZED ], + [SC_JS_FILE, 'js/global.js' ], + [SC_JS_FILE, 'js/locale.js' ], + [SC_JS_FILE, 'js/Markup.js' ], + [SC_CSS_FILE, 'css/basic.css' ], + [SC_CSS_FILE, 'css/global.css' ], + [SC_CSS_FILE, 'css/aowow.css' ], + [SC_CSS_FILE, 'css/locale_%s.css', SC_FLAG_LOCALIZED ] + ); + + // debug: stats + protected static float $time = 0.0; + // protected static array $sql = []; + // protected static array $cacheStats = []; + public array $pageStats = []; // static properties carry the values, this is just for the PageTemplate to reference + + // send to template + public array $title = []; // head title components + public string $h1 = ''; // body title + public string $h1Link = ''; // + public ?string $headerLogo = null; // url to non-standard logo for events etc. + public string $search = ''; // prefilled search bar + public string $wowheadLink = 'https://wowhead.com/'; + public int $contribute = CONTRIBUTE_NONE; + public ?array $inputbox = null; + public ?string $rss = null; // link rel=alternate for rss auto-discovery + public ?string $tabsTitle = null; + public ?Markup $extraText = null; + public ?string $extraHTML = null; + public array $redButtons = []; // see template/redButtons.tpl.php + + // send to template, but it is js stuff + public array $gPageInfo = []; + public bool $gDataKey = false; // send g_DataKey to template or don't (stored in $_SESSION) + public ?Markup $article = null; + public ?Tabs $lvTabs = null; + public array $pageTemplate = []; // js PageTemplate object + public array $jsGlobals = []; // ready to be used in template + + public function __construct(string $pageParam = '') + { + $this->title[] = Cfg::get('NAME'); + self::$time = microtime(true); + + parent::__construct(); + + $this->fullParams = $this->pageName; + if ($pageParam) + $this->fullParams .= '='.$pageParam; + + // prep js+css includes + $parentVars = get_class_vars(__CLASS__); + if ($parentVars['scripts'] != $this->scripts) // additions set in child class + $this->scripts = array_merge($parentVars['scripts'], $this->scripts); + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) + array_push($this->scripts, [SC_CSS_FILE, 'css/staff.css'], [SC_JS_FILE, 'js/staff.js']); + + // get alt header logo + if ($ahl = DB::Aowow()->selectCell('SELECT `altHeaderLogo` FROM ?_home_featuredbox WHERE ?d BETWEEN `startDate` AND `endDate` ORDER BY `id` DESC', time())) + $this->headerLogo = Util::defStatic($ahl); + + if ($this->pageName) + { + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), $this->pageName, $pageParam ? '=' . $pageParam : ''); + $this->pageTemplate['pageName'] = $this->pageName; + } + + if (!is_null($this->activeTab)) + $this->pageTemplate['activeTab'] = $this->activeTab; + + if (!$this->isValidPage()) + $this->onInvalidCategory(); + + if (Cfg::get('MAINTENANCE') && !User::isInGroup(U_GROUP_EMPLOYEE)) + $this->generateMaintenance(); + else if (Cfg::get('MAINTENANCE') && User::isInGroup(U_GROUP_EMPLOYEE)) + Util::addNote('Maintenance mode enabled!'); + } + + // by default goto login page + protected function onUserGroupMismatch() : never + { + if (User::isLoggedIn()) + $this->generateError(); + + $this->forwardToSignIn($_SERVER['QUERY_STRING'] ?? ''); + } + + // by default show error page + protected function onInvalidCategory() : never + { + $this->generateError(); + } + + // just pass through + protected function addScript(array ...$scriptDefs) : void + { + if (!$this->result) + $this->scripts = array_merge($this->scripts, $scriptDefs); + else + foreach ($scriptDefs as $s) + $this->result->addScript(...$s); + } + + protected function addDataLoader(string ...$dataFiles) : void + { + if (!$this->result) + $this->dataLoader = array_merge($this->dataLoader, $dataFiles); + else + $this->result->addDataLoader($dataFiles); + } + + public static function pageStatsHook(Template\PageTemplate &$pt, array &$stats) : void + { + if (User::isInGroup(U_GROUP_EMPLOYEE)) + { + $stats['time'] = Util::formatTime((microtime(true) - self::$time) * 1000, true); + $stats['sql'] = ['count' => parent::$sql['count'], 'time' => Util::formatTime(parent::$sql['time'] * 1000, true)]; + $stats['cache'] = !empty(static::$cacheStats) ? [static::$cacheStats[0], Util::formatTimeDiff(static::$cacheStats[1])] : null; + } + else + $stats = []; + } + + protected function getCategoryFromUrl(string $pageParam) : void + { + $arr = explode('.', $pageParam); + foreach ($arr as $v) + { + if (!is_numeric($v)) + break; + + $this->category[] = (int)$v; + } + } + + // functionally this should be in PageTemplate but inaccessible there + protected function fmtStaffTip(?string $text, string $tip) : string + { + if (!$text || !User::isInGroup(U_GROUP_EMPLOYEE)) + return $text ?? ''; + else + return sprintf(Util::$dfnString, $tip, $text); + } + + + /**********************/ + /* Prepare js-Globals */ + /**********************/ + + // add typeIds that should be displayed as jsGlobal on the page + public function extendGlobalIds(int $type, int ...$ids) : void + { + if (!$type || !$ids) + return; + + if (!isset($this->jsgBuffer[$type])) + $this->jsgBuffer[$type] = []; + + foreach ($ids as $id) + $this->jsgBuffer[$type][] = $id; + } + + // add jsGlobals or typeIds (can be mixed in one array: TYPE => [mixeddata]) to display on the page + public function extendGlobalData(array $data, ?array $extra = null) : void + { + foreach ($data as $type => $globals) + { + if (!is_array($globals) || !$globals) + continue; + + $this->initJSGlobal($type); + + // can be id => data + // or idx => id + // and may be mixed + foreach ($globals as $k => $v) + { + if (is_array($v)) + { + // localize name fields .. except for icons .. icons are special + if ($type != Type::ICON) + { + foreach (['name', 'namefemale'] as $n) + { + if (!isset($v[$n])) + continue; + + $v[$n . '_'.Lang::getLocale()->json()] = $v[$n]; + unset($v[$n]); + } + } + + $this->jsGlobals[$type][1][$k] = $v; + } + else if (is_numeric($v)) + $this->extendGlobalIds($type, $v); + } + } + + if ($extra) + { + $namedExtra = []; + foreach ($extra as $typeId => $data) + foreach ($data as $k => $v) + $namedExtra[$typeId][$k.'_'.Lang::getLocale()->json()] = $v; + + $this->jsGlobals[$type][2] = $namedExtra; + } + } + + // init store for type + private function initJSGlobal(int $type) : void + { + $jsg = &$this->jsGlobals; // shortcut + + if (isset($jsg[$type])) + return; + + if ($tpl = Type::getJSGlobalTemplate($type)) + $jsg[$type] = $tpl; + } + + // lookup jsGlobals from collected typeIds + private function applyGlobals() : void + { + foreach ($this->jsgBuffer as $type => $ids) + { + foreach ($ids as $k => $id) // filter already generated data, maybe we can save a lookup or two + if (isset($this->jsGlobals[$type][1][$id])) + unset($ids[$k]); + + if (!$ids) + continue; + + $this->initJSGlobal($type); + + $obj = Type::newList($type, [Cfg::get('SQL_LIMIT_NONE'), ['id', array_unique($ids, SORT_NUMERIC)]]); + if (!$obj) + continue; + + $this->extendGlobalData($obj->getJSGlobals(GLOBALINFO_SELF)); + + // delete processed ids + $this->jsgBuffer[$type] = []; + } + } + + + /************************/ + /* Generic Page Content */ + /************************/ + + // get announcements and notes for user + private function addAnnouncements(bool $onlyGenerics = false) : void + { + $announcements = []; + + // display occured notices + $notes = $_SESSION['notes'] ?? []; + unset($_SESSION['notes']); + + $notes[] = [...Util::getNotes(), 'One or more issues occured during page generation']; + + foreach ($notes as $i => [$messages, $logLevel, $head]) + { + if (!$messages) + continue; + + array_unshift($messages, $head); + + $colors = array( // [border, text] + LOG_LEVEL_ERROR => ['C50F1F', 'E51223'], + LOG_LEVEL_WARN => ['C19C00', 'E5B700'], + LOG_LEVEL_INFO => ['3A96DD', '42ADFF'] + ); + + $text = new LocString(['name_loc' . Lang::getLocale()->value => '[span]'.implode("[br]", $messages).'[/span]'], callback: Util::defStatic(...)); + $style = 'color: #'.($colors[$logLevel][1] ?? 'fff').'; font-weight: bold; font-size: 14px; padding-left: 40px; background-image: url('.Cfg::get('STATIC_URL').'/images/announcements/warn-small.png); background-size: 15px 15px; background-position: 12px center; border: dashed 2px #'.($colors[$logLevel][0] ?? 'fff').';'; + + $announcements[] = new Announcement(-$i, 'internal error', $text, style: $style); + } + + // fetch announcements + $fromDB = DB::Aowow()->select( + 'SELECT `id`, `mode`, `status`, `name`, `style`, `text_loc0`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc6`, `text_loc8` + FROM ?_announcements + WHERE (`status` = ?d { OR `status` = ?d } ) AND + (`page` = "*" { OR `page` = ? } ) AND + (`groupMask` = 0 OR `groupMask` & ?d)', + Announcement::STATUS_ENABLED, User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? Announcement::STATUS_DISABLED : DBSIMPLE_SKIP, + $onlyGenerics || !$this->pageName ? DBSIMPLE_SKIP : $this->pageName, + User::$groups + ); + + foreach ($fromDB as $a) + if (($ann = new Announcement($a['id'], $a['name'], new LocString($a, 'text', Util::defStatic(...)), $a['mode'], $a['status'], Util::defStatic($a['style'])))->status != Announcement::STATUS_DELETED) + $announcements[] = $ann; + + $this->result->announcements = $announcements; + } + + // get article & static infobox (run before processing jsGlobals) + private function addArticle() : void + { + if ($this->article) + return; + + $article = []; + if (isset($this->guideRevision)) + $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `rev` = ?d', + Type::GUIDE, $this->typeId, $this->guideRevision); + if (!$article && $this->gPageInfo['articleUrl']) + $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `url` = ? AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', + $this->gPageInfo['articleUrl'], [Lang::getLocale()->value, Locale::EN->value]); + if (!$article && !empty($this->type) && isset($this->typeId)) + $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', + $this->type, $this->typeId, [Lang::getLocale()->value, Locale::EN->value]); + + if (!$article) + return; + + $text = Util::defStatic($article['article']); + $opts = []; + + // convert U_GROUP_* to MARKUP.CLASS_* (as seen in js-object Markup) + if ($article['editAccess'] & (U_GROUP_ADMIN | U_GROUP_VIP | U_GROUP_DEV)) + $opts['allow'] = Markup::CLASS_ADMIN; + else if ($article['editAccess'] & U_GROUP_STAFF) + $opts['allow'] = Markup::CLASS_STAFF; + else if ($article['editAccess'] & U_GROUP_PREMIUM) + $opts['allow'] = Markup::CLASS_PREMIUM; + else if ($article['editAccess'] & U_GROUP_PENDING) + $opts['allow'] = Markup::CLASS_PENDING; + else + $opts['allow'] = Markup::CLASS_USER; + + if (!empty($this->type) && isset($this->typeId)) + $opts['dbpage'] = 1; + + if ($article['locale'] != Lang::getLocale()->value) + $opts['prepend'] = '
'.Lang::main('langOnly', [Lang::lang($article['locale'])]).'
'; + + $this->article = new Markup($text, $opts); + + if ($jsg = $this->article->getJsGlobals()) + $this->extendGlobalData($jsg); + + $this->gPageInfo['editAccess'] = $article['editAccess']; + + if (method_exists($this, 'postArticle')) // e.g. update variables in article + $this->postArticle($this->article['text']); + } + + private function addCommunityContent() : void + { + $community = array( + 'coError' => $_SESSION['error']['co'] ?? null, + 'ssError' => $_SESSION['error']['ss'] ?? null, + 'viError' => $_SESSION['error']['vi'] ?? null + ); + + if ($this->contribute & CONTRIBUTE_CO) + $community['co'] = Util::toJSON(CommunityContent::getComments($this->type, $this->typeId)); + + if ($this->contribute & CONTRIBUTE_SS) + $community['ss'] = Util::toJSON(CommunityContent::getScreenshots($this->type, $this->typeId)); + + if ($this->contribute & CONTRIBUTE_VI) + $community['vi'] = Util::toJSON(CommunityContent::getVideos($this->type, $this->typeId)); + + unset($_SESSION['error']); + + // as comments are not cached, those globals cant be either + $this->extendGlobalData(CommunityContent::getJSGlobals()); + + $this->result->community = $community; + $this->applyGlobals(); + } + + + /**************/ + /* Generators */ + /**************/ + + protected function generate() : void + { + $this->result = new Template\PageTemplate($this->template, $this); + + foreach ($this->scripts as $s) + $this->result->addScript(...$s); + + $this->result->addDataLoader(...$this->dataLoader); + + // static::class so pageStatsHook defined here, can access cacheStats defined in the implementation + $this->result->registerDisplayHook('pageStats', [static::class, 'pageStatsHook']); + + // only adds edit links to the staff menu: precursor to guides? + if (!($this instanceof GuideBaseResponse)) + $this->gPageInfo += array( + 'articleUrl' => $this->articleUrl ?? $this->fullParams, // is actually be the url-param + 'editAccess' => (U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_BUREAU) + ); + + if ($this->breadcrumb) + $this->pageTemplate['breadcrumb'] = $this->breadcrumb; + + if (isset($this->filter)) + $this->pageTemplate['filter'] = $this->filter->query ? 1 : 0; + + $this->addArticle(); + + $this->applyGlobals(); + } + + // we admit this page exists and an error occured on it + public function generateError(?string $altPageName = null) : never + { + $this->result = new Template\PageTemplate('text-page-generic', $this); + + // only use own script defs + foreach (get_class_vars(self::class)['scripts'] as $s) + $this->result->addScript(...$s); + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) + { + $this->result->addScript(SC_CSS_FILE, 'css/staff.css'); + $this->result->addScript(SC_JS_FILE, 'js/staff.js'); + } + + $this->result->registerDisplayHook('pageStats', [self::class, 'pageStatsHook']); + + $this->title[] = Lang::main('errPageTitle'); + $this->h1 = Lang::main('errPageTitle'); + $this->articleUrl = 'page-not-found'; + $this->gPageInfo += array( + 'articleUrl' => 'page-not-found', + 'editAccess' => (U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_BUREAU) + ); + + $this->pageTemplate['pageName'] ??= $altPageName ?? 'page-not-found'; + + $this->addArticle(); + + $this->sumSQLStats(); + + $this->header[] = ['HTTP/1.0 404 Not Found', true, 404]; + + $this->display(true); + exit; + } + + // we do not have this page + public function generateNotFound(string $title = '', string $msg = '') : never + { + $this->result = new Template\PageTemplate('text-page-generic', $this); + + // only use own script defs + foreach (get_class_vars(self::class)['scripts'] as $s) + $this->result->addScript(...$s); + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) + { + $this->result->addScript(SC_CSS_FILE, 'css/staff.css'); + $this->result->addScript(SC_JS_FILE, 'js/staff.js'); + } + + $this->result->registerDisplayHook('pageStats', [self::class, 'pageStatsHook']); + + array_unshift($this->title, Lang::main('nfPageTitle')); + + $this->inputbox = ['inputbox-status', array( + 'head' => isset($this->typeId) ? Util::ucWords($title).' #'.$this->typeId : $title, + 'error' => !$msg && isset($this->typeId) ? Lang::main('pageNotFound', [$title]) : $msg + )]; + + $this->contribute = CONTRIBUTE_NONE; + + if (!empty($this->breadcrumb)) + $this->pageTemplate['breadcrumb'] = $this->breadcrumb; + + $this->sumSQLStats(); + + $this->header[] = ['HTTP/1.0 404 Not Found', true, 404]; + + $this->display(true); + exit; + } + + // display brb gnomes + public function generateMaintenance() : never + { + $this->result = new Template\PageTemplate('maintenance', $this); + + $this->header[] = ['HTTP/1.0 503 Service Temporarily Unavailable', true, 503]; + $this->header[] = ['Retry-After: '.(3 * HOUR)]; + + $this->display(true); + exit; + } + + protected function display(bool $withError = false) : void + { + $this->title = Util::htmlEscape($this->title); + $this->search = Util::htmlEscape($this->search); + // can't escape >h1 here, because CharTitles legitimately add HTML + + $this->addAnnouncements($withError); + if (!$withError) + $this->addCommunityContent(); + + // force jsGlobals from Announcements/CommunityContent into PageTemplate + // as this may be loaded from cache, it will be unlinked from its response + if ($ptJSG = $this->result->jsGlobals) + { + Util::mergeJsGlobals($ptJSG, $this->jsGlobals); + $this->result->jsGlobals = $ptJSG; + } + else if ($this->jsGlobals) + $this->result->jsGlobals = $this->jsGlobals; + + if ($this instanceof ICache) + $this->applyOnCacheLoaded($this->result); + + if ($this->result && $this->filterError) + $this->result->setListviewError(); + + $this->sumSQLStats(); + + // Heisenbug: IE11 and FF32 will sometimes (under unknown circumstances) cache 302 redirects and stop + // re-requesting them from the server but load them from local cache, thus breaking menu features. + $this->sendNoCacheHeader(); + foreach ($this->header as $h) + header(...$h); + + $this->result?->render(); + } + + + /**********/ + /* Checks */ + /**********/ + + // has a valid combination of categories + private function isValidPage() : bool + { + if (!$this->category || !$this->validCats) + return true; + + $c = $this->category; // shorthand + + switch (count($c)) + { + case 0: // no params works always + return true; + case 1: // null is valid || value in a 1-dim-array || (key for a n-dim-array && ( has more subcats || no further subCats )) + $filtered = array_filter($this->validCats, fn ($x) => is_int($x)); + return $c[0] === null || in_array($c[0], $filtered) || (!empty($this->validCats[$c[0]]) && (is_array($this->validCats[$c[0]]) || $this->validCats[$c[0]] === true)); + case 2: // first param has to be a key. otherwise invalid + if (!isset($this->validCats[$c[0]])) + return false; + + // check if the sub-array is n-imensional + if (is_array($this->validCats[$c[0]]) && count($this->validCats[$c[0]]) == count($this->validCats[$c[0]], COUNT_RECURSIVE)) + return in_array($c[1], $this->validCats[$c[0]]); // second param is value in second level array + else + return isset($this->validCats[$c[0]][$c[1]]); // check if params is key of another array + case 3: // 3 params MUST point to a specific value + return isset($this->validCats[$c[0]][$c[1]]) && in_array($c[2], $this->validCats[$c[0]][$c[1]]); + } + + return false; + } + +} + +?> diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php new file mode 100644 index 00000000..e8000987 --- /dev/null +++ b/includes/components/response/textresponse.class.php @@ -0,0 +1,163 @@ +type, // DBType + $this->typeId, // DBTypeId + -1, // category + User::$groups, // staff mask + '' // misc (here tooltip) + ); + + if ($this->enhancedTT) + $key[4] = md5(serialize($this->enhancedTT)); + + return $key; + } +} + + +trait TrRss +{ + private array $feedData = []; + + protected function generateRSS(string $title, string $link) : string + { + $root = new SimpleXML(''); + $root->addAttribute('version', '2.0'); + + $channel = $root->addChild('channel'); + + $channel->addChild('title', Cfg::get('NAME_SHORT').' - '.$title); + $channel->addChild('link', Cfg::get('HOST_URL').'/?'.$link); + $channel->addChild('description', Cfg::get('NAME')); + $channel->addChild('language', implode('-', str_split(Lang::getLocale()->json(), 2))); + $channel->addChild('ttl', Cfg::get('TTL_RSS')); + $channel->addChild('lastBuildDate', date(DATE_RSS)); + + foreach ($this->feedData as $row) + { + $item = $channel->addChild('item'); + + foreach ($row as $key => [$isCData, $attrib, $text]) + { + if ($isCData && $text) + $child = $item->addChild($key)->addCData($text); + else + $child = $item->addChild($key, $text); + + foreach ($attrib as $k => $v) + $child->addAttribute($k, $v); + } + } + + return $root->asXML(); + } +} + +trait TrCommunityHelper +{ + private function handleCaption(?string $caption) : string + { + if (!$caption) + return ''; + + // trim excessive whitespaces + $caption = trim(preg_replace('/\s{2,}/', ' ', $caption)); + + // shorten to fit db + $caption = substr($caption, 0, 200); + + // jsEscape just in case + return Util::jsEscape($caption); + } +} + +abstract class TextResponse extends BaseResponse +{ + protected string $contentType = MIME_TYPE_JAVASCRIPT; + protected ?string $redirectTo = null; + protected array $params = []; + + /// generation stats + protected static float $time = 0.0; + + public function __construct(string $pageParam = '') + { + self::$time = microtime(true); + $this->params = explode('.', $pageParam); + // todo - validate params? + + parent::__construct(); + + if (Cfg::get('MAINTENANCE') && !User::isInGroup(U_GROUP_EMPLOYEE)) + $this->generate404(); + } + + // by default ajax has nothing to say + protected function onUserGroupMismatch() : never + { + trigger_error('TextResponse::onUserGroupMismatch - loggedIn: '.($this->requiresLogin ? 'yes' : 'no').'; expected: '.Util::asHex($this->requiredUserGroup).'; is: '.Util::asHex(User::$groups), E_USER_WARNING); + + $this->generate403(); + } + + public function generate404(?string $out = null) : never + { + header('HTTP/1.0 404 Not Found', true, 404); + header($this->contentType); + exit($out); + } + + public function generate403(?string $out = null) : never + { + header('HTTP/1.0 403 Forbidden', true, 403); + header($this->contentType); + exit($out); + } + + protected function display() : void + { + if ($this->redirectTo) + $this->forward($this->redirectTo); + + $out = ($this instanceof ICache) ? $this->applyOnCacheLoaded($this->result) : $this->result; + + $this->sendNoCacheHeader(); + header($this->contentType); + + // NOTE - this may fuck up some javascripts that say they expect ajax, but use the whole string anyway + // so it's limited to tooltips + if (Cfg::get('DEBUG') && User::isInGroup(U_GROUP_STAFF) && $this->result instanceof Tooltip) + { + $this->sumSQLStats(); + + echo "/*\n"; + echo " * generated in ".Util::formatTime((microtime(true) - self::$time) * 1000)."\n"; + echo " * " . parent::$sql['count'] . " SQL queries in " . Util::formatTime(parent::$sql['time'] * 1000) . "\n"; + if ($this instanceof ICache && static::$cacheStats) + { + [$mode, $set, $lifetime] = static::$cacheStats; + echo " * stored in " . ($mode == CACHE_MODE_MEMCACHED ? 'Memcached' : 'filecache') . ":\n"; + echo " * + ".date('c', $set) . ' - ' . Util::formatTimeDiff($set) . "\n"; + echo " * - ".date('c', $set + $lifetime) . ' - in '.Util::formatTime(($set + $lifetime - time()) * 1000) . "\n"; + } + echo " */\n\n"; + } + + echo $out; + } +} + +?> diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php index de383a06..59d346ef 100644 --- a/includes/dbtypes/user.class.php +++ b/includes/dbtypes/user.class.php @@ -56,6 +56,8 @@ class UserList extends DBTypeList public function getListviewData() : array { return []; } public function renderTooltip() : ?string { return null; } + + public static function getName($id) : ?LocString { return null; } } ?> diff --git a/includes/defines.php b/includes/defines.php index 0dfb320b..86f57e90 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -17,7 +17,7 @@ define('TDB_WORLD_EXPECTED_VER', 24041); // as of 01.01.2024 https://www.wowhead.com/wotlk/de/spell=40120/{seo} // https://www.wowhead.com/wotlk/es/search=vuelo -define('WOWHEAD_LINK', 'https://www.wowhead.com/wotlk/%s/%s=%s'); +define('WOWHEAD_LINK', 'https://www.wowhead.com/wotlk/%s/%s%s'); define('LOG_LEVEL_ERROR', 1); define('LOG_LEVEL_WARN', 2); @@ -25,7 +25,8 @@ define('LOG_LEVEL_INFO', 3); define('MIME_TYPE_TEXT', 'Content-Type: text/plain; charset=utf-8'); define('MIME_TYPE_XML', 'Content-Type: text/xml; charset=utf-8'); -define('MIME_TYPE_JSON', 'Content-Type: application/x-javascript; charset=utf-8'); +define('MIME_TYPE_JAVASCRIPT', 'Content-Type: application/x-javascript; charset=utf-8'); +define('MIME_TYPE_JSON', 'Content-Type: application/json; charset=utf-8'); define('MIME_TYPE_OPENSEARCH', 'Content-Type: application/x-suggestions+json; charset=utf-8'); define('MIME_TYPE_RSS', 'Content-Type: application/rss+xml; charset=utf-8'); define('MIME_TYPE_JPEG', 'Content-Type: image/jpeg'); diff --git a/includes/kernel.php b/includes/kernel.php index 20ef2719..4d444d5a 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -68,6 +68,8 @@ spl_autoload_register(function (string $class) : void require_once 'includes/components/'.strtolower($class).'.class.php'; else if (file_exists('includes/components/frontend/'.strtolower($class).'.class.php')) require_once 'includes/components/frontend/'.strtolower($class).'.class.php'; + else if (file_exists('includes/components/response/'.strtolower($class).'.class.php')) + require_once 'includes/components/response/'.strtolower($class).'.class.php'; }); // TC systems in components @@ -121,41 +123,6 @@ spl_autoload_register(function (string $class) : void throw new \Exception('could not register type class: '.$cl); }); -// endpoint loader -spl_autoload_register(function (string $class) : void -{ - if ($i = strrpos($class, '\\')) - $class = substr($class, $i + 1); - - if (preg_match('/[^\w]/i', $class)) - return; - - $class = strtolower($class); - - if (stripos($class, 'ajax') === 0) // handles ajax and jsonp requests - { - if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php')) - { - require_once 'includes/ajaxHandler/ajaxHandler.class.php'; - require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'; - } - else - throw new \Exception('could not register ajaxHandler class: '.$class); - - return; - } - else if (stripos($class, 'page')) // handles templated pages - { - if (file_exists('pages/'.strtr($class, ['page' => '']).'.php')) - { - require_once 'pages/genericPage.class.php'; - require_once 'pages/'.strtr($class, ['page' => '']).'.php'; - } - else if ($class == 'genericpage') // may be called directly in fatal error case - require_once 'pages/genericPage.class.php'; - } -}); - set_error_handler(function(int $errNo, string $errStr, string $errFile, int $errLine) : bool { // either from test function or handled separately @@ -208,13 +175,17 @@ set_exception_handler(function (\Throwable $e) : void else { Util::addNote('Exception - '.$e->getMessage().' @ '.$e->getFile(). ':'.$e->getLine()."\n".$e->getTraceAsString(), U_GROUP_EMPLOYEE, LOG_LEVEL_ERROR); - (new GenericPage())->error(); + (new TemplateResponse())->generateError(); } }); // handle fatal errors register_shutdown_function(function() : void { + // defer undisplayed error/exception notes + if (!CLI && ($n = Util::getNotes())) + $_SESSION['notes'][] = [$n[0], $n[1], 'Defered issues from previous request']; + if ($e = error_get_last()) { if (DB::isConnected(DB_AOWOW)) @@ -263,7 +234,7 @@ if (!CLI) { // not displaying the brb gnomes as static_host is missing, but eh... if (!DB::isConnected(DB_AOWOW) || !DB::isConnected(DB_WORLD) || !Cfg::get('HOST_URL') || !Cfg::get('STATIC_URL')) - (new GenericPage())->maintenance(); + (new TemplateResponse())->generateMaintenance(); // Setup Session $cacheDir = Cfg::get('SESSION_CACHE_DIR'); @@ -275,7 +246,7 @@ if (!CLI) if (!session_start()) { trigger_error('failed to start session', E_USER_ERROR); - (new GenericPage())->error(); + (new TemplateResponse())->generateError(); } if (User::init()) @@ -300,12 +271,6 @@ if (!CLI) if (DB::isConnected(DB_CHARACTERS . $idx)) DB::Characters($idx)->setLogger(DB::profiler(...)); } - - // parse page-parameters .. sanitize before use! - $str = explode('&', $_SERVER['QUERY_STRING'] ?? '', 2)[0]; - $_ = explode('=', $str, 2); - $pageCall = mb_strtolower($_[0]); - $pageParam = $_[1] ?? ''; } ?> diff --git a/includes/utilities.php b/includes/utilities.php index 43d00367..abb5bd21 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -18,104 +18,6 @@ class SimpleXML extends \SimpleXMLElement } } -trait TrRequestData -{ - // const in trait supported in php8.2+ - public const PATTERN_TEXT_LINE = '/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; - public const PATTERN_TEXT_BLOB = '/[\x00-\x09\x0B-\x1F\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; - - protected $_get = []; // fill with variables you that are going to be used; eg: - protected $_post = []; // 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList'] - protected $_cookie = []; - - private $filtered = false; - - private function initRequestData() : void - { - if ($this->filtered) - return; - - // php bug? If INPUT_X is empty, filter_input_array returns null/fails - // only really relevant for INPUT_POST - // manuall set everything null in this case - - if ($this->_post) - { - if ($_POST) - $this->_post = filter_input_array(INPUT_POST, $this->_post); - else - $this->_post = array_fill_keys(array_keys($this->_post), null); - } - - if ($this->_get) - { - if ($_GET) - $this->_get = filter_input_array(INPUT_GET, $this->_get); - else - $this->_get = array_fill_keys(array_keys($this->_get), null); - } - - if ($this->_cookie) - { - if ($_COOKIE) - $this->_cookie = filter_input_array(INPUT_COOKIE, $this->_cookie); - else - $this->_cookie = array_fill_keys(array_keys($this->_cookie), null); - } - - $this->filtered = true; - } - - private static function checkEmptySet(string $val) : bool - { - return $val === ''; // parameter is expected to be empty - } - - private static function checkInt(string $val) : int - { - if (preg_match('/^-?\d+$/', $val)) - return intVal($val); - - return 0; - } - - private static function checkIdList(string $val) : array - { - if (preg_match('/^-?\d+(,-?\d+)*$/', $val)) - return array_map('intVal', explode(',', $val)); - - return []; - } - - private static function checkIntArray(string $val) : array - { - if (preg_match('/^-?\d+(:-?\d+)*$/', $val)) - return array_map('intVal', explode(':', $val)); - - return []; - } - - private static function checkIdListUnsigned(string $val) : array - { - if (preg_match('/^\d+(,\d+)*$/', $val)) - return array_map('intVal', explode(',', $val)); - - return []; - } - - private static function checkTextLine(string $val) : string - { - // trim non-printable chars - return preg_replace(self::PATTERN_TEXT_LINE, '', $val); - } - - private static function checkTextBlob(string $val) : string - { - // trim non-printable chars - return preg_replace(self::PATTERN_TEXT_BLOB, '', $val); - } -} - abstract class Util { @@ -175,9 +77,7 @@ abstract class Util public static $guideratingString = " $(document).ready(function() {\n $('#guiderating').append(GetStars(%.10F, %s, %u, %u));\n });"; - public static $expansionString = array( // 3 & 4 unused .. obviously - null, 'bc', 'wotlk', 'cata', 'mop' - ); + public static $expansionString = [null, 'bc', 'wotlk']; public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; private static $notes = []; @@ -191,7 +91,7 @@ abstract class Util { $notes = []; $severity = LOG_LEVEL_INFO; - foreach (self::$notes as [$note, $uGroup, $level]) + foreach (self::$notes as $k => [$note, $uGroup, $level]) { if ($uGroup && !User::isInGroup($uGroup)) continue; @@ -200,6 +100,7 @@ abstract class Util $severity = $level; $notes[] = $note; + unset(self::$notes[$k]); } return [$notes, $severity]; @@ -309,14 +210,14 @@ abstract class Util if ($ms) return $ms."\u{00A0}".Lang::timeUnits('ab', 7); - return '0 '.Lang::timeUnits('ab', 6); + return "0\u{00A0}".Lang::timeUnits('ab', 6); } else { $_ = $d + $h / 24; if ($_ > 1 && !($_ % 365)) // whole years - return round(($d + $h / 24) / 364, 2)."\u{00A0}".Lang::timeUnits($d / 364 == 1 && !$h ? 'sg' : 'pl', 0); - if ($_ > 1 && !($_ % 30)) // whole month + return round(($d + $h / 24) / 365, 2)."\u{00A0}".Lang::timeUnits($d / 365 == 1 && !$h ? 'sg' : 'pl', 0); + if ($_ > 1 && !($_ % 30)) // whole months return round(($d + $h / 24) / 30, 2)."\u{00A0}".Lang::timeUnits($d / 30 == 1 && !$h ? 'sg' : 'pl', 1); if ($_ > 1 && !($_ % 7)) // whole weeks return round(($d + $h / 24) / 7, 2)."\u{00A0}".Lang::timeUnits($d / 7 == 1 && !$h ? 'sg' : 'pl', 2); @@ -329,15 +230,15 @@ abstract class Util if ($s) return round($s + $ms / 1000, 2)."\u{00A0}".Lang::timeUnits($s == 1 && !$ms ? 'sg' : 'pl', 6); if ($ms) - return $ms." ".Lang::timeUnits($ms == 1 ? 'sg' : 'pl', 7); + return $ms."\u{00A0}".Lang::timeUnits($ms == 1 ? 'sg' : 'pl', 7); - return '0 '.Lang::timeUnits('pl', 6); + return "0\u{00A0}".Lang::timeUnits('pl', 6); } } public static function formatTimeDiff(int $sec) : string { - $delta = time() - $sec; + $delta = abs(time() - $sec); [, $s, $m, $h, $d] = self::parseTime($delta * 1000); @@ -476,21 +377,23 @@ abstract class Util )); } - public static function defStatic($data) + public static function defStatic(array|string $data) : array|string { if (is_array($data)) { foreach ($data as &$v) - $v = self::defStatic($v); + if ($v) + $v = self::defStatic($v); return $data; } return strtr($data, array( - ' '' => 'scr"+"ipt>', - 'HOST_URL' => Cfg::get('HOST_URL'), - 'STATIC_URL' => Cfg::get('STATIC_URL') + 'HOST_URL' => Cfg::get('HOST_URL'), + 'STATIC_URL' => Cfg::get('STATIC_URL'), + 'NAME' => Cfg::get('NAME'), + 'NAME_SHORT' => Cfg::get('NAME_SHORT'), + 'CONTACT_EMAIL' => Cfg::get('CONTACT_EMAIL') )); } @@ -558,7 +461,7 @@ abstract class Util $first = mb_substr($str, 0, 1); $rest = mb_substr($str, 1); - return mb_strtoupper($first).mb_strtolower($rest); + return mb_strtoupper($first).$rest; } public static function ucWords(string $str) : string @@ -864,15 +767,6 @@ abstract class Util return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x)); } - public static function sendNoCacheHeader() - { - header('Expires: Sat, 01 Jan 2000 01:00:00 GMT'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - header('Cache-Control: no-store, no-cache, must-revalidate'); - header('Cache-Control: post-check=0, pre-check=0', false); - header('Pragma: no-cache'); - } - public static function toJSON($data, $forceFlags = 0) { $flags = $forceFlags ?: (JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE); @@ -884,7 +778,7 @@ abstract class Util // handle strings prefixed with $ as js-variables // literal: match everything (lazy) between first pair of unescaped double quotes. First character must be $. - $json = preg_replace_callback('/(? str_replace('\"', '"', $m[1]), $json); return $json; } @@ -1330,9 +1224,9 @@ abstract class Util 'Reply-To: ' . Cfg::get('CONTACT_EMAIL') . "\n" . 'X-Mailer: PHP/' . phpversion(); - if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) + if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) { - $_SESSION['debug-mail'] = $email . "\n\n" . $subject . "\n\n" . $body; + Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_DEV | U_GROUP_ADMIN, LOG_LEVEL_INFO); return true; } diff --git a/index.php b/index.php index 62867356..672173b2 100644 --- a/index.php +++ b/index.php @@ -8,160 +8,75 @@ if (CLI) die("this script must not be run from CLI.\nto setup aowow use 'php aowow'\n"); -$altClass = ''; -switch ($pageCall) +$pageCall = 'home'; // default to Homepage unless specified otherwise +$pageParam = ''; +parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $query); +foreach ($query as $page => $param) { - /* called by user */ - case '': // no parameter given -> MainPage - $altClass = 'home'; - case 'home': - case 'admin': - case 'account': // account management [nyi] - case 'achievement': - case 'achievements': - case 'areatrigger': - case 'areatriggers': - case 'arena-team': - case 'arena-teams': - case 'class': - case 'classes': - case 'currency': - case 'currencies': - case 'compare': // tool: item comparison - case 'emote': - case 'emotes': - case 'enchantment': - case 'enchantments': - case 'event': - case 'events': - case 'faction': - case 'factions': - case 'guide': - case 'guides': - case 'guild': - case 'guilds': - case 'icon': - case 'icons': - case 'item': - case 'items': - case 'itemset': - case 'itemsets': - case 'maps': // tool: map listing - case 'mail': - case 'mails': - case 'my-guides': - if ($pageCall == 'my-guides') - $altClass = 'guides'; - case 'npc': - case 'npcs': - case 'object': - case 'objects': - case 'pet': - case 'pets': - case 'petcalc': // tool: pet talent calculator - if ($pageCall == 'petcalc') - $altClass = 'talent'; - case 'profile': // character profiler [nyi] - case 'profiles': // character profile listing [nyi] - case 'profiler': // character profiler main page - case 'quest': - case 'quests': - case 'race': - case 'races': - case 'screenshot': // prepare uploaded screenshots - case 'search': // tool: searches - case 'skill': - case 'skills': - case 'sound': - case 'sounds': - case 'spell': - case 'spells': - case 'talent': // tool: talent calculator - case 'title': - case 'titles': - case 'user': - case 'video': - case 'zone': - case 'zones': - /* called by script */ - case 'data': // tool: dataset-loader - case 'cookie': // lossless cookies and user settings - case 'contactus': - case 'comment': - case 'edit': // guide editor: targeted by QQ fileuploader, detail-page article editor - case 'get-description': // guide editor: shorten fulltext into description - case 'filter': // pre-evaluate filter POST-data; sanitize and forward as GET-data - case 'go-to-reply': // find page the reply is on and forward - if ($pageCall == 'go-to-reply') - $altClass = 'go-to-comment'; - case 'go-to-comment': // find page the comment is on and forward - case 'locale': // subdomain-workaround, change the language - $cleanName = str_replace(['-', '_'], '', ucFirst($altClass ?: $pageCall)); - try // can it be handled as ajax? - { - $out = ''; - $class = __NAMESPACE__.'\\'.'Ajax'.$cleanName; - $ajax = new $class(explode('.', $pageParam)); + $page = preg_replace('/[^\w\-]/i', '', $page); - if ($ajax->handle($out)) - { - Util::sendNoCacheHeader(); + $pageCall = Util::lower($page); + $pageParam = $param ?? ''; + break; // only use first k/v-pair to determine page +} - if ($ajax->doRedirect) - header('Location: '.$out, true, 302); - else - { - header($ajax->getContentType()); - die($out); - } - } - else - throw new \Exception('not handled as ajax'); - } - catch (\Exception $e) // no, apparently not.. - { - $class = __NAMESPACE__.'\\'.$cleanName.'Page'; - $classInstance = new $class($pageCall, $pageParam); +[$classMod, $file] = match (true) +{ + // is search ajax + isset($_GET['json']) => ['Json', $pageCall . '_json' ], + isset($_GET['opensearch']) => ['Open', $pageCall . '_open' ], + // is powered tooltip + isset($_GET['power']) => ['Power', $pageCall . '_power' ], + // is item data xml dump + isset($_GET['xml']) => ['Xml', $pageCall . '_xml' ], + // is community content feed + isset($_GET['rss']) => ['Rss', $pageCall . '_rss' ], + // is sounds playlist + isset($_GET['playlist']) => ['Playlist', $pageCall . '_playlist'], + // pageParam can be sub page + (bool)preg_match('/^[a-z\-]+$/i', $pageParam) => [Util::ucFirst(strtr($pageParam, ['-' => ''])), Util::lower($pageParam)], + // no pageParam or PageParam is param for BasePage + default => ['Base', $pageCall ] +}; - if (is_callable([$classInstance, 'display'])) - $classInstance->display(); - else if (isset($_GET['power'])) - die('$WowheadPower.register(0, '.Lang::getLocale()->value.', {})'); - else // in conjunction with a proper rewriteRule in .htaccess... - (new GenericPage($pageCall))->error(); - } +// admin=X pages are mixed html and ajax on the same endpoint .. meh +if ($pageCall == 'admin' && isset($_GET['action']) && preg_match('/^[a-z]+$/', $_GET['action'])) +{ + $classMod .= 'Action' . Util::ucFirst($_GET['action']); + $file .= '_' . Util::lower($_GET['action']); +} - break; - /* other pages */ - case 'whats-new': - case 'searchplugins': - case 'searchbox': - case 'tooltips': - case 'help': - case 'faq': - case 'aboutus': - case 'reputation': - case 'privilege': - case 'privileges': - case 'top-users': - (new MorePage($pageCall, $pageParam))->display(); - break; - case 'latest-additions': - case 'latest-comments': - case 'latest-screenshots': - case 'latest-videos': - case 'unrated-comments': - case 'missing-screenshots': - case 'most-comments': - case 'random': - (new UtilityPage($pageCall, $pageParam))->display(); - break; - default: // unk parameter given -> ErrorPage - if (isset($_GET['power'])) - die('$WowheadPower.register(0, '.Lang::getLocale()->value.', {})'); - else // in conjunction with a proper rewriteRule in .htaccess... - (new GenericPage($pageCall))->error(); - break; +try { + $responder = new \StdClass; + + // 1. try specialized response + if (file_exists('endpoints/'.$pageCall.'/'.$file.'.php')) + { + require_once 'endpoints/'.$pageCall.'/'.$file.'.php'; + + $class = __NAMESPACE__.'\\' . Util::ucFirst(strtr($pageCall, ['-' => ''])).$classMod.'Response'; + $responder = new $class($pageParam); + } + // 2. try generalized response + else if (file_exists('endpoints/'.$pageCall.'/'.$pageCall.'.php')) + { + require_once 'endpoints/'.$pageCall.'/'.$pageCall.'.php'; + + $class = __NAMESPACE__.'\\' . Util::ucFirst(strtr($pageCall, ['-' => ''])).'BaseResponse'; + $responder = new $class($pageParam); + } + // 3. throw .. your hands in the air and give up + if (!is_callable([$responder, 'process'])) + throw new \Exception('request handler '.$pageCall.'::'.$classMod.'('.$pageParam.') not found'); + + $responder->process(); +} +catch (\Exception $e) +{ + if (isset($_GET['json']) || isset($_GET['opensearch']) || isset($_GET['power']) || isset($_GET['xml']) || isset($_GET['rss'])) + (new TextResponse($pageParam))->generate404(); + else + (new TemplateResponse($pageParam))->generateError($pageCall); } ?> diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php deleted file mode 100644 index f73fff90..00000000 --- a/pages/genericPage.class.php +++ /dev/null @@ -1,1204 +0,0 @@ -mode, $this->type, $this->typeId, $staff, Lang::getLocale()->value, '-1', '-1']; - - // item special: can modify tooltips - if (isset($this->enhancedTT)) - $key[] = md5(serialize($this->enhancedTT)); - - return implode('_', $key); - } - - protected function applyCCErrors() : void - { - if (!empty($_SESSION['error']['co'])) - $this->coError = $_SESSION['error']['co']; - - if (!empty($_SESSION['error']['ss'])) - $this->ssError = $_SESSION['error']['ss']; - - if (!empty($_SESSION['error']['vi'])) - $this->viError = $_SESSION['error']['vi']; - - unset($_SESSION['error']); - } -} - - -trait TrListPage -{ - protected $category = null; - protected $subCat = ''; - protected $lvTabs = []; // most pages have this - protected $redButtons = []; // see template/redButtons.tpl.php - - protected ?Filter $filterObj = null; - - protected function generateCacheKey(bool $withStaff = true) : string - { - $staff = intVal($withStaff && User::isInGroup(U_GROUP_EMPLOYEE)); - - // mode, type, typeId, employee-flag, localeId, - $key = [$this->mode, $this->type, '-1', $staff, Lang::getLocale()->value]; - - //category - $key[] = $this->category ? implode('.', $this->category) : '-1'; - - // filter - $key[] = $this->filterObj ? md5(serialize($this->filterObj)) : '-1'; - - return implode('_', $key); - } -} - - -trait TrProfiler -{ - protected $region = ''; - protected $realm = ''; - protected $realmId = 0; - protected $battlegroup = ''; // not implemented, since no pserver supports it - protected $subjectName = ''; - protected $subjectGUID = 0; - protected $sumSubjects = 0; - - protected $doResync = null; - - protected function generateCacheKey(bool $withStaff = true) : string - { - $staff = intVal($withStaff && User::isInGroup(U_GROUP_EMPLOYEE)); - - // mode, type, typeId, employee-flag, localeId, category, filter - $key = [$this->mode, $this->type, $this->subject->getField('id'), $staff, Lang::getLocale()->value, '-1', '-1']; - - return implode('_', $key); - } - - protected function getSubjectFromUrl(string $pageParam) : void - { - if (!$pageParam) - return; - - // cat[0] is always region - // cat[1] is realm or bGroup (must be realm if cat[2] is set) - // cat[2] is arena-team, guild or player - $cat = explode('.', mb_strtolower($pageParam), 3); - - $cat = array_map('urldecode', $cat); - - if (array_search($cat[0], Util::$regions) === false) - return; - - $this->region = $cat[0]; - - // if ($cat[1] == Profiler::urlize(Cfg::get('BATTLEGROUP'))) - // $this->battlegroup = Cfg::get('BATTLEGROUP'); - if (isset($cat[1])) - { - foreach (Profiler::getRealms() as $rId => $r) - { - if (Profiler::urlize($r['name'], true) == $cat[1]) - { - $this->realm = $r['name']; - $this->realmId = $rId; - if (isset($cat[2]) && mb_strlen($cat[2]) >= 2) - $this->subjectName = $cat[2]; // cannot reconstruct original name from urlized form; match against special name field - - break; - } - } - } - } - - protected function initialSync() : never - { - $this->prepareContent(); - - $this->notFound = array( - 'title' => sprintf(Lang::profiler('firstUseTitle'), $this->subjectName, $this->realm), - 'msg' => '' - ); - - if (isset($this->tabId)) - $this->pageTemplate['activeTab'] = $this->tabId; - - $this->sumSQLStats(); - - $this->display('text-page-generic'); - exit(); - } - - protected function generatePath() : void - { - if ($this->region) - { - $this->path[] = $this->region; - - if ($this->realm) - $this->path[] = Profiler::urlize($this->realm, true); - // else - // $this->path[] = Profiler::urlize(Cfg::get('BATTLEGROUP')); - } - } -} - - -class GenericPage -{ - use TrRequestData; - - protected $tpl = ''; - protected $reqUGroup = U_GROUP_NONE; - protected $reqAuth = false; - protected $mode = CACHE_TYPE_NONE; - protected $contribute = CONTRIBUTE_NONE; - - protected $wowheadLink = 'https://wowhead.com/'; - - protected $jsGlobals = []; - protected $lvData = []; - protected $title = []; // for title-Element - protected $name = ''; // for h1-Element - protected $tabId = null; - protected $gDataKey = false; // adds the dataKey to the user vars - protected $notFound = []; - protected $pageTemplate = []; - protected $article = null; - protected $articleUrl = ''; - protected $editAccess = null; // 0 is valid access value, so null - - protected $scripts = array( - [SC_JS_FILE, 'js/jquery-3.7.0.min.js', SC_FLAG_NO_TIMESTAMP ], - [SC_JS_FILE, 'js/basic.js' ], - [SC_JS_FILE, 'widgets/power.js', SC_FLAG_NO_TIMESTAMP | SC_FLAG_APPEND_LOCALE], - [SC_JS_FILE, 'js/locale_%s.js', SC_FLAG_LOCALIZED ], - [SC_JS_FILE, 'js/global.js' ], - [SC_JS_FILE, 'js/locale.js' ], - [SC_JS_FILE, 'js/Markup.js' ], - [SC_CSS_FILE, 'css/basic.css' ], - [SC_CSS_FILE, 'css/global.css' ], - [SC_CSS_FILE, 'css/aowow.css' ], - [SC_CSS_FILE, 'css/locale_%s.css', SC_FLAG_LOCALIZED ] - ); - - // private vars don't get cached - private $time = 0; - private $cacheDir = 'cache/template/'; - private $jsgBuffer = []; - private $gPageInfo = []; - private $gUser = []; - private $gFavorites = []; - private $community = ['co' => [], 'sc' => [], 'vi' => []]; - private $announcements = []; - - private $cacheLoaded = []; - private $skipCache = 0x0; - private $memcached = null; - private $mysql = ['time' => 0, 'count' => 0]; - - private $js = []; - private $css = []; - private $headerLogo = ''; - private $fullParams = ''; - - private $lvTemplates = array( - 'achievement' => ['template' => 'achievement', 'id' => 'achievements', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_achievements' ], - 'areatrigger' => ['template' => 'areatrigger', 'id' => 'areatrigger', 'parent' => 'lv-generic', 'data' => [], ], - 'calendar' => ['template' => 'holidaycal', 'id' => 'calendar', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_calendar' ], - 'class' => ['template' => 'classs', 'id' => 'classes', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_classes' ], - 'commentpreview' => ['template' => 'commentpreview', 'id' => 'comments', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_comments' ], - 'npc' => ['template' => 'npc', 'id' => 'npcs', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_npcs' ], - 'currency' => ['template' => 'currency', 'id' => 'currencies', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_currencies' ], - 'emote' => ['template' => 'emote', 'id' => 'emotes', 'parent' => 'lv-generic', 'data' => [] ], - 'enchantment' => ['template' => 'enchantment', 'id' => 'enchantments', 'parent' => 'lv-generic', 'data' => [] ], - 'event' => ['template' => 'holiday', 'id' => 'holidays', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_holidays' ], - 'faction' => ['template' => 'faction', 'id' => 'factions', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_factions' ], - 'genericmodel' => ['template' => 'genericmodel', 'id' => 'same-model-as', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_samemodelas' ], - 'icongallery' => ['template' => 'icongallery', 'id' => 'icons', 'parent' => 'lv-generic', 'data' => [] ], - 'item' => ['template' => 'item', 'id' => 'items', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_items' ], - 'itemset' => ['template' => 'itemset', 'id' => 'itemsets', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_itemsets' ], - 'mail' => ['template' => 'mail', 'id' => 'mails', 'parent' => 'lv-generic', 'data' => [] ], - 'model' => ['template' => 'model', 'id' => 'gallery', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_gallery' ], - 'object' => ['template' => 'object', 'id' => 'objects', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_objects' ], - 'pet' => ['template' => 'pet', 'id' => 'hunter-pets', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_pets' ], - 'profile' => ['template' => 'profile', 'id' => 'profiles', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_profiles' ], - 'quest' => ['template' => 'quest', 'id' => 'quests', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_quests' ], - 'race' => ['template' => 'race', 'id' => 'races', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_races' ], - 'replypreview' => ['template' => 'replypreview', 'id' => 'comment-replies', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_commentreplies'], - 'reputationhistory' => ['template' => 'reputationhistory', 'id' => 'reputation', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_reputation' ], - 'screenshot' => ['template' => 'screenshot', 'id' => 'screenshots', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_screenshots' ], - 'skill' => ['template' => 'skill', 'id' => 'skills', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_skills' ], - 'sound' => ['template' => 'sound', 'id' => 'sounds', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.types[19][2]' ], - 'spell' => ['template' => 'spell', 'id' => 'spells', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_spells' ], - 'title' => ['template' => 'title', 'id' => 'titles', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_titles' ], - 'topusers' => ['template' => 'topusers', 'id' => 'topusers', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.topusers' ], - 'video' => ['template' => 'video', 'id' => 'videos', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_videos' ], - 'zone' => ['template' => 'zone', 'id' => 'zones', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_zones' ], - 'guide' => ['template' => 'guide', 'id' => 'guides', 'parent' => 'lv-generic', 'data' => [], ] - ); - - public function __construct(string $pageCall = '', string $pageParam = '') - { - $this->time = microtime(true); - - $this->initRequestData(); - - $this->title[] = Cfg::get('NAME'); - - $this->fullParams = $pageCall; - if ($pageParam) - $this->fullParams .= '='.$pageParam; - - $cacheDir = Cfg::get('CACHE_DIR'); - if ($cacheDir && Util::writeDir($cacheDir)) - $this->cacheDir = mb_substr($cacheDir, -1) != '/' ? $cacheDir.'/' : $cacheDir; - - // force page refresh - if (isset($_GET['refresh']) && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV)) - { - if ($_GET['refresh'] == 'filecache') - $this->skipCache = CACHE_MODE_FILECACHE; - else if ($_GET['refresh'] == 'memcached') - $this->skipCache = CACHE_MODE_MEMCACHED; - else if ($_GET['refresh'] == '') - $this->skipCache = CACHE_MODE_FILECACHE | CACHE_MODE_MEMCACHED; - } - - // prep js+css includes - $parentVars = get_class_vars(__CLASS__); - if ($parentVars['scripts'] != $this->scripts) // additions set in child class - $this->scripts = array_merge($parentVars['scripts'], $this->scripts); - - $this->addScript(...$this->scripts); - - if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) - $this->addScript([SC_CSS_FILE, 'css/staff.css'], [SC_JS_FILE, 'js/staff.js']); - - // display modes - if (isset($_GET['power']) && method_exists($this, 'generateTooltip')) - $this->mode = CACHE_TYPE_TOOLTIP; - else if (isset($_GET['xml']) && method_exists($this, 'generateXML')) - $this->mode = CACHE_TYPE_XML; - else - { - // get alt header logo - if ($ahl = DB::Aowow()->selectCell('SELECT altHeaderLogo FROM ?_home_featuredbox WHERE ?d BETWEEN startDate AND endDate ORDER BY id DESC', time())) - $this->headerLogo = Util::defStatic($ahl); - - $this->gUser = User::getUserGlobal(); - $this->gFavorites = User::getFavorites(); - $this->pageTemplate['pageName'] = strtolower($pageCall); - - $this->wowheadLink = sprintf(WOWHEAD_LINK,Lang::getLocale()->domain(), $pageCall, $pageParam); - - if (!$this->isValidPage()) - $this->error(); - } - - // requires authed user - if ($this->reqAuth && !User::isLoggedIn()) - $this->forwardToSignIn($_SERVER['QUERY_STRING'] ?? ''); - - // restricted access - if ($this->reqUGroup && !User::isInGroup($this->reqUGroup)) - { - if (User::isLoggedIn()) - $this->error(); - else - $this->forwardToSignIn($_SERVER['QUERY_STRING'] ?? ''); - } - - if (Cfg::get('MAINTENANCE') && !User::isInGroup(U_GROUP_EMPLOYEE)) - $this->maintenance(); - else if (Cfg::get('MAINTENANCE') && User::isInGroup(U_GROUP_EMPLOYEE)) - Util::addNote('Maintenance mode enabled!'); - - // get errors from previous page from session and apply to template - if (method_exists($this, 'applyCCErrors')) - $this->applyCCErrors(); - } - - - /**********/ - /* Checks */ - /**********/ - - // "template_exists" - private function isSaneInclude(string $path, string $file) : bool - { - if (preg_match('/[^\w\-]/i', str_replace('admin/', '', $file))) - return false; - - if (!is_file($path.$file.'.tpl.php')) - return false; - - return true; - } - - // has a valid combination of categories - private function isValidPage() : bool - { - if (!isset($this->category) || empty($this->validCats)) - return true; - - $c = $this->category; // shorthand - - switch (count($c)) - { - case 0: // no params works always - return true; - case 1: // null is valid || value in a 1-dim-array || (key for a n-dim-array && ( has more subcats || no further subCats )) - $filtered = array_filter($this->validCats, function ($x) { return is_int($x); }); - return $c[0] === null || in_array($c[0], $filtered) || (!empty($this->validCats[$c[0]]) && (is_array($this->validCats[$c[0]]) || $this->validCats[$c[0]] === true)); - case 2: // first param has to be a key. otherwise invalid - if (!isset($this->validCats[$c[0]])) - return false; - - // check if the sub-array is n-imensional - if (is_array($this->validCats[$c[0]]) && count($this->validCats[$c[0]]) == count($this->validCats[$c[0]], COUNT_RECURSIVE)) - return in_array($c[1], $this->validCats[$c[0]]); // second param is value in second level array - else - return isset($this->validCats[$c[0]][$c[1]]); // check if params is key of another array - case 3: // 3 params MUST point to a specific value - return isset($this->validCats[$c[0]][$c[1]]) && in_array($c[2], $this->validCats[$c[0]][$c[1]]); - } - - return false; - } - - - /****************/ - /* Prepare Page */ - /****************/ - - // get from cache ?: run generators - protected function prepareContent() : void - { - if (!$this->loadCache()) - { - $this->generateContent(); - $this->generatePath(); - $this->generateTitle(); - $this->addArticle(); - - $this->applyGlobals(); - - $this->saveCache(); - } - - if ($this instanceof GuidePage) - { - $this->gPageInfo = ['name' => $this->name]; - if (isset($this->author)) - $this->gPageInfo['author'] = $this->author; - - } - else if (isset($this->type) && isset($this->typeId)) - { - $this->gPageInfo = array( // varies slightly for special pages like maps, user-dashboard or profiler - 'type' => $this->type, - 'typeId' => $this->typeId, - 'name' => $this->name - ); - } - - // only adds edit links to the staff menu: precursor to guides? - if (!empty($this->articleUrl) && !($this instanceof GuidePage && $this->show == GuidePage::SHOW_GUIDE)) - { - $this->gPageInfo = array( - 'articleUrl' => $this->fullParams, // is actually be the url-param - 'editAccess' => $this->editAccess ?? (U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_BUREAU) - ); - } - - if (!empty($this->path)) - $this->pageTemplate['breadcrumb'] = $this->path; - - if (!empty($this->filterObj)) - $this->pageTemplate['filter'] = empty($this->filterObj->query) ? 0 : 1; - - if (method_exists($this, 'postCache')) // e.g. update dates for events and such - $this->postCache(); - - // determine contribute tabs - if (isset($this->subject)) - { - $x = get_class($this->subject); - $this->contribute = $x::$contribute; - } - - if ($this->contribute & CONTRIBUTE_CO) - $this->community['co'] = CommunityContent::getComments($this->type, $this->typeId); - - if ($this->contribute & CONTRIBUTE_SS) - $this->community['ss'] = CommunityContent::getScreenshots($this->type, $this->typeId); - - if ($this->contribute & CONTRIBUTE_VI) - $this->community['vi'] = CommunityContent::getVideos($this->type, $this->typeId); - - // as comments are not cached, those globals cant be either - if ($this->contribute != CONTRIBUTE_NONE) - { - $this->extendGlobalData(CommunityContent::getJSGlobals()); - $this->applyGlobals(); - } - - $this->time = microtime(true) - $this->time; - $this->sumSQLStats(); - } - - public function addScript(array ...$structs) : void - { - array_walk($structs, function(&$x) { $x = array_pad($x, 3, 0); }); - - foreach ($structs as [$type, $str, $flags]) - { - if (empty($str)) - { - trigger_error('GenericPage::addScript - content empty', E_USER_WARNING); - continue; - } - - $dynData = strpos($str, '?data=') === 0; - $app = []; - - // insert locale string - if ($flags & SC_FLAG_LOCALIZED) - $str = sprintf($str, Lang::getLocale()->json()); - - if ($dynData) - { - $app[] = 'locale='.Lang::getLocale()->value; - $app[] = 't='.$_SESSION['dataKey']; - } - else if (($flags & SC_FLAG_APPEND_LOCALE) && Lang::getLocale() != Locale::EN) - $app[] = 'lang='.Lang::getLocale()->domain(); - - // append anti-cache timestamp - if (!($flags & SC_FLAG_NO_TIMESTAMP) && !$dynData) - if ($type == SC_JS_FILE || $type == SC_CSS_FILE) - $app[] = filemtime('static/'.$str) ?: 0; - - if ($app) - $str .= ($dynData ? '&' : '?').implode('&', $app); - - switch ($type) - { - case SC_JS_FILE: - $str = ($dynData ? Cfg::get('HOST_URL') : Cfg::get('STATIC_URL')).'/'.$str; - case SC_JS_STRING: - if ($flags & SC_FLAG_PREFIX) - array_unshift($this->js, [$type, $str]); - else - $this->js[] = [$type, $str]; - break; - case SC_CSS_FILE: - $str = Cfg::get('STATIC_URL').'/'.$str; - case SC_CSS_STRING: - if ($flags & SC_FLAG_PREFIX) - array_unshift($this->css, [$type, $str]); - else - $this->css[] = [$type, $str]; - break; - default: - trigger_error('GenericPage::addScript - unknown script type #'.$type, E_USER_WARNING); - } - } - } - - // get article & static infobox (run before processing jsGlobals) - private function addArticle() :void - { - if (isset($this->article)) - return; - - $article = []; - if (isset($this->guideRevision)) - $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `rev` = ?d', - Type::GUIDE, $this->typeId, $this->guideRevision); - else if (!empty($this->articleUrl)) - $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `url` = ? AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', - $this->articleUrl, [Lang::getLocale()->value, Locale::EN->value]); - else if (!empty($this->type) && isset($this->typeId)) - $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', - $this->type, $this->typeId, [Lang::getLocale()->value, Locale::EN->value]); - - if ($article) - { - if ($article['article']) - Markup::parseTags($article['article'], $this->jsgBuffer); - - $this->article = array( - 'text' => Util::jsEscape(Util::defStatic($article['article'])), - 'params' => [] - ); - - if (!empty($this->type) && isset($this->typeId)) - $this->article['params']['dbpage'] = true; - - // convert U_GROUP_* to MARKUP.CLASS_* (as seen in js-object Markup) - if ($article['editAccess'] & (U_GROUP_ADMIN | U_GROUP_VIP | U_GROUP_DEV)) - $this->article['params']['allow'] = '$Markup.CLASS_ADMIN'; - else if ($article['editAccess'] & U_GROUP_STAFF) - $this->article['params']['allow'] = '$Markup.CLASS_STAFF'; - else if ($article['editAccess'] & U_GROUP_PREMIUM) - $this->article['params']['allow'] = '$Markup.CLASS_PREMIUM'; - else if ($article['editAccess'] & U_GROUP_PENDING) - $this->article['params']['allow'] = '$Markup.CLASS_PENDING'; - else - $this->article['params']['allow'] = '$Markup.CLASS_USER'; - - $this->editAccess = $article['editAccess']; - - if ($article['locale'] != Lang::getLocale()->value) - $this->article['params']['prepend'] = '
'.Lang::main('langOnly', [Lang::lang($article['locale'])]).'
'; - - if (method_exists($this, 'postArticle')) // e.g. update variables in article - $this->postArticle($this->article['text']); - } - } - - // get announcements and notes for user - private function addAnnouncements(bool $pagespecific = true) : void - { - if (!isset($this->announcements)) - $this->announcements = []; - - // display occured notices - if (([$notes, $level] = Util::getNotes()) && $notes) - { - array_unshift($notes, 'One or more issues occured, while generating this page.'); - $colors = array( // [border, text] - LOG_LEVEL_ERROR => ['C50F1F', 'E51223'], - LOG_LEVEL_WARN => ['C19C00', 'E5B700'], - LOG_LEVEL_INFO => ['3A96DD', '42ADFF'] - ); - - $this->announcements[0] = array( - 'parent' => 'announcement-0', - 'id' => 0, - 'mode' => 1, - 'status' => 1, - 'name' => 'internal error', - 'style' => 'color: #'.($colors[$level][1] ?? 'fff').'; font-weight: bold; font-size: 14px; padding-left: 40px; background-image: url('.Cfg::get('STATIC_URL').'/images/announcements/warn-small.png); background-size: 15px 15px; background-position: 12px center; border: dashed 2px #'.($colors[$level][0] ?? 'fff').';', - 'text' => '[span]'.implode("[br]", $notes).'[/span]' - ); - } - - // debug output from Util::sendMail - if (isset($_SESSION['debug-mail'])) - { - if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) - $announcements[65535] = array( - 'parent' => 'announcement-65535', - 'id' => 0, - 'mode' => 0, - 'status' => 1, - 'name' => 'Debug sendmail', - 'style' => 'color: #'.$colors[LOG_LEVEL_INFO].'; font-weight: bold; font-size: 14px; padding-left: 40px; background-image: url('.Cfg::get('STATIC_URL').'/images/announcements/warn-small.png); background-size: 15px 15px; background-position: 12px center; border: dashed 2px #'.$colors[LOG_LEVEL_INFO].';', - 'text' => '[span]'.$_SESSION['debug-mail'].'[/span]' - ); - unset($_SESSION['debug-mail']); - } - - // fetch announcements - if ($this->pageTemplate['pageName']) - { - $ann = DB::Aowow()->Select('SELECT ABS(id) AS ARRAY_KEY, a.* FROM ?_announcements a WHERE status = 1 AND (page = ? OR page = "*") AND (groupMask = 0 OR groupMask & ?d)', $pagespecific ? $this->pageTemplate['pageName'] : '', User::$groups); - foreach ($ann as $k => $v) - { - if ($t = Util::localizedString($v, 'text')) - { - $_ = array( - 'parent' => 'announcement-'.$k, - 'id' => $v['id'], - 'mode' => $v['mode'], - 'status' => $v['status'], - 'name' => $v['name'], - 'text' => Util::defStatic($t) - ); - - if ($v['style']) // may be empty - $_['style'] = Util::defStatic($v['style']); - - $this->announcements[$k] = $_; - } - } - } - } - - protected function getCategoryFromUrl(string $urlParam) : void - { - $arr = explode('.', $urlParam); - $params = []; - - foreach ($arr as $v) - if (is_numeric($v)) - $params[] = (int)$v; - - $this->category = $params; - } - - protected function forwardToSignIn(string $next = '') : void - { - $next = $next ? '&next='.$next : ''; - header('Location: ?account=signin'.$next, true, 302); - } - - protected function sumSQLStats() : void - { - Util::arraySumByKey($this->mysql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); - } - - - /*******************/ - /* Special Display */ - /*******************/ - - // unknown entry - public function notFound(string $title = '', string $msg = '') : never - { - if ($this->mode == CACHE_TYPE_TOOLTIP && method_exists($this, 'generateTooltip')) - { - header(MIME_TYPE_JSON); - echo $this->generateTooltip(); - } - else if ($this->mode == CACHE_TYPE_XML && method_exists($this, 'generateXML')) - { - header(MIME_TYPE_XML); - echo $this->generateXML(); - } - else - { - header('HTTP/1.0 404 Not Found', true, 404); - - array_unshift($this->title, Lang::main('nfPageTitle')); - - $this->contribute = CONTRIBUTE_NONE; - $this->notFound = array( - 'title' => isset($this->typeId) ? Util::ucFirst($title).' #'.$this->typeId : $title, - 'msg' => !$msg && isset($this->typeId) ? sprintf(Lang::main('pageNotFound'), $title) : $msg - ); - - if (isset($this->tabId)) - $this->pageTemplate['activeTab'] = $this->tabId; - - $this->sumSQLStats(); - - - $this->display('list-page-generic'); - } - - exit(); - } - - // unknown page - public function error() : never - { - // $this->path = null; - // $this->tabId = null; - $this->articleUrl = 'page-not-found'; - $this->title[] = Lang::main('errPageTitle'); - $this->name = Lang::main('errPageTitle'); - // $this->lvTabs = []; - - $this->addArticle(); - - $this->sumSQLStats(); - - header('HTTP/1.0 404 Not Found', true, 404); - - $this->display('list-page-generic'); - exit(); - } - - // display brb gnomes - public function maintenance() : never - { - header('HTTP/1.0 503 Service Temporarily Unavailable', true, 503); - header('Retry-After: '.(3 * HOUR)); - - $this->display('maintenance'); - exit(); - } - - - /*******************/ - /* General Display */ - /*******************/ - - // load given template string or GenericPage::$tpl - public function display(string $override = '') : never - { - // Heisenbug: IE11 and FF32 will sometimes (under unknown circumstances) cache 302 redirects and stop - // re-requesting them from the server but load them from local cache, thus breaking menu features. - Util::sendNoCacheHeader(); - - if ($this->mode == CACHE_TYPE_TOOLTIP && method_exists($this, 'generateTooltip')) - $this->displayExtra([$this, 'generateTooltip']); - else if ($this->mode == CACHE_TYPE_XML && method_exists($this, 'generateXML')) - $this->displayExtra([$this, 'generateXML'], MIME_TYPE_XML); - else - { - if (isset($this->tabId)) - $this->pageTemplate['activeTab'] = $this->tabId; - - if ($override) - { - $this->addAnnouncements(false); - - include('template/pages/'.$override.'.tpl.php'); - die(); - } - else if ($this->tpl) - { - $this->prepareContent(); - - if (!$this->isSaneInclude('template/pages/', $this->tpl)) - { - trigger_error('Error: nonexistant template requested: template/pages/'.$this->tpl.'.tpl.php', E_USER_ERROR); - $this->error(); - } - - $this->addAnnouncements(); - - if (isset($this->lvTabs)) - { - $this->lvTabs = array_filter($this->lvTabs); - array_walk($this->lvTabs, function (&$x) { $x = array_pad($x, 3, null); }); - } - - include('template/pages/'.$this->tpl.'.tpl.php'); - die(); - } - else - $this->error(); - } - } - - // generate and cache - public function displayExtra(callable $generator, string $mime = MIME_TYPE_JSON) : never - { - $outString = ''; - if (!$this->loadCache($outString)) - { - $outString = $generator(); - $this->saveCache($outString); - } - - header($mime); - - if (method_exists($this, 'postCache') && ($pc = $this->postCache())) - die(sprintf($outString, ...$pc)); - else - die($outString); - } - - // load jsGlobal - public function writeGlobalVars() : string - { - $buff = ''; - - if (!empty($this->guideRating)) - $buff .= sprintf(Util::$guideratingString, ...$this->guideRating); - - foreach ($this->jsGlobals as $type => $struct) - { - $buff .= " var _ = ".$struct[0].';'; - - foreach ($struct[1] as $key => $data) - { - foreach ($data as $k => $v) - { - // localizes expected fields .. except for icons .. icons are special - if (in_array($k, ['name', 'namefemale']) && $struct[0] != Type::getJSGlobalString(Type::ICON)) - { - $data[$k.'_'.Lang::getLocale()->json()] = $v; - unset($data[$k]); - } - } - - $buff .= ' _['.(is_numeric($key) ? $key : "'".$key."'")."]=".Util::toJSON($data).';'; - } - - $buff .= "\n"; - - if (!empty($this->typeId) && !empty($struct[2][$this->typeId])) - { - $x = $struct[2][$this->typeId]; - - // spell - if (!empty($x['tooltip'])) // spell + item - $buff .= "\n _[".$x['id'].'].tooltip_'.Lang::getLocale()->json().' = '.Util::toJSON($x['tooltip']).";\n"; - if (!empty($x['buff'])) // spell - $buff .= " _[".$x['id'].'].buff_'.Lang::getLocale()->json().' = '.Util::toJSON($x['buff']).";\n"; - if (!empty($x['spells'])) // spell + item - $buff .= " _[".$x['id'].'].spells_'.Lang::getLocale()->json().' = '.Util::toJSON($x['spells']).";\n"; - if (!empty($x['buffspells'])) // spell - $buff .= " _[".$x['id'].'].buffspells_'.Lang::getLocale()->json().' = '.Util::toJSON($x['buffspells']).";\n"; - - $buff .= "\n"; - } - } - - return $buff; - } - - protected function fmtCreateIcon(int $iconIdx, int $type, int $typeId, int $pad = 0, string $element = 'icontab-icon', int $size = 1, string $num = '', string $qty = '') : string - { - // $element, $iconTabIdx, [typeId, size, num, qty] - $createIconString = "\$WH.ge('%s%d').appendChild(%s.createIcon(%s));\n"; - - if ($size < 0 || $size > 3) - { - trigger_error('GenericPage::fmtCreateIcon - invalid icon size '.$size.'. Normalied to 1 [small]', E_USER_WARNING); - $size = 1; - } - - $jsg = Type::getJSGlobalString($type); - if (!$jsg) - { - trigger_error('GenericPage::fmtCreateIcon - invalid type '.$type.'. Assumed '.Type::SPELL.' [spell]', E_USER_WARNING); - $jsg = Type::getJSGlobalString(Type::SPELL); - } - - $params = [$typeId, $size]; - if ($num || $qty) - $params[] = is_numeric($num) ? $num : "'".$num."'"; - if ($qty) - $params[] = is_numeric($qty) ? $qty : "'".$qty."'"; - - // $WH.ge('icontab-icon1').appendChild(g_spells.createIcon(40120, 1, '1-4', 0)); - return str_repeat(' ', $pad) . sprintf($createIconString, $element, $iconIdx, $jsg, implode(', ', $params)); - } - - protected function fmtStaffTip(?string $text, string $tip) : string - { - if (!$text || !User::isInGroup(U_GROUP_EMPLOYEE)) - return $text ?? ''; - else - return sprintf(Util::$dfnString, $tip, $text); - } - - // load brick - protected function brick(string $file, array $localVars = []) : void - { - foreach ($localVars as $n => $v) - $$n = $v; - - if (!$this->isSaneInclude('template/bricks/', $file)) - trigger_error('Nonexistant template requested: template/bricks/'.$file.'.tpl.php', E_USER_ERROR); - else - include('template/bricks/'.$file.'.tpl.php'); - } - - // load listview addIns - protected function lvBrick(string $file) : void - { - if (!$this->isSaneInclude('template/listviews/', $file)) - trigger_error('Nonexistant Listview addin requested: template/listviews/'.$file.'.tpl.php', E_USER_ERROR); - else - include('template/listviews/'.$file.'.tpl.php'); - } - - // load brick with more text then vars - protected function localizedBrick(string $file, ?Locale $loc = null) : void - { - $loc ??= Lang::getLocale(); - - if (!$this->isSaneInclude('template/localized/', $file.'_'.$loc->value)) - { - if ($loc == Locale::EN || !$this->isSaneInclude('template/localized/', $file.'_'.Locale::EN->value)) - trigger_error('Nonexistant template requested: template/localized/'.$file.'_'.$loc->value.'.tpl.php', E_USER_ERROR); - else - include('template/localized/'.$file.'_'.Locale::EN->value.'.tpl.php'); - } - else - include('template/localized/'.$file.'_'.$loc->value.'.tpl.php'); - } - - - /**********************/ - /* Prepare js-Globals */ - /**********************/ - - // add typeIds that should be displayed as jsGlobal on the page - public function extendGlobalIds(int $type, int ...$ids) : void - { - if (!$type || !$ids) - return; - - if (!isset($this->jsgBuffer[$type])) - $this->jsgBuffer[$type] = []; - - foreach ($ids as $id) - $this->jsgBuffer[$type][] = $id; - } - - // add jsGlobals or typeIds (can be mixed in one array: TYPE => [mixeddata]) to display on the page - public function extendGlobalData(array $data, ?array $extra = null) : void - { - foreach ($data as $type => $globals) - { - if (!is_array($globals) || !$globals) - continue; - - $this->initJSGlobal($type); - - // can be id => data - // or idx => id - // and may be mixed - foreach ($globals as $k => $v) - { - if (is_array($v)) - $this->jsGlobals[$type][1][$k] = $v; - else if (is_numeric($v)) - $this->extendGlobalIds($type, $v); - } - } - - if (is_array($extra) && $extra) - $this->jsGlobals[$type][2] = $extra; - } - - // init store for type - private function initJSGlobal(int $type) : void - { - $jsg = &$this->jsGlobals; // shortcut - - if (isset($jsg[$type])) - return; - - if ($tpl = Type::getJSGlobalTemplate($type)) - $jsg[$type] = $tpl; - } - - // lookup jsGlobals from collected typeIds - private function applyGlobals() : void - { - foreach ($this->jsgBuffer as $type => $ids) - { - foreach ($ids as $k => $id) // filter already generated data, maybe we can save a lookup or two - if (isset($this->jsGlobals[$type][1][$id])) - unset($ids[$k]); - - if (!$ids) - continue; - - $this->initJSGlobal($type); - - $obj = Type::newList($type, [Cfg::get('SQL_LIMIT_NONE'), ['id', array_unique($ids, SORT_NUMERIC)]]); - if (!$obj) - continue; - - $this->extendGlobalData($obj->getJSGlobals(GLOBALINFO_SELF)); - - // delete processed ids - $this->jsgBuffer[$type] = []; - } - } - - - /*********/ - /* Cache */ - /*********/ - - // visible properties or given strings are cached - private function saveCache(string $saveString = '') : void - { - if ($this->mode == CACHE_TYPE_NONE) - return; - - if (!Cfg::get('CACHE_MODE') || Cfg::get('DEBUG')) - return; - - $noCache = ['coError', 'ssError', 'viError']; - $cKey = $this->generateCacheKey(); - $cache = []; - if (!$saveString) - { - foreach ($this as $key => $val) - { - try - { - $rp = new \ReflectionProperty($this, $key); - if ($rp && ($rp->isPublic() || $rp->isProtected())) - if (!in_array($key, $noCache)) - $cache[$key] = $val; - } - catch (\ReflectionException $e) { } // shut up! - } - } - else - $cache = $saveString; - - if (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED) - { - // on &refresh also clear related - if ($this->skipCache == CACHE_MODE_MEMCACHED) - { - $oldMode = $this->mode; - for ($i = 1; $i < 5; $i++) // page (1), tooltips (2), searches (3) and xml (4) - { - $this->mode = $i; - for ($j = 0; $j < 2; $j++) // staff / normal - $this->memcached()->delete($this->generateCacheKey($j)); - } - - $this->mode = $oldMode; - } - - $data = array( - 'timestamp' => time(), - 'revision' => AOWOW_REVISION, - 'isString' => $saveString ? 1 : 0, - 'data' => $cache - ); - - $this->memcached()->set($cKey, $data); - } - - if (Cfg::get('CACHE_MODE') & CACHE_MODE_FILECACHE) - { - $data = time()." ".AOWOW_REVISION." ".($saveString ? '1' : '0')."\n"; - $data .= gzcompress($saveString ? $cache : serialize($cache), 9); - - // on &refresh also clear related - if ($this->skipCache == CACHE_MODE_FILECACHE) - { - $oldMode = $this->mode; - for ($i = 1; $i < 5; $i++) // page (1), tooltips (2), searches (3) and xml (4) - { - $this->mode = $i; - for ($j = 0; $j < 2; $j++) // staff / normal - { - $key = $this->generateCacheKey($j); - if (file_exists($this->cacheDir.$key)) - unlink($this->cacheDir.$key); - } - } - - $this->mode = $oldMode; - } - - file_put_contents($this->cacheDir.$cKey, $data); - } - } - - private function loadCache(string &$saveString = '') : bool - { - if ($this->mode == CACHE_TYPE_NONE) - return false; - - if (!Cfg::get('CACHE_MODE') || Cfg::get('DEBUG')) - return false; - - $cKey = $this->generateCacheKey(); - $rev = $type = $cache = $data = null; - - if ((Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED) && !($this->skipCache & CACHE_MODE_MEMCACHED)) - { - if ($cache = $this->memcached()->get($cKey)) - { - $type = $cache['isString']; - $data = $cache['data']; - - if ($cache['timestamp'] + Cfg::get('CACHE_DECAY') <= time() || $cache['revision'] != AOWOW_REVISION) - $cache = null; - else - $this->cacheLoaded = [CACHE_MODE_MEMCACHED, $cache['timestamp']]; - } - } - - if (!$cache && (Cfg::get('CACHE_MODE') & CACHE_MODE_FILECACHE) && !($this->skipCache & CACHE_MODE_FILECACHE)) - { - if (!file_exists($this->cacheDir.$cKey)) - return false; - - $cache = file_get_contents($this->cacheDir.$cKey); - if (!$cache) - return false; - - $cache = explode("\n", $cache, 2); - $data = $cache[1]; - if (substr_count($cache[0], ' ') < 2) - return false; - - [$time, $rev, $type] = explode(' ', $cache[0]); - - if ($time + Cfg::get('CACHE_DECAY') <= time() || $rev != AOWOW_REVISION) - $cache = null; - else - { - $this->cacheLoaded = [CACHE_MODE_FILECACHE, $time]; - $data = gzuncompress($data); - } - } - - if (!$cache) - return false; - - if ($type == '0') - { - if (is_string($data)) - $data = unserialize($data); - - foreach ($data as $k => $v) - $this->$k = $v; - - return true; - } - else if ($type == '1') - { - $saveString = $data; - return true; - } - - return false; - } - - private function memcached() : \Memcached - { - if (!$this->memcached && (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED)) - { - $this->memcached = new \Memcached(); - $this->memcached->addServer('localhost', 11211); - } - - return $this->memcached; - } -} - -?> diff --git a/setup/updates/1758578400_01.sql b/setup/updates/1758578400_01.sql new file mode 100644 index 00000000..66e1f52b --- /dev/null +++ b/setup/updates/1758578400_01.sql @@ -0,0 +1,6 @@ +DELETE FROM `aowow_config` WHERE `key` IN ('rep_req_ext_links', 'gtag_measurement_id'); +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('rep_req_ext_links', 150, 150, 5, 129, 'required reputation to link to external sites'), + ('gtag_measurement_id', '', NULL, 6, 136, 'Enter your Google Tag measurement ID here to track site stats'); + +UPDATE `aowow_config` SET `key` = 'ua_measurement_key', `comment` = '[DEPRECATED ?] Enter your Google Universal Analytics key here to track site stats' WHERE `key` = 'analytics_user'; diff --git a/static/css/consent.css b/static/css/consent.css new file mode 100644 index 00000000..049b58c4 --- /dev/null +++ b/static/css/consent.css @@ -0,0 +1,180 @@ +#consent-overlay .dark-filter.fade-in { + animation-name: fade-in; + animation-duration: 400ms; + animation-timing-function: ease-in-out; +} + +#consent-overlay .dark-filter.hide { + display: none !important +} + +#consent-overlay .dark-filter { + z-index: 2147483645; + background: rgba(0,0,0,.5); + width: 100%; + height: 100%; + overflow: hidden; + position: fixed; + top: 0; + bottom: 0; + left: 0; +} + +@keyframes fade-in { + 0% { opacity: 0 } + 100% { opacity: 1 } +} + +#consent-overlay #banner { + position: fixed; + z-index: 2147483645; + bottom: 0; + right: 0; + left: 0; + background-color: #181818; + max-height: 90%; + min-height: 125px; + overflow-x: hidden; + overflow-y: auto; + outline: none !important; +} + +#consent-overlay #policy { + margin: 1.25em 0 .625em 2em; + overflow: hidden; +} + +#consent-overlay #banner #policy-title { + font-size: revert; + line-height: 1.3; + margin-top: 0px; + margin-bottom: 10px; + color: #fff; + width: 50%; +} + +#consent-overlay #banner #policy-text { + clear: both; + text-align: left; + line-height: 1.5; + width: 50%; + border-right: 1px solid #d8d8d8; + padding-right: 1rem; + padding-bottom: 1em +} + +#consent-overlay #banner #policy-text * { + font-size: inherit; + line-height: inherit; +} + +#consent-overlay #banner #policy-text a { + font-weight: bold; + margin-left: 5px; +} + +#consent-overlay #banner #policy-title, +#consent-overlay #banner #policy-text { + float: left; +} + +#consent-overlay #banner .columns { + width: 100%; + float: left; + box-sizing: border-box; + padding: 0; + display: initial; + margin-left: 4%; + width: 82.6666666667%; +} + +#consent-overlay #banner .columns:first-child { + margin-left: 0; +} + +#consent-overlay #banner #accept-btn, +#consent-overlay #banner #reject-all { + outline-offset: 1px; +} + +#consent-overlay .ggl-container { + width: 45%; + padding-left: 1rem; + display: inline-block; + float: none; +} + +#consent-overlay .ggl-container div, +#consent-overlay #policy-text { + border-color: #303030 !important; +} + +#consent-overlay .ggl-title { + line-height: 1.7; + color: #fff; + margin-bottom: 10px; + margin-top: 0; + font-weight: 600; + font-family: inherit; +} + +#consent-overlay .ggl-text { + margin-bottom: 10px; + color: #bbb; +} + +#consent-overlay #banner #button-container { + left: auto; + right: 4%; + margin-left: 0; + min-height: 1px; + text-align: center; + position: absolute; + width: 13.3333333333%; +} + +#consent-overlay .columns { + float: left; + box-sizing: border-box; + padding: 0; + display: initial; +} + +#consent-overlay #banner #button-group { + display: inline-block; + margin-right: auto; +} + +#consent-overlay #banner #button-group button { + display: block; +} + +#consent-overlay #accept-btn, +#consent-overlay #reject-all { + margin-top: 1em; + margin-right: 1em; + outline-offset: 1px; + min-width: 125px; + height: auto; + white-space: normal; + word-break: break-word; + word-wrap: break-word; + padding: 12px 10px; + line-height: 1.2; + width: 100%; + background-color: #a71a19; + border-radius: 4px; + border: none; + text-decoration: none !important; + color: #fff; + font-weight: 600; + transition: 75ms; + opacity: 1 !important; +} + +#consent-overlay #accept-btn:hover, +#consent-overlay #reject-all:hover { + box-shadow: none; + background-color: #da2020 !important; + border-color: #da2020 !important; +} diff --git a/static/js/consent.js b/static/js/consent.js new file mode 100644 index 00000000..1142d60d --- /dev/null +++ b/static/js/consent.js @@ -0,0 +1,10 @@ +$(document).ready(function() { + $WH.qs('#consent-overlay #accept-btn').onclick = function () { + $WH.sc('consent', 1000, 1); + $WH.ge('consent-overlay').style.display = 'none'; + }; + $WH.qs('#consent-overlay #reject-all').onclick = function () { + $WH.sc('consent', 1000, 0); + $WH.ge('consent-overlay').style.display = 'none'; + }; +}); diff --git a/template/bricks/contribute.tpl.php b/template/bricks/contribute.tpl.php index a4387dd9..20d9834a 100644 --- a/template/bricks/contribute.tpl.php +++ b/template/bricks/contribute.tpl.php @@ -1,7 +1,7 @@ contribute)): +if ($this->contribute): ?>
@@ -11,7 +11,7 @@ if (!empty($this->contribute)):
localizedBrick('contrib'); + $this->localizedBrick('contrib', ['coError' => $this->community['coError'], 'ssError' => $this->community['ssError'], 'viError' => $this->community['viError']]); ?>
diff --git a/template/bricks/filter.tpl.php b/template/bricks/filter.tpl.php deleted file mode 100644 index e54c3104..00000000 --- a/template/bricks/filter.tpl.php +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/template/bricks/footer.tpl.php b/template/bricks/footer.tpl.php index b63bfba3..73eb9586 100644 --- a/template/bricks/footer.tpl.php +++ b/template/bricks/footer.tpl.php @@ -1,23 +1,27 @@ - +