diff --git a/endpoints/profile/avatar.php b/endpoints/profile/avatar.php new file mode 100644 index 00000000..6b6ce37f --- /dev/null +++ b/endpoints/profile/avatar.php @@ -0,0 +1,68 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^\d+\.jpg$/'] ], + 'size' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + size: [optional] + return: + + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + $this->generate404(); + + $profileId = substr($this->_get['id'], 0, -4); + + $charData = DB::Aowow()->selectRow('SELECT `race`, `gender` FROM ?_profiler_profiles WHERE id = ?d', $profileId); + if (!$charData) + $this->generate404(); + + $gender = $charData['gender'] ? 'female' : 'male'; + $race = ChrRace::tryFrom($charData['race'])?->json() ?? 'human'; + $size = match($this->_get['size']) + { + 'small', + 'medium', + 'large' => $this->_get['size'], + default => 'medium' + }; + + $this->redirectTo = sprintf('%s/images/armory/%s/default_%s_%s.jpg', Cfg::get('STATIC_URL'), $size, $race, $gender); + } +} + +?> diff --git a/endpoints/profile/delete.php b/endpoints/profile/delete.php new file mode 100644 index 00000000..0bf905cd --- /dev/null +++ b/endpoints/profile/delete.php @@ -0,0 +1,49 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']], + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return + null + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileDeleteResponse - profileId empty', E_USER_WARNING); + return; + } + + // only flag as deleted; only custom profiles + DB::Aowow()->query( + 'UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `cuFlags` & ?d {AND `user` = ?d}', + PROFILER_CU_DELETED, + $this->_get['id'], + PROFILER_CU_PROFILE, + User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? DBSIMPLE_SKIP : User::$id + ); + } +} + +?> diff --git a/endpoints/profile/link.php b/endpoints/profile/link.php new file mode 100644 index 00000000..bcd32ab0 --- /dev/null +++ b/endpoints/profile/link.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return: + null + */ + protected function generate() : void // links char with account + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileLinkResponse - profileId empty', E_USER_ERROR); + return; + } + + // only link characters, not custom profiles + $newId = DB::Aowow()->query( + 'REPLACE INTO ?_account_profiles (`accountId`, `profileId`, `extraFlags`) + SELECT ?d, p.`id`, 0 FROM ?_profiler_profiles p WHERE p.`id` = ?d AND (`cuFlags` & ?d) = 0', + User::$id, $this->_get['id'], PROFILER_CU_PROFILE + ); + + if (!is_int($newId)) + trigger_error('ProfileLinkResponse - some of the profileIds were custom or do not exist', E_USER_ERROR); + } +} + +?> diff --git a/endpoints/profile/load.php b/endpoints/profile/load.php new file mode 100644 index 00000000..8be7a2da --- /dev/null +++ b/endpoints/profile/load.php @@ -0,0 +1,302 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'items' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkItemList']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: profileId + items: string [itemIds.join(':')] + unnamed: unixtime [only to force the browser to reload instead of cache] + return + lots... + */ + protected function generate() : void + { + // titles, achievements, characterData, talents, pets + // and some onLoad-hook to .. load it registerProfile($data) + // everything else goes through data.php .. strangely enough + + if (!$this->assertGET('id')) + { + trigger_error('ProfileLoadResponse - profileId empty', E_USER_ERROR); + return; + } + + $pBase = DB::Aowow()->selectRow('SELECT pg.`name` AS "guildname", p.* FROM ?_profiler_profiles p LEFT JOIN ?_profiler_guild pg ON pg.`id` = p.`guild` WHERE p.`id` = ?d', $this->_get['id'][0]); + if (!$pBase) + { + trigger_error('ProfileLoadResponse - called with invalid profileId #'.$this->_get['id'][0], E_USER_WARNING); + return; + } + + if (($pBase['cuFlags'] & PROFILER_CU_DELETED) && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + return; + + + $rData = []; + foreach (Profiler::getRealms() as $rId => $rData) + if ($rId == $pBase['realm']) + break; + + if (!$rData) // realm doesn't exist or access is restricted + return; + + $profile = array( + 'id' => $pBase['id'], + 'source' => $pBase['id'], + 'level' => $pBase['level'], + 'classs' => $pBase['class'], + 'race' => $pBase['race'], + 'faction' => ChrRace::tryFrom($pBase['race'])?->getTeam() ?? TEAM_NEUTRAL, + 'gender' => $pBase['gender'], + 'skincolor' => $pBase['skincolor'], + 'hairstyle' => $pBase['hairstyle'], + 'haircolor' => $pBase['haircolor'], + 'facetype' => $pBase['facetype'], + 'features' => $pBase['features'], + 'title' => $pBase['title'], + 'name' => $pBase['name'], + 'guild' => "$'".$pBase['guildname']."'", + 'published' => !!($pBase['cuFlags'] & PROFILER_CU_PUBLISHED), + 'pinned' => !!($pBase['cuFlags'] & PROFILER_CU_PINNED), + 'nomodel' => $pBase['nomodelMask'], + 'playedtime' => $pBase['playedtime'], + 'lastupdated' => $pBase['lastupdated'] * 1000, + 'talents' => array( + 'builds' => array( // notice the bullshit to prevent the talent-string from becoming a float! NOTICE IT!! + ['talents' => '$"'.$pBase['talentbuild1'].'"', 'glyphs' => $pBase['glyphs1']], + ['talents' => '$"'.$pBase['talentbuild2'].'"', 'glyphs' => $pBase['glyphs2']] + ), + 'active' => $pBase['activespec'] + ), + // set later + 'inventory' => [], + 'bookmarks' => [], // list of userIds who claimed this profile (claiming and owning are two different things) + + // completion lists: [subjectId => amount/timestamp/1] + 'skills' => [], // skillId => [curVal, maxVal] + 'reputation' => [], // factionId => curVal + 'titles' => [], // titleId => 1 + 'spells' => [], // spellId => 1; recipes, vanity pets, mounts + 'achievements' => [], // achievementId => timestamp + 'quests' => [], // questId => 1 + 'achievementpoints' => 0, // max you have + 'statistics' => [], // all raid activity [achievementId => killCount] + 'activity' => [], // recent raid activity [achievementId => 1] (is a subset of statistics) + ); + + if ($pBase['cuFlags'] & PROFILER_CU_PROFILE) + { + // this parameter is _really_ strange .. probably still not doing this right + $profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0; + + $profile['sourcename'] = $pBase['sourceName']; + $profile['description'] = $pBase['description']; + $profile['user'] = $pBase['user']; + $profile['username'] = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE `id` = ?d', $pBase['user']); + } + + // custom profiles inherit this when copied from real char :( + if ($pBase['realm']) + { + $profile['region'] = [$rData['region'], Lang::profiler('regions', $rData['region'])]; + $profile['battlegroup'] = [Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP')]; + $profile['realm'] = [Profiler::urlize($rData['name'], true), $rData['name']]; + } + + // bookmarks + if ($_ = DB::Aowow()->selectCol('SELECT `accountId` FROM ?_account_profiles WHERE `profileId` = ?d', $pBase['id'])) + $profile['bookmarks'] = $_; + + // arena teams - [size(2|3|5) => name]; name gets urlized to use as link + if ($at = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `name` FROM ?_profiler_arena_team at JOIN ?_profiler_arena_team_member atm ON atm.`arenaTeamId` = at.`id` WHERE atm.`profileId` = ?d', $pBase['id'])) + $profile['arenateams'] = $at; + + // pets if hunter fields: [name:name, family:petFamily, npc:npcId, displayId:modelId, talents:talentString] + if ($pets = DB::Aowow()->select('SELECT `name`, `family`, `npc`, `displayId`, CONCAT("$\"", `talents`, "\"") AS "talents" FROM ?_profiler_pets WHERE `owner` = ?d', $pBase['id'])) + $profile['pets'] = $pets; + + // source for custom profiles; profileId => [name, ownerId, iconString(optional)] + if ($customs = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `name`, `user`, `icon` FROM ?_profiler_profiles WHERE `sourceId` = ?d AND `sourceId` <> `id` {AND (`cuFlags` & ?d) = 0}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : PROFILER_CU_DELETED)) + { + foreach ($customs as $id => $cu) + { + if (!$cu['icon']) + unset($cu['icon']); + + $profile['customs'][$id] = array_values($cu); + } + } + + + /* $profile[] + // CUSTOM + 'auras' => [], // custom list of buffs, debuffs [spellId] + + // UNUSED + 'glyphs' => [], // provided list of already known glyphs (post cataclysm feature) + */ + + + // questId => [cat1, cat2] + $profile['quests'] = []; + if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ?_profiler_completion_quests WHERE `id` = ?d', $pBase['id'])) + { + $qList = new QuestList(array(['id', $quests], Cfg::get('SQL_LIMIT_NONE'))); + if (!$qList->error) + foreach ($qList->iterate() as $id => $__) + $profile['quests'][$id] = [$qList->getField('cat1'), $qList->getField('cat2')]; + } + + // skillId => [value, max] + $profile['skills'] = DB::Aowow()->select('SELECT `skillId` AS ARRAY_KEY, `value` AS "0", `max` AS "1" FROM ?_profiler_completion_skills WHERE `id` = ?d', $pBase['id']); + + // factionId => amount + $profile['reputation'] = DB::Aowow()->selectCol('SELECT `factionId` AS ARRAY_KEY, `standing` FROM ?_profiler_completion_reputation WHERE `id` = ?d', $pBase['id']); + + // titleId => 1 + $profile['titles'] = DB::Aowow()->selectCol('SELECT `titleId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_titles WHERE `id` = ?d', $pBase['id']); + + // achievementId => js date object + $profile['achievements'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, CONCAT("$new Date(", `date` * 1000, ")") FROM ?_profiler_completion_achievements WHERE `id` = ?d', $pBase['id']); + + // just points + $profile['achievementpoints'] = $profile['achievements'] ? DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a)', array_keys($profile['achievements'])) : 0; + + // achievementId => counter + $profile['statistics'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, `counter` FROM ?_profiler_completion_statistics WHERE `id` = ?d', $pBase['id']); + + // achievementId => 1 + $profile['activity'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_statistics WHERE `id` = ?d AND `date` > ?d', $pBase['id'], time() - MONTH); + + // spellId => 1 + $profile['spells'] = DB::Aowow()->selectCol('SELECT `spellId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_spells WHERE `id` = ?d', $pBase['id']); + + + $gItems = []; + + $usedSlots = []; + if ($this->_get['items']) + { + $phItems = new ItemList(array(['id', $this->_get['items']], ['slot', INVTYPE_NON_EQUIP, '!'])); + if (!$phItems->error) + { + $data = $phItems->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); + foreach ($phItems->iterate() as $iId => $__) + { + $sl = $phItems->getField('slot'); + foreach (Profiler::$slot2InvType as $slot => $invTypes) + { + if (in_array($sl, $invTypes) && !in_array($slot, $usedSlots)) + { + // get and apply inventory + $gItems[$iId] = array( + 'name_'.Lang::getLocale()->json() => $phItems->getField('name', true), + 'quality' => $phItems->getField('quality'), + 'icon' => $phItems->getField('iconString'), + 'jsonequip' => $data[$iId] + ); + $profile['inventory'][$slot] = [$iId, 0, 0, 0, 0, 0, 0, 0]; + + $usedSlots[] = $slot; + break; + } + } + } + } + } + + if ($items = DB::Aowow()->select('SELECT * FROM ?_profiler_items WHERE `id` = ?d', $pBase['id'])) + { + $itemz = new ItemList(array(['id', array_column($items, 'item')], Cfg::get('SQL_LIMIT_NONE'))); + if (!$itemz->error) + { + $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); + + foreach ($items as $i) + { + if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots)) + { + // get and apply inventory + $gItems[$i['item']] = array( + 'name_'.Lang::getLocale()->json() => $itemz->getField('name', true), + 'quality' => $itemz->getField('quality'), + 'icon' => $itemz->getField('iconString'), + 'jsonequip' => $data[$i['item']] + ); + $profile['inventory'][$i['slot']] = [$i['item'], $i['subItem'], $i['permEnchant'], $i['tempEnchant'], $i['gem1'], $i['gem2'], $i['gem3'], $i['gem4']]; + } + } + } + } + + $buff = ''; + foreach ($gItems as $id => $item) + $buff .= 'g_items.add('.$id.', '.Util::toJSON($item, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).");\n"; + + + // if ($au = $char->getField('auras')) + // { + // $auraz = new SpellList(array(['id', $char->getField('auras')], Cfg::get('SQL_LIMIT_NONE'))); + // $dataz = $auraz->getListviewData(); + // $modz = $auraz->getProfilerMods(); + + // // get and apply aura-mods + // foreach ($dataz as $id => $data) + // { + // $mods = []; + // if (!empty($modz[$id])) + // { + // foreach ($modz[$id] as $k => $v) + // { + // if (is_array($v)) + // $mods[] = $v; + // else if ($str = @Game::$itemMods[$k]) + // $mods[$str] = $v; + // } + // } + + // $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', callback:".Util::toJSON($mods)."});\n"; + // } + // $buff .= "\n"; + // } + + + // load available titles + Util::loadStaticFile('p-titles-'.$pBase['gender'], $buff, true); + + // add profile to buffer + $buff .= "\n\n\$WowheadProfiler.registerProfile(".Util::toJSON($profile).");"; + + $this->result = $buff."\n"; + } + + protected static function checkItemList(string $val) : array + { + // expecting item-list + if (preg_match('/\d+(:\d+)*/', $val)) + return array_map('intVal', explode(':', $val)); + + return []; + } +} + +?> diff --git a/endpoints/profile/pin.php b/endpoints/profile/pin.php new file mode 100644 index 00000000..aef2ef64 --- /dev/null +++ b/endpoints/profile/pin.php @@ -0,0 +1,58 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] + return: null + */ + protected function generate() : void // (un)favorite + { + if (!$this->assertGET('id')) + { + trigger_error('ProfilePinResponse - profileId empty', E_USER_ERROR); + return; + } + + $uid = 0; + if (!$this->_get['user'] || User::$username == $this->_get['user']) + $uid = User::$id; + else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfilePinResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + // since only one character can be pinned at a time we can reset everything + DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ~?d WHERE `accountId` = ?d', PROFILER_CU_PINNED, $uid); + // and set a single char if necessary (Replace, because this entry may not exist yet) + DB::Aowow()->query('REPLACE INTO ?_account_profiles (`accountId`, `profileId`, `extraFlags`) VALUES (?d, ?d, ?d)', $uid, $this->_get['id'][0], PROFILER_CU_PINNED); + } +} + +?> diff --git a/endpoints/profile/private.php b/endpoints/profile/private.php new file mode 100644 index 00000000..5c72d9a5 --- /dev/null +++ b/endpoints/profile/private.php @@ -0,0 +1,64 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']], + // 'bookmarked' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] // something with signatures? (must have bookmarked profile to create signature from) + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] // user page this is may be executed from + return: + null + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('ProfilePrivateResponse - profileId empty', E_USER_ERROR); + return; + } + + if ($this->_get['user'] && User::$username != $this->_get['user'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + { + trigger_error('ProfilePrivateResponse - user #'.User::$id.' tried to mark profiles of "'.$this->_get['user'].'" as private.', E_USER_ERROR); + return; + } + + $uid = 0; + if (!$this->_get['user'] || User::$username == $this->_get['user']) + $uid = User::$id; + else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfilePrivateResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ~?d WHERE `profileId` IN (?a) AND `accountId` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` & ~?d WHERE `id` IN (?a) AND `user` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + } +} + +?> diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php new file mode 100644 index 00000000..ee921ccb --- /dev/null +++ b/endpoints/profile/profile.php @@ -0,0 +1,184 @@ + Profiler > New + + protected array $dataLoader = ['enchants', 'gems', 'glyphs', 'itemsets', 'pets', 'pet-talents', 'quick-excludes', 'realms', 'statistics', 'weight-presets', 'achievements']; + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_JS_FILE, 'js/TalentCalc.js'], + [SC_JS_FILE, 'js/swfobject.js'], + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'], + [SC_JS_FILE, 'js/Profiler.js'], + [SC_CSS_FILE, 'css/talentcalc.css'], + [SC_CSS_FILE, 'css/Profiler.css'] + ); + protected array $expectedGET = array( + 'new' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public int $type = Type::PROFILE; + public bool $gDataKey = true; + + public function __construct(string $idOrProfile) + { + parent::__construct($idOrProfile); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + // neither param nor &new > error + if (!$idOrProfile && !$this->_get['new']) + $this->generateError(); + + // display empty/new profile editor > ok + if (!$idOrProfile && $this->_get['new']) + return; + + $this->getSubjectFromUrl($idOrProfile); + + // we have an ID > ok + if ($this->typeId) + return; + + // param was incomplete profile > error + if (!$this->subjectName) + $this->notFound(); + + $rnItr = 0; + // pending rename + if (preg_match('/^([^\-]+)-(\d+)$/i', $this->subjectName, $m)) + { + $this->subjectName = $m[1]; + $rnItr = $m[2]; + } + + // 3 possibilities + // 1) already synced to aowow + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ? AND `renameItr` = ?d', $this->realmId, Util::ucFirst($this->subjectName), $rnItr)) + { + $this->typeId = $subject['id']; + + if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + $this->handleIncompleteData(Type::PROFILE, $subject['realmGUID']); + + return; + } + + // can not be used to look up char on realm + if ($rnItr) + $this->notFound(); + + // 2) not yet synced but exists on realm (and not a gm character) + if ($subject = DB::Characters($this->realmId)->selectRow( + 'SELECT c.`guid` AS "realmGUID", c.`name`, c.`race`, c.`class`, c.`level`, c.`gender`, c.`at_login`, g.`guildid` AS "guildGUID", IFNULL(g.`name`, "") AS "guildName", IFNULL(gm.`rank`, 0) AS "guildRank" + FROM characters c + LEFT JOIN guild_member gm ON gm.`guid` = c.`guid` + LEFT JOIN guild g ON g.`guildid` = gm.`guildid` + WHERE c.`name` = ? AND `level` <= ?d AND (`extra_flags` & ?d) = 0', + Util::ucFirst($this->subjectName), MAX_LEVEL, Profiler::CHAR_GMFLAGS + )) + { + $subject['realm'] = $this->realmId; + $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + + if ($subject['at_login'] & 0x1) + $subject['renameItr'] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $this->realmId, $subject['name']); + + if ($subject['guildGUID']) + { + // create empty guild if nessecary to satisfy foreign keys + $subject['guild'] = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $this->realmId, $subject['guildGUID']); + if (!$subject['guild']) + $subject['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (`realm`, `realmGUID`, `cuFlags`, `name`) VALUES (?d, ?d, ?d, ?)', $this->realmId, $subject['guildGUID'], PROFILER_CU_NEEDS_RESYNC, $subject['guildName']); + } + + unset($subject['guildGUID'], $subject['guildName'], $subject['at_login']); + + // create entry from realm with enough basic info to disply tooltips + DB::Aowow()->query('REPLACE INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($subject), array_values($subject)); + $this->typeId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $this->realmId, $subject['realmGUID']); + + $this->handleIncompleteData(Type::PROFILE, $subject['realmGUID']); + return; + } + + // 3) does not exist at all + $this->notFound(); + } + + protected function generate() : void + { + if ($this->doResync) + return; + + if ($this->typeId) + { + $subject = new LocalProfileList(array(['id', $this->typeId])); + if ($subject->error) + $this->notFound(); + + if (!$subject->isVisibleToUser()) + $this->notFound(); + + // character profile accessed by id + if (!$subject->isCustom() && !$this->subjectName) + $this->forward($subject->getProfileUrl()); + } + + parent::generate(); + + array_unshift($this->title, Util::ucFirst(Lang::game('profile'))); + + + // as demanded by the raid activity tracker + $bossIds = array( + // ruby: Halion + 39863, + // icc: Valanar, Lana'thel, Saurfang, Festergut, Deathwisper, Marrowgar, Putricide, Rotface, Sindragosa, Valithria, Lich King + 37970, 37955, 37813, 36626, 36855, 36612, 36678, 36627, 36853, 36789, 36597, + // toc: Jaraxxus, Anub'arak + 34780, 34564, + // ony: Onyxia + 10184, + // uld: Flame Levi, Ignis, Razorscale, XT-002, Kologarn, Auriaya, Freya, Hodir, Mimiron, Thorim, Vezaxx, Yogg, Algalon + 33113, 33118, 33186, 33293, 32930, 33515, 32906, 32845, 33350, 32864, 33271, 33288, 32871, + // nax: Anub, Faerlina, Maexxna, Noth, Heigan, Loatheb, Razuvious, Gothik, Patchwerk, Grobbulus, Gluth, Thaddius, Sapphiron, Kel'Thuzad + 15956, 15953, 15952, 15954, 15936, 16011, 16061, 16060, 16028, 15931, 15932, 15928, 15989, 15990 + ); + $this->extendGlobalIds(Type::NPC, ...$bossIds); + + // dummy title from dungeon encounter + foreach (Lang::profiler('encounterNames') as $id => $name) + $this->extendGlobalData([Type::NPC => [$id => ['name_'.Lang::getLocale()->json() => $name]]]); + } + + private function notFound() : never + { + if ($this->subjectName && $this->realm) + $head = Lang::profiler('firstUseTitle', [Util::ucFirst($this->subjectName), $this->realm]); + else + $head = Lang::profiler('profiler'); + + // unsetting typeId to prevent it from being added to the title string in the input-box is jank galore + // but it isn't needed for the not-found case anyway, right...? + unset($this->typeId); + + parent::generateNotFound($head, Lang::profiler('notFound', 'profile')); + } +} + +?> diff --git a/endpoints/profile/profile_power.php b/endpoints/profile/profile_power.php new file mode 100644 index 00000000..ea15084a --- /dev/null +++ b/endpoints/profile/profile_power.php @@ -0,0 +1,84 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->getSubjectFromUrl($pageParam); + + if ($this->subjectName) // pageParam is fully defined profiler string + { + // pending rename + if (preg_match('/([^\-]+)-(\d+)/i', $this->subjectName, $m)) + [, $this->subjectName, $renameItr] = $m; + + if ($x = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND LOWER(`name`) = ? AND `renameItr` = ?d', $this->realmId, $this->subjectName, $renameItr ?? 0)) + $this->typeId = $x; + } + + if (!$this->typeId) + $this->generate404(); + } + + protected function generate() : void + { + $profile = new LocalProfileList(array(['id', $this->typeId])); + if ($profile->error || !$profile->isVisibleToUser()) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $n = $profile->getField('name'); + $r = $profile->getField('race'); + $c = $profile->getField('class'); + $g = $profile->getField('gender'); + $l = $profile->getField('level'); + + if (!$this->subjectName) // implicit isCustom + $n .= Lang::profiler('customProfile'); + else if ($_ = $profile->getField('title')) + if ($title = (new TitleList(array(['id', $_])))?->getField($g ? 'female' : 'male', true)) + $n = sprintf($title, $n); + + $opts = array( + 'name' => $n, + 'tooltip' => $profile->renderTooltip(), + 'icon' => '$$WH.g_getProfileIcon('.$r.', '.$c.', '.$g.', '.$l.', \''.$profile->getIcon().'\')' + ); + } + + if ($this->subjectName) + $id = implode('.', [$this->region, Profiler::urlize($this->realm, true), urlencode($this->subjectName)]); + else + $id = $this->typeId; + + $this->result = new Tooltip(self::POWER_TEMPLATE, $id, $opts ?? []); + } +} + +?> diff --git a/endpoints/profile/public.php b/endpoints/profile/public.php new file mode 100644 index 00000000..71cb65bb --- /dev/null +++ b/endpoints/profile/public.php @@ -0,0 +1,64 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']], + // 'bookmarked' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] // something with signatures? (must have bookmarked profile to create signature from) + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] // user page this is may be executed from + return: + null + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('ProfilePublicResponse - profileId empty', E_USER_ERROR); + return; + } + + if ($this->_get['user'] && User::$username != $this->_get['user'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + { + trigger_error('ProfilePublicResponse - user #'.User::$id.' tried to mark profiles of "'.$this->_get['user'].'" as public.', E_USER_ERROR); + return; + } + + $uid = 0; + if (!$this->_get['user'] || User::$username == $this->_get['user']) + $uid = User::$id; + else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfilePublicResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` | ?d WHERE `profileId` IN (?a) AND `accountId` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `user` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + } +} + +?> diff --git a/endpoints/profile/purge.php b/endpoints/profile/purge.php new file mode 100644 index 00000000..05d63a64 --- /dev/null +++ b/endpoints/profile/purge.php @@ -0,0 +1,22 @@ + + data: [string, tabName] + return + null + */ + protected function generate() : void { } // removes completion data (as uploaded by the wowhead client) Just fail silently if someone triggers this manually +} + +?> diff --git a/endpoints/profile/resync.php b/endpoints/profile/resync.php new file mode 100644 index 00000000..afeb0d4c --- /dev/null +++ b/endpoints/profile/resync.php @@ -0,0 +1,42 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return: + 1 + */ + protected function generate() : void + { + if ($chars = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_profiles WHERE `id` IN (?a)', $this->_get['id'])) + { + foreach ($chars as $c) + Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']); + } + else + trigger_error('ProfileResyncResponse - profiles '.implode(', ', $this->_get['id']).' not found in db', E_USER_ERROR); + + $this->result = 1; + } +} + +?> diff --git a/endpoints/profile/save.php b/endpoints/profile/save.php new file mode 100644 index 00000000..b063cd39 --- /dev/null +++ b/endpoints/profile/save.php @@ -0,0 +1,197 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']], + ); + protected array $expectedPOST = array( + 'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'level' => ['filter' => FILTER_VALIDATE_INT ], + 'class' => ['filter' => FILTER_VALIDATE_INT ], + 'race' => ['filter' => FILTER_VALIDATE_INT ], + 'gender' => ['filter' => FILTER_VALIDATE_INT ], + 'nomodel' => ['filter' => FILTER_VALIDATE_INT ], + 'talenttree1' => ['filter' => FILTER_VALIDATE_INT ], + 'talenttree2' => ['filter' => FILTER_VALIDATE_INT ], + 'talenttree3' => ['filter' => FILTER_VALIDATE_INT ], + 'activespec' => ['filter' => FILTER_VALIDATE_INT ], + 'talentbuild1' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTalentString']], + 'glyphs1' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkGlyphString'] ], + 'talentbuild2' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTalentString']], + 'glyphs2' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkGlyphString'] ], + 'icon' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'description' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'source' => ['filter' => FILTER_VALIDATE_INT ], + 'copy' => ['filter' => FILTER_VALIDATE_INT ], + 'public' => ['filter' => FILTER_VALIDATE_INT ], + 'gearscore' => ['filter' => FILTER_VALIDATE_INT ], + 'inv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'], 'flags' => FILTER_REQUIRE_ARRAY] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params (get)) + id: [0: new profile] + params (post) + [see below] + return: + proileId [onSuccess] + -1 [onError] + */ + protected function generate() : void + { + $cuProfile = array( + 'user' => User::$id, + // 'userName' => User::$username, + 'name' => $this->_post['name'], + 'level' => $this->_post['level'], + 'class' => $this->_post['class'], + 'race' => $this->_post['race'], + 'gender' => $this->_post['gender'], + 'nomodelMask' => $this->_post['nomodel'], + 'talenttree1' => $this->_post['talenttree1'], + 'talenttree2' => $this->_post['talenttree2'], + 'talenttree3' => $this->_post['talenttree3'], + 'talentbuild1' => $this->_post['talentbuild1'], + 'talentbuild2' => $this->_post['talentbuild2'], + 'activespec' => $this->_post['activespec'], + 'glyphs1' => $this->_post['glyphs1'], + 'glyphs2' => $this->_post['glyphs2'], + 'gearscore' => $this->_post['gearscore'], + 'icon' => $this->_post['icon'], + 'cuFlags' => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0) + ); + + // remnant of a conflict between wotlk generic icons and cata+ auto-generated, char-based icons (see profile=avatar) + if (strstr($cuProfile['icon'], 'profile=avatar')) + $cuProfile['icon'] = ''; + + if ($_ = $this->_post['description']) + $cuProfile['description'] = $_; + + if ($_ = $this->_post['source']) // should i also set sourcename? + $cuProfile['sourceId'] = $_; + + if ($_ = $this->_post['copy']) // gets set to source profileId when "save as" is clicked. Whats the difference to 'source' though? + { + // get character origin info if possible + if ($r = DB::Aowow()->selectCell('SELECT `realm` FROM ?_profiler_profiles WHERE `id` = ?d AND `realm` IS NOT NULL', $_)) + $cuProfile['realm'] = $r; + + $cuProfile['sourceId'] = $_; + } + + if (!empty($cuProfile['sourceId'])) + $cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT `name` FROM ?_profiler_profiles WHERE `id` = ?d', $cuProfile['sourceId']); + + $charId = -1; + if ($id = $this->_get['id'][0]) // update + { + if ($charId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `id` = ?d', $id)) + DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE `id` = ?d', $cuProfile, $id); + } + else // new + { + $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE `user` = ?d AND (`cuFlags` & ?d) = 0 AND `realmGUID` IS NULL', User::$id, PROFILER_CU_DELETED); + if ($nProfiles < 10 || User::isPremium()) + if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile))) + $charId = $newId; + } + + // update items + if ($charId != -1) + { + // ok, 'funny' thing: whether an item has en extra prismatic socket is determined contextually + // either the socket is -1 or it has an itemId on an index where there shouldn't be one + $keys = ['id', 'slot', 'item', 'subitem', 'permEnchant', 'tempEnchant', 'gem1', 'gem2', 'gem3', 'gem4']; + + // validate Enchantments + $enchIds = array_merge( + array_column($this->_post['inv'], 3), // perm enchantments + array_column($this->_post['inv'], 4) // temp enchantments (not used..?) + ); + $enchs = new EnchantmentList(array(['id', $enchIds])); + + // validate items + $itemIds = array_merge( + array_column($this->_post['inv'], 1), // base item + array_column($this->_post['inv'], 5), // gem slot 1 + array_column($this->_post['inv'], 6), // gem slot 2 + array_column($this->_post['inv'], 7), // gem slot 3 + array_column($this->_post['inv'], 8) // gem slot 4 + ); + + $items = new ItemList(array(['id', $itemIds])); + if (!$items->error) + { + foreach ($this->_post['inv'] as $slot => $itemData) + { + if ($slot + 1 == array_sum($itemData)) // only slot definition set => empty slot + { + DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE `id` = ?d AND `slot` = ?d', $charId, $itemData[0]); + continue; + } + + // item does not exist + if (!$items->getEntry($itemData[1])) + continue; + + // sub-item check + if (!$items->getRandEnchantForItem($itemData[1])) + $itemData[2] = 0; + + // item sockets are fubar + $nSockets = $items->json[$itemData[1]]['nsockets'] ?? 0; + $nSockets += in_array($slot, [SLOT_WAIST, SLOT_WRISTS, SLOT_HANDS]) ? 1 : 0; + for ($i = 5; $i < 9; $i++) + if ($itemData[$i] > 0 && (!$items->getEntry($itemData[$i]) || $i >= (5 + $nSockets))) + $itemData[$i] = 0; + + // item enchantments are borked + if ($itemData[3] && !$enchs->getEntry($itemData[3])) + $itemData[3] = 0; + + if ($itemData[4] && !$enchs->getEntry($itemData[4])) + $itemData[4] = 0; + + // looks good + array_unshift($itemData, $charId); + DB::Aowow()->query('REPLACE INTO ?_profiler_items (?#) VALUES (?a)', $keys, $itemData); + } + } + } + + $this->result = $charId; + } + + protected static function checkTalentString(string $val) : string + { + if (preg_match('/^\d+$/', $val)) + return $val; + + return ''; + } + + protected static function checkGlyphString(string $val) : string + { + if (preg_match('/^\d+(:\d+)*$/', $val)) + return $val; + + return ''; + } +} + +?> diff --git a/endpoints/profile/status.php b/endpoints/profile/status.php new file mode 100644 index 00000000..15d0d781 --- /dev/null +++ b/endpoints/profile/status.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'guild' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']], + 'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return + + */ + protected function generate() : void + { + // roster resync for this guild was requested -> get char list + if ($this->_get['guild']) + $ids = DB::Aowow()->selectCol('SELECT `id` FROM ?_profiler_profiles WHERE `guild` IN (?a)', $this->_get['id']); + else if ($this->_get['arena-team']) + $ids = DB::Aowow()->selectCol('SELECT `profileId` FROM ?_profiler_arena_team_member WHERE `arenaTeamId` IN (?a)', $this->_get['id']); + else + $ids = $this->_get['id']; + + if (!$ids) + { + trigger_error('ProfileStatusResponse - no profileIds to resync'.($this->_get['guild'] ? ' for guild #'.$this->_get['guild'] : ($this->_get['arena-team'] ? ' for areana team #'.$this->_get['arena-team'] : '')), E_USER_ERROR); + $this->result = Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]); + } + + $this->result = Profiler::resyncStatus(Type::PROFILE, $ids); + } +} + +?> diff --git a/endpoints/profile/summary.php b/endpoints/profile/summary.php new file mode 100644 index 00000000..26eddd1d --- /dev/null +++ b/endpoints/profile/summary.php @@ -0,0 +1,17 @@ + diff --git a/endpoints/profile/unlink.php b/endpoints/profile/unlink.php new file mode 100644 index 00000000..8904db6e --- /dev/null +++ b/endpoints/profile/unlink.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] // user page this is may be executed from + return: + null + */ + protected function generate() : void // links char with account + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileUnlinkResponse - profileId empty', E_USER_ERROR); + return; + } + + if ($this->_get['user'] && User::$username != $this->_get['user'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + { + trigger_error('ProfileUnlinkResponse - user #'.User::$id.' tried to unlink profiles from "'.$this->_get['user'], E_USER_ERROR); + return; + } + + $uid = 0; + if (!$this->_get['user'] || User::$username == $this->_get['user']) + $uid = User::$id; + else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfileUnlinkResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d AND `profileId` IN (?a)', $uid, $this->_get['id']); + } +} + +?> diff --git a/endpoints/profile/unpin.php b/endpoints/profile/unpin.php new file mode 100644 index 00000000..7139f15a --- /dev/null +++ b/endpoints/profile/unpin.php @@ -0,0 +1,55 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] + return: null + */ + protected function generate() : void // (un)favorite + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileUnpinResponse - profileId empty', E_USER_ERROR); + return; + } + + $uid = 0; + if (!$this->_get['user'] || User::$username == $this->_get['user']) + $uid = User::$id; + else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfileUnpinResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ~?d WHERE `accountId` = ?d', PROFILER_CU_PINNED, $uid); + } +} + +?> diff --git a/endpoints/profiler/profiler.php b/endpoints/profiler/profiler.php new file mode 100644 index 00000000..eb6f79e0 --- /dev/null +++ b/endpoints/profiler/profiler.php @@ -0,0 +1,52 @@ +generateError(); + } + + protected function generate() : void + { + // just so the form does not break. There won't be any results. + $usedRegions = array_column(Profiler::getRealms(), 'region') ?: ['us']; + foreach (Util::$regions as $idx => $id) + if (in_array($id, $usedRegions)) + $this->regions[$id] = [Lang::profiler('regions', $id), $idx + 1]; + + if (!in_array($this->rg, $usedRegions)) + $this->rg = key($this->regions); + + array_unshift($this->title, Util::ucFirst(Lang::profiler('profiler'))); + + parent::generate(); + } +} + +?> diff --git a/endpoints/profiles/profiles.php b/endpoints/profiles/profiles.php new file mode 100644 index 00000000..e3d4dd78 --- /dev/null +++ b/endpoints/profiles/profiles.php @@ -0,0 +1,220 @@ + Profiler > Characters + + protected array $dataLoader = ['weight-presets', 'realms']; + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'], + [SC_CSS_FILE, 'css/Profiler.css'] + ); + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]], + // 1 guild; 2,3,4 arenateam (4 => 5-man): puts a resync button on the lv (was probably used before arenateams and guilds had a dedicated page) + 'roster' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 4]] + ); + + public int $type = Type::PROFILE; + public string $roster = ''; + + private int $sumSubjects = 0; + + public function __construct(string $pageParam) + { + $this->getSubjectFromUrl($pageParam); + + parent::__construct($pageParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + $realms = []; + foreach (Profiler::getRealms() as $idx => $r) + { + if ($this->region && $r['region'] != $this->region) + continue; + + if ($this->realm && $r['name'] != $this->realm) + continue; + + $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT COUNT(*) FROM characters WHERE `deleteInfos_Name` IS NULL AND `level` <= ?d AND (`extra_flags` & ?) = 0', MAX_LEVEL, Profiler::CHAR_GMFLAGS); + $realms[] = $idx; + } + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new ProfileListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('profiles')); + + $this->filter->evalCriteria(); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->followBreadcrumbPath(); + + + /**************/ + /* Page Title */ + /**************/ + + if ($this->realm) + array_unshift($this->title, $this->realm,/* Cfg::get('BATTLEGROUP'),*/ Lang::profiler('regions', $this->region), Lang::game('profiles')); + else if ($this->region) + array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('profiles')); + else + array_unshift($this->title, Lang::game('profiles')); + + + /****************/ + /* Main Content */ + /****************/ + + $conditions = []; + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + $fiExtraCols = $this->filter->fiExtraCols; + + $lvData = []; + $lvExtraCols = []; + $lvVisibleCols = ['race', 'classs', 'level', 'talents', 'achievementpoints', 'gearscore']; + $lvHiddenCols = []; + $lvNote = ''; + $lv_truncated = 0; + + $this->getRegions(); + + foreach ($fiExtraCols as $skill => $idx) + $lvExtraCols[] = "\$Listview.funcBox.createSimpleCol('skill-' + ".$skill.", g_spell_skills[".$skill."], '7%', 'skill-' + ".$skill.")"; + + if (!$this->filter->useLocalList) + { + $conditions[] = ['deleteInfos_Name', null]; + $conditions[] = ['level', MAX_LEVEL, '<=']; // prevents JS errors + $conditions[] = [['extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0]; + } + + $miscParams = ['calcTotal' => true]; + if ($this->realm) + $miscParams['sv'] = $this->realm; + if ($this->region) + $miscParams['rg'] = $this->region; + if ($_ = $this->filter->extraOpts) + $miscParams['extraOpts'] = $_; + + if ($this->filter->useLocalList) + $profiles = new LocalProfileList($conditions, $miscParams); + else + $profiles = new RemoteProfileList($conditions, $miscParams); + + if (!$profiles->error) + { + // init these chars on our side and get local ids + if (!$this->filter->useLocalList) + $profiles->initializeLocalEntries(); + + // Roster only if single realm selected + $roster = $this->realmId ? $this->_get['roster'] : 0; + if (!$roster && $this->realmId) + if (count($r = $this->filter->getSetCriteria(9, 12, 15, 18)) == 1) + $roster = ($r[0] - 6) / 3; // 1, 2, 3, or 4 + + $addInfoMask = PROFILEINFO_CHARACTER; + + // team rating filters + if ($this->filter->getSetCriteria(13, 16, 19)) + { + $lvVisibleCols[] = 'rating'; + $addInfoMask |= PROFILEINFO_ARENA; + } + + // init roster-listview + if ($roster == 1 && !$profiles->hasDiffFields('guild') && $profiles->getField('guild')) + { + $lvVisibleCols[] = 'guildrank'; + $lvHiddenCols[] = 'guild'; + + $this->roster = Lang::profiler('guildRoster', [$profiles->getField('guildname')]); + } + else if ($roster && !$profiles->hasDiffFields('arenateam') && $profiles->getField('arenateam')) + { + $lvVisibleCols[] = 'rating'; + + $addInfoMask |= PROFILEINFO_ARENA; + $this->roster = Lang::profiler('arenaRoster', [$profiles->getField('arenateam')]); + } + + $lvData = $profiles->getListviewData($addInfoMask, $fiExtraCols); + + if ($this->filter->getSetCriteria(10) && !in_array('guildrank', $lvHiddenCols)) + $lvVisibleCols[] = 'guildrank'; + + // create note if search limit was exceeded + if ($this->filter->query && $profiles->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $lvNote = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound2', $this->sumSubjects, $profiles->getMatches()); + $lv_truncated = 1; + } + else if ($profiles->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + $lvNote = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound', $this->sumSubjects, 0); + + if ($this->filter->useLocalList) + { + if (!empty($lvNote)) + $lvNote .= ' + "
'.Lang::profiler('complexFilter').'"'; + else + $lvNote = ''.Lang::profiler('complexFilter').''; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); + + $this->lvTabs->addListviewTab(new Listview(array( + 'id' => 'characters', + 'data' => $lvData, + 'hideCount' => 1, + 'onBeforeCreate' => '$pr_initRosterListview', // puts a resync button on the lv + 'extraCols' => $lvExtraCols ?: null, + 'visibleCols' => $lvVisibleCols, + 'hiddenCols' => $lvHiddenCols ?: null, + 'note' => $lvNote ?: null, + '_truncated' => $lv_truncated ?: null + ), ProfileList::$brickFile)); + + parent::generate(); + + $this->result->registerDisplayHook('filter', [self::class, 'filterFormHook']); + } + + public static function filterFormHook(Template\PageTemplate &$pt, ProfileListFilter $filter) : void + { + // sort for dropdown-menus + Lang::sort('game', 'ra'); + Lang::sort('game', 'cl'); + } +} + +?> diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php deleted file mode 100644 index 86e668a9..00000000 --- a/includes/ajaxHandler/profile.class.php +++ /dev/null @@ -1,780 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ], - 'items' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkItemList'], - 'size' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], - 'guild' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'], - 'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkUser' ] - ); - - protected $_post = array( - 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], - 'level' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'class' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'race' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'gender' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'nomodel' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'talenttree1' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'talenttree2' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'talenttree3' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'activespec' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'talentbuild1' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkTalentString'], - 'glyphs1' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkGlyphString' ], - 'talentbuild2' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkTalentString'], - 'glyphs2' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkGlyphString' ], - 'icon' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ], - 'description' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'source' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'copy' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'public' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'gearscore' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'inv' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY], - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params) - return; - - if (!Cfg::get('PROFILER_ENABLE')) - return; - - switch ($this->params[0]) - { - case 'unlink': - $this->undo = true; - case 'link': - $this->handler = 'handleLink'; // always returns null - break; - case 'unpin': - $this->undo = true; - case 'pin': - $this->handler = 'handlePin'; // always returns null - break; - case 'private': - $this->undo = true; - case 'public': - $this->handler = 'handlePrivacy'; // always returns null - break; - case 'avatar': - $this->handler = 'handleAvatar'; // sets an image header - break; // so it has to die here or another header will be set - case 'resync': - $this->handler = 'handleResync'; // always returns "1" - break; - case 'status': - $this->handler = 'handleStatus'; // returns status object - break; - case 'save': - $this->handler = 'handleSave'; - break; - case 'delete': - $this->handler = 'handleDelete'; - break; - case 'purge': - $this->handler = 'handlePurge'; - break; - case 'summary': // page is generated by jScript - die(); // just be empty - case 'load': - $this->handler = 'handleLoad'; - break; - } - } - - /* params - id: - user: [optional] - return: null - */ - protected function handleLink() : void // links char with account - { - if (!User::isLoggedIn() || empty($this->_get['id'])) - { - trigger_error('AjaxProfile::handleLink - profileId empty or user not logged in', E_USER_ERROR); - return; - } - - $uid = User::$id; - if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - { - if (!($uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))) - { - trigger_error('AjaxProfile::handleLink - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); - return; - } - } - - if ($this->undo) - DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d AND `profileId` IN (?a)', $uid, $this->_get['id']); - else - { - foreach ($this->_get['id'] as $prId) // only link characters, not custom profiles - { - if ($prId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `id` = ?d AND `realm` IS NOT NULL', $prId)) - DB::Aowow()->query('INSERT IGNORE INTO ?_account_profiles VALUES (?d, ?d, 0)', $uid, $prId); - else - { - trigger_error('AjaxProfile::handleLink - profile #'.$prId.' is custom or does not exist', E_USER_ERROR); - return; - } - } - } - } - - /* params - id: - user: [optional] - return: null - */ - protected function handlePin() : void // (un)favorite - { - if (!User::isLoggedIn() || empty($this->_get['id'][0])) - { - trigger_error('AjaxProfile::handlePin - profileId empty or user not logged in', E_USER_ERROR); - return; - } - - $uid = User::$id; - if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - { - if (!($uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))) - { - trigger_error('AjaxProfile::handlePin - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); - return; - } - } - - // since only one character can be pinned at a time we can reset everything - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ?d WHERE `accountId` = ?d', ~PROFILER_CU_PINNED, $uid); - // and set a single char if necessary - if (!$this->undo) - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` | ?d WHERE `profileId` = ?d AND `accountId` = ?d', PROFILER_CU_PINNED, $this->_get['id'][0], $uid); - } - - /* params - id: - user: [optional] - return: null - */ - protected function handlePrivacy() : void // public visibility - { - if (!User::isLoggedIn() || empty($this->_get['id'][0])) - { - trigger_error('AjaxProfile::handlePrivacy - profileId empty or user not logged in', E_USER_ERROR); - return; - } - - $uid = User::$id; - if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - { - if (!($uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))) - { - trigger_error('AjaxProfile::handlePrivacy - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); - return; - } - } - - if ($this->undo) - { - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ?d WHERE `profileId` IN (?a) AND `accountId` = ?d', ~PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); - DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` & ?d WHERE `id` IN (?a) AND `user` = ?d', ~PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); - } - else - { - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` | ?d WHERE `profileId` IN (?a) AND `accountId` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); - DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `user` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); - } - } - - /* params - id: - size: [optional] - return: image-header - */ - protected function handleAvatar() : void // image - { - // something happened in the last years: those textures do not include tiny icons - $sizes = [/* 'tiny' => 15, */'small' => 18, 'medium' => 36, 'large' => 56]; - $aPath = 'uploads/avatars/%d.jpg'; - $s = $this->_get['size'] ?: 'medium'; - - if (!$this->_get['id'] || !preg_match('/^([0-9]+)\.(jpg|gif)$/', $this->_get['id'][0], $matches) || !in_array($s, array_keys($sizes))) - { - trigger_error('AjaxProfile::handleAvatar - malformed request received', E_USER_ERROR); - return; - } - - $this->contentType = $matches[2] == 'png' ? MIME_TYPE_PNG : MIME_TYPE_JPEG; - - $id = $matches[1]; - $dest = imageCreateTruecolor($sizes[$s], $sizes[$s]); - - if (file_exists(sprintf($aPath, $id))) - { - $offsetX = $offsetY = 0; - - switch ($s) - { - case 'tiny': - $offsetX += $sizes['small']; - case 'small': - $offsetY += $sizes['medium']; - case 'medium': - $offsetX += $sizes['large']; - } - - $src = imageCreateFromJpeg(printf($aPath, $id)); - imagecopymerge($dest, $src, 0, 0, $offsetX, $offsetY, $sizes[$s], $sizes[$s], 100); - } - else - trigger_error('AjaxProfile::handleAvatar - avatar file #'.$id.' not found', E_USER_ERROR); - - if ($matches[2] == 'gif') - imageGif($dest); - else - imageJpeg($dest); - } - - /* params - id: - user: [optional, not used] - return: 1 - */ - protected function handleResync() : string - { - if ($chars = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_profiles WHERE id IN (?a)', $this->_get['id'])) - { - foreach ($chars as $c) - Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']); - } - else - trigger_error('AjaxProfile::handleResync - profiles '.implode(', ', $this->_get['id']).' not found in db', E_USER_ERROR); - - return '1'; - } - - /* params - id: - return - - [ - nQueueProcesses, - [statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries], - [] - ... - ] - - not all fields are required, if zero they are omitted - statusCode: - 0: end the request - 1: waiting - 2: working... - 3: ready; click to view - 4: error / retry - errorCode: - 0: unk error - 1: char does not exist - 2: armory gone - */ - protected function handleStatus() : string - { - // roster resync for this guild was requested -> get char list - if ($this->_get['guild']) - $ids = DB::Aowow()->selectCol('SELECT id FROM ?_profiler_profiles WHERE guild IN (?a)', $this->_get['id']); - else if ($this->_get['arena-team']) - $ids = DB::Aowow()->selectCol('SELECT profileId FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->_get['id']); - else - $ids = $this->_get['id']; - - if (!$ids) - { - trigger_error('AjaxProfile::handleStatus - no profileIds to resync'.($this->_get['guild'] ? ' for guild #'.$this->_get['guild'] : ($this->_get['arena-team'] ? ' for areana team #'.$this->_get['arena-team'] : '')), E_USER_ERROR); - return Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]); - } - - return Profiler::resyncStatus(Type::PROFILE, $ids); - } - - /* params (get)) - id: [0: new profile] - params (post) - [see below] - return: - proileId [onSuccess] - -1 [onError] - */ - protected function handleSave() : string // unKill a profile - { - // todo (med): detail check this post-data - $cuProfile = array( - 'user' => User::$id, - // 'userName' => User::$username, - 'name' => $this->_post['name'], - 'level' => $this->_post['level'], - 'class' => $this->_post['class'], - 'race' => $this->_post['race'], - 'gender' => $this->_post['gender'], - 'nomodelMask' => $this->_post['nomodel'], - 'talenttree1' => $this->_post['talenttree1'], - 'talenttree2' => $this->_post['talenttree2'], - 'talenttree3' => $this->_post['talenttree3'], - 'talentbuild1' => $this->_post['talentbuild1'], - 'talentbuild2' => $this->_post['talentbuild2'], - 'activespec' => $this->_post['activespec'], - 'glyphs1' => $this->_post['glyphs1'], - 'glyphs2' => $this->_post['glyphs2'], - 'gearscore' => $this->_post['gearscore'], - 'icon' => $this->_post['icon'], - 'cuFlags' => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0) - ); - - if (strstr($cuProfile['icon'], 'profile=avatar')) // how the profiler is supposed to handle icons is beyond me - $cuProfile['icon'] = ''; - - if ($_ = $this->_post['description']) - $cuProfile['description'] = $_; - - if ($_ = $this->_post['source']) // should i also set sourcename? - $cuProfile['sourceId'] = $_; - - if ($_ = $this->_post['copy']) // gets set to source profileId when "save as" is clicked. Whats the difference to 'source' though? - { - // get character origin info if possible - if ($r = DB::Aowow()->selectCell('SELECT realm FROM ?_profiler_profiles WHERE id = ?d AND realm IS NOT NULL', $_)) - $cuProfile['realm'] = $r; - - $cuProfile['sourceId'] = $_; - } - - if (!empty($cuProfile['sourceId'])) - $cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT name FROM ?_profiler_profiles WHERE id = ?d', $cuProfile['sourceId']); - - $charId = -1; - if ($id = $this->_get['id'][0]) // update - { - if ($charId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE id = ?d', $id)) - DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE id = ?d', $cuProfile, $id); - } - else // new - { - $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE user = ?d AND (cuFlags & ?d) = 0 AND realmGUID IS NULL', User::$id, PROFILER_CU_DELETED); - if ($nProfiles < 10 || User::isPremium()) - if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile))) - $charId = $newId; - } - - // update items - if ($charId != -1) - { - // ok, 'funny' thing: wether an item has en extra prismatic sockel is determined contextual - // either the socket is -1 or it has an itemId in a socket where there shouldn't be one - $keys = ['id', 'slot', 'item', 'subitem', 'permEnchant', 'tempEnchant', 'gem1', 'gem2', 'gem3', 'gem4']; - - // validate Enchantments - $enchIds = array_merge( - array_column($this->_post['inv'], 3), // perm enchantments - array_column($this->_post['inv'], 4) // temp enchantments (not used..?) - ); - $enchs = new EnchantmentList(array(['id', $enchIds])); - - // validate items - $itemIds = array_merge( - array_column($this->_post['inv'], 1), // base item - array_column($this->_post['inv'], 5), // gem slot 1 - array_column($this->_post['inv'], 6), // gem slot 2 - array_column($this->_post['inv'], 7), // gem slot 3 - array_column($this->_post['inv'], 8) // gem slot 4 - ); - - $items = new ItemList(array(['id', $itemIds])); - if (!$items->error) - { - foreach ($this->_post['inv'] as $slot => $itemData) - { - if ($slot + 1 == array_sum($itemData)) // only slot definition set => empty slot - { - DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE id = ?d AND slot = ?d', $charId, $itemData[0]); - continue; - } - - // item does not exist - if (!$items->getEntry($itemData[1])) - continue; - - // sub-item check - if (!$items->getRandEnchantForItem($itemData[1])) - $itemData[2] = 0; - - // item sockets are fubar - $nSockets = $items->json[$itemData[1]]['nsockets'] ?? 0; - $nSockets += in_array($slot, [SLOT_WAIST, SLOT_WRISTS, SLOT_HANDS]) ? 1 : 0; - for ($i = 5; $i < 9; $i++) - if ($itemData[$i] > 0 && (!$items->getEntry($itemData[$i]) || $i >= (5 + $nSockets))) - $itemData[$i] = 0; - - // item enchantments are borked - if ($itemData[3] && !$enchs->getEntry($itemData[3])) - $itemData[3] = 0; - - if ($itemData[4] && !$enchs->getEntry($itemData[4])) - $itemData[4] = 0; - - // looks good - array_unshift($itemData, $charId); - DB::Aowow()->query('REPLACE INTO ?_profiler_items (?#) VALUES (?a)', $keys, $itemData); - } - } - } - - return (string)$charId; - } - - /* params - id: - return - null - */ - protected function handleDelete() : void // kill a profile - { - if (!User::isLoggedIn() || !$this->_get['id']) - { - trigger_error('AjaxProfile::handleDelete - profileId empty or user not logged in', E_USER_ERROR); - return; - } - - // only flag as deleted; only custom profiles - DB::Aowow()->query( - 'UPDATE ?_profiler_profiles SET cuFlags = cuFlags | ?d WHERE id IN (?a) AND cuFlags & ?d {AND user = ?d}', - PROFILER_CU_DELETED, - $this->_get['id'], - PROFILER_CU_PROFILE, - User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? DBSIMPLE_SKIP : User::$id - ); - } - - /* params - id: profileId - items: string [itemIds.join(':')] - unnamed: unixtime [only to force the browser to reload instead of cache] - return - lots... - */ - protected function handleLoad() : string - { - // titles, achievements, characterData, talents, pets - // and some onLoad-hook to .. load it registerProfile($data) - // everything else goes through data.php .. strangely enough - - if (!$this->_get['id']) - { - trigger_error('AjaxProfile::handleLoad - profileId empty', E_USER_ERROR); - return ''; - } - - $pBase = DB::Aowow()->selectRow('SELECT pg.name AS guildname, p.* FROM ?_profiler_profiles p LEFT JOIN ?_profiler_guild pg ON pg.id = p.guild WHERE p.id = ?d', $this->_get['id'][0]); - if (!$pBase) - { - trigger_error('Profiler::handleLoad - called with invalid profileId #'.$this->_get['id'][0], E_USER_WARNING); - return ''; - } - - if (($pBase['cuFlags'] & PROFILER_CU_DELETED) && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - return ''; - - - $rData = []; - foreach (Profiler::getRealms() as $rId => $rData) - if ($rId == $pBase['realm']) - break; - - if (!$rData) // realm doesn't exist or access is restricted - return ''; - - $profile = array( - 'id' => $pBase['id'], - 'source' => $pBase['id'], - 'level' => $pBase['level'], - 'classs' => $pBase['class'], - 'race' => $pBase['race'], - 'faction' => ChrRace::tryFrom($pBase['race'])?->getTeam() ?? TEAM_NEUTRAL, - 'gender' => $pBase['gender'], - 'skincolor' => $pBase['skincolor'], - 'hairstyle' => $pBase['hairstyle'], - 'haircolor' => $pBase['haircolor'], - 'facetype' => $pBase['facetype'], - 'features' => $pBase['features'], - 'title' => $pBase['title'], - 'name' => $pBase['name'], - 'guild' => "$'".$pBase['guildname']."'", - 'published' => !!($pBase['cuFlags'] & PROFILER_CU_PUBLISHED), - 'pinned' => !!($pBase['cuFlags'] & PROFILER_CU_PINNED), - 'nomodel' => $pBase['nomodelMask'], - 'playedtime' => $pBase['playedtime'], - 'lastupdated' => $pBase['lastupdated'] * 1000, - 'talents' => array( - 'builds' => array( // notice the bullshit to prevent the talent-string from becoming a float! NOTICE IT!! - ['talents' => '$"'.$pBase['talentbuild1'].'"', 'glyphs' => $pBase['glyphs1']], - ['talents' => '$"'.$pBase['talentbuild2'].'"', 'glyphs' => $pBase['glyphs2']] - ), - 'active' => $pBase['activespec'] - ), - // set later - 'inventory' => [], - 'bookmarks' => [], // list of userIds who claimed this profile (claiming and owning are two different things) - - // completion lists: [subjectId => amount/timestamp/1] - 'skills' => [], // skillId => [curVal, maxVal] - 'reputation' => [], // factionId => curVal - 'titles' => [], // titleId => 1 - 'spells' => [], // spellId => 1; recipes, vanity pets, mounts - 'achievements' => [], // achievementId => timestamp - 'quests' => [], // questId => 1 - 'achievementpoints' => 0, // max you have - 'statistics' => [], // all raid activity [achievementId => killCount] - 'activity' => [], // recent raid activity [achievementId => 1] (is a subset of statistics) - ); - - if ($pBase['cuFlags'] & PROFILER_CU_PROFILE) - { - // this parameter is _really_ strange .. probably still not doing this right - $profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0; - - $profile['sourcename'] = $pBase['sourceName']; - $profile['description'] = $pBase['description']; - $profile['user'] = $pBase['user']; - $profile['username'] = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE `id` = ?d', $pBase['user']); - } - - // custom profiles inherit this when copied from real char :( - if ($pBase['realm']) - { - $profile['region'] = [$rData['region'], Lang::profiler('regions', $rData['region'])]; - $profile['battlegroup'] = [Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP')]; - $profile['realm'] = [Profiler::urlize($rData['name'], true), $rData['name']]; - } - - // bookmarks - if ($_ = DB::Aowow()->selectCol('SELECT accountId FROM ?_account_profiles WHERE profileId = ?d', $pBase['id'])) - $profile['bookmarks'] = $_; - - // arena teams - [size(2|3|5) => name]; name gets urlized to use as link - if ($at = DB::Aowow()->selectCol('SELECT type AS ARRAY_KEY, name FROM ?_profiler_arena_team at JOIN ?_profiler_arena_team_member atm ON atm.arenaTeamId = at.id WHERE atm.profileId = ?d', $pBase['id'])) - $profile['arenateams'] = $at; - - // pets if hunter fields: [name:name, family:petFamily, npc:npcId, displayId:modelId, talents:talentString] - if ($pets = DB::Aowow()->select('SELECT `name`, `family`, `npc`, `displayId`, CONCAT("$\"", `talents`, "\"") AS "talents" FROM ?_profiler_pets WHERE `owner` = ?d', $pBase['id'])) - $profile['pets'] = $pets; - - // source for custom profiles; profileId => [name, ownerId, iconString(optional)] - if ($customs = DB::Aowow()->select('SELECT id AS ARRAY_KEY, name, user, icon FROM ?_profiler_profiles WHERE sourceId = ?d AND sourceId <> id {AND (cuFlags & ?d) = 0}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : PROFILER_CU_DELETED)) - { - foreach ($customs as $id => $cu) - { - if (!$cu['icon']) - unset($cu['icon']); - - $profile['customs'][$id] = array_values($cu); - } - } - - - /* $profile[] - // CUSTOM - 'auras' => [], // custom list of buffs, debuffs [spellId] - - // UNUSED - 'glyphs' => [], // provided list of already known glyphs (post cataclysm feature) - */ - - - // questId => [cat1, cat2] - $profile['quests'] = []; - if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ?_profiler_completion_quests WHERE `id` = ?d', $pBase['id'])) - { - $qList = new QuestList(array(['id', $quests], Cfg::get('SQL_LIMIT_NONE'))); - if (!$qList->error) - foreach ($qList->iterate() as $id => $__) - $profile['quests'][$id] = [$qList->getField('cat1'), $qList->getField('cat2')]; - } - - // skillId => [value, max] - $profile['skills'] = DB::Aowow()->select('SELECT `skillId` AS ARRAY_KEY, `value` AS "0", `max` AS "1" FROM ?_profiler_completion_skills WHERE `id` = ?d', $pBase['id']); - - // factionId => amount - $profile['reputation'] = DB::Aowow()->selectCol('SELECT `factionId` AS ARRAY_KEY, `standing` FROM ?_profiler_completion_reputation WHERE `id` = ?d', $pBase['id']); - - // titleId => 1 - $profile['titles'] = DB::Aowow()->selectCol('SELECT `titleId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_titles WHERE `id` = ?d', $pBase['id']); - - // achievementId => js date object - $profile['achievements'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, CONCAT("$new Date(", `date` * 1000, ")") FROM ?_profiler_completion_achievements WHERE `id` = ?d', $pBase['id']); - - // just points - $profile['achievementpoints'] = $profile['achievements'] ? DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a)', array_keys($profile['achievements'])) : 0; - - // achievementId => counter - $profile['statistics'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, `counter` FROM ?_profiler_completion_statistics WHERE `id` = ?d', $pBase['id']); - - // achievementId => 1 - $profile['activity'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_statistics WHERE `id` = ?d AND `date` > ?d', $pBase['id'], time() - MONTH); - - // spellId => 1 - $profile['spells'] = DB::Aowow()->selectCol('SELECT `spellId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_spells WHERE `id` = ?d', $pBase['id']); - - - $gItems = []; - - $usedSlots = []; - if ($this->_get['items']) - { - $phItems = new ItemList(array(['id', $this->_get['items']], ['slot', INVTYPE_NON_EQUIP, '!'])); - if (!$phItems->error) - { - $data = $phItems->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); - foreach ($phItems->iterate() as $iId => $__) - { - $sl = $phItems->getField('slot'); - foreach (Profiler::$slot2InvType as $slot => $invTypes) - { - if (in_array($sl, $invTypes) && !in_array($slot, $usedSlots)) - { - // get and apply inventory - $gItems[$iId] = array( - 'name_'.Lang::getLocale()->json() => $phItems->getField('name', true), - 'quality' => $phItems->getField('quality'), - 'icon' => $phItems->getField('iconString'), - 'jsonequip' => $data[$iId] - ); - $profile['inventory'][$slot] = [$iId, 0, 0, 0, 0, 0, 0, 0]; - - $usedSlots[] = $slot; - break; - } - } - } - } - } - - if ($items = DB::Aowow()->select('SELECT * FROM ?_profiler_items WHERE id = ?d', $pBase['id'])) - { - $itemz = new ItemList(array(['id', array_column($items, 'item')], Cfg::get('SQL_LIMIT_NONE'))); - if (!$itemz->error) - { - $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); - - foreach ($items as $i) - { - if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots)) - { - // get and apply inventory - $gItems[$i['item']] = array( - 'name_'.Lang::getLocale()->json() => $itemz->getField('name', true), - 'quality' => $itemz->getField('quality'), - 'icon' => $itemz->getField('iconString'), - 'jsonequip' => $data[$i['item']] - ); - $profile['inventory'][$i['slot']] = [$i['item'], $i['subItem'], $i['permEnchant'], $i['tempEnchant'], $i['gem1'], $i['gem2'], $i['gem3'], $i['gem4']]; - } - } - } - } - - $buff = ''; - foreach ($gItems as $id => $item) - $buff .= 'g_items.add('.$id.', '.Util::toJSON($item, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).");\n"; - - - // if ($au = $char->getField('auras')) - // { - // $auraz = new SpellList(array(['id', $char->getField('auras')], Cfg::get('SQL_LIMIT_NONE'))); - // $dataz = $auraz->getListviewData(); - // $modz = $auraz->getProfilerMods(); - - // // get and apply aura-mods - // foreach ($dataz as $id => $data) - // { - // $mods = []; - // if (!empty($modz[$id])) - // { - // foreach ($modz[$id] as $k => $v) - // { - // if (is_array($v)) - // $mods[] = $v; - // else if ($str = @Game::$itemMods[$k]) - // $mods[$str] = $v; - // } - // } - - // $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', callback:".Util::toJSON($mods)."});\n"; - // } - // $buff .= "\n"; - // } - - - // load available titles - Util::loadStaticFile('p-titles-'.$pBase['gender'], $buff, true); - - // add profile to buffer - $buff .= "\n\n\$WowheadProfiler.registerProfile(".Util::toJSON($profile).");"; - - return $buff."\n"; - } - - /* params - id: - data: [string, tabName] - return - null - */ - protected function handlePurge() : void { } // removes completion data (as uploaded by the wowhead client) Just fail silently if someone triggers this manually - - protected static function checkItemList($val) : array - { - // expecting item-list - if (preg_match('/\d+(:\d+)*/', $val)) - return array_map('intVal', explode(':', $val)); - - return []; - } - - protected static function checkUser(string $val) : string - { - if (User::isValidName($val)) - return $val; - - return ''; - } - - protected static function checkTalentString(string $val) : string - { - if (preg_match('/^\d+$/', $val)) - return $val; - - return ''; - } - - protected static function checkGlyphString(string $val) : string - { - if (preg_match('/^\d+(:\d+)*$/', $val)) - return $val; - - return ''; - } -} - -?> diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index 1923a192..b1360f23 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -326,7 +326,7 @@ class PageTemplate { $result[] = "pr_setRegionRealm(\$WH.ge('fi').firstChild, '".$this->region."', '".$this->realm."');"; - if ($this->filter->values['ra']) + if (!empty($this->filter->values['ra'])) $result[] = "pr_onChangeRace();"; } diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 7d0ee910..6d18ec7d 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -247,6 +247,14 @@ class Profiler return self::$realms; } + public static function getRegions() : array + { + self::getRealms(); + + // sort depends on encountered order in `logon`.`realmlist`. Is that a problem? + return array_unique(array_column(self::$realms, 'region')); + } + private static function queueInsert($realmId, $guid, $type, $localId) { if ($rData = DB::Aowow()->selectRow('SELECT `requestTime` AS "time", `status` FROM ?_profiler_sync WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d AND `typeId` = ?d AND `status` <> ?d', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING)) diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index 4a6fce4e..8aa712b1 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -253,7 +253,8 @@ class ProfileListFilter extends Filter 'minle' => [parent::V_RANGE, [1, MAX_LEVEL], false], // min level 'maxle' => [parent::V_RANGE, [1, MAX_LEVEL], false], // max level 'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region - 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false], // server + 'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template + 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false] // server ); public bool $useLocalList = false; diff --git a/pages/profile.php b/pages/profile.php deleted file mode 100644 index 38320159..00000000 --- a/pages/profile.php +++ /dev/null @@ -1,258 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain'], - 'new' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'] - ); - - private $isCustom = false; - private $profile = null; - private $subject = null; - private $rnItr = 0; - private $powerTpl = '$WowheadPower.registerProfile(%s, %d, %s);'; - - public function __construct($pageCall, $pageParam) - { - parent::__construct($pageCall, $pageParam); - - if (!Cfg::get('PROFILER_ENABLE')) - $this->error(); - - $params = array_map('urldecode', explode('.', $pageParam)); - if ($params[0]) - $params[0] = Profiler::urlize($params[0]); - if (isset($params[1])) - $params[1] = Profiler::urlize($params[1], true); - - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - if (count($params) == 1 && intval($params[0])) - { - // redundancy much? - $this->subjectGUID = intval($params[0]); - $this->profile = intval($params[0]); - $this->isCustom = true; // until proven otherwise - - $this->subject = new LocalProfileList(array(['id', intval($params[0])])); - if ($this->subject->error) - $this->notFound(); - - if (!$this->subject->isVisibleToUser()) - $this->notFound(); - - if (!$this->subject->isCustom()) - header('Location: '.$this->subject->getProfileUrl(), true, 302); - } - else if (count($params) == 3) - { - $this->getSubjectFromUrl($pageParam); - if (!$this->subjectName) - $this->notFound(); - - // names MUST be ucFirst. Since we don't expect partial matches, search this way - $this->profile = $params; - - // pending rename - if (preg_match('/([^\-]+)-(\d+)/i', $this->subjectName, $m)) - { - $this->subjectName = $m[1]; - $this->rnItr = $m[2]; - } - - // 3 possibilities - // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT id, realmGUID, cuFlags FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID IS NOT NULL AND name = ? AND renameItr = ?d', $this->realmId, Util::ucFirst($this->subjectName), $this->rnItr)) - { - $this->subjectGUID = $subject['id']; - - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) - { - $this->handleIncompleteData($params, $subject['realmGUID']); - return; - } - - $this->subject = new LocalProfileList(array(['id', $subject['id']])); - if ($this->subject->error) - $this->notFound(); - } - // 2) not yet synced but exists on realm (and not a gm character) - else if (!$this->rnItr && ($char = DB::Characters($this->realmId)->selectRow('SELECT c.guid AS realmGUID, c.name, c.race, c.class, c.level, c.gender, c.at_login, g.guildid AS guildGUID, IFNULL(g.name, "") AS guildName, IFNULL(gm.rank, 0) AS guildRank FROM characters c LEFT JOIN guild_member gm ON gm.guid = c.guid LEFT JOIN guild g ON g.guildid = gm.guildid WHERE c.name = ? AND level <= ?d AND (extra_flags & ?d) = 0', Util::ucFirst($this->subjectName), MAX_LEVEL, Profiler::CHAR_GMFLAGS))) - { - $char['realm'] = $this->realmId; - $char['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; - - if ($char['at_login'] & 0x1) - $char['renameItr'] = DB::Aowow()->selectCell('SELECT MAX(renameItr) FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID IS NOT NULL AND name = ?', $this->realmId, $char['name']); - - if ($char['guildGUID']) - { - // create empty guild if necessary to satisfy foreign keys - $char['guild'] = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $this->realmId, $char['guildGUID']); - if (!$char['guild']) - $char['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (realm, realmGUID, cuFlags, name) VALUES (?d, ?d, ?d, ?)', $this->realmId, $char['guildGUID'], PROFILER_CU_NEEDS_RESYNC, $char['guildName']); - } - - unset($char['guildGUID']); - unset($char['guildName']); - unset($char['at_login']); - - // create entry from realm with enough basic info to disply tooltips - DB::Aowow()->query('REPLACE INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($char), array_values($char)); - $this->subjectGUID = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $this->realmId, $char['realmGUID']); - - $this->handleIncompleteData($params, $char['realmGUID']); - } - // 3) does not exist at all - else - $this->notFound(); - } - else if (($params && $params[0]) || !$this->_get['new']) - $this->notFound(); - else if ($this->_get['new']) - $this->mode = CACHE_TYPE_NONE; - } - - protected function generateContent() - { - if ($this->doResync) - return; - - // + .titles ? - $this->addScript([SC_JS_FILE, '?data=enchants.gems.glyphs.itemsets.pets.pet-talents.quick-excludes.realms.statistics.weight-presets.achievements']); - - // as demanded by the raid activity tracker - $bossIds = array( -/* Halion */ -/* ruby */ 39863, -/* Valanar, Lana'thel, Saurfang, Festergut, Deathwisper, Marrowgar, Putricide, Rotface, Sindragosa, Valithria, Lich King */ -/* icc */ 37970, 37955, 37813, 36626, 36855, 36612, 36678, 36627, 36853, 36789, 36597, -/* Jaraxxus, Anub'arak */ -/* toc */ 34780, 34564, -/* Onyxia */ -/* ony */ 10184, -/* Flame Levi, Ignis, Razorscale, XT-002, Kologarn, Auriaya, Freya, Hodir, Mimiron, Thorim, Vezaxx, Yogg, Algalon */ -/* uld */ 33113, 33118, 33186, 33293, 32930, 33515, 32906, 32845, 33350, 32864, 33271, 33288, 32871, -/* Anub, Faerlina, Maexxna, Noth, Heigan, Loatheb, Razuvious, Gothik, Patchwerk, Grobbulus, Gluth, Thaddius, Sapphiron, Kel'Thuzad */ -/* nax */ 15956, 15953, 15952, 15954, 15936, 16011, 16061, 16060, 16028, 15931, 15932, 15928, 15989, 15990 - ); - $this->extendGlobalIds(Type::NPC, ...$bossIds); - - // dummy title from dungeon encounter - foreach (Lang::profiler('encounterNames') as $id => $name) - $this->extendGlobalData([Type::NPC => [$id => ['name_'.Lang::getLocale()->json() => $name]]]); - } - - protected function generatePath() - { - - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::game('profile'))); - } - - protected function generateTooltip() - { - $id = $this->profile; - if (!$this->isCustom) - $id = "'".$this->profile[0].'.'.urlencode($this->profile[1]).'.'.urlencode($this->profile[2])."'"; - - $power = new \StdClass(); - if ($this->subject && !$this->subject->error && $this->subject->isVisibleToUser()) - { - $n = $this->subject->getField('name'); - $l = $this->subject->getField('level'); - $r = $this->subject->getField('race'); - $c = $this->subject->getField('class'); - $g = $this->subject->getField('gender'); - - if ($this->isCustom) - $n .= Lang::profiler('customProfile'); - else if ($_ = $this->subject->getField('title')) - if ($title = (new TitleList(array(['id', $_])))->getField($g ? 'female' : 'male', true)) - $n = sprintf($title, $n); - - $power->{'name_'.Lang::getLocale()->json()} = $n; - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - $power->icon = '$$WH.g_getProfileIcon('.$r.', '.$c.', '.$g.', '.$l.', \''.$this->subject->getIcon().'\')'; - } - - return sprintf($this->powerTpl, $id, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); - } - - public function display(string $override = ''): never - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - parent::display($override); - - // do not cache profile tooltips - header(MIME_TYPE_JSON); - die($this->generateTooltip()); - } - - public function notFound(string $title = '', string $msg = '') : never - { - parent::notFound($title ?: Util::ucFirst(Lang::profiler('profiler')), $msg ?: Lang::profiler('notFound', 'profile')); - } - - private function handleIncompleteData($params, $guid) - { - if ($this->mode == CACHE_TYPE_TOOLTIP) // enable tooltip display with basic data we just added - { - $this->subject = new LocalProfileList(array(['id', $this->subjectGUID]), ['sv' => $params[1]]); - if ($this->subject->error) - $this->notFound(); - - $this->profile = $params; - } - else // display empty page and queue status - { - $this->mode = CACHE_TYPE_NONE; - - // queue full fetch - if ($newId = Profiler::scheduleResync(Type::PROFILE, $this->realmId, $guid)) - { - $this->doResync = ['profile', $newId]; - $this->initialSync(); - } - else // todo: base info should have been created in __construct .. why are we here..? - header('Location: ?profiles='.$params[0].'.'.$params[1].'&filter=na='.Util::ucFirst($this->subjectName).';ex=on'); - } - } -} - -?> diff --git a/pages/profiler.php b/pages/profiler.php deleted file mode 100644 index 7b057719..00000000 --- a/pages/profiler.php +++ /dev/null @@ -1,42 +0,0 @@ -error(); - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=realms']); - } - - protected function generatePath() { } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::profiler('profiler'))); - } -} - -?> diff --git a/pages/profiles.php b/pages/profiles.php deleted file mode 100644 index e772354a..00000000 --- a/pages/profiles.php +++ /dev/null @@ -1,199 +0,0 @@ - 5-man), 1 guild .. it puts a resync button on the lv... - - protected $type = Type::PROFILE; - - protected $tabId = 1; - protected $path = [1, 5, 0]; - protected $tpl = 'profiles'; - protected $scripts = array( - [SC_JS_FILE, 'js/filters.js'], - [SC_JS_FILE, 'js/profile_all.js'], - [SC_JS_FILE, 'js/profile.js'], - [SC_CSS_FILE, 'css/Profiler.css'] - ); - - protected $_get = ['filter' => ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getSubjectFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - if (!Cfg::get('PROFILER_ENABLE')) - $this->error(); - - $realms = []; - foreach (Profiler::getRealms() as $idx => $r) - { - if ($this->region && $r['region'] != $this->region) - continue; - - if ($this->realm && $r['name'] != $this->realm) - continue; - - $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM characters WHERE deleteInfos_Name IS NULL AND level <= ?d AND (extra_flags & ?) = 0', MAX_LEVEL, Profiler::CHAR_GMFLAGS); - $realms[] = $idx; - } - - $this->filterObj = new ProfileListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); - - $this->name = Util::ucFirst(Lang::game('profiles')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateTitle() - { - if ($this->realm) - array_unshift($this->title, $this->realm,/* Cfg::get('BATTLEGROUP'),*/ Lang::profiler('regions', $this->region), Lang::game('profiles')); - else if ($this->region) - array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('profiles')); - else - array_unshift($this->title, Lang::game('profiles')); - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=weight-presets.realms']); - - $conditions = []; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - if (!$this->filterObj->useLocalList) - { - $conditions[] = ['deleteInfos_Name', null]; - $conditions[] = ['level', MAX_LEVEL, '<=']; // prevents JS errors - $conditions[] = [['extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0]; - } - - if ($x = $this->filterObj->fiSetCriteria) - { - if ($r = array_intersect([9, 12, 15, 18], $x['cr'])) - if (count($r) == 1) - $this->roster = (reset($r) - 6) / 3; // 1, 2, 3, or 4 - } - - $tabData = array( - 'id' => 'characters', - 'hideCount' => 1, - 'visibleCols' => ['race', 'classs', 'level', 'talents', 'achievementpoints', 'gearscore'], - 'onBeforeCreate' => '$pr_initRosterListview' // puts a resync button on the lv - ); - - $extraCols = $this->filterObj->fiExtraCols; - if ($extraCols) - { - $xc = []; - foreach ($extraCols as $idx => $col) - if ($idx > 0) - $xc[] = "\$Listview.funcBox.createSimpleCol('Skill' + ".$idx.", g_spell_skills[".$idx."], '7%', 'skill-' + ".$idx.")"; - - $tabData['extraCols'] = $xc; - } - - $miscParams = ['calcTotal' => true]; - if ($this->realm) - $miscParams['sv'] = $this->realm; - if ($this->region) - $miscParams['rg'] = $this->region; - if ($_ = $this->filterObj->extraOpts) - $miscParams['extraOpts'] = $_; - - if ($this->filterObj->useLocalList) - $profiles = new LocalProfileList($conditions, $miscParams); - else - $profiles = new RemoteProfileList($conditions, $miscParams); - - if (!$profiles->error) - { - // init these chars on our side and get local ids - if (!$this->filterObj->useLocalList) - $profiles->initializeLocalEntries(); - - $addInfoMask = PROFILEINFO_CHARACTER; - - // init roster-listview - // $_GET['roster'] = 1|2|3|4 originally supplemented this somehow .. 2,3,4 arenateam-size (4 => 5-man), 1 guild - if ($this->roster == 1 && !$profiles->hasDiffFields('guild') && $profiles->getField('guild')) - { - $tabData['roster'] = $this->roster; - $tabData['visibleCols'][] = 'guildrank'; - $tabData['hiddenCols'][] = 'guild'; - - $this->roster = Lang::profiler('guildRoster', [$profiles->getField('guildname')]); - } - else if ($this->roster && !$profiles->hasDiffFields('arenateam') && $profiles->getField('arenateam')) - { - $tabData['roster'] = $this->roster; - $tabData['visibleCols'][] = 'rating'; - - $addInfoMask |= PROFILEINFO_ARENA; - $this->roster = Lang::profiler('arenaRoster', [$profiles->getField('arenateam')]); - } - else - $this->roster = 0; - - $tabData['data'] = array_values($profiles->getListviewData($addInfoMask, array_filter($extraCols, fn($x) => $x > 0, ARRAY_FILTER_USE_KEY))); - - if ($sc = $this->filterObj->fiSetCriteria) - if (in_array(10, $sc['cr']) && !in_array('guildrank', $tabData['visibleCols'])) - $tabData['visibleCols'][] = 'guildrank'; - - // create note if search limit was exceeded - if ($this->filterObj->query && $profiles->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound2', $this->sumSubjects, $profiles->getMatches()); - $tabData['_truncated'] = 1; - } - else if ($profiles->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound', $this->sumSubjects, 0); - - if ($this->filterObj->useLocalList) - { - if (!empty($tabData['note'])) - $tabData['note'] .= ' + "
'.Lang::profiler('complexFilter').'"'; - else - $tabData['note'] = ''.Lang::profiler('complexFilter').''; - } - - if ($this->filterObj->error) - $tabData['_errors'] = '$1'; - } - else - $this->roster = 0; - - - $this->lvTabs[] = [ProfileList::$brickFile, $tabData]; - } - - protected function postCache() - { - // sort for dropdown-menus - Lang::sort('game', 'cl'); - Lang::sort('game', 'ra'); - } -} - -?> diff --git a/static/js/Profiler.js b/static/js/Profiler.js index 6804298a..ca60a8bf 100644 --- a/static/js/Profiler.js +++ b/static/js/Profiler.js @@ -900,7 +900,7 @@ function Profiler() { function _updateDefaultIcon() { var icon = $WH.g_getProfileIcon(_profile.race, _profile.classs, _profile.gender, _profile.level, 0, 'large'); - // aowow - and another bugged icon request + // aowow - see profile=avatar endpoint for explanation // var icon = $WH.g_getProfileIcon(_profile.race, _profile.classs, _profile.gender, _profile.level, _profile.source, 'large'); if (!_profile.icon) { diff --git a/static/js/global.js b/static/js/global.js index 3c8741aa..af34dd52 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -593,7 +593,7 @@ var PageTemplate = new function() { className: (character.pinned ? 'icon-star-right ' : '') + 'c' + character.classs, // tinyIcon: $WH.g_getProfileIcon(character.race, character.classs, character.gender, character.level, character.id, 'tiny') - // aowow: profileId should not be necessary here + // aowow - see profile=avatar endpoint for explanation tinyIcon: $WH.g_getProfileIcon(character.race, character.classs, character.gender, character.level, 0, 'tiny') }]; @@ -16037,7 +16037,7 @@ Listview.templates = { i.style.borderRight = 'none'; // $WH.ae(i, Icon.create($WH.g_getProfileIcon(profile.race, profile.classs, profile.gender, profile.level, profile.icon ? profile.icon : profile.id, 'medium'), 1, null, this.getItemLink(profile))); - // aowow . i dont know .. i dont know... char icon requests are strange + // aowow - see profile=avatar endpoint for explanation var ic = Icon.create($WH.g_getProfileIcon(profile.race, profile.classs, profile.gender, profile.level, profile.icon ? profile.icon : 0, 'medium'), 1, null, this.getItemLink(profile)); // aowow - custom if (profile.captain) { diff --git a/template/pages/profile.tpl.php b/template/pages/profile.tpl.php index a98c06fe..2edd318d 100644 --- a/template/pages/profile.tpl.php +++ b/template/pages/profile.tpl.php @@ -1,7 +1,8 @@ - - -brick('header'); ?> +brick('header'); +?>
@@ -15,7 +16,7 @@
diff --git a/template/pages/profiler.tpl.php b/template/pages/profiler.tpl.php index 79a3738d..f036e1f0 100644 --- a/template/pages/profiler.tpl.php +++ b/template/pages/profiler.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
@@ -23,17 +26,19 @@
-

+

ucFirst(Lang::main('name')).Lang::main('colon'); ?>

- $n): - echo ' '; -endforeach; - ?> +makeRadiosList('rg', $this->regions, $this->rg, 24, function (&$v, $k, &$attribs) { + $attribs = ['class' => 'profiler-button profiler-option-left']; + $v = ''.$v.''; + if ($k == $this->rg) + $attribs['class'] .= ' selected'; + return true; +}); ?>
diff --git a/template/pages/profiles.tpl.php b/template/pages/profiles.tpl.php index cb34d113..6cfcb132 100644 --- a/template/pages/profiles.tpl.php +++ b/template/pages/profiles.tpl.php @@ -1,10 +1,11 @@ - - brick('header'); -$f = $this->filterObj->values // shorthand -?> + namespace Aowow\Template; + use \Aowow\Lang; + +$this->brick('header'); +$f = $this->filter->values; // shorthand +?>
@@ -12,74 +13,64 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [2]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); -# for some arcane reason a newline (\n) means, the first childNode is a text instead of the form for the following div +# pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before
, it will become firstChild (a text node) ?> -
+
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

h1; ?>

+
-
+
ucFirst(Lang::game('class')).Lang::main('colon'); ?>
-
+
ucFirst(Lang::game('race')).Lang::main('colon'); ?>
- + - + - +
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
 />  /> />  />
        
      /> - /> /> - />
@@ -87,7 +78,7 @@ endforeach;
- />/> + />/>
@@ -112,7 +103,7 @@ endforeach; ?>
-brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?>