From bc834245d7545d7b57c186c76592c6fc9a92c9ee Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 26 Mar 2018 19:58:32 +0200 Subject: [PATCH 001/957] Setup/Sounds * make log more clear, that every available locale is checked --- setup/tools/filegen/soundfiles.func.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/tools/filegen/soundfiles.func.php b/setup/tools/filegen/soundfiles.func.php index 2380fed2..564b0938 100644 --- a/setup/tools/filegen/soundfiles.func.php +++ b/setup/tools/filegen/soundfiles.func.php @@ -49,7 +49,7 @@ if (!CLI) } } - CLI::write(' - did not find file: '.CLI::bold(CLI::nicePath($filePath, CLISetup::$srcDir, '')), CLI::LOG_WARN); + CLI::write(' - did not find file: '.CLI::bold(CLI::nicePath($filePath, CLISetup::$srcDir, '['.implode(',', CLISetup::$locales).']')), CLI::LOG_WARN); // flag as unusable in DB DB::Aowow()->query('UPDATE ?_sounds_files SET id = ?d WHERE ABS(id) = ?d', -$fileId, $fileId); } From 72c1dacd3f405edb5b630ba06a6b6aa2662bbe3f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 26 Mar 2018 21:49:58 +0200 Subject: [PATCH 002/957] Profiler/Guilds * fixed forcing guildnames to string for js * made static string localized --- includes/types/guild.class.php | 4 ++-- includes/types/profile.class.php | 9 ++++----- localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- pages/profile.php | 3 +-- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/includes/types/guild.class.php b/includes/types/guild.class.php index c6136261..06629210 100644 --- a/includes/types/guild.class.php +++ b/includes/types/guild.class.php @@ -16,14 +16,14 @@ class GuildList extends BaseType foreach ($this->iterate() as $__) { $data[$this->id] = array( - 'name' => "$'".$this->curTpl['name']."'", // MUST be a string + 'name' => '$"'.str_replace ('"', '', $this->curTpl['name']).'"', // MUST be a string, omit any quotes in name 'members' => $this->curTpl['members'], 'faction' => $this->curTpl['faction'], 'achievementpoints' => $this->getField('achievementpoints'), 'gearscore' => $this->getField('gearscore'), 'realm' => Profiler::urlize($this->curTpl['realmName']), 'realmname' => $this->curTpl['realmName'], - // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release + // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release // 'battlegroupname' => $this->curTpl['battlegroup'], 'region' => Profiler::urlize($this->curTpl['region']) ); diff --git a/includes/types/profile.class.php b/includes/types/profile.class.php index 02561231..c72bd575 100644 --- a/includes/types/profile.class.php +++ b/includes/types/profile.class.php @@ -33,18 +33,17 @@ class ProfileList extends BaseType 'talenttree1' => $this->getField('talenttree1'), 'talenttree2' => $this->getField('talenttree2'), 'talenttree3' => $this->getField('talenttree3'), - 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2 + 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2 'achievementpoints' => $this->getField('achievementpoints'), - 'guild' => '$"'.$this->getField('guildname').'"', // force this to be a string + 'guild' => '$"'.str_replace ('"', '', $this->curTpl['name']).'"', // force this to be a string 'guildrank' => $this->getField('guildrank'), 'realm' => Profiler::urlize($this->getField('realmName')), 'realmname' => $this->getField('realmName'), - // 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release + // 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release // 'battlegroupname' => $this->getField('battlegroup'), 'gearscore' => $this->getField('gearscore') ); - // for the lv this determins if the link is profile= or profile=.. if ($this->isCustom()) $data[$this->id]['published'] = (int)!!($this->getField('cuFlags') & PROFILER_CU_PUBLISHED); @@ -92,7 +91,7 @@ class ProfileList extends BaseType $title = (new TitleList(array(['bitIdx', $_])))->getField($this->getField('gender') ? 'female' : 'male', true); if ($this->isCustom()) - $name .= ' (Custom Profile)'; + $name .= Lang::profiler('customProfile'); else if ($title) $name = sprintf($title, $name); diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 9c4ab46f..fe03789c 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -156,7 +156,7 @@ $lang = array( '_cpFooter' => "Falls Ihr eine genauere Suche möchtet, probiert unsere erweiterten Suchoptionen. Ihr könnt außerdem ein neues individuelles Profil erstellen.", 'firstUseTitle' => "%s von %s", 'complexFilter' => "Komplexer Filter ausgewählt! Suchergebnisse sind auf gecachte Charaktere beschränkt.", - + 'customProfile' => " (Benutzerprofil)", 'resync' => "Resynchronisieren", 'guildRoster' => "Gildenliste für <%s>", 'arenaRoster' => "Arena-Teamliste für <%s>", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e12e9978..4a82d882 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -156,7 +156,7 @@ $lang = array( '_cpFooter' => "If you want a more refined search try out our advanced search options. You can also create a new custom profile.", 'firstUseTitle' => "%s of %s", 'complexFilter' => "Complex filter selected! Search results are limited to cached Characters.", - + 'customProfile' => " (Custom Profile)", 'resync' => "Resync", 'guildRoster' => "Guild Roster for <%s>", 'arenaRoster' => "Arena Team Roster for <%s>", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 774e6704..657dd3b1 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -156,7 +156,7 @@ $lang = array( '_cpFooter' => "Si quieres una búsqueda más refinada, prueba con nuestras opciones de búsqueda avanzada. También puedes crear un perfil nuevo personalizado.", 'firstUseTitle' => "%s de %s", 'complexFilter' => "[Complex filter selected! Search results are limited to cached Characters.]", - + 'customProfile' => " ([Custom Profile])", 'resync' => "Resincronizar", 'guildRoster' => "Lista de miembros de hermandad para <%s>", 'arenaRoster' => "Personajes del Equipo de Arena para <%s>", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index f0cf9920..52fd58b0 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -156,7 +156,7 @@ $lang = array( '_cpFooter' => "Si vous voulez une recherche plus raffinée, essayez nos options de recherche avancée. Vous pouvez aussi créer un nouveau profile personnalisé.", 'firstUseTitle' => "%s de %s", 'complexFilter' => "[Complex filter selected! Search results are limited to cached Characters.]", - + 'customProfile' => " ([Custom Profile])", 'resync' => "Resynchronisation", 'guildRoster' => "Liste des membres pour la guilde de <%s>", 'arenaRoster' => "[Arena Team Roster for <%s>]", // string probably lost diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 72003066..96a225b6 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -156,7 +156,7 @@ $lang = array( '_cpFooter' => "Если вам нужен более точный поиск, вы можете использовать дополнительные опции. Также, вы можете создать новый собственный профиль.", 'firstUseTitle' => "%s", // yes, thats correct. No nonsense, just the name 'complexFilter' => "[Complex filter selected! Search results are limited to cached Characters.]", - + 'customProfile' => " ([Custom Profile])", 'resync' => "Ресинхронизация", 'guildRoster' => "Список членов гильдии <%s>", 'arenaRoster' => "[Arena Team Roster for <%s>]", // string probably lost diff --git a/pages/profile.php b/pages/profile.php index 1c1c1d2e..3b349981 100644 --- a/pages/profile.php +++ b/pages/profile.php @@ -164,13 +164,12 @@ class ProfilePage extends GenericPage $ra = $this->subject->getField('race'); $cl = $this->subject->getField('class'); $gender = $this->subject->getField('gender'); - // $desc = $this->subject->getField('description'); $title = ''; if ($_ = $this->subject->getField('chosenTitle')) $title = (new TitleList(array(['bitIdx', $_])))->getField($gender ? 'female' : 'male', true); if ($this->isCustom) - $name .= ' (Custom Profile)'; + $name .= Lang::profiler('customProfile'); else if ($title) $name = sprintf($title, $name); From c17cf9c0436665843c501cef42cc3005f40c4b2c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 27 Mar 2018 00:00:29 +0200 Subject: [PATCH 003/957] Profiler/Profile * shorten raid activity string for locale deDE to prevent icons from showing up where they shouldn't (also needed for locale ruRU) * define offhand frill as unfit for enchantments * resort raid activity tracker and add placeholder for ulduar --- static/js/Profiler.js | 34 ++++++++++++++++++---------------- static/js/locale_dede.js | 4 ++-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/static/js/Profiler.js b/static/js/Profiler.js index 42739212..89b96ce4 100644 --- a/static/js/Profiler.js +++ b/static/js/Profiler.js @@ -93,25 +93,27 @@ function Profiler() { shasplpwr: ['shaspldmg'] }, - _progress = [ /* some example data */ - // { level: 150, instance: 2, name: LANG.pr_dungeons, icon: 'spell_holy_championsbond', achievs: [477,478,479,480,481,482,483,484,485,486,487,488,3778,4296,4516,4517,4518], kills: [[1232, 29120], [1235, 31134], [1236, 29306], [1233, 29311], [1242, 23980], [1231, 26723], [1240, 26861], [1239, 27656], [1238, 28923], ] }, // some lvl 80 Dungeons normal - // { level: 187, instance: 2, heroic: 1, name: LANG.pr_dungeons, icon: 'ability_rogue_feigndeath', achievs: [489,490,491,492,493,494,495,496,497,498,499,500,4297,4298,4519,4520,4521], kills: [[1506, 29120], [1509, 31134], [1510, 29306], [1507, 29311], [1504, 23980], [1505, 26723], [1514, 26861], [1513, 27656], [1512, 28923], ] }, // some lvl 80 Dungeons heroic - // { level: 200, instance: 7, zone: 3456, icon: 'achievement_dungeon_naxxramas_normal', achievs: [576], kills: [[1377, 15990]] }, // 10-man naxxramas - // { level: 213, instance: 7, heroic: 1, zone: 3456, icon: 'achievement_dungeon_naxxramas_10man', achievs: [577], kills: [[1390, 15990]] } // 25-man naxxramas - { level: 245, instance: 5, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4397], kills: [[1756, 10184]] }, // Onyxia's Lair 25 + _progress = [ + // { level: 150, instance: 2, name: LANG.pr_dungeons, icon: 'spell_holy_championsbond', achievs: [477,478,479,480,481,482,483,484,485,486,487,488,3778,4296,4516,4517,4518], kills: [[1232, 29120], [1235, 31134], [1236, 29306], [1233, 29311], [1242, 23980], [1231, 26723], [1240, 26861], [1239, 27656], [1238, 28923], ] }, // some lvl 80 Dungeons normal + // { level: 187, instance: 2, heroic: 1, name: LANG.pr_dungeons, icon: 'ability_rogue_feigndeath', achievs: [489,490,491,492,493,494,495,496,497,498,499,500,4297,4298,4519,4520,4521], kills: [[1506, 29120], [1509, 31134], [1510, 29306], [1507, 29311], [1504, 23980], [1505, 26723], [1514, 26861], [1513, 27656], [1512, 28923], ] }, // some lvl 80 Dungeons heroic + // { level: 200, instance: 3, zone: 3456, icon: 'achievement_dungeon_naxxramas_normal', achievs: [576], kills: [[1377, 15990]] }, // 10-man naxxramas + // { level: 213, instance: 5, zone: 3456, icon: 'achievement_dungeon_naxxramas_10man', achievs: [577], kills: [[1390, 15990]] }, // 25-man naxxramas + // { level: 219, instance: 3, zone: 4273, icon: 'achievement_dungeon_ulduarraid_misc_01', achievs: [2894], kills: [/*todo:fillme*/] }, // 10-man ulduar + // { level: 226, instance: 5, zone: 4273, icon: 'achievement_dungeon_ulduarraid_misc_01', achievs: [2895], kills: [/*todo:fillme*/] }, // 25-man ulduar { level: 232, instance: 3, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4396], kills: [[1098, 10184]] }, // Onyxia's Lair 10 - { level: 258, instance: 5, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3812], kills: [[4031, 200001], [4034, 34780], [4038, 200002], [4042, 200003], [4046, 34564]] }, // Trial of the Crusader 25 hc - { level: 245, instance: 5, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3916], kills: [[4029, 200001], [4035, 34780], [4039, 200002], [4043, 200003], [4047, 34564]] }, // Trial of the Crusader 25 nh - { level: 245, instance: 3, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3918], kills: [[4030, 200001], [4033, 34780], [4037, 200002], [4041, 200003], [4045, 34564]] }, // Trial of the Crusader 10 hc + { level: 245, instance: 5, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4397], kills: [[1756, 10184]] }, // Onyxia's Lair 25 { level: 232, instance: 3, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3917], kills: [[4028, 200001], [4032, 34780], [4036, 200002], [4040, 200003], [4044, 34564]] }, // Trial of the Crusader 10 nh - { level: 277, instance: 5, heroic: 1, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4637], kills: [[4673, 37970], [4682, 37955], [4664, 37813], [4667, 36626], [4661, 100001], [4656, 36855], [4642, 36612], [4679, 36678], [4670, 36627], [4685, 36853], [4676, 36789], [4688, 36597]] }, // Icecrown Citadel 25 hc - { level: 264, instance: 5, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4608], kills: [[4672, 37970], [4681, 37955], [4663, 37813], [4666, 36626], [4660, 100001], [4655, 36855], [4641, 36612], [4678, 36678], [4669, 36627], [4683, 36853], [4675, 36789], [4687, 36597]] }, // Icecrown Citadel 25 nh - { level: 264, instance: 3, heroic: 1, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4636], kills: [[4671, 37970], [4680, 37955], [4662, 37813], [4665, 36626], [4659, 100001], [4654, 36855], [4640, 36612], [4677, 36678], [4668, 36627], [4684, 36853], [4674, 36789], [4686, 36597]] }, // Icecrown Citadel 10 hc + { level: 245, instance: 3, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3918], kills: [[4030, 200001], [4033, 34780], [4037, 200002], [4041, 200003], [4045, 34564]] }, // Trial of the Crusader 10 hc + { level: 245, instance: 5, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3916], kills: [[4029, 200001], [4035, 34780], [4039, 200002], [4043, 200003], [4047, 34564]] }, // Trial of the Crusader 25 nh + { level: 258, instance: 5, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3812], kills: [[4031, 200001], [4034, 34780], [4038, 200002], [4042, 200003], [4046, 34564]] }, // Trial of the Crusader 25 hc { level: 251, instance: 3, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4532], kills: [[4648, 37970], [4651, 37955], [4645, 37813], [4646, 36626], [4644, 100001], [4643, 36855], [4639, 36612], [4650, 36678], [4647, 36627], [4652, 36853], [4649, 36789], [4653, 36597]] }, // Icecrown Citadel 10 nh - { level: 284, instance: 5, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4816], kills: [[4823, 39863]] }, // Ruby Sanctum 25 hc - { level: 271, instance: 5, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4815], kills: [[4820, 39863]] }, // Ruby Sanctum 25 nh + { level: 264, instance: 3, heroic: 1, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4636], kills: [[4671, 37970], [4680, 37955], [4662, 37813], [4665, 36626], [4659, 100001], [4654, 36855], [4640, 36612], [4677, 36678], [4668, 36627], [4684, 36853], [4674, 36789], [4686, 36597]] }, // Icecrown Citadel 10 hc + { level: 264, instance: 5, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4608], kills: [[4672, 37970], [4681, 37955], [4663, 37813], [4666, 36626], [4660, 100001], [4655, 36855], [4641, 36612], [4678, 36678], [4669, 36627], [4683, 36853], [4675, 36789], [4687, 36597]] }, // Icecrown Citadel 25 nh + { level: 277, instance: 5, heroic: 1, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4637], kills: [[4673, 37970], [4682, 37955], [4664, 37813], [4667, 36626], [4661, 100001], [4656, 36855], [4642, 36612], [4679, 36678], [4670, 36627], [4685, 36853], [4676, 36789], [4688, 36597]] }, // Icecrown Citadel 25 hc + { level: 258, instance: 3, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4817], kills: [[4821, 39863]] }, // Ruby Sanctum 10 nh { level: 271, instance: 3, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4818], kills: [[4822, 39863]] }, // Ruby Sanctum 10 hc - { level: 258, instance: 3, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4817], kills: [[4821, 39863]] } // Ruby Sanctum 10 nh + { level: 271, instance: 5, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4815], kills: [[4820, 39863]] }, // Ruby Sanctum 25 nh + { level: 284, instance: 5, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4816], kills: [[4823, 39863]] } // Ruby Sanctum 25 hc ]; @@ -4082,7 +4084,7 @@ function ProfilerInventory(_parent) { { id: 13, name: 'trinket', itemslots: [12], nomodel: 2 }, { id: 14, name: 'trinket', itemslots: [12], nomodel: 2 }, { id: 16, name: 'mainhand', itemslots: [21, 13, 17], enchant: 1, weapon: 1 }, - { id: 17, name: 'offhand', itemslots: [22, 13, 23, 14], enchant: 3, weapon: 2 }, + { id: 17, name: 'offhand', itemslots: [22, 13, 23, 14], enchant: 3, weapon: 2, noenchantclasses: [-5] }, { id: 18, name: 'ranged', itemslots: [15, 25], enchant: 3, weapon: 1, classes: [3, 8, 5, 4, 9, 1], nomodel: 1, noenchantclasses: [16, 19] }, { id: 18, name: 'relic', itemslots: [28], classes: [6, 11, 2, 7], nomodel: 2 } // Special case ]; diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 23311121..6152d189 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -4160,8 +4160,8 @@ var LANG = { pr_qf_resynced: "Resynchronisiert ", pr_qf_notsaved: "Profil wurde nicht gespeichert!", pr_qf_gearmeter: "Ausr.", - pr_qf_raidactivity1: "Gesamte Schlachtzugsaktivität", - pr_qf_raidactivity2: "Kürzliche Schlachtzugsaktivität", + pr_qf_raidactivity1: "Gesamte Schlachtzugsakt.", + pr_qf_raidactivity2: "Kürzliche Schlachtzugsakt.", pr_qf_activitytip1: "Klickt, um kürzliche Schlachtzugsaktivität anzuzeigen", pr_qf_activitytip2: "Klickt, um gesamte Schlachtzugsaktivität anzuzeigen", pr_qf_activitypct1: "$1% der gesamten Aktivität", From 22d02378ef71ffc2d48c19831da731ee2fc951b4 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 27 Mar 2018 12:39:14 +0200 Subject: [PATCH 004/957] Config/Profiler * update config to enable/disable profiler in general, instead of just the queue * regenerate affected files on config change --- includes/ajaxHandler/admin.class.php | 3 ++- includes/ajaxHandler/profile.class.php | 3 +++ includes/profiler.class.php | 2 +- pages/arenateam.php | 3 +++ pages/arenateams.php | 3 +++ pages/guild.php | 3 +++ pages/guilds.php | 3 +++ pages/profiler.php | 8 ++++++++ pages/profiles.php | 3 +++ prQueue | 4 ++-- setup/db_structure.sql | 4 ++-- setup/tools/clisetup/siteconfig.func.php | 3 ++- setup/updates/1522146994_01.sql | 2 ++ 13 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 setup/updates/1522146994_01.sql diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php index b9edffcb..93982ccd 100644 --- a/includes/ajaxHandler/admin.class.php +++ b/includes/ajaxHandler/admin.class.php @@ -471,7 +471,8 @@ class AjaxAdmin extends AjaxHandler $msg .= ' * remember to rebuild all static files for the language you just added.
'; $msg .= ' * you can speed this up by supplying the regionCode to the setup:
--locales= -f
'; break; - case 'profiler_queue': + case 'profiler_enable': + $buildList = 'realms,realmMenu'; $fn = function($x) use (&$msg) { if (!$x) return true; diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php index 50cded7d..5c6436cc 100644 --- a/includes/ajaxHandler/profile.class.php +++ b/includes/ajaxHandler/profile.class.php @@ -47,6 +47,9 @@ class AjaxProfile extends AjaxHandler if (!$this->params) return; + if (!CFG_PROFILER_ENABLE) + return; + switch ($this->params[0]) { case 'unlink': diff --git a/includes/profiler.class.php b/includes/profiler.class.php index 6be872d5..dcee1f3c 100644 --- a/includes/profiler.class.php +++ b/includes/profiler.class.php @@ -244,7 +244,7 @@ class Profiler public static function resyncStatus($type, array $subjectGUIDs) { - $response = [CFG_PROFILER_QUEUE ? 2 : 0]; // in theory you could have multiple queues; used as divisor for: (15 / x) + 2 + $response = [CFG_PROFILER_ENABLE ? 2 : 0]; // in theory you could have multiple queues; used as divisor for: (15 / x) + 2 if (!$subjectGUIDs) $response[] = [PR_QUEUE_STATUS_ENDED, 0, 0, PR_QUEUE_ERROR_CHAR]; else diff --git a/pages/arenateam.php b/pages/arenateam.php index ff2104e1..24ec0f99 100644 --- a/pages/arenateam.php +++ b/pages/arenateam.php @@ -20,6 +20,9 @@ class ArenaTeamPage extends GenericPage public function __construct($pageCall, $pageParam) { + if (!CFG_PROFILER_ENABLE) + $this->error(); + $params = array_map('urldecode', explode('.', $pageParam)); if ($params[0]) $params[0] = Profiler::urlize($params[0]); diff --git a/pages/arenateams.php b/pages/arenateams.php index 3fdddf26..e72f2f59 100644 --- a/pages/arenateams.php +++ b/pages/arenateams.php @@ -17,6 +17,9 @@ class ArenaTeamsPage extends GenericPage public function __construct($pageCall, $pageParam) { + if (!CFG_PROFILER_ENABLE) + $this->error(); + $this->getSubjectFromUrl($pageParam); $this->filterObj = new ArenaTeamListFilter(); diff --git a/pages/guild.php b/pages/guild.php index 39a3ee0d..8ba18176 100644 --- a/pages/guild.php +++ b/pages/guild.php @@ -20,6 +20,9 @@ class GuildPage extends GenericPage public function __construct($pageCall, $pageParam) { + if (!CFG_PROFILER_ENABLE) + $this->error(); + $params = array_map('urldecode', explode('.', $pageParam)); if ($params[0]) $params[0] = Profiler::urlize($params[0]); diff --git a/pages/guilds.php b/pages/guilds.php index 05aae2fa..cee61f76 100644 --- a/pages/guilds.php +++ b/pages/guilds.php @@ -17,6 +17,9 @@ class GuildsPage extends GenericPage public function __construct($pageCall, $pageParam) { + if (!CFG_PROFILER_ENABLE) + $this->error(); + $this->getSubjectFromUrl($pageParam); $this->filterObj = new GuildListFilter(); diff --git a/pages/profiler.php b/pages/profiler.php index 08a941b8..f0a26ddb 100644 --- a/pages/profiler.php +++ b/pages/profiler.php @@ -13,6 +13,14 @@ class ProfilerPage extends GenericPage protected $js = ['profile_all.js', 'profile.js']; protected $css = [['path' => 'Profiler.css']]; + public function __construct($pageCall, $pageParam) + { + if (!CFG_PROFILER_ENABLE) + $this->error(); + + parent::__construct($pageCall, $pageParam); + } + protected function generateContent() { $this->addJS('?data=realms&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); diff --git a/pages/profiles.php b/pages/profiles.php index a0943620..3cd64741 100644 --- a/pages/profiles.php +++ b/pages/profiles.php @@ -20,6 +20,9 @@ class ProfilesPage extends GenericPage public function __construct($pageCall, $pageParam) { + if (!CFG_PROFILER_ENABLE) + $this->error(); + $this->getSubjectFromUrl($pageParam); $realms = []; diff --git a/prQueue b/prQueue index 4d2c645d..ad162fd7 100755 --- a/prQueue +++ b/prQueue @@ -41,8 +41,8 @@ $error = function ($type, $typeId, $realmId) }; -// if (CFG_PROFILER_QUEUE) - wont work because it is not redefined if changed in config -while (DB::Aowow()->selectCell('SELECT value FROM ?_config WHERE `key` = "profiler_queue"')) +// if (CFG_PROFILER_ENABLE) - wont work because it is not redefined if changed in config +while (DB::Aowow()->selectCell('SELECT value FROM ?_config WHERE `key` = "profiler_enable"')) { if (($tDiff = (microtime(true) - $tCycle)) < (CFG_PROFILER_QUEUE_DELAY / 1000)) { diff --git a/setup/db_structure.sql b/setup/db_structure.sql index d6202737..7eea4c1d 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2996,7 +2996,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_config` WRITE; /*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',1,129,'default: 500 - max results for search'),('sql_limit_default','300',1,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',1,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',1,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',1,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',1,136,' - website title'),('name_short','Aowow',1,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',1,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',1,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',1,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',1,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',1,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',1,129,'default: 50 - vote limit per day'),('force_ssl','0',1,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',1,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',1,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',1,136,' - points js to executable files'),('static_host','',1,136,' - points js to images & scripts'),('cache_decay','25200',2,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',2,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',2,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',3,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',3,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',3,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',3,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',3,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',3,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',4,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',4,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','1',4,200,'default: 0 - probability to remove session data on garbage collection'),('session.gc_divisor','100',4,200,'default: 100 - probability to remove session data on garbage collection'),('session_cache_dir','',4,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',5,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',5,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',5,129,'default: 75 - required reputation to write a comment'),('rep_req_reply','75',5,129,'default: 75 - required reputation to write a reply'),('rep_req_supervote','2500',5,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',5,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',5,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',5,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',5,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',5,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',5,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',5,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',5,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',5,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',5,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',5,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',5,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',5,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',5,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','5',0,65,' - some derelict code, probably unused'),('memory_limit','1500M',0,200,'default: 1500M - parsing spell.dbc is quite intense'),('default_charset','UTF-8',0,72,'default: UTF-8'),('analytics_user','',6,136,'default: - enter your GA-user here to track site stats'),('profiler_queue','0',7,132,'default: 0 - enable/disable profiler queue'),('profiler_queue_delay','3000',7,129,'default: 3000 - min. delay between queue cycles (in ms)'),('profiler_resync_ping','5000',7,129,'default: 5000 - how often the javascript asks for for updates, when queued (in ms)'),('profiler_resync_delay','3600',7,129,'default: 1*60*60 - how often a character can be refreshed (in sec)'); +INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',1,129,'default: 500 - max results for search'),('sql_limit_default','300',1,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',1,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',1,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',1,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',1,136,' - website title'),('name_short','Aowow',1,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',1,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',1,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',1,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',1,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',1,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',1,129,'default: 50 - vote limit per day'),('force_ssl','0',1,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',1,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',1,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',1,136,' - points js to executable files'),('static_host','',1,136,' - points js to images & scripts'),('cache_decay','25200',2,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',2,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',2,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',3,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',3,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',3,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',3,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',3,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',3,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',4,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',4,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','1',4,200,'default: 0 - probability to remove session data on garbage collection'),('session.gc_divisor','100',4,200,'default: 100 - probability to remove session data on garbage collection'),('session_cache_dir','',4,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',5,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',5,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',5,129,'default: 75 - required reputation to write a comment'),('rep_req_reply','75',5,129,'default: 75 - required reputation to write a reply'),('rep_req_supervote','2500',5,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',5,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',5,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',5,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',5,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',5,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',5,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',5,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',5,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',5,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',5,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',5,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',5,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',5,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',5,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','5',0,65,' - some derelict code, probably unused'),('memory_limit','1500M',0,200,'default: 1500M - parsing spell.dbc is quite intense'),('default_charset','UTF-8',0,72,'default: UTF-8'),('analytics_user','',6,136,'default: - enter your GA-user here to track site stats'),('profiler_enable','0',7,132,'default: 0 - enable/disable profiler feature'),('profiler_queue_delay','3000',7,129,'default: 3000 - min. delay between queue cycles (in ms)'),('profiler_resync_ping','5000',7,129,'default: 5000 - how often the javascript asks for for updates, when queued (in ms)'),('profiler_resync_delay','3600',7,129,'default: 1*60*60 - how often a character can be refreshed (in sec)'); /*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; UNLOCK TABLES; @@ -3006,7 +3006,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1521735364,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1522146995,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/tools/clisetup/siteconfig.func.php b/setup/tools/clisetup/siteconfig.func.php index aad8a67f..e68c8464 100644 --- a/setup/tools/clisetup/siteconfig.func.php +++ b/setup/tools/clisetup/siteconfig.func.php @@ -46,7 +46,8 @@ function siteconfig() CLI::write(' * remember to rebuild all static files for the language you just added.', CLI::LOG_INFO); CLI::write(' * you can speed this up by supplying the regionCode to the setup: '.CLI::bold('--locales= -f')); break; - case 'profiler_queue': + case 'profiler_enable': + array_push($updScripts, 'realms', 'realmMenu'); $fn = function($x) { if (!$x) return true; diff --git a/setup/updates/1522146994_01.sql b/setup/updates/1522146994_01.sql new file mode 100644 index 00000000..fbebd0aa --- /dev/null +++ b/setup/updates/1522146994_01.sql @@ -0,0 +1,2 @@ +DELETE FROM `aowow_config` WHERE `key` IN ('profiler_queue', 'profiler_enable'); +INSERT INTO `aowow_config` VALUES ('profiler_enable', '0', 7, 132, 'default: 0 - enable/disable profiler feature'); From e973e5e33b60e3f1981ac5847bd765925774680d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 28 Mar 2018 11:20:41 +0200 Subject: [PATCH 005/957] Profiler/RaidTracker * ALL the Raids .. track em --- includes/profiler.class.php | 14 +++++++++----- localization/locale_dede.php | 5 ++++- localization/locale_enus.php | 5 ++++- localization/locale_eses.php | 5 ++++- localization/locale_frfr.php | 5 ++++- localization/locale_ruru.php | 5 ++++- pages/profile.php | 24 +++++++++++++++--------- static/js/Profiler.js | 34 +++++++++++++++++----------------- 8 files changed, 61 insertions(+), 36 deletions(-) diff --git a/includes/profiler.class.php b/includes/profiler.class.php index dcee1f3c..3f73a998 100644 --- a/includes/profiler.class.php +++ b/includes/profiler.class.php @@ -33,7 +33,11 @@ class Profiler 19 => [INVTYPE_TABARD], // tabard ); - public static $raidProgression = array( // statisticAchievement => relevantCriterium + public static $raidProgression = array( // statisticAchievement => relevantCriterium ; don't forget to enable this in /js/Profiler.js as well + 1361 => 5100, 1362 => 5101, 1363 => 5102, 1365 => 5104, 1366 => 5108, 1364 => 5110, 1369 => 5112, 1370 => 5113, 1371 => 5114, 1372 => 5117, 1373 => 5119, 1374 => 5120, 1375 => 7805, 1376 => 5122, 1377 => 5123, // Naxxramas 10 + 1367 => 5103, 1368 => 5111, 1378 => 5124, 1379 => 5125, 1380 => 5126, 1381 => 5127, 1382 => 5128, 1383 => 7806, 1384 => 5130, 1385 => 5131, 1386 => 5132, 1387 => 5133, 1388 => 5134, 1389 => 5135, 1390 => 5136, // Naxxramas 25 + 2856 => 9938, 2857 => 9939, 2858 => 9940, 2859 => 9941, 2861 => 9943, 2865 => 9947, 2866 => 9948, 2868 => 9950, 2869 => 9951, 2870 => 9952, 2863 => 10558, 2864 => 10559, 2862 => 10560, 2867 => 10565, 2860 => 10580, // Ulduar 10 + 2872 => 9954, 2873 => 9955, 2874 => 9956, 2884 => 9957, 2875 => 9959, 2879 => 9963, 2880 => 9964, 2882 => 9966, 2883 => 9967, 3236 => 10542, 3257 => 10561, 3256 => 10562, 3258 => 10563, 2881 => 10566, 2885 => 10581, // Ulduar 25 1098 => 3271, // Onyxia's Lair 10 1756 => 13345, // Onyxia's Lair 25 4031 => 12230, 4034 => 12234, 4038 => 12238, 4042 => 12242, 4046 => 12246, // Trial of the Crusader 25 nh @@ -44,10 +48,10 @@ class Profiler 4641 => 13092, 4655 => 13105, 4660 => 13109, 4663 => 13112, 4666 => 13115, 4669 => 13118, 4672 => 13121, 4675 => 13124, 4678 => 13127, 4681 => 13130, 4683 => 13133, 4687 => 13136, // Icecrown Citadel 25 nh 4640 => 13090, 4654 => 13104, 4659 => 13110, 4662 => 13113, 4665 => 13116, 4668 => 13119, 4671 => 13122, 4674 => 13125, 4677 => 13128, 4680 => 13131, 4684 => 13134, 4686 => 13137, // Icecrown Citadel 10 hc 4639 => 13089, 4643 => 13093, 4644 => 13094, 4645 => 13095, 4646 => 13096, 4647 => 13097, 4648 => 13098, 4649 => 13099, 4650 => 13100, 4651 => 13101, 4652 => 13102, 4653 => 13103, // Icecrown Citadel 10 nh - // 4823 => 13467, // Ruby Sanctum 25 hc - // 4820 => 13465, // Ruby Sanctum 25 nh - // 4822 => 13468, // Ruby Sanctum 10 hc - // 4821 => 13466, // Ruby Sanctum 10 nh + 4823 => 13467, // Ruby Sanctum 25 hc + 4820 => 13465, // Ruby Sanctum 25 nh + 4822 => 13468, // Ruby Sanctum 10 hc + 4821 => 13466, // Ruby Sanctum 10 nh ); public static function getBuyoutForItem($itemId) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index fe03789c..d03cd141 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -172,7 +172,10 @@ $lang = array( 'profile' => "Dieser Charakter existiert nicht oder wurde noch nicht in die Datenbank übernommen." ), 'dummyNPCs' => array( - 100001 => "Luftschiffkampf", 200001 => "Bestien von Nordend", 200002 => "Fraktionschampions", 200003 => "Zwillingsval'kyr" + 100001 => "Luftschiffkampf", + 200001 => "Bestien von Nordend", 200002 => "Fraktionschampions", 200003 => "Zwillingsval'kyr", + 300001 => "Die Vier Reiter", + 400001 => "Versammlung des Eisens" ), ), 'screenshot' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 4a82d882..2e200f0f 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -172,7 +172,10 @@ $lang = array( 'profile' => "This character doesn't exist or is not yet in the database." ), 'dummyNPCs' => array( - 100001 => "Gunship Battle", 200001 => "Northrend Beasts", 200002 => "Faction Champions", 200003 => "Val'kyr Twins" + 100001 => "Gunship Battle", + 200001 => "Northrend Beasts", 200002 => "Faction Champions", 200003 => "Val'kyr Twins", + 300001 => "The Four Horsemen", + 400001 => "Assembly of Iron" ), ), 'screenshot' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 657dd3b1..145ab091 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -172,7 +172,10 @@ $lang = array( 'profile' => "Este personaje no existe o no está aun en la base de datos.", ), 'dummyNPCs' => array( - 100001 => "Batalla de naves de guerra", 200001 => "Bestias de Rasganorte", 200002 => "Campeones de facciones", 200003 => "Gemelas Val'kyr" + 100001 => "Batalla de naves de guerra", + 200001 => "Bestias de Rasganorte", 200002 => "Campeones de facciones", 200003 => "Gemelas Val'kyr", + 300001 => "Los Cuatro Jinetes", + 400001 => "La Asamblea de Hierro" ), ), 'screenshot' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 52fd58b0..abd908b7 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -172,7 +172,10 @@ $lang = array( 'profile' => "[This character doesn't exist or is not yet in the database.]" ), 'dummyNPCs' => array( - 100001 => "Bataille des canonnières", 200001 => "Bêtes du Norfendre", 200002 => "Champions de faction", 200003 => "Les jumelles val'kyrs" + 100001 => "Bataille des canonnières", + 200001 => "Bêtes du Norfendre", 200002 => "Champions de faction", 200003 => "Les jumelles val'kyrs", + 300001 => "Les Quatre Cavaliers", + 400001 => "Mande-foudre Brundir" ), ), 'screenshot' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 96a225b6..8d4f394f 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -172,7 +172,10 @@ $lang = array( 'guild' => "Такая гильдия не существует, или еще не добавлена в базу данных." ), 'dummyNPCs' => array( - 100001 => "Бой на Кораблях", 200001 => "Звери Нордскола", 200002 => "Чемпионы фракций", 200003 => "Валь'киры-близнецы" + 100001 => "Бой на Кораблях", + 200001 => "Звери Нордскола", 200002 => "Чемпионы фракций", 200003 => "Валь'киры-близнецы", + 300001 => "Четыре Всадника", + 400001 => "Железное Собрание" ), ), 'screenshot' => array( diff --git a/pages/profile.php b/pages/profile.php index 3b349981..8cc81fde 100644 --- a/pages/profile.php +++ b/pages/profile.php @@ -116,21 +116,27 @@ class ProfilePage extends GenericPage // as demanded by the raid activity tracker $bossIds = array( -/* Halion */ +/* Halion */ /* ruby */ 39863, -/* Valanar, Lana'thel, Saurfang, Festergut, Deathwisper, Marrowgar, Putricide, Rotface, Sindragosa, Valithria, Lich King */ +/* 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 */ +/* Jaraxxus, Anub'arak */ /* toc */ 34780, 34564, -/* Onyxia */ -/* ony */ 10184 +/* 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 ); // some events have no singular creature to point to .. create dummy entries $dummyNPCs = [TYPE_NPC => array( - 100001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 100001)], - 200001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200001)], - 200002 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200002)], - 200003 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200003)] + 100001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 100001)], // Gunship Battle + 200001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200001)], // Northrend Beasts + 200002 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200002)], // Faction Champions + 200003 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200003)], // Val'kyr Twins + 300001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 300001)], // The Four Horsemen + 400001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 400001)] // Assembly of Iron )]; $this->extendGlobalIds(TYPE_NPC, $bossIds); diff --git a/static/js/Profiler.js b/static/js/Profiler.js index 89b96ce4..04508848 100644 --- a/static/js/Profiler.js +++ b/static/js/Profiler.js @@ -93,27 +93,27 @@ function Profiler() { shasplpwr: ['shaspldmg'] }, - _progress = [ - // { level: 150, instance: 2, name: LANG.pr_dungeons, icon: 'spell_holy_championsbond', achievs: [477,478,479,480,481,482,483,484,485,486,487,488,3778,4296,4516,4517,4518], kills: [[1232, 29120], [1235, 31134], [1236, 29306], [1233, 29311], [1242, 23980], [1231, 26723], [1240, 26861], [1239, 27656], [1238, 28923], ] }, // some lvl 80 Dungeons normal - // { level: 187, instance: 2, heroic: 1, name: LANG.pr_dungeons, icon: 'ability_rogue_feigndeath', achievs: [489,490,491,492,493,494,495,496,497,498,499,500,4297,4298,4519,4520,4521], kills: [[1506, 29120], [1509, 31134], [1510, 29306], [1507, 29311], [1504, 23980], [1505, 26723], [1514, 26861], [1513, 27656], [1512, 28923], ] }, // some lvl 80 Dungeons heroic - // { level: 200, instance: 3, zone: 3456, icon: 'achievement_dungeon_naxxramas_normal', achievs: [576], kills: [[1377, 15990]] }, // 10-man naxxramas - // { level: 213, instance: 5, zone: 3456, icon: 'achievement_dungeon_naxxramas_10man', achievs: [577], kills: [[1390, 15990]] }, // 25-man naxxramas - // { level: 219, instance: 3, zone: 4273, icon: 'achievement_dungeon_ulduarraid_misc_01', achievs: [2894], kills: [/*todo:fillme*/] }, // 10-man ulduar - // { level: 226, instance: 5, zone: 4273, icon: 'achievement_dungeon_ulduarraid_misc_01', achievs: [2895], kills: [/*todo:fillme*/] }, // 25-man ulduar - { level: 232, instance: 3, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4396], kills: [[1098, 10184]] }, // Onyxia's Lair 10 - { level: 245, instance: 5, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4397], kills: [[1756, 10184]] }, // Onyxia's Lair 25 - { level: 232, instance: 3, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3917], kills: [[4028, 200001], [4032, 34780], [4036, 200002], [4040, 200003], [4044, 34564]] }, // Trial of the Crusader 10 nh - { level: 245, instance: 3, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3918], kills: [[4030, 200001], [4033, 34780], [4037, 200002], [4041, 200003], [4045, 34564]] }, // Trial of the Crusader 10 hc - { level: 245, instance: 5, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3916], kills: [[4029, 200001], [4035, 34780], [4039, 200002], [4043, 200003], [4047, 34564]] }, // Trial of the Crusader 25 nh - { level: 258, instance: 5, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3812], kills: [[4031, 200001], [4034, 34780], [4038, 200002], [4042, 200003], [4046, 34564]] }, // Trial of the Crusader 25 hc + _progress = [ // aowow: don't forget to enable tracking in includes/profiler.class.php + // { level: 150, instance: 2, name: LANG.pr_dungeons, icon: 'spell_holy_championsbond', achievs: [477,478,479,480,481,482,483,484,485,486,487,488,3778,4296,4516,4517,4518], kills: [[1232, 29120], [1235, 31134], [1236, 29306], [1233, 29311], [1242, 23980], [1231, 26723], [1240, 26861], [1239, 27656], [1238, 28923]] }, // some lvl 80 Dungeons normal + // { level: 187, instance: 2, heroic: 1, name: LANG.pr_dungeons, icon: 'ability_rogue_feigndeath', achievs: [489,490,491,492,493,494,495,496,497,498,499,500,4297,4298,4519,4520,4521], kills: [[1506, 29120], [1509, 31134], [1510, 29306], [1507, 29311], [1504, 23980], [1505, 26723], [1514, 26861], [1513, 27656], [1512, 28923]] }, // some lvl 80 Dungeons heroic + // { level: 200, instance: 3, zone: 3456, icon: 'achievement_dungeon_naxxramas_normal', achievs: [576], kills: [1361, 15956], [1362, 15953], [1363, 15952], [1365, 15954], [1366, 16060], [1364, 16028], [1369, 15936], [1370, 16011], [1371, 15931], [1372, 15932], [1373, 15928], [1374, 16061], [1375, 300001], [1376, 15989], [1377, 15990]] }, // 10-man naxxramas + // { level: 213, instance: 5, zone: 3456, icon: 'achievement_dungeon_naxxramas_10man', achievs: [577], kills: [[1367, 16028], [1368, 15956], [1378, 15932], [1379, 16060], [1380, 15953], [1381, 15931], [1382, 15936], [1383, 300001], [1384, 16061], [1385, 16011], [1386, 15952], [1387, 15954], [1388, 15928], [1389, 15989], [1390, 15990]] }, // 25-man naxxramas + // { level: 219, instance: 3, zone: 4273, icon: 'achievement_dungeon_ulduarraid_misc_01', achievs: [2894], kills: [[2856, 33113], [2857, 33186], [2858, 33118], [2859, 33293], [2861, 32930], [2865, 33432], [2866, 33271], [2868, 33515], [2869, 33288], [2870, 33993], [2863, 64985], [2864, 65074], [2862, 64899], [2867, 65184], [2860, 400001]] }, // 10-man ulduar + // { level: 226, instance: 5, zone: 4273, icon: 'achievement_dungeon_ulduarraid_misc_01', achievs: [2895], kills: [[2872, 33113], [2873, 33186], [2874, 33118], [2884, 33293], [2875, 32930], [2879, 33432], [2880, 33271], [2882, 33515], [2883, 33288], [3236, 33993], [3257, 64985], [3256, 64899], [3258, 65074], [2881, 65184], [2885, 400001]] }, // 25-man ulduar + { level: 232, instance: 3, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4396], kills: [[1098, 10184]] }, // Onyxia's Lair 10 + { level: 245, instance: 5, zone: 2159, icon: 'achievement_boss_onyxia', achievs: [4397], kills: [[1756, 10184]] }, // Onyxia's Lair 25 + { level: 232, instance: 3, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3917], kills: [[4028, 200001], [4032, 34780], [4036, 200002], [4040, 200003], [4044, 34564]] }, // Trial of the Crusader 10 nh + { level: 245, instance: 3, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3918], kills: [[4030, 200001], [4033, 34780], [4037, 200002], [4041, 200003], [4045, 34564]] }, // Trial of the Crusader 10 hc + { level: 245, instance: 5, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3916], kills: [[4029, 200001], [4035, 34780], [4039, 200002], [4043, 200003], [4047, 34564]] }, // Trial of the Crusader 25 nh + { level: 258, instance: 5, heroic: 1, zone: 4722, icon: 'achievement_reputation_argentchampion', achievs: [3812], kills: [[4031, 200001], [4034, 34780], [4038, 200002], [4042, 200003], [4046, 34564]] }, // Trial of the Crusader 25 hc { level: 251, instance: 3, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4532], kills: [[4648, 37970], [4651, 37955], [4645, 37813], [4646, 36626], [4644, 100001], [4643, 36855], [4639, 36612], [4650, 36678], [4647, 36627], [4652, 36853], [4649, 36789], [4653, 36597]] }, // Icecrown Citadel 10 nh { level: 264, instance: 3, heroic: 1, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4636], kills: [[4671, 37970], [4680, 37955], [4662, 37813], [4665, 36626], [4659, 100001], [4654, 36855], [4640, 36612], [4677, 36678], [4668, 36627], [4684, 36853], [4674, 36789], [4686, 36597]] }, // Icecrown Citadel 10 hc { level: 264, instance: 5, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4608], kills: [[4672, 37970], [4681, 37955], [4663, 37813], [4666, 36626], [4660, 100001], [4655, 36855], [4641, 36612], [4678, 36678], [4669, 36627], [4683, 36853], [4675, 36789], [4687, 36597]] }, // Icecrown Citadel 25 nh { level: 277, instance: 5, heroic: 1, zone: 4812, icon: 'achievement_dungeon_icecrown_frostmourne', achievs: [4637], kills: [[4673, 37970], [4682, 37955], [4664, 37813], [4667, 36626], [4661, 100001], [4656, 36855], [4642, 36612], [4679, 36678], [4670, 36627], [4685, 36853], [4676, 36789], [4688, 36597]] }, // Icecrown Citadel 25 hc - { level: 258, instance: 3, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4817], kills: [[4821, 39863]] }, // Ruby Sanctum 10 nh - { level: 271, instance: 3, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4818], kills: [[4822, 39863]] }, // Ruby Sanctum 10 hc - { level: 271, instance: 5, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4815], kills: [[4820, 39863]] }, // Ruby Sanctum 25 nh - { level: 284, instance: 5, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4816], kills: [[4823, 39863]] } // Ruby Sanctum 25 hc + { level: 258, instance: 3, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4817], kills: [[4821, 39863]] }, // Ruby Sanctum 10 nh + { level: 271, instance: 3, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4818], kills: [[4822, 39863]] }, // Ruby Sanctum 10 hc + { level: 271, instance: 5, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4815], kills: [[4820, 39863]] }, // Ruby Sanctum 25 nh + { level: 284, instance: 5, heroic: 1, zone: 4987, icon: 'spell_shadow_twilight', achievs: [4816], kills: [[4823, 39863]] } // Ruby Sanctum 25 hc ]; From 431e984f03f2585c27ada8395936b06a3fd4f935 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 28 Mar 2018 11:59:13 +0200 Subject: [PATCH 006/957] DB/Structure * update collate to use utf8mb4 * fixed creature paths being capped at 255 waypoints --- includes/database.class.php | 2 +- setup/db_structure.sql | 962 ++++++++++++++++---------------- setup/updates/1522230798_01.sql | 279 +++++++++ 3 files changed, 761 insertions(+), 482 deletions(-) create mode 100644 setup/updates/1522230798_01.sql diff --git a/includes/database.class.php b/includes/database.class.php index de2e2f91..b097750c 100644 --- a/includes/database.class.php +++ b/includes/database.class.php @@ -32,7 +32,7 @@ class DB die('Failed to connect to database.'); $interface->setErrorHandler(['DB', 'errorHandler']); - $interface->query('SET NAMES ?', 'utf8'); + $interface->query('SET NAMES ?', 'utf8mb4'); if ($options['prefix']) $interface->setIdentPrefix($options['prefix']); diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 7eea4c1d..9407f5e4 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -25,31 +25,31 @@ DROP TABLE IF EXISTS `aowow_account`; CREATE TABLE `aowow_account` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `extId` int(10) unsigned NOT NULL COMMENT 'external user id', - `user` varchar(64) NOT NULL COMMENT 'login', - `passHash` varchar(128) NOT NULL, - `displayName` varchar(64) NOT NULL COMMENT 'nickname', - `email` varchar(64) NOT NULL, + `user` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'login', + `passHash` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `displayName` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'nickname', + `email` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, `joinDate` int(10) unsigned NOT NULL COMMENT 'unixtime', `allowExpire` tinyint(1) unsigned NOT NULL, `dailyVotes` smallint(5) unsigned NOT NULL DEFAULT 0, `consecutiveVisits` smallint(5) unsigned NOT NULL DEFAULT 0, - `curIP` varchar(45) NOT NULL, - `prevIP` varchar(45) NOT NULL, + `curIP` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, + `prevIP` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, `curLogin` int(15) unsigned NOT NULL COMMENT 'unixtime', `prevLogin` int(15) unsigned NOT NULL, `locale` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT '0,2,3,6,8', `userGroups` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'bitmask', - `avatar` varchar(50) NOT NULL DEFAULT '' COMMENT 'icon-string for internal or id for upload', - `title` varchar(50) NOT NULL DEFAULT '' COMMENT 'user can obtain custom titles', - `description` text NOT NULL COMMENT 'markdown formated', - `excludeGroups` smallint(5) unsigned NOT NULL DEFAULT 1 COMMENT 'profiler - completion exclude bitmask', + `avatar` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'icon-string for internal or id for upload', + `title` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'user can obtain custom titles', + `description` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'markdown formated', + `excludeGroups` smallint(5) unsigned NOT NULL DEFAULT 1 COMMENT 'profiler - exclude bitmask', `userPerms` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT 'bool isAdmin', `status` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT 'flag, see defines', `statusTimer` int(10) unsigned NOT NULL DEFAULT 0, - `token` varchar(40) NOT NULL COMMENT 'creation & recovery', + `token` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'creation & recovery', PRIMARY KEY (`id`), UNIQUE KEY `user` (`user`) -) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -66,11 +66,11 @@ CREATE TABLE `aowow_account_banned` ( `typeMask` tinyint(4) unsigned NOT NULL COMMENT 'ACC_BAN_*', `start` int(10) unsigned NOT NULL COMMENT 'unixtime', `end` int(10) unsigned NOT NULL COMMENT 'automatic unban @ unixtime', - `reason` varchar(255) NOT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `FK_acc_banned` (`userId`), CONSTRAINT `FK_acc_banned` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -81,12 +81,12 @@ DROP TABLE IF EXISTS `aowow_account_bannedips`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_account_bannedips` ( - `ip` varchar(45) NOT NULL, + `ip` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, `type` tinyint(4) NOT NULL COMMENT '0: onSignin; 1:onSignup', `count` smallint(6) NOT NULL COMMENT 'nFails', `unbanDate` int(11) NOT NULL COMMENT 'automatic remove @ unixtime', PRIMARY KEY (`ip`,`type`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -98,12 +98,12 @@ DROP TABLE IF EXISTS `aowow_account_cookies`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_account_cookies` ( `userId` int(10) unsigned NOT NULL, - `name` varchar(127) NOT NULL, - `data` text NOT NULL, + `name` varchar(127) COLLATE utf8mb4_unicode_ci NOT NULL, + `data` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`userId`), UNIQUE KEY `userId_name` (`userId`,`name`), CONSTRAINT `FK_acc_cookies` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -121,7 +121,7 @@ CREATE TABLE `aowow_account_excludes` ( UNIQUE KEY `userId_type_typeId` (`userId`,`type`,`typeId`), KEY `userId` (`userId`), CONSTRAINT `FK_acc_excludes` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -140,7 +140,7 @@ CREATE TABLE `aowow_account_profiles` ( KEY `profileId` (`profileId`), CONSTRAINT `FK_account_id` FOREIGN KEY (`accountId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `FK_profile_id` FOREIGN KEY (`profileId`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -160,7 +160,7 @@ CREATE TABLE `aowow_account_reputation` ( UNIQUE KEY `userId_action_source` (`userId`,`action`,`sourceA`,`sourceB`), KEY `userId` (`userId`), CONSTRAINT `FK_acc_rep` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='reputation log'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT COMMENT='reputation log'; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -172,11 +172,11 @@ DROP TABLE IF EXISTS `aowow_account_weightscale_data`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_account_weightscale_data` ( `id` int(32) NOT NULL, - `field` varchar(15) NOT NULL, + `field` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL, `val` smallint(6) unsigned NOT NULL, KEY `id` (`id`), CONSTRAINT `FK_acc_weightscales` FOREIGN KEY (`id`) REFERENCES `aowow_account_weightscales` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -189,13 +189,13 @@ DROP TABLE IF EXISTS `aowow_account_weightscales`; CREATE TABLE `aowow_account_weightscales` ( `id` int(32) NOT NULL AUTO_INCREMENT, `userId` int(10) unsigned NOT NULL, - `name` varchar(32) NOT NULL, + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, `class` tinyint(3) unsigned NOT NULL DEFAULT 0, - `icon` varchar(48) NOT NULL DEFAULT '', + `icon` varchar(48) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`,`userId`), KEY `FK_acc_weights` (`userId`), CONSTRAINT `FK_acc_weights` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -222,24 +222,24 @@ CREATE TABLE `aowow_achievement` ( `refAchievement` smallint(5) unsigned NOT NULL, `itemExtra` mediumint(8) unsigned NOT NULL, `cuFlags` int(10) unsigned NOT NULL COMMENT 'see defines.php for flags', - `name_loc0` varchar(78) NOT NULL, - `name_loc2` varchar(79) NOT NULL, - `name_loc3` varchar(86) NOT NULL, - `name_loc6` varchar(78) NOT NULL, - `name_loc8` varchar(76) NOT NULL, - `description_loc0` text NOT NULL, - `description_loc2` text NOT NULL, - `description_loc3` text NOT NULL, - `description_loc6` text NOT NULL, - `description_loc8` text NOT NULL, - `reward_loc0` varchar(74) NOT NULL, - `reward_loc2` varchar(88) NOT NULL, - `reward_loc3` varchar(92) NOT NULL, - `reward_loc6` varchar(83) NOT NULL, - `reward_loc8` varchar(95) NOT NULL, + `name_loc0` varchar(78) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(79) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(86) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(78) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(76) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `reward_loc0` varchar(74) COLLATE utf8mb4_unicode_ci NOT NULL, + `reward_loc2` varchar(88) COLLATE utf8mb4_unicode_ci NOT NULL, + `reward_loc3` varchar(92) COLLATE utf8mb4_unicode_ci NOT NULL, + `reward_loc6` varchar(83) COLLATE utf8mb4_unicode_ci NOT NULL, + `reward_loc8` varchar(95) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -252,14 +252,14 @@ DROP TABLE IF EXISTS `aowow_achievementcategory`; CREATE TABLE `aowow_achievementcategory` ( `id` int(11) unsigned NOT NULL, `parentCategory` mediumint(9) NOT NULL, - `name_loc0` varchar(255) NOT NULL, - `name_loc2` varchar(255) NOT NULL, - `name_loc3` varchar(255) NOT NULL, - `name_loc6` varchar(255) NOT NULL, - `name_loc8` varchar(255) NOT NULL, + `name_loc0` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `idx_achievement` (`parentCategory`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -279,18 +279,18 @@ CREATE TABLE `aowow_achievementcriteria` ( `value4` int(10) unsigned NOT NULL, `value5` int(10) unsigned NOT NULL, `value6` int(10) unsigned NOT NULL, - `name_loc0` varchar(92) NOT NULL, - `name_loc2` varchar(104) NOT NULL, - `name_loc3` varchar(128) NOT NULL, - `name_loc6` varchar(119) NOT NULL, - `name_loc8` varchar(118) NOT NULL, + `name_loc0` varchar(92) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(104) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(119) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(118) COLLATE utf8mb4_unicode_ci NOT NULL, `completionFlags` tinyint(3) unsigned NOT NULL, `groupFlags` tinyint(3) unsigned NOT NULL, `timeLimit` smallint(5) unsigned NOT NULL, `order` smallint(5) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `idx_achievement` (`refAchievementId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -302,19 +302,19 @@ DROP TABLE IF EXISTS `aowow_announcements`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_announcements` ( `id` int(16) NOT NULL AUTO_INCREMENT COMMENT 'iirc negative Ids cant be deleted', - `page` varchar(256) NOT NULL, - `name` varchar(256) NOT NULL, + `page` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, `groupMask` smallint(5) unsigned NOT NULL, - `style` varchar(256) NOT NULL, + `style` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, `mode` tinyint(4) unsigned NOT NULL COMMENT '0:pageTop; 1:contentTop', `status` tinyint(4) unsigned NOT NULL COMMENT '0:disabled; 1:enabled; 2:deleted', - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, + `text_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -328,13 +328,13 @@ CREATE TABLE `aowow_articles` ( `type` smallint(5) DEFAULT NULL, `typeId` mediumint(9) DEFAULT NULL, `locale` tinyint(4) unsigned NOT NULL, - `url` varchar(50) DEFAULT NULL, + `url` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `editAccess` smallint(5) unsigned NOT NULL DEFAULT 2, - `article` text DEFAULT NULL COMMENT 'Markdown formated', - `quickInfo` text DEFAULT NULL COMMENT 'Markdown formated', + `article` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Markdown formated', + `quickInfo` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Markdown formated', UNIQUE KEY `type` (`type`,`typeId`,`locale`), UNIQUE KEY `locale_url` (`locale`,`url`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -346,23 +346,23 @@ DROP TABLE IF EXISTS `aowow_classes`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_classes` ( `id` int(16) NOT NULL, - `fileString` varchar(128) NOT NULL, - `name_loc0` varchar(128) NOT NULL, - `name_loc2` varchar(128) NOT NULL, - `name_loc3` varchar(128) NOT NULL, - `name_loc6` varchar(128) NOT NULL, - `name_loc8` varchar(128) NOT NULL, + `fileString` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc0` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, `powerType` tinyint(4) NOT NULL, `raceMask` int(16) NOT NULL, `roles` int(16) NOT NULL, - `skills` varchar(32) NOT NULL, + `skills` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, `flags` mediumint(16) NOT NULL, `cuFlags` int(10) unsigned NOT NULL, `weaponTypeMask` int(32) NOT NULL, `armorTypeMask` int(32) NOT NULL, `expansion` tinyint(2) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -378,7 +378,7 @@ CREATE TABLE `aowow_comments` ( `typeId` mediumint(9) NOT NULL COMMENT 'ID Of Page', `userId` int(10) unsigned DEFAULT NULL COMMENT 'User ID', `roles` smallint(5) unsigned NOT NULL, - `body` text NOT NULL COMMENT 'Comment text', + `body` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Comment text', `date` int(11) NOT NULL COMMENT 'Comment timestap', `flags` smallint(6) NOT NULL DEFAULT 0 COMMENT 'deleted, outofdate, sticky', `replyTo` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Reply To, comment ID', @@ -388,13 +388,13 @@ CREATE TABLE `aowow_comments` ( `deleteUserId` int(10) unsigned NOT NULL DEFAULT 0, `deleteDate` int(10) unsigned NOT NULL DEFAULT 0, `responseUserId` int(10) unsigned NOT NULL DEFAULT 0, - `responseBody` text DEFAULT NULL, + `responseBody` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, `responseRoles` smallint(5) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `type_typeId` (`type`,`typeId`), KEY `FK_acc_co` (`userId`), CONSTRAINT `FK_acc_co` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -413,7 +413,7 @@ CREATE TABLE `aowow_comments_rates` ( KEY `FK_acc_co_rate_user` (`userId`), CONSTRAINT `FK_acc_co_rate` FOREIGN KEY (`commentId`) REFERENCES `aowow_comments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `FK_acc_co_rate_user` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -424,13 +424,13 @@ DROP TABLE IF EXISTS `aowow_config`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_config` ( - `key` varchar(25) NOT NULL, - `value` varchar(255) NOT NULL, + `key` varchar(25) COLLATE utf8mb4_unicode_ci NOT NULL, + `value` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `cat` tinyint(3) unsigned NOT NULL DEFAULT 5, `flags` tinyint(3) unsigned NOT NULL DEFAULT 0, - `comment` varchar(255) NOT NULL, + `comment` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`key`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -452,20 +452,20 @@ CREATE TABLE `aowow_creature` ( `displayId2` mediumint(8) unsigned NOT NULL DEFAULT 0, `displayId3` mediumint(8) unsigned NOT NULL DEFAULT 0, `displayId4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `textureString` varchar(50) DEFAULT NULL, + `textureString` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `modelId` mediumint(8) NOT NULL, `humanoid` tinyint(1) unsigned NOT NULL DEFAULT 0, - `iconString` varchar(50) DEFAULT NULL COMMENT 'first texture of first model for search (up to 11 other skins omitted..)', - `name_loc0` varchar(100) NOT NULL DEFAULT '0', - `name_loc2` varchar(100) DEFAULT NULL, - `name_loc3` varchar(100) DEFAULT NULL, - `name_loc6` varchar(100) DEFAULT NULL, - `name_loc8` varchar(100) DEFAULT NULL, - `subname_loc0` varchar(100) DEFAULT NULL, - `subname_loc2` varchar(100) DEFAULT NULL, - `subname_loc3` varchar(100) DEFAULT NULL, - `subname_loc6` varchar(100) DEFAULT NULL, - `subname_loc8` varchar(100) DEFAULT NULL, + `iconString` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'first texture of first model for search (up to 11 other skins omitted..)', + `name_loc0` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0', + `name_loc2` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc3` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc6` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc8` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subname_loc0` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subname_loc2` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subname_loc3` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subname_loc6` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subname_loc8` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `minLevel` tinyint(3) unsigned NOT NULL DEFAULT 1, `maxLevel` tinyint(3) unsigned NOT NULL DEFAULT 1, `exp` smallint(6) NOT NULL DEFAULT 0, @@ -510,7 +510,7 @@ CREATE TABLE `aowow_creature` ( `vehicleId` mediumint(8) unsigned NOT NULL DEFAULT 0, `minGold` mediumint(8) unsigned NOT NULL DEFAULT 0, `maxGold` mediumint(8) unsigned NOT NULL DEFAULT 0, - `aiName` varchar(50) NOT NULL DEFAULT '', + `aiName` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `healthMin` int(10) unsigned NOT NULL DEFAULT 1, `healthMax` int(10) unsigned NOT NULL DEFAULT 1, `manaMin` int(10) unsigned NOT NULL DEFAULT 1, @@ -520,13 +520,13 @@ CREATE TABLE `aowow_creature` ( `racialLeader` tinyint(3) unsigned NOT NULL DEFAULT 0, `mechanicImmuneMask` int(10) unsigned NOT NULL DEFAULT 0, `flagsExtra` int(10) unsigned NOT NULL DEFAULT 0, - `scriptName` varchar(50) NOT NULL DEFAULT '', + `scriptName` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `idx_name` (`name_loc0`), KEY `difficultyEntry1` (`difficultyEntry1`), KEY `difficultyEntry2` (`difficultyEntry2`), KEY `difficultyEntry3` (`difficultyEntry3`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -568,7 +568,7 @@ CREATE TABLE `aowow_creature_sounds` ( `transform` smallint(5) unsigned NOT NULL, `transformanimated` smallint(5) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a creatureId, but displayId\r\n\r\ncolumn names from LANG.sound_activities'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a creatureId, but displayId\r\n\r\ncolumn names from LANG.sound_activities'; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -580,14 +580,14 @@ DROP TABLE IF EXISTS `aowow_creature_waypoints`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_creature_waypoints` ( `creatureOrPath` int(11) NOT NULL, - `point` tinyint(3) unsigned NOT NULL, + `point` smallint(5) unsigned NOT NULL, `areaId` smallint(5) unsigned NOT NULL, `floor` tinyint(3) unsigned NOT NULL, `posX` float unsigned NOT NULL, `posY` float unsigned NOT NULL, `wait` mediumint(8) unsigned NOT NULL, PRIMARY KEY (`creatureOrPath`,`point`,`areaId`,`floor`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -604,19 +604,19 @@ CREATE TABLE `aowow_currencies` ( `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, `itemId` int(16) NOT NULL, `cap` mediumint(8) unsigned NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, - `description_loc0` varchar(256) NOT NULL, - `description_loc2` varchar(256) NOT NULL, - `description_loc3` varchar(256) NOT NULL, - `description_loc6` varchar(256) NOT NULL, - `description_loc8` varchar(256) NOT NULL, + `name_loc0` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc0` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc2` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc3` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc6` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc8` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -629,9 +629,9 @@ DROP TABLE IF EXISTS `aowow_dbversion`; CREATE TABLE `aowow_dbversion` ( `date` int(10) unsigned NOT NULL DEFAULT 0, `part` tinyint(3) unsigned NOT NULL DEFAULT 0, - `sql` text DEFAULT NULL, - `build` text DEFAULT NULL -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + `sql` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `build` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -643,26 +643,26 @@ DROP TABLE IF EXISTS `aowow_emotes`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_emotes` ( `id` smallint(5) unsigned NOT NULL, - `cmd` varchar(15) NOT NULL, + `cmd` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL, `isAnimated` tinyint(1) unsigned NOT NULL, `cuFlags` int(10) unsigned NOT NULL, - `target_loc0` varchar(65) DEFAULT NULL, - `target_loc2` varchar(70) DEFAULT NULL, - `target_loc3` varchar(95) DEFAULT NULL, - `target_loc6` varchar(90) DEFAULT NULL, - `target_loc8` varchar(70) DEFAULT NULL, - `noTarget_loc0` varchar(65) DEFAULT NULL, - `noTarget_loc2` varchar(110) DEFAULT NULL, - `noTarget_loc3` varchar(85) DEFAULT NULL, - `noTarget_loc6` varchar(75) DEFAULT NULL, - `noTarget_loc8` varchar(60) DEFAULT NULL, - `self_loc0` varchar(65) DEFAULT NULL, - `self_loc2` varchar(115) DEFAULT NULL, - `self_loc3` varchar(85) DEFAULT NULL, - `self_loc6` varchar(75) DEFAULT NULL, - `self_loc8` varchar(70) DEFAULT NULL, + `target_loc0` varchar(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `target_loc2` varchar(70) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `target_loc3` varchar(95) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `target_loc6` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `target_loc8` varchar(70) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `noTarget_loc0` varchar(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `noTarget_loc2` varchar(110) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `noTarget_loc3` varchar(85) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `noTarget_loc6` varchar(75) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `noTarget_loc8` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `self_loc0` varchar(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `self_loc2` varchar(115) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `self_loc3` varchar(85) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `self_loc6` varchar(75) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `self_loc8` varchar(70) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -675,10 +675,10 @@ DROP TABLE IF EXISTS `aowow_emotes_aliasses`; CREATE TABLE `aowow_emotes_aliasses` ( `id` smallint(6) unsigned NOT NULL, `locales` smallint(6) unsigned NOT NULL, - `command` varchar(15) NOT NULL, + `command` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL, UNIQUE KEY `id_command` (`id`,`command`), KEY `id` (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -697,7 +697,7 @@ CREATE TABLE `aowow_emotes_sounds` ( KEY `emoteId` (`emoteId`), KEY `raceId` (`raceId`), KEY `soundId` (`soundId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -709,15 +709,15 @@ DROP TABLE IF EXISTS `aowow_errors`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_errors` ( `date` int(10) unsigned DEFAULT NULL, - `version` smallint(5) unsigned NOT NULL, + `version` tinyint(3) unsigned NOT NULL, `phpError` smallint(5) unsigned NOT NULL, - `file` varchar(250) NOT NULL, + `file` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL, `line` smallint(5) unsigned NOT NULL, - `query` varchar(250) NOT NULL, + `query` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, `userGroups` smallint(5) unsigned NOT NULL, - `message` text DEFAULT NULL, + `message` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`file`,`line`,`phpError`,`version`,`userGroups`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -735,11 +735,11 @@ CREATE TABLE `aowow_events` ( `endTime` bigint(20) NOT NULL, `occurence` bigint(20) unsigned NOT NULL, `length` bigint(20) unsigned NOT NULL, - `requires` varchar(255) DEFAULT NULL, - `description` varchar(255) DEFAULT NULL, + `requires` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `holidayId` (`holidayId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -754,20 +754,20 @@ CREATE TABLE `aowow_factions` ( `repIdx` smallint(5) unsigned NOT NULL, `side` tinyint(1) unsigned NOT NULL, `expansion` tinyint(1) unsigned NOT NULL, - `qmNpcIds` varchar(12) NOT NULL COMMENT 'space separated', - `templateIds` tinytext NOT NULL COMMENT 'space separated', + `qmNpcIds` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'space separated', + `templateIds` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'space separated', `cuFlags` int(10) unsigned NOT NULL, `parentFactionId` smallint(5) unsigned NOT NULL, `spilloverRateIn` float(8,2) NOT NULL, `spilloverRateOut` float(8,2) NOT NULL, `spilloverMaxRank` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(35) NOT NULL, - `name_loc2` varchar(49) NOT NULL, - `name_loc3` varchar(40) NOT NULL, - `name_loc6` varchar(50) NOT NULL, - `name_loc8` varchar(47) NOT NULL, + `name_loc0` varchar(35) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(49) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(47) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -783,7 +783,7 @@ CREATE TABLE `aowow_factiontemplate` ( `A` tinyint(4) NOT NULL COMMENT 'Aliance: -1 - hostile, 1 - friendly, 0 - neutral', `H` tinyint(4) NOT NULL COMMENT 'Horde: -1 - hostile, 1 - friendly, 0 - neutral', PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -800,7 +800,7 @@ CREATE TABLE `aowow_glyphproperties` ( `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -814,22 +814,22 @@ CREATE TABLE `aowow_holidays` ( `id` smallint(6) unsigned NOT NULL, `bossCreature` mediumint(8) unsigned NOT NULL, `achievementCatOrId` mediumint(9) NOT NULL, - `name_loc0` varchar(36) NOT NULL, - `name_loc2` varchar(42) NOT NULL, - `name_loc3` varchar(36) NOT NULL, - `name_loc6` varchar(49) NOT NULL, - `name_loc8` varchar(29) NOT NULL, - `description_loc0` text DEFAULT NULL, - `description_loc2` text DEFAULT NULL, - `description_loc3` text DEFAULT NULL, - `description_loc6` text DEFAULT NULL, - `description_loc8` text DEFAULT NULL, + `name_loc0` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(42) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(49) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(29) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, `looping` tinyint(2) NOT NULL, `scheduleType` tinyint(2) NOT NULL, - `textureString` varchar(30) NOT NULL, - `iconString` varchar(51) NOT NULL, + `textureString` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL, + `iconString` varchar(51) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -846,18 +846,18 @@ CREATE TABLE `aowow_home_featuredbox` ( `startDate` int(10) unsigned NOT NULL DEFAULT 0, `endDate` int(10) unsigned NOT NULL DEFAULT 0, `extraWide` tinyint(3) unsigned NOT NULL DEFAULT 0, - `boxBG` varchar(150) DEFAULT NULL, - `altHomeLogo` varchar(150) DEFAULT NULL, - `altHeaderLogo` varchar(150) DEFAULT NULL, - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, + `boxBG` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `altHomeLogo` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `altHeaderLogo` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `text_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `FK_acc_hFBox` (`editorId`), CONSTRAINT `FK_acc_hFBox` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -871,15 +871,15 @@ CREATE TABLE `aowow_home_featuredbox_overlay` ( `featureId` smallint(5) unsigned NOT NULL, `left` smallint(5) unsigned NOT NULL, `width` smallint(5) unsigned NOT NULL, - `url` varchar(150) NOT NULL, - `title_loc0` varchar(100) NOT NULL DEFAULT '', - `title_loc2` varchar(100) NOT NULL DEFAULT '', - `title_loc3` varchar(100) NOT NULL DEFAULT '', - `title_loc6` varchar(100) NOT NULL DEFAULT '', - `title_loc8` varchar(100) NOT NULL DEFAULT '', + `url` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL, + `title_loc0` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `title_loc2` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `title_loc3` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `title_loc6` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `title_loc8` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', KEY `FK_home_featurebox` (`featureId`), CONSTRAINT `FK_home_featurebox` FOREIGN KEY (`featureId`) REFERENCES `aowow_home_featuredbox` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -894,15 +894,15 @@ CREATE TABLE `aowow_home_oneliner` ( `editorId` int(10) unsigned DEFAULT NULL, `editDate` int(10) unsigned NOT NULL, `active` tinyint(1) unsigned NOT NULL, - `text_loc0` varchar(200) NOT NULL, - `text_loc2` varchar(200) NOT NULL, - `text_loc3` varchar(200) NOT NULL, - `text_loc6` varchar(200) NOT NULL, - `text_loc8` varchar(200) NOT NULL, + `text_loc0` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc2` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc3` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc6` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc8` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `FK_acc_hOneliner` (`editorId`), CONSTRAINT `FK_acc_hOneliner` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -917,15 +917,15 @@ CREATE TABLE `aowow_home_titles` ( `editorId` int(10) unsigned DEFAULT NULL, `editDate` int(10) unsigned NOT NULL, `active` tinyint(1) unsigned NOT NULL, - `title_loc0` varchar(100) NOT NULL, - `title_loc2` varchar(100) NOT NULL, - `title_loc3` varchar(100) NOT NULL, - `title_loc6` varchar(100) NOT NULL, - `title_loc8` varchar(100) NOT NULL, + `title_loc0` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `title_loc2` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `title_loc3` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `title_loc6` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `title_loc8` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `FK_acc_hTitles` (`editorId`), CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -938,10 +938,10 @@ DROP TABLE IF EXISTS `aowow_icons`; CREATE TABLE `aowow_icons` ( `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, `cuFlags` int(11) unsigned NOT NULL DEFAULT 0, - `name` varchar(55) NOT NULL DEFAULT '', + `name` varchar(55) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `name` (`name`) -) ENGINE=InnoDB AUTO_INCREMENT=5856 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1034,7 +1034,7 @@ CREATE TABLE `aowow_item_stats` ( `natsplpwr` smallint(6) NOT NULL, `arcsplpwr` smallint(6) NOT NULL, PRIMARY KEY (`typeId`,`type`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1059,17 +1059,17 @@ CREATE TABLE `aowow_itemenchantment` ( `object1` mediumint(9) unsigned NOT NULL, `object2` mediumint(9) unsigned NOT NULL, `object3` smallint(6) unsigned NOT NULL, - `name_loc0` varchar(65) NOT NULL, - `name_loc2` varchar(91) NOT NULL, - `name_loc3` varchar(84) NOT NULL, - `name_loc6` varchar(89) NOT NULL, - `name_loc8` varchar(96) NOT NULL, + `name_loc0` varchar(65) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(91) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(84) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(89) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(96) COLLATE utf8mb4_unicode_ci NOT NULL, `conditionId` tinyint(3) unsigned NOT NULL, `skillLine` smallint(5) unsigned NOT NULL, `skillLevel` smallint(5) unsigned NOT NULL, `requiredLevel` tinyint(3) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1102,7 +1102,7 @@ CREATE TABLE `aowow_itemenchantmentcondition` ( `value4` tinyint(4) unsigned zerofill NOT NULL, `value5` tinyint(4) unsigned zerofill NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1129,7 +1129,7 @@ CREATE TABLE `aowow_itemextendedcost` ( `itemCount5` smallint(5) unsigned NOT NULL, `reqPersonalRating` smallint(5) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1141,15 +1141,15 @@ DROP TABLE IF EXISTS `aowow_itemlimitcategory`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_itemlimitcategory` ( `id` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(31) NOT NULL, - `name_loc2` varchar(36) NOT NULL, - `name_loc3` varchar(34) NOT NULL, - `name_loc6` varchar(40) NOT NULL, - `name_loc8` varchar(35) NOT NULL, + `name_loc0` varchar(31) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(34) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(35) COLLATE utf8mb4_unicode_ci NOT NULL, `count` tinyint(3) unsigned NOT NULL, `isGem` tinyint(3) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1161,12 +1161,12 @@ DROP TABLE IF EXISTS `aowow_itemrandomenchant`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_itemrandomenchant` ( `id` smallint(6) NOT NULL, - `name_loc0` varchar(250) NOT NULL, - `name_loc2` varchar(250) NOT NULL, - `name_loc3` varchar(250) NOT NULL, - `name_loc6` varchar(250) NOT NULL, - `name_loc8` varchar(250) NOT NULL, - `nameINT` char(250) NOT NULL, + `name_loc0` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, + `nameINT` char(250) COLLATE utf8mb4_unicode_ci NOT NULL, `enchantId1` smallint(5) unsigned NOT NULL, `enchantId2` smallint(5) unsigned NOT NULL, `enchantId3` smallint(5) unsigned NOT NULL, @@ -1178,7 +1178,7 @@ CREATE TABLE `aowow_itemrandomenchant` ( `allocationPct4` smallint(5) unsigned NOT NULL, `allocationPct5` smallint(5) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1206,7 +1206,7 @@ CREATE TABLE `aowow_itemrandomproppoints` ( `uncommon4` smallint(5) unsigned NOT NULL, `uncommon5` smallint(5) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1224,11 +1224,11 @@ CREATE TABLE `aowow_items` ( `subClassBak` tinyint(3) NOT NULL, `soundOverrideSubclass` tinyint(3) NOT NULL, `subSubClass` tinyint(3) NOT NULL, - `name_loc0` varchar(127) NOT NULL DEFAULT '', - `name_loc2` varchar(127) DEFAULT NULL, - `name_loc3` varchar(127) DEFAULT NULL, - `name_loc6` varchar(127) DEFAULT NULL, - `name_loc8` varchar(127) DEFAULT NULL, + `name_loc0` varchar(127) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `name_loc2` varchar(127) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc3` varchar(127) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc6` varchar(127) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc8` varchar(127) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, `displayId` mediumint(8) unsigned NOT NULL DEFAULT 0, `spellVisualId` smallint(5) unsigned NOT NULL DEFAULT 0, @@ -1254,7 +1254,7 @@ CREATE TABLE `aowow_items` ( `requiredFactionRank` smallint(5) unsigned NOT NULL DEFAULT 0, `maxCount` int(11) NOT NULL DEFAULT 0, `cuFlags` int(10) unsigned NOT NULL, - `model` varchar(50) NOT NULL, + `model` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, `stackable` int(11) DEFAULT 1, `slots` tinyint(3) unsigned NOT NULL DEFAULT 0, `statType1` tinyint(3) unsigned NOT NULL DEFAULT 0, @@ -1333,11 +1333,11 @@ CREATE TABLE `aowow_items` ( `spellCategory5` smallint(5) unsigned NOT NULL DEFAULT 0, `spellCategoryCooldown5` int(11) NOT NULL DEFAULT -1, `bonding` tinyint(3) unsigned NOT NULL DEFAULT 0, - `description_loc0` varchar(255) NOT NULL DEFAULT '', - `description_loc2` varchar(255) DEFAULT NULL, - `description_loc3` varchar(255) DEFAULT NULL, - `description_loc6` varchar(255) DEFAULT NULL, - `description_loc8` varchar(255) DEFAULT NULL, + `description_loc0` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `description_loc2` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc3` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc6` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_loc8` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `pageTextId` mediumint(8) unsigned NOT NULL DEFAULT 0, `languageId` tinyint(3) unsigned NOT NULL DEFAULT 0, `startQuest` mediumint(8) unsigned NOT NULL DEFAULT 0, @@ -1363,7 +1363,7 @@ CREATE TABLE `aowow_items` ( `duration` int(10) unsigned NOT NULL DEFAULT 0, `itemLimitCategory` smallint(6) NOT NULL DEFAULT 0, `eventId` smallint(5) unsigned NOT NULL, - `scriptName` varchar(64) NOT NULL DEFAULT '', + `scriptName` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `foodType` tinyint(3) unsigned NOT NULL DEFAULT 0, `gemEnchantmentId` mediumint(8) NOT NULL, `minMoneyLoot` int(10) unsigned NOT NULL DEFAULT 0, @@ -1379,7 +1379,7 @@ CREATE TABLE `aowow_items` ( KEY `idx_model` (`displayId`), KEY `idx_faction` (`requiredFaction`), KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1393,7 +1393,7 @@ CREATE TABLE `aowow_items_sounds` ( `soundId` smallint(5) unsigned NOT NULL, `subClassMask` mediumint(8) unsigned NOT NULL, PRIMARY KEY (`soundId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='actually .. its only weapon related sounds in here'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='actually .. its only weapon related sounds in here'; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1407,11 +1407,11 @@ CREATE TABLE `aowow_itemset` ( `id` int(16) NOT NULL, `refSetId` int(11) NOT NULL, `cuFlags` int(10) unsigned NOT NULL, - `name_loc0` varchar(255) NOT NULL, - `name_loc2` varchar(255) NOT NULL, - `name_loc3` varchar(255) NOT NULL, - `name_loc6` varchar(255) NOT NULL, - `name_loc8` varchar(255) NOT NULL, + `name_loc0` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `item1` mediumint(11) unsigned NOT NULL, `item2` mediumint(11) unsigned NOT NULL, `item3` mediumint(11) unsigned NOT NULL, @@ -1438,12 +1438,12 @@ CREATE TABLE `aowow_itemset` ( `bonus6` tinyint(1) unsigned NOT NULL, `bonus7` tinyint(1) unsigned NOT NULL, `bonus8` tinyint(1) unsigned NOT NULL, - `bonusText_loc0` varchar(256) NOT NULL, - `bonusText_loc2` varchar(256) NOT NULL, - `bonusText_loc3` varchar(256) NOT NULL, - `bonusText_loc6` varchar(256) NOT NULL, - `bonusText_loc8` varchar(256) NOT NULL, - `bonusParsed` varchar(256) NOT NULL COMMENT 'serialized itemMods', + `bonusText_loc0` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `bonusText_loc2` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `bonusText_loc3` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `bonusText_loc6` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `bonusText_loc8` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `bonusParsed` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'serialized itemMods', `npieces` tinyint(3) NOT NULL, `minLevel` smallint(6) NOT NULL, `maxLevel` smallint(6) NOT NULL, @@ -1457,7 +1457,7 @@ CREATE TABLE `aowow_itemset` ( `skillId` smallint(3) unsigned NOT NULL, `skillLevel` smallint(3) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1485,7 +1485,7 @@ CREATE TABLE `aowow_lock` ( `reqSkill4` mediumint(8) unsigned NOT NULL, `reqSkill5` mediumint(8) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1500,7 +1500,7 @@ CREATE TABLE `aowow_loot_link` ( `objectId` mediumint(8) unsigned NOT NULL, UNIQUE KEY `npcId` (`npcId`), KEY `objectId` (`objectId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1512,18 +1512,18 @@ DROP TABLE IF EXISTS `aowow_mailtemplate`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_mailtemplate` ( `id` smallint(5) unsigned NOT NULL, - `subject_loc0` varchar(128) NOT NULL, - `subject_loc2` varchar(128) NOT NULL, - `subject_loc3` varchar(128) NOT NULL, - `subject_loc6` varchar(128) NOT NULL, - `subject_loc8` varchar(128) NOT NULL, - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, + `subject_loc0` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc2` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc3` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc6` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc8` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1539,11 +1539,11 @@ CREATE TABLE `aowow_objects` ( `typeCat` tinyint(3) NOT NULL DEFAULT 0, `event` smallint(5) unsigned NOT NULL DEFAULT 0, `displayId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `name_loc0` varchar(100) DEFAULT NULL, - `name_loc2` varchar(100) DEFAULT NULL, - `name_loc3` varchar(100) DEFAULT NULL, - `name_loc6` varchar(100) DEFAULT NULL, - `name_loc8` varchar(100) DEFAULT NULL, + `name_loc0` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc2` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc3` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc6` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc8` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `faction` smallint(5) unsigned NOT NULL DEFAULT 0, `flags` int(10) unsigned NOT NULL DEFAULT 0, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, @@ -1558,11 +1558,11 @@ CREATE TABLE `aowow_objects` ( `onSuccessSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, `auraSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, `triggeredSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, - `miscInfo` varchar(128) NOT NULL, - `ScriptOrAI` varchar(64) NOT NULL, + `miscInfo` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `ScriptOrAI` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name_loc0`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1582,11 +1582,11 @@ CREATE TABLE `aowow_pet` ( `type` tinyint(4) NOT NULL, `exotic` tinyint(4) NOT NULL, `expansion` tinyint(4) NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, + `name_loc0` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, `skillLineId` mediumint(9) NOT NULL, `spellId1` mediumint(9) NOT NULL, @@ -1598,7 +1598,7 @@ CREATE TABLE `aowow_pet` ( `health` mediumint(9) NOT NULL, PRIMARY KEY (`id`), KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1612,8 +1612,8 @@ CREATE TABLE `aowow_profiler_arena_team` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `realm` tinyint(3) unsigned NOT NULL, `realmGUID` int(10) unsigned NOT NULL, - `name` varchar(24) NOT NULL, - `nameUrl` varchar(24) NOT NULL, + `name` varchar(24) COLLATE utf8mb4_unicode_ci NOT NULL, + `nameUrl` varchar(24) COLLATE utf8mb4_unicode_ci NOT NULL, `type` tinyint(3) unsigned NOT NULL DEFAULT 0, `cuFlags` int(11) unsigned NOT NULL, `rating` smallint(5) unsigned NOT NULL DEFAULT 0, @@ -1630,7 +1630,7 @@ CREATE TABLE `aowow_profiler_arena_team` ( PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1653,7 +1653,7 @@ CREATE TABLE `aowow_profiler_arena_team_member` ( KEY `guid` (`profileId`), CONSTRAINT `FK_aowow_profiler_arena_team_member_aowow_profiler_arena_team` FOREIGN KEY (`arenaTeamId`) REFERENCES `aowow_profiler_arena_team` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `FK_aowow_profiler_arena_team_member_aowow_profiler_profiles` FOREIGN KEY (`profileId`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1673,7 +1673,7 @@ CREATE TABLE `aowow_profiler_completion` ( KEY `type` (`type`), KEY `typeId` (`typeId`), CONSTRAINT `FK_pr_completion` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1687,9 +1687,9 @@ CREATE TABLE `aowow_profiler_excludes` ( `type` smallint(5) unsigned NOT NULL, `typeId` mediumint(8) unsigned NOT NULL, `groups` smallint(5) unsigned NOT NULL COMMENT 'see exclude group defines', - `comment` varchar(50) NOT NULL COMMENT 'rebuilding profiler files will delete everything without a comment', + `comment` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'rebuilding profiler files will delete everything without a comment', PRIMARY KEY (`type`,`typeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1704,19 +1704,19 @@ CREATE TABLE `aowow_profiler_guild` ( `realm` int(10) unsigned NOT NULL, `realmGUID` int(10) unsigned NOT NULL, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, - `name` varchar(26) NOT NULL, - `nameUrl` varchar(26) NOT NULL, + `name` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `nameUrl` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL, `emblemStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, `emblemColor` tinyint(3) unsigned NOT NULL DEFAULT 0, `borderStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, `borderColor` tinyint(3) unsigned NOT NULL DEFAULT 0, `backgroundColor` tinyint(3) unsigned NOT NULL DEFAULT 0, - `info` varchar(500) NOT NULL DEFAULT '', + `info` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `createDate` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1729,11 +1729,11 @@ DROP TABLE IF EXISTS `aowow_profiler_guild_rank`; CREATE TABLE `aowow_profiler_guild_rank` ( `guildId` int(10) unsigned NOT NULL DEFAULT 0, `rank` tinyint(3) unsigned NOT NULL, - `name` varchar(20) NOT NULL DEFAULT '', + `name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`guildId`,`rank`), KEY `rank` (`rank`), CONSTRAINT `FK_aowow_profiler_guild_rank_aowow_profiler_guild` FOREIGN KEY (`guildId`) REFERENCES `aowow_profiler_guild` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1759,7 +1759,7 @@ CREATE TABLE `aowow_profiler_items` ( KEY `id` (`id`), KEY `item` (`item`), CONSTRAINT `FK_pr_items` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1772,15 +1772,15 @@ DROP TABLE IF EXISTS `aowow_profiler_pets`; CREATE TABLE `aowow_profiler_pets` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `owner` int(10) unsigned DEFAULT NULL, - `name` varchar(50) DEFAULT NULL, + `name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `family` tinyint(3) unsigned DEFAULT NULL, `npc` smallint(5) unsigned DEFAULT NULL, `displayId` smallint(5) unsigned DEFAULT NULL, - `talents` varchar(20) DEFAULT NULL, + `talents` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `owner` (`owner`), CONSTRAINT `FK_pr_pets` FOREIGN KEY (`owner`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1796,11 +1796,11 @@ CREATE TABLE `aowow_profiler_profiles` ( `realmGUID` int(11) unsigned DEFAULT NULL, `cuFlags` int(11) unsigned NOT NULL DEFAULT 0, `sourceId` int(11) unsigned DEFAULT NULL, - `sourceName` varchar(50) DEFAULT NULL, + `sourceName` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `copy` int(10) unsigned DEFAULT NULL, - `icon` varchar(50) DEFAULT NULL, + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `user` int(11) unsigned DEFAULT NULL, - `name` varchar(50) NOT NULL, + `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, `race` tinyint(3) unsigned NOT NULL, `class` tinyint(3) unsigned NOT NULL, `level` tinyint(3) unsigned NOT NULL, @@ -1814,7 +1814,7 @@ CREATE TABLE `aowow_profiler_profiles` ( `features` tinyint(3) unsigned NOT NULL, `nomodelMask` int(11) unsigned NOT NULL DEFAULT 0, `title` tinyint(3) unsigned NOT NULL, - `description` text DEFAULT NULL, + `description` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, `playedtime` int(11) unsigned NOT NULL, `gearscore` smallint(5) unsigned NOT NULL, `achievementpoints` smallint(5) unsigned NOT NULL, @@ -1822,17 +1822,17 @@ CREATE TABLE `aowow_profiler_profiles` ( `talenttree1` tinyint(4) unsigned NOT NULL COMMENT 'points spend in 1st tree', `talenttree2` tinyint(4) unsigned NOT NULL COMMENT 'points spend in 2nd tree', `talenttree3` tinyint(4) unsigned NOT NULL COMMENT 'points spend in 3rd tree', - `talentbuild1` varchar(105) NOT NULL, - `talentbuild2` varchar(105) NOT NULL, - `glyphs1` varchar(45) NOT NULL, - `glyphs2` varchar(45) NOT NULL, + `talentbuild1` varchar(105) COLLATE utf8mb4_unicode_ci NOT NULL, + `talentbuild2` varchar(105) COLLATE utf8mb4_unicode_ci NOT NULL, + `glyphs1` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, + `glyphs2` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, `activespec` tinyint(1) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID_name` (`realm`,`realmGUID`,`name`), KEY `user` (`user`), KEY `guild` (`guild`), CONSTRAINT `FK_aowow_profiler_profiles_aowow_profiler_guild` FOREIGN KEY (`guild`) REFERENCES `aowow_profiler_guild` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1852,7 +1852,7 @@ CREATE TABLE `aowow_profiler_sync` ( `errorCode` tinyint(3) unsigned NOT NULL DEFAULT 0, UNIQUE KEY `realm_realmGUID_type_typeId` (`realm`,`realmGUID`,`type`), UNIQUE KEY `type_typeId` (`type`,`typeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1938,41 +1938,41 @@ CREATE TABLE `aowow_quests` ( `rewardFactionValue3` mediumint(8) NOT NULL DEFAULT 0, `rewardFactionValue4` mediumint(8) NOT NULL DEFAULT 0, `rewardFactionValue5` mediumint(8) NOT NULL DEFAULT 0, - `name_loc0` text DEFAULT NULL, - `name_loc2` text DEFAULT NULL, - `name_loc3` text DEFAULT NULL, - `name_loc6` text DEFAULT NULL, - `name_loc8` text DEFAULT NULL, - `objectives_loc0` text DEFAULT NULL, - `objectives_loc2` text DEFAULT NULL, - `objectives_loc3` text DEFAULT NULL, - `objectives_loc6` text DEFAULT NULL, - `objectives_loc8` text DEFAULT NULL, - `details_loc0` text DEFAULT NULL, - `details_loc2` text DEFAULT NULL, - `details_loc3` text DEFAULT NULL, - `details_loc6` text DEFAULT NULL, - `details_loc8` text DEFAULT NULL, - `end_loc0` text DEFAULT NULL, - `end_loc2` text DEFAULT NULL, - `end_loc3` text DEFAULT NULL, - `end_loc6` text DEFAULT NULL, - `end_loc8` text DEFAULT NULL, - `offerReward_loc0` text DEFAULT NULL, - `offerReward_loc2` text DEFAULT NULL, - `offerReward_loc3` text DEFAULT NULL, - `offerReward_loc6` text DEFAULT NULL, - `offerReward_loc8` text DEFAULT NULL, - `requestItems_loc0` text DEFAULT NULL, - `requestItems_loc2` text DEFAULT NULL, - `requestItems_loc3` text DEFAULT NULL, - `requestItems_loc6` text DEFAULT NULL, - `requestItems_loc8` text DEFAULT NULL, - `completed_loc0` text DEFAULT NULL, - `completed_loc2` text DEFAULT NULL, - `completed_loc3` text DEFAULT NULL, - `completed_loc6` text DEFAULT NULL, - `completed_loc8` text DEFAULT NULL, + `name_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectives_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectives_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectives_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectives_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectives_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `details_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `details_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `details_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `details_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `details_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `end_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `end_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `end_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `end_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `end_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `offerReward_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `offerReward_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `offerReward_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `offerReward_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `offerReward_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `requestItems_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `requestItems_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `requestItems_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `requestItems_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `requestItems_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `completed_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `completed_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `completed_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `completed_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `completed_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reqNpcOrGo1` mediumint(8) NOT NULL DEFAULT 0, `reqNpcOrGo2` mediumint(8) NOT NULL DEFAULT 0, `reqNpcOrGo3` mediumint(8) NOT NULL DEFAULT 0, @@ -2001,29 +2001,29 @@ CREATE TABLE `aowow_quests` ( `reqItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, `reqItemCount5` smallint(5) unsigned NOT NULL DEFAULT 0, `reqItemCount6` smallint(5) unsigned NOT NULL DEFAULT 0, - `objectiveText1_loc0` text DEFAULT NULL, - `objectiveText1_loc2` text DEFAULT NULL, - `objectiveText1_loc3` text DEFAULT NULL, - `objectiveText1_loc6` text DEFAULT NULL, - `objectiveText1_loc8` text DEFAULT NULL, - `objectiveText2_loc0` text DEFAULT NULL, - `objectiveText2_loc2` text DEFAULT NULL, - `objectiveText2_loc3` text DEFAULT NULL, - `objectiveText2_loc6` text DEFAULT NULL, - `objectiveText2_loc8` text DEFAULT NULL, - `objectiveText3_loc0` text DEFAULT NULL, - `objectiveText3_loc2` text DEFAULT NULL, - `objectiveText3_loc3` text DEFAULT NULL, - `objectiveText3_loc6` text DEFAULT NULL, - `objectiveText3_loc8` text DEFAULT NULL, - `objectiveText4_loc0` text DEFAULT NULL, - `objectiveText4_loc2` text DEFAULT NULL, - `objectiveText4_loc3` text DEFAULT NULL, - `objectiveText4_loc6` text DEFAULT NULL, - `objectiveText4_loc8` text DEFAULT NULL, + `objectiveText1_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText1_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText1_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText1_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText1_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText2_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText2_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText2_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText2_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText2_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText3_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText3_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText3_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText3_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText3_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText4_loc0` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText4_loc2` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText4_loc3` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText4_loc6` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `objectiveText4_loc8` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `nextQuestIdChain` (`nextQuestIdChain`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2040,7 +2040,7 @@ CREATE TABLE `aowow_quests_startend` ( `method` tinyint(4) unsigned NOT NULL COMMENT '&0x1: starts; &0x2:ends', `eventId` smallint(6) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`type`,`typeId`,`questId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2060,15 +2060,15 @@ CREATE TABLE `aowow_races` ( `leader` bigint(20) NOT NULL, `baseLanguage` bigint(20) NOT NULL, `side` int(3) NOT NULL, - `fileString` varchar(64) NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, + `fileString` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc0` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, `expansion` int(1) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2085,7 +2085,7 @@ CREATE TABLE `aowow_races_sounds` ( UNIQUE KEY `race_soundId_gender` (`raceId`,`soundId`,`gender`), KEY `race` (`raceId`), KEY `soundId` (`soundId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2103,16 +2103,16 @@ CREATE TABLE `aowow_reports` ( `mode` tinyint(3) unsigned NOT NULL, `reason` tinyint(3) unsigned NOT NULL, `subject` mediumint(9) NOT NULL DEFAULT 0, - `ip` varchar(50) NOT NULL, - `description` text NOT NULL, - `userAgent` varchar(255) NOT NULL, - `appName` varchar(32) NOT NULL, - `url` varchar(255) NOT NULL, - `relatedUrl` varchar(255) DEFAULT NULL, - `email` varchar(255) DEFAULT NULL, + `ip` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `userAgent` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `appName` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, + `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `relatedUrl` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `userId` (`userId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2146,7 +2146,7 @@ CREATE TABLE `aowow_scalingstatdistribution` ( `modifier10` smallint(5) unsigned NOT NULL, `maxLevel` tinyint(3) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2181,7 +2181,7 @@ CREATE TABLE `aowow_scalingstatvalues` ( `mailChestArmor` smallint(5) unsigned NOT NULL, `plateChestArmor` smallint(5) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2199,7 +2199,7 @@ CREATE TABLE `aowow_screenshots` ( `date` int(32) unsigned NOT NULL, `width` smallint(5) unsigned NOT NULL, `height` smallint(5) unsigned NOT NULL, - `caption` varchar(250) DEFAULT NULL, + `caption` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `status` tinyint(3) unsigned NOT NULL COMMENT 'see defines.php - CC_FLAG_*', `userIdApprove` int(10) unsigned DEFAULT NULL, `userIdDelete` int(10) unsigned DEFAULT NULL, @@ -2207,7 +2207,7 @@ CREATE TABLE `aowow_screenshots` ( KEY `type` (`type`,`typeId`), KEY `FK_acc_ss` (`userIdOwner`), CONSTRAINT `FK_acc_ss` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2231,9 +2231,9 @@ CREATE TABLE `aowow_shapeshiftforms` ( `spellId6` bigint(20) NOT NULL, `spellId7` bigint(20) NOT NULL, `spellId8` bigint(20) NOT NULL, - `comment` varchar(30) DEFAULT NULL, + `comment` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`Id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2248,23 +2248,23 @@ CREATE TABLE `aowow_skillline` ( `typeCat` tinyint(4) NOT NULL, `cuFlags` int(10) unsigned NOT NULL, `categoryId` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, - `description_loc0` text NOT NULL, - `description_loc2` text NOT NULL, - `description_loc3` text NOT NULL, - `description_loc6` text NOT NULL, - `description_loc8` text NOT NULL, + `name_loc0` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, `professionMask` smallint(5) unsigned NOT NULL, `recipeSubClass` tinyint(3) unsigned NOT NULL, - `specializations` varchar(30) NOT NULL COMMENT 'space-separated spellIds', + `specializations` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'space-separated spellIds', PRIMARY KEY (`Id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2277,7 +2277,7 @@ DROP TABLE IF EXISTS `aowow_sounds`; CREATE TABLE `aowow_sounds` ( `id` smallint(5) unsigned NOT NULL, `cat` tinyint(3) unsigned NOT NULL, - `name` varchar(100) NOT NULL, + `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `cuFlags` int(10) unsigned NOT NULL, `soundFile1` smallint(5) unsigned DEFAULT NULL, `soundFile2` smallint(5) unsigned DEFAULT NULL, @@ -2293,7 +2293,7 @@ CREATE TABLE `aowow_sounds` ( PRIMARY KEY (`id`), KEY `cat` (`cat`), KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2305,11 +2305,11 @@ DROP TABLE IF EXISTS `aowow_sounds_files`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_sounds_files` ( `id` smallint(6) NOT NULL COMMENT '<0 not found in client files', - `file` varchar(75) NOT NULL, - `path` varchar(75) NOT NULL COMMENT 'in client', + `file` varchar(75) COLLATE utf8mb4_unicode_ci NOT NULL, + `path` varchar(75) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'in client', `type` tinyint(1) unsigned NOT NULL COMMENT '1: ogg; 2: mp3', PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2350,7 +2350,7 @@ CREATE TABLE `aowow_source` ( `src23` tinyint(1) unsigned DEFAULT NULL COMMENT 'Skinned', `src24` tinyint(1) unsigned DEFAULT NULL COMMENT 'In-Game Store [not used]', PRIMARY KEY (`type`,`typeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2362,14 +2362,14 @@ DROP TABLE IF EXISTS `aowow_sourcestrings`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_sourcestrings` ( `id` int(16) NOT NULL, - `source_loc0` varchar(128) NOT NULL, - `source_loc2` varchar(128) NOT NULL, - `source_loc3` varchar(128) NOT NULL, - `source_loc6` varchar(128) NOT NULL, - `source_loc8` varchar(128) NOT NULL, + `source_loc0` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `source_loc2` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `source_loc3` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `source_loc6` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `source_loc8` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `Id` (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2395,7 +2395,7 @@ CREATE TABLE `aowow_spawns` ( KEY `type_idx` (`typeId`,`type`), KEY `zone_idx` (`areaId`), KEY `guid` (`guid`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2544,26 +2544,26 @@ CREATE TABLE `aowow_spell` ( `iconIdAlt` smallint(5) unsigned NOT NULL DEFAULT 0, `rankNo` tinyint(3) unsigned NOT NULL, `spellVisualId` smallint(5) unsigned NOT NULL, - `name_loc0` varchar(85) NOT NULL, - `name_loc2` varchar(85) NOT NULL, - `name_loc3` varchar(85) NOT NULL, - `name_loc6` varchar(91) NOT NULL, - `name_loc8` varchar(50) NOT NULL, - `rank_loc0` varchar(21) NOT NULL, - `rank_loc2` varchar(24) NOT NULL, - `rank_loc3` varchar(22) NOT NULL, - `rank_loc6` varchar(27) NOT NULL, - `rank_loc8` varchar(29) NOT NULL, - `description_loc0` text NOT NULL, - `description_loc2` text NOT NULL, - `description_loc3` text NOT NULL, - `description_loc6` text NOT NULL, - `description_loc8` text NOT NULL, - `buff_loc0` text NOT NULL, - `buff_loc2` text NOT NULL, - `buff_loc3` text NOT NULL, - `buff_loc6` text NOT NULL, - `buff_loc8` text NOT NULL, + `name_loc0` varchar(85) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(85) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(85) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(91) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `rank_loc0` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL, + `rank_loc2` varchar(24) COLLATE utf8mb4_unicode_ci NOT NULL, + `rank_loc3` varchar(22) COLLATE utf8mb4_unicode_ci NOT NULL, + `rank_loc6` varchar(27) COLLATE utf8mb4_unicode_ci NOT NULL, + `rank_loc8` varchar(29) COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `description_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `buff_loc0` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `buff_loc2` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `buff_loc3` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `buff_loc6` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `buff_loc8` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, `maxTargetLevel` tinyint(3) unsigned NOT NULL, `spellFamilyId` tinyint(3) unsigned NOT NULL, `spellFamilyFlags1` int(10) unsigned NOT NULL, @@ -2589,7 +2589,7 @@ CREATE TABLE `aowow_spell` ( KEY `effects` (`effect1Id`,`effect2Id`,`effect3Id`), KEY `items` (`effect1CreateItemId`,`effect2CreateItemId`,`effect3CreateItemId`), KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2620,7 +2620,7 @@ CREATE TABLE `aowow_spell_sounds` ( `missile` smallint(5) unsigned NOT NULL COMMENT 'not predicted by js', `impactarea` smallint(5) unsigned NOT NULL COMMENT 'not predicted by js', PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a spellId, but spellVisualId\r\n\r\ncolumn names from LANG.sound_activities'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a spellId, but spellVisualId\r\n\r\ncolumn names from LANG.sound_activities'; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2639,7 +2639,7 @@ CREATE TABLE `aowow_spelldifficulty` ( KEY `normal25` (`normal25`), KEY `heroic10` (`heroic10`), KEY `heroic25` (`heroic25`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2651,13 +2651,13 @@ DROP TABLE IF EXISTS `aowow_spellfocusobject`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_spellfocusobject` ( `id` smallint(5) unsigned NOT NULL, - `name_loc0` varchar(83) NOT NULL, - `name_loc2` varchar(89) NOT NULL, - `name_loc3` varchar(95) NOT NULL, - `name_loc6` varchar(90) NOT NULL, - `name_loc8` varchar(91) NOT NULL, + `name_loc0` varchar(83) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(89) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(95) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(90) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(91) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2675,7 +2675,7 @@ CREATE TABLE `aowow_spelloverride` ( `spellId4` bigint(20) NOT NULL, `spellId5` bigint(20) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2692,13 +2692,13 @@ CREATE TABLE `aowow_spellrange` ( `rangeMaxHostile` smallint(5) unsigned NOT NULL, `rangeMaxFriend` smallint(5) unsigned NOT NULL, `rangeType` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(27) NOT NULL, - `name_loc2` varchar(27) NOT NULL, - `name_loc3` varchar(27) NOT NULL, - `name_loc6` varchar(27) NOT NULL, - `name_loc8` varchar(27) NOT NULL, + `name_loc0` varchar(27) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(27) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(27) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(27) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(27) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2710,9 +2710,9 @@ DROP TABLE IF EXISTS `aowow_spellvariables`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_spellvariables` ( `id` tinyint(3) unsigned NOT NULL, - `vars` varchar(368) NOT NULL, + `vars` varchar(368) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2734,7 +2734,7 @@ CREATE TABLE `aowow_talents` ( PRIMARY KEY (`id`,`rank`), KEY `spell` (`spell`), KEY `class` (`class`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2753,13 +2753,13 @@ CREATE TABLE `aowow_taxinodes` ( `typeId` mediumint(9) unsigned NOT NULL, `reactA` tinyint(4) NOT NULL, `reactH` tinyint(4) NOT NULL, - `name_loc0` varchar(46) NOT NULL, - `name_loc2` varchar(62) NOT NULL, - `name_loc3` varchar(55) NOT NULL, - `name_loc6` varchar(63) NOT NULL, - `name_loc8` varchar(50) NOT NULL, + `name_loc0` varchar(46) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(62) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(55) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(63) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2774,7 +2774,7 @@ CREATE TABLE `aowow_taxipath` ( `startNodeId` smallint(6) unsigned NOT NULL, `endNodeId` smallint(6) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2794,19 +2794,19 @@ CREATE TABLE `aowow_titles` ( `src12Ext` mediumint(9) unsigned NOT NULL, `eventId` smallint(5) unsigned NOT NULL, `bitIdx` tinyint(3) unsigned NOT NULL, - `male_loc0` varchar(33) NOT NULL, - `male_loc2` varchar(35) NOT NULL, - `male_loc3` varchar(37) NOT NULL, - `male_loc6` varchar(34) NOT NULL, - `male_loc8` varchar(37) NOT NULL, - `female_loc0` varchar(33) NOT NULL, - `female_loc2` varchar(35) NOT NULL, - `female_loc3` varchar(39) NOT NULL, - `female_loc6` varchar(35) NOT NULL, - `female_loc8` varchar(41) NOT NULL, + `male_loc0` varchar(33) COLLATE utf8mb4_unicode_ci NOT NULL, + `male_loc2` varchar(35) COLLATE utf8mb4_unicode_ci NOT NULL, + `male_loc3` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL, + `male_loc6` varchar(34) COLLATE utf8mb4_unicode_ci NOT NULL, + `male_loc8` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL, + `female_loc0` varchar(33) COLLATE utf8mb4_unicode_ci NOT NULL, + `female_loc2` varchar(35) COLLATE utf8mb4_unicode_ci NOT NULL, + `female_loc3` varchar(39) COLLATE utf8mb4_unicode_ci NOT NULL, + `female_loc6` varchar(35) COLLATE utf8mb4_unicode_ci NOT NULL, + `female_loc8` varchar(41) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `bitIdx` (`bitIdx`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2818,15 +2818,15 @@ DROP TABLE IF EXISTS `aowow_totemcategory`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_totemcategory` ( `id` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(29) NOT NULL, - `name_loc2` varchar(45) NOT NULL, - `name_loc3` varchar(31) NOT NULL, - `name_loc6` varchar(36) NOT NULL, - `name_loc8` varchar(69) NOT NULL, + `name_loc0` varchar(29) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc2` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(31) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(69) COLLATE utf8mb4_unicode_ci NOT NULL, `category` tinyint(3) unsigned NOT NULL, `categoryMask` int(10) unsigned NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2842,8 +2842,8 @@ CREATE TABLE `aowow_videos` ( `typeId` mediumint(9) NOT NULL, `userIdOwner` int(10) unsigned DEFAULT NULL, `date` int(32) NOT NULL, - `videoId` varchar(12) NOT NULL, - `caption` text DEFAULT NULL, + `videoId` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL, + `caption` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, `status` int(8) NOT NULL, `userIdApprove` int(10) unsigned DEFAULT NULL, `userIdeDelete` int(10) unsigned DEFAULT NULL, @@ -2851,7 +2851,7 @@ CREATE TABLE `aowow_videos` ( KEY `type` (`type`,`typeId`), KEY `FK_acc_vi` (`userIdOwner`), CONSTRAINT `FK_acc_vi` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2880,18 +2880,18 @@ CREATE TABLE `aowow_zones` ( `levelHeroic` tinyint(3) unsigned NOT NULL, `levelMin` tinyint(4) unsigned NOT NULL, `levelMax` tinyint(4) unsigned NOT NULL, - `attunementsN` text NOT NULL COMMENT 'space separated; type:typeId', - `attunementsH` text NOT NULL COMMENT 'space separated; type:typeId', + `attunementsN` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'space separated; type:typeId', + `attunementsH` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'space separated; type:typeId', `parentAreaId` smallint(5) unsigned NOT NULL, `parentX` float NOT NULL, `parentY` float NOT NULL, - `name_loc0` varchar(120) NOT NULL COMMENT 'Map Name', - `name_loc2` varchar(120) NOT NULL, - `name_loc3` varchar(120) NOT NULL, - `name_loc6` varchar(120) NOT NULL, - `name_loc8` varchar(120) NOT NULL, + `name_loc0` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Map Name', + `name_loc2` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc3` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc6` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_loc8` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2911,7 +2911,7 @@ CREATE TABLE `aowow_zones_sounds` ( `worldStateId` smallint(5) unsigned NOT NULL, `worldStateValue` smallint(6) NOT NULL, KEY `id` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -2923,7 +2923,7 @@ CREATE TABLE `aowow_zones_sounds` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2018-03-26 18:57:18 +-- Dump completed on 2018-03-28 11:47:47 -- MySQL dump 10.16 Distrib 10.2.10-MariaDB, for debian-linux-gnu (x86_64) -- -- Host: localhost Database: aowow @@ -3006,7 +3006,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1522146995,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1522230799,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/updates/1522230798_01.sql b/setup/updates/1522230798_01.sql new file mode 100644 index 00000000..78531b79 --- /dev/null +++ b/setup/updates/1522230798_01.sql @@ -0,0 +1,279 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE `aowow_creature_waypoints` + CHANGE COLUMN `point` `point` SMALLINT UNSIGNED NOT NULL AFTER `creatureOrPath`; + +ALTER TABLE `aowow_errors` + CHANGE COLUMN `file` `file` VARCHAR(150) NOT NULL AFTER `phpError`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); + +ALTER TABLE `aowow_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_banned` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_bannedips` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_cookies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_reputation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscale_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscales` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcriteria` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_announcements` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_classes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments_rates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_waypoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_currencies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_dbversion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_aliasses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_errors` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factiontemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_glyphproperties` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_holidays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox_overlay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_oneliner` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_icons` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_item_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantmentcondition` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemextendedcost` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemlimitcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomenchant` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomproppoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemset` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_lock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_loot_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_mailtemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_pet` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team_member` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_completion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild_rank` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_pets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_sync` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests_startend` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatdistribution` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatvalues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_screenshots` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_shapeshiftforms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_skillline` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_source` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sourcestrings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spawns` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelldifficulty` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellfocusobject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelloverride` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellrange` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellvariables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_talents` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxinodes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxipath` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_totemcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_videos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_banned` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_bannedips` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_cookies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_reputation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscale_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscales` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcriteria` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_announcements` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_characters` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_classes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments_rates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_waypoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_currencies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_dbversion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_aliasses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_errors` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factiontemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_glyphproperties` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_holidays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox_overlay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_oneliner` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_icons` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_item_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantmentcondition` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemextendedcost` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemlimitcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomenchant` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomproppoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemset` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_lock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_loot_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_mailtemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_pet` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_powerdisplay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests_startend` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatdistribution` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatvalues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_screenshots` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_shapeshiftforms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_skillline` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_source` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sourcestrings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spawns` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelldifficulty` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellfocusobject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelloverride` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellrange` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellvariables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_talents` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxinodes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxipath` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_totemcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_videos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_banned` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_bannedips` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_cookies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_reputation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscale_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscales` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcriteria` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_announcements` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_classes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments_rates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_waypoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_currencies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_dbversion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_aliasses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_errors` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factiontemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_glyphproperties` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_holidays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox_overlay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_oneliner` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_icons` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_item_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantmentcondition` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemextendedcost` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemlimitcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomenchant` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomproppoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemset` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_lock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_loot_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_mailtemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_pet` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team_member` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_completion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild_rank` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_pets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_sync` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests_startend` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatdistribution` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatvalues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_screenshots` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_shapeshiftforms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_skillline` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_source` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sourcestrings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spawns` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelldifficulty` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellfocusobject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelloverride` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellrange` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellvariables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_talents` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxinodes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxipath` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_totemcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_videos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +SET FOREIGN_KEY_CHECKS = 1; From bf42973c00cc6fe88b37b69ec62ac772f9cba852 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 28 Mar 2018 20:34:18 +0200 Subject: [PATCH 007/957] Profiler * further optimize search - use achievement 4496 as shortcut for everything based around total achievement points - get talent distribution separately - get total profiler-items found separately - opt to not sort found results * fixed Profiles with zero Achievement Points * added cache key genrator i forgot :< * fixed typos --- includes/basetype.class.php | 14 +++++- includes/types/profile.class.php | 75 +++++++++++++++----------------- pages/achievement.php | 2 +- pages/achievements.php | 2 +- pages/arenateam.php | 2 + pages/arenateams.php | 2 + pages/class.php | 2 +- pages/classes.php | 2 +- pages/currencies.php | 2 +- pages/currency.php | 2 +- pages/emote.php | 2 +- pages/emotes.php | 2 +- pages/enchantment.php | 2 +- pages/enchantments.php | 2 +- pages/event.php | 2 +- pages/events.php | 2 +- pages/faction.php | 2 +- pages/factions.php | 2 +- pages/genericPage.class.php | 14 +++++- pages/guild.php | 2 + pages/guilds.php | 2 + pages/icon.php | 2 +- pages/icons.php | 2 +- pages/item.php | 2 +- pages/items.php | 2 +- pages/itemset.php | 2 +- pages/itemsets.php | 2 +- pages/npc.php | 2 +- pages/npcs.php | 2 +- pages/object.php | 2 +- pages/objects.php | 2 +- pages/pet.php | 2 +- pages/pets.php | 2 +- pages/profile.php | 6 ++- pages/profiles.php | 4 +- pages/quest.php | 2 +- pages/quests.php | 2 +- pages/race.php | 2 +- pages/races.php | 2 +- pages/skill.php | 2 +- pages/skills.php | 2 +- pages/sound.php | 2 +- pages/sounds.php | 2 +- pages/spell.php | 2 +- pages/spells.php | 2 +- pages/title.php | 2 +- pages/titles.php | 2 +- pages/zones.php | 2 +- static/js/global.js | 4 +- 49 files changed, 117 insertions(+), 86 deletions(-) diff --git a/includes/basetype.class.php b/includes/basetype.class.php index f5b1acab..5e9e9f0c 100644 --- a/includes/basetype.class.php +++ b/includes/basetype.class.php @@ -257,12 +257,24 @@ abstract class BaseType // execute query (finally) $mtch = 0; + $rows = []; // this is purely because of multiple realms per server foreach ($this->dbNames as $dbIdx => $n) { $query = str_replace('DB_IDX', $dbIdx, $this->queryBase); - if ($rows = DB::{$n}($dbIdx)->SelectPage($mtch, $query)) + if (key($this->dbNames) === 0) + { + if ($rows = DB::{$n}($dbIdx)->select($query)) + { + $mtchQry = preg_replace('/SELECT .*? FROM/', 'SELECT count(1) FROM', $this->queryBase); + $mtch = DB::{$n}($dbIdx)->selectCell($mtchQry); + } + } + else + $rows = DB::{$n}($dbIdx)->SelectPage($mtch, $query); + + if ($rows) { $this->matches += $mtch; foreach ($rows as $id => $row) diff --git a/includes/types/profile.class.php b/includes/types/profile.class.php index c72bd575..8c7d4169 100644 --- a/includes/types/profile.class.php +++ b/includes/types/profile.class.php @@ -35,7 +35,7 @@ class ProfileList extends BaseType 'talenttree3' => $this->getField('talenttree3'), 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2 'achievementpoints' => $this->getField('achievementpoints'), - 'guild' => '$"'.str_replace ('"', '', $this->curTpl['name']).'"', // force this to be a string + 'guild' => '$"'.str_replace ('"', '', $this->curTpl['guildname']).'"',// force this to be a string 'guildrank' => $this->getField('guildrank'), 'realm' => Profiler::urlize($this->getField('realmName')), 'realmname' => $this->getField('realmName'), @@ -181,7 +181,7 @@ class ProfileListFilter extends Filter protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet 2 => [FILTER_CR_NUMERIC, 'gearscore', NUM_CAST_INT ], // gearscore [num] - 3 => [FILTER_CR_NUMERIC, 'achievementpoints', NUM_CAST_INT ], // achievementpoints [num] + 3 => [FILTER_CR_CALLBACK, 'cbAchievs', null, null], // achievementpoints [num] 5 => [FILTER_CR_NUMERIC, 'talenttree1', NUM_CAST_INT ], // talenttree1 [num] 6 => [FILTER_CR_NUMERIC, 'talenttree2', NUM_CAST_INT ], // talenttree2 [num] 7 => [FILTER_CR_NUMERIC, 'talenttree3', NUM_CAST_INT ], // talenttree3 [num] @@ -245,7 +245,7 @@ class ProfileListFilter extends Filter parent::__construct($fromPOST, $opts); if (!empty($this->fiData['c']['cr'])) - if (array_intersect($this->fiData['c']['cr'], [2, 3, 5, 6, 7, 21])) + if (array_intersect($this->fiData['c']['cr'], [2, 5, 6, 7, 21])) $this->useLocalList = true; } @@ -435,6 +435,17 @@ class ProfileListFilter extends Filter return ['AND', ['at.type', $this->enums[-1][$cr[0]]], ['at.rating', $cr[2], $cr[1]]]; } + + protected function cbAchievs($cr) + { + if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) + return false; + + if ($this->useLocalList) + return ['p.achievementpoints', $cr[2], $cr[1]]; + else + return ['cap.counter', $cr[2], $cr[1]]; + } } @@ -442,9 +453,8 @@ class RemoteProfileList extends ProfileList { protected $queryBase = 'SELECT `c`.*, `c`.`guid` AS ARRAY_KEY FROM characters c'; protected $queryOpts = array( - 'c' => [['gm', 'g', 'ca', 'ct'], 'g' => 'ARRAY_KEY', 'o' => 'level DESC, name ASC'], - 'ca' => ['j' => ['character_achievement ca ON ca.guid = c.guid', true], 's' => ', GROUP_CONCAT(DISTINCT ca.achievement SEPARATOR " ") AS _acvs'], - 'ct' => ['j' => ['character_talent ct ON ct.guid = c.guid AND ct.spec = c.activespec', true], 's' => ', GROUP_CONCAT(DISTINCT ct.spell SEPARATOR " ") AS _talents'], + 'c' => [['gm', 'g', 'cap']], // 12698: use criteria of Achievement 4496 as shortcut to get total achievement points + 'cap' => ['j' => ['character_achievement_progress cap ON cap.guid = c.guid AND cap.criteria = 12698', true], 's' => ', IFNULL(cap.counter, 0) AS achievementpoints'], 'gm' => ['j' => ['guild_member gm ON gm.guid = c.guid', true], 's' => ', gm.rank AS guildrank'], 'g' => ['j' => ['guild g ON g.guildid = gm.guildid', true], 's' => ', g.guildid AS guild, g.name AS guildname'], 'atm' => ['j' => ['arena_team_member atm ON atm.guid = c.guid', true], 's' => ', atm.personalRating AS rating'], @@ -466,14 +476,12 @@ class RemoteProfileList extends ProfileList return; reset($this->dbNames); // only use when querying single realm - $realmId = key($this->dbNames); - $realms = Profiler::getRealms(); - $acvCache = []; - $talentCache = []; - $atCache = []; - $distrib = null; - $talentData = []; - $limit = CFG_SQL_LIMIT_DEFAULT; + $realmId = key($this->dbNames); + $realms = Profiler::getRealms(); + $talentSpells = []; + $talentLookup = []; + $distrib = null; + $limit = CFG_SQL_LIMIT_DEFAULT; foreach ($conditions as $c) if (is_int($c)) @@ -486,7 +494,7 @@ class RemoteProfileList extends ProfileList $curTpl['battlegroup'] = CFG_BATTLEGROUP; // realm - $r = explode(':', $guid)[0]; + list($r, $g) = explode(':', $guid); if (!empty($realms[$r])) { $curTpl['realm'] = $r; @@ -503,17 +511,9 @@ class RemoteProfileList extends ProfileList // temp id $curTpl['id'] = 0; - // achievement points pre - if ($acvs = explode(' ', $curTpl['_acvs'])) - foreach ($acvs as $a) - if ($a && !isset($acvCache[$a])) - $acvCache[$a] = $a; - // talent points pre - if ($talents = explode(' ', $curTpl['_talents'])) - foreach ($talents as $t) - if ($t && !isset($talentCache[$t])) - $talentCache[$t] = $t; + $talentLookup[$r][$g] = []; + $talentSpells[] = $curTpl['class']; // equalize distribution if ($limit != CFG_SQL_LIMIT_NONE) @@ -527,8 +527,10 @@ class RemoteProfileList extends ProfileList $curTpl['cuFlags'] = 0; } - if ($talentCache) - $talentData = DB::Aowow()->select('SELECT spell AS ARRAY_KEY, tab, rank FROM ?_talents WHERE spell IN (?a)', $talentCache); + foreach ($talentLookup as $realm => $chars) + $talentLookup[$realm] = DB::Characters($realm)->selectCol('SELECT guid AS ARRAY_KEY, spell AS ARRAY_KEY2, spec FROM character_talent ct WHERE guid IN (?a)', array_keys($chars)); + + $talentSpells = DB::Aowow()->select('SELECT spell AS ARRAY_KEY, tab, rank FROM ?_talents WHERE class IN (?a)', array_unique($talentSpells)); if ($distrib !== null) { @@ -537,9 +539,6 @@ class RemoteProfileList extends ProfileList $d = ceil($limit * $d / $total); } - if ($acvCache) - $acvCache = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, points FROM ?_achievement WHERE id IN (?a)', $acvCache); - foreach ($this->iterate() as $guid => &$curTpl) { if ($distrib !== null) @@ -554,22 +553,18 @@ class RemoteProfileList extends ProfileList $limit--; } - - $a = explode(' ', $curTpl['_acvs']); - $t = explode(' ', $curTpl['_talents']); - unset($curTpl['_acvs']); - unset($curTpl['_talents']); - - // achievement points post - $curTpl['achievementpoints'] = array_sum(array_intersect_key($acvCache, array_combine($a, $a))); + list($r, $g) = explode(':', $guid); // talent points post $curTpl['talenttree1'] = 0; $curTpl['talenttree2'] = 0; $curTpl['talenttree3'] = 0; - foreach ($talentData as $spell => $data) - if (in_array($spell, $t)) + if (!empty($talentLookup[$r][$g])) + { + $talents = array_filter($talentLookup[$r][$g], function($v) use ($curTpl) { return $curTpl['activespec'] == $v; } ); + foreach (array_intersect_key($talentSpells, $talents) as $spell => $data) $curTpl['talenttree'.($data['tab'] + 1)] += $data['rank']; + } } } diff --git a/pages/achievement.php b/pages/achievement.php index adb57d02..b04a2f9c 100644 --- a/pages/achievement.php +++ b/pages/achievement.php @@ -23,7 +23,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class AchievementPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_ACHIEVEMENT; protected $typeId = 0; diff --git a/pages/achievements.php b/pages/achievements.php index f1cde8e0..ce65a26e 100644 --- a/pages/achievements.php +++ b/pages/achievements.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class AchievementsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_ACHIEVEMENT; protected $tpl = 'achievements'; diff --git a/pages/arenateam.php b/pages/arenateam.php index 24ec0f99..29833051 100644 --- a/pages/arenateam.php +++ b/pages/arenateam.php @@ -12,6 +12,8 @@ class ArenaTeamPage extends GenericPage protected $lvTabs = []; + protected $type = TYPE_ARENA_TEAM; + protected $tabId = 1; protected $path = [1, 5, 3]; protected $tpl = 'roster'; diff --git a/pages/arenateams.php b/pages/arenateams.php index e72f2f59..5bde8627 100644 --- a/pages/arenateams.php +++ b/pages/arenateams.php @@ -10,6 +10,8 @@ class ArenaTeamsPage extends GenericPage { use TrProfiler; + protected $type = TYPE_ARENA_TEAM; + protected $tabId = 1; protected $path = [1, 5, 3]; protected $tpl = 'arena-teams'; diff --git a/pages/class.php b/pages/class.php index a890119b..0775831b 100644 --- a/pages/class.php +++ b/pages/class.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ClassPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_CLASS; protected $typeId = 0; diff --git a/pages/classes.php b/pages/classes.php index 9b8a2448..838fd5cb 100644 --- a/pages/classes.php +++ b/pages/classes.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ClassesPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_CLASS; protected $tpl = 'list-page-generic'; diff --git a/pages/currencies.php b/pages/currencies.php index 33179b6a..0534f4db 100644 --- a/pages/currencies.php +++ b/pages/currencies.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class CurrenciesPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_CURRENCY; protected $tpl = 'list-page-generic'; diff --git a/pages/currency.php b/pages/currency.php index beea3502..4fe68ed0 100644 --- a/pages/currency.php +++ b/pages/currency.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class CurrencyPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_CURRENCY; protected $typeId = 0; diff --git a/pages/emote.php b/pages/emote.php index d70a0c80..26d3bbb3 100644 --- a/pages/emote.php +++ b/pages/emote.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabid 0: Database g_initHeader() class EmotePage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_EMOTE; protected $typeId = 0; diff --git a/pages/emotes.php b/pages/emotes.php index 753dcd62..6720fe40 100644 --- a/pages/emotes.php +++ b/pages/emotes.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabid 0: Database g_initHeader() class EmotesPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_EMOTE; protected $tpl = 'list-page-generic'; diff --git a/pages/enchantment.php b/pages/enchantment.php index 6dcf65f0..f3e8b502 100644 --- a/pages/enchantment.php +++ b/pages/enchantment.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class EnchantmentPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_ENCHANTMENT; protected $typeId = 0; diff --git a/pages/enchantments.php b/pages/enchantments.php index 5f14e89c..a35cb354 100644 --- a/pages/enchantments.php +++ b/pages/enchantments.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class EnchantmentsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_ENCHANTMENT; protected $tpl = 'enchantments'; diff --git a/pages/event.php b/pages/event.php index 66b17f75..b2e87498 100644 --- a/pages/event.php +++ b/pages/event.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class EventPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_WORLDEVENT; protected $typeId = 0; diff --git a/pages/events.php b/pages/events.php index 1b9a9330..c8b25867 100644 --- a/pages/events.php +++ b/pages/events.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class EventsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_WORLDEVENT; protected $tpl = 'list-page-generic'; diff --git a/pages/faction.php b/pages/faction.php index d52cf613..292f7322 100644 --- a/pages/faction.php +++ b/pages/faction.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class FactionPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_FACTION; protected $typeId = 0; diff --git a/pages/factions.php b/pages/factions.php index e25b32f9..bb8bba39 100644 --- a/pages/factions.php +++ b/pages/factions.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class FactionsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_FACTION; protected $tpl = 'list-page-generic'; diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index 99036841..65337489 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -4,7 +4,7 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -trait DetailPage +trait TrDetailPage { protected $hasComContent = true; protected $category = null; // not used on detail pages @@ -48,7 +48,7 @@ trait DetailPage } -trait ListPage +trait TrListPage { protected $category = null; protected $filter = []; @@ -85,6 +85,16 @@ trait TrProfiler protected $doResync = null; + protected function generateCacheKey($withStaff = true) + { + $staff = intVal($withStaff && User::isInGroup(U_GROUP_EMPLOYEE)); + + // mode, type, typeId, employee-flag, localeId, category, filter + $key = [$this->mode, $this->type, $this->subject->getField('id'), $staff, User::$localeId, '-1', '-1']; + + return implode('_', $key); + } + protected function getSubjectFromUrl($str) { if (!$str) diff --git a/pages/guild.php b/pages/guild.php index 8ba18176..7696e99b 100644 --- a/pages/guild.php +++ b/pages/guild.php @@ -12,6 +12,8 @@ class GuildPage extends GenericPage protected $lvTabs = []; + protected $type = TYPE_GUILD; + protected $tabId = 1; protected $path = [1, 5, 2]; protected $tpl = 'roster'; diff --git a/pages/guilds.php b/pages/guilds.php index cee61f76..7e3175f8 100644 --- a/pages/guilds.php +++ b/pages/guilds.php @@ -10,6 +10,8 @@ class GuildsPage extends GenericPage { use TrProfiler; + protected $type = TYPE_GUILD; + protected $tabId = 1; protected $path = [1, 5, 2]; protected $tpl = 'guilds'; diff --git a/pages/icon.php b/pages/icon.php index 2c5d3677..c3872fe9 100644 --- a/pages/icon.php +++ b/pages/icon.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class IconPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_ICON; protected $typeId = 0; diff --git a/pages/icons.php b/pages/icons.php index c20f7580..4ebdc164 100644 --- a/pages/icons.php +++ b/pages/icons.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class IconsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_ICON; protected $tpl = 'icons'; diff --git a/pages/item.php b/pages/item.php index fa65ac21..1d238e6e 100644 --- a/pages/item.php +++ b/pages/item.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ItemPage extends genericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_ITEM; protected $typeId = 0; diff --git a/pages/items.php b/pages/items.php index a40eaa16..12e58d1d 100644 --- a/pages/items.php +++ b/pages/items.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ItemsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_ITEM; protected $tpl = 'items'; diff --git a/pages/itemset.php b/pages/itemset.php index fd6b7990..263e4c02 100644 --- a/pages/itemset.php +++ b/pages/itemset.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ItemsetPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_ITEMSET; protected $typeId = 0; diff --git a/pages/itemsets.php b/pages/itemsets.php index 80d9553a..1bbf059f 100644 --- a/pages/itemsets.php +++ b/pages/itemsets.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ItemsetsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_ITEMSET; protected $tpl = 'itemsets'; diff --git a/pages/npc.php b/pages/npc.php index b0947a27..9aea7ef7 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class NpcPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_NPC; protected $typeId = 0; diff --git a/pages/npcs.php b/pages/npcs.php index 161cc4e6..a90ffd30 100644 --- a/pages/npcs.php +++ b/pages/npcs.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class NpcsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_NPC; protected $tpl = 'npcs'; diff --git a/pages/object.php b/pages/object.php index 86203277..e68c3200 100644 --- a/pages/object.php +++ b/pages/object.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ObjectPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_OBJECT; protected $typeId = 0; diff --git a/pages/objects.php b/pages/objects.php index 27717b14..5f6fd68b 100644 --- a/pages/objects.php +++ b/pages/objects.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ObjectsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_OBJECT; protected $tpl = 'objects'; diff --git a/pages/pet.php b/pages/pet.php index daa5edd1..a6d9e933 100644 --- a/pages/pet.php +++ b/pages/pet.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabid 0: Database g_initHeader() class PetPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_PET; protected $typeId = 0; diff --git a/pages/pets.php b/pages/pets.php index a3d1b814..f1686033 100644 --- a/pages/pets.php +++ b/pages/pets.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabid 0: Database g_initHeader() class PetsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_PET; protected $tpl = 'list-page-generic'; diff --git a/pages/profile.php b/pages/profile.php index 8cc81fde..b596832f 100644 --- a/pages/profile.php +++ b/pages/profile.php @@ -11,7 +11,9 @@ class ProfilePage extends GenericPage use TrProfiler; protected $gDataKey = true; - protected $mode = CACHE_TYPE_PAGE; + protected $mode = CACHE_TYPE_PAGE; + + protected $type = TYPE_PROFILE; protected $tabId = 1; protected $path = [1, 5, 1]; @@ -125,7 +127,7 @@ class ProfilePage extends GenericPage /* 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 +/* 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 ); diff --git a/pages/profiles.php b/pages/profiles.php index 3cd64741..da76266a 100644 --- a/pages/profiles.php +++ b/pages/profiles.php @@ -12,6 +12,8 @@ class ProfilesPage extends GenericPage protected $roster = 0; // $_GET['roster'] = 1|2|3|4 .. 2,3,4 arenateam-size (4 => 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'; @@ -34,7 +36,7 @@ class ProfilesPage extends GenericPage if ($this->realm && $r['name'] != $this->realm) continue; - $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM characters WHERE deleteInfos_Name IS NULL'); + $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; } diff --git a/pages/quest.php b/pages/quest.php index 0d37b261..b06a1317 100644 --- a/pages/quest.php +++ b/pages/quest.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class QuestPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_QUEST; protected $typeId = 0; diff --git a/pages/quests.php b/pages/quests.php index 478c1072..9d25ad0a 100644 --- a/pages/quests.php +++ b/pages/quests.php @@ -7,7 +7,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class QuestsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_QUEST; protected $tpl = 'quests'; diff --git a/pages/race.php b/pages/race.php index d21d0a33..e8be0958 100644 --- a/pages/race.php +++ b/pages/race.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class RacePage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_RACE; protected $typeId = 0; diff --git a/pages/races.php b/pages/races.php index ce1ccac2..9a11d86d 100644 --- a/pages/races.php +++ b/pages/races.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class RacesPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_RACE; protected $tpl = 'list-page-generic'; diff --git a/pages/skill.php b/pages/skill.php index 595643a2..86f35075 100644 --- a/pages/skill.php +++ b/pages/skill.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class SkillPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_SKILL; protected $typeId = 0; diff --git a/pages/skills.php b/pages/skills.php index 377f2471..5ecdf59d 100644 --- a/pages/skills.php +++ b/pages/skills.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class SkillsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_SKILL; protected $tpl = 'list-page-generic'; diff --git a/pages/sound.php b/pages/sound.php index 17ccfdb2..9ec120e7 100644 --- a/pages/sound.php +++ b/pages/sound.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class SoundPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_SOUND; protected $tpl = 'sound'; diff --git a/pages/sounds.php b/pages/sounds.php index b778c329..14111aee 100644 --- a/pages/sounds.php +++ b/pages/sounds.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class SoundsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_SOUND; protected $tpl = 'sounds'; diff --git a/pages/spell.php b/pages/spell.php index 486592fe..26f47a86 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class SpellPage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_SPELL; protected $typeId = 0; diff --git a/pages/spells.php b/pages/spells.php index 241131fb..fa2e0dd0 100644 --- a/pages/spells.php +++ b/pages/spells.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class SpellsPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_SPELL; protected $tpl = 'spells'; diff --git a/pages/title.php b/pages/title.php index 66f61181..b12804a9 100644 --- a/pages/title.php +++ b/pages/title.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class TitlePage extends GenericPage { - use DetailPage; + use TrDetailPage; protected $type = TYPE_TITLE; protected $typeId = 0; diff --git a/pages/titles.php b/pages/titles.php index ed41c6ea..e0904b24 100644 --- a/pages/titles.php +++ b/pages/titles.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class TitlesPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_TITLE; protected $tpl = 'list-page-generic'; diff --git a/pages/zones.php b/pages/zones.php index 5ceba164..5b1ff2e9 100644 --- a/pages/zones.php +++ b/pages/zones.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) // tabId 0: Database g_initHeader() class ZonesPage extends GenericPage { - use ListPage; + use TrListPage; protected $type = TYPE_ZONE; protected $tpl = 'list-page-generic'; diff --git a/static/js/global.js b/static/js/global.js index ccc6e3d6..5e9081b3 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -9692,7 +9692,9 @@ Listview.funcBox = { } } - if (achievementPoints > 0) { + // aowow: changed because legitemately passing zero APs from the profiler is a thing + // (achievementPoints > 0) { + if (typeof achievementPoints == 'number') { if (ns) { $WH.ae(d, $WH.ct(' ')); } From 51eda1209929d5c14714d50b49b26fcfa37423b4 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 29 Mar 2018 13:14:47 +0200 Subject: [PATCH 008/957] Pages/Home * implement random home titles --- pages/home.php | 5 +- setup/db_structure.sql | 22 ++++-- setup/updates/1522321542_01.sql | 114 ++++++++++++++++++++++++++++++++ template/pages/home.tpl.php | 20 ++++-- 4 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 setup/updates/1522321542_01.sql diff --git a/pages/home.php b/pages/home.php index 47feffb4..4eda5d31 100644 --- a/pages/home.php +++ b/pages/home.php @@ -12,6 +12,7 @@ class HomePage extends GenericPage protected $featuredBox = []; protected $oneliner = ''; + protected $homeTitle = ''; public function __construct() { @@ -52,8 +53,8 @@ class HomePage extends GenericPage protected function generateTitle() { - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_home_titles WHERE active = 1 AND title_loc?d <> "" ORDER BY RAND() LIMIT 1', User::$localeId)) - $this->title[0] .= Lang::main('colon').Util::localizedString($_, 'title'); + if ($_ = DB::Aowow()->selectCell('SELECT title FROM ?_home_titles WHERE active = 1 AND locale = ?d ORDER BY RAND() LIMIT 1', User::$localeId)) + $this->homeTitle = CFG_NAME.Lang::main('colon').$_; } protected function generatePath() {} diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 9407f5e4..312bdf57 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -913,16 +913,14 @@ DROP TABLE IF EXISTS `aowow_home_titles`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_home_titles` ( - `id` smallint(5) unsigned NOT NULL, + `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, `editorId` int(10) unsigned DEFAULT NULL, `editDate` int(10) unsigned NOT NULL, `active` tinyint(1) unsigned NOT NULL, - `title_loc0` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - `title_loc2` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - `title_loc3` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - `title_loc6` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - `title_loc8` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), + UNIQUE KEY `locale_title` (`locale`,`title`), KEY `FK_acc_hTitles` (`editorId`), CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -3006,7 +3004,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1522230799,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1522321543,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; @@ -3030,6 +3028,16 @@ INSERT INTO `aowow_home_featuredbox_overlay` VALUES (2,405,100,'http://example.c /*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` ENABLE KEYS */; UNLOCK TABLES; +-- +-- Dumping data for table `aowow_home_titles` +-- + +LOCK TABLES `aowow_home_titles` WRITE; +/*!40000 ALTER TABLE `aowow_home_titles` DISABLE KEYS */; +INSERT INTO `aowow_home_titles` VALUES (1,0,1522321542,1,0,'That\'s a 50 DKP plus!'),(2,0,1522321542,1,0,'We\'ve got what you need!'),(3,0,1522321542,1,0,'You haven\'t found the secret title yet.'),(4,0,1522321542,1,0,'...and knowing is half the battle!'),(5,0,1522321542,1,0,'Good news, everyone!'),(6,0,1522321542,1,0,'+1, Insightful'),(7,0,1522321542,1,0,'More effective than a [Booterang].'),(8,0,1522321542,1,0,'There is no cow level.'),(9,0,1522321542,1,0,'We\'ve got more style than a fashion designer who knows CSS.'),(10,0,1522321542,1,3,'Eure Fertigkeit in WoW hat sich auf 450 erhöht.'),(11,0,1522321542,1,0,'If you use your mouse to search, you won\'t be able to click on Rend.'),(12,0,1522321542,1,2,'Tout est dans l\'élégance.'),(13,0,1522321542,1,2,'Rend les chargements supportables depuis 2006.'),(14,0,1522321542,1,2,'Vous allez revenir.'),(15,0,1522321542,1,2,'Base de données extraordinaire'),(16,0,1522321542,1,2,'Si vous lisez ceci, arrêtez d\'appuyer sur F5.'),(17,0,1522321542,1,3,'Und der Tag ist gerettet.'),(18,0,1522321542,1,3,'Jetzt in allen bekannten Internetzen verfügbar!'),(19,0,1522321542,1,3,'Morgens, halb drei in Nordend'),(20,0,1522321542,1,3,'Macht auch Euren Webbrowser glücklich!'),(21,0,1522321542,1,3,'Hier findet Ihr sogar Mankriks Frau.'),(22,0,1522321542,1,6,'Base de datos extraordinaria de WoW'),(23,0,1522321542,1,6,'La única cosa en la que los ninjas y los piratas estan de acuerdo.'),(24,0,1522321542,1,6,'La elegancia lo es todo.'),(25,0,1522321542,1,6,'Hace feliz a los navegadores.'),(26,0,1522321542,1,8,'Ты ещё вернёшься.'),(27,0,1522321542,1,8,'Осваивание нового босса - 45 золота на ремонт. Персональный эпический предмет - 650 золотых'),(28,0,1522321542,1,8,'Не именной. Поделитесь им с друзьями!'),(29,0,1522321542,1,8,'Если вы здесь впервые, то вам необходимо воспользоваться поиском!'),(30,0,1522321542,1,8,'Приколы Мулгора без чата в Мулгоре.'),(31,0,1522321542,1,2,'Les trois premières lettres veulent tout dire.'),(32,0,1522321542,1,2,'Trouvez la femme de Mankrik grâce à lui.'),(33,0,1522321542,1,6,'Tu habilidad con WoW se ha incrementado a 450.'),(34,0,1522321542,1,6,'Buscando uno más: Tú'),(35,0,1522321542,1,8,'Первые три буквы говорят сами за себя.'),(36,0,1522321542,1,8,'У нас больше стиля, чем у дизайнера, знающего CSS.'),(37,0,1522321542,1,0,'Preventing wipes since 2006.'),(38,0,1522321542,1,0,'Never gonna give you up. Never gonna let you down.'),(39,0,1522321542,1,0,'The closest thing to an F1 key for WoW.'),(40,0,1522321542,1,2,'Non lié. Partagez-le avec vos amis !'),(41,0,1522321542,1,2,'Votre navigateur l\'adore !'),(42,0,1522321542,1,3,'Verhindert Wipes seit 2006.'),(43,0,1522321542,1,6,'+1, Utilidad'),(44,0,1522321542,1,6,'Épico, como tu líder de facción.'),(45,0,1522321542,1,8,'Он такой один...'),(46,0,1522321542,1,8,'Если вы это читаете, то прекратите обновлять страницу.'),(47,0,1522321542,1,0,'If you are reading this, stop pressing F5.'),(48,0,1522321542,1,2,'Chasse les jours pluvieux.'),(49,0,1522321542,1,3,'+1, Hilfreich'),(50,0,1522321542,1,3,'Episch - markant - dreifach verzaubert'),(51,0,1522321542,1,8,'Работает как положено.'),(52,0,1522321542,1,0,'Flagged for awesome.'),(53,0,1522321542,1,0,'Thrall-tested, Jaina-approved.'),(54,0,1522321542,1,8,'Всё дело в элегантности.'),(55,0,1522321542,1,0,'What does it mean?'),(56,0,1522321542,1,0,'YOU ARE NOW PREPARED!'),(57,0,1522321542,1,0,'srsly'),(58,0,1522321542,1,2,'C\'est comme prétendre être malade et aller à la plage, mais pour les bases de données.'),(59,0,1522321542,1,3,'Thrall-getestet, Jaina-genehmigt'),(60,0,1522321542,1,6,'Haciendo las pantallas de carga más soportables desde el 2006'),(61,0,1522321542,1,8,'Создан быть лидером.'),(62,0,1522321542,1,0,'You\'ll say \"Wow\" every time.'),(63,0,1522321542,1,0,'Dataz! We need more dataz!'),(64,0,1522321542,1,0,'Your skill in WoW has increased to 450.'),(65,0,1522321542,1,3,'Eleganz ist alles.'),(66,0,1522321542,1,8,'+1, Полезный'),(67,0,1522321542,1,8,'Ух ты!'),(68,0,1522321542,1,0,'Sometimes there is fire. You need to not be in it.'),(69,0,1522321542,1,0,'Working as intended.'),(70,0,1522321542,1,2,'La seule chose sur laquelle les ninjas et les pirates sont d\'accord.'),(71,0,1522321542,1,3,'Nicht seelengebunden. Teilt es mit Euren Freunden!'),(72,0,1522321542,1,8,'Теперь доступен во всех известных Интернетах!'),(73,0,1522321542,1,8,'Вы получаете добычу: [Легендарное Знание]'),(74,0,1522321542,1,0,'You\'ll be back.'),(75,0,1522321542,1,0,'Epic like your faction leader.'),(76,0,1522321542,1,3,'Manchmal gibt es Feuer. Ihr dürft nicht drin stehen.'),(77,0,1522321542,1,3,'Wer das hier lesen kann, drückt zu oft F5.'),(78,0,1522321542,1,6,'¡Datos! ¡Más Datos!'),(79,0,1522321542,1,8,'НЯМ НЯМ НЯМ'),(80,0,1522321542,1,2,'Testé par Thrall, approuvé par Jaina.'),(81,0,1522321542,1,8,'Сделайте его вашей новой расовой возможностью уже сегодня!'),(82,0,1522321542,1,0,'We do math, so you don\'t have to.'),(83,0,1522321542,1,0,'OM NOM NOM'),(84,0,1522321542,1,0,'Now available on all known internets!'),(85,0,1522321542,1,0,'We brake for dataz.'),(86,0,1522321542,1,3,'Neues von der Obstverkäuferfront'),(87,0,1522321542,1,6,'Las primeras tres palabras lo dicen todo.'),(88,0,1522321542,1,8,'Это как будто сказать всем, что ты болен, а самому пойти на пляж, - только для баз данных.'),(89,0,1522321542,1,8,'Меняем семечки на данные!'),(90,0,1522321542,1,0,'It\'s all about elegance.'),(91,0,1522321542,1,0,'Never underestimate the power of the Scout\'s code.'),(92,0,1522321542,1,6,'Elimina los días lluviosos.'),(93,0,1522321542,1,0,'You just won the game.'),(94,0,1522321542,1,8,'Данные! Нам надо больше данных!'),(95,0,1522321542,1,0,'WoW Database Extraordinaire'),(96,0,1522321542,1,0,'No longer soulbound. Can now be shared with friends!'),(97,0,1522321542,1,0,'The dataz you could be using.'),(98,0,1522321542,1,8,'Превосходен, как лидер вашей фракции.'),(99,0,1522321542,1,6,'¡Regresarás!'); +/*!40000 ALTER TABLE `aowow_home_titles` ENABLE KEYS */; +UNLOCK TABLES; + -- -- Dumping data for table `aowow_loot_link` -- diff --git a/setup/updates/1522321542_01.sql b/setup/updates/1522321542_01.sql new file mode 100644 index 00000000..ed40c674 --- /dev/null +++ b/setup/updates/1522321542_01.sql @@ -0,0 +1,114 @@ +DROP TABLE IF EXISTS `aowow_home_titles`; +CREATE TABLE `aowow_home_titles` ( + `id` SMALLINT(5) UNSIGNED NOT NULL AUTO_INCREMENT, + `editorId` INT(10) UNSIGNED NULL DEFAULT NULL, + `editDate` INT(10) UNSIGNED NOT NULL, + `active` TINYINT(1) UNSIGNED NOT NULL, + `locale` TINYINT(3) UNSIGNED NOT NULL, + `title` VARCHAR(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `locale_title` (`locale`, `title`), + INDEX `FK_acc_hTitles` (`editorId`), + CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE ON DELETE SET NULL +) COLLATE='utf8_general_ci' ENGINE=InnoDB; + +INSERT INTO `aowow_home_titles` (editorId, editDate, active, locale, title) VALUES + (0, 1522321542, 1, 0, 'That\'s a 50 DKP plus!'), + (0, 1522321542, 1, 0, 'We\'ve got what you need!'), + (0, 1522321542, 1, 0, 'You haven\'t found the secret title yet.'), + (0, 1522321542, 1, 0, '...and knowing is half the battle!'), + (0, 1522321542, 1, 0, 'Good news, everyone!'), + (0, 1522321542, 1, 0, '+1, Insightful'), + (0, 1522321542, 1, 0, 'More effective than a [Booterang].'), + (0, 1522321542, 1, 0, 'There is no cow level.'), + (0, 1522321542, 1, 0, 'We\'ve got more style than a fashion designer who knows CSS.'), + (0, 1522321542, 1, 3, 'Eure Fertigkeit in WoW hat sich auf 450 erhöht.'), + (0, 1522321542, 1, 0, 'If you use your mouse to search, you won\'t be able to click on Rend.'), + (0, 1522321542, 1, 2, 'Tout est dans l\'élégance.'), + (0, 1522321542, 1, 2, 'Rend les chargements supportables depuis 2006.'), + (0, 1522321542, 1, 2, 'Vous allez revenir.'), + (0, 1522321542, 1, 2, 'Base de données extraordinaire'), + (0, 1522321542, 1, 2, 'Si vous lisez ceci, arrêtez d\'appuyer sur F5.'), + (0, 1522321542, 1, 3, 'Und der Tag ist gerettet.'), + (0, 1522321542, 1, 3, 'Jetzt in allen bekannten Internetzen verfügbar!'), + (0, 1522321542, 1, 3, 'Morgens, halb drei in Nordend'), + (0, 1522321542, 1, 3, 'Macht auch Euren Webbrowser glücklich!'), + (0, 1522321542, 1, 3, 'Hier findet Ihr sogar Mankriks Frau.'), + (0, 1522321542, 1, 6, 'Base de datos extraordinaria de WoW'), + (0, 1522321542, 1, 6, 'La única cosa en la que los ninjas y los piratas estan de acuerdo.'), + (0, 1522321542, 1, 6, 'La elegancia lo es todo.'), + (0, 1522321542, 1, 6, 'Hace feliz a los navegadores.'), + (0, 1522321542, 1, 8, 'Ты ещё вернёшься.'), + (0, 1522321542, 1, 8, 'Осваивание нового босса - 45 золота на ремонт. Персональный эпический предмет - 650 золотых'), + (0, 1522321542, 1, 8, 'Не именной. Поделитесь им с друзьями!'), + (0, 1522321542, 1, 8, 'Если вы здесь впервые, то вам необходимо воспользоваться поиском!'), + (0, 1522321542, 1, 8, 'Приколы Мулгора без чата в Мулгоре.'), + (0, 1522321542, 1, 2, 'Les trois premières lettres veulent tout dire.'), + (0, 1522321542, 1, 2, 'Trouvez la femme de Mankrik grâce à lui.'), + (0, 1522321542, 1, 6, 'Tu habilidad con WoW se ha incrementado a 450.'), + (0, 1522321542, 1, 6, 'Buscando uno más: Tú'), + (0, 1522321542, 1, 8, 'Первые три буквы говорят сами за себя.'), + (0, 1522321542, 1, 8, 'У нас больше стиля, чем у дизайнера, знающего CSS.'), + (0, 1522321542, 1, 0, 'Preventing wipes since 2006.'), + (0, 1522321542, 1, 0, 'Never gonna give you up. Never gonna let you down.'), + (0, 1522321542, 1, 0, 'The closest thing to an F1 key for WoW.'), + (0, 1522321542, 1, 2, 'Non lié. Partagez-le avec vos amis !'), + (0, 1522321542, 1, 2, 'Votre navigateur l\'adore !'), + (0, 1522321542, 1, 3, 'Verhindert Wipes seit 2006.'), + (0, 1522321542, 1, 6, '+1, Utilidad'), + (0, 1522321542, 1, 6, 'Épico, como tu líder de facción.'), + (0, 1522321542, 1, 8, 'Он такой один...'), + (0, 1522321542, 1, 8, 'Если вы это читаете, то прекратите обновлять страницу.'), + (0, 1522321542, 1, 0, 'If you are reading this, stop pressing F5.'), + (0, 1522321542, 1, 2, 'Chasse les jours pluvieux.'), + (0, 1522321542, 1, 3, '+1, Hilfreich'), + (0, 1522321542, 1, 3, 'Episch - markant - dreifach verzaubert'), + (0, 1522321542, 1, 8, 'Работает как положено.'), + (0, 1522321542, 1, 0, 'Flagged for awesome.'), + (0, 1522321542, 1, 0, 'Thrall-tested, Jaina-approved.'), + (0, 1522321542, 1, 8, 'Всё дело в элегантности.'), + (0, 1522321542, 1, 0, 'What does it mean?'), + (0, 1522321542, 1, 0, 'YOU ARE NOW PREPARED!'), + (0, 1522321542, 1, 0, 'srsly'), + (0, 1522321542, 1, 2, 'C\'est comme prétendre être malade et aller à la plage, mais pour les bases de données.'), + (0, 1522321542, 1, 3, 'Thrall-getestet, Jaina-genehmigt'), + (0, 1522321542, 1, 6, 'Haciendo las pantallas de carga más soportables desde el 2006'), + (0, 1522321542, 1, 8, 'Создан быть лидером.'), + (0, 1522321542, 1, 0, 'You\'ll say "Wow" every time.'), + (0, 1522321542, 1, 0, 'Dataz! We need more dataz!'), + (0, 1522321542, 1, 0, 'Your skill in WoW has increased to 450.'), + (0, 1522321542, 1, 3, 'Eleganz ist alles.'), + (0, 1522321542, 1, 8, '+1, Полезный'), + (0, 1522321542, 1, 8, 'Ух ты!'), + (0, 1522321542, 1, 0, 'Sometimes there is fire. You need to not be in it.'), + (0, 1522321542, 1, 0, 'Working as intended.'), + (0, 1522321542, 1, 2, 'La seule chose sur laquelle les ninjas et les pirates sont d\'accord.'), + (0, 1522321542, 1, 3, 'Nicht seelengebunden. Teilt es mit Euren Freunden!'), + (0, 1522321542, 1, 8, 'Теперь доступен во всех известных Интернетах!'), + (0, 1522321542, 1, 8, 'Вы получаете добычу: [Легендарное Знание]'), + (0, 1522321542, 1, 0, 'You\'ll be back.'), + (0, 1522321542, 1, 0, 'Epic like your faction leader.'), + (0, 1522321542, 1, 3, 'Manchmal gibt es Feuer. Ihr dürft nicht drin stehen.'), + (0, 1522321542, 1, 3, 'Wer das hier lesen kann, drückt zu oft F5.'), + (0, 1522321542, 1, 6, '¡Datos! ¡Más Datos!'), + (0, 1522321542, 1, 8, 'НЯМ НЯМ НЯМ'), + (0, 1522321542, 1, 2, 'Testé par Thrall, approuvé par Jaina.'), + (0, 1522321542, 1, 8, 'Сделайте его вашей новой расовой возможностью уже сегодня!'), + (0, 1522321542, 1, 0, 'We do math, so you don\'t have to.'), + (0, 1522321542, 1, 0, 'OM NOM NOM'), + (0, 1522321542, 1, 0, 'Now available on all known internets!'), + (0, 1522321542, 1, 0, 'We brake for dataz.'), + (0, 1522321542, 1, 3, 'Neues von der Obstverkäuferfront'), + (0, 1522321542, 1, 6, 'Las primeras tres palabras lo dicen todo.'), + (0, 1522321542, 1, 8, 'Это как будто сказать всем, что ты болен, а самому пойти на пляж, - только для баз данных.'), + (0, 1522321542, 1, 8, 'Меняем семечки на данные!'), + (0, 1522321542, 1, 0, 'It\'s all about elegance.'), + (0, 1522321542, 1, 0, 'Never underestimate the power of the Scout\'s code.'), + (0, 1522321542, 1, 6, 'Elimina los días lluviosos.'), + (0, 1522321542, 1, 0, 'You just won the game.'), + (0, 1522321542, 1, 8, 'Данные! Нам надо больше данных!'), + (0, 1522321542, 1, 0, 'WoW Database Extraordinaire'), + (0, 1522321542, 1, 0, 'No longer soulbound. Can now be shared with friends!'), + (0, 1522321542, 1, 0, 'The dataz you could be using.'), + (0, 1522321542, 1, 8, 'Превосходен, как лидер вашей фракции.'), + (0, 1522321542, 1, 6, '¡Regresarás!'); diff --git a/template/pages/home.tpl.php b/template/pages/home.tpl.php index b76a8abf..39c82ef0 100644 --- a/template/pages/home.tpl.php +++ b/template/pages/home.tpl.php @@ -6,13 +6,19 @@
-featuredBox['altHomeLogo'])): ?> - +homeTitle): + echo " \n"; +endif; + +if (!empty($this->featuredBox['altHomeLogo'])): +?> +

Aowow

From fab71f9325c2d13a1da39adcb200a18d72302649 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 29 Mar 2018 13:52:08 +0200 Subject: [PATCH 009/957] Misc * don't cache playlists * don't cache new custom profiles * forgot to sanitize and use param from js --- includes/ajaxHandler/profile.class.php | 8 ++++++++ pages/profile.php | 5 +++++ pages/sound.php | 1 + 3 files changed, 14 insertions(+) diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php index 5c6436cc..039121ae 100644 --- a/includes/ajaxHandler/profile.class.php +++ b/includes/ajaxHandler/profile.class.php @@ -14,6 +14,7 @@ class AjaxProfile extends AjaxHandler 'size' => [FILTER_SANITIZE_STRING, 0xC], // FILTER_FLAG_STRIP_LOW | *_HIGH 'guild' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkEmptySet']], 'arena-team' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkEmptySet']], + 'user' => [FILTER_CALLBACK, ['options' => 'AjaxProfile::checkUser']] ); protected $_post = array( @@ -714,6 +715,13 @@ class AjaxProfile extends AjaxHandler return null; } + protected function checkUser($val) + { + if (User::isValidName($val)) + return $val + + return null; + } } ?> diff --git a/pages/profile.php b/pages/profile.php index b596832f..1e9c4b7c 100644 --- a/pages/profile.php +++ b/pages/profile.php @@ -29,6 +29,9 @@ class ProfilePage extends GenericPage public function __construct($pageCall, $pageParam) { + if (!CFG_PROFILER_ENABLE) + $this->error(); + $params = array_map('urldecode', explode('.', $pageParam)); if ($params[0]) $params[0] = Profiler::urlize($params[0]); @@ -106,6 +109,8 @@ class ProfilePage extends GenericPage } else if (($params && $params[0]) || !isset($_GET['new'])) $this->notFound(); + else if (isset($_GET['new'])) + $this->mode = CACHE_TYPE_NONE; } protected function generateContent() diff --git a/pages/sound.php b/pages/sound.php index 9ec120e7..7bf5b4a8 100644 --- a/pages/sound.php +++ b/pages/sound.php @@ -31,6 +31,7 @@ class SoundPage extends GenericPage $this->cat = 1000; $this->articleUrl = 'sound&playlist'; $this->hasComContent = false; + $this->mode = CACHE_TYPE_NONE; } // regular case else From 0912248fd57e6600cb62269096b1fba8cfc43f69 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 29 Mar 2018 18:58:22 +0200 Subject: [PATCH 010/957] Misc/Typos --- includes/ajaxHandler/profile.class.php | 2 +- pages/guild.php | 2 +- prQueue | 2 +- static/js/Profiler.js | 13 ++++++++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php index 039121ae..e2fcd183 100644 --- a/includes/ajaxHandler/profile.class.php +++ b/includes/ajaxHandler/profile.class.php @@ -718,7 +718,7 @@ class AjaxProfile extends AjaxHandler protected function checkUser($val) { if (User::isValidName($val)) - return $val + return $val; return null; } diff --git a/pages/guild.php b/pages/guild.php index 7696e99b..9c9f7d2f 100644 --- a/pages/guild.php +++ b/pages/guild.php @@ -118,7 +118,7 @@ class GuildPage extends GenericPage /**************/ // tab: members - $member = new LocalProfileList(array(['p.guild', $this->subjectGUID])); + $member = new LocalProfileList(array(['p.guild', $this->subjectGUID], CFG_SQL_LIMIT_NONE)); if (!$member->error) { $this->lvTabs[] = ['profile', array( diff --git a/prQueue b/prQueue index ad162fd7..e26d2f65 100755 --- a/prQueue +++ b/prQueue @@ -87,7 +87,7 @@ while (DB::Aowow()->selectCell('SELECT value FROM ?_config WHERE `key` = "profil case TYPE_GUILD: if (!Profiler::getGuildFromRealm($row['realm'], $row['realmGUID'])) { - $error(TYPE_ARENA_GUILD, $row['realmGUID'], $row['realm']); + $error(TYPE_GUILD, $row['realmGUID'], $row['realm']); continue 2; } diff --git a/static/js/Profiler.js b/static/js/Profiler.js index 04508848..eea038c8 100644 --- a/static/js/Profiler.js +++ b/static/js/Profiler.js @@ -4659,14 +4659,21 @@ function ProfilerInventory(_parent) { _mvInited = true; } - else { /* aowow: the idea of this is to directly access the swf. it just doesn't work with the ZAMviewerfp11.swf */ + /* + aowow: the idea of this is to directly access the swf. + though ZAMviewerfp11.swf is unpredictable af + + custom: try/catch; check for empty equipList + */ + else { _swfModel = $WH.ge(_swfModel.id); if (_swfModel.clearSlots) { _swfModel.setAppearance(_profile.hairstyle, _profile.haircolor, _profile.facetype, _profile.skincolor, _profile.features, _profile.haircolor); - _swfModel.clearSlots(emptySlots); - _swfModel.attachList(equipList); + try { _swfModel.clearSlots(emptySlots); } catch (x) { } + if (equipList.length) + _swfModel.attachList(equipList); } } } From fa46aefa27ac013ad5dddde028536d2d932dbde1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 29 Mar 2018 21:16:53 +0200 Subject: [PATCH 011/957] Modelviewer * removed quality options (java .. holy crap!) * added animations menu --- static/css/aowow.css | 36 +-- static/css/locale_dede.css | 9 - static/css/locale_enus.css | 9 - static/css/locale_eses.css | 9 - static/css/locale_frfr.css | 9 - static/css/locale_ruru.css | 9 - static/js/global.js | 436 ++++++++++++++++--------------------- 7 files changed, 195 insertions(+), 322 deletions(-) diff --git a/static/css/aowow.css b/static/css/aowow.css index 5f012512..1a54e25d 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -2534,36 +2534,9 @@ div.modelviewer-screen { margin: 0 auto; } -div.modelviewer-quality { - margin: 10px 10px 5px 0; - line-height: 22px; - height: 22px; - display: block; - float: left; -} - -div.modelviewer-quality var { - float: left; - height: 22px; - display: block; - color: #a0a0a0; - text-transform: uppercase; - text-decoration: none; - font-size: 15px; - font-style: normal; - margin-right: 10px; - font-family: Tahoma, sans-serif; -} - -div.modelviewer-quality span { - float: left; - display: block; - line-height: 22px; -} - div.modelviewer-model { -/* clear: left; - padding: 5px 0 10px 0; */ + clear: left; + padding: 5px 0 10px 0; padding: 10px 0; } @@ -2586,13 +2559,16 @@ a.modelviewer-help { display: block; padding: 10px 0 10px 10px; margin-right: 10px; + margin-top:5px; } a.modelviewer-close { float: right; display: block; padding: 10px 0 10px 10px; -} + clear:right; + margin-top:5px; + } /**********/ /* LAYOUT */ diff --git a/static/css/locale_dede.css b/static/css/locale_dede.css index 6004c159..ee898302 100644 --- a/static/css/locale_dede.css +++ b/static/css/locale_dede.css @@ -1,13 +1,4 @@ -div.modelviewer-quality div { - float: left; - width: 71px; - height: 22px; - margin-right: 10px; - background: url(../images/dede/modelviewer-picshures.gif) left center no-repeat; -} - div.modelviewer-model div { - margin-left: 25px; margin-right: 10px; float: left; width: 50px; diff --git a/static/css/locale_enus.css b/static/css/locale_enus.css index 9e5d204a..90cf74ef 100644 --- a/static/css/locale_enus.css +++ b/static/css/locale_enus.css @@ -1,13 +1,4 @@ -div.modelviewer-quality div { - float: left; - width: 61px; - height: 22px; - margin-right: 10px; - background: url(../images/enus/modelviewer-picshures.gif) left center no-repeat; -} - div.modelviewer-model div { - margin-left: 40px; margin-right: 10px; float: left; width: 50px; diff --git a/static/css/locale_eses.css b/static/css/locale_eses.css index 2fd01db7..a338b0a9 100644 --- a/static/css/locale_eses.css +++ b/static/css/locale_eses.css @@ -3,16 +3,7 @@ width, left, background-position values may need adjustment! */ -div.modelviewer-quality div { - float: left; - width: 70px; - height: 22px; - margin-right: 10px; - background: url(../images/eses/modelviewer-picshures.gif) left center no-repeat; -} - div.modelviewer-model div { - margin-left: 40px; margin-right: 5px; float: left; width: 80px; diff --git a/static/css/locale_frfr.css b/static/css/locale_frfr.css index 46ba8b21..627ac340 100644 --- a/static/css/locale_frfr.css +++ b/static/css/locale_frfr.css @@ -1,13 +1,4 @@ -div.modelviewer-quality div { - float: left; - width: 61px; - height: 22px; - margin-right: 10px; - background: url(../images/frfr/modelviewer-picshures.gif) left center no-repeat; -} - div.modelviewer-model div { - margin-left: 25px; margin-right: 10px; float: left; width: 58px; diff --git a/static/css/locale_ruru.css b/static/css/locale_ruru.css index d9891b9b..137c1834 100644 --- a/static/css/locale_ruru.css +++ b/static/css/locale_ruru.css @@ -1,13 +1,4 @@ -div.modelviewer-quality div { - float: left; - width: 76px; - height: 22px; - margin-right: 10px; - background: url(../images/ruru/modelviewer-picshures.gif) left center no-repeat; -} - div.modelviewer-model div { - margin-left: 40px; margin-right: 10px; float: left; width: 61px; diff --git a/static/js/global.js b/static/js/global.js index 5e9081b3..21ce8bf7 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -19886,9 +19886,6 @@ var ModelViewer = new function() { modelType, equipList = [], optBak, - _w, - _o, - _z, modelDiv, raceSel1, raceSel2, @@ -19896,6 +19893,7 @@ var ModelViewer = new function() { oldHash, mode, readExtraPound, + animsLoaded = false, races = [ {id: 10, name: g_chr_races[10], model: 'bloodelf' }, @@ -19915,25 +19913,19 @@ var ModelViewer = new function() { {id: 1, name: LANG.female, model: 'female' } ]; - function clear() { - _w.style.display = 'none'; - _o.style.display = 'none'; - _z.style.display = 'none'; - } + function clear() { } function getRaceSex() { var race, sex; - if (raceSel1.style.display == '') { - race = (raceSel1.selectedIndex >= 0 ? raceSel1.options[raceSel1.selectedIndex].value : ''); - } - else { - race = (raceSel2.selectedIndex >= 0 ? raceSel2.options[raceSel2.selectedIndex].value : ''); - } + if (raceSel1.is(':visible')) + race = (raceSel1[0].selectedIndex >= 0 ? raceSel1.val() : ''); + else + race = (raceSel2[0].selectedIndex >= 0 ? raceSel2.val() : ''); - sex = (sexSel.selectedIndex >= 0 ? sexSel.options[sexSel.selectedIndex].value : 0); + sex = (sexSel[0].selectedIndex >= 0 ? sexSel.val() : 0); return { r: race, s: sex }; } @@ -19945,54 +19937,31 @@ var ModelViewer = new function() { } function render() { - if (mode == 2 && !f()) { - mode = 0; - } - if (mode == 2) { - var G = ''; - if (modelType == 16 && equipList.length) { - G += ''; - } - G += ''; - _z.innerHTML = G; - _z.style.display = ''; - } - else if (mode == 1) { - var G = ''; - if (modelType == 16 && equipList.length) { - G += ''; - } - G += ''; - _o.innerHTML = G; - _o.style.display = ''; - } - else { - var flashVars = { - model: model, - modelType: modelType, - // contentPath: 'http://static.wowhead.com/modelviewer/' - contentPath: g_staticUrl + '/modelviewer/' - }; + var flashVars = { + model: model, + modelType: modelType, + // contentPath: 'http://static.wowhead.com/modelviewer/' + contentPath: g_staticUrl + '/modelviewer/' + }; - var params = { - quality: 'high', - allowscriptaccess: 'always', - allowfullscreen: true, - menu: false, - bgcolor: '#181818', - wmode: 'direct' - }; + var params = { + quality: 'high', + allowscriptaccess: 'always', + allowfullscreen: true, + menu: false, + bgcolor: '#181818', + wmode: 'direct' + }; - var attributes = { }; + var attributes = { }; - if (modelType == 16 && equipList.length) { - flashVars.equipList = equipList.join(','); - } - - // swfobject.embedSWF('http://static.wowhead.com/modelviewer/ZAMviewerfp11.swf', 'modelviewer-generic', '600', '400', "11.0.0", 'http://static.wowhead.com/modelviewer/expressInstall.swf', flashVars, params, attributes); - swfobject.embedSWF(g_staticUrl + '/modelviewer/ZAMviewerfp11.swf', 'modelviewer-generic', '600', '400', "11.0.0", g_staticUrl + '/modelviewer/expressInstall.swf', flashVars, params, attributes); - _w.style.display = ''; + if (modelType == 16 && equipList.length) { + flashVars.equipList = equipList.join(','); } + + // swfobject.embedSWF('http://static.wowhead.com/modelviewer/ZAMviewerfp11.swf', 'modelviewer-generic', '600', '400', "11.0.0", 'http://static.wowhead.com/modelviewer/expressInstall.swf', flashVars, params, attributes); + swfobject.embedSWF(g_staticUrl + '/modelviewer/ZAMviewerfp11.swf', 'modelviewer-generic', '600', '400', "11.0.0", g_staticUrl + '/modelviewer/expressInstall.swf', flashVars, params, attributes); + var foo = getRaceSex(), race = foo.r, @@ -20027,6 +19996,9 @@ var ModelViewer = new function() { if (optBak.extraPound != null) { url += ':' + optBak.extraPound; } + + animsLoaded = false, + location.replace($WH.rtrim(url, ':')); } } @@ -20038,11 +20010,10 @@ var ModelViewer = new function() { sex = foo.s; if (!race) { - if (sexSel.style.display == 'none') { + if (!sexSel.is(':visible')) return; - } - sexSel.style.display = 'none'; + sexSel.hide(); model = equipList[1]; switch (optBak.slot) { @@ -20057,15 +20028,12 @@ var ModelViewer = new function() { } } else { - if (sexSel.style.display == 'none') { - sexSel.style.display = ''; - } + if (!sexSel.is(':visible')) + sexSel.show(); - var foo = function(x) { - return x.id; - }; + var foo = function(x) { return x.id; }; var raceIndex = $WH.in_array(races, race, foo); - var sexIndex = $WH.in_array(sexes, sex, foo); + var sexIndex = $WH.in_array(sexes, sex, foo); if (raceIndex != -1 && sexIndex != -1) { model = races[raceIndex].model + sexes[sexIndex].model; @@ -20079,25 +20047,50 @@ var ModelViewer = new function() { render(); } - function j(newMode) { - if (newMode == mode) { + function onAnimationChange() { + var viewer = $('#modelviewer-generic'); + if (viewer.length == 0) + return; + viewer = viewer[0]; + + var animList = $('select', animDiv); + if (animList.val() && viewer.isLoaded && viewer.isLoaded()) + viewer.setAnimation(animList.val()); + } + + function onAnimationMouseover() { + if (animsLoaded) + return; + + var viewer = $('#modelviewer-generic'); + if (viewer.length == 0) + return; + viewer = viewer[0]; + + var animList = $('select', animDiv); + animList.empty(); + if (!viewer.isLoaded || !viewer.isLoaded()) { + animList.append($('
\n"; endif; ?> diff --git a/template/pages/guide-edit.tpl.php b/template/pages/guide-edit.tpl.php index 8122c7f9..f9ae7c5e 100644 --- a/template/pages/guide-edit.tpl.php +++ b/template/pages/guide-edit.tpl.php @@ -54,7 +54,7 @@ $this->brick('pageTemplate'); $l): - if (Cfg::get('LOCALES') & (1 << $i)) - echo ' \n"; +foreach (Locale::cases() as $l): + if ($l->validate()): + echo ' \n"; + endif; endforeach; ?> @@ -68,8 +69,9 @@ endforeach; diff --git a/template/pages/text-page-generic.tpl.php b/template/pages/text-page-generic.tpl.php index f68388d4..a43edcc5 100644 --- a/template/pages/text-page-generic.tpl.php +++ b/template/pages/text-page-generic.tpl.php @@ -9,7 +9,7 @@ $this->brick('announcement'); $this->brick('pageTemplate'); -if (isset($this->notFound)): +if ($this->notFound): ?> doResync)): From 4ccf917707ecd75b3b4ff660803b37ae7cff8c79 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 29 Mar 2025 14:14:23 +0100 Subject: [PATCH 579/957] Listview/ReplyPreview * fix links to parent comment * implement go-to-reply redirect --- includes/ajaxHandler/gotocomment.class.php | 2 +- includes/community.class.php | 2 +- index.php | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/ajaxHandler/gotocomment.class.php b/includes/ajaxHandler/gotocomment.class.php index bc9481fa..25f4d176 100644 --- a/includes/ajaxHandler/gotocomment.class.php +++ b/includes/ajaxHandler/gotocomment.class.php @@ -26,7 +26,7 @@ class AjaxGotocomment extends AjaxHandler if (!$this->_get['id']) return '.'; // go home - if ($_ = DB::Aowow()->selectRow('SELECT IFNULL(c2.id, c1.id) AS id, IFNULL(c2.type, c1.type) AS type, IFNULL(c2.typeId, c1.typeId) AS typeId FROM ?_comments c1 LEFT JOIN ?_comments c2 ON c1.replyTo = c2.id WHERE c1.id = ?d', $this->_get['id'])) + if ($_ = DB::Aowow()->selectRow('SELECT IFNULL(c2.`id`, c1.`id`) AS "id", IFNULL(c2.`type`, c1.`type`) AS "type", IFNULL(c2.`typeId`, c1.`typeId`) AS "typeId" FROM ?_comments c1 LEFT JOIN ?_comments c2 ON c1.`replyTo` = c2.`id` WHERE c1.`id` = ?d', $this->_get['id'])) return '?'.Type::getFileString(intVal($_['type'])).'='.$_['typeId'].'#comments:id='.$_['id'].($_['id'] != $this->_get['id'] ? ':reply='.$this->_get['id'] : null); else trigger_error('AjaxGotocomment::handleGoToComment - could not find comment #'.$this->_get['id'], E_USER_ERROR); diff --git a/includes/community.class.php b/includes/community.class.php index 81225bf8..c1d0c23d 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -162,7 +162,7 @@ class CommunityContent $c['date'] = $dateFmt ? date(Util::$dateFormatInternal, $c['date']) : intVal($c['date']); // remove commentid if not looking for replies - if (empty($params['replies'])) + if (empty($opt['replies'])) unset($c['commentid']); // format text for listview diff --git a/index.php b/index.php index 74c9dfbc..048c243f 100644 --- a/index.php +++ b/index.php @@ -89,6 +89,9 @@ switch ($pageCall) case 'edit': // guide editor: targeted by QQ fileuploader, detail-page article editor case 'get-description': // guide editor: shorten fulltext into description case 'filter': // pre-evaluate filter POST-data; sanitize and forward as GET-data + case 'go-to-reply': // find page the reply is on and forward + if ($pageCall == 'go-to-reply') + $altClass = 'go-to-comment'; case 'go-to-comment': // find page the comment is on and forward case 'locale': // subdomain-workaround, change the language $cleanName = str_replace(['-', '_'], '', ucFirst($altClass ?: $pageCall)); From 3a6c86092b73158d8a05ba5a4cc9cde2f1bfd289 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 31 Mar 2025 14:44:44 +0200 Subject: [PATCH 580/957] Core/Compat * create namespace Aowow to avoid naming conflicts * inclues/libs/ is outside of the Aowow namespace --- includes/ajaxHandler.class.php | 2 + includes/ajaxHandler/account.class.php | 8 ++- includes/ajaxHandler/admin.class.php | 36 +++++----- includes/ajaxHandler/arenateam.class.php | 6 +- includes/ajaxHandler/comment.class.php | 30 +++++---- includes/ajaxHandler/contactus.class.php | 20 +++--- includes/ajaxHandler/cookie.class.php | 4 +- includes/ajaxHandler/data.class.php | 14 ++-- includes/ajaxHandler/edit.class.php | 10 +-- includes/ajaxHandler/filter.class.php | 2 + includes/ajaxHandler/getdescription.class.php | 4 +- includes/ajaxHandler/gotocomment.class.php | 4 +- includes/ajaxHandler/guide.class.php | 2 + includes/ajaxHandler/guild.class.php | 6 +- includes/ajaxHandler/locale.class.php | 4 +- includes/ajaxHandler/profile.class.php | 30 +++++---- includes/basetype.class.php | 8 ++- includes/community.class.php | 2 + .../Conditions/Conditions.class.php | 2 + includes/components/SmartAI/SmartAI.class.php | 4 +- .../components/SmartAI/SmartAction.class.php | 2 + .../components/SmartAI/SmartEvent.class.php | 2 + .../components/SmartAI/SmartTarget.class.php | 2 + includes/config.class.php | 12 ++-- includes/database.class.php | 6 +- includes/defines.php | 2 + includes/game.php | 4 +- includes/kernel.php | 34 ++++++---- includes/locale.class.php | 6 +- includes/loot.class.php | 2 + includes/markup.class.php | 2 + includes/profiler.class.php | 2 + includes/stats.class.php | 2 + includes/types/achievement.class.php | 2 + includes/types/areatrigger.class.php | 2 + includes/types/arenateam.class.php | 2 + includes/types/charclass.class.php | 2 + includes/types/charrace.class.php | 2 + includes/types/creature.class.php | 2 + includes/types/currency.class.php | 2 + includes/types/emote.class.php | 2 + includes/types/enchantment.class.php | 2 + includes/types/faction.class.php | 2 + includes/types/gameobject.class.php | 2 + includes/types/guide.class.php | 2 + includes/types/guild.class.php | 2 + includes/types/icon.class.php | 2 + includes/types/item.class.php | 4 +- includes/types/itemset.class.php | 2 + includes/types/mail.class.php | 2 + includes/types/pet.class.php | 2 + includes/types/profile.class.php | 2 + includes/types/quest.class.php | 2 + includes/types/skill.class.php | 2 + includes/types/sound.class.php | 2 + includes/types/spell.class.php | 2 + includes/types/title.class.php | 2 + includes/types/user.class.php | 2 + includes/types/worldevent.class.php | 2 + includes/types/zone.class.php | 2 + includes/user.class.php | 8 ++- includes/utilities.php | 66 ++++++++++--------- index.php | 10 +-- localization/lang.class.php | 2 + localization/locale_dede.php | 2 + localization/locale_enus.php | 2 + localization/locale_eses.php | 2 + localization/locale_frfr.php | 2 + localization/locale_ruru.php | 2 + localization/locale_zhcn.php | 2 + pages/account.php | 8 ++- pages/achievement.php | 6 +- pages/achievements.php | 2 + pages/admin.php | 6 +- pages/areatrigger.php | 2 + pages/areatriggers.php | 2 + pages/arenateam.php | 2 + pages/arenateams.php | 2 + pages/class.php | 2 + pages/classes.php | 2 + pages/compare.php | 6 +- pages/currencies.php | 2 + pages/currency.php | 6 +- pages/emote.php | 2 + pages/emotes.php | 2 + pages/enchantment.php | 2 + pages/enchantments.php | 2 + pages/event.php | 6 +- pages/events.php | 2 + pages/faction.php | 2 + pages/factions.php | 2 + pages/genericPage.class.php | 14 ++-- pages/guide.php | 30 +++++---- pages/guides.php | 2 + pages/guild.php | 2 + pages/guilds.php | 2 + pages/home.php | 2 + pages/icon.php | 2 + pages/icons.php | 2 + pages/item.php | 14 ++-- pages/items.php | 2 + pages/itemset.php | 6 +- pages/itemsets.php | 2 + pages/mail.php | 2 + pages/mails.php | 2 + pages/maps.php | 2 + pages/more.php | 2 + pages/npc.php | 6 +- pages/npcs.php | 2 + pages/object.php | 6 +- pages/objects.php | 2 + pages/pet.php | 2 + pages/pets.php | 2 + pages/profile.php | 8 ++- pages/profiler.php | 2 + pages/profiles.php | 2 + pages/quest.php | 6 +- pages/quests.php | 2 + pages/race.php | 2 + pages/races.php | 2 + pages/screenshot.php | 8 ++- pages/search.php | 18 +++-- pages/skill.php | 2 + pages/skills.php | 2 + pages/sound.php | 4 +- pages/sounds.php | 2 + pages/spell.php | 6 +- pages/spells.php | 2 + pages/talent.php | 2 + pages/title.php | 2 + pages/titles.php | 2 + pages/user.php | 2 + pages/utility.php | 4 +- pages/zone.php | 2 + pages/zones.php | 4 +- prQueue | 2 + setup/setup.php | 2 + setup/tools/CLISetup.class.php | 16 +++-- setup/tools/clisetup/account.us.php | 2 + setup/tools/clisetup/datagen.us.php | 2 + setup/tools/clisetup/dbc.us.php | 2 + setup/tools/clisetup/dbconfig.us.php | 2 + setup/tools/clisetup/filegen.us.php | 2 + setup/tools/clisetup/setup.us.php | 4 +- setup/tools/clisetup/siteconfig.us.php | 2 + setup/tools/clisetup/sync.us.php | 2 + setup/tools/clisetup/update.us.php | 2 + setup/tools/dbc.class.php | 2 + setup/tools/filegen/demo.ss.php | 2 + setup/tools/filegen/enchants.ss.php | 2 + setup/tools/filegen/gems.ss.php | 2 + setup/tools/filegen/glyphs.ss.php | 2 + setup/tools/filegen/img-artwork.ss.php | 2 + setup/tools/filegen/img-maps.ss.php | 8 ++- setup/tools/filegen/img-talentcalc.ss.php | 2 + setup/tools/filegen/itemscaling.ss.php | 2 + setup/tools/filegen/itemsets.ss.php | 2 + setup/tools/filegen/locales.ss.php | 2 + setup/tools/filegen/markup.ss.php | 2 + setup/tools/filegen/pets.ss.php | 2 + setup/tools/filegen/profiler.ss.php | 4 +- setup/tools/filegen/realmmenu.ss.php | 2 + setup/tools/filegen/realms.ss.php | 2 + setup/tools/filegen/searchbox.ss.php | 2 + setup/tools/filegen/searchplugin.ss.php | 2 + setup/tools/filegen/simpleimg.ss.php | 2 + setup/tools/filegen/soundfiles.ss.php | 2 + setup/tools/filegen/statistics.ss.php | 2 + setup/tools/filegen/talentcalc.ss.php | 2 + setup/tools/filegen/talenticons.ss.php | 4 +- setup/tools/filegen/tooltips.ss.php | 2 + setup/tools/filegen/weightpresets.ss.php | 2 + setup/tools/imagecreatefromblp.func.php | 39 +++++++---- setup/tools/setupScript.class.php | 16 +++-- setup/tools/sqlgen/achievement.ss.php | 2 + setup/tools/sqlgen/achievementcriteria.ss.php | 2 + setup/tools/sqlgen/areatrigger.ss.php | 2 + setup/tools/sqlgen/classes.ss.php | 2 + setup/tools/sqlgen/creature.ss.php | 2 + setup/tools/sqlgen/currencies.ss.php | 2 + setup/tools/sqlgen/declinedword.ss.php | 2 + setup/tools/sqlgen/dungeonmap.ss.php | 2 + setup/tools/sqlgen/emotes.ss.php | 2 + setup/tools/sqlgen/events.ss.php | 2 + setup/tools/sqlgen/factions.ss.php | 2 + setup/tools/sqlgen/glyphproperties.ss.php | 2 + setup/tools/sqlgen/holidays.ss.php | 2 + setup/tools/sqlgen/icons.ss.php | 2 + setup/tools/sqlgen/itemenchantment.ss.php | 2 + .../sqlgen/itemenchantmentcondition.ss.php | 2 + setup/tools/sqlgen/itemextendedcost.ss.php | 2 + setup/tools/sqlgen/itemlimitcategory.ss.php | 2 + setup/tools/sqlgen/itemrandomenchant.ss.php | 2 + .../tools/sqlgen/itemrandomproppoints.ss.php | 2 + setup/tools/sqlgen/items.ss.php | 2 + setup/tools/sqlgen/itemset.ss.php | 2 + setup/tools/sqlgen/itemstats.ss.php | 2 + setup/tools/sqlgen/lock.ss.php | 2 + setup/tools/sqlgen/mailtemplate.ss.php | 2 + setup/tools/sqlgen/objects.ss.php | 2 + setup/tools/sqlgen/pet.ss.php | 2 + setup/tools/sqlgen/quests.ss.php | 2 + setup/tools/sqlgen/questsstartend.ss.php | 2 + setup/tools/sqlgen/races.ss.php | 2 + .../sqlgen/scalingstatdistribution.ss.php | 2 + setup/tools/sqlgen/scalingstatvalues.ss.php | 2 + setup/tools/sqlgen/shapeshiftforms.ss.php | 2 + setup/tools/sqlgen/skillline.ss.php | 2 + setup/tools/sqlgen/skilllineability.ss.php | 2 + setup/tools/sqlgen/soundemitter.ss.php | 2 + setup/tools/sqlgen/sounds.ss.php | 2 + setup/tools/sqlgen/source.ss.php | 2 + setup/tools/sqlgen/spawns.ss.php | 2 + setup/tools/sqlgen/spell.ss.php | 2 + setup/tools/sqlgen/spelldifficulty.ss.php | 2 + setup/tools/sqlgen/spellfocusobject.ss.php | 2 + setup/tools/sqlgen/spelloverride.ss.php | 2 + setup/tools/sqlgen/spellrange.ss.php | 2 + setup/tools/sqlgen/spellvariables.ss.php | 2 + setup/tools/sqlgen/summonproperties.ss.php | 2 + setup/tools/sqlgen/talents.ss.php | 2 + setup/tools/sqlgen/taxi.ss.php | 2 + setup/tools/sqlgen/titles.ss.php | 2 + setup/tools/sqlgen/totemcategory.ss.php | 2 + setup/tools/sqlgen/worldmaparea.ss.php | 2 + setup/tools/sqlgen/zones.ss.php | 2 + setup/tools/utilityScript.class.php | 2 + template/bricks/announcement.tpl.php | 4 +- template/bricks/article.tpl.php | 2 + template/bricks/book.tpl.php | 2 + template/bricks/contribute.tpl.php | 4 +- template/bricks/filter.tpl.php | 2 + template/bricks/footer.tpl.php | 2 + template/bricks/head.tpl.php | 2 + template/bricks/headIcons.tpl.php | 2 + template/bricks/header.tpl.php | 2 + template/bricks/headerMenu.tpl.php | 2 + template/bricks/infobox.tpl.php | 2 + template/bricks/lvTabs.tpl.php | 2 + template/bricks/mail.tpl.php | 2 + template/bricks/mapper.tpl.php | 2 + template/bricks/pageTemplate.tpl.php | 2 + template/bricks/reagentList.tpl.php | 2 + template/bricks/redButtons.tpl.php | 2 + template/bricks/rewards.tpl.php | 2 + template/bricks/series.tpl.php | 2 + template/bricks/tooltip.tpl.php | 2 + template/listviews/areatrigger.tpl.php | 2 + template/listviews/commentAdminCol.tpl.php | 2 + template/listviews/emote.tpl.php | 2 + template/listviews/enchantment.tpl.php | 2 + template/listviews/guideAdminCol.tpl.php | 2 + template/listviews/itemStandingCol.tpl.php | 2 + template/listviews/mail.tpl.php | 2 + template/listviews/membersCol.tpl.php | 2 + template/listviews/npcRepCol.tpl.php | 2 + template/listviews/petFoodCol.tpl.php | 2 + template/listviews/questRepCol.tpl.php | 2 + template/listviews/vendorRestockCol.tpl.php | 2 + template/localized/contrib_0.tpl.php | 2 + template/localized/contrib_2.tpl.php | 2 + template/localized/contrib_3.tpl.php | 2 + template/localized/contrib_4.tpl.php | 2 + template/localized/contrib_6.tpl.php | 2 + template/localized/contrib_8.tpl.php | 2 + template/localized/ssReminder_0.tpl.php | 2 + template/localized/ssReminder_3.tpl.php | 2 + template/localized/ssReminder_4.tpl.php | 2 + template/pages/acc-dashboard.tpl.php | 2 + template/pages/acc-recover.tpl.php | 2 + template/pages/acc-signIn.tpl.php | 2 + template/pages/acc-signUp.tpl.php | 2 + template/pages/achievement.tpl.php | 2 + template/pages/achievements.tpl.php | 2 + template/pages/admin/reports.tpl.php | 2 + template/pages/admin/screenshots.tpl.php | 2 + template/pages/admin/siteconfig.tpl.php | 2 + template/pages/admin/weight-presets.tpl.php | 2 + template/pages/areatriggers.tpl.php | 2 + template/pages/arena-teams.tpl.php | 2 + template/pages/compare.tpl.php | 2 + template/pages/detail-page-generic.tpl.php | 2 + template/pages/enchantment.tpl.php | 2 + template/pages/enchantments.tpl.php | 2 + template/pages/guide-edit.tpl.php | 2 + template/pages/guilds.tpl.php | 2 + template/pages/home.tpl.php | 2 + template/pages/icon.tpl.php | 2 + template/pages/icons.tpl.php | 2 + template/pages/item.tpl.php | 2 + template/pages/items.tpl.php | 2 + template/pages/itemset.tpl.php | 4 +- template/pages/itemsets.tpl.php | 2 + template/pages/list-page-generic.tpl.php | 2 + template/pages/maintenance.tpl.php | 2 + template/pages/maps.tpl.php | 2 + template/pages/npc.tpl.php | 2 + template/pages/npcs.tpl.php | 2 + template/pages/object.tpl.php | 2 + template/pages/objects.tpl.php | 2 + template/pages/privilege.tpl.php | 2 + template/pages/privileges.tpl.php | 2 + template/pages/profile.tpl.php | 2 + template/pages/profiler.tpl.php | 2 + template/pages/profiles.tpl.php | 2 + template/pages/quest.tpl.php | 2 + template/pages/quests.tpl.php | 2 + template/pages/roster.tpl.php | 2 + template/pages/screenshot.tpl.php | 4 +- template/pages/search.tpl.php | 2 + template/pages/sound.tpl.php | 2 + template/pages/sounds.tpl.php | 2 + template/pages/spell.tpl.php | 2 + template/pages/spells.tpl.php | 2 + template/pages/talent.tpl.php | 2 + template/pages/text-page-generic.tpl.php | 2 + template/pages/user.tpl.php | 2 + 317 files changed, 898 insertions(+), 243 deletions(-) diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index 27b6aae3..5094e92f 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_SANITIZE_NUMBER_INT], 'save' => ['filter' => FILTER_SANITIZE_NUMBER_INT], 'delete' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList'], - 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAccount::checkName' ], - 'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAccount::checkScale' ], + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList'], + 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAccount::checkName' ], + 'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAccount::checkScale' ], 'reset' => ['filter' => FILTER_SANITIZE_NUMBER_INT], 'mode' => ['filter' => FILTER_SANITIZE_NUMBER_INT], 'type' => ['filter' => FILTER_SANITIZE_NUMBER_INT], diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php index 8a88a375..4de03301 100644 --- a/includes/ajaxHandler/admin.class.php +++ b/includes/ajaxHandler/admin.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine' ], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdListUnsigned'], - 'key' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkKey' ], - 'all' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet' ], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkUser' ], - 'val' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob' ], - 'guid' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ], - 'area' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ], - 'floor' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ] + 'action' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ], + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], + 'key' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ], + 'all' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet' ], + 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], + 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkUser' ], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], + 'guid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], + 'area' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], + 'floor' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ] ); protected $_post = array( - 'alt' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob'], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ], - 'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkScale' ], - '__icon' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkKey' ], - 'status' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ], - 'msg' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob'] + 'alt' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'], + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], + 'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkScale' ], + '__icon' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ], + 'status' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], + 'msg' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/arenateam.class.php b/includes/ajaxHandler/arenateam.class.php index 715924bc..8d609e61 100644 --- a/includes/ajaxHandler/arenateam.class.php +++ b/includes/ajaxHandler/arenateam.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList' ], - 'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'], + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ], + 'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'], ); public function __construct(array $params) diff --git a/includes/ajaxHandler/comment.class.php b/includes/ajaxHandler/comment.class.php index 12bf766e..2f77b0f1 100644 --- a/includes/ajaxHandler/comment.class.php +++ b/includes/ajaxHandler/comment.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdListUnsigned'], - 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob' ], - 'commentbody' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob' ], - 'response' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob' ], - 'reason' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob' ], - 'remove' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'commentId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'replyId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'sticky' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - // 'username' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine' ] + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], + 'commentbody' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], + 'response' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], + 'reason' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], + 'remove' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'commentId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'replyId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'sticky' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + // 'username' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ] ); protected $_get = array( - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'], - 'rating' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'], + 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'], + 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'], + 'rating' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/contactus.class.php b/includes/ajaxHandler/contactus.class.php index a21c1eed..c665cfa6 100644 --- a/includes/ajaxHandler/contactus.class.php +++ b/includes/ajaxHandler/contactus.class.php @@ -1,20 +1,22 @@ ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'reason' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'ua' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'], - 'appname' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'], - 'page' => ['filter' => FILTER_SANITIZE_URL ], - 'desc' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob'], - 'id' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'relatedurl' => ['filter' => FILTER_SANITIZE_URL ], - 'email' => ['filter' => FILTER_SANITIZE_EMAIL ] + 'mode' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'reason' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'ua' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], + 'appname' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], + 'page' => ['filter' => FILTER_SANITIZE_URL ], + 'desc' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'], + 'id' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'relatedurl' => ['filter' => FILTER_SANITIZE_URL ], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL ] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/cookie.class.php b/includes/ajaxHandler/cookie.class.php index 066b742c..5e018e2b 100644 --- a/includes/ajaxHandler/cookie.class.php +++ b/includes/ajaxHandler/cookie.class.php @@ -1,5 +1,7 @@ _get = array( - $params[0] => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'], + $params[0] => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], ); // NOW we know, what to expect and sanitize diff --git a/includes/ajaxHandler/data.class.php b/includes/ajaxHandler/data.class.php index ec57e1ea..0dd74805 100644 --- a/includes/ajaxHandler/data.class.php +++ b/includes/ajaxHandler/data.class.php @@ -1,17 +1,19 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFrom' ], - 't' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'], - 'catg' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'skill' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxData::checkSkill' ], - 'class' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'callback' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxData::checkCallback' ] + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFrom' ], + 't' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], + 'catg' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'skill' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxData::checkSkill' ], + 'class' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], + 'callback' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxData::checkCallback' ] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/edit.class.php b/includes/ajaxHandler/edit.class.php index e8f31539..4bf2b34f 100644 --- a/includes/ajaxHandler/edit.class.php +++ b/includes/ajaxHandler/edit.class.php @@ -1,13 +1,15 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'], - 'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] + 'qqfile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], + 'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] ); public function __construct(array $params) @@ -41,12 +43,12 @@ class AjaxEdit extends AjaxHandler $tmpPath = 'static/uploads/temp/'; $tmpFile = User::$displayName.'-'.Type::GUIDE.'-0-'.Util::createHash(16); - $uploader = new qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); + $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); $result = $uploader->handleUpload($tmpPath, $tmpFile, true); if (isset($result['success'])) { - $finfo = new finfo(FILEINFO_MIME); + $finfo = new \finfo(FILEINFO_MIME); $mime = $finfo->file($tmpPath.$result['newFilename']); if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) { diff --git a/includes/ajaxHandler/filter.class.php b/includes/ajaxHandler/filter.class.php index c02e80eb..951a84be 100644 --- a/includes/ajaxHandler/filter.class.php +++ b/includes/ajaxHandler/filter.class.php @@ -1,5 +1,7 @@ [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkTextBlob']] + 'description' => [FILTER_CALLBACK, ['options' => 'Aowow\AjaxHandler::checkTextBlob']] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/gotocomment.class.php b/includes/ajaxHandler/gotocomment.class.php index 25f4d176..915c9102 100644 --- a/includes/ajaxHandler/gotocomment.class.php +++ b/includes/ajaxHandler/gotocomment.class.php @@ -1,12 +1,14 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'] + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/guide.class.php b/includes/ajaxHandler/guide.class.php index 5bbbd04c..eb425f15 100644 --- a/includes/ajaxHandler/guide.class.php +++ b/includes/ajaxHandler/guide.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList' ], - 'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'], + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ], + 'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'], ); public function __construct(array $params) diff --git a/includes/ajaxHandler/locale.class.php b/includes/ajaxHandler/locale.class.php index e89fb33a..6dae7e95 100644 --- a/includes/ajaxHandler/locale.class.php +++ b/includes/ajaxHandler/locale.class.php @@ -1,12 +1,14 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFrom'] + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFrom'] ); public function __construct(array $params) diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php index c636a046..a3bd2435 100644 --- a/includes/ajaxHandler/profile.class.php +++ b/includes/ajaxHandler/profile.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList' ], - 'items' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkItemList'], - 'size' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'], - 'guild' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'], - 'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkUser' ] + 'id' => ['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' => 'AjaxHandler::checkTextLine'], + '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], @@ -28,17 +30,17 @@ class AjaxProfile extends AjaxHandler 'talenttree2' => ['filter' => FILTER_SANITIZE_NUMBER_INT], 'talenttree3' => ['filter' => FILTER_SANITIZE_NUMBER_INT], 'activespec' => ['filter' => FILTER_SANITIZE_NUMBER_INT], - 'talentbuild1' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkTalentString'], - 'glyphs1' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkGlyphString' ], - 'talentbuild2' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkTalentString'], - 'glyphs2' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkGlyphString' ], - 'icon' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine' ], - 'description' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob' ], + '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' => 'AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY], + 'inv' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY], ); public function __construct(array $params) diff --git a/includes/basetype.class.php b/includes/basetype.class.php index 02688a99..2398fab1 100644 --- a/includes/basetype.class.php +++ b/includes/basetype.class.php @@ -1,5 +1,7 @@ matches += DB::{$n}($dbIdx)->selectCell('SELECT COUNT(*) FROM ('.$totalQuery.') x'); @@ -581,7 +583,7 @@ trait spawnHelper private function createShortSpawns() // [zoneId, floor, [[x1, y1], [x2, y2], ..]] as tooltip2 if enabled by or anchor #map (one area, one floor, one creature, no survivors) { - $this->spawnResult[SPAWNINFO_SHORT] = new StdClass; + $this->spawnResult[SPAWNINFO_SHORT] = new \StdClass; // first get zone/floor with the most spawns if ($res = DB::Aowow()->selectRow('SELECT `areaId`, `floor` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d AND `posX` > 0 AND `posY` > 0 GROUP BY `areaId`, `floor` ORDER BY COUNT(1) DESC LIMIT 1', self::$type, $this->id)) @@ -1023,7 +1025,7 @@ abstract class Filter $cats[$idx] = $cat; } - private function &criteriaIterator() : Generator + private function &criteriaIterator() : \Generator { if (!$this->fiData['c']) return; diff --git a/includes/community.class.php b/includes/community.class.php index c1d0c23d..128b955c 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -1,5 +1,7 @@ rawData); diff --git a/includes/components/SmartAI/SmartAction.class.php b/includes/components/SmartAI/SmartAction.class.php index 0e2d0711..6ab636e9 100644 --- a/includes/components/SmartAI/SmartAction.class.php +++ b/includes/components/SmartAI/SmartAction.class.php @@ -1,5 +1,7 @@ [, $flags, $catg, , ]) if ($catg == $category && !($flags & self::FLAG_INTERNAL)) diff --git a/includes/database.class.php b/includes/database.class.php index b8ec0dbe..7c9cfc4d 100644 --- a/includes/database.class.php +++ b/includes/database.class.php @@ -1,5 +1,7 @@ setErrorHandler(['DB', 'errorHandler']); + $interface->setErrorHandler(self::errorHandler(...)); if ($options['prefix']) $interface->setIdentPrefix($options['prefix']); diff --git a/includes/defines.php b/includes/defines.php index 6738ab2a..42496b14 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -1,5 +1,7 @@ id; diff --git a/includes/kernel.php b/includes/kernel.php index eacde4bf..427bc40e 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -1,5 +1,7 @@ !extension_loaded($x))) $error .= 'Required Extension '.implode(', ', $ext)." was not found. Please check if it should exist, using \"php -m\"\n\n"; @@ -47,18 +49,18 @@ require_once 'pages/genericPage.class.php'; // TC systems spl_autoload_register(function ($class) { - switch($class) + switch ($class) { - case 'SmartAI': - case 'SmartEvent': - case 'SmartAction': - case 'SmartTarget': + case __NAMESPACE__.'\SmartAI': + case __NAMESPACE__.'\SmartEvent': + case __NAMESPACE__.'\SmartAction': + case __NAMESPACE__.'\SmartTarget': require_once 'includes/components/SmartAI/SmartAI.class.php'; require_once 'includes/components/SmartAI/SmartEvent.class.php'; require_once 'includes/components/SmartAI/SmartAction.class.php'; require_once 'includes/components/SmartAI/SmartTarget.class.php'; break; - case 'Conditions': + case __NAMESPACE__.'\Conditions': require_once 'includes/components/Conditions/Conditions.class.php'; break; } @@ -72,6 +74,9 @@ spl_autoload_register(function ($class) if (class_exists($class)) // already registered return; + if ($i = strrpos($class, '\\')) + $class = substr($class, $i + 1); + if (preg_match('/[^\w]/i', $class)) // name should contain only letters return; @@ -90,7 +95,7 @@ spl_autoload_register(function ($class) if (file_exists('includes/types/'.$cl.'.class.php')) require_once 'includes/types/'.$cl.'.class.php'; else - throw new Exception('could not register type class: '.$cl); + throw new \Exception('could not register type class: '.$cl); return; } @@ -101,7 +106,7 @@ spl_autoload_register(function ($class) if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php')) require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'; else - throw new Exception('could not register ajaxHandler class: '.$class); + throw new \Exception('could not register ajaxHandler class: '.$class); return; } @@ -220,7 +225,10 @@ if (!CLI) { // not displaying the brb gnomes as static_host is missing, but eh... if (!DB::isConnected(DB_AOWOW) || !DB::isConnected(DB_WORLD) || !Cfg::get('HOST_URL') || !Cfg::get('STATIC_URL')) + { + Lang::load(Locale::EN); (new GenericPage())->maintenance(); + } // Setup Session $cacheDir = Cfg::get('SESSION_CACHE_DIR'); @@ -247,15 +255,15 @@ if (!CLI) // set up some logging (~10 queries will execute before we init the user and load the config) if (Cfg::get('DEBUG') >= CLI::LOG_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) { - DB::Aowow()->setLogger(['DB', 'profiler']); - DB::World()->setLogger(['DB', 'profiler']); + DB::Aowow()->setLogger(DB::profiler(...)); + DB::World()->setLogger(DB::profiler(...)); if (DB::isConnected(DB_AUTH)) - DB::Auth()->setLogger(['DB', 'profiler']); + DB::Auth()->setLogger(DB::profiler(...)); if (!empty($AoWoWconf['characters'])) foreach ($AoWoWconf['characters'] as $idx => $__) if (DB::isConnected(DB_CHARACTERS . $idx)) - DB::Characters($idx)->setLogger(['DB', 'profiler']); + DB::Characters($idx)->setLogger(DB::profiler(...)); } // parse page-parameters .. sanitize before use! diff --git a/includes/locale.class.php b/includes/locale.class.php index cc7d08e6..235d2d92 100644 --- a/includes/locale.class.php +++ b/includes/locale.class.php @@ -1,5 +1,7 @@ store = new WeakMap(); + $this->store = new \WeakMap(); $callback ??= fn($x) => $x; diff --git a/includes/loot.class.php b/includes/loot.class.php index 13b28e0f..c7029470 100644 --- a/includes/loot.class.php +++ b/includes/loot.class.php @@ -1,5 +1,7 @@ $id, 'tooltip' => $this->renderTooltip(true), - 'spells' => new StdClass // placeholder for knownSpells + 'spells' => new \StdClass // placeholder for knownSpells ); } } diff --git a/includes/types/itemset.class.php b/includes/types/itemset.class.php index f833c9a4..e2a1eaba 100644 --- a/includes/types/itemset.class.php +++ b/includes/types/itemset.class.php @@ -1,5 +1,7 @@ ownerDocument; @@ -23,7 +25,7 @@ trait TrRequestData public const PATTERN_TEXT_BLOB = '/[\x00-\x09\x0B-\x1F\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; protected $_get = []; // fill with variables you that are going to be used; eg: - protected $_post = []; // 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList'] + protected $_post = []; // 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList'] protected $_cookie = []; private $filtered = false; @@ -1702,35 +1704,35 @@ abstract class Type public const IDX_JSG_TPL = 2; public const IDX_FLAGS = 3; - private static /* array */ $data = array( - self::NPC => ['CreatureList', 'npc', 'g_npcs', 0x1], - self::OBJECT => ['GameObjectList', 'object', 'g_objects', 0x1], - self::ITEM => ['ItemList', 'item', 'g_items', 0x1], - self::ITEMSET => ['ItemsetList', 'itemset', 'g_itemsets', 0x1], - self::QUEST => ['QuestList', 'quest', 'g_quests', 0x1], - self::SPELL => ['SpellList', 'spell', 'g_spells', 0x1], - self::ZONE => ['ZoneList', 'zone', 'g_gatheredzones', 0x1], - self::FACTION => ['FactionList', 'faction', 'g_factions', 0x1], - self::PET => ['PetList', 'pet', 'g_pets', 0x1], - self::ACHIEVEMENT => ['AchievementList', 'achievement', 'g_achievements', 0x1], - self::TITLE => ['TitleList', 'title', 'g_titles', 0x1], - self::WORLDEVENT => ['WorldEventList', 'event', 'g_holidays', 0x1], - self::CHR_CLASS => ['CharClassList', 'class', 'g_classes', 0x1], - self::CHR_RACE => ['CharRaceList', 'race', 'g_races', 0x1], - self::SKILL => ['SkillList', 'skill', 'g_skills', 0x1], - self::STATISTIC => ['AchievementList', 'achievement', 'g_achievements', 0x0], // alias for achievements; exists only for Markup - self::CURRENCY => ['CurrencyList', 'currency', 'g_gatheredcurrencies',0x1], - self::SOUND => ['SoundList', 'sound', 'g_sounds', 0x1], - self::ICON => ['IconList', 'icon', 'g_icons', 0x1], - self::GUIDE => ['GuideList', 'guide', '', 0x0], - self::PROFILE => ['ProfileList', '', '', 0x0], // x - not known in javascript - self::GUILD => ['GuildList', '', '', 0x0], // x - self::ARENA_TEAM => ['ArenaTeamList', '', '', 0x0], // x - self::USER => ['UserList', 'user', 'g_users', 0x0], // x - self::EMOTE => ['EmoteList', 'emote', 'g_emotes', 0x1], - self::ENCHANTMENT => ['EnchantmentList', 'enchantment', 'g_enchantments', 0x1], - self::AREATRIGGER => ['AreatriggerList', 'areatrigger', '', 0x0], - self::MAIL => ['MailList', 'mail', '', 0x1] + private static array $data = array( + self::NPC => [__NAMESPACE__ . '\CreatureList', 'npc', 'g_npcs', 0x1], + self::OBJECT => [__NAMESPACE__ . '\GameObjectList', 'object', 'g_objects', 0x1], + self::ITEM => [__NAMESPACE__ . '\ItemList', 'item', 'g_items', 0x1], + self::ITEMSET => [__NAMESPACE__ . '\ItemsetList', 'itemset', 'g_itemsets', 0x1], + self::QUEST => [__NAMESPACE__ . '\QuestList', 'quest', 'g_quests', 0x1], + self::SPELL => [__NAMESPACE__ . '\SpellList', 'spell', 'g_spells', 0x1], + self::ZONE => [__NAMESPACE__ . '\ZoneList', 'zone', 'g_gatheredzones', 0x1], + self::FACTION => [__NAMESPACE__ . '\FactionList', 'faction', 'g_factions', 0x1], + self::PET => [__NAMESPACE__ . '\PetList', 'pet', 'g_pets', 0x1], + self::ACHIEVEMENT => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x1], + self::TITLE => [__NAMESPACE__ . '\TitleList', 'title', 'g_titles', 0x1], + self::WORLDEVENT => [__NAMESPACE__ . '\WorldEventList', 'event', 'g_holidays', 0x1], + self::CHR_CLASS => [__NAMESPACE__ . '\CharClassList', 'class', 'g_classes', 0x1], + self::CHR_RACE => [__NAMESPACE__ . '\CharRaceList', 'race', 'g_races', 0x1], + self::SKILL => [__NAMESPACE__ . '\SkillList', 'skill', 'g_skills', 0x1], + self::STATISTIC => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x0], // alias for achievements; exists only for Markup + self::CURRENCY => [__NAMESPACE__ . '\CurrencyList', 'currency', 'g_gatheredcurrencies',0x1], + self::SOUND => [__NAMESPACE__ . '\SoundList', 'sound', 'g_sounds', 0x1], + self::ICON => [__NAMESPACE__ . '\IconList', 'icon', 'g_icons', 0x1], + self::GUIDE => [__NAMESPACE__ . '\GuideList', 'guide', '', 0x0], + self::PROFILE => [__NAMESPACE__ . '\ProfileList', '', '', 0x0], // x - not known in javascript + self::GUILD => [__NAMESPACE__ . '\GuildList', '', '', 0x0], // x + self::ARENA_TEAM => [__NAMESPACE__ . '\ArenaTeamList', '', '', 0x0], // x + self::USER => [__NAMESPACE__ . '\UserList', 'user', 'g_users', 0x0], // x + self::EMOTE => [__NAMESPACE__ . '\EmoteList', 'emote', 'g_emotes', 0x1], + self::ENCHANTMENT => [__NAMESPACE__ . '\EnchantmentList', 'enchantment', 'g_enchantments', 0x1], + self::AREATRIGGER => [__NAMESPACE__ . '\AreatriggerList', 'areatrigger', '', 0x0], + self::MAIL => [__NAMESPACE__ . '\MailList', 'mail', '', 0x1] ); diff --git a/index.php b/index.php index 048c243f..62867356 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,7 @@ handle($out)) @@ -114,11 +116,11 @@ switch ($pageCall) } } else - throw new Exception('not handled as ajax'); + throw new \Exception('not handled as ajax'); } - catch (Exception $e) // no, apparently not.. + catch (\Exception $e) // no, apparently not.. { - $class = $cleanName.'Page'; + $class = __NAMESPACE__.'\\'.$cleanName.'Page'; $classInstance = new $class($pageCall, $pageParam); if (is_callable([$classInstance, 'display'])) diff --git a/localization/lang.class.php b/localization/lang.class.php index 9c76f6bf..6a3d5ff0 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -1,5 +1,7 @@ ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'password' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextLine'], - 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextLine'], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], 'token' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => 'AccountPage::rememberCallback'], + 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AccountPage::rememberCallback'], 'email' => ['filter' => FILTER_SANITIZE_EMAIL] ); diff --git a/pages/achievement.php b/pages/achievement.php index 5ac531b8..ff11d36f 100644 --- a/pages/achievement.php +++ b/pages/achievement.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $powerTpl = '$WowheadPower.registerAchievement(%d, %d, %s);'; @@ -548,7 +550,7 @@ class AchievementPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); diff --git a/pages/achievements.php b/pages/achievements.php index 27251c1d..41528b4b 100644 --- a/pages/achievements.php +++ b/pages/achievements.php @@ -1,5 +1,7 @@ ['filter' => FILTER_UNSAFE_RAW], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], + 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] ); diff --git a/pages/areatrigger.php b/pages/areatrigger.php index a588fb0f..1bd29174 100644 --- a/pages/areatrigger.php +++ b/pages/areatrigger.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'ComparePage::checkCompareString']]; - protected $_cookie = ['compare_groups' => ['filter' => FILTER_CALLBACK, 'options' => 'ComparePage::checkCompareString']]; + protected $_get = ['compare' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\ComparePage::checkCompareString']]; + protected $_cookie = ['compare_groups' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\ComparePage::checkCompareString']]; private $compareString = ''; diff --git a/pages/currencies.php b/pages/currencies.php index 4186feab..dbdf57ca 100644 --- a/pages/currencies.php +++ b/pages/currencies.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $powerTpl = '$WowheadPower.registerCurrency(%d, %d, %s);'; @@ -228,7 +230,7 @@ class CurrencyPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); diff --git a/pages/emote.php b/pages/emote.php index 8d634b78..79425311 100644 --- a/pages/emote.php +++ b/pages/emote.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $powerTpl = '$WowheadPower.registerHoliday(%d, %d, %s);'; private $hId = 0; @@ -280,7 +282,7 @@ class EventPage extends GenericPage protected function generateTooltip() : string { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); diff --git a/pages/events.php b/pages/events.php index fa13bb4d..4807ce20 100644 --- a/pages/events.php +++ b/pages/events.php @@ -1,5 +1,7 @@ isSaneInclude('template/localized/', $file.'_'.$loc->value)) { if ($loc == Locale::EN || !$this->isSaneInclude('template/localized/', $file.'_'.Locale::EN->value)) @@ -1039,12 +1043,12 @@ class GenericPage { try { - $rp = new ReflectionProperty($this, $key); + $rp = new \ReflectionProperty($this, $key); if ($rp && ($rp->isPublic() || $rp->isProtected())) if (!in_array($key, $noCache)) $cache[$key] = $val; } - catch (ReflectionException $e) { } // shut up! + catch (\ReflectionException $e) { } // shut up! } } else @@ -1175,11 +1179,11 @@ class GenericPage return false; } - private function memcached() : Memcached + private function memcached() : \Memcached { if (!$this->memcached && (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED)) { - $this->memcached = new Memcached(); + $this->memcached = new \Memcached(); $this->memcached->addServer('localhost', 11211); } diff --git a/pages/guide.php b/pages/guide.php index ed60239d..9b0c62f4 100644 --- a/pages/guide.php +++ b/pages/guide.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'rev' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'] + 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'rev' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'] ); protected /* array */ $_post = array( - 'save' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet'], - 'submit' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet'], - 'title' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextLine'], - 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextLine'], - 'description' => ['filter' => FILTER_CALLBACK, 'options' => 'GuidePage::checkDescription'], - 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextBlob'], - 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextBlob'], - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'category' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'specId' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'classId' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'] + 'save' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], + 'submit' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], + 'title' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], + 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], + 'description' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GuidePage::checkDescription'], + 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'category' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'specId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'classId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'] ); public function __construct($pageCall, $pageParam) @@ -500,7 +502,7 @@ class GuidePage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = strip_tags($this->name); diff --git a/pages/guides.php b/pages/guides.php index 1848cc99..13201813 100644 --- a/pages/guides.php +++ b/pages/guides.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain'], - 'rand' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'ench' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'gems' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkIntArray'], - 'sock' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet'] + 'domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain'], + 'rand' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'ench' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'gems' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkIntArray'], + 'sock' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'] ); private $powerTpl = '$WowheadPower.registerItem(%s, %d, %s);'; @@ -1030,7 +1032,7 @@ class ItemPage extends genericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true, false, $this->enhancedTT), Lang::FMT_RAW); diff --git a/pages/items.php b/pages/items.php index 1800a5c7..9ef10744 100644 --- a/pages/items.php +++ b/pages/items.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $powerTpl = '$WowheadPower.registerItemSet(%d, %d, %s);'; @@ -241,7 +243,7 @@ class ItemsetPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); diff --git a/pages/itemsets.php b/pages/itemsets.php index 540d6a9a..87b274b5 100644 --- a/pages/itemsets.php +++ b/pages/itemsets.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $soundIds = []; private $powerTpl = '$WowheadPower.registerNpc(%d, %d, %s);'; @@ -899,7 +901,7 @@ class NpcPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); diff --git a/pages/npcs.php b/pages/npcs.php index a2feafde..6b798132 100644 --- a/pages/npcs.php +++ b/pages/npcs.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $powerTpl = '$WowheadPower.registerObject(%d, %d, %s);'; @@ -485,7 +487,7 @@ class ObjectPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW); diff --git a/pages/objects.php b/pages/objects.php index 7f173ac7..8f46d233 100644 --- a/pages/objects.php +++ b/pages/objects.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain'], - 'new' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet'] + 'domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain'], + 'new' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'] ); private $isCustom = false; @@ -189,7 +191,7 @@ class ProfilePage extends GenericPage if (!$this->isCustom) $id = "'".$this->profile[0].'.'.urlencode($this->profile[1]).'.'.urlencode($this->profile[2])."'"; - $power = new StdClass(); + $power = new \StdClass(); if ($this->subject && !$this->subject->error && $this->subject->isVisibleToUser()) { $n = $this->subject->getField('name'); diff --git a/pages/profiler.php b/pages/profiler.php index 934234a8..7b057719 100644 --- a/pages/profiler.php +++ b/pages/profiler.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $powerTpl = '$WowheadPower.registerQuest(%d, %d, %s);'; @@ -1042,7 +1044,7 @@ class QuestPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW); diff --git a/pages/quests.php b/pages/quests.php index 6ddc9431..3172a09d 100644 --- a/pages/quests.php +++ b/pages/quests.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'ScreenshotPage::checkCoords'], - 'screenshotalt' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkTextBlob'] + 'coords' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\ScreenshotPage::checkCoords'], + 'screenshotalt' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'] ); public function __construct($pageCall, $pageParam) @@ -350,7 +352,7 @@ class ScreenshotPage extends GenericPage } // check if file is an image; allow jpeg, png - $finfo = new finfo(FILEINFO_MIME); // fileInfo appends charset information and other nonsense + $finfo = new \finfo(FILEINFO_MIME); // fileInfo appends charset information and other nonsense $mime = $finfo->file($_FILES['screenshotfile']['tmp_name']); if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) $isPNG = $m[0] == 'image/png'; diff --git a/pages/search.php b/pages/search.php index fc60c1b1..af8bf81c 100644 --- a/pages/search.php +++ b/pages/search.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkIntArray'], - 'wtv' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkIntArray'], - 'slots' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkIntArray'], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkInt'], - 'json' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet'], - 'opensearch' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet'] + 'wt' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkIntArray'], + 'wtv' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkIntArray'], + 'slots' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkIntArray'], + 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], + 'json' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], + 'opensearch' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'] ); private $maxResults = 500; @@ -235,6 +237,8 @@ class SearchPage extends GenericPage header(MIME_TYPE_JSON); exit(Util::toJSON([$this->search, [], []])); } + + exit; } public function display(string $override = '') : never @@ -245,6 +249,8 @@ class SearchPage extends GenericPage $this->displayExtra([$this, 'generateOpenSearch'], MIME_TYPE_OPENSEARCH); else if ($this->searchMask & SEARCH_TYPE_JSON) $this->displayExtra([$this, 'generateJsonSearch']); + + exit; } // !note! dear reader, if you ever try to generate a string, that is to be evaled by JS, NEVER EVER terminate with a \n ..... $totalHoursWasted +=2; diff --git a/pages/skill.php b/pages/skill.php index 6902d9c1..efcf6a7f 100644 --- a/pages/skill.php +++ b/pages/skill.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet']]; + protected $_get = ['playlist' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet']]; private $cat = 0; diff --git a/pages/sounds.php b/pages/sounds.php index 244d1117..6263e45d 100644 --- a/pages/sounds.php +++ b/pages/sounds.php @@ -1,5 +1,7 @@ ['filter' => FILTER_CALLBACK, 'options' => 'Locale::tryFromDomain']]; + protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; private $difficulties = []; private $firstRank = 0; @@ -1279,7 +1281,7 @@ class SpellPage extends GenericPage protected function generateTooltip() { - $power = new StdClass(); + $power = new \StdClass(); if (!$this->subject->error) { [$tooltip, $ttSpells] = $this->subject->renderTooltip(); diff --git a/pages/spells.php b/pages/spells.php index 632a9004..f4068c9a 100644 --- a/pages/spells.php +++ b/pages/spells.php @@ -1,5 +1,7 @@ 'latest-videos', 12 => 'most-comments', 13 => 'missing-screenshots' ); - protected $_get = ['rss' => ['filter' => FILTER_CALLBACK, 'options' => 'GenericPage::checkEmptySet']]; + protected $_get = ['rss' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet']]; private $page = ''; private $rss = false; diff --git a/pages/zone.php b/pages/zone.php index 7ff8ad9a..7ee4bec5 100644 --- a/pages/zone.php +++ b/pages/zone.php @@ -1,5 +1,7 @@ 'mapper-generic' ), 'som' => $somData, - 'mapperData' => [$mapFile => new stdClass()] + 'mapperData' => [$mapFile => new \StdClass()] ); } } diff --git a/prQueue b/prQueue index 9680aa89..92166883 100755 --- a/prQueue +++ b/prQueue @@ -1,6 +1,8 @@ #!/usr/bin/env php $us) - if (in_array('TrSubScripts', class_uses($us))) + if (in_array(__NAMESPACE__.'\TrSubScripts', class_uses($us))) $us->assignGenerators($name); self::evalOpts(); } - public static function getSubScripts(string $invoker = '') : generator + public static function getSubScripts(string $invoker = '') : \Generator { foreach (self::$setupScriptRefs as [$src, $name, $ref]) if (!$invoker || $src == $invoker) @@ -473,10 +475,10 @@ class CLISetup try { - $iterator = new RecursiveDirectoryIterator(self::$srcDir); - $iterator->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new \RecursiveDirectoryIterator(self::$srcDir); + $iterator->setFlags(\RecursiveDirectoryIterator::SKIP_DOTS); - foreach (new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST) as $path) + foreach (new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST) as $path) { $_ = CLI::nicePath($path->getPathname()); self::$mpqFiles[strtolower($_)] = $_; @@ -484,7 +486,7 @@ class CLISetup CLI::write('indexing game data from '.self::$srcDir.' for first time use... done!', CLI::LOG_INFO); } - catch (UnexpectedValueException $e) + catch (\UnexpectedValueException $e) { CLI::write('- mpqData dir '.self::$srcDir.' does not exist', CLI::LOG_ERROR); return false; @@ -601,7 +603,7 @@ class CLISetup return true; } - public static function searchGlobalStrings(string $pattern) : Generator + public static function searchGlobalStrings(string $pattern) : \Generator { if (!self::$gsFiles) return; diff --git a/setup/tools/clisetup/account.us.php b/setup/tools/clisetup/account.us.php index 10e5e3eb..a9d97421 100644 --- a/setup/tools/clisetup/account.us.php +++ b/setup/tools/clisetup/account.us.php @@ -1,5 +1,7 @@ getPath()); diff --git a/setup/tools/clisetup/siteconfig.us.php b/setup/tools/clisetup/siteconfig.us.php index fe692b78..17dcf0c8 100644 --- a/setup/tools/clisetup/siteconfig.us.php +++ b/setup/tools/clisetup/siteconfig.us.php @@ -1,5 +1,7 @@ genSteps[self::M_SPAWNS][self::GEN_IDX_DEST_INFO][0][0] . $zoneId . '.png'; @@ -769,7 +771,7 @@ CLISetup::registerSetup("build", new class extends SetupScript $this->success = false; } - private function buildSubZones(GdImage $resMap, int $wmaId, Locale $loc) : void + private function buildSubZones(\GdImage $resMap, int $wmaId, Locale $loc) : void { foreach ($this->wmOverlays[$wmaId] as &$row) { @@ -806,7 +808,7 @@ CLISetup::registerSetup("build", new class extends SetupScript } } - private function generateOverlay(int $wmaId, string $basePath) : ?GdImage + private function generateOverlay(int $wmaId, string $basePath) : ?\GdImage { if (!isset($this->wmOverlays[$wmaId])) return null; diff --git a/setup/tools/filegen/img-talentcalc.ss.php b/setup/tools/filegen/img-talentcalc.ss.php index 77908825..e9ac6a6d 100644 --- a/setup/tools/filegen/img-talentcalc.ss.php +++ b/setup/tools/filegen/img-talentcalc.ss.php @@ -1,5 +1,7 @@ $x & (1 << $i)))) $excludes[$type][$i + 1] = $ids; - $buff = "g_excludes = ".Util::toJSON($excludes ?: (new StdClass)).";\n"; + $buff = "g_excludes = ".Util::toJSON($excludes ?: (new \StdClass)).";\n"; if (!CLISetup::writeFile('datasets/quick-excludes', $buff)) $this->success = false; diff --git a/setup/tools/filegen/realmmenu.ss.php b/setup/tools/filegen/realmmenu.ss.php index 08180f9d..ca4ae692 100644 --- a/setup/tools/filegen/realmmenu.ss.php +++ b/setup/tools/filegen/realmmenu.ss.php @@ -1,5 +1,7 @@ success; } - private function compileTexture(string $ttField, int $searchMask, int $tabIdx) : ?GDImage + private function compileTexture(string $ttField, int $searchMask, int $tabIdx) : ?\GdImage { $icons = DB::Aowow()->SelectCol( 'SELECT ic.`name` AS "iconString" diff --git a/setup/tools/filegen/tooltips.ss.php b/setup/tools/filegen/tooltips.ss.php index b6782247..9330dc75 100644 --- a/setup/tools/filegen/tooltips.ss.php +++ b/setup/tools/filegen/tooltips.ss.php @@ -1,5 +1,7 @@ $fileSize) { CLI::write('file '.$fileName.' is corrupted/incomplete'.$debugStr, CLI::LOG_ERROR); - return; + return null; } if ($header['type'] == 1) @@ -117,16 +119,19 @@ else { CLI::write('file '.$fileName.' has unsupported type'.$debugStr, CLI::LOG_ERROR); - return; + return null; } return $img; } // uncompressed - function icfb1($width, $height, $palette, $data) + function icfb1(int $width, int $height, $palette, string $data) : ?\GdImage { $img = imagecreatetruecolor($width, $height); + if (!$img) + return null; + imagesavealpha($img, true); imagealphablending($img, false); @@ -148,15 +153,18 @@ } // DXTC - function icfb2($width, $height, $data, $alphaBits, $alphaType) + function icfb2(int $width, int $height, string $data, int $alphaBits, int $alphaType) : ?\GdImage { if (!in_array($alphaBits * 10 + $alphaType, [0, 10, 41, 81, 87, 88])) { CLI::write('unsupported compression type', CLI::LOG_ERROR); - return; + return null; } $img = imagecreatetruecolor($width, $height); + if (!$img) + return null; + imagesavealpha($img, true); imagealphablending($img, false); @@ -304,9 +312,12 @@ } // plain - function icfb3($width, $height, $data) + function icfb3(int $width, int $height, string $data) : ?\GdImage { $img = imagecreatetruecolor($width, $height); + if (!$img) + return null; + $i = unpack("V*", $data); for ($y = 0; $y < $height; $y++) diff --git a/setup/tools/setupScript.class.php b/setup/tools/setupScript.class.php index a6f9896d..c26ef13e 100644 --- a/setup/tools/setupScript.class.php +++ b/setup/tools/setupScript.class.php @@ -1,5 +1,7 @@ query('UPDATE ?_'.$this->getName().' SET ?a WHERE id = ?d', $data, $id); } - catch (Exception $e) + catch (\Exception $e) { trigger_error('custom data for entry #'.$id.': '.$e->getMessage(), E_USER_ERROR); $ok = false; @@ -239,7 +241,7 @@ trait TrImageProcessor // prefer manually converted PNG files (as the imagecreatefromblp-script has issues with some formats) // alpha channel issues observed with locale deDE Hilsbrad and Elwynn - maps // see: https://github.com/Kanma/BLPConverter - private function loadImageFile(string $path, ?bool &$noSrc = false) : ?GdImage + private function loadImageFile(string $path, ?bool &$noSrc = false) : ?\GdImage { $result = null; $noSrc = false; @@ -264,7 +266,7 @@ trait TrImageProcessor return $result; } - private function writeImageFile(GdImage $src, string $outFile, array $srcDims, array $destDims) : bool + private function writeImageFile(\GdImage $src, string $outFile, array $srcDims, array $destDims) : bool { $success = false; $outRes = imagecreatetruecolor($destDims['w'], $destDims['h']); @@ -313,7 +315,7 @@ trait TrComplexImage { use TrImageProcessor { TrImageProcessor::writeImageFile as _writeImageFile; } - private function writeImageFile(GdImage $src, string $outFile, int $w, int $h) : bool + private function writeImageFile(\GdImage $src, string $outFile, int $w, int $h) : bool { $srcDims = array( 'x' => 0, @@ -331,7 +333,7 @@ trait TrComplexImage return $this->_writeImageFile($src, $outFile, $srcDims, $destDims); } - private function createAlphaImage(int $w, int $h) : ?GdImage + private function createAlphaImage(int $w, int $h) : ?\GdImage { $img = imagecreatetruecolor($w, $h); if (!$img) @@ -351,7 +353,7 @@ trait TrComplexImage return $img; } - private function assembleImage(string $baseName, array $tileData, int $destW, int $destH) : ?GdImage + private function assembleImage(string $baseName, array $tileData, int $destW, int $destH) : ?\GdImage { $dest = imagecreatetruecolor($destW, $destH); if (!$dest) @@ -487,7 +489,7 @@ abstract class SetupScript CLI::write('[build] created '.$newDirs.' extra paths'); // load DBC files - if (!in_array('TrDBCcopy', class_uses($this))) + if (!in_array(__NAMESPACE__.'\TrDBCcopy', class_uses($this))) { foreach ($this->getRequiredDBCs() as $req) { diff --git a/setup/tools/sqlgen/achievement.ss.php b/setup/tools/sqlgen/achievement.ss.php index 3baee0b9..fc5d54be 100644 --- a/setup/tools/sqlgen/achievement.ss.php +++ b/setup/tools/sqlgen/achievement.ss.php @@ -1,5 +1,7 @@ + announcements as $id => $data): ?> @@ -7,4 +9,4 @@ foreach ($this->announcements as $id => $data): \ No newline at end of file +?> diff --git a/template/bricks/article.tpl.php b/template/bricks/article.tpl.php index cbc1c5a0..9df83fbf 100644 --- a/template/bricks/article.tpl.php +++ b/template/bricks/article.tpl.php @@ -1,3 +1,5 @@ + + article)): ?> diff --git a/template/bricks/book.tpl.php b/template/bricks/book.tpl.php index 9557013e..2116874e 100644 --- a/template/bricks/book.tpl.php +++ b/template/bricks/book.tpl.php @@ -1,3 +1,5 @@ + + pageText)): ?> diff --git a/template/bricks/contribute.tpl.php b/template/bricks/contribute.tpl.php index 8e602a46..a4387dd9 100644 --- a/template/bricks/contribute.tpl.php +++ b/template/bricks/contribute.tpl.php @@ -1,3 +1,5 @@ + + contribute)): ?> @@ -9,7 +11,7 @@ if (!empty($this->contribute)):
localizedBrick('contrib', Lang::getLocale()); + $this->localizedBrick('contrib'); ?>
diff --git a/template/bricks/filter.tpl.php b/template/bricks/filter.tpl.php index 8d96eae6..0cbc279c 100644 --- a/template/bricks/filter.tpl.php +++ b/template/bricks/filter.tpl.php @@ -1,3 +1,5 @@ + + = CLI::LOG_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)): +if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)): ?> [/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'searchbox',0,2,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(NULL,NULL,0,'searchplugins',0,2,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]
Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(NULL,NULL,2,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(NULL,NULL,0,'faq',0,2,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(NULL,NULL,0,'whats-new',0,2,'[small]this page for example[/small]',NULL),(NULL,NULL,0,'aboutus',0,2,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(NULL,NULL,3,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(NULL,NULL,6,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(NULL,NULL,0,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(NULL,NULL,0,'markup-guide',0,2,'[menu tab=2 path=2,13,7]Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: en (default), de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,NULL,0,2,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,NULL,0,2,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,NULL,0,2,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,NULL,0,2,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,NULL,0,2,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,NULL,0,2,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,NULL,0,2,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,NULL,0,2,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,NULL,0,2,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,NULL,0,2,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,NULL,0,2,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,NULL,0,2,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,NULL,0,2,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,NULL,0,2,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,NULL,0,2,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,NULL,0,2,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,NULL,0,2,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,NULL,0,2,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,NULL,0,2,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,NULL,0,2,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,NULL,0,2,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,NULL,0,2,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,NULL,0,2,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,NULL,0,2,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,NULL,0,2,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,NULL,0,2,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,NULL,0,2,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,NULL,0,2,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,NULL,0,2,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,NULL,0,2,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,NULL,0,2,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL),(NULL,NULL,0,'sound&playlist',0,2,'Here you can set up a playlist of sounds and music. \n\nJust click the \"Add\" button near an audio control, then return to this page to listen to the list you\'ve created.',NULL),(14,11,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Draenei[/b] sont des adeptes de Naaru et adorateurs de la Lumière Sainte. Chassées d’Argus, leur monde natal, les honorables Draeneï durent fuir des siècles durant Sargeras et sa Légion Ardente, après qu’il ait essayé de les corrompre. Les Draeneï ont alors trouvé une lointaine planète où s’établir. Ils appelèrent Draenor ce monde qu’ils partageaient avec les Orcs chamaniques. Une période de paix s’est alors installée.\nLa Légion Ardente fini par retrouver les DraeneÏ et corrompt les Orcs grâce à Guldan. Les Orcs partirent en guerre et exterminèrent les paisibles Draeneï. De rares survivants purent s’enfuir en Azeroth pour chercher de l’aide dans leur combat contre la Légion Ardente.\n\n[b]Capitale :[/b] Les Draeneï ont le siège de leur pouvoir dans les ruines de leur vaisseau : [zone=3557].\n\n[b]Zone de départ :[/b] [zone=3524] et [zone=3525] couvrent les tentatives des Draeneï de s’installer sur leurs nouvelles îles et de faire face à la corruption présente.\n\n[b]Montures :[/b] [npc=17584] vend des variétés d’Elekks, ainsi que [npc=33657] au tournoi d’Argent.',NULL),(14,8,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Trolls[/b] Sombrelance vécurent à l\'origine dans les îles Brisées mais furent envahis par les nagas et les murlocs. Chassés de chez eux, la [url=?faction=530]tribu de Sombrelance[/url] se lie finalement d\'amitié avec les orcs qui ont sauvés les Trolls de la destruction. [npc=4949] leur offre l\'amnistie parmi la Horde, en contrepartie, la tribu Sombrelance jura fidélité au chef de guerre orque.\nBien qu\'ils refusent d\'abandonner leur sombre héritage, les féroces Trolls Sombrelance occupent une place d\'honneur au sein de la Horde.\n\n[b]Capitale :[/b] Les Trolls Sombrelance vivent maintenant dans la capitale de la Horde : [zone=1637].\n\n[b]Zone de départ :[/b] Les Trolls commencent leurs quêtes en [zone=14]\n\n[b]Montures :[/b] [npc=7952] au village de Sen\'jin vend de nombreux raptors ; [npc=33554], au tournoi d\'Argent, vend quelques modèles distincts.',NULL),(14,10,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Hauts-Elfes[/b], race fière et hautaine, fondèrent jadis Quel’Thalas où ils créèrent une fontaine magique appelée Puits de Soleil. Ils profitèrent de sa puissance mais devinrent peu à peu dépendants de la magie. Si celle-ci devait être enlevée, les Hauts-Elfes soufreraient horriblement. Ils se séparèrent donc du reste de la société elfique.\nDe nombreux siècles plus tard, le fléau mort-vivant détruisit le Puit de Soleil et tua la plupart des Hauts-Elfes. Les survivants de l’assaut d’Arthas sur Lune-d’Argent, qui ont alors pris le nom d’Elfes de Sang, rebâtissent Quel’Thalas et cherchent de nouvelles sources de magie pour calmer leur douloureux manque.\nLes Elfes de Sang rejoignent la Horde à Burning Crusade.\n\n[b]Capitale :[/b] Les Elfes de Sang ont reconstruit [zone=3487].\n\n[b]Zone de départ :[/b] Les Elfes de Sang commencent au [zone=3430].\n\n[b]Montures :[/b] [npc=16264], aux Bois des Chants Eternelles, vend de nombreux faucons pèlerins ; [npc=33557], au tournoi d’Argent, vend quelques modèles uniques.',NULL),(14,7,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Gnomes[/b], race excentrique, sont obsédés par les gadgets et la technologie. Malgré leur petite taille, ils ont mis à profit leur grande intelligence pour s\'assurer une place dans l\'Histoire.\nA l\'origine, les Gnomes viennent de la ville de [zone=721], qui était autrefois une merveille technologique mue à la vapeur. Malheureusement, la ville a été détruite par [npc=7937] à la suite d\'une tentative pour sauver la ville d\'une armée massive de Troggs.\nSes bâtisseurs sont désormais des vagabonds qui errent sur les terres des nains, venant en aide à leurs alliés du mieux qu\'il le peuvent.\n\n[b]Capitale :[/b] Aujourd\'hui, les Gnomes font leurs maisons à [zone=1537] malgré les efforts fournis pour reprendre leur bien aimée ancienne ville avec l\'[achievement=4786].\n\n[b]Zone de départ :[/b] Les Gnomes commencent à [zone=1], mais ont une séquence de quêtes très différente des Nains, couvrant Gnomeregan\n\n[b]Montures :[/b] [npc=7955] à Dun Morogh vend de nombreux mécanotrotteurs, ainsi que [npc=33650] au tournoi d\'Argent.',NULL),(14,6,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Taurens[/b], race aux racines chamaniques profondes, sont des résidents de longue date de Kalimdor. Ils vouent un amour profond et durable à la nature, la grande majorité d’entre eux adorent une divinité connue sous le nom de la Terre Mère.\nRécemment attaqués par des centaures, les Taurens auraient été exterminés s’ils n’avaient pas rencontré, par hasard, les Orcs qui les aidèrent à repousser leurs ennemis. Afin d’honorer cette dette de sang, les Taurens ont rejoint la Horde, renforçant ainsi l’amitié entre les deux races.\n\n[b]Capitale :[/b] [zone=1638] est le lieu de résidence des Taurens\n\n[b]Zone de départ :[/b] Les Taurens commencent leurs quêtes en [zone=215].\n\n[b]Montures :[/b] [npc=3685] vend de nombreux kodos ; [npc=33556], au tournoi d’Argent, vend quelques modèles distinctifs.',NULL),(14,5,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Réprouvés[/b], résultat d’une première attaque du Fléau en Azeroth, sont une métamorphose d’un certain nombre de membres de l’Alliance en mort vivant. Quand les forces combinées des Orcs, des Elfes, des Trolls, des Nains et des Humains se mirent à se défendre, [npc=36597] se mit à affaiblir ses armées en perdant le contrôle de certaines. Libérés de l’emprise du Roi Liche ainsi que des émotions gênantes et des liens de leurs vies humaines, les Réprouvés, menés par la banshee Sylvanas, réclament vengeance contre le fléau.\nLes Humain sont également devenus des ennemis, impitoyables dans leur désir de purger les terres de tous les mort-vivants. \nLes Réprouvés ne se soucient que très peu de leurs alliés. La Horde ne représente à leurs yeux qu’un simple outil qui pourrait servir leurs sombres desseins.\n\n[b]Capitale :[/b] Les Réprouvés résident sous les ruines de l’ancienne ville humaine de Lordaeron : la [zone=1497].\n\n[b]Zone de départ :[/b] Tous les joueurs de Réprouvés commencent dans la [zone=85]. Ils sont élevés par les Val’kyrs comme des réprouvés de seconde génération\n\n[b]Montures :[/b] [npc=4731] vend de nombreux chevaux mort-vivants ; [npc=33555], au tournoi d’Argent, vend quelques modèles distincts.',NULL),(14,4,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Elfes de la nuit[/b], race ancienne et mystérieuse, vivaient à Kalimdor pendant des milliers d\'années, ils fondèrent un vaste empire, mais leur usage imprudent de la magie les conduisit à leur perte. Pétris de douleur, ils se retirèrent dans les forêts et demeurèrent ainsi isolés jusqu\'au retour de leur ancien ennemi. Ne disposant d\'aucune alternative, les Elfes de la nuit furent contraints de sacrifié l\'arbre monde afin d\'arrêter l\'avancé de la Légion Ardente. \nIls émergèrent de leur isolement, afin de défendre leur place dans le nouveau monde.\n\n[b]Capitale :[/b] La capitale des Elfes de la nuit est [zone=1657], située dans les branches de l\'arbre monde.\n\n[b]Zone de départ :[/b] Les Elfes de la nuit commencent à [zone=141]\n\n[b]Montures :[/b] [npc=4730], à Darnassus, vent une variété de sabre de nuit, ainsi que [npc=33653] au tournoi d\'Argent.',NULL),(14,3,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Nains[/b], race robuste, viennent de Khaz Modan dans les Royaumes de l’Est. Par la passé, les Nains ne s’intéressaient qu’aux richesses extraites des profondeurs de la terre. Lorsque des études semblèrent indiquer que les Nains étaient les descendants d’une race proche des Titans qui leur aurait conféré un héritage enchanté, la curiosité des Nains fut piquée au vif. Décidés à en savoir plus, les Nains commencèrent à rechercher des artefacts perdus et des connaissances disparues. Aujourd’hui, les Nains dirigent des fouilles archéologiques partout dans le monde.\nTrois principaux Clans de Nains sont répartis dans tout Azeroth : Les Barbes de Bronze, Les Marteaux Hardis et les Sombrefers.\n\n[b]Capitale :[/b] Les Nains font leur maison dans leur siège ancestral de [zone=1537].\n\n[b]Zone de départ :[/b] Les Nains commencent à [zone=1].\n\n[b]Montures :[/b] [npc=1261] vend des béliers à la ferme des Amberstill, ainsi que [npc=33310] au tournoi d’Argent.',NULL),(14,1,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Humains[/b], race la plus jeune et la plus peuplés d\'Azeroth, maîtrisent les arts du combat, l\'artisanat et la magie avec une efficacité stupéfiante. La valeur et l\'optimisme des Humains les ont conduits à bâtir certains des plus grands royaumes du monde. En cette ère de troubles, après des générations de conflit, l\'Humanité aspire à ranimer sa gloire passée et à se forger un nouvel avenir rayonnant.\nLes Humains, aux talents très variés, sont devenus les chefs de l\'Alliance grâce à leurs ambitions et leurs résiliences. \n \n[b]Capitale :[/b] Le siège du pouvoir Humain est dans la ville reconstruite de [zone=1519].\n \n[b]Zone de départ :[/b] Les Humains commencent leurs quêtes dans la [zone=12].\n \n[b]Montures :[/b] [npc=384] vend des palefrois dans Hurlevent, et [npc=33307], au tournoi d’Argent, vend quelques modèles distincts.',NULL),(14,2,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Orcs[/b] étaient, à l\'origine, un peuple pacifique aux croyances chamaniques résidant sur le monde de Draenor. Malheureusement, infectés par le sang démoniaque de Mannoroth le destructeur, les Orcs furent réduit en esclavage par la Légion Ardente, contraint de guerroyer contre les Draenei et de conquérir Azeroth. \nAprès de nombreuse années de joug, les Orcs ont réussi à se libérer de l\'emprise démoniaque et ont conquis leur liberté, pour revenir à leurs racines chamaniques.\nMaintenant, sous la direction de leur nouveau chef de guerre, les Orcs se construisent un nouveau foyer, où ils combattent pour l\'honneur, dans un monde étranger, haïs et calomniés.\n\n[b]Capitale :[/b] Les Orcs résident maintenant dans la ville d\'[zone=1637], du nom du défunt Orgrim Doomhammer, ancien chef de guerre de la Horde.\n\n[b]Zone de départ :[/b] Les Orcs commencent leurs quêtes en [zone=14].\n\n[b]Montures :[/b] [npc=3362], à Orgrimmar, vend une variété de loups ; [npc=33553], au tournoi d\'Argent, vend quelques montures distinctives',NULL),(NULL,NULL,0,'reputation',0,2,'[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPLOAD reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n',NULL),(NULL,NULL,0,'privilege=1',0,2,'[h3]Reputation required for posting comments?[/h3]\nThe very first privilege you can earn is the ability to post comments. Because this privilege requires only CFG_REP_REQ_COMMENT reputation, it is earned soon upon registering an account (which awards CFG_REP_REWARD_REGISTER reputation)! Keep this in mind if you\'ve recently registered to post on a contest thread.\n\n[h3]How do I post a comment?[/h3]\nOnce you have earned the ability to post comments, it\'s easy to do! Got some interesting information about an item? Strategies for earning an achievement or killing a boss? These are just a few examples of what could make a quality comment here!\n\nSimply visit any database page that you wish to leave a comment on and scroll down to the \'Contribute\' section. In the \'Add your comment\' tab, you can easily write and format your database comment. You can use our handy formatting buttons to improve the visual quality of your post, and easily add database links using the \'Links\' menu and entering database entry IDs. Once you\'re done, simply click the \'Submit\' button below and voila!\n\n[h3]Comment rating and you![/h3]\nAll comments made on database pages are subject to our rating system. This allows users who have reached the appropriate reputation level to upvote and downvote comments based on their quality. Making quality comments will earn you website reputation each time it has been upvoted, but make a poor quality comment and you may end up losing reputation if it is downvoted!\n\nFor more information on commenting, be sure to check out our handy [url=?help=commenting-and-you]Commenting and You[/url] guide in the website help section!',NULL),(NULL,NULL,0,'privilege=2',0,2,'[h3]Posting External Links[/h3]\nOne of the first privileges allowed to users is the ability to post external links on the site. This will allow you to link to relevant information found on other websites from our database as well as in our forums. You can also add a link to your user profile, such as to your guild website or personal blog. Users without the appropriate reputation level will have their links filtered automatically, to help prevent spammers and malicious links from being posted on our website.\n\n[h3]Posting Policy[/h3]\nPlease be aware that some URLs may still be filtered out by our moderation team, as they made be deemed inappropriate or advertising. If you are uncertain whether or not a link will be considered advertisement, please do not hesitate to contact our Feedback team with any questions!\n',NULL),(NULL,NULL,0,'privilege=4',0,2,'[h3]No CAPTCHAs[/h3]\nAh, CAPTCHAS. Love \'em or hate \'em, they\'re often a necessary evil for popular websites which allow any sort of user contribution. Here, we use [url=https://www.google.com/recaptcha/intro/index.html]ReCAPTCHA[/url] which helps thwart bots and spammers from abusing our forum and comment systems. Unfortunately, this also creates a minor inconvenience for our more active users, who are still occasionally asked to input a CAPTCHA despite long since establishing themselves as a legitimate member of the community. Well, not anymore! Users who reach the appropriate reputation level will no longer have to enter CAPTCHAs anywhere on the site!\n',NULL),(NULL,NULL,0,'privilege=5',0,2,'[h3]Comment rating value increase[/h3]\nWhen you have reached a higher reputation level, your contributions to the site will raise in value! As a more trusted member of our community, your comment ratings will now have an increased weight and, as a result, have a greater effect on the total rating of a comment! Your vote contribution are doubled, so each of upvote will count as two votes (and each of your downvotes as two, as well)! This will allow higher reputation users to have more of an effect on considering quality of a comment, raising quality comments higher and lowering poor comments faster.\n',NULL),(NULL,NULL,0,'privilege=9',0,2,'[h3]More votes per day[/h3]\nWe have a daily cap for comment votes set to CFG_USER_MAX_VOTES.\n\nThis privilege instantly increases the cap by 1, and then increases the cap by an additional 1 point for each CFG_REP_REQ_VOTEMORE_ADD reputation you have above CFG_REP_REQ_VOTEMORE_BASE.\n',NULL),(NULL,NULL,0,'privilege=10',0,2,'[h3]Upvoting Comments[/h3]\nDid you find a comment particularly insightful or laugh out loud funny? Upvote it then! Upvoting is a way of giving props to those who truly contribute. From small guides to witty jokes, if a comment has enhanced your user experience, you should remember to upvote it.\n\nThe higher amount of upvotes a comment has, the higher up on the page it is. This way the community can help determine what comments are worth reading by sending some upvotes their way.\n\n[h3]Upvoting Policy[/h3]\nYou should not use upvotes to reward your friends or withhold upvotes to punish users you dislike. These are bannable offenses and you will probably lose your ability to upvote if we catch you doing it.\n',NULL),(NULL,NULL,0,'privilege=11',0,2,'[h3]Downvoting Comments[/h3]\nDid you find a comment that was out of date, irrelevant, or otherwise less than useful? Downvote it then! Downvoting is a way of removing the clutter from the database and ensuring our comments are up to date. Downvotes remove an upvote--and if a comment has too many downvotes, it can even become a negative comment which appear at the end of an article rather than the beginning. \n\n[h3]Downvoting Policy[/h3]\nYou should not use downvotes to punish users you dislike nor should you downvote in quick succession. Try to use downvotes only to help us out, leaving personal bias out of it. If you abuse downvotes either by making too many in a short time frame or targeting a specific user, you may be warned and in some cases banned.\n',NULL),(NULL,NULL,0,'privilege=12',0,2,'[h3]Replying to a Comment[/h3]\nYou can reply to comments easily and quickly with the new commenting system. All you have to do is leave a reply on an existing comment for this to work.\n\nA reply is best used to illustrate alternatives to a comment, highlight its accuracy, or expand on a joke. For example, if someone says an item drops from a certain boss but you know it does not, you could reply to explain it doesn\'t; it\'s likely people will find your comment helpful so they don\'t waste time trying to get the item from that NPC.\n\nPlease be aware that you should not use comments like forum threads for discussion.\n',NULL),(NULL,NULL,0,'privilege=13',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an uncommon-quality green border.',NULL),(NULL,NULL,0,'privilege=14',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a rare-quality blue border.',NULL),(NULL,NULL,0,'privilege=15',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an epic-quality purple border.',NULL),(NULL,NULL,0,'privilege=16',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a legendary-quality orange border.',NULL),(NULL,NULL,0,'privilege=17',0,2,'[img src=STATIC_URL/images/premium/user-badge.png border=0 float=right]Unlock [url=HOST_URL/?premium]AoWoW Premium[/url] status for free.\n\nAs a Premium user, you can access a variety of perks:\n[ul]\n[li]Images in tooltips[/li]\n[li]Additional avatar borders[/li]\n[li]And much more![/li][/ul]\n\n',NULL),(13,1,2,NULL,0,2,'[b][color=c1]Les Guerriers[/color][/b] sont une classe très puissante, avec la capacité de taner ou d\'infliger des dégâts de mêlée. Sa caractéristique principale est la force, mais les tanks s\'intéresseront également à l\'Endurance.\n\nCe combattant se bat avec une posture ce qui lui permet l\'accès à différentes capacités et lui accorde des bonus. Il utilisera [spell=71] pour tanker (appris au niveau 10) et [spell=2457] (appris au niveau 1) ou [spell=2458] (appris au niveau 30) pour les dégâts en mêlée.\n\nL\'arbre de protection du Guerrier contient de nombreux talents pour améliorer leur survie et générer des menaces contre les monstres. Les Guerriers de protection sont l\'une des principales classes de tank du jeu. Pour aller au combat, ils peuvent utiliser [spell=100] ou [spell=20252] mais seul le Guerrier protection peut protéger un allié en utilisant [spell=3411].\nIls ont également deux arbres de talent orientés sur les dégâts [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Armes[/url][/icon] et [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], ce dernier comprend le talent [spell=46917], qui permet au Guerrier de manier deux armes à deux mains. Les Guerriers sont capable de faire de gros dégâts de zone avec des sorts tels que [spell=845], [spell=1680] et [spell=46924]. \n\nLe Guerrier porte une armure en plaques et aspire à la perfection dans les combats. Lorsqu\'il inflige ou subit des dégâts, il génère de la rage, utilisée pour alimenter ses attaques spéciales.\n[ul]\n[li] Allié utile, qui peut ajouter des buffs au groupe ou raid avec [spell=6673] et [spell=469], mais seul les Guerriers Fury peuvent fournir un buff passif [spell=29801] qui augmente les coups critiques en mêlée et à distance.[/li]\n[li] L\'avantages uniques des Guerriers, ce sont les 3 postures de combats.[/li]\n[li] Il peut choisir de se spécialiser dans le port d’armes à deux mains, d\'arme à une main, ou dans l\'utilisation du bouclier en plus d\'une arme à une main.[/li]\n[li] Et dispose de plusieurs techniques qui permettent de se déplacer rapidement sur le champ de bataille.[/li]\n[/ul]',NULL),(13,2,2,NULL,0,2,'[b][color=c2]Les Paladins[/color][/b] sont des combattants qui utilisent la magie du sacré pour soigner les blessures et combattre le mal. Ils sont relativement autonomes et disposent de nombreuses techniques destinées à empêcher les morts. Le paladin peut choisir de se battre, de protégés ou de soigner, il utilisera le mana pour combattre le mal. Ses caractéristiques principales dépendent du rôle choisi.\n\nIl est un mélange d’un combattant en mêlée et d’un lanceur de sorts secondaires. Allié indispensable dans un combat, il renforce leurs amis avec de saintes auras (une aura active par paladin sur chaque membre du raid) et des bénédictions spécifiques pour les protéger du mal et renforcer leurs pouvoirs.\n\nPortant de lourdes armures, ils peuvent résister à des coups terribles dans les batailles les plus dures tout en guérissant leurs alliés blessés et en ressuscitant les morts. Au combat, ils peuvent utiliser des armes à deux mains, paralyser leurs ennemis, détruire des morts vivants et des démons, et les juger avec une sainte vengeance.\nLes paladins sont une classe défensive, principalement conçus pour survivre à leurs adversaires, grâce à leur assortiment de capacités défensives. Ils font aussi d’excellents tanks en utilisant leurs capacités [spell=25780].\n\n[ul]\n[li] Classe pouvant guérir, tanker avec leur précieux bouclier et infliger des dégâts en mêlée.[/li]\n[li] Renforce les alliées avec les [url=spells=7.2&filter=na=aura]Auras[/url], les [url=spells=7.2&filter=na=bénédiction]bénédictions[/url] et d’autres buffs.[/li]\n[li] Seule classe avec un véritable sort d’invulnérabilité [spell=642].[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=13819] est un destrier royal que seuls les plus fervents des paladins peuvent appeler à leur service. Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23214] est un équipier infatigable capable d\'amener son valeureux maître dans tout Azeroth. Niveau 40 - Bonus de vitesse de 100%. [/li]\n[/ul]',NULL),(13,4,2,NULL,0,2,'[b][color=c4]Les Voleurs[/color][/b] sont une classe de mêlée capable d\'infliger de grandes quantités de dégâts à leurs ennemis avec des attaques rapides en utilisant de l\'énergie comme ressources. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Voleurs ont un puissant arsenal de compétences, dont beaucoup sont renforcés par leur capacité de furtivité et d\'étourdissement de leurs victimes. Capables d\'utiliser des poisons, ils paralysent leurs adversaires, les affaiblissant massivement dans la bataille. Avec l\'ambidextrie, ils peuvent utiliser une large gamme d\'armes, mais les Voleurs privilégient la dague, qui est la plus représentative de cette classe. \n\nCe sont les maîtres pour se déplacer furtivement autour de leurs ennemis, frapper dans l\'ombre un adversaire pour tenter de l\'achever rapidement puis s\'échapper du combat en un clin d’œil. \nIls endossent donc souvent le rôle d\'assassin ou d\'éclaireur, mais nombre d\'entre eux sont des loups solitaires.\n\n[ul]\n[li] Porte des armures en cuir.[/li]\n[li] Porte une arme dans chaque main.[/li]\n[li] Utilise une grand variété d\'armes de mêlée, comme les poignards, les armes de pugilats, les masses à une main, les épées à une main et les haches à une main.[/li]\n[li] Recouvre leurs armes avec du [url=items=0.-3&filter=na=poison;ub=4]poison[/url] pour gravement affaiblir leurs ennemis.[/li]\n[li] Utilise le [spell=1784] pour n’être visible que par les ennemis les plus perspicaces.[/li]\n[li] Cumule 5 points de combo pour infliger de puissants coups de grâce.[/li]\n[/ul]',NULL),(13,3,2,NULL,0,2,'[b][color=c3]Les Chasseurs[/color][/b] sont une classe très unique dans le monde de World of Warcraft. C\'est la seule classe non-magique qui fait des dégâts à distance. Ils se battent avec des arcs, des armes à feu ou des arbalètes. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Chasseurs se sentent chez eux dans la natures et ont une affinité spéciale avec les animaux. Il sait apprivoiser son propre [url=pets]familier[/url] qui l\'aidera à vaincre son ennemi. L\'animal du chasseur est unique, il possède un arbre de talent où le Chasseur peut attribuer des points dans des compétences diverses et des capacités passives. Chaques espèces de familier a une capacité spéciale unique. Le Chasseur peut rechercher les bêtes les plus appréciables en fonction de leurs apparences ou capacités. Seuls certains familiers ne sont accessibles que si le Chasseur choisi dans son arbre de talent [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Maîtrise des bêtes[/url][/icon] qui lui donne accès aux bêtes « exotique » tels que [pet=46] ou [pet=39].\n\nPendant que leurs familiers attaques, les Chasseurs font pleuvoir leurs projectiles sur leurs malheureuses cibles. Ils préfèrent s’évader du corps-à-corps et ralentir leurs ennemis pour s\'éloigner et lancer leurs salves mortelles. Ils sont aussi capable de poser des pièges pour infliger des dégâts, ralentir ou rendre impossible toutes actions de leurs ennemis.\n\nLes Chasseurs portent des armures intermédiaires (cuir/maille) et utilisent le mana pour faire des dégâts.\n[ul]\n[li] Il peut voyager très vite en utilisant [spell=13161] et le partager avec [spell=13159].[/li]\n[li] Ils ont un certain nombre de compétence accès sur la survie qu\'ils peuvent utiliser pour échapper ou éviter un danger potentiel, comme [spell=5384] et [spell=781].[/li]\n[li] Les Chasseurs spécialisés dans la [icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survie[/url][/icon] peuvent avoir [spell=53292], ce qui leur permet de fournir aux membres du raid le [spell=57669].[/li]\n[/ul]',NULL),(13,5,2,NULL,0,2,'[b][color=c5]Les Prêtres[/color][/b] sont généralement considérés comme l\'une des classes de soins les plus répandus dans World of Warcraft, car ils ont deux arbres de talents qui peuvent être utilisés pour guérir très efficacement. Les caractéristiques principales sont la puissance des sorts, l\'intelligence et l\'Esprit (s\'il s\'est spécialisé dans les soins).\n\nL\'arbre [icon name=spell_holy_holybolt][url=spells=7.5.56]Sacré[/url][/icon] comprend des talents qui renforcent fortement la guérison faite à leurs alliés, y compris des sorts qui peuvent être utilisés pour guérir plusieurs joueurs à la fois, comme [spell=48089]. \nL\'arbre de talent [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] se concentre principalement sur l\'absorption et l\'atténuation des dommages grâce à l\'utilisation de [spell=48066] et réduit les dégâts subis avec [spell=63944].\n\nLes Prêtres disposent d\'une grande palette d\'outils pour soigner, mais ils peuvent également sacrifier leurs soins pour infliger des dégâts grâce à la magie de l\'[icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Ombre[/url][/icon]. Ils sont alors capables d\'infliger des dégâts importants avec leurs capacités uniques et une fois qu\'ils se mettent en [spell=15473], leurs dégâts d\'ombre augmentent de manière significative tout en perdant la capacité de lancer des sorts du sacré.\n\nIl porte une armure en tissus, soigne les dégâts grâce à la magie du sacré mais inflige des dégâts grâce à la magie de l\'Ombre. Il utilise le mana comme ressource.\n[ul]\n[li] Fournissant les buffs les plus appréciés dans le jeu - [spell=48161], qui donne un buff d\'endurance indispensable à tout raid. Ils peuvent utiliser [spell=48073] et [spell=48169].[/li]\n[li] Les prêtres d\'ombre sont très sollicités dans n\'importe quel raid , fournissant le buff [spell=57669] pour stimuler la régénération de mana et peut même guérir leur propre groupe avec [spell=15286].[/li]\n[/ul]',NULL),(13,8,2,NULL,1,2,'[b][color=c8]Les Mages[/color][/b] sont les utilisateurs emblématiques de la magie en Azeroth, qui apprennent leur art au cours de leurs recherches et études approfondies. Ils maîtrisent la magie du feu, du givre et des arcanes pour détruire ou neutraliser leurs ennemis. Leurs caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nIls portent des armures légères, mais compensent cette faiblesse par une puissante gamme de sorts offensifs et défensifs. Le mage fait donc des gros dégâts à distance, envoyant des boules élémentaires sur un ennemi isolé mais faisant pleuvoir la destruction sur une armée. En cas d\'attaque, il peut échapper aux combats rapprochés avec [spell=1953] et devient un [spell=45438] quand cela devient trop critique.\n\nLes Mages peuvent également augmenter les pouvoirs de leurs alliés : [spell=23028], les inviter à leurs [spell=43987] et même les faire voyager à travers des [url=spells=7.8.237&filter=na=portail]portails[/url]. Classe indispensable pour voyager en toute tranquillité. Ils utilisent le mana comme ressource. Les Mages :\n[ul]\n[li]Transforment leurs ennemis en créatures inoffensives ou les geler sur place grâce à [spell=122].[/li]\n[li]Utilisent [item=50045] pour avoir un élémentaire d\'eau en familier.[/li]\n[/ul]',NULL),(13,6,2,NULL,0,2,'[b][color=c6]Les Chevaliers de la mort[/color][/b] sont d\'anciens agent du Fléau, désormais alliés avec la Horde ou l\'Alliance. Cette classe de héros débute le jeu à haut niveau (55). Ses caractéristiques principales sont la force, sans oublier l\'endurance pour les tanks.\n\nTous leurs arbres de talent peuvent être utilisés pour faire des dégâts ou tanker.\n\nLes Chevaliers de la mort qui ont une affinité avec le [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Sang[/url][/icon] ont une grande capacité d’auto-guérison et peuvent fournir à un allié : [spell=49016] qui l’enrage à la vue du sang du champ de bataille.\nL’arbre de talent [icon name=spell_frost_freezingbreath][url=spells=7.6.771]Givre[/url][/icon] permet une augmentation significative de l’armure et spécialise le Chevalier de la mort dans les dégâts de zone avec [spell=49184]\nLes maîtres des maladies et des invocations sont les chevaliers de la mort [icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Impie[/url][/icon]. Ils peuvent utiliser leurs talents [spell=52143] et [spell=49206] pour être aidé lors des combats. Ils ont aussi une plus grande résistance à la magie grâce à la [spell=51052].\n\nLe chevalier de la mort utilise des runes comme ressource principale, dont chacun des trois types est utilisé pour différentes techniques.\n[ul]\n[li] Ils se battent avec les présences (semblable aux positions d\'un Guerrier) qui fournit des bonus spéciaux à leurs rôles.[/li]\n[li] Il dispose de plus de capacités à distance que la plupart de classes de corps à corps et privilégie les maladies et les dégâts infligés par ses familiers morts-vivants.[/li]\n[li] La classe de chevalier de la mort a sa propre capacité d\'enchantement d\'arme spéciale appelée [spell=53428], ce qui remplace le besoin d\'enchantements d\'armes classiques.[/li]\n[li] Ont accès à une zone spéciale inscrite inaccessible par toutes les autres classe : Acherus, le fort d’ébène, situé dans [zone=4298]. Où ils gagneront leurs points de talent en tant que récompenses de quêtes dans les premières heures de jeux.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=48778] - Niveau 55 - Bonus de Vitesse de 100%. [/li]\n[li] [spell=54729] - Niveau 60 - Bonus de vitesse : s’adapte à la compétence de monte. [/li]\n[/ul]',NULL),(13,7,2,NULL,0,2,'[b][color=c7]Les Chamans[/color][/b], maîtres des éléments et de la nature, apportent un grand nombre de buffs à tout un groupe sous forme de totem. Un Chaman peut appeler un totem de chaque élément : terre, feu, eau et air. Ces totems apparaissent à leurs pieds et sont actifs pour toutes les personnes du raid se trouvant dans la zone d’effet du totem. Un bon Chaman sait quels totems sont à lancer et dans quelles circonstances les utiliser, pour maximiser les dégâts du groupe et la survie.\n\nIls sont principalement des lanceurs de sorts, bien qu’un Chaman [icon name=spell_nature_lightningshield][url=spells=7.7.373]Amélioration[/url][/icon] aime se rapprocher des ennemis pour faire de gros dégâts. Il apprend l’[spell=30798] et peut utiliser le sort [spell=51533] pour invoquer 2 Esprits de Loups qui combattent avec lui. Bien qu’il soit principalement de mêlée, le Chaman Amélioration peut bénéficier de la puissance des sorts et lancer instantanément [spell=403] ou des soins avec le talent [spell=51530]. \n\nLes Chamans [icon name=spell_nature_lightning][url=spells=7.7.375]Élémentaires[/url][/icon] se tiennent en retrait pour lancer leurs sorts de feu et de foudre et infliger de grandes quantités de dégâts. Ils peuvent repousser leurs ennemis avec [spell=51490] et aussi les enraciner avec [spell=51486]. Ils apportent le [icon name=spell_fire_totemofwrath][url=spell=57722]Totem de courrou[/url][/icon] et le [spell=51470], buffs très recherchés dans les raids.\n\nLes Chamans qui choisissent [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restauration[/url][/icon] ont un grand panel de sort de guérison se qui leurs permets de se spécialiser dans le soin mono-cible ou multi-cible. Ils sont reconnus pour leurs puissantes [spell=1064] et pour créer un [spell=16190] qui aide la restauration de mana aux membres de leurs groupes. Ils gagnent aussi un puissant [spell=974], peuvent employer [spell=51886] pour enlever les malédictions, et ont un sort de guérison instantané : [spell=61295] qui soigne aussi au fil du temps.\n\nLes Chamans invoquent la puissance des éléments pour améliorer les dégâts de leurs armes ou sorts. Ils portent des armures moyennes, boucliers et utilisent le mana comme ressources.\n[ul]\n[li] Il peut apprendre plus de 20 totems différents.[/li]\n[li] Peuvent lancer [spell=32182] (ou [spell=2825]) pour amplifier les dégâts et les soins de tout le raid. Un buff unique très recherché.[/li]\n[li] Un chaman peut se transformer en [spell=2645] à partir du niveau 16 et peut même le rendre instantané avec le talent [spell=16287]. Ce sort ne peut être utilisé qu\'en extérieur.[/li]\n[li] Il ne peut avoir qu\'un seul bouclier élémentaire d\'actif sur lui [spell=324] ou [spell=52127]. Le [spell=974], peut-être posé sur un autre joueur.[/li]\n[/ul]',NULL),(13,11,2,NULL,0,2,'[b][color=c11]Les Druides[/color][/b] sont la « classe à tout faire » de World of Warcraft, c\'est-à-dire, capable de remplir tous les rôles : soigner, faire des dégâts à distance, faire des dégâts de mêlée ou tanker, en utilisant le Changeforme. Le druide offre donc aux joueurs de nombreux styles de jeu. Ses caractéristiques principales dépendent du rôle choisi.\n\nSous sa forme normale, c’est un lanceur de sorts qui peut se battre à distance et se soigner. Mais il peut aussi prendre d’autres formes dont des formes animales :\n\nLorsqu’un druide se transforme en [spell=5487] (et à un niveau plus avancé, [spell=9634]), son mana se change alors en rage, capable de charger sa cible, de la [spell=8983] et de subir des coups de plusieurs adversaires simultanément. C’est une forme orientée vers le tanking qui fournit une armure et de la vie supplémentaire. Il peut esquiver les coups, utiliser [spell=22812] pour augmenter sa résistance.\nQuand il se transforme en [spell=768], son mana se change alors en énergie, pouvant [spell=5215] tout en se déplaçant, d’augmenter parfois ça vitesse de courses de 70% et de bondir derrière ces ennemis pour attaquer avec le talent [spell=49376]. C’est une forme orienté vers les dégâts de mêlée en faisant saigner leur cible avec [spell=49800] ou [spell=62078] lorsque le druide est entouré d’ennemis.\nAvec les talents de druide équilibre, la [spell=24858] est réputé pour faire beaucoup de dégâts à distance notamment avec les sorts [spell=5176] et [spell=48505] qui peuvent être augmenté avec des points de talent. Il émet aussi une aura, qui augmente les coups critiques des sorts, très appréciée en raid.\nSa forme d’[spell=33891] (talent restauration) est conçue pour soigner sur la durée notamment avec les sorts [spell=33763] et [spell=48438]. Il émet une aura, qui augmente les soins de 6%. Il a la particularité d’avoir une grande régénération de mana.\n\nD’autres formes animales secondaires complètent cette liste : sa [spell=783] qui permet au druide d’augmenter sa vitesse de déplacement, sa [spell=1066] qui lui permet de respirer sous l’eau tout en nageant plus vite et sa [spell=33943] (et avec la compétence [spell=34091], la [spell=40120]) lui permet de voler instantanément.\n\n[ul]\n[li] Dans l’arbre de talent Combat farouche, les druides ont une aura [spell=17007] très utile pour tout groupe de raid.[/li]\n[li] Le sort [spell=20484] est utilisable en combat, mais à une recharge de 10 min.[/li]\n[li] Il possède le sort [spell=29166] qui lui permet de régénérer le mana très vite même en combat, sur lui ou tout autre membre.[/li]\n[li] Les Druides ont leur propre capacité de téléportation qui leur permet de voyager vers [zone=493], ce qui est utile lorsqu’ils ont besoin de s’entraîner.[/li]',NULL),(13,9,2,NULL,0,2,'[b][color=c9]Les Démonistes[/color][/b], vêtue d’armure légère, sont les maîtres des arts démoniaques. Ils possèdent des capacités très puissantes qui, si elles sont utilisées correctement, en font un adversaire formidable. Utilisant leurs malédictions en combinaison avec des sorts de dégâts directs, il cause des ravages et la destruction. Ses caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nLes Démonistes qui ont choisi de se spécialiser dans l’arbre de talent Affliction, excellent dans l’utilisation des malédictions, ils posent sur leurs ennemis [spell=47865] pour les affaiblir ou [spell=47864] pour leurs faire des dégâts. Ils ont la [spell=18271] ce qui augmente les dégâts des sorts d’ombre de 25%.\nLe démonologue appel des démons pour l’aider dans ces combats, il emploie principalement l’[spell=30146]. Il peut aussi se [spell=59672] en démon pour augmenter ses dégâts durant une courte période.\nLe Démonistes destruction utilise des sorts de feu tels que [spell=5740] ou [spell=17962] pour infliger d’importants dégâts directs.\n\nLes Démonistes, tout en étant d’excellent dans les dégâts à distance, soutiennent beaucoup leurs alliés en appelant d’autre joueur avec [spell=698] ou en utilisant des magies rituelles pour conjurer des pierres imbues du pouvoir de guérir : [icon name=inv_stone_04][url=item=5509]Pierre de soin[/url][/icon].\n\n[ul]\n[li] Le démoniste est doté du sort [spell=1454] qui lui permet de sacrifier des points de vie pour régénérer son mana.[/li]\n[li] Le [spell=48020] lui permet une grande mobilité en annulant tous les effets de déplacement, et en s\'éloignant du corps-à-corps.[/li]\n[li] En utilisant le sort [spell=20022], le démoniste permet à la personne sur qui elle a été appliqué de ressusciter.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=5784], leurs yeux ne brûlent plus que d\'une haine inextinguible pour les démonistes qui les ont corrompus - Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23161] sont des destriers recréés qui ont été corrompus par les énergies infernales, transpirant et soufflant le feu - Niveau 60 - Bonus de vitesse : 100%. [/li]\n[/ul]',NULL),(8,81,2,NULL,0,2,'[b]Les Pitons du Tonnerre[/b] est la faction de la capitale des Taurens : [zone=1638], située dans la partie nord de la région de [zone=215]. L\'ensemble de la ville est construit sur des falaises à plusieurs centaines de pieds au-dessus du paysage environnant, elle est accessible par des ascenseurs sur les côtés sud-ouest et nord-est.\n\n[h3]Histoire[/h3]\n\nLa grande ville de Pitons du Tonnerre se trouve au sommet d\'une série de mesas qui donnent sur les prairies verdoyantes de Mulgore. Les Taurens, autrefois nomade, ont récemment construit la ville pour dresser un centre de caravanes commerciales avec des artisans itinérants et des artisans de toutes sortes. Elle a été établi par le puissant chef [npc=3057] après que les Taurens, avec l\'aide des Orcs, ont chassé les centaures qui habitaient à l\'origine Mulgore. De longs ponts de corde et de bois font la liaison entre les mesas qui sont surmontées de tentes, de longues maisons, de totems peints aux couleurs vives et de huttes spirituelles. Le chef de Tauren surveille la ville animée, en veillant à ce que les tribus unies de Tauren vivent en paix et en sécurité.\n\n[h3]Réputation[/h3]\n\n[npc=14728] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Pitons du Tonnerre, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url].',NULL),(8,1038,2,NULL,0,2,'[b]Ogri\'la[/b] est un groupe d\'Ogres localisé dans [zone=3522], où leur proximité avec [item=32572] leur a permis d\'évoluer au-delà de leur nature brutale. Ils sont particulièrement impliqué dans une guerre contre le Dragon noir et la Légion ardente, qui cherchent les cristaux Apogides pour leurs propres fins.\n\n[h3]Localisation[/h3]\nOgri\'la est situé près du bord ouest des Tranchantes, entre le Camp de Forge: Terreur et le Camp de Forge: Courroux, juste à l\'ouest de Sylvanaar. Ogri\'la est seulement accessible en monture volante ou en forme de vol. Une autre alternative est d\'avoir une réputation d\'honoré ou plus élevé avec [faction=1031]. Mais un joueur doit avoir une monture volante pour atteindre le camp Garde Ciel près de Skettis.[pad]\n\n[h3]Reputation[/h3]\nLa reputation avec Ogri\'la ne peut être acquise que par quêtes, et il n\'y a que des quêtes répétables dont les [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes journalières[/url]. Il ya un plafond sur la quantité de réputation que l\'on peut obtenir chaque jour pour un joueur avec Ogri\'la, ce qui en fait une réputation \"difficile à farmer\".\n\n[b]Eclats Apogides[/b]\n[item=32569] peuvent être collectées de diverses manières. Ils peuvent être pillés sur le cadavres de monstres, recueillis à partir de l\'environnement, ou ils peuvent être en récompenses de quêtes terminées.[pad]\n[b]Cristaux Apogies[/b]\n[item=32572] se ramassent sur les élites de type Demons ou Dragons dans les Tranchantes. Pour appeler ces mobs, 35 Eclats Apogides sont nécessaires, et il est recommandé que vous ayez un groupe de 5 personnes pour les vaincre.\n\n[b]Quêtes[/b]\nIl y a un certain [url=?quests&filter=cr=1;crs=1038;crv=0]nombre de quêtes[/url] qu\'un joueur peut faire pour gagner de la réputation avec Ogri\'la, ainsi que plusieurs [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes quotidiennes[/url]. Beaucoup de quêtes quotidiennes seront également accordée à la réputation de la Garde Ciel Sha\'tari lorsqu\'elles seront complétées. \n\nPour accéder aux principales quêtes d\'Ogri\'la, un joueur doit d\'abord compléter les 5 quêtes de groupe de [npc=22941].\n\n[h3]Éléments épuisés[/h3]\nUn certain nombre d\'éléments apogides tombent parfois de mobs une fois mort. Lorsque vous avez amassé 50 éclats apogides, [url=?search=Apexis+Crystal+Infusion]les objets suivants peuvent être améliorés[/url], obtenant des statistiques supplémentaires et des emplacements de gemmes. Une fois ces objets améliorés, ils deviendront liés si équipés, et peuvent donc être vendus ou échangés avec d\'autres joueurs. Une chose à noter cependant, bien que les éléments épuisés peuvent également avoir des statistiques ou des effets, ils ne peuvent pas être équipés.',NULL),(8,911,2,NULL,0,2,'[b]Lune d\'Argent[/b] est la capitale des elfes de sang, située dans la partie nord-est de [zone=3430] dans le royaume de Quel\'Thalas. La capitale,des elfes de sang, est à couper le souffle. Elle peut rivaliser avec la capitale naine de [zone=1537], capitale la plus ancienne du monde toujours debout. Récemment reconstruite, la ville abrite la plus grande population d\'elfes de sang en Azeroth. \n\nAujourd\'hui, Lune d\'Argent n\'est que la moitié orientale de la ville d\'origine. La moitié occidentale a été presque entièrement détruite par le fléau pendant la troisième guerre. La place de lÉpervier, est la seule partie occidental de Lune d\'Argent restant sous le contrôle des elfes de sang. La Malebrèche, chemin parcouru par Arthas Menethil et son armée de morts-vivants parties en quête de ressusciter Kel\'Thuzad, traverse tout le Bois des Chants éternels. Il sépare la Lune d\'Argent reconstruite et ces ruines de la moitié occidentale. Fait intéressant, les ruines de Lune d\'Argent ne logent pas de morts-vivants, au lieu de cela, elles contiennent des [url=?npcs&filter=cr=37;crs=6;crv=1502;na=Déshérité;maxle=8]déshérités[/url] et des [npc=15638]. Dans l\'état actuel des choses, Lune d\'Argent est encore la plus grandes des villes Hordeuses.\n\n[h3]Histoire[/h3]\n\nLa ville de Lune d\'Argent a été fondée par les hauts élus après leur arrivée à Lordaeron, il y a des milliers d\'années. La ville a été construite en pierre blanche autour de plantes vivantes dans le style de l\'ancien Empire Kaldorei. La ville contenait les célèbres académies de Lune d\'Argent, centre d\'apprentissage de la magie arcane, et la Flèche de Solfurie, majestueux palais abritant la famille royale des hauts-elfes. Également basé dans la ville, la convocation de Lune d\'Argent, également connu sous le nom de « Le Concile de Lune d\'Argent », était l\'organe dirigeant des hauts-elfes. À travers une étendue d\'océan vers le nord, il y a l\'île qui contient le plateau du puits du Soleil.\n\nBien que Lune d\'Argent ait resorti relativement indemne de la deuxième guerre, dans la troisième guerre, le Chevalier de la mort Arthas a mené le Fléau dans la ville, l\'attaquant au cours de sa quête pour atteindre le puit du Soleil. Le roi High Elven a été tué et la majorité de la population a été exterminée. Les forces de fléau ont tenu la ville pendant un certain temps mais l\'ont abandonné après l\'épuisement de ses ressources. \n\nBien que la ville ait été attaquée par le Fléau, elle n\'est pas aussi détruite qu\'on pourrait le penser. Beaucoup de ses plantes sont mortes, quelques cadavres sont étendu sur le pavé, la ville était à l\'abri du feu et de la destruction. Lune d\'Argent ressemble maintenant à une ville fantôme, intacte, mais étrangement abandonnée. Néanmoins, les chasseurs de trésors fréquentent fréquemment les ruines de Lune d\'Argent pour essayer de trouver certains des artefacts précieux que les elfes ont laissés derrière avant de déserter la ville, mais les fantômes des anciens habitants de Lune d\'Argent les en empêchent.\n\n[h3]Réputation[/h3]\n\n[npc=20612] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Lune d\'Argent, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=151;crs=6;crv=35513;na=Faucon-pérégrin]Faucon-pérégrins[/url].\n\nLes zones environnantes du Bois des Chants éternels et des terres fantômes contiennent la plupart des quêtes pour gagner de la réputation avec Lune d\'Argent.',NULL),(8,577,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[b]Long-guet[/b]\n[faction=369]\n[faction=470]\n[/Minibox]\n\n[b]Long-guet[/b], faction de la ville du même nom, est un poste commercial dirigé par les gobelins du Cartel Gentepression. Il se trouve au carrefour des principales routes commerciales du [zone=618].\n\n[h3]Histoire[/h3]\n\nCette ville est le dernier point de la civilisation avant d\'atteindre le Mont Hyjal. Il est géré par les gobelins comme un poste commercial. La ville est officiellement neutre pour toutes les races et factions. Seuls les pèlerins peuvent monter jusquà lArbre-Monde, point culminant du Mont Hyjal. Long-guet est donc la destination la plus haute que les marchands et les aventuriers peuvent atteindre sans l\'autorisation des Elfes de nuit. Elle offrirait une vue dominante sur Kalimdor, si les nuages qui enveloppent continuellement les flancs de la montagne, disparaissaient.\n\nLong-guet est le seul avant-poste de gobelin majeur dans le nord de Kalimdor. Tout d\'abord, il sert de base aux opérations pour les mineurs de thorium et d\'arcanites puisque le Berceau-de-lHiver possède quelques veines inexploitées de ces matériaux. Deuxièmement, il sert de centre d\'échanges entre l\'Alliance et la Horde. Alors que Long-guet est à peine plus sûr que Reflet-de-Lune, généralement, l\'Alliance et la Horde se traitent assez bien là-bas. En outre, Long-guet est un point d\'arrêt et de réapprovisionnement fréquent pour les fidèles qui font le pèlerinage du Berceau-de-lHiver au Mont Hyjal.\n\n[h3]Réputation[/h3]\n\nLa réputation de Long-guet et du Cartel Gentepressin provient surtout des quêtes du Berceau-de-lHiver. Avec une réputation au minimum amicale, les gardiens vous aident en cas dattaque initiée contre vous.',NULL),(8,21,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[b]Baie-du-Butin[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Baie-du-Butin[/b] est une grande ville pirate nichée dans les falaises entourant un magnifique lagon bleu, à lextrémité de [zone=33]. Pour entrer dans la ville, il faut passer au travers les mâchoires blanchis d\'un requin géant.\n\nParcouru par les Écumeurs des Flots noirs qui sont étroitement associés eu Cartel Gentepression, le port offre des opportunités à n\'importe quel voyageur passant par là, indépendamment de leur faction. Combiné à la célèbre « taverne du Loup de mer », le [event=15], de nombreux maîtres de profession et des vendeurs, qui vendent de tout (des animaux de compagnie aux anneaux de diamant), c\'est l\'un des endroits les plus populaires en Azeroth.\n\n[npc=2496], chef de la ville, embauche toute l\'aide qu\'il peut obtenir contre [faction=87] et autres menaces de la ville. Il réside avec le chef des Écumeurs des Flots noirs, [npc=2487], au sommet de l\'auberge de Baie-du-Butin.\n\nEn raison de la liaison par bateau de Baie-du-Butin à Cabestan, les joueurs de tout niveau (surtout de la Horde, si le niveau est faible) peut-être croisés dans le port, bien que les visiteurs les plus fréquents seront dans les niveaux 35-45, car les quêtes disponibles auprès des gens du pays se situent dans cette tranche de niveau.\n\nL\'eau est parsemée de débris flottants et de bancs de poissons. Plusieurs types de poissons se pèchent dans les eaux de la Baie, tels que le [item=6359], le [item=6358], et l\'[item=13422]. La pêche, dans les débris flottants, vous donnera également plus de chance de pêcher des coffres et d\'autres articles, faisant de Baie-du-Butin un endroit idéal pour la pêche.\n\n[h3]Réputation[/h3]\nLa plupart des quêtes pour augmenter la réputation avec Baie-du-Butin sont situés au Cap de Strangleronce. Avec une réputation au minimum amicale, les gardes vous aiderons en cas dattaque contre vous.\n\nSi vous êtes haï avec Baie-du-butin vous pouvez faire la quête répétable [quest=9259] pour revenir à Neutre.',NULL),(8,470,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Cabestan[/b]\n[/Minibox]\n\n[b]Cabestan[/b], faction de la ville du même nom, situé sur la côte est de Kalimdor dans [zone=17]. Elle est dirigée par des gobelins. Ses rues se répandent dans toutes les directions, et l\'architecture ne montre aucune cohérence ni vision commune. C\'est une ville de divertissement et de commerce, où tout ce que vous voudriez acheter est en vente mais aussi beaucoup de chose que personne ne veut jamais. \n\nCabestan est actuellement géré par un groupe d\'entreprises connu sous le nom du Cartel Gentepression, un groupe fragmenté de la KapitalRisk, qui a d\'abord construit la ville portuaire pour la négociation avec [zone=1637]. C\'est d\'abord une faction neutre où Horde et Alliance se côtoient. Un bateau relie commodément Cabestan à Baie-du-butin.\n\n[h3]Histoire[/h3]\n\nConstruit à part égales entre l\'industrie et de la décadence, la ville portuaire gobeline de Cabestan s\'étend sur près d\'un kilomètre de littoral des Tarides de l\'est, entre [zone=14] et [zone=15]. Cabestan est la fierté des gobelins, une ville commerciale où vous pouvez trouver presque tout ce que votre cur désire, et si quelque chose n\'est pas en stock, vous pouvez parier que les gobelins peuvent le commander. Cabestan est desservie régulièrement par les bateaux qui font la traversé en passant devant la forteresse de Theramore, vers le sud.\n\nCabestan est une ville où les habitants, qui étaient autrefois des truands, règnent maintenant. Ses rues errent sans rime ni raison à travers des quartiers dédiés à une seule activité : le commerce. Des entrepôts délabrés se situent à côté de maisons en pierre majestueuses. Les belles boutiques sont voisines avec des cabanes grossières. Des objets de toutes les formes, et certains au-delà de l\'imagination, sont exposés sur les marchés et les boutiques exclusives.\n\nLes Gobelins accueillent toutes personnes ayant de l\'or, des éléments de valeur et une volonté de les échanger contre leurs marchandises et leurs services. Les marchands traversent la ville tous les jours, vendent tout, de la soie aux esclaves. Même la nuit, les magasins qui bordent les rues et les allées restent ouverts aux entreprises. Ceux qui ont de l\'argent peuvent écouter des musiciens qualifiés, tout en buvant des bières fines et en mangeant des aliments préparés par des grands chefs. Pour ceux qui ont des goûts plus terriens, on retrouve le long des quais des marchants d\'armes, la banque et des casinos.\n\nCabestan est le plus grand port de Kalimdor, beaucoup de navires transportant de la cargaison sortent pour d\'autres sites autour de Kalimdor. En plus des navires commerciaux légitimes, les bâtiments pirates reçoivent une amnistie dans le port de Cabestan tant qu\'ils peuvent payer des droits d\'accostage rigides. Cette situation rend les capitaines marchands furieux, mais ils ne peuvent boycotter Cabestan, sinon c\'est la faillite pour leurs commerces. En outre, les avocats et les mercenaires qui rôdent sur le front de mer sont impatients de faire face à tous ceux qui cherchent à causer des problèmes.\n\n[h3]Réputation[/h3]\n\nLa plupart des quêtes pour élever la réputation avec Cabestan et le Cartel Gentepression sont situées dans les Tarides. Avoir une réputation au minimum amicale, les gardiens aident en cas d\'attaque contre vous.\n\nSi vous êtes détesté auprès de Cabestan, vous pouvez faire la quête répétable [quest=9267] pour revenir à une réputation Neutre.',NULL),(8,369,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] est la faction de la ville du même nom, qui abrite les plus grands ingénieurs, alchimistes et marchands gobelins. Seul endroit de civilisation au nord du désert de [zone=440], elle est perçue comme une oasis. Gadgetzan est le siège du Cartel Gentepression, le plus grand cartel gobelin. Les gobelins croient au profit plus quà la loyauté, donc Gadgetzan est considéré comme territoire Neutre dans le conflit Horde / Alliance.\n\n[h3]Histoire[/h3]\n\nBien que la neutralité des gobelins soit presque universellement reconnue, il y a encore ceux qui cherchent à semer le chaos et lanarchie. Pour Gadgetzan, cela vient sous la forme des bandits Bat-le-désert, une bande de mécréants qui occupe le champ des Puisatiers et les ruines d\'Ombre-du-Zénith au Nord-est de Tanaris. Peu de Gobelins se soucient des ruines antiques (à moins quils y aient un trésor), les bandits peuvent avoir les vieux blocs de pierre. \nCependant, le champ des Puisatiers est vital pour la survie des gobelins, leur fournissant lor liquide du désert. Les tours d\'eau dans le champ ont été construites sous la chaleur ardente du soleil, par le travail de leurs esclaves. Les gobelins ne vont pas abandonner leurs tours durement gagnées, aussi facilement. Mais, ils doivent rester en ville pour arrêter le conflit, en apparence interminable, parmi les différents visiteurs et donc empêcher de perturber les affaires. Par conséquent, ils embauchent de braves mercenaires venant de tous les coins du monde pour les aider.\n\n[h3]Réputation[/h3]\n\nEn tuant les [url=?npcs=7&filter=na=mers+du+Sud]Flibustiers des mers du Sud[/url] et les [url=?npcs=7&filter=na=bat-le-désert]Bandits Bat-le-désert[/url], la réputation avec le cartel Gentepression augmentera. Ayant une réputation au minimum amicale, les gardes vous aideront en cas d\'attaque contre vous. Avoir une réputation exaltée signifie que les gardes ne vous attaqueront jamais même si vous lancez des attaques sur la faction opposée. \n\nLa plupart des quêtes associées à la faction Gadgetzan sont situées à Tanaris. \n\nSi vous êtes détestés avec Gadgetzan, vous pouvez faire la quête répétable [quest=9268] pour obtenir la Neutralité.',NULL),(8,47,2,NULL,0,2,'[b]Forgefer[/b] est la faction associée à la capitale des nains, [zone=1537]. [npc=2784] règle son royaume de Khaz Modan de sa salle du trône dans la ville, et [npc=7937], chef des gnomes, a temporairement dû s\'établir dans Brikabrok après la récente chute de la ville gnome [zone=133].\n\n[h3]Histoire[/h3]\n\nForgefer est l\'ancienne demeure des nains, une merveille façonnée dans la pierre. Forgefer a été construite au cur même des montagnes, une ville souterraine qui abrite des explorateurs, des mineurs et des guerriers. Les portes massives de roche protègent la ville en temps de guerre, et la lave de la montagne est redirigée et distribuée à des fins de chaleur, d\'énergie et de forage. \nAvant que le clan de Sombrefer ne soit banni de la ville, menant à la Guerre des Trois Marteaux, Forgefer était le centre commercial et social de tous les clans nains. Il appartient maintenant au Clan Barbe-de-bronze. \nBeaucoup de bastions nains ont chuté pendant la Guerre de Lordaeron, entre la Horde et l\'Alliance, mais la puissante ville de Forgefer, nichée dans les sommets hivernaux de [zone=1] et protégée par ses grandes portes, n\'a jamais été violée par la Horde envahissante.\n\nRelativement récemment, Forgefer est également devenu le foyer des Exilés de Gnomeregan. Après la troisième guerre, la ville gnome fut envahie par Troggs. Depuis lors, un certain nombre de gnomes se sont installés à Forgefer, transformant une zone de cette ville à leur goût, une région connue sous le nom de Brikabrok.\n\nForgefer est l\'une des villes les plus peuplées du monde, venant après la ville humaine de [zone=1519], et abritant 20 000 personnes.\n\nAlors que l\'Alliance a été affaiblie par les événements récents, les nains de Forgefer, dirigés par le roi Magni Barbe-de-bronze, forment un nouveau futur dans le monde. \n\n[h3]Réputation[/h3]\n\n[npc=14723] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Forgefer, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92:151:151;crs=2:1:6:6;crv=0:0:33977:33976;na=bélier] béliers [/url].\n\nLes zones environnantes [zone=1], [zone=38] et [zone=11] contiennent la plupart des quêtes pour gagner de la réputation auprès de Forgefer.',NULL),(8,54,2,NULL,0,2,'[b]Les Exilés de Gnomeregan[/b] est la faction des gnomes qui ont fui leur domicile, [zone=133] à [zone=1]. Elle a été détruite par [url=?npcs=7&filter=na=Trogg] les Troggs[/url] après une invasion toxique. Maintenant, membre de lalliance, la plupart sont situés à Brikabrok, une partie de la ville voisine [zone=1537], y compris le leader [npc=7937].\n\n[h3]Histoire[/h3]\n\nOn a spéculé que les gnomes ont été formés comme des robots par les titans, en raison de leur nature curieuse et de leurs compétences techniques. Ils vivaient autrefois dans la cité de Gnomeregan, sans doute la plus belle ville technologique du monde.\n\nLes gnomes étaient une race souterraine de bricoleurs, jusquà ce que les Troggs aient détruit Gnomeregan. Dans cette guerre, plus de 80% de la population gnome a été exterminé.\n\n[h3]Réputation[/h3]\n\n[npc=14724] offre une quêtes répétables où il faut fournir des étoffes. En étant exalté aux Exilés de Gnomeregan, les joueurs sont capables de conduire des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=mécanotrotteur]mécanotrotteurs[/url].\n[zone=1] contient la plupart des quêtes pour gagner la réputation avec les exiés de Gnomeregan.',NULL),(8,72,2,NULL,0,2,'[b]Hurlevent[/b] est la faction associée à [zone=1519], la capitale des Humains. Elle est située dans la partie nord-ouest de la [zone=12]. L\'enfant roi, [npc=1747], réside dans le Donjon de Hurlevent, entouré de ses gardes du corps et de ses conseillers, [npc=1748] (le régent) et [npc=1749]. La ville est nommée ainsi à cause des rafales soudaines et occasionnelles créées par la forme spéciale des montagnes autour de la ville glorieuse.\n\n[h3]Histoire[/h3]\n\nPendant la Première Guerre, le Royaume d\'Azeroth, y compris sa capitale, le Donjon de Hurlevent, a été complètement détruit par la Horde. Ses survivants ont fui vers Lordaeron. Après que les orcs ont été vaincus, au Portail des Ténèbres, à la fin de la Deuxième Guerre, il a été décidé que la ville serait reconstruite, dépassant sa grandeur dantan. Des tailleurs de pierres et des architectes ont pu été rassemblés par les nobles de Hurlevent. Sous la directio de cette équipe, la plus qualifiée et la plus ingénieuse, Hurlevent a été reconstruit dans une période de temps incroyablement courte. Maintenant, à la fin de la troisième guerre, dans le renommé Royaume de Hurlevent. Cest l\'un des derniers bastions du pouvoir humain laissé dans le monde.\n\nAvec la chute des Royaumes du Nord, Hurlevent est de loin la ville la plus peuplée du monde. Avec une population de deux cents mille personnes (principalement humaines), elle sert à bien des égards comme le centre culturel et commercial de l\'Alliance, même avec un accès à la mer. Les humains qui vivent dans la ville sont généralement insouciants et artistiques, favorisant les vêtements légers et colorés, la cuisine et l\'art. Elle abrite l\'Académie des sciences arcanes, la seule école de sorcellerie dans les royaumes de l\'Est, ainsi que le SI:7, une organisation de renseignement.\n\nCependant, les gens de Hurlevent ont du mal à accepter le rôle de Theramore en tant que foyer de la nouvelle Alliance. Ils sont convaincus que Hurlevent devrait être l\'héritière légitime du rôle de la ville de Lordaeron comme par le passé, mais aussi que Theramore est attristé face à l\'aggravation de la situation au sein de Les Royaumes de l\'Est.\n\n[h3]Réputation[/h3]\n\n[npc=14722] propose une quête répétable pour obtenir une réputation plus élevée avec Hurlevent. En contrepartie d\'une réputation exaltée, les joueurs non-humains peuvent monter sur des chevaux.\n\nLa plupart des quêtes associées à Hurlevent viennent des zones environnantes de la forêt d\'Elwynn, [zone=40] et [zone=44].',NULL),(8,930,2,NULL,0,2,'[b]Exodar[/b] est la faction associée à [zone=3557], la capitale enchantée des Draeneï construit avec la plus grande partie de leur vaisseau qui sest écrasé. Il est situé dans la partie ouest de l[zone=3524]. Le chef de la faction Exodar est [npc=17468], qui est situé près des maîtres de combat dans la Voûte des Lumières.\n\n[h3]Histoire[/h3]\n\nLes Draeneï rescapés du crash de leur vaisseau se sont récemment réveillés pour reconstruire lExodar, encore fumant de limpact. L\'Exodar était autrefois une structure de satellite naaru autour de la forteresse dimensionnelle du [url=?search=donjon+tempête]Donjon de la Tempête[/url]. L\'Exodar contient une grande quantité de merveilles technologiques (en raison de ses origines avec le Donjon), comme des «fils» magiquement enchantés qui transmettent de l\'énergie sainte dans tout le navire pour alimenter le chauffage et l\'éclairage, tout en augmentant les pouvoirs, déjà considérable, des Draeneï.\n\n[h3]Réputation[/h3]\n\nComme pour les autres grandes factions associées aux races principales, la réputation de l\'Exodar peut être acquise en faisant la quête répétable de [npc=20604] [small][/small], ou alors, en tuant la faction adverse dans [zone=2597] (les elfes de sang) et en faisant les quêtes appropriées. Avec la réputation, le joueur peut acheter des objets provenant de fournisseurs liés à Exodar pour 10% de moins et, une fois exalté, le joueur peut acheter [url=?Items=15.5&filter=na=elekk;cr=93:92;Crs=2:1;crv=0:0] diverses montures[/url].',NULL),(8,69,2,NULL,0,2,'[b]Darnassus[/b] est la faction de la ville de [zone=1657], la capitale des Elfes de la nuit. La haute prêtresse, [npc=7999], réside dans le Temples de la Lune, entourée d\'autres surs d\'Elune. Dans l\'Enclave Cénarien, l\'[npc=3516] conduit le [faction=609], souvent en opposition directe avec ses autres druides à [zone=493] et Tyrande elle-même.\n\n[h3]Histoire[/h3]\n\nAu lendemain de la troisième guerre, les Elfes de la nuit devaient s\'adapter à leur existence mortelle. Un tel ajustement était loin d\'être facile. Beaucoup d\'Elfes de la nuit ne pouvaient pas s\'adapter aux perspectives de vieillissement, de maladie et de fragilité. En cherchant à retrouver leur immortalité, un certain nombre de druides capricieux conspiraient pour planter un arbre spécial qui rétablirait un lien entre leurs esprits et le monde éternel.\n\nAvec [npc=15362] disparu, Fandral Forteramure, le chef de la conspiration qui souhaitaient planter le nouvel Arbre-Monde, est devenu le nouvel Archidruide. En un rien de temps, lui et ses camarades druides ont pris les devants et ont planté le grand arbre, [zone=141], au large des côtes orageuses du nord de Kalimdor. Avec leur soin, l\'arbre a poussé au-dessus des nuages. Parmi les branches crépusculaires de l\'arbre colossal, la merveilleuse ville de Darnassus a pris racine. Cependant, l\'arbre n\'a pas été béni par la nature et s\'avère être corrompu par la Légion Ardente. Maintenant, la faune et même les membres de Teldrassil sont contaminés par une obscurité croissante.\n\n[h3]Réputation[/h3]\n\n[npc=14725] offre une quête répétable [quest=7800] utilisé par les joueurs de l\'Alliance pour obtenir le droit de monter des [url=?items=15.5&filter=cr=93:92:151;crs=2:1:6;crv=0:0:13086;na=sabre;si=-1]Sabres-de-nuit[/url]. Les joueurs qui sont au minimum niveau 44, cherchant à gagner la faveur de Darnassus, devraient trouver et compléter les quêtes de [zone=357]. Les quêtes sont associées à Darnassus et pourraient accroître considérablement votre réputation.',NULL),(8,809,2,NULL,0,2,'Les [b]Shen\'dralar[/b] sont la faction des Elfes de nuit restant dans [zone=2557]. Ils sont un groupe qui pratique la magie arcane à son apogée sur les traces de leur ancienne reine Azshara, et de ses partisans, les Bien-nées. Ils vivent à Eldre\'Thalas (nom antérieur de Hache-tripes) depuis la fin de la guerre des Anciens. Ils sont peu nombreux, mais leur connaissance et leur pouvoir mystique sont géniaux.\n\nLeur chef, [npc=11486], était chargé de superviser la construction des pylônes pour contenir le grand démon [npc=11496] et absorber son pouvoir démoniaque. Après de longues et nombreuses années, le pouvoir des pylônes a commencé à diminuer, le prince a entrepris de tuer les elfes de nuit restants pour maintenir l\'énergie. Les esprits des défunts demandent vengeance, mais seuls des aventuriers aguerris peuvent le tuer. Faite-vite, il reste très peu d\'habitants en vie.\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en rendant à plusieurs reprises les quêtes obtenus avec les trois Librams de Hache-Tripes : [item=18333], [item=18334] et [item=18332]. \nLa réputation peut être obtenue aussi via les livres de classe suivant :\n[ul] \n[li] [item=18357] - Guerrier [/li] \n[li] [item=18363] - Chaman [/li] \n[li] [item=18356] - Voleur [/li] \n[li] [item=18360] - Démoniste [/li] \n[li] [item=18362] - Prêtre [/li]\n[li] [item=18358] - Mage [/li]\n[li] [item=18364] - Druide [/li]\n[li] [item=18361] - Chasseur [/li]\n[li] [item=18359] - Paladin [/li]\n[li] [item=18401] - Guerrier et Paladin [/li] \n[/ul] \nLes livres de classe et les librams donnent 500 points de réputation chacun.',NULL),(8,349,2,NULL,0,2,'[b]Ravenholdt[/b] est une guilde de voleurs et d\'assassins qui ne reçoit que ceux d\'une extraordinaire prouesse. Ils sont opposés à la [faction=70]. La quête, [quest=8249], est disponible pour les classes non-voleurs, mais elle nécessite l\'aide d\'un voleur pour obtenir les objets pour la quête. Le manoir de Ravenholdt, le siège de la faction, est situé dans [zone=36], mais pour y arriver, vous devez venir du coin nord-est de [zone=267].\n\n[h3]Réputation[/h3]\n\nTous les [url=?Search=Syndicat#npcs]membres du Syndicat [/url] donnent 1-5 points de réputation en fonction de votre niveau actuel. De plus, il existe quelques quêtes qui augmentent votre réputation, mais la méthode principale pour élever votre réputation provient des quêtes répétées pour fournir les objets demandés.\n\nVous commencez à une réputation Neutre (0/3000) avec Ravenholdt, ce qui signifie que si vous tuez un NPC de Ravenholdt avant d\'augmenter votre réputation d\'au moins 5, vous deviendrez hostile et ne pourrez jamais augmenter votre réputation. \nPour augmenter votre réputation de Neutre à Amicale, la quête répétable [quest=6701] est disponible. Vous devrez fournir 11-12 [item=17124] et une fois que vous êtes amical, cette quête n\'est plus disponible. Vous pouvez également fournir cinq [item=16885].\nPour augmenter votre réputation au-delà de Amical, le seul choix est la quête répétable, [quest=8249]. \n\n[h3]Récompense[/h3]\n\nIl n\'y a aucune récompense de faction connue pour obtenir que se soit avec une réputation Amicale, un honoré, révéré ou exalté, sauf que les gardes vous parlent avec plus de respect. \n\nCependant, La réputation Exalté est nécessaire pour obtenir le Haut-Fait : [achievement=2336].',NULL),(8,87,2,NULL,0,2,'Les [b] Pirates de la Voile Sanglante [/b] semblent être l\'une de ces organisations, qui sont apparues en Azeroth pendant les événements menant à la troisième guerre et à la suite de la troisième guerre. Ils sont originaires du Rivage Cruel, où leur chef, l\'[npc=2546], organise les opérations. Ils ont maintenant l\'intention de paralyser et de piller la ville portuaire de [faction=21], contrôlée par le Cartel Gentepression et sous la protection des Ecumeurs des Flots noirs. Il est probable que les Pirates de le Voile Sanglante sont venus profiter de la perte actuelle de leur flotte, sur la côte de la [zone=45], dans laquelle deux de ses navires ont été détruits. Le navire restant a été obligé de trouver un abri dans une crique où son équipe lutte maintenant pour survivre aux escarmouches des Nagas.\n\nEn préparation de l\'attaque, les Pirates de la Voile Sanglante ont pris position dans des endroits clés près de la ville. À l\'heure actuelle, ils ont trois navires ancrés le long du littoral au sud de Baie-du-Butin, à l\'abri des canons défensifs de la ville. Des camps ont également été construits le long de la même côte en prévision de l\'attaque. En outre, une fête scoute a atterri juste à l\'ouest de l\'entrée de la ville, signalant toutes les activités, ainsi qu\'un camp construit le long de la route menant vers la ville, susceptible d\'empêcher tout renfort.\n\nLes Pirates de la Voile Sanglante cherchent à atteindre leurs objectifs sans avoir leurs forces engagées dans la bataille, à cette fin, chaque côté cherche maintenant l\'aide d\'aventuriers sympathiques à leur cause.\n\n[h3]Réputation [/h3]\n\nIl n\'y a qu\'une seule façon d\'augmenter votre réputation auprès des Pirates de la Voile Sanglante et c\'est de libérer votre colère contre tous les citoyens de Baie-du-Butin. Voici une liste de tous les citoyens de Baie-du-Butin et leur valeur de réputation. \n[ul]\n[li] [npc=4624] : 25 points de réputation gagné [/li]\n[li] [npc=15088] : 25 points de réputation gagné [/li]\n[li] [npc=2496] : 5 points de réputation gagné [/li]\n[li] [npc=2636] : 5 points de réputation gagné [/li]\n[li] [url=?Npcs&filter=cr=3;crs=21;crv=0] Plusieurs autres NPC [/url][/Li]\n[/Ul]\nLe montant gagné avec les Pirates de la Voile Sanglante est indiqué pour un niveau 60 non humain. Le montant perdu pour tuer un citoyen ne peut pas être démontré car il dépend de votre niveau actuel avec Baie-du-Butin et de l\'importance de la personne que vous tuez. En plus de cela, quand vous perdez de la réputation avec Baie-du-Butin, vous perdez la moitié dans les trois autres villes du Cartel Gentepression. Par exemple, si vous perdez 25 points avec Baie-du-Butin, vous perdrez 12,5 points avec [faction=470].\n\nLe moyen le plus rapide d\'augmenter votre réputation avec les Pirates de la Voile Sanglante est de tuer des habitants de Baie-du-Butin. Au début, cela peut sembler une tâche simple car les gardes n\'apparaissent pas aussi menaçants que les autres monstres auxquels un joueur est confronté dans le jeu. Cependant, les gardes sont très équipés pour neutraliser les joueurs de toute classe, afin d\'éviter que les gens ne s\'attaquent les uns les autres dans la ville. \n\nLe Cogneur de Baie-du-butin a l\'avantage avec plusieurs capacités. Lune dentre elle est lutilisation de filet pour vous bloquer sur place, vous empêchant de vous échapper. Une autre est le fait qu\'ils appellent dautres Cogneurs chaque fois que vous attaquiez un citoyen de la ville ou si vous êtes sous un statut hostile avec Baie-du-Butin, les joueurs peuvent bientôt se retrouver rapidement submergés par les Cogneurs.\nLa capacité la plus forte du Cogneur est quune fois qu\'il tire son arme, il est peu probable que vous vivez, si vous ne vous échappez pas assez vite. Chaque fois qu\'un Cogneur vous tire dessus, l\'attaque vous retient, tout comme une attaque de marteau d\'Ogre. La différence ici, est que le Cogneur peut tirer rapidement en succession, provoquant des lances de chaîne. Un joueur peut littéralement être jeté d\'un côté de la ville à l\'autre, ce qui vous empêche d\'attaquer. Plus souvent, vous vous retrouverez coincé dans un coin, incapable de bouger et incapable d\'attaquer avec tous les sortilèges interrompues par l\'attaque du Cogneur. Parce que les Cogneurs ne rangent pas leurs armes à feu une fois qu\'elles sont sorties, la meilleure façon d\'agir est de s\'enfuir.\n\nPar essais et erreurs, la plupart des gens ont découvert un endroit sûr pour tuer les Cogneurs de Baie-du-Butin. Si vous suivez le tunnel qui mène à la ville, le chemin de votre gauche qui mène à la maison du Forgeron est l\'endroit idéal pour tuer les gardes. Seuls deux gardes patrouillent sur ce chemin. Une fois qu\'ils sont partis, entrer dans la première construction sur le chemin pour provoquer un rassemblement. Un joueur devrait pouvoir tuer 2 à 4 Cogneurs avant que les deux Cogneurs de patrouille en appellent dautres. En moyenne, un joueur qui fait cela peut tuer environ 30 à 40 Cogneurs de Baie-du-Butin, gagnant environ 800 points de réputation auprès de la Voile Sanglante. Les Cogneurs ici ne semblent pas sortir leurs armes, mais si vous vous trouvez dans une mauvaise situation, vous pouvez sauter sur la balustrade, courir sur le chemin des eaux, pour vous échapper.\n\nPour augmenter votre réputation au-delà de honoré, seuls deux NPC vous le permettent : \n[ul]\n[li] [npc=9179] : 5 points de réputation toutes les 7 minutes jusquà révéré [/li]\n[li] [npc=26081]: 5 points de réputation toutes les 24 heures jusquà exalté [/li]\n[/Ul]\n\n[h3]Récompenses[/h3]\n\nDevenir amical avec Les Pirates de la Voile Sanglante, vous donnera accès aux éléments suivants :\n[ul]\n[li] [item=12185] - Invoque un [npc=11236] [/li]\n[li] [item=22742] [/li]\n[li] [item=22743] [/li]\n[li] [item=22745] [/li]\n[/Ul]\nVous aurez besoin d\'être honoré avec la Voile Sanglante pour [achievement=2336].',NULL),(8,70,2,NULL,0,2,'Le[b] Syndicat [/b] est une organisation criminelle humaine qui opère principalement dans les [zone=45] et les [zone=36], bien que quelques petits campements soient éparpillés dans les [zone=267]. Leur effectif compte environ 3 000 personnes.\n\nIls ont trois chefs : [npc=2423], descendant du premier Lord d\'Alterac, qui dirige les actions du Syndicat dans les montagnes Alterac, [npc=2597] dirige les actions du Syndicat dans les Hautes Terres d\'Arathi à partir de la principale demeure, le Donjon semi-abandonnée de Stromgarde, et Lady Beve Perenolde, fille d\'Aiden Perenolde.\n\n[h3]Histoire[/h3]\n\nPendant la seconde guerre, Lord Perenolde qui dirige le royaume d\'Alterac, a été découvert pour être en liaison avec les orcs de la Horde. Perenolde croyait qu\'une victoire de le Horde était inévitable et offrait ainsi une aide à la Horde en suscitant des rébellions, en attaquant les bases de l\'Alliance et en leur fournissant des armes. Lorsque cette trahison fut découverte, l\'Alliance marchait contre Alterac et la détruisit. Perenolde et tous les nobles qui ont accompagné ses projets ont été dépouillés de leurs titres et de leurs terres. Beaucoup d\'entre eux ont réussi à s\'échapper, mais ont commencé à comploter pour se venger. En utilisant leur fortune encore considérables, la noblesse a engagé une bande de voleurs et d\'assassins, formant une organisation connue sous le nom de Syndicat.\n\nAu début, le but du Syndicat était simplement de répandre le chaos et le désordre, frappant des bases cachées dans les montagnes d\'Alterac. Avec la fin de la troisième guerre et le chaos qui suivie, les dirigeants du Syndicat ont vu leur chance de reprendre Alterac et de retrouver leurs anciens pouvoirs. Ils ont maintenant pris le contrôle de plusieurs avant-postes dans la région environnante, y compris le donjon abandonnée et une partie de la ville de Stromgarde.\n\nIls sont haïe par l\'Alliance, qu\'ils considèrent comme leurs ennemis mortels, et la Horde, qu\'ils considèrent comme des brutes faits pour travailler en esclaves. En conséquence, le Syndicat est maintenant chassé par les deux factions, avec [npc=10181], en particulier, une prime est sur sa tête, tous les membres du Syndicat capturés seront exécutés sommairement. En outre, [npc=4949] a commandé un certain nombre de ses agents, y compris [npc=2229], [npc=2239], [npc=2238] et leur chef [npc=2316] pour lancer une enquête sur la nature du Syndicate et ses activités, ainsi que pour récupérer [item=3498], un collier maintenant porté par Elysa, la maîtresse de Lord Aliden, qui appartenait à un son cher ami, [npc=18887].\n\n[h3]Réputation[/h3]\n\nLe Syndicat, en tant que faction dans World of Warcraft, est très étrange par rapport à la plupart des factions. En effet, que le meurtre des membres de cette faction ne réduira pas votre réputation. Pour la plupart des joueurs, qui ne sont pas voleur, la seule façon d\'afficher le Syndicat dans leur menu de réputation est de compléter la quête [quest=8249]. Cependant, la quête requiert [item=16885] ... que seuls les voleurs peuvent obtenir en volant à la tir des PNJ au-dessus du niveau cinquante ce qui rend difficile d\'organiser une telle transaction.\n\nActuellement, il n\'y a qu\'une seule option connue pour augmenter la réputation d\'un joueur avec le Syndicat, en tuant des membres de la faction [faction=349]. Il n\'y a pas de récompenses connues pour avoir augmenté la réputation du Syndicat. Les PNJ affiliés à Ravenholdt ne donnent que 1 point de réputation, à l\'exception de [npc=13085], qui donne 5 (bien que la perte de réputation correspondante avec Ravenholdt soit aussi cinq fois plus grande ). Tous les joueurs commençent à une réputation détestée de 32000/36000, il faudrait tuer 10 000 PNJ de Ravenholdt pour atteindre le statut neutre avec la faction. Malheureusement, l\'état neutre est le plus élevé que vous puissiez atteindre avec le Syndicat, ce n\'est pas pour dissuader les joueurs, aucun des NPC Ravenholdt ne grimpe la réputation.\n\n[b]AVERTISSEMENT[/b]: Si vous décidez de tuer les PNJ de Ravenholdt, sachez qu\'il n\'y a actuellement aucun moyen de restaurer votre positionnement avec Ravenholdt, si vous passez en dessous de Neutre. La raison du problème est qu\'aucune des quêtes qui donnent des points de réputation de Ravenholdt ne sera disponible car aucun des membres de Ravenholdt ne vous parleront. Cela signifierait qu\'il s\'agit d\'un changement permanent et que vous ne pourrez plus jamais interagir avec l\'un des NPC fidèles à Ravenholdt. Notez également que les joueurs commencent à la réputation de 0/3000 avec Ravenholdt, et le fait de tuer même un de leurs PNJ à ce niveau de réputation vous empêchera pour toujours de rétablir votre réputation avec eux.',NULL),(8,59,2,NULL,0,2,'[b]La Confrérie du Thorium[/b] est un groupe d\'artisans d\'élite qui vend un certain nombre de recettes épiques, par contre, vous devez obtenir suffisamment de réputation avec eux. Tous les joueurs commencent à la réputation : Neutre.\n\n[h3]Histoire[/h3]\n\nLa [zone=51] abrite un groupe de nains exceptionnellement robustes qui se sont séparés du Clan Sombrefer. Sur les falaises surplombant la région appelée « Le Chaudron », dans le grand nord des Gorges des vents brulants, les nains de la Confrérie du Thorium ont établi une base d\'opérations, la Halte du Thorium. De là, ils surveillent de près les activités des nains de Sombrefer dans les Gorges des vents brûlants. Les aventuriers qui cherchent la Halte du Thorium trouveront que les nains de la Confrérie du Thorium qui donnent de grandes récompenses pour ceux qui les aident dans leur lutte sans fin contre leurs anciens frères.\n\nLa Confrérie du Thorium comprend de nombreux artisans exceptionnellement talentueux, et les forgerons de la Confrérie sont censés être parmi les meilleurs Azeroth. Ils possèdent les connaissances requises pour fabriquer les armes et les armures de [npc=11502], le Seigneur du Feu, mais n\'ont pas de main-d\'uvre pour obtenir les matériaux nécessaires à l\'artisanat. On raconte qu\'un membre de la Confrérie du Thorium a été habilité à échanger les recettes et les projets fabuleux des nains avec ceux qui peuvent prouver leur fidélité à la Confrérie. Bien sûr, pour prouver sa fidélité, l\'aventurier doit s\'aventurer au coeur de [zone=2717], le domaine de Ragnaros, le Seigneur du Feu lui-même, pour fournir aux nains les matières premières rares trouvées là-bas. Une tâche ardue, sans aucun doute, mais avoir accès aux secrets de la Confrérie du Thorium devrait s\'avérer être une récompense qui vaut bien l\'effort.\n\n[h3]Réputation[/h3]\n\n[b]De Neutre à Amical[/b]\n[ul]\n[li] Fournir : [item=18944], [item=3857] et [item=4234], [item=3575] ou [item=3356] au [npc=14624]. [/Li]\n[/ul]\n[b]De Amical à Honoré[/b]\n[ul]\n[li] Fournir : [item=18945] au [npc=14624]. [/Li] \n[/ul]\n[b]De Honoré à Exalté[/b]\n[ul]\n[li] Fournir : [item=11370] à [npc=12944]. [/Li]\n[li] Fournir : [item=17012] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17010] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17011] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=11382] à Lokhtos Sombrescompte. [/Li] \n[/ul]',NULL),(8,68,2,NULL,0,2,'[b]Fossoyeuse[/b] est la faction pour la capitale du même nom, [zone=1497], régie par Sylvanas Coursevent. La cité est situé dans la [zone=85], au bord nord des Royaumes de l\'Est. La ville proprement dite est sous les ruines de la ville historique de Lordaeron. Pour y entrer, vous traverserez les défenses extérieures en ruines de Lordaeron et la salle du trône abandonnée, jusqu\'à ce que vous atteigniez l\'un des trois ascenseurs gardés par deux abominations.\n\n[h3]Histoire[/h3]\n\nFossoyeuse était à l\'origine un système d\'égouts, de cryptes et de catacombes sous la capitale de Lordaeron. Après que la ville a été détruite par le Fléau, Arthas a reconstruit et agrandit le dédale de souterrain. Initialement, il voulait que Fossoyeuse soit son siège de pouvoir, d\'où il gouvernerait les terres de pestes. Cependant, peu de temps après la fin de la troisième guerre, Arthas a été obligé de retourner à Norfendre et de sauver le Roi Liche. En son absence, [npc=10181] et ses non-morts rebelles ont capturé les ruines de la ville. Peu de temps après, elle a découvert la grande forteresse souterraine et a décidé de l\'établir comme base principale des opérations pour les Réprouvés.\n\n[h3]Réputation[/h3]\n\n[npc=14729] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Fossoyeuse, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=squelette] chevaux squelettiques [/url].\n\nLes zones environnantes [zone=267], [zone=130], et la [zone=85] contiennent la plupart des quêtes pour gagner de la réputation auprès de Fossoyeuse.',NULL),(8,909,2,NULL,0,2,'La [b]Foire de Sombrelune[/b] est un mystérieux carnaval itinérant, qui parcourt non seulement Azeroth, mais aussi lOutreterre. Conduite par l\'inimitable [npc=14823], un gnome d\'héritage douteux et de racine inconnue. La Foire amène des jeux, des prix et des bibelots exotiques inattendus, puissants ou non, en [zone=215], à la [zone=12] ou à la [zone=3519] chaque mois.\n\nUne variété de divertissement est proposée par la Foire, mais l\'attraction la plus commune est la rédaction du billet. Plusieurs forains distribuent des [item=19182], répartis dans toute la Foire, ils offrent des bons contre des articles fabriqués par des travailleurs du cuir, des forgerons ou des ingénieurs ainsi que des objets rassemblés dans la nature tels que [item=11404] et [item=19933]. Les bons peuvent être échangés contre de nombreuses choses allant de la [item=19295] à des colliers de grande puissance.\n\nBeaucoup d\'aventuriers recherchent la Foire de Sombrelune pour trouver les mystiques [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]carte de Sombrelune[/url]. Les cartes de Sombrelune viennent en huit combinaisons, chacune ayant une suite de l\'As aux Huit. Avec la combinaison de toutes les cartes, la suite est créée qui commencera une quête pour vous envoyer à la foire de Sombrelune. \nChacune des huit suites produit un [url=?items=4.-4&filter=na=carte+sombrelune] bijou [/url] différent avec un effet différent, dont certains sont assez puissants.\n\nLe calendrier habituel de la Foire de Sombrelune arrive sur le site, le premier vendredi du mois et le départ commencera tôt le lundi suivant.',NULL),(8,76,2,NULL,0,2,'[b]Orgrimmar[/b] est la faction de la capital des orcs : [zone=1637]. Situé au bord nord de [zone=14], la ville imposante abrite le chef de guerre orcs, [npc=4949].\n\n[h3]Histoire[/h3]\n\nThrall a dirigé les orcs vers le continent de Kalimdor, où ils ont fondé une nouvelle patrie avec l\'aide de leurs frères tauren. En nommant leur nouvelle terre, Durotar, nom du père assassiné de Thrall, les orcs se sont installés pour reconstruire leur société autrefois glorieuse. La malédiction démoniaque sur leur race a pris fin, la Horde a décidé de passer dun discours de conquête avec une coalition lâche à la survie et à la prospérité pour tous. Aidé par les nobles Taurens et les Trolls rusés de la tribu Sombrelance, Thrall et ses orcs attendaient une nouvelle ère de paix dans leur propre pays.\n\nDe là, ils ont commencé la création de la grande ville guerrière, Orgrimmar. Nommé de l\'ancien chef de guerre, Orgrim [color=#ff143c]Doomhammer[/color], la nouvelle ville a été construite en peu de temps, à l\'aide des gobelins, des Taurens, des trolls et de [color=#ff122a]Mok\'Nathal Rexxar[/color]. En dépit d\'avoir des problèmes avec les centaures, les harpies, les lézards de tonnerre enragés, les kobolds, et malheureusement, l\'Alliance, Orgrimmar a prospéré et est devenu le foyer des orcs et des Trolls Sombrelance.\n\nAujourd\'hui, Orgrimmar se trouve à la base d\'une montagne entre Durotar et [zone=16]. Une ville guerrière en effet, elle abrite d\'innombrables quantités d\'Orcs, Trolls, Taurens, et une quantité croissante de Réprouvés rejoignent maintenant la ville, ainsi que les Elfes de Sang qui ont récemment été acceptés dans la Horde.\n\n[h3]Réputation[/h3]\n\n[npc=14726] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Orgrimmar, en récompense, les joueurs peuvent acheter des[url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=Loup] loups [/url].\n\nLes zones environnantes Durotar et [zone=17] contiennent la plupart des quêtes pour gagner de la réputation avec Orgrimmar.',NULL),(8,530,2,NULL,0,2,'[b]Les Trolls Sombrelances[/b], tribu de Trolls exilés, ont uni leurs forces avec [npc=4949] et la Horde. Ils appellent maintenant [zone=1637] leur maison, qu\'ils partagent avec leurs alliés Orc. [npc=10540] est leur chef actuel.\n\n[h3]Histoire [/h3]\n\nLorsque les rivalités tribales ont éclaté dans l\'ancien Empire Gurubashi, la tribu Sombrelance s\'est trouvée chassée de sa patrie dans [zone=33]. S\'étant installés dans ce que l\'on croit aujourd\'hui être les îles brisées, la tribu se retrouve bientôt enchevêtrée dans un conflit avec une bande de murlocs. Leur sort semblait scellé jusqu\'à ce que Thrall, chef de guerre Orc, et son armée, nouvellement libérés, s\'emparent de leurs maisons. Contrôlée par une sorcière des mers, un groupe de murlocs a capturé le chef des Sombrelances, Sen\'jin, avec Thrall et plusieurs autres Orcs et Trolls. Thrall a réussi à se libérer avec d\'autres, mais n\'a finalement pas pu sauver le chef des Trolls. Bien que Sen\'jin ait été sacrifié par la sorcière des mers, il a pu révéler une vision qu\'il avait eu, dans laquelle Thrall conduirait les Sombrelances hors des îles.\n\nAprès son retour, Thrall et ses partisans ont réussi à repousser de nouvelles attaques de la sorcière des mers et de ses murlocs, et se sont à nouveau dirigés vers Kalimdor. Sous la direction de [npc=10540], les Sombrelances ont alors juré allégeance à la Horde de Thrall et les ont suivi. Maintenant considérés comme ennemis par toutes les autres tribus Trolls sauf les Vengebroches et les Zandalar, les Sombrelances sont aujourd\'hui méprisés. Pourtant, les Trolls Sombrelances n\'ont pas oublié quils ont été chassés de leurs terres ancestrales et cette animosité gardée est accentuée avec limpatience, surtout vers les autres tribus Trolls. Après avoir atteint la nouvelle patrie des Orcs, [zone=14], les trolls se sont alors installés sur les rives orientales du royaume Orc, les îles Echo.\n\nCependant, avec l\'arrivée de Kul Tiras et de sa marine, les Sombrelances ont été forcés de reculer à l\'intérieur des terres sous l\'assaut du commandant. Les Trolls, se battant avec la Horde aux côtés de leurs frères, ont vaincu l\'ennemi. Les Trolls ont alors réclamé leur nouvelle patrie. Peu de temps après, un sorcier du nom de [npc=3205] a commencé à utiliser la magie noire pour prendre possession de ses collègues Sombrelances. Au fur et à mesure que son armée de disciples augmentait, Vol\'jin ordonna que les trolls restant évacuent, alors Zalazane prit le contrôle des îles Echo. Les Sombrelances se sont installés sur la rive voisine, en nommant leur nouveau village en hommage à leur ancien chef Sen\'jin. Du village de Sen\'jin, ils envoient, avec leurs alliés, des forces pour combattre Zalazane et son armée asservie.\n\n[h3]Réputation[/h3]\n\n[npc=14727] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Trolls Sombrelances, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0] Raptors [/url].\nLa zone environnante, Durotar, contient la plupart des quêtes pour gagner de la réputation avec les Trolls Sombrelances. De plus, les joueurs de niveau supérieur ont également une bonne quantité de quêtes dans [zone=3521].',NULL),(8,92,2,NULL,0,2,'[b]Les Gelkis[/b] sont une tribu de centaures qui ont construit leur campement dans les parties les plus au sud de [zone=405]. Ce sont les ennemis mortels des [faction=93], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Gelkis était [npc=13741], deuxième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5602] et ont pour représentant [npc=5397].\nLes Gelkis ne tiennent aucune alliance avec leurs tribus de frères, mais sont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nInitialement dirigé par le Second Khan Gelk, les Gelkis se situaient dans les régions les plus au sud de Desolace lorsque la tribu centaure se divisa en cinq.\nLorsque la tribu Gelkis s\'est prononcée contre le Khan Magra, une éternelle querelle entre les Magram et les Gelkis est née.\n\nLes Gelkis considérés comme plus civilisés que leurs frères avec une structure sociale organisée et une compréhension ferme de la langue commune, respectent la nature et leur mère de naissance Theradras. \nAlors que les Magram prônent la force comme essentielle et que la survie de la tribu dépend de leur esprit de combat.\n\nPour alléger ce conflit, Theradras veille toujours sur les centaures et gardera les tribus en sécurité et en vie. Les Gelkis ont alors demandé sa protection et donc le pouvoir de la terre maintien leur existence. \n\nBien que la Magram considère que cela soit faible, il semblerait que ce soit une vue erronée, car des élémentaires peuvent être aperçu dans Village Gelkis, mettant un terme aux intrus indésirables aux côtés de leurs maîtres centaures.\n\n[h3]Réputation[/h3]\n\nCest une des deux factions situées en Desolace, vous devez avoir une certaine réputation auprès des Gelkis pour commencer leurs quêtes. La réputation pour les Gelkis peut être obtenue en tuant les [url=?Npcs=7&filter=na=Magram]centaures Magram[/url].\n\nVous gagnez 20 points de réputation chez les Gelkis et perds 100 avec la tribu Magram.',NULL),(8,93,2,NULL,0,2,'[b]Les Magram[/b] sont une tribu de centaures qui construit leur campement dans les parties sud-est de [zone=405]. Ce sont les ennemis mortels de la [faction=92], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Magram était [npc=13740], troisième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5601] et ont pour représentant [npc=5398].\nLes Magram ne tiennent aucune alliance avec leurs tribus de frères, mais osont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nÀ l\'origine menée par le troisième Khan Magra, les Magram se situaient contre les chaînes de montagnes de Desolace lorsque la tribu centaure se divisa en cinq.\nAvant la mort de Magra, il a installé l\'idée que la force était essentielle et que la survie de la tribu dépendait de son esprit de combat. Quand leur frère, la tribu Gelkis, s\'est prononcée contre cette notion, une éternelle querelle entre les deux tribus est née.\n\nLa poursuite de la force a continué à travers les Khans Magram jusqu\'à ce jour, transformant les centaures en des êtres violents et déterminés. Pour solidifier leur titre de plus fort, la tribu lutte encore férocement pour affaiblir ou détruire leurs clans de frères, considérant les Kolkar comme faible, les Gelkis comme une nuisance, et les Maraudon comme un formidable ennemi.\n\nOn peut supposer que la culture Magram s\'est développée autour de la force de culte avant tout. Par rapport aux Gelkis, les Magram tiennent des formes très primitives de la parole et de la structure sociale. Par exemple, leur compréhension commune est limitée et la position de Khan serait vraisemblablement recherchée par un démon de la mort.\n\n[h3]Réputation[/h3]\n\nC\'est une des deux factions situées à Desolace, vous devez avoir une certaine réputation auprès des Magram pour commencer leurs quêtes. La réputation pour les Magram peut être obtenue en tuant [url=?npcs=7&filter=na=Gelkis]les centaures Gelkis[/url]. \n\nVous gagnez 20 points de réputation chez les Magram et perds 100 avec la tribu Gelkis.',NULL),(8,270,2,NULL,0,2,'Les trolls de la[b] Tribu Zandalar[/b] sont venus à île de Yojamba dans la [zone=33] pour recruter de l\'aide contre le Dieu du sang ressuscité et ses prêtres d\'Atal\'ai dans [zone=19] et [zone=1417].\n\n[h3]Histoire[/h3]\n\nLes Zandalar étaient les premiers trolls connus, tribu d\'où provenaient toutes les tribus. Au fil du temps, deux empires troll distincts ont émergé, l\'Amani et le Gurubashi. Ils existaient pendant des milliers d\'années jusqu\'à l\'avènement des Elfes de la nuit, qui ont combattu avec eux et ont finalement conduit les deux empires à l\'exil.\n\nÀ la suite du Great Sundering, les Gurubashi vaincus sont de plus en plus désespérés. En cherchant un moyen de survivre, ils ont enrôlé l\'aide du sauvage [npc=14834], également appelé Soulflayer. Hakkar s\'est transformé en un oppresseur impitoyable qui a exigé des sacrifices quotidiens de ses sujets, les Gurubashi se sont alors retournés contre leur sombre maître. Les tribus les plus fortes (y compris les Zandalar) se sont regroupées pour vaincre Hakkar et ses fidèles prêtres, les Atal\'ai. Les tribus unies ont vaincu le Dieu des Sang et ont expulsé les Atal\'ai, et malgré leur victoire, l\'Empire Gurubashi tomba peu de temps après.\n\nAu cours des dernières années, les prêtres d\'Atal\'ai ont découvert que la forme physique de Hakkar ne peut être convoquée que dans la capitale ancienne et déserte de l\'Empire Gurubashi, Zul\'Gurub. Malheureusement, au cur de cette nouvelle quête, les prêtres ont invoqué, avec succès, Hakkar, confirmant la présence du Soulflayer redouté au cur des ruines.\n\nAinsi, la tribu Zandalar est arrivée sur les rives d\'Azeroth pour combattre encore Hakkar. Mais le dieu du sang est devenu de plus en plus puissant, pliant plusieurs tribus à sa volonté, et même, commandant les avatars des dieux primitifs: chauve-souris, panthère, tigre, araignée et serpent. Avec les tribus trolls éparpillées, les Zandalri ont été forcés de recruter des aventuriers de diverse origine d\'Azeroth pour les rejoindre dans la bataille, et espèrent une fois de plus vaincre, le Soulflayer.\n\n[h3]Réputation[/h3]\n\nLa réputation avec la tribu Zandalar est obtenue en tuant les monstres et boss dans Zul\'Gurub. Des quêtes répétitives et spécifiques sont aussi disponibles, elles requièrent des éléments qui ont été abandonnés dans linstance. Chaque Zul\'Gurub donne environ 2 500 à 3 000 de réputation.\nAvant la croisade brûlante, la principale raison de monter la réputation avec la tribu était les enchantements [url=?Items=0.6&filter=na=Zandalar]dépaule[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]de tête et de jambe[/url]. De plus, il y avait des pièces darmure en récompense de quête à faire dans Zul\'Gurub nécessitant un niveau de réputation.',NULL),(8,471,2,NULL,0,2,'[b]Les Marteaux-hardis[/b] sont un clan de nains actuellement centrés dans [zone=47] et la [zone=3520]. La faction a été supprimée dans le patch 2.0.1.\n\n[h3]Histoire[/h3]\n\nJuste avant le [objet=175739], le clan Marteaux-hardis, dirigé par Thane Khardros Marteaux-hardis, habitait les contreforts et les falaises autour de Forgefer. Le clan Marteaux-hardis a échoué à prendre le contrôle de [zone=1537], des clans Barbe-de-bronze et Sombrefer. Khardros et ses guerriers Marteaux-hardis se sont rendus au nord par les barrières de Dun Algaz et ont fondé leur propre royaume dans le lointain sommet de Grim Batol. Là, les Marteaux-hardis ont prospéré et reconstruit leurs richesses.\n\n[npc=9019] et ses Sombrefer ont juré de se venger de Forgefer. Thaurissan et sa femme sorcière, Modgud, ont lancé un attentat contre Forgefer et Grim Batol. les forces de Modgud ont commencé à franchir les portes de Grim Batol, elle a utilisé ses pouvoirs pour frapper la peur dans leurs curs. Les ombres se déplaçaient à son commandement, et des choses sombres se glissaient dans les profondeurs de la terre pour traquer les Marteaux-hardis dans leurs propres retranchements. Finalement, Modgud a franchi les portes et a assiégé la forteresse elle-même. Les Marteaux-hardis se sont battus désespérément, Khardros lui-même sest lancé dans la bataille pour tuer la sorcière reine. Avec leur reine perdue, les Sombrefer ont fui avant la fureur des Marteaux-hardis.\n\nUne fois que la menace immédiate des Sombrefer a été éliminée, les Marteaux-hardis sont rentrés à Grim Batol. Cependant, la mort du Modgud avait laissé une tache maléfique sur la forteresse de la montagne, et les Marteaux-hardis la trouvaient inhabitable. Khardros a conduit son peuple vers le nord vers les terres de Lordaeron. En s\'installant dans la région montagneuse des Hinterlands, et ces forêts luxuriantes, les Marteaux-hardis ont construit la ville de Nid-de-laigle, où les Marteaux-hardis se sont rapprochés de la nature et même liés aux puissants griffons de la région.\n\nLa menace la plus immédiate pour leurs sécurités vient de l\'est sous la forme de deux clans trolls, les Vilebranches et les Fanécorces. Ils sont les plus célèbres pour organiser des batailles contre la ville des Marteaux-hardis, tout en brandissant des armes puissantes.\nLes nains Marteaux-hardis ont un certain nombre de clans, chacun gouverné par un Thane. Le plus fort Thane règne sur Nid-de-laigle.',NULL),(8,509,2,NULL,0,2,'[b]La Ligue d\'Arathor[/b] a été initialement établie par les survivants du Royaume de Stromgarde pour récupérer la [zone=45] des mains des Profanateurs au Trépas d\'Orgrim. Aujourd\'hui, c\'est une organisation à l\'appui de l\'Alliance, basée sur [zone=3358] dans le Refuge de lOrnière. Ils se sont chargés d\'aider à fournir des forces, pour l\'Alliance, lorsque cest nécessaire, leurs membres incluent toutes les races de l\'Alliance mais se sont encore principalement des humains stromgardiens.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec Ligue dArathor et les deux autres factions du champ de bataille, [faction=890] et [faction=730].',NULL),(8,730,2,NULL,0,2,'[b]Les Gardes Foudrepiques[/b] est la faction de l\'Alliance dans le champ de bataille [zone=2597]. Ils sont une expédition de nains du clan Foudrepique, originaire des « vallées d\'Alterac » dans [zone=36]. La recherche des Foudrepiques pour les reliques de leurs passés et la récolte de ressources dans la vallée d\'Alterac ont conduit à une guerre ouverte avec les Orcs de la [faction=729] habitant dans la partie sud de la vallée. Ils ont également reçu un « ordre de la souveraineté impérialiste » par [npc=2784] pour prendre les vallées d\'Alterac pour [zone=1537].\n\nLa principale base des Foudrepiques est Dun Baldar, où son chef, [npc=11948], réside avec ses maréchaux. Son second commandant, [npc=11949], se trouve au sud de Dun Baldar, à Cur de pierre.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée dAlterac, en faisant diverses tâches et en tuant les membres de la faction adverse, le clan Frostwolf.\n\nOn vous accorde le titre : [title=48] au joueur, une fois quil est exalté avec les Gardes Foudrepiques et les deux autres factions des champs de bataille, [faction=890] et [faction=509].',NULL),(8,510,2,NULL,0,2,'[b]Les Profanateurs[/b] cherchent à feuilleter la [faction=509] dans le champ de bataille, [zone=3358]. Aujourd\'hui, c\'est une organisation à l\'appui de la Horde, basée au Trépas dOrgrim dans [zone=45]. Ils se sont investis pour aider les forces de la Horde, au besoin, et leurs membres incluent toutes les races de la Horde, même si, se sont encore principalement des Orcs.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec les Profanateurs et les deux autres factions du champ de bataille, [faction=889] et [faction=729].',NULL),(8,529,2,NULL,0,2,'L[b]Aube dArgent[/b] est une organisation axée sur la protection d\'Azeroth des menaces qui cherchent à la détruire, comme la Légion Ardente et le Fléau. Les forteresses de l\'Aube d\'Argent se trouvent dans les [zone=139] et les [zone=28]. Elle maintient également une présence dans [zone=1657] et dans les [zone=85], et dans dautres zones moins remarquables. La réputation avec lAube dArgent peut être utilisée pour acheter divers plans, consommables, et pour atténuer le coût à [zone=3456]. Avec l\'expansion « Burnning Croisade », la réputation de lAube dArgent a diminué en valeur.\n\nLe [item=22999] a pour icône un lever de soleil argenté.\n\n[h3]Histoire[/h3]\n\nAprès la mort du [npc=16062], la corruption de la Croisade Écarlate est devenu évidente pour certains de ses membres, qui ont par la suite abandonné les rangs de la [url=?npcs&filter=na=croisade%20écarlate;ex=on]Croisade Écarlate[/url] et a créé lAube dArgent pour protéger Azeroth de la menace du Fléau sans présence de fanatique dans la Croisade Écarlate.\n\nAlors qu\'ils partagent les mêmes objectifs que la Croisade, lAube dArgent a ouvert ses rangs non seulement aux races de l\'Alliance, mais aussi aux membres de la Horde et même à certains des Réprouvés. Ils mettent en garde contre la discrétion et l\'introspection, et mettent beaucoup l\'accent sur la recherche du Fléau et sur la façon de le combattre.\n\nAvec le temps, lAube dArgent s\'est diversifié, comme le Fléau qui s\'est divisé de nouveau, avec un rejeton appelé la Fraternité de la Lumière, un compromis entre l\'approche plus savante de lAube dArgent et le fanatisme de la Croisade écarlate.\n\n[h3] Réputation [/h3]\n\n[b]Les pierres du Fléau[/b]\nTout en portant un bijou accordant l\'effet « Commission pour lAube dArgent », les personnages peuvent tuer des monstres mort-vivants pour leurs [url=?items=12&filter=cr=151;crs=6;crv=43169;na=pierre%20du%20fléau] pierres du Fléau[/url] et ensuite les transformer en monnaies échange contre [item=12844]. Les quêtes requièrent beaucoup de [item=12843], [item=12841] et [item=12840]. Il convient de noter que les monnaies déchanges reçus des entités doivent être sauvegardés jusqu\'à ce que le statut de Révéré soit atteint, car les quêtes ne donneront plus de réputation après.\n\nUne autre façon daugmenter la réputation avec lAube dArgent est de faire la quête répétable « Chaudron ». Les chaudrons sont une source de « production » de membres du Fléau.\n\nComme la plupart des factions, le joueur peut faire des instances pour augmenter sa réputation. Les instances associées sont [zone=2017] et [zone=2057]. Naturellement, ces instances incluent également des quêtes qui augmentent la réputation de lAube dArgent.',NULL),(8,933,2,NULL,0,2,'[b]Le Consortium[/b],dirigé par [npc=19674], sont des passeurs éthérés, des commerçants et des voleurs qui sont venus en Outreterre. Le principal base d\'opérations et le plus grand rassemblement se trouve à Foudreflèche, mais ils peuvent être trouvés à[color=#ff0537] Midrealm Post[/color], Aeris Landing, près d\'Auchindoun à [zone=3792] et dans d\'autres endroits.\n\nEn arrivant à un statut amical, les joueurs sont officiellement considérés comme membres du Consortium et bénéficient d\'un salaire. Le salaire est un sac de gemmes au début de chaque mois, donné par [npc=18265] chez Aeris Landing. Une plus grande réputation avec le Consortium produit des qualités et quantités supérieures de gemmes chaque mois.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à Amical[/b]\n[ul]\n[li]Faire le donjon Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 points de réputation[/li]\n[li]Donner des [item=25416] à [npc=18265].[/li]\n[li]Donner des [item=25463] à [npc=18333].[/li]\n[/ul]\n\n[b]De amical à honoré[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 point de réputation.[/li]\n[li]Activer les [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode héroïque[/i] rapporte environs 2 400 points de réputation.[/li]\n[li]Faire toutes les [url=?Quêtes et filtre=cr=1;crs=933;crv=0]quêtes[/url].[/li]\n[li]Donner des [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\nToutes personnes qui essayent de gagner simultanément la réputation du Consortium et des [faction=941] ou [faction=978] peuvent se concentrer à tuer des ogres ([url=?npcs&filter=na=rochepoing;cr=6;crs=3518;crv=0]Rochepoing[/url], [url=?npcs&filter=na=cogneguerre;cr=6;crs=3518;crv=0]Cogneguerre[/url]) à Nagrand et rendre les perles de guerre obsidienne au Consortium.\n\nLa seule mise en garde est le taux de loot, soit environ 33% pour les Cogneguerre, alors qu\'il est de 50% pour les insignes. Si vous êtes au niveau 70 et que vous voulez monter cette réputation plus rapidement sans se soucier de la réputation de Mag\'har / Kurenai, vous voudrez peut-être donner des insignes à la place. Ensuite, les ogres sont généralement plus faciles à tuer, allant du niveau 65 à 67. Le choix dépend finalement du joueur.',NULL),(8,932,2,NULL,0,2,'[b]L\'Aldor[/b] est un ancien ordre de prêtres draeneïs qui vénèrent les naaru, et à ce jour ils assistent les naaru [faction=935] dans leur combat contre [npc=22917] et la Légion Ardente. Ils se trouvent principalement dans la [zone=3520] et [zone=3703]. Bien qu\'ils aient beaucoup souffert des Elfes du sang qui sont devenus [faction=934], ont mis de côté une guerre ouverte contre les Sha\'tar. Le temple le plus saint de l\'Aldor repose sur léminence de l\'Aldor, surplombant la ville à l\'ouest.\n\nLa plupart des joueurs commenceront à une réputation neutre auprès de l\'Aldor. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec Aldor ou Les clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin.\nLes joueurs de Draenei seront directement amicaux avec Aldor et hostiles avec les Clairvoyants, alors que les joueurs Elfe du sang seront hostiles à l\'Aldor et amicaux envers les Clairvoyants.\n\n[npc=19321] et [npc=20807] sont situés dans la banque Aldor, sur le bord nord de la terrasse de la lumière. Le sanctuaire de la lumière sans fin sur léminence de l\'Aldor abrite [npc=20616] [petit][/small] et [npc=21906] [petit][/small], qui échangent, respectivement, des jetons épiques d\'armure contre des pièces de set de [url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec Aldor correspondent à une perte de réputation de 10% plus élevée chez les Clairvoyants. La plupart des gains de réputation avec Aldor accorderont également 50% de la réputation avec le Sha\'tar.[/i]\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29425][/span] à [npc=18537] dans léminence de l\'Aldor accordera 250 points de réputation pour l\'Aldor. Il existe également une quête répétable où donner une unique marque accorde 25 points de réputation. Ces marques tombent sur des membres inférieurs de la Légion Ardente trouvés dans la plupart des zones de Outreterre, y compris les deux camps au nord d\'Auchindoun dans les déchets osseux de [zone=3519].\nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique marque.\n\nLes joueurs qui souhaitent également faire la réputation des factions [faction=978] ou [faction=941] iront tuer des Orcs à la forteresse de Kil\'Sorrow dans le sud-est de [zone=3518], car ils donnent des marques ainsi que 10 points de réputation auprès des Kurenai ou des Mag\'har.\n\n[b]Jusqu\'à Exalté[/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1] [item=30809][/span], c\'est le même principe que les marques de Kil\'jaeden mais ceux-ci tombent sur des partisans de haut rang de la Légion Ardente. Si vous le souhaitez, vous pouvez transformer les marques de niveau supérieur avant la réputation honorée. Dans [zone=3522], la porte de la mort dispose du plus grand nombre de membre avec ce grade.\n\n[b]Arme gangrenée[/b]\n\n[span class=q2][item=29740][/span] peut être donné à tout moment à [npc=18538] [small][/small] à léminence de l\'Aldor. Cela augmentera votre réputation avec l\'Aldor de 350 par arme gangrenée.\nEn plus des gains de réputation, vous recevrez [span class=q1][item=29735][/span], qui est la condition pour acheter lenchantement d\'épaule à [npc=20807] dans la banque de l\'Aldor.\n\n[h3]Passer à la réputation de l\'Aldor[/h3]\n\nPour changer votre faction des Claivoyants vers l\'Aldor et donc pour accéder à leurs recettes d\'artisanat (et annuler toutes les réputations que vous avez faites), trouvez [npc=18597], un membre de l\'Aldor dans la ville basse. Elle propose une quête répétable où pour 8x [span class=q1][item=25802][/span] vous montez la réputation Aldor. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.',NULL),(8,922,2,NULL,0,2,'[b]Tranquilliens[/b] a été reprise par les Réprouvés et les Elfes de sang puis est devenu une faction des [zone=3433].\n\n[h3]Histoire[/h3]\n\nAlors que l\'armée du Fléau faisait son chemin vers le Puit-du-Soleil, les elfes n\'avaient pas d\'autre choix que de se retirer, Tranquillien fût donc abandonnée. La ville est maintenant utilisée par les Elfes de sang et les Réprouvés comme base d\'opération pour lancer des attaques visant à reprendre les Terres Fantômes. Cependant, la ville est entourée par le fléau, même les courriers ont du mal à traverser l\'ennemi pour atteindre la ville. Les forces mortels de Mortholme sont la menace la plus dangereuse pour la ville.\n\n[h3]Réputation[/h3]\n\nContrairement à la plupart des zones de départ, la ville de Tranquillien a sa propre faction.\nToutes les quêtes que vous effectuez pour eux accumuleront au moins 1000 points de réputation. [npc=16528] agit comme lintendant des Tranquilliens. Vredigar peut être trouvé près de l\'auberge et vendra divers éléments [span class=q2]commun[/span], et même un manteau [span class=q3]rare[/span] lorsque vous atteignez la réputation exaltée.\n\nSi vous complétez toutes les quêtes des Tranquilliens, vous devriez être exalté.\nIl existe une variété de quêtes concernant principalement la récupération des villages envahis, l\'enquête sur les morts-vivants et l\'aide apportée à la population. La suite de quête prend « fin » avec la quête où il faut tuer [npc=16329].',NULL),(8,910,2,NULL,0,2,'La [b]Progéniture de Nozdormu[/b] est une faction composée du vol Draconique de bronze. Leur chef, [npc=15192], se trouve à l\'extérieur des [b]Grottes du temps[/b], avec beaucoup de ses agents volant dans le ciel de [zone=1377].\n\nPour ouvrir les portes d[b]Ahn\'Qiraj[/b], un champion doit compléter une longue ligne de quête pour le dragon de bronze Anachronos. Cette réputation est également présente dans [zone=3428]; Elle permet dobtenir des équipements et des bagues épiques.\n\n[h3]Réputation [/h3]\n\nLes joueurs commencent leur réputation au plus bas niveau possible, cestà-dire 0/36000 de détestés.\n\nLa réputation de la Progéniture de Nozdormu peut être gagnée en tuant des monstres à l\'intérieur du temple d\'Ahn\'Qiraj et en faisant des quêtes liées. Vous pouvez également exploiter [item=20384], cela prend beaucoup plus de temps et nécessite l\'obtention de [item=20383] dans [zone=2677] pour la suite de quête [item=21175].\n\nTuer des monstres dans le temple d\'Ahn\'Qiraj ne permet que datteindre une réputation de 2999/3000 de neutre, la réputation ne peut donc être avancée que par des quêtes et la remise de [item=21229] et [item=21230]. \nUn conseil, gardez tous les insignes jusqu\'à ce que vous soyez à une réputation neutre, car à ce moment-là, cela devient beaucoup plus difficile.',NULL),(8,749,2,NULL,0,2,'Les [b]Hydraxiens[/b] sont des élémentaires qui se sont installés sur les îles à l\'est de [zone=16]. Les ennemis jurés des armées de [npc=11502]. Historiquement serviteurs des Anciens Dieux, les quatre Lords Élémentaires ont servi les dieux avec une loyauté éternelle. Les minions de Neptulon, le chasse-marée, étaient nombreux et insensés. On ne sait pas encore comment le [npc=13278] a libéré le contrôle de son seigneur ou quels sont ses objectifs ultimes, mais les élémentaires deau sont les seuls éléments qui n\'attaquent pas les races mortelles.\n\nSitué sur une île éloignée dans l\'extrême est d\'Azshara, le Duke Hydraxis propose des quêtes. Les deux premiers nécessitent de tuer divers élémentaires dans les [zone=139] et en [zone=1377]. Une réputation accrue avec les Hydraxiens ouvre des quêtes supplémentaires menant à [zone=2717]. Tous les objets obtenus auprès des Hydraxiens sont gagnés à partir de différentes missions.\n\nL\'achèvement de la suite de quête permet aux joueurs d\'obtenir [item=17333] utilisé pour endommager les runes trouvées près de la plupart des boss dans Cur de Magma. Ceci est nécessaire pour convoquer [npc=12018], l\'avant-dernier boss, et, après sa défaite, pour convoquer Ragnaros lui-même. Comme il y a sept runes, tout raid nécessite au moins sept joueurs qui apportent une quintessence s\'ils souhaitent terminer l\'instance. Comme la majeure partie de la suite de quête a lieu au sein de Cur de Magma, toutes personnes du raid peuvent compléter cette tâche avec un peu plus que quelques voyages et une course au [zone=1583].\n\n[h3] Réputation [/h3]\n\nLa réputation des Hydraxiens est obtenue en tuant les ennemis élémentaires suivants :\n[ul][li] [npc=11746] - 5 points de réputation, jusqu\'à l\'Honoré. [/li]\n[li] [npc=11744] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=7032] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=9017] - 15 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=14478] - 25 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=9816] - 50 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11658], [npc=11673], [npc=12101] et [npc=11668] - 20 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11659], [npc=12100], [npc=12076], [npc=11667] et [npc=11666] - 40 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264] et [npc=12098] - 100 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11988] - 150 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11502] - 200 points de réputation, jusqu\'à Exalté. [/li][/ul]\n\nLa réputation au statut de Révéré avec les Hydraxiens permet aux joueurs dobtenir le [item=22754], qui se recharge. Et donc évite la nécessité de retourner à Hydraxis pour obtenir une nouvelle quintessence chaque semaine.',NULL),(8,609,2,NULL,0,2,'Le [b]Cercle Cénarien [/b] est une organisation de druides, à la fois tauren et elfe de nuit, nommé d\'après Cénarius. Ses membres se consacrent à la protection de la nature et à la restauration de celle-ci suite aux dégâts subis par des forces malveillantes.\n\nLe Cercle a de nombreux sites, mais leur ville principale est la ville de Havre- nuit dans la [zone=493]. Les druides apprennent le sort [sortilège=18960] au niveau 10, mais il est aussi possible dy arriver par [zone=361] via le tunnel des Grumegueles.\n\nLe cercle Cénarien est aussi beaucoup présent en [zone=1377], où ils combattent les Silithides, les Qirajis et larmée du crépuscule. Le repos du vaillant et le Fort Cénarien servent de base dans ces terres hostiles et offrent de nombreuses opportunités aux aventuriers qui cherchent à aider les druides.\n\n[h3]Membres notables[/h3]\n\n[ul][li][npc=11832], fils de Cenarius [/li]\n[li][npc=3516], chef des druides - elfes de la nuit [/li]\n[li][npc=5769], chef des druides - Taurens [/li][/ul]\n\n[h3]Réputation[/h3]\n\nIl existe plusieurs façons de se faire connaître avec le cercle Cénarien.\nMise à part les [url=?Quests&filter=cr=1;crs=609;crv=0]quêtes[/url], vous pouvez faire ce qui suit pour gagner en réputation: \n[ul]\n[li]Le raid des [zone=3429] est de loin le moyen le plus rapide de gagner en réputation, car un clean complet peut dépasser 2000 points de réputation. [/li]\n[li] Tuez larmée du crépuscule. Elle cesse daugmenter une fois que vous atteignez la réputation Honoré pour [npc=11880] et [npc=11881], et Révéré pour [npc=15201].[/li]\n[li] Trouvez des [item=20404 ]. Ceux-ci se trouvent sur larmée du crépuscule et produisent 250 points de réputation pour 10 textes.[/li]\n[li] Trouvez des [item=20513], [item=20514] et [item=20515]. Ceux-ci se trouvent sur les mini-boss qui sont convoqués aux pierres de vent en utilisant [itemset=492]. [/li]\n[li] Effectuez la quête : [quest=8507]. Ce sont soit des [url=?search=logistique+Briefing] Quêtes de logistique [/url], des [url=?search=combat+Briefing]quêtes de Combat[/url] ou des [url=?search=tactique+Briefing] Quêtes tactiques [/url]. Les badges que vous gagnez de ces quêtes peuvent être transformés en réputation supplémentaire, si vous choisissez d\'abandonner les récompenses. [/li]\n[li] Collectez les [object=181598] de la zone et rendez les à votre faction.[/li]\n[/ul]',NULL),(8,589,2,NULL,0,2,'Les [b]Éleveurs de sabres-d\'hiver[/b] est une faction de l\'Alliance composée de deux Elfes de la nuit qui peuvent être trouvés au [zone=618]. À l\'heure actuelle, le seul donneur de quête est [npc=10618], qui est situé au sommet du Rocher des Sabres-d\'hiver au Berceau-de-lhiver. En atteignant un niveau de réputation exalté avec cette faction, Rivern vendra une monture spéciale, le [item=13086].\n\nLa monture de cette faction est la seule monture épique, ayant une vitesse de 100%, utilisable avec une compétence en équitation de 75. La faction est connue pour ne pas avoir déquivalant côté Horde et être la plus longue et la plus répétitive des réputations à monter dans l\'ensemble du jeu. La première quête peut être faite au niveau 58, tandis que les deux autres sont réalisables quau niveau 60.\n\n[h3]Réputation[/h3]\n\nLa réputation avec les Éleveurs de sabres-d\'hiver ne peut être obtenue que par trois quêtes répétables. Il n\'y a pas d\'objets de faction ni de mobs qui récompensent la réputation directement.\n\n[b]De neutre 0 à 1500[/b]\n\nUne seule quête répétable sera disponible jusqu\'à ce quune réputation de 1500/3000 soit atteinte, la quête : [quest=4970] doit donc être répétée. Tous les [url=?npcs&filter=cr=6;crs=618;crv=0;na=Croc%20acéré]Ours[/url] et [url=?npcs&filter=cr=6;crs=618;crv=0;na=Noroît]Noroît[/url] au Berceau-de-lhivers peuvent looter les objets de quête. Cette quête doit être effectuée en solo, car les taux de loot sont faibles et ne sont pas partageables si d\'autres ont la quête.\n\n[b]De neutre 1500 à exalté [/b]\n\nÀ mi-chemin du neutre, la quête : [quest=5201] sera disponible. Cette quête nécessite de tuer 10 Tombe-hivers dans le village Tombe-hivers, juste à l\'est de Long-guet. Si la quête : [quest=8464] a été effectuée pour [faction=576], les [item=21383] peuvent tomber sur les Tombe-hivers. Si un joueur veut les deux réputations, il préférable quil les gardes jusquà ce quil soit Révéré avec les Grumegueules. Ce qui entraînera beaucoup de réputation \"gratuite\".\n\nCette quête peut se faire en groupes pour aller plus vite. Les joueurs qui augmentent les réputations des Éleveurs de sabres-d\'hiver et des Grumegueules peuvent être trouvés dans le village des Tombe-hivers. Même en épique, le voyage vers le village Tombe-hivers prend beaucoup de temps. Il y a des tigres sur la route qui vous étourdiront, ce qui entraînera un désarçonnement, cela devrait être évité (mais peut être difficile car ils vont vous rattraper sur une monture de 60%). \n\n[b]De honoré à exalté[/b]\n\nA partir dhonoré, la troisième quête : [quest=5981] est disponible. La quête exige que le joueur tue 8 géants. Ils sont beaucoup plus difficiles que les Tombe-hivers et le trajet est assez long. Cette quête est généralement ignorée.\n\nEn raison de certains joueurs qui augmentent la réputation des Grumegueules, dans le village de Tombe-hivers, cette quête peut effectivement se révéler une récompense de réputation plus rapide que [quest=5201].',NULL),(8,576,2,NULL,0,2,'[b]Les Grumegueules[/b], dernière tribu furbolg non-corrompue (au moins dans leur point de vue), cherchent à conserver leurs voies spirituelles et à mettre fin à la souffrance de leurs frères.\n\nLes Grumegueules habitent deux zones : [zone=16] et [zone=361]. Ils sont présumés être la seule tribu furbolg à échapper à la corruption démoniaque, mais ce n\'est peut-être pas vrai, en raison de l\'existence de [npc=3897], furbolg de tribu inconnue, et la tribu Stillpine sur [zone=3524]. Cependant, de nombreuses autres races tuent les furbolgs aveuglément maintenant, sans savoir si elles sont alliées ou non. Pour cette raison, les Grumegueles ne se montrent pratiquement pas.\n\nLes aventuriers qui recherchent les Grumegueules dans le nord de Gangrebois et s\'aventurent chez eux apprendront quil faut mieux être leurs alliés. Bien qu\'ils ne possèdent pas de bijoux fins ou de richesses mondaines, la tradition chamanique des Grumegueules est encore forte. Ils connaissent bien l\'art de fabriquer des armures à partir de peaux d\'animaux, et ils sont plus qu\'heureux de partager leurs connaissances de guérison avec des amis de leur tribu. En outre, à partir dune réputation inamical, les Grumegueules vous accorderont également un accès sans problème à [zone=493] et [zone=618] dans leurs tunnels.\n\n[h3] Réputation[/h3]\n\nLa réputation avec la faction des Grumegueules est principalement acquise grâce à des quêtes. Les membres de la tribu Mort-bois, une autre tribu de Furbolg à Gangrebois, sont les principaux ennemis des Grumegueules et peuvent être tué pour gagner de la réputation.\n\n[ul]\n[li] Tuer des furbolgs [url=?Npcs&filter=na=Tombe-hivers]Tombe-hivers[/url] ou [url=?Npcs&filter=na=Mort-bois]Mort-bois[/url], donne 10 points de réputation. Les gains s\'arrêtent à révéré. [/li]\n[li] Tuer [npc=9464] ou [npc=9462], donne 60 points de réputation.[/li]\n[li] Tuer [npc=10738], située dans une grotte à l\'est de [faction=577], donne 50 points de réputation. Son taux de réapparition est de 6 à 8 minutes. [/li]\n[li] Tuer [npc=14342], élite rare, donne 50 points de réputation. Il se situe au village des Mort-bois à Gangrebois. Donne de la réputation jusquà exalté. [/ Li]\n[li] Tuer [npc=10199], élite rare, donne 50 points de réputation. Il se situe dans le village des Tombe-hivers au Berceau-de-lHivers. Donne de la réputation jusquà exalté. [/li]\n[li] Après avoir terminé la quête : [quest=8460], avec les [item=21377] ramassés sur les Furbolgs Mort-bois, la réputation augmente de 150 points. [/li]\n[li] Après avoir terminé la quête : [quest=8464], avec les [item=21383] ramassés sur les furbolgs Tombe-hivers, la réputation augmente de 150 points.[/li]\n[/ul]',NULL),(8,890,2,NULL,0,2,'[b]Les Sentinelles d\'Aile-argent[/b] représente la faction de l\'Alliance sur le champ de bataille [zone=3277]. Les elfes de la nuit, qui ont commencé une avancée massive pour reprendre les forêts de [zone=331], concentrent leur attention sur le débarquement sur leur terre de la [faction=889] une fois pour toutes. Et ainsi, les Sentinelles d\'Aile-argent ont répondu à l\'appel et ont juré qu\'ils ne vont pas se reposer avant que tous les orcs soient vaincus et expulsés du Goulet des Chanteguerres.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois quil est exalté avec Les Sentinelles d\'Aile-argent et les deux autres factions des champs de bataille, [faction=730] et [faction=509].',NULL),(8,889,2,NULL,0,2,'[b]Les Voltigeurs Chanteguerre[/b] est un clan orc précédemment dirigé par [npc=18076], daprès lequel le clan a été nommé. Les Voltigeurs Chanteguerre représentent la faction de la Horde sur le champ de bataille [zone=3277], où ils tentent de défendre leurs opérations d\'enregistrement dans [zone=331] de la [faction=890].\n\nCest l\'un des clans les plus forts et les plus violents, le clan de Chanteguerre était également l\'un des clans les plus distingués de Draenor, ce clan a pu échapper aux forces de l\'expédition de l\'Alliance à chaque tournant. Formés comme Grunts, ils ont maîtrisé l\'utilisation d\'épées et de lames et quelques-uns ont même atteint le rang de Maître-lames.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois quil est exalté avec Les Voltigeurs Chanteguerre et les deux autres factions des champs de bataille, [faction=510] et [faction=729].',NULL),(8,729,2,NULL,0,2,'[b]Le Clan Loup-de-givre[/b], ainsi que [npc=11946], ont vécu dans [zone=36] et ont des Loups de givre comme compagnons. Des nains, connue sous le nom de [faction=730], ont commencé une expédition dans le territoire des Loup-de-givre pour creuser la vallée et miner les veines. Une transgression envers les Orcs qui habitaient en Alterac. Cela a provoqué lextermination de la première expédition et la bataille pour [zone=2597] a commencé.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée dAlterac, en effectuant diverses tâches et en tuant les membres de la faction opposée, les Gardes Foudrepiques.\n\nOn vous accorde le titre : [title=47] au joueur une fois quil est exalté avec le clan Loup-de-givre et les deux autres factions des champs de bataille, [faction=889] et [faction=510].',NULL),(8,935,2,NULL,0,2,'[b]Les Sha\'tar[/b], ou \"né de la lumière\", sont des naaru qui ont aidé [faction=932], l\'ordre des prêtres draenei précédemment dirigés par [npc=17468], en reconstruction à [zone=3703]. La ville a été détruite par les Orcs pendant leur fuite à travers Draenor avant la Première Guerre mondiale. \nLa défaite de la Légion ardente est le but ultime des Sha\'tar. Les Sha\'tar sont aidés dans cette guerre par l\'Aldor et leurs rivaux, la faction des elfes du sang connue sous le nom : [faction=934]. \nL\'Aldor et les Clairvoyants se battent pour la faveur du Sha\'tar afin qu\'ils puissent être aidés dans leur guerre pour les pouvoirs des naaru. L\'entité qui dirige le Sha\'tar est connue sous le nom de [npc=18481] ; Il peut être trouvé sur la terrasse de la lumière dans la ville de Shattrath.\n\nLes joueurs de l\'Alliance et de la Horde commencent avec une réputation neutre auprès des Sha\'tar. Les joueurs peuvent augmenter leur réputation, Sha\'tar, à travers diverses quêtes, en élevant leur réputation avec lAldor ou les clairvoyants, ou en s\'aventurant dans le [url=?search=donjon+tempête]donjon des tempêtes [/url].\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLa réputation peut être obtenue à partir de divers objets. Ce qui suit n\'accordera que de la réputation de Sha\'tar jusqu\'à ce que vous obteniez un statut honoré : \n[li]Pour une réputation envers les Clairvoyants : [item=29426], [item=30810] et [item=29739][/li]\n[li]Pour une réputation envers l\'Aldor : [item=29425], [item=30809] et [item=29740][/li]\n\n[i]Notez que ce gain de réputation ne s\'affiche pas dans le journal de combat, mais peut être vérifié en regardant votre panneau de réputation.[/i]\n\nLa réputation peut également être obtenue en faisant le temple des tempêtes : [zone=3847], [zone=3846] et [zone=3849].\n\n[b]Jusquà exalté [/b]\n\nAprès avoir épuisé les récompenses de réputation de Aldor ou des Clairvoyants, les joueurs souhaiteront peut-être compléter les quelques quêtes de Sha\'tar disponibles. En plus des quêtes, les instances qui se trouvent au temple des tempêtes : Botanica, Arcatraz et Mechanar continueront à accorder de la réputation. À ce stade, il est probablement plus utile d\'exécuter ces instances en mode héroïque.',NULL),(8,934,2,NULL,0,2,'[b]Les Clairvoyants[/b] sont des elfes de sang qui résident dans [zone=3703] dirigé par [npc=18530]. Le groupe s\'est éloigné de [npc=19622] et a offert de leur aide au Naaru de Shattrath. Ils sont en désaccord avec [faction=932], et rivalisent avec eux pour le pouvoir de Shattrath et la faveur du Naaru. \n\nLa plupart des joueurs commenceront avec une réputation neutre auprès des Clairvoyants. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec lAldor ou Les Clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin. \nLes joueurs delfes de sang seront amicaux avec les Clairvoyants et hostiles avec l\'Aldor, alors que les joueurs draenei seront hostiles aux Clairvoyants et amicaux envers lAldor.\n\n[npc=19331] et [npc=20808] sont situés dans la banque des Clairvoyants, sur le bord sud de la terrasse de lumière. La Bibliothèque du Visiteur abrite [npc=20613] [small][/small] et [npc=21905] [small][/small], qui échangent des pièces d\'armure épique contre des pièces de set de[url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec les Clairvoyants correspondent à une perte de réputation de 10% plus élevée chez lAldor. La plupart des gains de réputation avec les Clairvoyants accorderont également 50% de la réputation avec [faction=935].[/i]\n\n[h3]Tradition [/h3]\n\nAprès avoir subi des assauts implacables de leurs ennemis, les gardes harassés de Sha\'tar et de lAldor se sont regroupés pour la prochaine attaque alors qu\'elle marchait sur l\'horizon. Cette fois, l\'attaque provenait des armées de [npc=22917]. Un grand régiment d\'elfes de sang avait été envoyé par l\'allié d\'Illidan, le prince Kael\'thas pour détruit la ville. Alors que le régiment d\'elfes de sang traversait le pont, les exarques et les vindicateurs de lAldor se sont alignés pour défendre la Terrasse de Lumière. Alors l\'inattendu arriva, les elfes de sang déposèrent leurs armes devant les défenseurs de la ville.\nLeur chef, un ainé de sang connu sous le nom de Voren\'thal, a exigé de parler au naaru [npc=18481]. À mesure que le naaru s\'approchait de lui, Voren\'thal s\'agenouilla et prononça les mots suivants : « Je vous ai vu dans une vision, naaru. Le seul espoir de survie de ma race est avec vous. Mes disciples et moi-même sommes là pour vous servir ».\nLa défection de Voren\'thal et de ses partisans a été la plus grande perte jamais subie par les forces de Kael\'thas. Beaucoup des plus forts et les plus brillants parmi les savants et les magistrats de Kael\'thas ont été influencés par l\'influence de Voren\'thal. Le naaru a accepté les déflecteurs qui sont devenus connus sous le nom de Clairvoyant.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29426][/span] à [npc=18531] dans la bibliothèque du Visiteur des Clairvoyants accordera une réputation de 250 points de réputation pour les Clairvoyants. Il existe également une quête répétable où donner une unique chevalière accorde 25 points de réputation. Ces chevalières tombent sur des membres Aile-de feu dans la partie nord-est de la forêt de Terrokar. \nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique chevalière.\n\n[b]Jusqu\'à exalté [/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1][item=30810][/span], cest le même principe que les chevalières mais ceux-ci tombent sur des elfes de sang Solfurie de haut rang. Si vous le souhaitez, vous pouvez transformer les chevalières de niveau supérieur avant une réputation honorée. Vous les trouverez dans [zone=3523], [zone=3520] et les instances du [url=?Search=tempête+donjon]donjon de la tempêtes[/url].\n\n[b]Tome des Arcanes[/b]\n\n[span class=q2][item=29739][/span] peut être donné à tout moment à [npc=18530] à l\'intérieur la Bibliothèque du Visiteur. Cela augmentera votre réputation avec les Clairvoyants de 350 par Tome des Arcane.\nEn plus des gains de réputation, vous recevrez une [span class=q1][item=29736][/span], qui est la condition pour acheter l\'enchantements d\'épaule à [npc=20808], qui réside dans la banque des Claivoyants.\n\n[h3]Passer à la réputation des Claivoyants[/h3]\n\nPour changer votre faction d\'Aldor vers Claivoyants et donc accéder à leurs recettes d\'artisanat (et annuler toutes les avancées de réputation que vous avez faites), trouvez [npc=18596], membre des Claivroyants dans la ville basse. Elle vous propose une quête répétable, [quest=10024], où pour huit [span class=q1][item=25744][/span] vous montez la réputation Claivoyant. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.',NULL),(8,942,2,NULL,0,2,'L[b]Expédition Cénarienne[/b] a été envoyé par [faction=609], lors de la réouverture de la porte des ténèbres vers l\'Outreterre, pour explorer ce monde inconnu. Tout comme le cercle, il s\'agit d\'une coalition de forces entre les Elfes de la nuit et les Taurens. Depuis l\'ouverture de la porte, l\'expédition Cénarienne a rapidement gagné en taille et en autonomie, obtenant suffisamment de puissance pour être considérée comme une propre et unique faction. L\'expédition maintient sa base principale au refuge Cénarien dans [zone=3521], située immédiatement à louest de la péninsule des flammes infernales. Elle est aussi présente sur [zone=3483], dans [zone=3519], et dans [zone=3522]. \n\nLe Refuge est situé dans le marécage de Zangar afin détudier la faune riche située là-bas. Cependant, l\'expédition a révélé des retombées inquiétantes dans le marais. Les niveaux d\'eau dans de nombreuses régions du marécage diminuent, et certaines régions comme Morte-bourbe ont déjà beaucoup souffert de ce phénomène étrange. On sait que cette diminution des niveaux d\'eau peut être attribuée aux pompes qui ont été construites dans le marécage par les naga. Leur but est de créer un nouveau puits d\'éternité pour [npc=22917].\nCependant, l\'expédition ne peut pas se permettre une confrontation directe avec le naga si nombreux dans le marécage de Zangar et le [url=?Search=Glissecroc#c0z]Réservoir de Glissecroc [/url]. Elle a besoin de l\'aide daventurier qui veulent soutenir les druides dans leur dangereuse bataille contre les Nagas qui cherchent à perturber l\'équilibre naturel du marais. Naturellement, ceux assez héroïques pour combattre au réservoir de Glissecroc seront bien récompensés.\n\n[h3]Réputation[/h3]\n\n[b]De neutre à honoré[/b]\n\nTuez des Nagas chaque fois que vous le pouvez. Le mieux sera de parcourir les instances, la réputation monte plus rapidement.\nAlternativement, le joueur peut commencer à trouver des [item=24401] pour avoir une chance davoir des [item=24407], qui peuvent être transformé en 500 points de réputation. Il est suggéré que le joueur garde ses espèces non cataloguées jusqu\'à ce que son statut honoré soit atteint, car la quête ne peut pas être poursuivie après ce point, alors que les espèces non cataloguées peuvent être utilisées jusqu\'à Exalté.\n\nSi vous êtes un herboriste et que vous êtes intéressé par la réputation [faction=970], vous voudrez peut-être trouver les [url=?Npcs&filter=na=Seigneur+tourbe]Seigneurs-tourbes[/url] qui se trouve dans lEst, et le coin Sud-ouest du Marécage de Zangar. Leurs corps peuvent être «récoltés» par les herboristes et produisent souvent des végétaux non identifiées, alors que chaque monstre tué donne 15 points de réputation chez Sporeggar. \n\n[b]De honoré à révéré[/b]\n\nUne fois que le joueur est honoré, faire lenclos aux esclaves et [zone=3716] (à l\'exception de [npc=17770] et de certains géants), n\'accorderont plus de réputation. Vous devriez maintenant faire des quêtes de l\'Expédition Cénarienne dans la péninsule des flammes infernal, le marécage de Zangar, la forêt de Terokkar et les Tranchantes. Il est également temps de transformer toutes les espèces non cataloguées que vous avez trouvées. Faire cela devrait vous faire passer révérer.\n\nAlternativement, vous pouvez, en étant niveau 70, faire [zone=3715]. Chaque donjon donne un peu plus de 1500 points de réputation si vous tuez toutes les mobs.\nDans le Caveau de la vapeur, se trouve, aussi, une quête répétable, [quest=9764], qui commence par [item=24367]. Vous pourrez ensuite donner les [item=24368], qui tombe à la fois dans le caveau de la vapeur et lenclos aux esclaves, recevant 250 points de réputation pour les premières armes et 75 points de réputation par la suite. Cette quête est disponible jusqu\'à exalté.\n\nUne fois que vous avez le niveau 70 et que vous avez amélioré votre équipement, vous pouvez choisir d\'entrer dans lenclos des esclaves, le caveau de la vapeur et basse-tourbière en mode héroïque avec l\'achat de la [item=30623]. Ils accordent une réputation importante : les mobs ordinaires valent 15 points de réputation, 2 pour les non élites et 150 à 250 pour les boss. Cette méthode fonctionne jusqu\'à exalté.\n\n[b]De révéré à exalté [/b]\n\nContinuez avec la même stratégie que ci-dessus : terminez toutes les requêtes restantes, faites caveau de la vapeur et continuez avec la quête des [item=24368].\n\nIl est également possible de faire lenclos des esclaves, Basse-tourbière et caveau de la vapeur en mode héroïque. La réputation acquise n\'est pas beaucoup plus intéressante que le caveau de la vapeur en mode normal, alors que l\'investissement dans le temps pour les donjons héroïques est beaucoup plus élevé, le butin est mieux et vous recevrez [item=29434] sur les boss qui peuvent être utilisés pour acheter des équipements épiques de haute qualité.',NULL),(8,941,2,NULL,0,2,'Les [b]Mag\'har[/b] sont la faction d\'orcs à peau brune qui sont restées en Outreterre et se sont séparés des autres clans orcs restants qui ont été victimes de [npc=17257] et qui sont maintenant dirigés par le puissant [npc=16808]. Les Mag\'har sont présent dans la forteresse de Garadar dans le magnifique pays de [zone=3518], une fois bien installés, la majorité des orcs sont retournés dans [zone=3519] et [zone=3522].\n\nLes Maghar n\'ont jamais été corrompus par Mannoroth ou Magtheridon. Contrairement à dautres anciens clans qui vivent dans les ruines de leurs ancêtres, les Mag\'har sont composés de membres de différents clans d\'orc qui ont échappé à la corruption. Le chef actuel des Mag\'har, la vénérable [npc=18141], est une orc ancienne et sage, mais elle est tombée récemment extrêmement malade. [npc=18063], fils du puissant Grom hurlenfer, sert de chef militaire aux Mag\'har, aidé par [npc=18106], fils du vénérable chef du clan Orbite-Sanglante, Kilrogg Deadeye. En outre, il existe un orc dans un camp de Mag\'har à l\'ouest connu sous le nom [npc=18229].\n\nIl n\'est pas clair comment le Mag\'har a réussi à conserver sa peau marron d\'origine. La peau orque devient verte lorsqu\'elle est exposée à la magie du sorcier, indépendamment des croyances ou des pratiques de l\'individu ; Garrosh et Jorin auraient certainement été exposés, compte tenu de la position hiérarchique de leurs pères.\n\nLes joueurs de la Horde commencent inamical avec le Mag\'har. Les joueurs de l\'Alliance seront toujours traités comme hostiles. La contrepartie de l\'Alliance à cette faction est la faction des : [faction=978].\n\n[h3]Quête[/h3]\n\nLes quêtes pour les Mag\'har commencent dans [zone=3483] avec [quest=9400] de [faction=947]. Cette quête vous mènera à un petit avant-poste Mag\'har au nord de la Citadelle des flammes infernales. Une fois à Nagrand, les joueurs trouveront la principale ville de Mag\'har, Garadar. La ville détient la plupart des quêtes restantes qui récompenseront la réputation de Mag\'har.\n\n[i]Note : Vous DEVEZ compléter la suite de quête de \"lassassin\" jusqu\'à la quête [quest=9410] (où vous devenez neutre) afin que vous puissiez parler à la plupart des gens de Garadar.[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x[item=25433], qui tombent de ces ogres.\n\nLes joueurs qui recherchent la réputation : [faction=933] peuvent vouloir garder leurs perles, car la réputation Mag\'har est généralement plus facile à obtenir. \nLes joueurs qui recherchent la réputation :[faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, car ils donnent aussi des [item=29425] pour la réputation Aldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent une réputation jusquà exalté![/i]',NULL),(8,946,2,NULL,0,2,'Le [b]Bastion de lHonneur[/b], refuge des explorateurs humains, élu, draenei et nains, est la première grande ville que les explorateurs de l\'Alliance rencontreront en traversant la porte des ténèbres. Les vestiges des fils de Lothar, anciens combattants de l\'Alliance qui sont venus à Draenor, se sont tenus fermement dans cet avant-poste des flammes infernales. Ils sont maintenant rejoints par les armées de Hurlevent et Forgefer.\n\n[h3]Réputation[/h3]\n\nLa réputation du Bastion de l\'Honneur est gagnée par divers moyens dans la péninsule des flammes infernales. Les PNJs, dans et autour, de la citadelle donnent en récompensés de quêtes de l\'honneur et de la réputation. En raison du manque de représentants dans d\'autres endroits dOutreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation au bastion de lhonneur une fois que vous partez de la péninsule.\n\n[b]Jusquà Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes du Bastion de l\'honneur, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à lextérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée sarrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10106]. Cette quête accorde 70 points d\'honneur et 150 points de réputation au Bastion de lHonneur, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24579][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=17657] et [npc=18266] au Bastion de lHonneur ainsi que [npc=18581] aux marécages de Zangar.\n\n[b]Jusquà Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). Linstance des salles brisées abrite des PNJs qui donnent de la réputation jusquà Exalté.[/li]\n[li]Après avoir obtenu le statut dhonoré, vous pouvez acheter [span class=q1][item=30622][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusquà Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24520][/span] à l\'adjudant Tracy Proudwell et augmenter le montant gagné de réputation (et dexpérience) acquise lors de l\'exécution de ces instances.[/i]',NULL),(8,967,2,NULL,0,2,'[b]L\'Oeil Pourpre[/b] est une secte secrète fondée par le Kirin Tor de Dalaran pour espionner le gardien de Tirisfal, [npc=15608], dans la tour de [zone=2562]. Bien que Medivh soit mort, l\'il pourpre reste dans Karazhan, défendant le mal qui semble lenvahir en l\'absence de son maître.\n\nOn ignore si l\'apprenti de Medivh, [npc=18166], était membre de lOeil Pourpre, ou s\'il connaissait leurs activités à l\'époque.\n\n[h3]Réputation[/h3]\n\nLa réputation de lil pourpre est obtenue en tuant des mobs à l\'intérieur de Karazhan et en complétant les quêtes liées à Karazhan. La réputation grâce aux mobs de Karazhan peut être acquise à partir d\'une position neutre jusquà une réputation exalté. Chaque mob apporte une réputation d\'environ 15 points, les boss accordent davantage de réputation.\n\n[npc=18253] propose une chaîne de quête assez longue commençant par [quest=9824] et [quest=9825]. Cette suite de quête se termine par [quest=9644] et récompense les joueurs avec [span class=q1][item=24490][/span]. L\'achèvement complet de cette suite de quête récompense le joueur avec 10 270 point de réputation d\'environ.\n\n[h3]Récompenses de la réputation[/h3]\n\n[npc=18253] offrira aux joueurs des bagues en récompenses pour chaque niveau de réputation sous forme de quêtes. La première de ces quêtes est disponible dès la réputation neutre. Vous recevrez une version nouvelle et améliorée de la bague que vous avez choisi chaque fois que vous entrez dans un nouveau niveau de réputation. Les anneaux sont triés dans les 4 catégories suivantes :\n[ul]\n[li][quest=10731] : [item=29280], [item=29281], [item=29282] et [item=29283][/li]\n[li][quest=10729] : [item=29284], [item=29285], [item=29286] et [item=29287][/li]\n[li][quest=10732] : [item=29276], [item=29277], [item=29278] et [item=29279][/li]\n[li][quest=10730] : [item=29288], [item=29289], [item=29291] et [item=29290][/li]\n[/ul]\n\n[npc=16388], un forgeron situé à l\'intérieur de Karazhan juste après [npc=15550], offre aux joueurs ayant une réputation assez élevée la possibilité d\'acheter des plans de forge épique. Les joueurs honorés ou au-dessus pourront également réparer des armures et des armes chez ce fournisseur.\n\n[npc=18255], qui se trouve juste à l\'extérieur des portes principales de Karazhan, vendra une recette de joaillerie épique et un enchantement d\'épaule aux joueurs qui ont une haute réputation avec lOeil Pourpre.',NULL),(8,970,2,NULL,0,2,'Les[b]Sporeggar[/b] sont une race de champignons essentiellement pacifique originaire d\'Outreterre. Ils vivent dans une ville située dans les tourbières occidentales de [zone=3521].\n\n[h3]Réputation [/h3]\n\nLes joueurs de l\'Alliance et de la Horde commencent amicalement avec Sporeggar. Il existe de nombreuses façons d\'augmenter votre réputation au début : \n[ul]\n[li]Apporter 10 [span class=q1][item=24290][/ span] à [npc=17923] pour compléter [quest=9739][/li]\n[li]Apporter 6 [span class=q1][item=24291][/span] à Fahssn pour compléter [quest=9743][/li]\n[i]Ces deux quêtes ne seront disponibles que si vous avez une réputation au minimum amical[/i]\n[li]Tuer [url=?Search=seigneurs +tourbes+-hungry #z0z]Seigneurs tourbes[/url] [i](jusqu\'à honoré)[/i][/li]\n[li]Tuer [npc=18137] et [npc=18136] [i](jusqu\'à révéré)[/i][/li]\n[li]Apporter 10 [span class=q1][item=24245][/span] à [npc=17924] dans Sporeggar[i] (jusquà amical)[/i][/li]\n[/ul]\n\nAprès avoir une réputation [b]amicale[/b], de nouvelles quêtes répétitives s\'ouvrent en même temps que les quêtes de Fahssn, notamment :\n[ul]\n[li]Tuer 12 [npc=18088] et [npc=18089] pour [npc=17856] pour compléter [quest=9726][/li]\n[li]Apporter 10 [span class=q1][item=24449][/span] à [npc=17925] pour compléter [quest=9806][/li]\n[li] S\'aventurer dans [zone=3716] pour rassembler 5 [span class=q1][item=24246][/span] pour terminer [quest=9715][/li]\n[/ul]\nCes 3 quêtes sont répétables et seront disponibles jusquà la réputation exalté.\nLes joueurs qui sont exaltés avec Sporeggar devraient parler à [npc=17877] pour une dernière quête.',NULL),(8,978,2,NULL,0,2,'Les Kurenaï, pour « racheté », ont échappé à lesclavage en Outreterre et ont fait leur maison à Telaar dans le sud de [zone=3518]. C\'est là qu\'ils cherchent à redécouvrir leur destinée. Ils conservent également une petite présence en [zone=3521]. Leur intendant, [npc=20240], est situé à l\'extérieur de l\'auberge à Telaar, en dessous du point de vol.\n\nLes joueurs de l\'Alliance commencent à faire preuve d\'hostilité avec les Kurenai. Les joueurs de la Horde seront toujours traités comme hostiles. La contrepartie de la Horde à cette faction est [faction=941].\n\n[i]Kurenai est le japonais pour « cramoisi ».[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?Npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x [item=25433], qui tombent de ces ogres.\n\nLes joueurs qui cherchent la réputation de la faction [faction=933] peuvent vouloir garder leurs perles, car la réputation de Kurenai est généralement plus facile à obtenir.\n\nLes joueurs qui cherchent la réputation de la faction [faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, alors qu\'ils donnent des [item=29425] pour la réputation de lAldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent de la réputation jusquà exalté.[/i]',NULL),(8,989,2,NULL,0,2,'Les [b]Gardiens du Temps[/b] sont des dragons de bronze sélectionnés par Nozdormu pour surveiller les grottes du temps. Ils sont dirigés par [npc=19932] et [npc=19933], qui remplacent également Nozdormu en son absence.\n\n[h3]Réputation[/h3]\n\nActuellement, la seule façon d\'obtenir la faveur des dragons de bronze est de faire les instances : [zone=2367] et [zone=2366]. Lintendant des Gardiens du Temps, [npc=21643], se situe au quartier-intendant dans les grottes du temps. Les Gardiens vous demanderont d\'être au minimum niveau 66 et de compléter la courte quête [quest=10277] avant d\'autoriser le passage dans Les contreforts dHautebande dantan pour accomplir la destinée du Chef de la Horde, [npc=17876].',NULL),(8,990,2,NULL,0,2,'La [b]Balance des sables[/b] est un sous-groupe secret du vol des Dragons de bronze, dirigé par [npc=19935], premier partenaire de [npc=15185]. Leur chef, Nozdormu, a envoyé ces factions gardiennes à [zone=3606] où ils gardent l\'Arbre Monde d\'une autre attaque par les démons, contribuent à restaurer le temps et à préserver l\'avenir du monde.\n\n[h3]Réputation[/h3]\n\nTuer les boss et monstres du Fléau font monter la réputation. [npc=17968], le boss final, récompense de 1 500 points de réputation tandis que les quatre autres boss donnent 375 points de réputations. La réputation général des montres du Fléau donnent 12 points de réputation, tandis que [npc=17907] donnent 60 points de réputation. En produisant une moyenne de 7 800 points de réputations par raid, 6 raids sont nécessaires pour atteindre la réputation exaltée.\n\nActuellement, la réputation permet davoir lune des meilleurs [span class=q4][url=?Items=4.-2&filter=na=bague+éternel]Bagues[/url][/span] pour les raids. Afin de recevoir ces anneaux, vous devez compléter la quête précédemment requise, [quest=10445]. Chaque nouveau niveau de réputation accorde une bague améliorée.',NULL),(8,1012,2,NULL,0,2,'Les [b]Ligemorts Cendrelangues[/b] sont l\'élite de la tribu Kurenaï connue sous le nom de Cendrelangue. La tribu Cendrelangue est dirigée par la sage aînée [npc=21700]. Les Ligemorts sont [i]officiellement[/i] alignés avec [npc=22917] [small][/small]. Les Ligemorts sont les lieutenants les plus dignes d\'Akama et sont au courant des motivations mystérieuses de leur chef.\n\nPour découvrir les Ligemorts Centrelangues en tant que faction, le joueur doit commencer et compléter la majorité de la suite de quête qui commence par [quest=10568] ou [quest=10683]. Finalement, vous parlerez avec Akama, après quoi vous deviendrez neutre avec les Ligemorts Cendrelangues.',NULL),(8,947,2,NULL,0,2,'[b]Thrallmar[/b], expédition envoyée par le Portail des Ténèbres par Thrall, a construit un bastion dans la péninsule des flammes infernales qui sert de base d\'opérations pour une grande partie des activités de la Horde en Outreterre.\n\n[h3]Réputation[/h3]\n\nLa réputation de Thrallmar jusqu\'à l\'honorée est relativement facile à gagner. Même les quêtes les plus faciles (celles qui vous emmènent d\'un fournisseur de quête à la prochaine, par exemple) peuvent produire 75 points de réputation, alors que ceux qui nécessitent plus defforts pour compléter ont généralement 250 points de réputation ou plus. Certaines quêtes de groupe impliquant de tuer un élite peuvent donner jusqu\'à 1 000 points de réputation.\n\nSi vous faites la majeure partie des quêtes de Thrallmar au lieu de passer rapidement à la prochaine zone, vous pourriez vous attendre à être honoré après 1 ou 2 niveaux de jeu. En raison du manque de représentants dans d\'autres endroits dOutreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation à Thrallmar une fois que vous partez de la péninsule. Cest seulement au niveau 68 que vous pouvez commencer à regagner des points dans le donjon [zone=3714].\n\n[b]Jusquà Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes de Thrallmar, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à lextérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée sarrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10110]. Cette quête accorde 70 points d\'honneur et 150 points de réputation à Thrallmar, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24581][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=18267] et [npc=18564] à Thrallmar et près de Zabrajin dans [zone=3521].\n\n[b]Jusquà Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). Linstance des salles brisées abrite des PNJs qui donnent de la réputation jusquà Exalté.[/li]\n[li]Après avoir obtenu le statut dhonoré, vous pouvez acheter [span class=q1][item=30637][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusquà Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24522][/span] au Crieur-de-guerre Coquard et augmenter le montant gagné de réputation (et dexpérience) acquise lors de l\'exécution de ces instances.[/i]',NULL),(8,1011,2,NULL,0,2,'[b]Ville Basse[/b] de [zone=3703] est l\'endroit où les réfugiés se rassemblent et saident par leurs propres moyens. Lorsque vous aidez l\'une des races qui ont fui la guerre, la réputation se débrouille rapidement. Leur intendant, [npc=21655], est situé sur le marché dans la ville basse.\n\nLa ville basse de Shattrath contient de nombreux artisans qui possèdent de vastes connaissances :\n[ul]\n[li][npc=19187], [small]< Maître des travailleurs du cuirs >[/ small].[/li]\n[li][npc=19180], [small]< Maître des dépeceurs >[/small].[/li]\n[li][npc=19052], [small]< Maître des alchimistes >[/small]. Il donne la quête [quest=10902] (pour une spécialisation). Un laboratoire dalchimiste se trouve également à son côté.[/li]\n[li]Trois tailleurs qui vous permettent de se spécialiser et d\'acheter de nouvelles recettes de couture épiques pour des ensembles d\'armures et des sacs spéciaux :\n[ul][li][npc=22212], [small]< Spécialiste de couture de tisse-ombre >[/small] vend des recettes pour [itemset=553][/li]\n[li][npc=22213], [small]< Spécialiste de couture de feu-sorcier >[/small] vend des recettes pour [itemset=552].[/li]\n[li][npc=22208], [small]< Spécialiste de couture détoffe lunaire > [/small] vend des recettes pour [itemset=554].[/li][/ul]\n[/ul]\n\nLes maîtres de guerre, Alliance et Horde, des quatre [zones=6] peuvent également être trouvés ici, ainsi que la Tavernes de la Fin du Monde.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré [/b]\n[ul]\n[li]Faire [zone=3790] en [i]mode normal[/i], vous récompense denvirons 750 points de réputation.[/li]\n[li]Faire [zone=3791] en [i]mode normal[/i], vous récompense denvirons 1 250 points de réputation.[/li]\n[li]Faire [zone=3789] en [i]mode normal[/i], vous récompense denvirons 2 000 points de réputation.[/li]\n[li]Fournir 30 x [item=25719] à [npc=22429], vous récompense de 250 points de réputations par quête.[/li]\n[/ul]\n[i]Note : Les joueurs qui visent une faction supérieure à Honorée devraient attendre jusqu\'à dêtre honoré avant de compléter les quêtes de la Ville Basse.[/i]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire de Labyrinthe des ombres en [i]mode normal[/i], vous récompense de 2 000 points de réputation.[/li]\n[li]Terminer toutes les [url=?quests&filter=cr=1;crs=1011;crv=0]quête de la Ville-Basse[/url].[/li]\n[/ul]\n[b]De révéré à exalté[/b]\n[ul]\n[li]Faire les Cryptages Auchenai en [i]mode héroïque[/i], vous récompense denvirons 750 points de réputation.[/li]\n[li]Faire les salles de Sethekk en [i]mode héroïque[/i], vous récompense denvirons 1 250 points de réputation.[/li]\n[li]Faire le Labyrinthe des ombres en [i]mode normal[/i] ou en [i]mode héroïque[/i], vous récompense denvirons 2 000 points de réputation.[/li]\n[/ul]\n\n[h3]Anecdotes[/h3]\n\n[npc=19227], un vendeur dans la ville basse, vend des amulettes qui sont très ... intéressantes. Il vend des articles comme [item=27940], qui vous permettent de revenir à la vie lorsque vous retournez à l\'endroit où vous êtes mort. [i]Buyer se méfiez-vous![/i]\n\nEn tant quexalté, vous pouvez acheter un [item=31778]. Curieusement, aucun des habitants de la Ville Basse na été vu avec un tel objet. Peut-être qu\'ils ne peuvent pas se le permettre',NULL),(8,1015,2,NULL,0,2,'L[b]Aile-du-Néant[/b] est une faction de dragons situés en Outreterre. La couvée inhabituelle a été engendrée par les ufs du vol de dragon noir dAile-de-Mort et infusée d\'énergies brutes. Maintenant, ils cherchent à trouver leur identité au-delà de l\'ombre du patrimoine destructeur de leur père.\n\n[h3]Réputation[/h3]\n\nLes joueurs, au commencement, sont haïe à la faction Aile-du-Néant et doivent être exaltés pour recevoir des [span class=q4][url=?Items=15.-7&filter=na=Aile-du-Néant+Drake]Drakes Aile-du-Néant[/url][/spanclass]. La suite de quête de la réputation est une suite qui se fait en solitaire impliquant des quêtes journalières, une quête de groupes (5 joueurs) pour passer Neutre et les quêtes journalières de groupe (3 joueurs) après être passer Révéré.\nUne monture volante est requise pour cette réputation et 300 compétences de monte sont nécessaires pour passer neutre.\n\n[b]De Haïe à Neutre[/b]\n\nLes joueurs de niveau 70 commenceront leur voyage pour une réputation exaltée en choisissant la suite de quête offerte par [npc=22113], un elfe du sang errant la surface des champs dAile-du-Néant, dans le coin sud-est de [zone=3520]. La suite de quête commence par [quest=10804]. L\'achèvement de cette suite fournira une réputation instantanée neutre et le choix de l\'un de [span class=q3][url=?Items&filter=qu=18;cr=1;crv=0;na=Aile%20néant;qu=3]ces 5 items[/url][/span].\n\n[h3]Après Neutre [/h3]\n\nAprès avoir terminé la suite de quête, Mordenai sassurera qui vous ayez acquis 300 compétences [spell=34091] et que vous ayez une réputation neutre auprsè de lAile-de-Néant.\nCela vous accordera un déguisement dOrc Gueule-de-Dragon lorsque vous entrez dans la zone Aile-du-Néant et vous permettra de communiquer et de travailler pour les Gueules-de-Dragon stationné là-bas.\n\nMordenai vous enverra d\'abord à [npc=23139] avec un ensemble de faux papiers. L\'achèvement de cette quête débloque le début des quêtes Gueule-de-Dragon sur lesquelles vous travaillerez pour augmenter votre réputation Aile-du-Néant.\n\nLa plupart de ces quêtes seront journalières (ajoutée à la 2.1). Les quêtes journalières diffèrent des quêtes régulières car elles sont infiniment repérables, mais vous ne pouvez compléter chaque quête journalière qu\'une fois par jour et se limiter à 25 quêtes journalières par jour.\n[i]Remarque : De nouvelles quêtes seront débloquées après chaque niveau de réputation, et toutes les quêtes journalières des niveaux précédents seront toujours disponibles.[/i]\n\n[b][toggler id=Neutralcaché]Neutre[/toggler][/b]\n\n[div id=Neutralcaché] \nAprès avoir donné la [item=32469] à [npc=23139] pour compléter [quest=11013], votre première suite de quêtes sera disponible pour accéder au prochain niveau de réputation avec Aile-du-Néant.\n\nMor\'ghor vous indiquera daller voir le maître d\'uvre afin de commencer votre travail, et [npc=23141] se révélera comme un allié déguisé et vous proposera dautres quêtes.\nL\'une d\'entre elles est [quest=11049]. Les joueurs pourront trouver, avec un peu de chance (1% de loot), l[item=32506] sur presque toutes les créatures de lescarpement dAile-du-Néant et sur un [item=185881] ou un [item=185877].\nYarzill voudra aussi une trouvaille rare, l[item=185915], trouvée n\'importe où sur le rebord dAile-du-Néant et dans la forteresse Gueule-de-Dragon, coin sud-est de la vallée de dOmbrelune. Cette quête n\'est pas étiquetée comme journalière et peut donc être effectuée autant de fois que vous voulez, du moment que vous pouvez trouver des ufs. Cette quête nest pas comprise dans votre limite de quête journalière.\n\nAutres quêtes disponibles dès le début:\n[ul]\n[li][i][small]Journalière[/small][/i] - [quest=11018], [quest=11016], [quest=11017] Nest disponible que pour les joueurs qui possèdent la profession adaptée pour rassembler chaque élément.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11015] - Une quête de collecte simple ouverte à tous les joueurs indépendamment de leur profession.[/li] \n[li][i][small]Journalière[/small][/i] - [quest=11020] - Yarzill vous demandera de collecter des [item=32502]s et de les utiliser afin dempoisonner les péons qui travaillent pour rassembler des ressources pour Gueule-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11035] - Vous devrez voler vers le coin nord-est de lescarpement dAile-du-Néant et vous positionner sur une des roches flottantes pour intercepter le [npc= 23188] et récupérer 10 x [item=32509].[/li]\n[/ul]\n[/div]\n[b][toggler id=Friendlyhidden]Amical[/toggler][/b]\n\n[div id=Friendlyhidden]\nMor\'ghor vous donnera un [item=32694] pour circuler avec votre nouveau rang parmi les Gueules-de-Dragon.\n[ul]\n[li][quest=11083] - [npc=23166] vous enverra tuer des bourbesangs qui sont stationné profondément dans les mines.[/li]\n[li][quest=11081] - Après avoir trouvé les [item=32726] dans un [item=32724], vous révélerez ce qui se passe réellement avec les bourbesangs dans la mine.[/li]\n[li][quest=11054] - [npc=23291] vous donnera vos propres [item=32680] pour garder les pétons Gueules-de-Dragon en ligne et travailler avec efficacité[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11076] - La [npc=23149] vous demandera de vous aventurer dans les mines Ailes-du-Néant et de récupérer la cargaison contenue dans les chars de la mine qui est jetée au hasard dans l\'intérieur de la mine.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11077] - L\'un des [npc=23376] vous informera que des créatures plus profondes dans la mine interrompent la production et vous demandent de réduire leur nombre.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11055] - Cette quête humoristique commence chez le [npc=23291] après que vous lui apportiez le matériel requis. Vous pourrez survoler lescarpement Aile-du-Néant et lancer le Booterang à n\'importe quel [npc=23311] qui sy trouve autour des cris-taux.[/li]\n[/ul]\n[/div]\n[b][toggler id=Honorécaché]Honoré[/toggler][/b]\n\n[div id=Honorécaché]\nMor\'ghor vous donnera votre nouveau [item=32695], qui est maintenant utilisable n\'importe où, tant que vous êtes à l\'extérieur.\n[ul]\n[li][quest=11063] - Cette quête en six parties est une course aérienne contre les autres maîtres de vol Gueule-de-Dragon. Ils tenteront tous de vous renverser, vous et votre monture, avec des attaques aériennes habilement placées, vous devez rester visible et sur votre monture jusqu\'à leur atterrissage, si vous échouez, vous devez redémarrer la quête. Après avoir vaincu le dernier des six coureurs, vous recevrez un [item=32863], qui fonctionne exactement comme une [item=25653]. Les effets des deux bijoux ne sadditionnent pas.[/li]\n[li][quest=11089] Le [npc=23427] demandera un ensemble de matériaux pour créer un dispositif spécial pour détruire son frère et entraver les avancées de la légion dans l\'ouest de [zone=3518].[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11086] - Mor\'ghor Vous enverra au Portal de Nagrand pour tuer 20 [url=?npcs=7&filter=na=ombremort] Agents Ombremort[/url]. Attention aux seigneurs, ils patrouillent dans la région et peuvent vous tuer dcoup de poing.[/li]\n[/ul]\n[/div]\n[b][toggler id=Révéréhidden]Révéré[/toggler][/b]\n\n[div id=Révéréhidden]\nMor\'ghor vous donnera votre nouveau [item=32864], le plus haut bijou.\n[ul]\n[li][url=?quests&filter=na=tuez%20les%20tous;minle=70;maxle=70] Tuez-les tous ![/url] - Mor\'ghor vous ordonnera de commencer l\'attaque la base d\'opérations de votre faction dans la vallée de Sombrelune. De toute évidence, vous n\'allez pas autoriser les Gueules-de-Dragon à attaquer vos alliés, alors vous informerez au leader approprié et débloquerez votre dernière quête journalière pour les Gueules-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] [url=?quests&filter=na=le%20plus%20mortel%20des%20pièges]Le plus mortel des pièges[/url] - Les forces Gueules-de-Dragon vont attaquer la base des opérations. Apportez des alliés, car il s\'agit d\'une grande bataille.[/li]\n[/ul]\n[/div]\n[b][toggler id=Exaltécaché]Exalté[/toggler][/b]\n\n[div id=Exaltécaché]\nAprès de nombreux jours de travail, finalement le dénouement de la suite des quêtes Aile-du-Néant / Gueule-de-Dragon, vous dirigera à Mor\'ghor une dernière fois, qui vous informera que vous serez promu par [npc=22917] lui-même.\nSans gâcher les événements qui s\'ensuivent, vous vous retrouverez à Shattrath avec une sélection de montures épiques Aile-du-Néant. Vous pouvez en choisir un gratuitement, et si vous décidez d\'une couleur différente plus tard, vous pouvez acheter un autre drake chez [npc=23489] dans le camp de Gueule-de-Dragon pour 200 or.\n[/div]',NULL),(8,1031,2,NULL,0,2,'Les [b]Gardes-ciel sha\'tari[/b] sont les gardiens aériens de [zone=3703], défendant la capitale des assaillants dans les collines ainsi que la lutte contre les Arakkoas de Terokk dans les sommets de Skettis. [faction=935] dirigent les gardes-ciel shatari.\nIls ont deux avant-postes, l\'un au nord des montages de Skettis et un près d[faction=1038]. Les joueurs commencent avec une réputation neutre chez les Gardes-ciel sha\'tari.\n\n[h3]Réputation[/h3]\n\n[b]Quêtes journalières[/b]\n[ul]\n[li][quest=11008] - [npc=23048] vous accordera un paquet d\'explosifs pour détruire les oeufs qui reposent au sommet des structures de Skettis. [/li]\n[li][quest=11085] - Le [npc=23383] peut être trouvé au sommet de certaines structures, les joueurs l\'escorteront pour la réputation, l\'or et un choix entre deux potions : [item=28100] ou [item=28101].[/li]\n[li][quest=11065] - [npc=23335] vous informera que les bombardements, de lavant-poste de la garde-ciel, ont coûté la vie de leurs montures et vous demandent de rassembler des Raies de léther pour compléter leurs forces aériennes.[/li]\n[li][quest=11010] - [npc=23120] vous demande de détruire les munitions pour les canons de la Légion afin que les gardes-ciel puissent continuer leur travail.[/li]\n[li][quest=11004] - Après avoir recueilli 6 [item=32388], [npc=23042] fera une potion qui permettra de voir l\'arakkoa le plus puissant, tel que [npc=23066].[i][small] Note : cette quête n\'est pas une quête journalière, mais peut être répété autant de fois que nécessaire. [/small][/i][/li]\n[/ul]\n\n[b]Créatures[/b]\n\n[ul]\n[li][npc=21804] - 5 points de réputation, jusqu\'à la fin de Révéré[/li]\n[li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70] Tous les Arakkoa de Skettis[/url] - 10 points de réputation.[/li]\n[li][npc=23029] - 30 points de réputation.[/li]\n[/ul]',NULL),(NULL,NULL,0,'new',0,2,'Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft\'s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow\'s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don\'t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won\'t risk losing them.[/li]\n[li][b]Make sure it\'s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]',NULL),(NULL,NULL,0,'edit',0,2,'Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft\'s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow\'s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don\'t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won\'t risk losing them.[/li]\n[li][b]Make sure it\'s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]',NULL),(13,1,3,NULL,0,2,'[b][color=c1]Krieger[/color][/b] sind eine sehr mächtige Klasse, die sowohl tanken als auch im Nahkampf erheblichen Schaden anrichten kann. Der [icon name=ability_warrior_defensivestance][url=?spells=7.1.257]Schutz[/url][/icon]-Talentbaum des Kriegers enthält viele Talente, um seine Überlebensfähigkeit zu verbessern und Bedrohung gegenüber Monstern zu erzeugen. Schutz-Krieger sind eine der wichtigsten Tank-Klassen des Spiels.\n\nAußerdem verfügen Krieger über zwei schadensorientierte Talentbäume - [icon name=ability_rogue_eviscerate][url=?spells=7.1.26]Waffen[/url][/icon] und [icon name=ability_warrior_innerrage][url=?spells=7.1.256]Furor[/url][/icon]. Der Furor-Talentbaum enthält das Talent [spell=46917], das es dem Krieger erlaubt, zwei Zweihandwaffen gleichzeitig zu führen! Krieger sind in der Lage, mit Fähigkeiten wie [spell=845], [spell=1680] und [spell=46924] starken Flächenschaden im Nahkampf zu verursachen. Ein Krieger kämpft in einer bestimmten [i]Haltung[/i], die ihm Boni und Zugang zu verschiedenen Fähigkeiten gewährt. Zu Beginn verfügen Krieger nur über die [spell=2457], erlernen aber mit Level 10 [spell=71] und mit Level 30 [spell=2458]. Die Verteidigungshaltung wird zum Tanken, die Kampfhaltung oder Berserkerhaltung für erheblichen Nahkampfschaden verwendet.\n\n[ul][li]Alle Krieger können ihren Schlachtzug oder ihre Gruppe mit einem [spell=6673] oder [spell=469] verstärken. Furor-Krieger besitzen den passiven Stärkungszauber [spell=29801], der die Chance auf kritische Treffer im Nah- und Fernkampf für ihre Verbündeten deutlich erhöht.[/li][li]Krieger haben zahlreiche nützliche Fähigkeiten, um schnell an ihr Ziel zu gelangen! Alle Krieger können [spell=100] oder [spell=20252] benutzen, um einen Gegner zu erreichen. Zudem können sie schnell [spell=3411], um ein befreundetes Ziel vor einem Angriff zu schützen.[/li][/ul]',NULL),(13,2,3,NULL,0,2,'[b][color=c2]Paladine[/color][/b] unterstützen ihre Verbündeten mit heiligen Auren und Segen, um sie vor Schaden zu bewahren und ihre Kräfte zu stärken. Sie tragen Plattenrüstungen und können in den härtesten Schlachten verheerenden Schlägen standhalten, während sie ihre Verwundeten heilen und die Gefallenen wiederbeleben. Im Kampf können sie mächtige Zweihandwaffen führen, ihre Feinde betäuben, Untote und Dämonen vernichten und ihre Feinde mit heiliger Vergeltung richten. Paladine sind eine defensive Klasse, die in erster Linie darauf ausgelegt ist, ihre Gegner zu überdauern.\n\nDer Paladin ist hauptsächlich ein Nahkämpfer und in geringem Maße Zauberer, der aufgrund seiner [url=?spells=7.2&filter=cr=109:12;crs=10:1;crv=0:0]Heilzauber[/url], [url=?spells=7.2&filter=na=Segen]Segen[/url] und anderen Fähigkeiten sehr nützlich für die Gruppe ist. Sie können eine aktive [url=?spells=7.2&filter=na=Aura]Aura[/url] pro Paladin auf alle Gruppen- und Schlachtzugsmitglieder legen und bestimmte Segen für bestimmte Spieler verwenden. Dank ihrer zahlreichen defensiven Fähigkeiten vergessen Paladine einfach unglaublich oft zu sterben. Mit ihrer Fähigkeit [spell=25780] sind sie außerdem ausgezeichnete Tanks.\n\n[ul][li]Paladine können effektiv [icon name=spell_holy_holybolt][url=?spells=7.2.594]heilen[/url][/icon], [icon name=spell_holy_devotionaura][url=?spells=7.2.267]tanken[/url][/icon] und im Nahkampf [icon name=spell_holy_auraoflight][url=?spells=7.2.184]Schaden[/url][/icon] verursachen.[/li][li]Sie besitzen eine große Auswahl an Segen, Auren und anderen Verstärkungszaubern.[/li][li]Der Paladin ist die einzige Klasse mit Zugang zu einem echten Unverwundbarkeitszauber: [spell=642].[/li][/ul]',NULL),(13,3,3,NULL,0,2,'[b][color=c3]Jäger[/color][/b] sind eine besonders einzigartige Klasse in World of Warcraft. Sie sind die einzigen nicht-magischen Fernkämpfer, die mit Bögen und Gewehren kämpfen. Jäger verfügen über verschiedene Arten von Schüssen und Bissen zur Schwächung ihrer Gegner und können [url=?spells=7.3&filter=cr=4;crs=1;crv=0;na=Falle]Fallen[/url] legen, um Schaden zu verursachen oder den Gegner auf andere Weise zu verlangsamen oder kampfunfähig zu machen.\n\nJäger [icon name=ability_hunter_beasttaming][url=?spell=1515]zähmen Wildtiere[/url][/icon], damit diese sie als [url=?pets]Begleiter[/url] im Kampf unterstützen. Zwar sind Jäger nicht die einzige Klasse, die Begleiter einsetzen kann. Ihre Tierbegleiter sind aber insofern einzigartig, als jede Spezies einen [url=?petcalc]eigenen Talentbaum[/url] hat, den der Jäger nutzen kann, um Punkte auf verschiedene Fähigkeiten zu verteilen.\n\nDarüber hinaus hat jede Spezies eine einzigartige Spezialfähigkeit. Jäger können sich die begehrtesten Begleiter aufgrund ihres Aussehens oder ihrer Fähigkeiten aussuchen. Und wenn sie genug Talentpunkte in den Baum der [icon name=ability_hunter_beasttaming][url=?spells=7.3.50]Tierherrschaft[/url][/icon] investieren, können sie besondere, "exotische" Bestien zähmen, wie [url=?pet=46]Geisterbestien[/url] oder [url=?pet=39]Teufelssaurier[/url]!\n\n[ul][li]Jäger haben Zugriff auf 25 (32 als [icon name=ability_hunter_beastmastery][url=?spell=53270]Meister der Tierherrschaft[/url][/icon]) verschiedene Arten von Begleitern mit über 150 verschiedenen Erscheinungsbildern![/li][li]Jäger haben eine Reihe von überlebensorientierten Fähigkeiten, die sie einsetzen können, um potentiellen Gefahren zu entkommen oder ihnen auszuweichen, wie z.B. [spell=5384] und [spell=781].[/li][li]Auf das [icon name=ability_hunter_swiftstrike][url=?spells=7.3.51]Überleben[/url][/icon] spezialisierte Jäger können in ihrem Talentbaum Punkte in das Talent [icon name=ability_hunter_huntingparty][url=?spells=-2.3&filter=na=jagdgesellschaft rel=spell=53292]Jagdgesellschaft[/url][/icon] investieren, welches es ihnen ermöglicht, ihre Gruppen- und Schlachtzugsmitglieder mit dem Stärkungszauber [spell=57669] zu versorgen.[/li][/ul]',NULL),(13,4,3,NULL,0,2,'[b][color=c4]Schurken[/color][/b] sind eine Nahkampfklasse, die Lederrüstungen trägt und ihren Feinden mit sehr schnellen Angriffen großen Schaden zufügen kann. Sie sind Meister der Verstohlenheit und des Meuchelns, die sich ungesehen an Feinden vorbeischleichen, aus den Schatten heraus zuschlagen und dann blitzschnell aus dem Kampf verschwinden.\n\nSie sind in der Lage, [url=?items=0.-3&filter=cr=152;crs=4;crv=0;ty=-3#0+1-2]Gifte[/url] einzusetzen, um ihre Gegner zu verkrüppeln und sie so im Kampf massiv zu schwächen. Schurken verfügen über ein mächtiges Arsenal an Fähigkeiten, von denen viele dadurch verstärkt werden, dass sie in [spell=1784] schleichen und ihre Opfer kampfunfähig machen können.\n\nSchurken können sich auf drei unterschiedliche Kampfstile mithilfe ihrer Talentbäume Meucheln, Kampf und Täuschung spezialisieren.\n\nAuf das [icon name=ability_rogue_eviscerate][url=?spells=7.4.253]Meucheln[/url][/icon] spezialisierte Schurken sind [icon name=ability_creature_poison_06][url=?spells=-2&filter=na=meister+der+gifte rel=spell=58410]Meister der Gifte[/url][/icon] und [icon name=ability_rogue_disembowel][url=?spell=57993]vergiften[/url][/icon] ihre Gegner mit schnellen Dolchen, die mit [icon name=ability_rogue_feigndeath][url=?spells=-2.4.253&filter=na=Üble+Gifte rel=spell=16515]üblen[/url][/icon] und [icon name=ability_poisons][url=?spells=-2.4.253&filter=na=Verbesserte+Gifte rel=spell=14117]verbesserten[/url][/icon] Giften versehen sind.\n\nAuf den [icon name=ability_backstab][url=?spells=7.4.38]Kampf[/url][/icon] spezialisierte Schurken können den Umgang mit [icon name=inv_sword_27][url=?spells=-2&filter=na=Niedermetzeln rel=spell=13964]Axt und Schwert[/url][/icon] oder [icon name=inv_mace_01][url=?spells=-2&filter=na=Streitkolben-Spezialisierung;cl=4 rel=spell=13803]Streitkolben[/url][/icon] meistern und haben mithilfe ihrer Talente auch in langwierigen Kämpfen eine verbesserte Energiezufuhr, um zuverlässig ihre Angriffscombos durchzuführen.\n\nAuf die [icon name=ability_stealth][url=?spells=7.4.39]Täuschung[/url][/icon] spezialisierte Schurken besitzen Fähigkeiten, die unvorhergesehene Aktionen ermöglichen. So können sie dank [spell=51713] etwa kurzzeitig Fähigkeiten nutzen, die eigentlich nur aus der Verstohlenheit heraus nutzbar wären, mit [spell=36554] plötzlich hinter einem Gegner auftauchen oder mit [icon name=ability_rogue_cheatdeath][url=?spells=-2.4.39&filter=cr=15;crs=0;crv=ability_rogue_cheatdeath rel=spell=31230]Von der Schippe springen[/url][/icon] einen sicheren Todesstoß überleben.',NULL),(13,5,3,NULL,0,2,'[b][color=c5]Priester[/color][/b] gelten allgemein als eine der Standard-Heilerklassen in World of Warcraft, da sie über zwei Talentspezialisierungen zur effektiven Heilung verfügen.\n\nIhr [icon name=spell_holy_holybolt][url=?spells=7.5.56]Heilig[/url][/icon]-Talentbaum enthält Talente, die die Heilung auf ihre Verbündeten erheblich verstärken - einschließlich Zaubern, mit denen mehrere Spieler gleichzeitig geheilt werden können, wie z.B. [spell=48089].\nDer [icon name=spell_holy_wordfortitude][url=?spells=7.5.613]Disziplin[/url][/icon]-Talentbaum ist zwar auch in der Lage, eine beträchtliche Menge an Heilung zu bewirken, konzentriert sich aber in erster Linie auf die Schadensabsorption und -verminderung durch den Einsatz von [spell=48066] und [icon name=spell_holy_devineaegis][url=?spells=-2.5&filter=cr=15;crs=0;crv=spell_holy_devineaegis rel=spell=47515]Göttliche Aegis[/url][/icon].\nPriester können außerdem mit ihren [icon name=spell_shadow_shadowwordpain][url=?spells=7.5.78]Schatten[/url][/icon]fähigkeiten sehr mächtigen Fernkampfschaden verursachen. Insbesondere wenn sie ihre [spell=15473] annehmen, erhöht sich ihr Schattenschaden erheblich, aber sie verlieren ihre Fähigkeit, Heiligzauber zu wirken.\n\n[ul][li]Der Disziplin-Talentbaum wird in der Regel zur Heilung verwendet, enthält aber auch einige Talente zur Erhöhung des Schadens des Priesters, wobei Schattenzauber und -fähigkeiten in erster Linie zur Verursachung von Fernkampfschaden verwendet werden sollten.[/li][li]Priester verfügen über einen der am meisten geschätzten Stärkungszauber im Spiel - [spell=48161], welcher allen befreundeten Mitspielern eine unverzichtbare Erhöhung ihrer Ausdauer gewährt. Außerdem können sie ihre Mitspieler mit [spell=48073] und [spell=48169] verstärken und mit einzigartigen Hymnen das [icon name=spell_holy_divinehymn][url=?spell=64843]Leben[/url][/icon] und [icon name=spell_holy_symbolofhope][url=?spell=64901]Mana[/url][/icon] ihres Schlachtzug signifikant wiederherstellen![/li][li]Schattenpriester unterstützen, zusätzlich zu ihrem Schaden, jeden Schlachtzug mit dem beliebten Stärkungszauber [spell=57669] zur Erhöhung der Manaregeneration und mit ihrer [spell=15286], die ihre gesamte Gruppe passiv heilt.[/li][/ul]',NULL),(13,6,3,NULL,0,2,'Die [b][color=c6]Todesritter[/color][/b] wurden in der Erweiterung Wrath of the Lich King eingeführt und sind die erste Heldenklasse von World of Warcraft. Todesritter beginnen auf Stufe 55 in einer speziellen, instanzierten Zone, die für andere Klassen unzugänglich ist: [url=?maps=4298:511346]Acherus, die Schwarze Festung[/url] in der Scharlachroten Enklave der Östlichen Pestländer. Hier erhalten sie ihre Talentpunkte durch Questbelohnungen und bekommen sogar ein besonderes beschworenes Reittier: das [spell=48778]!\n\nTodesritter haben mehrere sehr starke Möglichkeiten zur Schadensverursachung, da jeder ihrer Talentbäume es erlaubt mit einer Vielfalt an Nahkampffähigkeiten, Zaubern und Schaden-über-Zeit verursachenden Krankheiten überragende Leistung zu erbringen. Sie sind auch sehr fähige Tanks, wobei sowohl ihr Blut- als auch ihr Frost-Talentbaum einzigartige Optionen bietet. [icon name=spell_deathknight_bloodpresence][url=?spells=7.6.770]Blut[/url][/icon] bietet mehr Selbstheilungsfähigkeiten, [icon name=spell_deathknight_frostpresence][url=?spells=7.6.771]Frost[/url][/icon] bietet erhebliche Schadensminderung und starken Flächenschaden.\n\nTodesritter kämpfen mit einem besonderen Verstärkungszauber, der [url=?spells=7&filter=na=präsenz]Präsenz[/url] genannt wird (ähnlich wie die Haltungen eines Kriegers), der ihnen besondere Boni für ihre Rollen verleiht. Todesritter verwenden ein einzigartiges Ressourcensystem, bei dem die meisten Zauber entweder [url=?spells=7.6&filter=cr=45;crs=10;crv=0#50+1+13+3]Runen[/url] kosten, die während des Kampfes wieder aufgefüllt werden, oder [url=?spells=7.6&filter=cr=45;crs=11;crv=0]Runenmacht[/url], die durch verschiedene Fähigkeiten erzeugt werden kann.\n\n[ul][li]Auf [icon name=spell_deathknight_unholypresence][url=?spells=7.6.772]Unheilig[/url][/icon] spezialisierte Todesritter können sich in [spell=52143] spezialisieren, was ihren beschworenen Ghul-Wächter zu einem permanenten Begleiter macht, der sie im Kampf unterstützt![/li][li]Die Klasse der Todesritter verfügt über eine eigene spezielle Waffenverzauberungsfähigkeit namens [spell=53428], die herkömmliche Waffenverzauberungen überflüssig macht.[/li][li]Todesritter sind eine Schadensklasse, die ihren Schaden sowohl durch Nahkampffähigkeiten als auch durch Zauber verursacht![/li][/ul]',NULL),(13,7,3,NULL,0,2,'[b][color=c7]Schamanen[/color][/b] beherrschen Elementar- und Naturmagie und bringen einer (Schlachtzugs-) Gruppe die größte Vielfalt an potenziellen Stärkungszaubern in Form von [url=?spells=7&filter=na=Totem;cl=7]Totems[/url]. Ein Schamane kann für jedes Element - Erde, Feuer, Luft und Wasser - ein Totem beschwören, welches zu seinen Füßen erscheint und allen Mitgliedern seiner (Schlachtzugs-) Gruppe in Reichweite einen Stärkungszauber verleiht. Einige Totems, insbesondere Feuer-Totems, fügen Gegnern auch Schaden zu. Der Trick beim Spielen jeder Art von Schamanen besteht darin, zu wissen, welche Totems in welcher Situation beschworen werden müssen, um den verursachten Schaden und die Überlebensfähigkeit ihrer Gruppe zu maximieren.\n\nSchamanen sind in erster Linie Zauberer, wobei ein auf [icon name=spell_nature_lightningshield][url=?spells=7.7.373]Verstärkung[/url][/icon] spezialisierter Schamane Schaden in Nahkampfreichweite verursacht. Ein solcher Schamane erlernt das Führen zweier Waffen durch [spell=30798] und kann mit [spell=51533] zwei Schattenwölfe zur Unterstützung im Kampf beschwören. Obwohl sie hauptsächlich im Nahkampf eingesetzt werden, können auf Verstärkung spezialisierte Schamanen dennoch einen gewissen Nutzen aus ihrer Zaubermacht ziehen und spontane [icon name=spell_nature_lightning][url=?spell=49238]Blitzschläge[/url][/icon] oder [url=?spells=7&filter=cr=109:12:14;crs=10:1:5;crv=0:0:60000;cl=7]Heilungen[/url] durch [icon name=spell_shaman_maelstromweapon][url=?spells=-2&filter=na=waffe+des+mahlstroms rel=spell=51532]Waffe des Mahlstroms[/url][/icon] wirken.\n\nAuf [icon name=spell_nature_lightning][url=?spells=7.7.375]Elementarkampf[/url][/icon] spezialisierte Schamanen wirken Feuer- und Blitzzauber auf Distanz und verursachen so großen Schaden. Sie können Gegner durch [spell=59159] zurückstoßen und mit [icon name=spell_shaman_stormearthfire][url=?spells=-2&filter=na=sturm%2C+erde+und+feuer rel=spell=51486]Sturm, Erde und Feuer[/url][/icon] alle Feinde in einem Gebiet festwurzeln. Außerdem gewähren sie durch [spell=57722] und [icon name=spell_shaman_elementaloath][url=?spells=-2&filter=na=Elementarer+Schwur rel=spell=51470]Elementarer Schwur[/url][/icon] begehrte Stärkungszauber für Zauberer ihres Schlachtzugs.\n\nEin auf [icon name=spell_nature_magicimmunity][url=?spells=7.7.374]Wiederherstellung[/url][/icon] spezialisierter Schamane erhält verbesserte Heilzauber und kann ein ausgezeichneter Schlachtzugs- oder Tankheiler sein. Sie sind bekannt für ihre mächtige Fähigkeit [spell=55459] und dafür, dass sie ein [spell=16190] zur Verfügung stellen, welches der Gruppe hilft Mana wiederherzustellen. Sie erhalten außerdem ein mächtiges [spell=49284], können mit [spell=51886] Flüche entfernen und verfügen durch [spell=61301] über einen Spontanheilungseffekt, der zusätzlich eine Heilung über Zeit verursacht.\n\n[ul][li]Es gibt über zwanzig verschiedene Totems, die ein Schamane erlernen kann![/li][li]Schamanen der Horde können [spell=2825] und Schamanen der Allianz [spell=32182] wirken, wodurch der verursachte Schaden und die gewirkte Heilung der gesamten Gruppe erhöht wird. Dieser Stärkungszauber ist einzigartig und in jeder Schlachtzugsgruppe sehr begehrt.[/li][li]Ein Schamane kann sich ab Stufe 16 in einen [spell=2645] verwandeln und dies mit dem Talent [spell=16287] sogar als Spontanzauber wirken. Dieser Zauber kann im Kampf eingesetzt werden, aber nicht in geschlossenen Räumen.[/li][li]Schamanen können immer nur einen Elementarschild - [spell=49281] oder [spell=57960] - gleichzeitig benutzen. Auf Wiederherstellung spezialisierte Schamanen können zudem [spell=49284] auf einen anderen Spieler wirken.[/li][/ul]',NULL),(13,8,3,NULL,0,2,'[b][color=c8]Magier[/color][/b] bändigen die Elemente Feuer, Frost und Arkan, um ihre Feinde zu vernichten oder unter Kontrolle zu halten. Dazu besitzen sie ein Arsenal voller Zauber zu unterschiedlichen Zwecken.\nStärkungszauber, [icon name=ability_mage_conjurefoodrank10][url=?spell=42956]herbeigezauberte Erfrischungen[/url][/icon] oder arkane [url=?spells=7&filter=na=Portal]Portale[/url] zur schnellen Weltreise in ferne Länder machen einen Magier zu einem idealen Weggefährten.\nUnd wenn man eine Klasse sucht, die Gegner in eine Welt des Schmerzes einführt, ist der Magier eine gute Wahl. Ihren Gegnern können Magier mit verschiedensten Schwächungszaubern die Bedingungen eines jeden Kampfes diktieren, mit Elementarblitzen massiven Schaden aus der Ferne anrichten, oder Zerstörung in einem großen Wirkungsbereich niederregnen lassen.\n\nAuf [icon name=spell_holy_magicalsentry][url=?spells=7.8.237]Arkan[/url][/icon] spezialisierte Magier haben das Potenzial, mit [icon name=spell_arcane_blast][url=?spell=42897]Arkanschlägen[/url][/icon] und [icon name=ability_mage_missilebarrage][url=?spells=-2&filter=na=Geschosssalve rel=spell=54490]Salven[/url][/icon] an [icon name=spell_nature_starfall][url=?spell=42846]Arkanen Geschossen[/url][/icon] in kurzer Zeit enormen Schaden zu verursachen. Das Bändigen der reinen arkanen Mächte hat jedoch ihre Kehrseite: einem unerfahrenen Arkanmagier verzehrt es schon nach kurzer Zeit seine gesamten Kräfte.\n\nAuf [icon name=spell_fire_flamebolt][url=?spells=7.8.8]Feuer[/url][/icon] spezialisierte Magier verfallen durch kritische Treffer mit Feuerzaubern in [icon name=ability_mage_hotstreak][url=?spells=-2&filter=na=Kampfeshitze rel=spell=44448]Kampfeshitze[/url][/icon] und äschern so ihre Gegner mit verheerenden [icon name=spell_fire_fireball02][url=?spell=42891]Pyroschlägen[/url][/icon] ein. Zudem verwandeln sie ihre Gegner in [icon name=ability_mage_livingbomb][url=?spell=55360]Lebende Bomben[/url][/icon] und verursachen dadurch explosiven Flächenschaden.\n\n[icon name=spell_frost_frostbolt02][url=?spells=7.8.6]Frost[/url][/icon]magier können ihre Gegner [icon name=ability_mage_deepfreeze][url=?spell=44572]in Eis erstarren[/url][/icon] lassen. Ihre Spezialisierung auf Kälteeffekte erlaubt ihnen eine starke Kontrolle über ihre Gegner und erhöht dadurch ihre Überlebensfähigkeit enorm.\n\n[ul][li]Magier können Erfrischungen herbeizaubern, um die Gesundheit und das Mana ihrer Verbündeten wiederherzustellen.[/li][li]Sie sind die einzige Klasse, die Portale erschaffen kann, um andere Spieler zu transportieren. Sie können jedoch keine Spieler von einem entfernten Ort herbeirufen - das ist die Aufgabe eines [class=9]![/li][li]Der verursachte Fernkampfschaden von Magiern ist einer der höchsten im Spiel und macht sie zu einem unverzichtbaren Verbündeten in jedem Schlachtzug.[/li][/ul]',NULL),(13,9,3,NULL,0,2,'[b][color=c9]Hexenmeister[/color][/b] sind Meister der dämonischen Künste. Gekleidet in Gewänder sind sie Meister im Wirken von [url=?spells=7&filter=cr=12;crs=1;crv=0;na=Fluch+der;cl=9]Flüchen[/url], dem Schleudern von Feuer- oder Schattenblitzen und der Beschwörung von [url=?spells=7&filter=cr=14;crs=6;crv=48018;na=beschwören;cl=9]Dämonen[/url] unter ihre Kontrolle zur Unterstützung im Kampf. Die Kombination ihrer Flüche und direkten Schadenszauber richten Verwüstung und Zerstörung an und machen Hexenmeister zu sehr gefürchteten Gegnern.\n\nNeben Mana als primäre Ressource können Hexenmeister Gegnern Teile ihrer [icon name=spell_shadow_haunting][url=?spell=47855]Seele stehlen[/url][/icon] und dadurch [item=6265] erzeugen. Seelensplitter ermöglichen mächtige rituelle Magie, etwa zur [icon name=spell_shadow_twilight][url=?spell=698]Beschwörung von anderen Spielern[/url][/icon] oder von [icon name=spell_shadow_shadesofdarkness][url=?spell=58887]Gesundheitssteinen[/url][/icon] mit heilenden Kräften. Insbesondere kann jedoch ein Hexenmeister mit ihnen die Seele eines Verbündeten in einem [icon name=spell_shadow_soulgem][url=?spell=47884]Seelenstein[/url][/icon] speichern, sodass dieser im Todesfall sich selbst wiederbeleben kann.\n\n[ul][li]Hexenmeister können durch ein Ritual der Beschwörung ein Portal erschaffen, um einen anderen Spieler an den Ort des Portals zu beschwören.[/li][li]Sie können Gesundheitssteine beschwören, die den Anwender heilen.[/li][li]Die Flüche eines Hexenmeisters können ihre Feinde schwächen oder ihnen Schaden zufügen.[/li][/ul]',NULL),(13,11,3,NULL,0,2,'[b][color=c11]Druiden[/color][/b] sind die "Alleskönner"-Klasse in World of Warcraft - das heißt, sie können in einer Vielzahl von verschiedenen Rollen agieren und bieten daher einen der vielfältigsten Spielstile. Durch das [i]Annehmen der Gestalt von verschiedenen Kreaturen[/i] kann der Druide heilen, Schaden im Nah- und Fernkampf verursachen oder als Tank agieren. Mit steigenden Stufen kann der Druide neue, immer mächtigere Gestaltwandlungen erlernen, um sich in eine Kreatur passend zu seiner Rolle zu verwandeln.\n\nAuf niedrigeren Stufen wird ein Druide in seiner humanoiden Gestalt heilen oder im Fernkampf Schaden verursachen. Auf späteren Stufen jedoch erhalten Druiden durch die spezialisierten Talentbäume Zugang zu zwei besonderen Gestalten für jede unterschiedliche Rolle.\n\nAuf [icon name=spell_nature_healingtouch][url=?spells=7.11.573]Wiederherstellung[/url][/icon] spezialisierte Druiden erlernen den [spell=33891], der die Manakosten ihrer Heilzauber reduziert und jegliche Heilung auf ihre Verbündeten verstärkt.\nAuf [icon name=spell_nature_starfall][url=?spells=7.11.574]Gleichgewicht[/url][/icon] spezialisierte Druiden verursachen Schaden im Fernkampf und erlernen die [spell=24858], die ihre Rüstung sowie die Chance auf kritische Treffer mit Zaubern bei ihnen und ihren Verbündeten erhöht.\nEs gibt auch zwei Druidenformen für den [icon name=ability_racial_bearform][url=?spells=7.11.134]Wilden Kampf[/url][/icon]. Zum einen die mächtige [spell=5487] (und [spell=9634] ab einer höheren Stufe) - eine auf das Tanken ausgelegte Gestalt, die zusätzliche Rüstung, Gesundheit und Zugang zu einem Arsenal von Fähigkeiten zur Erhöhung der Bedrohung und Schadensverminderung gewährt. Zum anderen die schurkenähnliche [spell=768], die erheblichen Nahkampfschaden verursachen kann.\n\n[ul][li]Druiden erlernen ihre verschiedenen Gestalten durch das Abschließen von Quests oder durch Training. Einige Gestalten können nur durch Talente erlernt werden.[/li][li]Es gibt einige Gestalten, die alle Druiden erlernen können. Die Bärengestalt erhält man ab Stufe 10, die [spell=1066] und [spell=783] ab Stufe 16, die Katzengestalt ab Stufe 20 und die Terrorbärengestalt ab Stufe 40.[/li][li]Druiden haben sogar ihre eigene fliegende Reisegestalt: die [spell=33943] kann ab Stufe 60 und die [spell=40120] ab Stufe 71 erlernt werden, sofern der Spieler bereits [icon name=spell_nature_swiftness][url=?spell=34093]Gekonntes Reiten[/url][/icon] erlernt hat.[/li][li]Einige Druidengestalten können nur über Talente erlernt werden - die Mondkingestalt kann ab Stufe 40 erlernt werden, wenn ein Spieler viele Talentpunkte im Gleichgewicht-Talentbaum verteilt, und Baum des Lebens ab Stufe 50, wenn er viele Talentpunkte im Wiederherstellung-Talentbaum verteilt.[/li][li]Druiden haben ihre eigene, klassenspezifische [icon name=spell_arcane_teleportmoonglade][url=?spell=18960]Teleportationsfähigkeit[/url][/icon], die es ihnen erlaubt, zur [zone=493] zu reisen - praktisch, wenn sie trainieren müssen![/li][li]Druiden in (Terror-) Bärengestalt oder Katzengestalt schwingen zur Verursachung von Nahkampfschaden keine Waffen. Stattdessen erhalten sie einen speziellen Wert für jede ausgerüstete Nahkampfwaffe: die "Angriffskraft in Tiergestalt". Dieser Wert ist eine Umwandlung des "Schaden pro Sekunde"-Wertes einer Waffe in einen Wert, der Angriffskraft verleiht und den verursachten Schaden des Druiden in Katzen- oder (Terror-) Bärengestalt beeinflusst.[/li][/ul]',NULL); +INSERT INTO `aowow_articles` VALUES (13,4,0,NULL,0,2,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]',NULL),(14,1,0,NULL,0,2,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.',NULL),(13,1,0,NULL,0,2,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]',NULL),(13,2,0,NULL,0,2,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]',NULL),(14,2,0,NULL,0,2,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.',NULL),(13,3,0,NULL,0,2,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]',NULL),(13,5,0,NULL,0,2,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]',NULL),(13,6,0,NULL,0,2,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]',NULL),(13,7,0,NULL,0,2,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]',NULL),(13,8,0,NULL,0,2,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]',NULL),(13,9,0,NULL,0,2,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]',NULL),(13,11,0,NULL,0,2,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]',NULL),(14,3,0,NULL,0,2,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.',NULL),(14,4,0,NULL,0,2,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.',NULL),(14,5,0,NULL,0,2,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.',NULL),(14,6,0,NULL,0,2,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.',NULL),(14,7,0,NULL,0,2,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.',NULL),(14,8,0,NULL,0,2,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.',NULL),(14,10,0,NULL,0,2,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.',NULL),(14,11,0,NULL,0,2,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.',NULL),(8,21,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=15], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.',NULL),(8,47,0,NULL,0,2,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.',NULL),(8,54,0,NULL,0,2,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.',NULL),(8,59,0,NULL,0,2,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]',NULL),(8,68,0,NULL,0,2,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.',NULL),(8,69,0,NULL,0,2,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.',NULL),(8,70,0,NULL,0,2,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.',NULL),(8,72,0,NULL,0,2,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].',NULL),(8,76,0,NULL,0,2,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.',NULL),(8,81,0,NULL,0,2,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.',NULL),(8,87,0,NULL,0,2,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].',NULL),(8,92,0,NULL,0,2,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.',NULL),(8,93,0,NULL,0,2,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.',NULL),(8,270,0,NULL,0,2,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.',NULL),(8,349,0,NULL,0,2,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].',NULL),(8,369,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.',NULL),(8,470,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.',NULL),(8,471,0,NULL,0,2,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.',NULL),(8,509,0,NULL,0,2,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].',NULL),(8,510,0,NULL,0,2,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].',NULL),(8,529,0,NULL,0,2,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.',NULL),(8,530,0,NULL,0,2,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].',NULL),(8,576,0,NULL,0,2,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]',NULL),(NULL,NULL,0,'commenting-and-you',0,2,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'item-comparison',0,2,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'stat-weighting',0,2,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'screenshots-tips-tricks',0,2,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(NULL,NULL,0,'profiler',0,2,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(NULL,NULL,0,'talent-calculator',0,2,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(NULL,NULL,0,'modelviewer',0,2,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(NULL,NULL,0,'tooltips',0,2,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'searchbox',0,2,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(NULL,NULL,0,'searchplugins',0,2,'[menu tab=2 path=2,8]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.png border=0 margin=5 float=left]Firefox[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-firefox.png][/div]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n if (typeof window.external.AddSearchProvider == "function")\r\n window.external.AddSearchProvider("STATIC_URL/download/searchplugins/aowow.xml");\r\n else\r\n alert("This feature is unavailable.");\r\n}\r\n[/script]\r\n[pad]\r\nEither\r\n[ul]\r\n[li]Click on the button below to install the search plugin in your browser or[/li]\r\n[li]Right-click your address bar and then clck on "Add AoWoW" or[/li]\r\n[li]Click on the [img src=STATIC_URL/images/icons/add.png border=0] on the browser search bar and then on [img src=STATIC_URL/images/icons/add.png border=0] "Add search engine"[/li]\r\n[/ul]\r\n\r\n[pad]\r\n[html]Install pluginInstall plugin[/html]\r\n[div clear=both][/div]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/edge.png border=0 float=left][img src=STATIC_URL/images/help/searchplugins/chrome.png border=0 float=left]MS Edge / Google Chrome[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-edge.png][/div]\r\n[pad]\r\nFor Chrome-based browsers go to settings and fill in the add search engine form as shown.\r\n[pad]\r\n[div width=500px]\r\n[pre]HOST_URL/?search=%s[img src=STATIC_URL/images/icons/pages.gif float=right][/pre]\r\n[/div]\r\n[script]\r\nsetTimeout(() => $WH.clickToCopy($WH.qs("pre > img"), "HOST_URL/?search=%s"), 100);\r\n[/script]\r\n[pad]\r\nSave your changes, and you\'ll be able to perform Aowow searches by typing "db" followed by the search terms in the address bar (e.g. db sword).\r\n[div clear=both][/div]\r\n',NULL),(NULL,NULL,2,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(NULL,NULL,0,'faq',0,2,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(NULL,NULL,0,'whats-new',0,2,'[small]this page for example[/small]',NULL),(NULL,NULL,0,'aboutus',0,2,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(NULL,NULL,3,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(NULL,NULL,6,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(NULL,NULL,0,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(NULL,NULL,0,'markup-guide',0,2,'[menu tab=2 path=2,13,7]Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: en (default), de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,NULL,0,2,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,NULL,0,2,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,NULL,0,2,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,NULL,0,2,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,NULL,0,2,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,NULL,0,2,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,NULL,0,2,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,NULL,0,2,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,NULL,0,2,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,NULL,0,2,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,NULL,0,2,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,NULL,0,2,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,NULL,0,2,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,NULL,0,2,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,NULL,0,2,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,NULL,0,2,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,NULL,0,2,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,NULL,0,2,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,NULL,0,2,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,NULL,0,2,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,NULL,0,2,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,NULL,0,2,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,NULL,0,2,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,NULL,0,2,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,NULL,0,2,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,NULL,0,2,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,NULL,0,2,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,NULL,0,2,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,NULL,0,2,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,NULL,0,2,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,NULL,0,2,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL),(NULL,NULL,0,'sound&playlist',0,2,'Here you can set up a playlist of sounds and music. \n\nJust click the \"Add\" button near an audio control, then return to this page to listen to the list you\'ve created.',NULL),(14,11,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Draenei[/b] sont des adeptes de Naaru et adorateurs de la Lumière Sainte. Chassées d’Argus, leur monde natal, les honorables Draeneï durent fuir des siècles durant Sargeras et sa Légion Ardente, après qu’il ait essayé de les corrompre. Les Draeneï ont alors trouvé une lointaine planète où s’établir. Ils appelèrent Draenor ce monde qu’ils partageaient avec les Orcs chamaniques. Une période de paix s’est alors installée.\nLa Légion Ardente fini par retrouver les DraeneÏ et corrompt les Orcs grâce à Guldan. Les Orcs partirent en guerre et exterminèrent les paisibles Draeneï. De rares survivants purent s’enfuir en Azeroth pour chercher de l’aide dans leur combat contre la Légion Ardente.\n\n[b]Capitale :[/b] Les Draeneï ont le siège de leur pouvoir dans les ruines de leur vaisseau : [zone=3557].\n\n[b]Zone de départ :[/b] [zone=3524] et [zone=3525] couvrent les tentatives des Draeneï de s’installer sur leurs nouvelles îles et de faire face à la corruption présente.\n\n[b]Montures :[/b] [npc=17584] vend des variétés d’Elekks, ainsi que [npc=33657] au tournoi d’Argent.',NULL),(14,8,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Trolls[/b] Sombrelance vécurent à l\'origine dans les îles Brisées mais furent envahis par les nagas et les murlocs. Chassés de chez eux, la [url=?faction=530]tribu de Sombrelance[/url] se lie finalement d\'amitié avec les orcs qui ont sauvés les Trolls de la destruction. [npc=4949] leur offre l\'amnistie parmi la Horde, en contrepartie, la tribu Sombrelance jura fidélité au chef de guerre orque.\nBien qu\'ils refusent d\'abandonner leur sombre héritage, les féroces Trolls Sombrelance occupent une place d\'honneur au sein de la Horde.\n\n[b]Capitale :[/b] Les Trolls Sombrelance vivent maintenant dans la capitale de la Horde : [zone=1637].\n\n[b]Zone de départ :[/b] Les Trolls commencent leurs quêtes en [zone=14]\n\n[b]Montures :[/b] [npc=7952] au village de Sen\'jin vend de nombreux raptors ; [npc=33554], au tournoi d\'Argent, vend quelques modèles distincts.',NULL),(14,10,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Hauts-Elfes[/b], race fière et hautaine, fondèrent jadis Quel’Thalas où ils créèrent une fontaine magique appelée Puits de Soleil. Ils profitèrent de sa puissance mais devinrent peu à peu dépendants de la magie. Si celle-ci devait être enlevée, les Hauts-Elfes soufreraient horriblement. Ils se séparèrent donc du reste de la société elfique.\nDe nombreux siècles plus tard, le fléau mort-vivant détruisit le Puit de Soleil et tua la plupart des Hauts-Elfes. Les survivants de l’assaut d’Arthas sur Lune-d’Argent, qui ont alors pris le nom d’Elfes de Sang, rebâtissent Quel’Thalas et cherchent de nouvelles sources de magie pour calmer leur douloureux manque.\nLes Elfes de Sang rejoignent la Horde à Burning Crusade.\n\n[b]Capitale :[/b] Les Elfes de Sang ont reconstruit [zone=3487].\n\n[b]Zone de départ :[/b] Les Elfes de Sang commencent au [zone=3430].\n\n[b]Montures :[/b] [npc=16264], aux Bois des Chants Eternelles, vend de nombreux faucons pèlerins ; [npc=33557], au tournoi d’Argent, vend quelques modèles uniques.',NULL),(14,7,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Gnomes[/b], race excentrique, sont obsédés par les gadgets et la technologie. Malgré leur petite taille, ils ont mis à profit leur grande intelligence pour s\'assurer une place dans l\'Histoire.\nA l\'origine, les Gnomes viennent de la ville de [zone=721], qui était autrefois une merveille technologique mue à la vapeur. Malheureusement, la ville a été détruite par [npc=7937] à la suite d\'une tentative pour sauver la ville d\'une armée massive de Troggs.\nSes bâtisseurs sont désormais des vagabonds qui errent sur les terres des nains, venant en aide à leurs alliés du mieux qu\'il le peuvent.\n\n[b]Capitale :[/b] Aujourd\'hui, les Gnomes font leurs maisons à [zone=1537] malgré les efforts fournis pour reprendre leur bien aimée ancienne ville avec l\'[achievement=4786].\n\n[b]Zone de départ :[/b] Les Gnomes commencent à [zone=1], mais ont une séquence de quêtes très différente des Nains, couvrant Gnomeregan\n\n[b]Montures :[/b] [npc=7955] à Dun Morogh vend de nombreux mécanotrotteurs, ainsi que [npc=33650] au tournoi d\'Argent.',NULL),(14,6,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Taurens[/b], race aux racines chamaniques profondes, sont des résidents de longue date de Kalimdor. Ils vouent un amour profond et durable à la nature, la grande majorité d’entre eux adorent une divinité connue sous le nom de la Terre Mère.\nRécemment attaqués par des centaures, les Taurens auraient été exterminés s’ils n’avaient pas rencontré, par hasard, les Orcs qui les aidèrent à repousser leurs ennemis. Afin d’honorer cette dette de sang, les Taurens ont rejoint la Horde, renforçant ainsi l’amitié entre les deux races.\n\n[b]Capitale :[/b] [zone=1638] est le lieu de résidence des Taurens\n\n[b]Zone de départ :[/b] Les Taurens commencent leurs quêtes en [zone=215].\n\n[b]Montures :[/b] [npc=3685] vend de nombreux kodos ; [npc=33556], au tournoi d’Argent, vend quelques modèles distinctifs.',NULL),(14,5,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Réprouvés[/b], résultat d’une première attaque du Fléau en Azeroth, sont une métamorphose d’un certain nombre de membres de l’Alliance en mort vivant. Quand les forces combinées des Orcs, des Elfes, des Trolls, des Nains et des Humains se mirent à se défendre, [npc=36597] se mit à affaiblir ses armées en perdant le contrôle de certaines. Libérés de l’emprise du Roi Liche ainsi que des émotions gênantes et des liens de leurs vies humaines, les Réprouvés, menés par la banshee Sylvanas, réclament vengeance contre le fléau.\nLes Humain sont également devenus des ennemis, impitoyables dans leur désir de purger les terres de tous les mort-vivants. \nLes Réprouvés ne se soucient que très peu de leurs alliés. La Horde ne représente à leurs yeux qu’un simple outil qui pourrait servir leurs sombres desseins.\n\n[b]Capitale :[/b] Les Réprouvés résident sous les ruines de l’ancienne ville humaine de Lordaeron : la [zone=1497].\n\n[b]Zone de départ :[/b] Tous les joueurs de Réprouvés commencent dans la [zone=85]. Ils sont élevés par les Val’kyrs comme des réprouvés de seconde génération\n\n[b]Montures :[/b] [npc=4731] vend de nombreux chevaux mort-vivants ; [npc=33555], au tournoi d’Argent, vend quelques modèles distincts.',NULL),(14,4,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Elfes de la nuit[/b], race ancienne et mystérieuse, vivaient à Kalimdor pendant des milliers d\'années, ils fondèrent un vaste empire, mais leur usage imprudent de la magie les conduisit à leur perte. Pétris de douleur, ils se retirèrent dans les forêts et demeurèrent ainsi isolés jusqu\'au retour de leur ancien ennemi. Ne disposant d\'aucune alternative, les Elfes de la nuit furent contraints de sacrifié l\'arbre monde afin d\'arrêter l\'avancé de la Légion Ardente. \nIls émergèrent de leur isolement, afin de défendre leur place dans le nouveau monde.\n\n[b]Capitale :[/b] La capitale des Elfes de la nuit est [zone=1657], située dans les branches de l\'arbre monde.\n\n[b]Zone de départ :[/b] Les Elfes de la nuit commencent à [zone=141]\n\n[b]Montures :[/b] [npc=4730], à Darnassus, vent une variété de sabre de nuit, ainsi que [npc=33653] au tournoi d\'Argent.',NULL),(14,3,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Nains[/b], race robuste, viennent de Khaz Modan dans les Royaumes de l’Est. Par la passé, les Nains ne s’intéressaient qu’aux richesses extraites des profondeurs de la terre. Lorsque des études semblèrent indiquer que les Nains étaient les descendants d’une race proche des Titans qui leur aurait conféré un héritage enchanté, la curiosité des Nains fut piquée au vif. Décidés à en savoir plus, les Nains commencèrent à rechercher des artefacts perdus et des connaissances disparues. Aujourd’hui, les Nains dirigent des fouilles archéologiques partout dans le monde.\nTrois principaux Clans de Nains sont répartis dans tout Azeroth : Les Barbes de Bronze, Les Marteaux Hardis et les Sombrefers.\n\n[b]Capitale :[/b] Les Nains font leur maison dans leur siège ancestral de [zone=1537].\n\n[b]Zone de départ :[/b] Les Nains commencent à [zone=1].\n\n[b]Montures :[/b] [npc=1261] vend des béliers à la ferme des Amberstill, ainsi que [npc=33310] au tournoi d’Argent.',NULL),(14,1,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Humains[/b], race la plus jeune et la plus peuplés d\'Azeroth, maîtrisent les arts du combat, l\'artisanat et la magie avec une efficacité stupéfiante. La valeur et l\'optimisme des Humains les ont conduits à bâtir certains des plus grands royaumes du monde. En cette ère de troubles, après des générations de conflit, l\'Humanité aspire à ranimer sa gloire passée et à se forger un nouvel avenir rayonnant.\nLes Humains, aux talents très variés, sont devenus les chefs de l\'Alliance grâce à leurs ambitions et leurs résiliences. \n \n[b]Capitale :[/b] Le siège du pouvoir Humain est dans la ville reconstruite de [zone=1519].\n \n[b]Zone de départ :[/b] Les Humains commencent leurs quêtes dans la [zone=12].\n \n[b]Montures :[/b] [npc=384] vend des palefrois dans Hurlevent, et [npc=33307], au tournoi d’Argent, vend quelques modèles distincts.',NULL),(14,2,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Orcs[/b] étaient, à l\'origine, un peuple pacifique aux croyances chamaniques résidant sur le monde de Draenor. Malheureusement, infectés par le sang démoniaque de Mannoroth le destructeur, les Orcs furent réduit en esclavage par la Légion Ardente, contraint de guerroyer contre les Draenei et de conquérir Azeroth. \nAprès de nombreuse années de joug, les Orcs ont réussi à se libérer de l\'emprise démoniaque et ont conquis leur liberté, pour revenir à leurs racines chamaniques.\nMaintenant, sous la direction de leur nouveau chef de guerre, les Orcs se construisent un nouveau foyer, où ils combattent pour l\'honneur, dans un monde étranger, haïs et calomniés.\n\n[b]Capitale :[/b] Les Orcs résident maintenant dans la ville d\'[zone=1637], du nom du défunt Orgrim Doomhammer, ancien chef de guerre de la Horde.\n\n[b]Zone de départ :[/b] Les Orcs commencent leurs quêtes en [zone=14].\n\n[b]Montures :[/b] [npc=3362], à Orgrimmar, vend une variété de loups ; [npc=33553], au tournoi d\'Argent, vend quelques montures distinctives',NULL),(NULL,NULL,0,'reputation',0,2,'[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPLOAD reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n',NULL),(NULL,NULL,0,'privilege=1',0,2,'[h3]Reputation required for posting comments?[/h3]\nThe very first privilege you can earn is the ability to post comments. Because this privilege requires only CFG_REP_REQ_COMMENT reputation, it is earned soon upon registering an account (which awards CFG_REP_REWARD_REGISTER reputation)! Keep this in mind if you\'ve recently registered to post on a contest thread.\n\n[h3]How do I post a comment?[/h3]\nOnce you have earned the ability to post comments, it\'s easy to do! Got some interesting information about an item? Strategies for earning an achievement or killing a boss? These are just a few examples of what could make a quality comment here!\n\nSimply visit any database page that you wish to leave a comment on and scroll down to the \'Contribute\' section. In the \'Add your comment\' tab, you can easily write and format your database comment. You can use our handy formatting buttons to improve the visual quality of your post, and easily add database links using the \'Links\' menu and entering database entry IDs. Once you\'re done, simply click the \'Submit\' button below and voila!\n\n[h3]Comment rating and you![/h3]\nAll comments made on database pages are subject to our rating system. This allows users who have reached the appropriate reputation level to upvote and downvote comments based on their quality. Making quality comments will earn you website reputation each time it has been upvoted, but make a poor quality comment and you may end up losing reputation if it is downvoted!\n\nFor more information on commenting, be sure to check out our handy [url=?help=commenting-and-you]Commenting and You[/url] guide in the website help section!',NULL),(NULL,NULL,0,'privilege=2',0,2,'[h3]Posting External Links[/h3]\nOne of the first privileges allowed to users is the ability to post external links on the site. This will allow you to link to relevant information found on other websites from our database as well as in our forums. You can also add a link to your user profile, such as to your guild website or personal blog. Users without the appropriate reputation level will have their links filtered automatically, to help prevent spammers and malicious links from being posted on our website.\n\n[h3]Posting Policy[/h3]\nPlease be aware that some URLs may still be filtered out by our moderation team, as they made be deemed inappropriate or advertising. If you are uncertain whether or not a link will be considered advertisement, please do not hesitate to contact our Feedback team with any questions!\n',NULL),(NULL,NULL,0,'privilege=4',0,2,'[h3]No CAPTCHAs[/h3]\nAh, CAPTCHAS. Love \'em or hate \'em, they\'re often a necessary evil for popular websites which allow any sort of user contribution. Here, we use [url=https://www.google.com/recaptcha/intro/index.html]ReCAPTCHA[/url] which helps thwart bots and spammers from abusing our forum and comment systems. Unfortunately, this also creates a minor inconvenience for our more active users, who are still occasionally asked to input a CAPTCHA despite long since establishing themselves as a legitimate member of the community. Well, not anymore! Users who reach the appropriate reputation level will no longer have to enter CAPTCHAs anywhere on the site!\n',NULL),(NULL,NULL,0,'privilege=5',0,2,'[h3]Comment rating value increase[/h3]\nWhen you have reached a higher reputation level, your contributions to the site will raise in value! As a more trusted member of our community, your comment ratings will now have an increased weight and, as a result, have a greater effect on the total rating of a comment! Your vote contribution are doubled, so each of upvote will count as two votes (and each of your downvotes as two, as well)! This will allow higher reputation users to have more of an effect on considering quality of a comment, raising quality comments higher and lowering poor comments faster.\n',NULL),(NULL,NULL,0,'privilege=9',0,2,'[h3]More votes per day[/h3]\nWe have a daily cap for comment votes set to CFG_USER_MAX_VOTES.\n\nThis privilege instantly increases the cap by 1, and then increases the cap by an additional 1 point for each CFG_REP_REQ_VOTEMORE_ADD reputation you have above CFG_REP_REQ_VOTEMORE_BASE.\n',NULL),(NULL,NULL,0,'privilege=10',0,2,'[h3]Upvoting Comments[/h3]\nDid you find a comment particularly insightful or laugh out loud funny? Upvote it then! Upvoting is a way of giving props to those who truly contribute. From small guides to witty jokes, if a comment has enhanced your user experience, you should remember to upvote it.\n\nThe higher amount of upvotes a comment has, the higher up on the page it is. This way the community can help determine what comments are worth reading by sending some upvotes their way.\n\n[h3]Upvoting Policy[/h3]\nYou should not use upvotes to reward your friends or withhold upvotes to punish users you dislike. These are bannable offenses and you will probably lose your ability to upvote if we catch you doing it.\n',NULL),(NULL,NULL,0,'privilege=11',0,2,'[h3]Downvoting Comments[/h3]\nDid you find a comment that was out of date, irrelevant, or otherwise less than useful? Downvote it then! Downvoting is a way of removing the clutter from the database and ensuring our comments are up to date. Downvotes remove an upvote--and if a comment has too many downvotes, it can even become a negative comment which appear at the end of an article rather than the beginning. \n\n[h3]Downvoting Policy[/h3]\nYou should not use downvotes to punish users you dislike nor should you downvote in quick succession. Try to use downvotes only to help us out, leaving personal bias out of it. If you abuse downvotes either by making too many in a short time frame or targeting a specific user, you may be warned and in some cases banned.\n',NULL),(NULL,NULL,0,'privilege=12',0,2,'[h3]Replying to a Comment[/h3]\nYou can reply to comments easily and quickly with the new commenting system. All you have to do is leave a reply on an existing comment for this to work.\n\nA reply is best used to illustrate alternatives to a comment, highlight its accuracy, or expand on a joke. For example, if someone says an item drops from a certain boss but you know it does not, you could reply to explain it doesn\'t; it\'s likely people will find your comment helpful so they don\'t waste time trying to get the item from that NPC.\n\nPlease be aware that you should not use comments like forum threads for discussion.\n',NULL),(NULL,NULL,0,'privilege=13',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an uncommon-quality green border.',NULL),(NULL,NULL,0,'privilege=14',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a rare-quality blue border.',NULL),(NULL,NULL,0,'privilege=15',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an epic-quality purple border.',NULL),(NULL,NULL,0,'privilege=16',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a legendary-quality orange border.',NULL),(NULL,NULL,0,'privilege=17',0,2,'[img src=STATIC_URL/images/premium/user-badge.png border=0 float=right]Unlock [url=HOST_URL/?premium]AoWoW Premium[/url] status for free.\n\nAs a Premium user, you can access a variety of perks:\n[ul]\n[li]Images in tooltips[/li]\n[li]Additional avatar borders[/li]\n[li]And much more![/li][/ul]\n\n',NULL),(13,1,2,NULL,0,2,'[b][color=c1]Les Guerriers[/color][/b] sont une classe très puissante, avec la capacité de taner ou d\'infliger des dégâts de mêlée. Sa caractéristique principale est la force, mais les tanks s\'intéresseront également à l\'Endurance.\n\nCe combattant se bat avec une posture ce qui lui permet l\'accès à différentes capacités et lui accorde des bonus. Il utilisera [spell=71] pour tanker (appris au niveau 10) et [spell=2457] (appris au niveau 1) ou [spell=2458] (appris au niveau 30) pour les dégâts en mêlée.\n\nL\'arbre de protection du Guerrier contient de nombreux talents pour améliorer leur survie et générer des menaces contre les monstres. Les Guerriers de protection sont l\'une des principales classes de tank du jeu. Pour aller au combat, ils peuvent utiliser [spell=100] ou [spell=20252] mais seul le Guerrier protection peut protéger un allié en utilisant [spell=3411].\nIls ont également deux arbres de talent orientés sur les dégâts [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Armes[/url][/icon] et [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], ce dernier comprend le talent [spell=46917], qui permet au Guerrier de manier deux armes à deux mains. Les Guerriers sont capable de faire de gros dégâts de zone avec des sorts tels que [spell=845], [spell=1680] et [spell=46924]. \n\nLe Guerrier porte une armure en plaques et aspire à la perfection dans les combats. Lorsqu\'il inflige ou subit des dégâts, il génère de la rage, utilisée pour alimenter ses attaques spéciales.\n[ul]\n[li] Allié utile, qui peut ajouter des buffs au groupe ou raid avec [spell=6673] et [spell=469], mais seul les Guerriers Fury peuvent fournir un buff passif [spell=29801] qui augmente les coups critiques en mêlée et à distance.[/li]\n[li] L\'avantages uniques des Guerriers, ce sont les 3 postures de combats.[/li]\n[li] Il peut choisir de se spécialiser dans le port d’armes à deux mains, d\'arme à une main, ou dans l\'utilisation du bouclier en plus d\'une arme à une main.[/li]\n[li] Et dispose de plusieurs techniques qui permettent de se déplacer rapidement sur le champ de bataille.[/li]\n[/ul]',NULL),(13,2,2,NULL,0,2,'[b][color=c2]Les Paladins[/color][/b] sont des combattants qui utilisent la magie du sacré pour soigner les blessures et combattre le mal. Ils sont relativement autonomes et disposent de nombreuses techniques destinées à empêcher les morts. Le paladin peut choisir de se battre, de protégés ou de soigner, il utilisera le mana pour combattre le mal. Ses caractéristiques principales dépendent du rôle choisi.\n\nIl est un mélange d’un combattant en mêlée et d’un lanceur de sorts secondaires. Allié indispensable dans un combat, il renforce leurs amis avec de saintes auras (une aura active par paladin sur chaque membre du raid) et des bénédictions spécifiques pour les protéger du mal et renforcer leurs pouvoirs.\n\nPortant de lourdes armures, ils peuvent résister à des coups terribles dans les batailles les plus dures tout en guérissant leurs alliés blessés et en ressuscitant les morts. Au combat, ils peuvent utiliser des armes à deux mains, paralyser leurs ennemis, détruire des morts vivants et des démons, et les juger avec une sainte vengeance.\nLes paladins sont une classe défensive, principalement conçus pour survivre à leurs adversaires, grâce à leur assortiment de capacités défensives. Ils font aussi d’excellents tanks en utilisant leurs capacités [spell=25780].\n\n[ul]\n[li] Classe pouvant guérir, tanker avec leur précieux bouclier et infliger des dégâts en mêlée.[/li]\n[li] Renforce les alliées avec les [url=spells=7.2&filter=na=aura]Auras[/url], les [url=spells=7.2&filter=na=bénédiction]bénédictions[/url] et d’autres buffs.[/li]\n[li] Seule classe avec un véritable sort d’invulnérabilité [spell=642].[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=13819] est un destrier royal que seuls les plus fervents des paladins peuvent appeler à leur service. Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23214] est un équipier infatigable capable d\'amener son valeureux maître dans tout Azeroth. Niveau 40 - Bonus de vitesse de 100%. [/li]\n[/ul]',NULL),(13,4,2,NULL,0,2,'[b][color=c4]Les Voleurs[/color][/b] sont une classe de mêlée capable d\'infliger de grandes quantités de dégâts à leurs ennemis avec des attaques rapides en utilisant de l\'énergie comme ressources. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Voleurs ont un puissant arsenal de compétences, dont beaucoup sont renforcés par leur capacité de furtivité et d\'étourdissement de leurs victimes. Capables d\'utiliser des poisons, ils paralysent leurs adversaires, les affaiblissant massivement dans la bataille. Avec l\'ambidextrie, ils peuvent utiliser une large gamme d\'armes, mais les Voleurs privilégient la dague, qui est la plus représentative de cette classe. \n\nCe sont les maîtres pour se déplacer furtivement autour de leurs ennemis, frapper dans l\'ombre un adversaire pour tenter de l\'achever rapidement puis s\'échapper du combat en un clin d’œil. \nIls endossent donc souvent le rôle d\'assassin ou d\'éclaireur, mais nombre d\'entre eux sont des loups solitaires.\n\n[ul]\n[li] Porte des armures en cuir.[/li]\n[li] Porte une arme dans chaque main.[/li]\n[li] Utilise une grand variété d\'armes de mêlée, comme les poignards, les armes de pugilats, les masses à une main, les épées à une main et les haches à une main.[/li]\n[li] Recouvre leurs armes avec du [url=items=0.-3&filter=na=poison;ub=4]poison[/url] pour gravement affaiblir leurs ennemis.[/li]\n[li] Utilise le [spell=1784] pour n’être visible que par les ennemis les plus perspicaces.[/li]\n[li] Cumule 5 points de combo pour infliger de puissants coups de grâce.[/li]\n[/ul]',NULL),(13,3,2,NULL,0,2,'[b][color=c3]Les Chasseurs[/color][/b] sont une classe très unique dans le monde de World of Warcraft. C\'est la seule classe non-magique qui fait des dégâts à distance. Ils se battent avec des arcs, des armes à feu ou des arbalètes. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Chasseurs se sentent chez eux dans la natures et ont une affinité spéciale avec les animaux. Il sait apprivoiser son propre [url=pets]familier[/url] qui l\'aidera à vaincre son ennemi. L\'animal du chasseur est unique, il possède un arbre de talent où le Chasseur peut attribuer des points dans des compétences diverses et des capacités passives. Chaques espèces de familier a une capacité spéciale unique. Le Chasseur peut rechercher les bêtes les plus appréciables en fonction de leurs apparences ou capacités. Seuls certains familiers ne sont accessibles que si le Chasseur choisi dans son arbre de talent [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Maîtrise des bêtes[/url][/icon] qui lui donne accès aux bêtes « exotique » tels que [pet=46] ou [pet=39].\n\nPendant que leurs familiers attaques, les Chasseurs font pleuvoir leurs projectiles sur leurs malheureuses cibles. Ils préfèrent s’évader du corps-à-corps et ralentir leurs ennemis pour s\'éloigner et lancer leurs salves mortelles. Ils sont aussi capable de poser des pièges pour infliger des dégâts, ralentir ou rendre impossible toutes actions de leurs ennemis.\n\nLes Chasseurs portent des armures intermédiaires (cuir/maille) et utilisent le mana pour faire des dégâts.\n[ul]\n[li] Il peut voyager très vite en utilisant [spell=13161] et le partager avec [spell=13159].[/li]\n[li] Ils ont un certain nombre de compétence accès sur la survie qu\'ils peuvent utiliser pour échapper ou éviter un danger potentiel, comme [spell=5384] et [spell=781].[/li]\n[li] Les Chasseurs spécialisés dans la [icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survie[/url][/icon] peuvent avoir [spell=53292], ce qui leur permet de fournir aux membres du raid le [spell=57669].[/li]\n[/ul]',NULL),(13,5,2,NULL,0,2,'[b][color=c5]Les Prêtres[/color][/b] sont généralement considérés comme l\'une des classes de soins les plus répandus dans World of Warcraft, car ils ont deux arbres de talents qui peuvent être utilisés pour guérir très efficacement. Les caractéristiques principales sont la puissance des sorts, l\'intelligence et l\'Esprit (s\'il s\'est spécialisé dans les soins).\n\nL\'arbre [icon name=spell_holy_holybolt][url=spells=7.5.56]Sacré[/url][/icon] comprend des talents qui renforcent fortement la guérison faite à leurs alliés, y compris des sorts qui peuvent être utilisés pour guérir plusieurs joueurs à la fois, comme [spell=48089]. \nL\'arbre de talent [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] se concentre principalement sur l\'absorption et l\'atténuation des dommages grâce à l\'utilisation de [spell=48066] et réduit les dégâts subis avec [spell=63944].\n\nLes Prêtres disposent d\'une grande palette d\'outils pour soigner, mais ils peuvent également sacrifier leurs soins pour infliger des dégâts grâce à la magie de l\'[icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Ombre[/url][/icon]. Ils sont alors capables d\'infliger des dégâts importants avec leurs capacités uniques et une fois qu\'ils se mettent en [spell=15473], leurs dégâts d\'ombre augmentent de manière significative tout en perdant la capacité de lancer des sorts du sacré.\n\nIl porte une armure en tissus, soigne les dégâts grâce à la magie du sacré mais inflige des dégâts grâce à la magie de l\'Ombre. Il utilise le mana comme ressource.\n[ul]\n[li] Fournissant les buffs les plus appréciés dans le jeu - [spell=48161], qui donne un buff d\'endurance indispensable à tout raid. Ils peuvent utiliser [spell=48073] et [spell=48169].[/li]\n[li] Les prêtres d\'ombre sont très sollicités dans n\'importe quel raid , fournissant le buff [spell=57669] pour stimuler la régénération de mana et peut même guérir leur propre groupe avec [spell=15286].[/li]\n[/ul]',NULL),(13,8,2,NULL,1,2,'[b][color=c8]Les Mages[/color][/b] sont les utilisateurs emblématiques de la magie en Azeroth, qui apprennent leur art au cours de leurs recherches et études approfondies. Ils maîtrisent la magie du feu, du givre et des arcanes pour détruire ou neutraliser leurs ennemis. Leurs caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nIls portent des armures légères, mais compensent cette faiblesse par une puissante gamme de sorts offensifs et défensifs. Le mage fait donc des gros dégâts à distance, envoyant des boules élémentaires sur un ennemi isolé mais faisant pleuvoir la destruction sur une armée. En cas d\'attaque, il peut échapper aux combats rapprochés avec [spell=1953] et devient un [spell=45438] quand cela devient trop critique.\n\nLes Mages peuvent également augmenter les pouvoirs de leurs alliés : [spell=23028], les inviter à leurs [spell=43987] et même les faire voyager à travers des [url=spells=7.8.237&filter=na=portail]portails[/url]. Classe indispensable pour voyager en toute tranquillité. Ils utilisent le mana comme ressource. Les Mages :\n[ul]\n[li]Transforment leurs ennemis en créatures inoffensives ou les geler sur place grâce à [spell=122].[/li]\n[li]Utilisent [item=50045] pour avoir un élémentaire d\'eau en familier.[/li]\n[/ul]',NULL),(13,6,2,NULL,0,2,'[b][color=c6]Les Chevaliers de la mort[/color][/b] sont d\'anciens agent du Fléau, désormais alliés avec la Horde ou l\'Alliance. Cette classe de héros débute le jeu à haut niveau (55). Ses caractéristiques principales sont la force, sans oublier l\'endurance pour les tanks.\n\nTous leurs arbres de talent peuvent être utilisés pour faire des dégâts ou tanker.\n\nLes Chevaliers de la mort qui ont une affinité avec le [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Sang[/url][/icon] ont une grande capacité d’auto-guérison et peuvent fournir à un allié : [spell=49016] qui l’enrage à la vue du sang du champ de bataille.\nL’arbre de talent [icon name=spell_frost_freezingbreath][url=spells=7.6.771]Givre[/url][/icon] permet une augmentation significative de l’armure et spécialise le Chevalier de la mort dans les dégâts de zone avec [spell=49184]\nLes maîtres des maladies et des invocations sont les chevaliers de la mort [icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Impie[/url][/icon]. Ils peuvent utiliser leurs talents [spell=52143] et [spell=49206] pour être aidé lors des combats. Ils ont aussi une plus grande résistance à la magie grâce à la [spell=51052].\n\nLe chevalier de la mort utilise des runes comme ressource principale, dont chacun des trois types est utilisé pour différentes techniques.\n[ul]\n[li] Ils se battent avec les présences (semblable aux positions d\'un Guerrier) qui fournit des bonus spéciaux à leurs rôles.[/li]\n[li] Il dispose de plus de capacités à distance que la plupart de classes de corps à corps et privilégie les maladies et les dégâts infligés par ses familiers morts-vivants.[/li]\n[li] La classe de chevalier de la mort a sa propre capacité d\'enchantement d\'arme spéciale appelée [spell=53428], ce qui remplace le besoin d\'enchantements d\'armes classiques.[/li]\n[li] Ont accès à une zone spéciale inscrite inaccessible par toutes les autres classe : Acherus, le fort d’ébène, situé dans [zone=4298]. Où ils gagneront leurs points de talent en tant que récompenses de quêtes dans les premières heures de jeux.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=48778] - Niveau 55 - Bonus de Vitesse de 100%. [/li]\n[li] [spell=54729] - Niveau 60 - Bonus de vitesse : s’adapte à la compétence de monte. [/li]\n[/ul]',NULL),(13,7,2,NULL,0,2,'[b][color=c7]Les Chamans[/color][/b], maîtres des éléments et de la nature, apportent un grand nombre de buffs à tout un groupe sous forme de totem. Un Chaman peut appeler un totem de chaque élément : terre, feu, eau et air. Ces totems apparaissent à leurs pieds et sont actifs pour toutes les personnes du raid se trouvant dans la zone d’effet du totem. Un bon Chaman sait quels totems sont à lancer et dans quelles circonstances les utiliser, pour maximiser les dégâts du groupe et la survie.\n\nIls sont principalement des lanceurs de sorts, bien qu’un Chaman [icon name=spell_nature_lightningshield][url=spells=7.7.373]Amélioration[/url][/icon] aime se rapprocher des ennemis pour faire de gros dégâts. Il apprend l’[spell=30798] et peut utiliser le sort [spell=51533] pour invoquer 2 Esprits de Loups qui combattent avec lui. Bien qu’il soit principalement de mêlée, le Chaman Amélioration peut bénéficier de la puissance des sorts et lancer instantanément [spell=403] ou des soins avec le talent [spell=51530]. \n\nLes Chamans [icon name=spell_nature_lightning][url=spells=7.7.375]Élémentaires[/url][/icon] se tiennent en retrait pour lancer leurs sorts de feu et de foudre et infliger de grandes quantités de dégâts. Ils peuvent repousser leurs ennemis avec [spell=51490] et aussi les enraciner avec [spell=51486]. Ils apportent le [icon name=spell_fire_totemofwrath][url=spell=57722]Totem de courrou[/url][/icon] et le [spell=51470], buffs très recherchés dans les raids.\n\nLes Chamans qui choisissent [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restauration[/url][/icon] ont un grand panel de sort de guérison se qui leurs permets de se spécialiser dans le soin mono-cible ou multi-cible. Ils sont reconnus pour leurs puissantes [spell=1064] et pour créer un [spell=16190] qui aide la restauration de mana aux membres de leurs groupes. Ils gagnent aussi un puissant [spell=974], peuvent employer [spell=51886] pour enlever les malédictions, et ont un sort de guérison instantané : [spell=61295] qui soigne aussi au fil du temps.\n\nLes Chamans invoquent la puissance des éléments pour améliorer les dégâts de leurs armes ou sorts. Ils portent des armures moyennes, boucliers et utilisent le mana comme ressources.\n[ul]\n[li] Il peut apprendre plus de 20 totems différents.[/li]\n[li] Peuvent lancer [spell=32182] (ou [spell=2825]) pour amplifier les dégâts et les soins de tout le raid. Un buff unique très recherché.[/li]\n[li] Un chaman peut se transformer en [spell=2645] à partir du niveau 16 et peut même le rendre instantané avec le talent [spell=16287]. Ce sort ne peut être utilisé qu\'en extérieur.[/li]\n[li] Il ne peut avoir qu\'un seul bouclier élémentaire d\'actif sur lui [spell=324] ou [spell=52127]. Le [spell=974], peut-être posé sur un autre joueur.[/li]\n[/ul]',NULL),(13,11,2,NULL,0,2,'[b][color=c11]Les Druides[/color][/b] sont la « classe à tout faire » de World of Warcraft, c\'est-à-dire, capable de remplir tous les rôles : soigner, faire des dégâts à distance, faire des dégâts de mêlée ou tanker, en utilisant le Changeforme. Le druide offre donc aux joueurs de nombreux styles de jeu. Ses caractéristiques principales dépendent du rôle choisi.\n\nSous sa forme normale, c’est un lanceur de sorts qui peut se battre à distance et se soigner. Mais il peut aussi prendre d’autres formes dont des formes animales :\n\nLorsqu’un druide se transforme en [spell=5487] (et à un niveau plus avancé, [spell=9634]), son mana se change alors en rage, capable de charger sa cible, de la [spell=8983] et de subir des coups de plusieurs adversaires simultanément. C’est une forme orientée vers le tanking qui fournit une armure et de la vie supplémentaire. Il peut esquiver les coups, utiliser [spell=22812] pour augmenter sa résistance.\nQuand il se transforme en [spell=768], son mana se change alors en énergie, pouvant [spell=5215] tout en se déplaçant, d’augmenter parfois ça vitesse de courses de 70% et de bondir derrière ces ennemis pour attaquer avec le talent [spell=49376]. C’est une forme orienté vers les dégâts de mêlée en faisant saigner leur cible avec [spell=49800] ou [spell=62078] lorsque le druide est entouré d’ennemis.\nAvec les talents de druide équilibre, la [spell=24858] est réputé pour faire beaucoup de dégâts à distance notamment avec les sorts [spell=5176] et [spell=48505] qui peuvent être augmenté avec des points de talent. Il émet aussi une aura, qui augmente les coups critiques des sorts, très appréciée en raid.\nSa forme d’[spell=33891] (talent restauration) est conçue pour soigner sur la durée notamment avec les sorts [spell=33763] et [spell=48438]. Il émet une aura, qui augmente les soins de 6%. Il a la particularité d’avoir une grande régénération de mana.\n\nD’autres formes animales secondaires complètent cette liste : sa [spell=783] qui permet au druide d’augmenter sa vitesse de déplacement, sa [spell=1066] qui lui permet de respirer sous l’eau tout en nageant plus vite et sa [spell=33943] (et avec la compétence [spell=34091], la [spell=40120]) lui permet de voler instantanément.\n\n[ul]\n[li] Dans l’arbre de talent Combat farouche, les druides ont une aura [spell=17007] très utile pour tout groupe de raid.[/li]\n[li] Le sort [spell=20484] est utilisable en combat, mais à une recharge de 10 min.[/li]\n[li] Il possède le sort [spell=29166] qui lui permet de régénérer le mana très vite même en combat, sur lui ou tout autre membre.[/li]\n[li] Les Druides ont leur propre capacité de téléportation qui leur permet de voyager vers [zone=493], ce qui est utile lorsqu’ils ont besoin de s’entraîner.[/li]',NULL),(13,9,2,NULL,0,2,'[b][color=c9]Les Démonistes[/color][/b], vêtue d’armure légère, sont les maîtres des arts démoniaques. Ils possèdent des capacités très puissantes qui, si elles sont utilisées correctement, en font un adversaire formidable. Utilisant leurs malédictions en combinaison avec des sorts de dégâts directs, il cause des ravages et la destruction. Ses caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nLes Démonistes qui ont choisi de se spécialiser dans l’arbre de talent Affliction, excellent dans l’utilisation des malédictions, ils posent sur leurs ennemis [spell=47865] pour les affaiblir ou [spell=47864] pour leurs faire des dégâts. Ils ont la [spell=18271] ce qui augmente les dégâts des sorts d’ombre de 25%.\nLe démonologue appel des démons pour l’aider dans ces combats, il emploie principalement l’[spell=30146]. Il peut aussi se [spell=59672] en démon pour augmenter ses dégâts durant une courte période.\nLe Démonistes destruction utilise des sorts de feu tels que [spell=5740] ou [spell=17962] pour infliger d’importants dégâts directs.\n\nLes Démonistes, tout en étant d’excellent dans les dégâts à distance, soutiennent beaucoup leurs alliés en appelant d’autre joueur avec [spell=698] ou en utilisant des magies rituelles pour conjurer des pierres imbues du pouvoir de guérir : [icon name=inv_stone_04][url=item=5509]Pierre de soin[/url][/icon].\n\n[ul]\n[li] Le démoniste est doté du sort [spell=1454] qui lui permet de sacrifier des points de vie pour régénérer son mana.[/li]\n[li] Le [spell=48020] lui permet une grande mobilité en annulant tous les effets de déplacement, et en s\'éloignant du corps-à-corps.[/li]\n[li] En utilisant le sort [spell=20022], le démoniste permet à la personne sur qui elle a été appliqué de ressusciter.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=5784], leurs yeux ne brûlent plus que d\'une haine inextinguible pour les démonistes qui les ont corrompus - Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23161] sont des destriers recréés qui ont été corrompus par les énergies infernales, transpirant et soufflant le feu - Niveau 60 - Bonus de vitesse : 100%. [/li]\n[/ul]',NULL),(8,81,2,NULL,0,2,'[b]Les Pitons du Tonnerre[/b] est la faction de la capitale des Taurens : [zone=1638], située dans la partie nord de la région de [zone=215]. L\'ensemble de la ville est construit sur des falaises à plusieurs centaines de pieds au-dessus du paysage environnant, elle est accessible par des ascenseurs sur les côtés sud-ouest et nord-est.\n\n[h3]Histoire[/h3]\n\nLa grande ville de Pitons du Tonnerre se trouve au sommet d\'une série de mesas qui donnent sur les prairies verdoyantes de Mulgore. Les Taurens, autrefois nomade, ont récemment construit la ville pour dresser un centre de caravanes commerciales avec des artisans itinérants et des artisans de toutes sortes. Elle a été établi par le puissant chef [npc=3057] après que les Taurens, avec l\'aide des Orcs, ont chassé les centaures qui habitaient à l\'origine Mulgore. De longs ponts de corde et de bois font la liaison entre les mesas qui sont surmontées de tentes, de longues maisons, de totems peints aux couleurs vives et de huttes spirituelles. Le chef de Tauren surveille la ville animée, en veillant à ce que les tribus unies de Tauren vivent en paix et en sécurité.\n\n[h3]Réputation[/h3]\n\n[npc=14728] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Pitons du Tonnerre, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url].',NULL),(8,1038,2,NULL,0,2,'[b]Ogri\'la[/b] est un groupe d\'Ogres localisé dans [zone=3522], où leur proximité avec [item=32572] leur a permis d\'évoluer au-delà de leur nature brutale. Ils sont particulièrement impliqué dans une guerre contre le Dragon noir et la Légion ardente, qui cherchent les cristaux Apogides pour leurs propres fins.\n\n[h3]Localisation[/h3]\nOgri\'la est situé près du bord ouest des Tranchantes, entre le Camp de Forge: Terreur et le Camp de Forge: Courroux, juste à l\'ouest de Sylvanaar. Ogri\'la est seulement accessible en monture volante ou en forme de vol. Une autre alternative est d\'avoir une réputation d\'honoré ou plus élevé avec [faction=1031]. Mais un joueur doit avoir une monture volante pour atteindre le camp Garde Ciel près de Skettis.[pad]\n\n[h3]Reputation[/h3]\nLa reputation avec Ogri\'la ne peut être acquise que par quêtes, et il n\'y a que des quêtes répétables dont les [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes journalières[/url]. Il ya un plafond sur la quantité de réputation que l\'on peut obtenir chaque jour pour un joueur avec Ogri\'la, ce qui en fait une réputation \"difficile à farmer\".\n\n[b]Eclats Apogides[/b]\n[item=32569] peuvent être collectées de diverses manières. Ils peuvent être pillés sur le cadavres de monstres, recueillis à partir de l\'environnement, ou ils peuvent être en récompenses de quêtes terminées.[pad]\n[b]Cristaux Apogies[/b]\n[item=32572] se ramassent sur les élites de type Demons ou Dragons dans les Tranchantes. Pour appeler ces mobs, 35 Eclats Apogides sont nécessaires, et il est recommandé que vous ayez un groupe de 5 personnes pour les vaincre.\n\n[b]Quêtes[/b]\nIl y a un certain [url=?quests&filter=cr=1;crs=1038;crv=0]nombre de quêtes[/url] qu\'un joueur peut faire pour gagner de la réputation avec Ogri\'la, ainsi que plusieurs [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes quotidiennes[/url]. Beaucoup de quêtes quotidiennes seront également accordée à la réputation de la Garde Ciel Sha\'tari lorsqu\'elles seront complétées. \n\nPour accéder aux principales quêtes d\'Ogri\'la, un joueur doit d\'abord compléter les 5 quêtes de groupe de [npc=22941].\n\n[h3]Éléments épuisés[/h3]\nUn certain nombre d\'éléments apogides tombent parfois de mobs une fois mort. Lorsque vous avez amassé 50 éclats apogides, [url=?search=Apexis+Crystal+Infusion]les objets suivants peuvent être améliorés[/url], obtenant des statistiques supplémentaires et des emplacements de gemmes. Une fois ces objets améliorés, ils deviendront liés si équipés, et peuvent donc être vendus ou échangés avec d\'autres joueurs. Une chose à noter cependant, bien que les éléments épuisés peuvent également avoir des statistiques ou des effets, ils ne peuvent pas être équipés.',NULL),(8,911,2,NULL,0,2,'[b]Lune d\'Argent[/b] est la capitale des elfes de sang, située dans la partie nord-est de [zone=3430] dans le royaume de Quel\'Thalas. La capitale,des elfes de sang, est à couper le souffle. Elle peut rivaliser avec la capitale naine de [zone=1537], capitale la plus ancienne du monde toujours debout. Récemment reconstruite, la ville abrite la plus grande population d\'elfes de sang en Azeroth. \n\nAujourd\'hui, Lune d\'Argent n\'est que la moitié orientale de la ville d\'origine. La moitié occidentale a été presque entièrement détruite par le fléau pendant la troisième guerre. La place de lÉpervier, est la seule partie occidental de Lune d\'Argent restant sous le contrôle des elfes de sang. La Malebrèche, chemin parcouru par Arthas Menethil et son armée de morts-vivants parties en quête de ressusciter Kel\'Thuzad, traverse tout le Bois des Chants éternels. Il sépare la Lune d\'Argent reconstruite et ces ruines de la moitié occidentale. Fait intéressant, les ruines de Lune d\'Argent ne logent pas de morts-vivants, au lieu de cela, elles contiennent des [url=?npcs&filter=cr=37;crs=6;crv=1502;na=Déshérité;maxle=8]déshérités[/url] et des [npc=15638]. Dans l\'état actuel des choses, Lune d\'Argent est encore la plus grandes des villes Hordeuses.\n\n[h3]Histoire[/h3]\n\nLa ville de Lune d\'Argent a été fondée par les hauts élus après leur arrivée à Lordaeron, il y a des milliers d\'années. La ville a été construite en pierre blanche autour de plantes vivantes dans le style de l\'ancien Empire Kaldorei. La ville contenait les célèbres académies de Lune d\'Argent, centre d\'apprentissage de la magie arcane, et la Flèche de Solfurie, majestueux palais abritant la famille royale des hauts-elfes. Également basé dans la ville, la convocation de Lune d\'Argent, également connu sous le nom de « Le Concile de Lune d\'Argent », était l\'organe dirigeant des hauts-elfes. À travers une étendue d\'océan vers le nord, il y a l\'île qui contient le plateau du puits du Soleil.\n\nBien que Lune d\'Argent ait resorti relativement indemne de la deuxième guerre, dans la troisième guerre, le Chevalier de la mort Arthas a mené le Fléau dans la ville, l\'attaquant au cours de sa quête pour atteindre le puit du Soleil. Le roi High Elven a été tué et la majorité de la population a été exterminée. Les forces de fléau ont tenu la ville pendant un certain temps mais l\'ont abandonné après l\'épuisement de ses ressources. \n\nBien que la ville ait été attaquée par le Fléau, elle n\'est pas aussi détruite qu\'on pourrait le penser. Beaucoup de ses plantes sont mortes, quelques cadavres sont étendu sur le pavé, la ville était à l\'abri du feu et de la destruction. Lune d\'Argent ressemble maintenant à une ville fantôme, intacte, mais étrangement abandonnée. Néanmoins, les chasseurs de trésors fréquentent fréquemment les ruines de Lune d\'Argent pour essayer de trouver certains des artefacts précieux que les elfes ont laissés derrière avant de déserter la ville, mais les fantômes des anciens habitants de Lune d\'Argent les en empêchent.\n\n[h3]Réputation[/h3]\n\n[npc=20612] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Lune d\'Argent, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=151;crs=6;crv=35513;na=Faucon-pérégrin]Faucon-pérégrins[/url].\n\nLes zones environnantes du Bois des Chants éternels et des terres fantômes contiennent la plupart des quêtes pour gagner de la réputation avec Lune d\'Argent.',NULL),(8,577,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[b]Long-guet[/b]\n[faction=369]\n[faction=470]\n[/Minibox]\n\n[b]Long-guet[/b], faction de la ville du même nom, est un poste commercial dirigé par les gobelins du Cartel Gentepression. Il se trouve au carrefour des principales routes commerciales du [zone=618].\n\n[h3]Histoire[/h3]\n\nCette ville est le dernier point de la civilisation avant d\'atteindre le Mont Hyjal. Il est géré par les gobelins comme un poste commercial. La ville est officiellement neutre pour toutes les races et factions. Seuls les pèlerins peuvent monter jusquà lArbre-Monde, point culminant du Mont Hyjal. Long-guet est donc la destination la plus haute que les marchands et les aventuriers peuvent atteindre sans l\'autorisation des Elfes de nuit. Elle offrirait une vue dominante sur Kalimdor, si les nuages qui enveloppent continuellement les flancs de la montagne, disparaissaient.\n\nLong-guet est le seul avant-poste de gobelin majeur dans le nord de Kalimdor. Tout d\'abord, il sert de base aux opérations pour les mineurs de thorium et d\'arcanites puisque le Berceau-de-lHiver possède quelques veines inexploitées de ces matériaux. Deuxièmement, il sert de centre d\'échanges entre l\'Alliance et la Horde. Alors que Long-guet est à peine plus sûr que Reflet-de-Lune, généralement, l\'Alliance et la Horde se traitent assez bien là-bas. En outre, Long-guet est un point d\'arrêt et de réapprovisionnement fréquent pour les fidèles qui font le pèlerinage du Berceau-de-lHiver au Mont Hyjal.\n\n[h3]Réputation[/h3]\n\nLa réputation de Long-guet et du Cartel Gentepressin provient surtout des quêtes du Berceau-de-lHiver. Avec une réputation au minimum amicale, les gardiens vous aident en cas dattaque initiée contre vous.',NULL),(8,21,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[b]Baie-du-Butin[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Baie-du-Butin[/b] est une grande ville pirate nichée dans les falaises entourant un magnifique lagon bleu, à lextrémité de [zone=33]. Pour entrer dans la ville, il faut passer au travers les mâchoires blanchis d\'un requin géant.\n\nParcouru par les Écumeurs des Flots noirs qui sont étroitement associés eu Cartel Gentepression, le port offre des opportunités à n\'importe quel voyageur passant par là, indépendamment de leur faction. Combiné à la célèbre « taverne du Loup de mer », le [event=15], de nombreux maîtres de profession et des vendeurs, qui vendent de tout (des animaux de compagnie aux anneaux de diamant), c\'est l\'un des endroits les plus populaires en Azeroth.\n\n[npc=2496], chef de la ville, embauche toute l\'aide qu\'il peut obtenir contre [faction=87] et autres menaces de la ville. Il réside avec le chef des Écumeurs des Flots noirs, [npc=2487], au sommet de l\'auberge de Baie-du-Butin.\n\nEn raison de la liaison par bateau de Baie-du-Butin à Cabestan, les joueurs de tout niveau (surtout de la Horde, si le niveau est faible) peut-être croisés dans le port, bien que les visiteurs les plus fréquents seront dans les niveaux 35-45, car les quêtes disponibles auprès des gens du pays se situent dans cette tranche de niveau.\n\nL\'eau est parsemée de débris flottants et de bancs de poissons. Plusieurs types de poissons se pèchent dans les eaux de la Baie, tels que le [item=6359], le [item=6358], et l\'[item=13422]. La pêche, dans les débris flottants, vous donnera également plus de chance de pêcher des coffres et d\'autres articles, faisant de Baie-du-Butin un endroit idéal pour la pêche.\n\n[h3]Réputation[/h3]\nLa plupart des quêtes pour augmenter la réputation avec Baie-du-Butin sont situés au Cap de Strangleronce. Avec une réputation au minimum amicale, les gardes vous aiderons en cas dattaque contre vous.\n\nSi vous êtes haï avec Baie-du-butin vous pouvez faire la quête répétable [quest=9259] pour revenir à Neutre.',NULL),(8,470,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Cabestan[/b]\n[/Minibox]\n\n[b]Cabestan[/b], faction de la ville du même nom, situé sur la côte est de Kalimdor dans [zone=17]. Elle est dirigée par des gobelins. Ses rues se répandent dans toutes les directions, et l\'architecture ne montre aucune cohérence ni vision commune. C\'est une ville de divertissement et de commerce, où tout ce que vous voudriez acheter est en vente mais aussi beaucoup de chose que personne ne veut jamais. \n\nCabestan est actuellement géré par un groupe d\'entreprises connu sous le nom du Cartel Gentepression, un groupe fragmenté de la KapitalRisk, qui a d\'abord construit la ville portuaire pour la négociation avec [zone=1637]. C\'est d\'abord une faction neutre où Horde et Alliance se côtoient. Un bateau relie commodément Cabestan à Baie-du-butin.\n\n[h3]Histoire[/h3]\n\nConstruit à part égales entre l\'industrie et de la décadence, la ville portuaire gobeline de Cabestan s\'étend sur près d\'un kilomètre de littoral des Tarides de l\'est, entre [zone=14] et [zone=15]. Cabestan est la fierté des gobelins, une ville commerciale où vous pouvez trouver presque tout ce que votre cur désire, et si quelque chose n\'est pas en stock, vous pouvez parier que les gobelins peuvent le commander. Cabestan est desservie régulièrement par les bateaux qui font la traversé en passant devant la forteresse de Theramore, vers le sud.\n\nCabestan est une ville où les habitants, qui étaient autrefois des truands, règnent maintenant. Ses rues errent sans rime ni raison à travers des quartiers dédiés à une seule activité : le commerce. Des entrepôts délabrés se situent à côté de maisons en pierre majestueuses. Les belles boutiques sont voisines avec des cabanes grossières. Des objets de toutes les formes, et certains au-delà de l\'imagination, sont exposés sur les marchés et les boutiques exclusives.\n\nLes Gobelins accueillent toutes personnes ayant de l\'or, des éléments de valeur et une volonté de les échanger contre leurs marchandises et leurs services. Les marchands traversent la ville tous les jours, vendent tout, de la soie aux esclaves. Même la nuit, les magasins qui bordent les rues et les allées restent ouverts aux entreprises. Ceux qui ont de l\'argent peuvent écouter des musiciens qualifiés, tout en buvant des bières fines et en mangeant des aliments préparés par des grands chefs. Pour ceux qui ont des goûts plus terriens, on retrouve le long des quais des marchants d\'armes, la banque et des casinos.\n\nCabestan est le plus grand port de Kalimdor, beaucoup de navires transportant de la cargaison sortent pour d\'autres sites autour de Kalimdor. En plus des navires commerciaux légitimes, les bâtiments pirates reçoivent une amnistie dans le port de Cabestan tant qu\'ils peuvent payer des droits d\'accostage rigides. Cette situation rend les capitaines marchands furieux, mais ils ne peuvent boycotter Cabestan, sinon c\'est la faillite pour leurs commerces. En outre, les avocats et les mercenaires qui rôdent sur le front de mer sont impatients de faire face à tous ceux qui cherchent à causer des problèmes.\n\n[h3]Réputation[/h3]\n\nLa plupart des quêtes pour élever la réputation avec Cabestan et le Cartel Gentepression sont situées dans les Tarides. Avoir une réputation au minimum amicale, les gardiens aident en cas d\'attaque contre vous.\n\nSi vous êtes détesté auprès de Cabestan, vous pouvez faire la quête répétable [quest=9267] pour revenir à une réputation Neutre.',NULL),(8,369,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] est la faction de la ville du même nom, qui abrite les plus grands ingénieurs, alchimistes et marchands gobelins. Seul endroit de civilisation au nord du désert de [zone=440], elle est perçue comme une oasis. Gadgetzan est le siège du Cartel Gentepression, le plus grand cartel gobelin. Les gobelins croient au profit plus quà la loyauté, donc Gadgetzan est considéré comme territoire Neutre dans le conflit Horde / Alliance.\n\n[h3]Histoire[/h3]\n\nBien que la neutralité des gobelins soit presque universellement reconnue, il y a encore ceux qui cherchent à semer le chaos et lanarchie. Pour Gadgetzan, cela vient sous la forme des bandits Bat-le-désert, une bande de mécréants qui occupe le champ des Puisatiers et les ruines d\'Ombre-du-Zénith au Nord-est de Tanaris. Peu de Gobelins se soucient des ruines antiques (à moins quils y aient un trésor), les bandits peuvent avoir les vieux blocs de pierre. \nCependant, le champ des Puisatiers est vital pour la survie des gobelins, leur fournissant lor liquide du désert. Les tours d\'eau dans le champ ont été construites sous la chaleur ardente du soleil, par le travail de leurs esclaves. Les gobelins ne vont pas abandonner leurs tours durement gagnées, aussi facilement. Mais, ils doivent rester en ville pour arrêter le conflit, en apparence interminable, parmi les différents visiteurs et donc empêcher de perturber les affaires. Par conséquent, ils embauchent de braves mercenaires venant de tous les coins du monde pour les aider.\n\n[h3]Réputation[/h3]\n\nEn tuant les [url=?npcs=7&filter=na=mers+du+Sud]Flibustiers des mers du Sud[/url] et les [url=?npcs=7&filter=na=bat-le-désert]Bandits Bat-le-désert[/url], la réputation avec le cartel Gentepression augmentera. Ayant une réputation au minimum amicale, les gardes vous aideront en cas d\'attaque contre vous. Avoir une réputation exaltée signifie que les gardes ne vous attaqueront jamais même si vous lancez des attaques sur la faction opposée. \n\nLa plupart des quêtes associées à la faction Gadgetzan sont situées à Tanaris. \n\nSi vous êtes détestés avec Gadgetzan, vous pouvez faire la quête répétable [quest=9268] pour obtenir la Neutralité.',NULL),(8,47,2,NULL,0,2,'[b]Forgefer[/b] est la faction associée à la capitale des nains, [zone=1537]. [npc=2784] règle son royaume de Khaz Modan de sa salle du trône dans la ville, et [npc=7937], chef des gnomes, a temporairement dû s\'établir dans Brikabrok après la récente chute de la ville gnome [zone=133].\n\n[h3]Histoire[/h3]\n\nForgefer est l\'ancienne demeure des nains, une merveille façonnée dans la pierre. Forgefer a été construite au cur même des montagnes, une ville souterraine qui abrite des explorateurs, des mineurs et des guerriers. Les portes massives de roche protègent la ville en temps de guerre, et la lave de la montagne est redirigée et distribuée à des fins de chaleur, d\'énergie et de forage. \nAvant que le clan de Sombrefer ne soit banni de la ville, menant à la Guerre des Trois Marteaux, Forgefer était le centre commercial et social de tous les clans nains. Il appartient maintenant au Clan Barbe-de-bronze. \nBeaucoup de bastions nains ont chuté pendant la Guerre de Lordaeron, entre la Horde et l\'Alliance, mais la puissante ville de Forgefer, nichée dans les sommets hivernaux de [zone=1] et protégée par ses grandes portes, n\'a jamais été violée par la Horde envahissante.\n\nRelativement récemment, Forgefer est également devenu le foyer des Exilés de Gnomeregan. Après la troisième guerre, la ville gnome fut envahie par Troggs. Depuis lors, un certain nombre de gnomes se sont installés à Forgefer, transformant une zone de cette ville à leur goût, une région connue sous le nom de Brikabrok.\n\nForgefer est l\'une des villes les plus peuplées du monde, venant après la ville humaine de [zone=1519], et abritant 20 000 personnes.\n\nAlors que l\'Alliance a été affaiblie par les événements récents, les nains de Forgefer, dirigés par le roi Magni Barbe-de-bronze, forment un nouveau futur dans le monde. \n\n[h3]Réputation[/h3]\n\n[npc=14723] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Forgefer, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92:151:151;crs=2:1:6:6;crv=0:0:33977:33976;na=bélier] béliers [/url].\n\nLes zones environnantes [zone=1], [zone=38] et [zone=11] contiennent la plupart des quêtes pour gagner de la réputation auprès de Forgefer.',NULL),(8,54,2,NULL,0,2,'[b]Les Exilés de Gnomeregan[/b] est la faction des gnomes qui ont fui leur domicile, [zone=133] à [zone=1]. Elle a été détruite par [url=?npcs=7&filter=na=Trogg] les Troggs[/url] après une invasion toxique. Maintenant, membre de lalliance, la plupart sont situés à Brikabrok, une partie de la ville voisine [zone=1537], y compris le leader [npc=7937].\n\n[h3]Histoire[/h3]\n\nOn a spéculé que les gnomes ont été formés comme des robots par les titans, en raison de leur nature curieuse et de leurs compétences techniques. Ils vivaient autrefois dans la cité de Gnomeregan, sans doute la plus belle ville technologique du monde.\n\nLes gnomes étaient une race souterraine de bricoleurs, jusquà ce que les Troggs aient détruit Gnomeregan. Dans cette guerre, plus de 80% de la population gnome a été exterminé.\n\n[h3]Réputation[/h3]\n\n[npc=14724] offre une quêtes répétables où il faut fournir des étoffes. En étant exalté aux Exilés de Gnomeregan, les joueurs sont capables de conduire des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=mécanotrotteur]mécanotrotteurs[/url].\n[zone=1] contient la plupart des quêtes pour gagner la réputation avec les exiés de Gnomeregan.',NULL),(8,72,2,NULL,0,2,'[b]Hurlevent[/b] est la faction associée à [zone=1519], la capitale des Humains. Elle est située dans la partie nord-ouest de la [zone=12]. L\'enfant roi, [npc=1747], réside dans le Donjon de Hurlevent, entouré de ses gardes du corps et de ses conseillers, [npc=1748] (le régent) et [npc=1749]. La ville est nommée ainsi à cause des rafales soudaines et occasionnelles créées par la forme spéciale des montagnes autour de la ville glorieuse.\n\n[h3]Histoire[/h3]\n\nPendant la Première Guerre, le Royaume d\'Azeroth, y compris sa capitale, le Donjon de Hurlevent, a été complètement détruit par la Horde. Ses survivants ont fui vers Lordaeron. Après que les orcs ont été vaincus, au Portail des Ténèbres, à la fin de la Deuxième Guerre, il a été décidé que la ville serait reconstruite, dépassant sa grandeur dantan. Des tailleurs de pierres et des architectes ont pu été rassemblés par les nobles de Hurlevent. Sous la directio de cette équipe, la plus qualifiée et la plus ingénieuse, Hurlevent a été reconstruit dans une période de temps incroyablement courte. Maintenant, à la fin de la troisième guerre, dans le renommé Royaume de Hurlevent. Cest l\'un des derniers bastions du pouvoir humain laissé dans le monde.\n\nAvec la chute des Royaumes du Nord, Hurlevent est de loin la ville la plus peuplée du monde. Avec une population de deux cents mille personnes (principalement humaines), elle sert à bien des égards comme le centre culturel et commercial de l\'Alliance, même avec un accès à la mer. Les humains qui vivent dans la ville sont généralement insouciants et artistiques, favorisant les vêtements légers et colorés, la cuisine et l\'art. Elle abrite l\'Académie des sciences arcanes, la seule école de sorcellerie dans les royaumes de l\'Est, ainsi que le SI:7, une organisation de renseignement.\n\nCependant, les gens de Hurlevent ont du mal à accepter le rôle de Theramore en tant que foyer de la nouvelle Alliance. Ils sont convaincus que Hurlevent devrait être l\'héritière légitime du rôle de la ville de Lordaeron comme par le passé, mais aussi que Theramore est attristé face à l\'aggravation de la situation au sein de Les Royaumes de l\'Est.\n\n[h3]Réputation[/h3]\n\n[npc=14722] propose une quête répétable pour obtenir une réputation plus élevée avec Hurlevent. En contrepartie d\'une réputation exaltée, les joueurs non-humains peuvent monter sur des chevaux.\n\nLa plupart des quêtes associées à Hurlevent viennent des zones environnantes de la forêt d\'Elwynn, [zone=40] et [zone=44].',NULL),(8,930,2,NULL,0,2,'[b]Exodar[/b] est la faction associée à [zone=3557], la capitale enchantée des Draeneï construit avec la plus grande partie de leur vaisseau qui sest écrasé. Il est situé dans la partie ouest de l[zone=3524]. Le chef de la faction Exodar est [npc=17468], qui est situé près des maîtres de combat dans la Voûte des Lumières.\n\n[h3]Histoire[/h3]\n\nLes Draeneï rescapés du crash de leur vaisseau se sont récemment réveillés pour reconstruire lExodar, encore fumant de limpact. L\'Exodar était autrefois une structure de satellite naaru autour de la forteresse dimensionnelle du [url=?search=donjon+tempête]Donjon de la Tempête[/url]. L\'Exodar contient une grande quantité de merveilles technologiques (en raison de ses origines avec le Donjon), comme des «fils» magiquement enchantés qui transmettent de l\'énergie sainte dans tout le navire pour alimenter le chauffage et l\'éclairage, tout en augmentant les pouvoirs, déjà considérable, des Draeneï.\n\n[h3]Réputation[/h3]\n\nComme pour les autres grandes factions associées aux races principales, la réputation de l\'Exodar peut être acquise en faisant la quête répétable de [npc=20604] [small][/small], ou alors, en tuant la faction adverse dans [zone=2597] (les elfes de sang) et en faisant les quêtes appropriées. Avec la réputation, le joueur peut acheter des objets provenant de fournisseurs liés à Exodar pour 10% de moins et, une fois exalté, le joueur peut acheter [url=?Items=15.5&filter=na=elekk;cr=93:92;Crs=2:1;crv=0:0] diverses montures[/url].',NULL),(8,69,2,NULL,0,2,'[b]Darnassus[/b] est la faction de la ville de [zone=1657], la capitale des Elfes de la nuit. La haute prêtresse, [npc=7999], réside dans le Temples de la Lune, entourée d\'autres surs d\'Elune. Dans l\'Enclave Cénarien, l\'[npc=3516] conduit le [faction=609], souvent en opposition directe avec ses autres druides à [zone=493] et Tyrande elle-même.\n\n[h3]Histoire[/h3]\n\nAu lendemain de la troisième guerre, les Elfes de la nuit devaient s\'adapter à leur existence mortelle. Un tel ajustement était loin d\'être facile. Beaucoup d\'Elfes de la nuit ne pouvaient pas s\'adapter aux perspectives de vieillissement, de maladie et de fragilité. En cherchant à retrouver leur immortalité, un certain nombre de druides capricieux conspiraient pour planter un arbre spécial qui rétablirait un lien entre leurs esprits et le monde éternel.\n\nAvec [npc=15362] disparu, Fandral Forteramure, le chef de la conspiration qui souhaitaient planter le nouvel Arbre-Monde, est devenu le nouvel Archidruide. En un rien de temps, lui et ses camarades druides ont pris les devants et ont planté le grand arbre, [zone=141], au large des côtes orageuses du nord de Kalimdor. Avec leur soin, l\'arbre a poussé au-dessus des nuages. Parmi les branches crépusculaires de l\'arbre colossal, la merveilleuse ville de Darnassus a pris racine. Cependant, l\'arbre n\'a pas été béni par la nature et s\'avère être corrompu par la Légion Ardente. Maintenant, la faune et même les membres de Teldrassil sont contaminés par une obscurité croissante.\n\n[h3]Réputation[/h3]\n\n[npc=14725] offre une quête répétable [quest=7800] utilisé par les joueurs de l\'Alliance pour obtenir le droit de monter des [url=?items=15.5&filter=cr=93:92:151;crs=2:1:6;crv=0:0:13086;na=sabre;si=-1]Sabres-de-nuit[/url]. Les joueurs qui sont au minimum niveau 44, cherchant à gagner la faveur de Darnassus, devraient trouver et compléter les quêtes de [zone=357]. Les quêtes sont associées à Darnassus et pourraient accroître considérablement votre réputation.',NULL),(8,809,2,NULL,0,2,'Les [b]Shen\'dralar[/b] sont la faction des Elfes de nuit restant dans [zone=2557]. Ils sont un groupe qui pratique la magie arcane à son apogée sur les traces de leur ancienne reine Azshara, et de ses partisans, les Bien-nées. Ils vivent à Eldre\'Thalas (nom antérieur de Hache-tripes) depuis la fin de la guerre des Anciens. Ils sont peu nombreux, mais leur connaissance et leur pouvoir mystique sont géniaux.\n\nLeur chef, [npc=11486], était chargé de superviser la construction des pylônes pour contenir le grand démon [npc=11496] et absorber son pouvoir démoniaque. Après de longues et nombreuses années, le pouvoir des pylônes a commencé à diminuer, le prince a entrepris de tuer les elfes de nuit restants pour maintenir l\'énergie. Les esprits des défunts demandent vengeance, mais seuls des aventuriers aguerris peuvent le tuer. Faite-vite, il reste très peu d\'habitants en vie.\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en rendant à plusieurs reprises les quêtes obtenus avec les trois Librams de Hache-Tripes : [item=18333], [item=18334] et [item=18332]. \nLa réputation peut être obtenue aussi via les livres de classe suivant :\n[ul] \n[li] [item=18357] - Guerrier [/li] \n[li] [item=18363] - Chaman [/li] \n[li] [item=18356] - Voleur [/li] \n[li] [item=18360] - Démoniste [/li] \n[li] [item=18362] - Prêtre [/li]\n[li] [item=18358] - Mage [/li]\n[li] [item=18364] - Druide [/li]\n[li] [item=18361] - Chasseur [/li]\n[li] [item=18359] - Paladin [/li]\n[li] [item=18401] - Guerrier et Paladin [/li] \n[/ul] \nLes livres de classe et les librams donnent 500 points de réputation chacun.',NULL),(8,349,2,NULL,0,2,'[b]Ravenholdt[/b] est une guilde de voleurs et d\'assassins qui ne reçoit que ceux d\'une extraordinaire prouesse. Ils sont opposés à la [faction=70]. La quête, [quest=8249], est disponible pour les classes non-voleurs, mais elle nécessite l\'aide d\'un voleur pour obtenir les objets pour la quête. Le manoir de Ravenholdt, le siège de la faction, est situé dans [zone=36], mais pour y arriver, vous devez venir du coin nord-est de [zone=267].\n\n[h3]Réputation[/h3]\n\nTous les [url=?Search=Syndicat#npcs]membres du Syndicat [/url] donnent 1-5 points de réputation en fonction de votre niveau actuel. De plus, il existe quelques quêtes qui augmentent votre réputation, mais la méthode principale pour élever votre réputation provient des quêtes répétées pour fournir les objets demandés.\n\nVous commencez à une réputation Neutre (0/3000) avec Ravenholdt, ce qui signifie que si vous tuez un NPC de Ravenholdt avant d\'augmenter votre réputation d\'au moins 5, vous deviendrez hostile et ne pourrez jamais augmenter votre réputation. \nPour augmenter votre réputation de Neutre à Amicale, la quête répétable [quest=6701] est disponible. Vous devrez fournir 11-12 [item=17124] et une fois que vous êtes amical, cette quête n\'est plus disponible. Vous pouvez également fournir cinq [item=16885].\nPour augmenter votre réputation au-delà de Amical, le seul choix est la quête répétable, [quest=8249]. \n\n[h3]Récompense[/h3]\n\nIl n\'y a aucune récompense de faction connue pour obtenir que se soit avec une réputation Amicale, un honoré, révéré ou exalté, sauf que les gardes vous parlent avec plus de respect. \n\nCependant, La réputation Exalté est nécessaire pour obtenir le Haut-Fait : [achievement=2336].',NULL),(8,87,2,NULL,0,2,'Les [b] Pirates de la Voile Sanglante [/b] semblent être l\'une de ces organisations, qui sont apparues en Azeroth pendant les événements menant à la troisième guerre et à la suite de la troisième guerre. Ils sont originaires du Rivage Cruel, où leur chef, l\'[npc=2546], organise les opérations. Ils ont maintenant l\'intention de paralyser et de piller la ville portuaire de [faction=21], contrôlée par le Cartel Gentepression et sous la protection des Ecumeurs des Flots noirs. Il est probable que les Pirates de le Voile Sanglante sont venus profiter de la perte actuelle de leur flotte, sur la côte de la [zone=45], dans laquelle deux de ses navires ont été détruits. Le navire restant a été obligé de trouver un abri dans une crique où son équipe lutte maintenant pour survivre aux escarmouches des Nagas.\n\nEn préparation de l\'attaque, les Pirates de la Voile Sanglante ont pris position dans des endroits clés près de la ville. À l\'heure actuelle, ils ont trois navires ancrés le long du littoral au sud de Baie-du-Butin, à l\'abri des canons défensifs de la ville. Des camps ont également été construits le long de la même côte en prévision de l\'attaque. En outre, une fête scoute a atterri juste à l\'ouest de l\'entrée de la ville, signalant toutes les activités, ainsi qu\'un camp construit le long de la route menant vers la ville, susceptible d\'empêcher tout renfort.\n\nLes Pirates de la Voile Sanglante cherchent à atteindre leurs objectifs sans avoir leurs forces engagées dans la bataille, à cette fin, chaque côté cherche maintenant l\'aide d\'aventuriers sympathiques à leur cause.\n\n[h3]Réputation [/h3]\n\nIl n\'y a qu\'une seule façon d\'augmenter votre réputation auprès des Pirates de la Voile Sanglante et c\'est de libérer votre colère contre tous les citoyens de Baie-du-Butin. Voici une liste de tous les citoyens de Baie-du-Butin et leur valeur de réputation. \n[ul]\n[li] [npc=4624] : 25 points de réputation gagné [/li]\n[li] [npc=15088] : 25 points de réputation gagné [/li]\n[li] [npc=2496] : 5 points de réputation gagné [/li]\n[li] [npc=2636] : 5 points de réputation gagné [/li]\n[li] [url=?Npcs&filter=cr=3;crs=21;crv=0] Plusieurs autres NPC [/url][/Li]\n[/Ul]\nLe montant gagné avec les Pirates de la Voile Sanglante est indiqué pour un niveau 60 non humain. Le montant perdu pour tuer un citoyen ne peut pas être démontré car il dépend de votre niveau actuel avec Baie-du-Butin et de l\'importance de la personne que vous tuez. En plus de cela, quand vous perdez de la réputation avec Baie-du-Butin, vous perdez la moitié dans les trois autres villes du Cartel Gentepression. Par exemple, si vous perdez 25 points avec Baie-du-Butin, vous perdrez 12,5 points avec [faction=470].\n\nLe moyen le plus rapide d\'augmenter votre réputation avec les Pirates de la Voile Sanglante est de tuer des habitants de Baie-du-Butin. Au début, cela peut sembler une tâche simple car les gardes n\'apparaissent pas aussi menaçants que les autres monstres auxquels un joueur est confronté dans le jeu. Cependant, les gardes sont très équipés pour neutraliser les joueurs de toute classe, afin d\'éviter que les gens ne s\'attaquent les uns les autres dans la ville. \n\nLe Cogneur de Baie-du-butin a l\'avantage avec plusieurs capacités. Lune dentre elle est lutilisation de filet pour vous bloquer sur place, vous empêchant de vous échapper. Une autre est le fait qu\'ils appellent dautres Cogneurs chaque fois que vous attaquiez un citoyen de la ville ou si vous êtes sous un statut hostile avec Baie-du-Butin, les joueurs peuvent bientôt se retrouver rapidement submergés par les Cogneurs.\nLa capacité la plus forte du Cogneur est quune fois qu\'il tire son arme, il est peu probable que vous vivez, si vous ne vous échappez pas assez vite. Chaque fois qu\'un Cogneur vous tire dessus, l\'attaque vous retient, tout comme une attaque de marteau d\'Ogre. La différence ici, est que le Cogneur peut tirer rapidement en succession, provoquant des lances de chaîne. Un joueur peut littéralement être jeté d\'un côté de la ville à l\'autre, ce qui vous empêche d\'attaquer. Plus souvent, vous vous retrouverez coincé dans un coin, incapable de bouger et incapable d\'attaquer avec tous les sortilèges interrompues par l\'attaque du Cogneur. Parce que les Cogneurs ne rangent pas leurs armes à feu une fois qu\'elles sont sorties, la meilleure façon d\'agir est de s\'enfuir.\n\nPar essais et erreurs, la plupart des gens ont découvert un endroit sûr pour tuer les Cogneurs de Baie-du-Butin. Si vous suivez le tunnel qui mène à la ville, le chemin de votre gauche qui mène à la maison du Forgeron est l\'endroit idéal pour tuer les gardes. Seuls deux gardes patrouillent sur ce chemin. Une fois qu\'ils sont partis, entrer dans la première construction sur le chemin pour provoquer un rassemblement. Un joueur devrait pouvoir tuer 2 à 4 Cogneurs avant que les deux Cogneurs de patrouille en appellent dautres. En moyenne, un joueur qui fait cela peut tuer environ 30 à 40 Cogneurs de Baie-du-Butin, gagnant environ 800 points de réputation auprès de la Voile Sanglante. Les Cogneurs ici ne semblent pas sortir leurs armes, mais si vous vous trouvez dans une mauvaise situation, vous pouvez sauter sur la balustrade, courir sur le chemin des eaux, pour vous échapper.\n\nPour augmenter votre réputation au-delà de honoré, seuls deux NPC vous le permettent : \n[ul]\n[li] [npc=9179] : 5 points de réputation toutes les 7 minutes jusquà révéré [/li]\n[li] [npc=26081]: 5 points de réputation toutes les 24 heures jusquà exalté [/li]\n[/Ul]\n\n[h3]Récompenses[/h3]\n\nDevenir amical avec Les Pirates de la Voile Sanglante, vous donnera accès aux éléments suivants :\n[ul]\n[li] [item=12185] - Invoque un [npc=11236] [/li]\n[li] [item=22742] [/li]\n[li] [item=22743] [/li]\n[li] [item=22745] [/li]\n[/Ul]\nVous aurez besoin d\'être honoré avec la Voile Sanglante pour [achievement=2336].',NULL),(8,70,2,NULL,0,2,'Le[b] Syndicat [/b] est une organisation criminelle humaine qui opère principalement dans les [zone=45] et les [zone=36], bien que quelques petits campements soient éparpillés dans les [zone=267]. Leur effectif compte environ 3 000 personnes.\n\nIls ont trois chefs : [npc=2423], descendant du premier Lord d\'Alterac, qui dirige les actions du Syndicat dans les montagnes Alterac, [npc=2597] dirige les actions du Syndicat dans les Hautes Terres d\'Arathi à partir de la principale demeure, le Donjon semi-abandonnée de Stromgarde, et Lady Beve Perenolde, fille d\'Aiden Perenolde.\n\n[h3]Histoire[/h3]\n\nPendant la seconde guerre, Lord Perenolde qui dirige le royaume d\'Alterac, a été découvert pour être en liaison avec les orcs de la Horde. Perenolde croyait qu\'une victoire de le Horde était inévitable et offrait ainsi une aide à la Horde en suscitant des rébellions, en attaquant les bases de l\'Alliance et en leur fournissant des armes. Lorsque cette trahison fut découverte, l\'Alliance marchait contre Alterac et la détruisit. Perenolde et tous les nobles qui ont accompagné ses projets ont été dépouillés de leurs titres et de leurs terres. Beaucoup d\'entre eux ont réussi à s\'échapper, mais ont commencé à comploter pour se venger. En utilisant leur fortune encore considérables, la noblesse a engagé une bande de voleurs et d\'assassins, formant une organisation connue sous le nom de Syndicat.\n\nAu début, le but du Syndicat était simplement de répandre le chaos et le désordre, frappant des bases cachées dans les montagnes d\'Alterac. Avec la fin de la troisième guerre et le chaos qui suivie, les dirigeants du Syndicat ont vu leur chance de reprendre Alterac et de retrouver leurs anciens pouvoirs. Ils ont maintenant pris le contrôle de plusieurs avant-postes dans la région environnante, y compris le donjon abandonnée et une partie de la ville de Stromgarde.\n\nIls sont haïe par l\'Alliance, qu\'ils considèrent comme leurs ennemis mortels, et la Horde, qu\'ils considèrent comme des brutes faits pour travailler en esclaves. En conséquence, le Syndicat est maintenant chassé par les deux factions, avec [npc=10181], en particulier, une prime est sur sa tête, tous les membres du Syndicat capturés seront exécutés sommairement. En outre, [npc=4949] a commandé un certain nombre de ses agents, y compris [npc=2229], [npc=2239], [npc=2238] et leur chef [npc=2316] pour lancer une enquête sur la nature du Syndicate et ses activités, ainsi que pour récupérer [item=3498], un collier maintenant porté par Elysa, la maîtresse de Lord Aliden, qui appartenait à un son cher ami, [npc=18887].\n\n[h3]Réputation[/h3]\n\nLe Syndicat, en tant que faction dans World of Warcraft, est très étrange par rapport à la plupart des factions. En effet, que le meurtre des membres de cette faction ne réduira pas votre réputation. Pour la plupart des joueurs, qui ne sont pas voleur, la seule façon d\'afficher le Syndicat dans leur menu de réputation est de compléter la quête [quest=8249]. Cependant, la quête requiert [item=16885] ... que seuls les voleurs peuvent obtenir en volant à la tir des PNJ au-dessus du niveau cinquante ce qui rend difficile d\'organiser une telle transaction.\n\nActuellement, il n\'y a qu\'une seule option connue pour augmenter la réputation d\'un joueur avec le Syndicat, en tuant des membres de la faction [faction=349]. Il n\'y a pas de récompenses connues pour avoir augmenté la réputation du Syndicat. Les PNJ affiliés à Ravenholdt ne donnent que 1 point de réputation, à l\'exception de [npc=13085], qui donne 5 (bien que la perte de réputation correspondante avec Ravenholdt soit aussi cinq fois plus grande ). Tous les joueurs commençent à une réputation détestée de 32000/36000, il faudrait tuer 10 000 PNJ de Ravenholdt pour atteindre le statut neutre avec la faction. Malheureusement, l\'état neutre est le plus élevé que vous puissiez atteindre avec le Syndicat, ce n\'est pas pour dissuader les joueurs, aucun des NPC Ravenholdt ne grimpe la réputation.\n\n[b]AVERTISSEMENT[/b]: Si vous décidez de tuer les PNJ de Ravenholdt, sachez qu\'il n\'y a actuellement aucun moyen de restaurer votre positionnement avec Ravenholdt, si vous passez en dessous de Neutre. La raison du problème est qu\'aucune des quêtes qui donnent des points de réputation de Ravenholdt ne sera disponible car aucun des membres de Ravenholdt ne vous parleront. Cela signifierait qu\'il s\'agit d\'un changement permanent et que vous ne pourrez plus jamais interagir avec l\'un des NPC fidèles à Ravenholdt. Notez également que les joueurs commencent à la réputation de 0/3000 avec Ravenholdt, et le fait de tuer même un de leurs PNJ à ce niveau de réputation vous empêchera pour toujours de rétablir votre réputation avec eux.',NULL),(8,59,2,NULL,0,2,'[b]La Confrérie du Thorium[/b] est un groupe d\'artisans d\'élite qui vend un certain nombre de recettes épiques, par contre, vous devez obtenir suffisamment de réputation avec eux. Tous les joueurs commencent à la réputation : Neutre.\n\n[h3]Histoire[/h3]\n\nLa [zone=51] abrite un groupe de nains exceptionnellement robustes qui se sont séparés du Clan Sombrefer. Sur les falaises surplombant la région appelée « Le Chaudron », dans le grand nord des Gorges des vents brulants, les nains de la Confrérie du Thorium ont établi une base d\'opérations, la Halte du Thorium. De là, ils surveillent de près les activités des nains de Sombrefer dans les Gorges des vents brûlants. Les aventuriers qui cherchent la Halte du Thorium trouveront que les nains de la Confrérie du Thorium qui donnent de grandes récompenses pour ceux qui les aident dans leur lutte sans fin contre leurs anciens frères.\n\nLa Confrérie du Thorium comprend de nombreux artisans exceptionnellement talentueux, et les forgerons de la Confrérie sont censés être parmi les meilleurs Azeroth. Ils possèdent les connaissances requises pour fabriquer les armes et les armures de [npc=11502], le Seigneur du Feu, mais n\'ont pas de main-d\'uvre pour obtenir les matériaux nécessaires à l\'artisanat. On raconte qu\'un membre de la Confrérie du Thorium a été habilité à échanger les recettes et les projets fabuleux des nains avec ceux qui peuvent prouver leur fidélité à la Confrérie. Bien sûr, pour prouver sa fidélité, l\'aventurier doit s\'aventurer au coeur de [zone=2717], le domaine de Ragnaros, le Seigneur du Feu lui-même, pour fournir aux nains les matières premières rares trouvées là-bas. Une tâche ardue, sans aucun doute, mais avoir accès aux secrets de la Confrérie du Thorium devrait s\'avérer être une récompense qui vaut bien l\'effort.\n\n[h3]Réputation[/h3]\n\n[b]De Neutre à Amical[/b]\n[ul]\n[li] Fournir : [item=18944], [item=3857] et [item=4234], [item=3575] ou [item=3356] au [npc=14624]. [/Li]\n[/ul]\n[b]De Amical à Honoré[/b]\n[ul]\n[li] Fournir : [item=18945] au [npc=14624]. [/Li] \n[/ul]\n[b]De Honoré à Exalté[/b]\n[ul]\n[li] Fournir : [item=11370] à [npc=12944]. [/Li]\n[li] Fournir : [item=17012] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17010] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17011] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=11382] à Lokhtos Sombrescompte. [/Li] \n[/ul]',NULL),(8,68,2,NULL,0,2,'[b]Fossoyeuse[/b] est la faction pour la capitale du même nom, [zone=1497], régie par Sylvanas Coursevent. La cité est situé dans la [zone=85], au bord nord des Royaumes de l\'Est. La ville proprement dite est sous les ruines de la ville historique de Lordaeron. Pour y entrer, vous traverserez les défenses extérieures en ruines de Lordaeron et la salle du trône abandonnée, jusqu\'à ce que vous atteigniez l\'un des trois ascenseurs gardés par deux abominations.\n\n[h3]Histoire[/h3]\n\nFossoyeuse était à l\'origine un système d\'égouts, de cryptes et de catacombes sous la capitale de Lordaeron. Après que la ville a été détruite par le Fléau, Arthas a reconstruit et agrandit le dédale de souterrain. Initialement, il voulait que Fossoyeuse soit son siège de pouvoir, d\'où il gouvernerait les terres de pestes. Cependant, peu de temps après la fin de la troisième guerre, Arthas a été obligé de retourner à Norfendre et de sauver le Roi Liche. En son absence, [npc=10181] et ses non-morts rebelles ont capturé les ruines de la ville. Peu de temps après, elle a découvert la grande forteresse souterraine et a décidé de l\'établir comme base principale des opérations pour les Réprouvés.\n\n[h3]Réputation[/h3]\n\n[npc=14729] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Fossoyeuse, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=squelette] chevaux squelettiques [/url].\n\nLes zones environnantes [zone=267], [zone=130], et la [zone=85] contiennent la plupart des quêtes pour gagner de la réputation auprès de Fossoyeuse.',NULL),(8,909,2,NULL,0,2,'La [b]Foire de Sombrelune[/b] est un mystérieux carnaval itinérant, qui parcourt non seulement Azeroth, mais aussi lOutreterre. Conduite par l\'inimitable [npc=14823], un gnome d\'héritage douteux et de racine inconnue. La Foire amène des jeux, des prix et des bibelots exotiques inattendus, puissants ou non, en [zone=215], à la [zone=12] ou à la [zone=3519] chaque mois.\n\nUne variété de divertissement est proposée par la Foire, mais l\'attraction la plus commune est la rédaction du billet. Plusieurs forains distribuent des [item=19182], répartis dans toute la Foire, ils offrent des bons contre des articles fabriqués par des travailleurs du cuir, des forgerons ou des ingénieurs ainsi que des objets rassemblés dans la nature tels que [item=11404] et [item=19933]. Les bons peuvent être échangés contre de nombreuses choses allant de la [item=19295] à des colliers de grande puissance.\n\nBeaucoup d\'aventuriers recherchent la Foire de Sombrelune pour trouver les mystiques [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]carte de Sombrelune[/url]. Les cartes de Sombrelune viennent en huit combinaisons, chacune ayant une suite de l\'As aux Huit. Avec la combinaison de toutes les cartes, la suite est créée qui commencera une quête pour vous envoyer à la foire de Sombrelune. \nChacune des huit suites produit un [url=?items=4.-4&filter=na=carte+sombrelune] bijou [/url] différent avec un effet différent, dont certains sont assez puissants.\n\nLe calendrier habituel de la Foire de Sombrelune arrive sur le site, le premier vendredi du mois et le départ commencera tôt le lundi suivant.',NULL),(8,76,2,NULL,0,2,'[b]Orgrimmar[/b] est la faction de la capital des orcs : [zone=1637]. Situé au bord nord de [zone=14], la ville imposante abrite le chef de guerre orcs, [npc=4949].\n\n[h3]Histoire[/h3]\n\nThrall a dirigé les orcs vers le continent de Kalimdor, où ils ont fondé une nouvelle patrie avec l\'aide de leurs frères tauren. En nommant leur nouvelle terre, Durotar, nom du père assassiné de Thrall, les orcs se sont installés pour reconstruire leur société autrefois glorieuse. La malédiction démoniaque sur leur race a pris fin, la Horde a décidé de passer dun discours de conquête avec une coalition lâche à la survie et à la prospérité pour tous. Aidé par les nobles Taurens et les Trolls rusés de la tribu Sombrelance, Thrall et ses orcs attendaient une nouvelle ère de paix dans leur propre pays.\n\nDe là, ils ont commencé la création de la grande ville guerrière, Orgrimmar. Nommé de l\'ancien chef de guerre, Orgrim [color=#ff143c]Doomhammer[/color], la nouvelle ville a été construite en peu de temps, à l\'aide des gobelins, des Taurens, des trolls et de [color=#ff122a]Mok\'Nathal Rexxar[/color]. En dépit d\'avoir des problèmes avec les centaures, les harpies, les lézards de tonnerre enragés, les kobolds, et malheureusement, l\'Alliance, Orgrimmar a prospéré et est devenu le foyer des orcs et des Trolls Sombrelance.\n\nAujourd\'hui, Orgrimmar se trouve à la base d\'une montagne entre Durotar et [zone=16]. Une ville guerrière en effet, elle abrite d\'innombrables quantités d\'Orcs, Trolls, Taurens, et une quantité croissante de Réprouvés rejoignent maintenant la ville, ainsi que les Elfes de Sang qui ont récemment été acceptés dans la Horde.\n\n[h3]Réputation[/h3]\n\n[npc=14726] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Orgrimmar, en récompense, les joueurs peuvent acheter des[url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=Loup] loups [/url].\n\nLes zones environnantes Durotar et [zone=17] contiennent la plupart des quêtes pour gagner de la réputation avec Orgrimmar.',NULL),(8,530,2,NULL,0,2,'[b]Les Trolls Sombrelances[/b], tribu de Trolls exilés, ont uni leurs forces avec [npc=4949] et la Horde. Ils appellent maintenant [zone=1637] leur maison, qu\'ils partagent avec leurs alliés Orc. [npc=10540] est leur chef actuel.\n\n[h3]Histoire [/h3]\n\nLorsque les rivalités tribales ont éclaté dans l\'ancien Empire Gurubashi, la tribu Sombrelance s\'est trouvée chassée de sa patrie dans [zone=33]. S\'étant installés dans ce que l\'on croit aujourd\'hui être les îles brisées, la tribu se retrouve bientôt enchevêtrée dans un conflit avec une bande de murlocs. Leur sort semblait scellé jusqu\'à ce que Thrall, chef de guerre Orc, et son armée, nouvellement libérés, s\'emparent de leurs maisons. Contrôlée par une sorcière des mers, un groupe de murlocs a capturé le chef des Sombrelances, Sen\'jin, avec Thrall et plusieurs autres Orcs et Trolls. Thrall a réussi à se libérer avec d\'autres, mais n\'a finalement pas pu sauver le chef des Trolls. Bien que Sen\'jin ait été sacrifié par la sorcière des mers, il a pu révéler une vision qu\'il avait eu, dans laquelle Thrall conduirait les Sombrelances hors des îles.\n\nAprès son retour, Thrall et ses partisans ont réussi à repousser de nouvelles attaques de la sorcière des mers et de ses murlocs, et se sont à nouveau dirigés vers Kalimdor. Sous la direction de [npc=10540], les Sombrelances ont alors juré allégeance à la Horde de Thrall et les ont suivi. Maintenant considérés comme ennemis par toutes les autres tribus Trolls sauf les Vengebroches et les Zandalar, les Sombrelances sont aujourd\'hui méprisés. Pourtant, les Trolls Sombrelances n\'ont pas oublié quils ont été chassés de leurs terres ancestrales et cette animosité gardée est accentuée avec limpatience, surtout vers les autres tribus Trolls. Après avoir atteint la nouvelle patrie des Orcs, [zone=14], les trolls se sont alors installés sur les rives orientales du royaume Orc, les îles Echo.\n\nCependant, avec l\'arrivée de Kul Tiras et de sa marine, les Sombrelances ont été forcés de reculer à l\'intérieur des terres sous l\'assaut du commandant. Les Trolls, se battant avec la Horde aux côtés de leurs frères, ont vaincu l\'ennemi. Les Trolls ont alors réclamé leur nouvelle patrie. Peu de temps après, un sorcier du nom de [npc=3205] a commencé à utiliser la magie noire pour prendre possession de ses collègues Sombrelances. Au fur et à mesure que son armée de disciples augmentait, Vol\'jin ordonna que les trolls restant évacuent, alors Zalazane prit le contrôle des îles Echo. Les Sombrelances se sont installés sur la rive voisine, en nommant leur nouveau village en hommage à leur ancien chef Sen\'jin. Du village de Sen\'jin, ils envoient, avec leurs alliés, des forces pour combattre Zalazane et son armée asservie.\n\n[h3]Réputation[/h3]\n\n[npc=14727] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Trolls Sombrelances, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0] Raptors [/url].\nLa zone environnante, Durotar, contient la plupart des quêtes pour gagner de la réputation avec les Trolls Sombrelances. De plus, les joueurs de niveau supérieur ont également une bonne quantité de quêtes dans [zone=3521].',NULL),(8,92,2,NULL,0,2,'[b]Les Gelkis[/b] sont une tribu de centaures qui ont construit leur campement dans les parties les plus au sud de [zone=405]. Ce sont les ennemis mortels des [faction=93], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Gelkis était [npc=13741], deuxième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5602] et ont pour représentant [npc=5397].\nLes Gelkis ne tiennent aucune alliance avec leurs tribus de frères, mais sont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nInitialement dirigé par le Second Khan Gelk, les Gelkis se situaient dans les régions les plus au sud de Desolace lorsque la tribu centaure se divisa en cinq.\nLorsque la tribu Gelkis s\'est prononcée contre le Khan Magra, une éternelle querelle entre les Magram et les Gelkis est née.\n\nLes Gelkis considérés comme plus civilisés que leurs frères avec une structure sociale organisée et une compréhension ferme de la langue commune, respectent la nature et leur mère de naissance Theradras. \nAlors que les Magram prônent la force comme essentielle et que la survie de la tribu dépend de leur esprit de combat.\n\nPour alléger ce conflit, Theradras veille toujours sur les centaures et gardera les tribus en sécurité et en vie. Les Gelkis ont alors demandé sa protection et donc le pouvoir de la terre maintien leur existence. \n\nBien que la Magram considère que cela soit faible, il semblerait que ce soit une vue erronée, car des élémentaires peuvent être aperçu dans Village Gelkis, mettant un terme aux intrus indésirables aux côtés de leurs maîtres centaures.\n\n[h3]Réputation[/h3]\n\nCest une des deux factions situées en Desolace, vous devez avoir une certaine réputation auprès des Gelkis pour commencer leurs quêtes. La réputation pour les Gelkis peut être obtenue en tuant les [url=?Npcs=7&filter=na=Magram]centaures Magram[/url].\n\nVous gagnez 20 points de réputation chez les Gelkis et perds 100 avec la tribu Magram.',NULL),(8,93,2,NULL,0,2,'[b]Les Magram[/b] sont une tribu de centaures qui construit leur campement dans les parties sud-est de [zone=405]. Ce sont les ennemis mortels de la [faction=92], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Magram était [npc=13740], troisième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5601] et ont pour représentant [npc=5398].\nLes Magram ne tiennent aucune alliance avec leurs tribus de frères, mais osont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nÀ l\'origine menée par le troisième Khan Magra, les Magram se situaient contre les chaînes de montagnes de Desolace lorsque la tribu centaure se divisa en cinq.\nAvant la mort de Magra, il a installé l\'idée que la force était essentielle et que la survie de la tribu dépendait de son esprit de combat. Quand leur frère, la tribu Gelkis, s\'est prononcée contre cette notion, une éternelle querelle entre les deux tribus est née.\n\nLa poursuite de la force a continué à travers les Khans Magram jusqu\'à ce jour, transformant les centaures en des êtres violents et déterminés. Pour solidifier leur titre de plus fort, la tribu lutte encore férocement pour affaiblir ou détruire leurs clans de frères, considérant les Kolkar comme faible, les Gelkis comme une nuisance, et les Maraudon comme un formidable ennemi.\n\nOn peut supposer que la culture Magram s\'est développée autour de la force de culte avant tout. Par rapport aux Gelkis, les Magram tiennent des formes très primitives de la parole et de la structure sociale. Par exemple, leur compréhension commune est limitée et la position de Khan serait vraisemblablement recherchée par un démon de la mort.\n\n[h3]Réputation[/h3]\n\nC\'est une des deux factions situées à Desolace, vous devez avoir une certaine réputation auprès des Magram pour commencer leurs quêtes. La réputation pour les Magram peut être obtenue en tuant [url=?npcs=7&filter=na=Gelkis]les centaures Gelkis[/url]. \n\nVous gagnez 20 points de réputation chez les Magram et perds 100 avec la tribu Gelkis.',NULL),(8,270,2,NULL,0,2,'Les trolls de la[b] Tribu Zandalar[/b] sont venus à île de Yojamba dans la [zone=33] pour recruter de l\'aide contre le Dieu du sang ressuscité et ses prêtres d\'Atal\'ai dans [zone=19] et [zone=1417].\n\n[h3]Histoire[/h3]\n\nLes Zandalar étaient les premiers trolls connus, tribu d\'où provenaient toutes les tribus. Au fil du temps, deux empires troll distincts ont émergé, l\'Amani et le Gurubashi. Ils existaient pendant des milliers d\'années jusqu\'à l\'avènement des Elfes de la nuit, qui ont combattu avec eux et ont finalement conduit les deux empires à l\'exil.\n\nÀ la suite du Great Sundering, les Gurubashi vaincus sont de plus en plus désespérés. En cherchant un moyen de survivre, ils ont enrôlé l\'aide du sauvage [npc=14834], également appelé Soulflayer. Hakkar s\'est transformé en un oppresseur impitoyable qui a exigé des sacrifices quotidiens de ses sujets, les Gurubashi se sont alors retournés contre leur sombre maître. Les tribus les plus fortes (y compris les Zandalar) se sont regroupées pour vaincre Hakkar et ses fidèles prêtres, les Atal\'ai. Les tribus unies ont vaincu le Dieu des Sang et ont expulsé les Atal\'ai, et malgré leur victoire, l\'Empire Gurubashi tomba peu de temps après.\n\nAu cours des dernières années, les prêtres d\'Atal\'ai ont découvert que la forme physique de Hakkar ne peut être convoquée que dans la capitale ancienne et déserte de l\'Empire Gurubashi, Zul\'Gurub. Malheureusement, au cur de cette nouvelle quête, les prêtres ont invoqué, avec succès, Hakkar, confirmant la présence du Soulflayer redouté au cur des ruines.\n\nAinsi, la tribu Zandalar est arrivée sur les rives d\'Azeroth pour combattre encore Hakkar. Mais le dieu du sang est devenu de plus en plus puissant, pliant plusieurs tribus à sa volonté, et même, commandant les avatars des dieux primitifs: chauve-souris, panthère, tigre, araignée et serpent. Avec les tribus trolls éparpillées, les Zandalri ont été forcés de recruter des aventuriers de diverse origine d\'Azeroth pour les rejoindre dans la bataille, et espèrent une fois de plus vaincre, le Soulflayer.\n\n[h3]Réputation[/h3]\n\nLa réputation avec la tribu Zandalar est obtenue en tuant les monstres et boss dans Zul\'Gurub. Des quêtes répétitives et spécifiques sont aussi disponibles, elles requièrent des éléments qui ont été abandonnés dans linstance. Chaque Zul\'Gurub donne environ 2 500 à 3 000 de réputation.\nAvant la croisade brûlante, la principale raison de monter la réputation avec la tribu était les enchantements [url=?Items=0.6&filter=na=Zandalar]dépaule[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]de tête et de jambe[/url]. De plus, il y avait des pièces darmure en récompense de quête à faire dans Zul\'Gurub nécessitant un niveau de réputation.',NULL),(8,471,2,NULL,0,2,'[b]Les Marteaux-hardis[/b] sont un clan de nains actuellement centrés dans [zone=47] et la [zone=3520]. La faction a été supprimée dans le patch 2.0.1.\n\n[h3]Histoire[/h3]\n\nJuste avant le [objet=175739], le clan Marteaux-hardis, dirigé par Thane Khardros Marteaux-hardis, habitait les contreforts et les falaises autour de Forgefer. Le clan Marteaux-hardis a échoué à prendre le contrôle de [zone=1537], des clans Barbe-de-bronze et Sombrefer. Khardros et ses guerriers Marteaux-hardis se sont rendus au nord par les barrières de Dun Algaz et ont fondé leur propre royaume dans le lointain sommet de Grim Batol. Là, les Marteaux-hardis ont prospéré et reconstruit leurs richesses.\n\n[npc=9019] et ses Sombrefer ont juré de se venger de Forgefer. Thaurissan et sa femme sorcière, Modgud, ont lancé un attentat contre Forgefer et Grim Batol. les forces de Modgud ont commencé à franchir les portes de Grim Batol, elle a utilisé ses pouvoirs pour frapper la peur dans leurs curs. Les ombres se déplaçaient à son commandement, et des choses sombres se glissaient dans les profondeurs de la terre pour traquer les Marteaux-hardis dans leurs propres retranchements. Finalement, Modgud a franchi les portes et a assiégé la forteresse elle-même. Les Marteaux-hardis se sont battus désespérément, Khardros lui-même sest lancé dans la bataille pour tuer la sorcière reine. Avec leur reine perdue, les Sombrefer ont fui avant la fureur des Marteaux-hardis.\n\nUne fois que la menace immédiate des Sombrefer a été éliminée, les Marteaux-hardis sont rentrés à Grim Batol. Cependant, la mort du Modgud avait laissé une tache maléfique sur la forteresse de la montagne, et les Marteaux-hardis la trouvaient inhabitable. Khardros a conduit son peuple vers le nord vers les terres de Lordaeron. En s\'installant dans la région montagneuse des Hinterlands, et ces forêts luxuriantes, les Marteaux-hardis ont construit la ville de Nid-de-laigle, où les Marteaux-hardis se sont rapprochés de la nature et même liés aux puissants griffons de la région.\n\nLa menace la plus immédiate pour leurs sécurités vient de l\'est sous la forme de deux clans trolls, les Vilebranches et les Fanécorces. Ils sont les plus célèbres pour organiser des batailles contre la ville des Marteaux-hardis, tout en brandissant des armes puissantes.\nLes nains Marteaux-hardis ont un certain nombre de clans, chacun gouverné par un Thane. Le plus fort Thane règne sur Nid-de-laigle.',NULL),(8,509,2,NULL,0,2,'[b]La Ligue d\'Arathor[/b] a été initialement établie par les survivants du Royaume de Stromgarde pour récupérer la [zone=45] des mains des Profanateurs au Trépas d\'Orgrim. Aujourd\'hui, c\'est une organisation à l\'appui de l\'Alliance, basée sur [zone=3358] dans le Refuge de lOrnière. Ils se sont chargés d\'aider à fournir des forces, pour l\'Alliance, lorsque cest nécessaire, leurs membres incluent toutes les races de l\'Alliance mais se sont encore principalement des humains stromgardiens.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec Ligue dArathor et les deux autres factions du champ de bataille, [faction=890] et [faction=730].',NULL),(8,730,2,NULL,0,2,'[b]Les Gardes Foudrepiques[/b] est la faction de l\'Alliance dans le champ de bataille [zone=2597]. Ils sont une expédition de nains du clan Foudrepique, originaire des « vallées d\'Alterac » dans [zone=36]. La recherche des Foudrepiques pour les reliques de leurs passés et la récolte de ressources dans la vallée d\'Alterac ont conduit à une guerre ouverte avec les Orcs de la [faction=729] habitant dans la partie sud de la vallée. Ils ont également reçu un « ordre de la souveraineté impérialiste » par [npc=2784] pour prendre les vallées d\'Alterac pour [zone=1537].\n\nLa principale base des Foudrepiques est Dun Baldar, où son chef, [npc=11948], réside avec ses maréchaux. Son second commandant, [npc=11949], se trouve au sud de Dun Baldar, à Cur de pierre.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée dAlterac, en faisant diverses tâches et en tuant les membres de la faction adverse, le clan Frostwolf.\n\nOn vous accorde le titre : [title=48] au joueur, une fois quil est exalté avec les Gardes Foudrepiques et les deux autres factions des champs de bataille, [faction=890] et [faction=509].',NULL),(8,510,2,NULL,0,2,'[b]Les Profanateurs[/b] cherchent à feuilleter la [faction=509] dans le champ de bataille, [zone=3358]. Aujourd\'hui, c\'est une organisation à l\'appui de la Horde, basée au Trépas dOrgrim dans [zone=45]. Ils se sont investis pour aider les forces de la Horde, au besoin, et leurs membres incluent toutes les races de la Horde, même si, se sont encore principalement des Orcs.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec les Profanateurs et les deux autres factions du champ de bataille, [faction=889] et [faction=729].',NULL),(8,529,2,NULL,0,2,'L[b]Aube dArgent[/b] est une organisation axée sur la protection d\'Azeroth des menaces qui cherchent à la détruire, comme la Légion Ardente et le Fléau. Les forteresses de l\'Aube d\'Argent se trouvent dans les [zone=139] et les [zone=28]. Elle maintient également une présence dans [zone=1657] et dans les [zone=85], et dans dautres zones moins remarquables. La réputation avec lAube dArgent peut être utilisée pour acheter divers plans, consommables, et pour atténuer le coût à [zone=3456]. Avec l\'expansion « Burnning Croisade », la réputation de lAube dArgent a diminué en valeur.\n\nLe [item=22999] a pour icône un lever de soleil argenté.\n\n[h3]Histoire[/h3]\n\nAprès la mort du [npc=16062], la corruption de la Croisade Écarlate est devenu évidente pour certains de ses membres, qui ont par la suite abandonné les rangs de la [url=?npcs&filter=na=croisade%20écarlate;ex=on]Croisade Écarlate[/url] et a créé lAube dArgent pour protéger Azeroth de la menace du Fléau sans présence de fanatique dans la Croisade Écarlate.\n\nAlors qu\'ils partagent les mêmes objectifs que la Croisade, lAube dArgent a ouvert ses rangs non seulement aux races de l\'Alliance, mais aussi aux membres de la Horde et même à certains des Réprouvés. Ils mettent en garde contre la discrétion et l\'introspection, et mettent beaucoup l\'accent sur la recherche du Fléau et sur la façon de le combattre.\n\nAvec le temps, lAube dArgent s\'est diversifié, comme le Fléau qui s\'est divisé de nouveau, avec un rejeton appelé la Fraternité de la Lumière, un compromis entre l\'approche plus savante de lAube dArgent et le fanatisme de la Croisade écarlate.\n\n[h3] Réputation [/h3]\n\n[b]Les pierres du Fléau[/b]\nTout en portant un bijou accordant l\'effet « Commission pour lAube dArgent », les personnages peuvent tuer des monstres mort-vivants pour leurs [url=?items=12&filter=cr=151;crs=6;crv=43169;na=pierre%20du%20fléau] pierres du Fléau[/url] et ensuite les transformer en monnaies échange contre [item=12844]. Les quêtes requièrent beaucoup de [item=12843], [item=12841] et [item=12840]. Il convient de noter que les monnaies déchanges reçus des entités doivent être sauvegardés jusqu\'à ce que le statut de Révéré soit atteint, car les quêtes ne donneront plus de réputation après.\n\nUne autre façon daugmenter la réputation avec lAube dArgent est de faire la quête répétable « Chaudron ». Les chaudrons sont une source de « production » de membres du Fléau.\n\nComme la plupart des factions, le joueur peut faire des instances pour augmenter sa réputation. Les instances associées sont [zone=2017] et [zone=2057]. Naturellement, ces instances incluent également des quêtes qui augmentent la réputation de lAube dArgent.',NULL),(8,933,2,NULL,0,2,'[b]Le Consortium[/b],dirigé par [npc=19674], sont des passeurs éthérés, des commerçants et des voleurs qui sont venus en Outreterre. Le principal base d\'opérations et le plus grand rassemblement se trouve à Foudreflèche, mais ils peuvent être trouvés à[color=#ff0537] Midrealm Post[/color], Aeris Landing, près d\'Auchindoun à [zone=3792] et dans d\'autres endroits.\n\nEn arrivant à un statut amical, les joueurs sont officiellement considérés comme membres du Consortium et bénéficient d\'un salaire. Le salaire est un sac de gemmes au début de chaque mois, donné par [npc=18265] chez Aeris Landing. Une plus grande réputation avec le Consortium produit des qualités et quantités supérieures de gemmes chaque mois.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à Amical[/b]\n[ul]\n[li]Faire le donjon Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 points de réputation[/li]\n[li]Donner des [item=25416] à [npc=18265].[/li]\n[li]Donner des [item=25463] à [npc=18333].[/li]\n[/ul]\n\n[b]De amical à honoré[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 point de réputation.[/li]\n[li]Activer les [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode héroïque[/i] rapporte environs 2 400 points de réputation.[/li]\n[li]Faire toutes les [url=?Quêtes et filtre=cr=1;crs=933;crv=0]quêtes[/url].[/li]\n[li]Donner des [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\nToutes personnes qui essayent de gagner simultanément la réputation du Consortium et des [faction=941] ou [faction=978] peuvent se concentrer à tuer des ogres ([url=?npcs&filter=na=rochepoing;cr=6;crs=3518;crv=0]Rochepoing[/url], [url=?npcs&filter=na=cogneguerre;cr=6;crs=3518;crv=0]Cogneguerre[/url]) à Nagrand et rendre les perles de guerre obsidienne au Consortium.\n\nLa seule mise en garde est le taux de loot, soit environ 33% pour les Cogneguerre, alors qu\'il est de 50% pour les insignes. Si vous êtes au niveau 70 et que vous voulez monter cette réputation plus rapidement sans se soucier de la réputation de Mag\'har / Kurenai, vous voudrez peut-être donner des insignes à la place. Ensuite, les ogres sont généralement plus faciles à tuer, allant du niveau 65 à 67. Le choix dépend finalement du joueur.',NULL),(8,932,2,NULL,0,2,'[b]L\'Aldor[/b] est un ancien ordre de prêtres draeneïs qui vénèrent les naaru, et à ce jour ils assistent les naaru [faction=935] dans leur combat contre [npc=22917] et la Légion Ardente. Ils se trouvent principalement dans la [zone=3520] et [zone=3703]. Bien qu\'ils aient beaucoup souffert des Elfes du sang qui sont devenus [faction=934], ont mis de côté une guerre ouverte contre les Sha\'tar. Le temple le plus saint de l\'Aldor repose sur léminence de l\'Aldor, surplombant la ville à l\'ouest.\n\nLa plupart des joueurs commenceront à une réputation neutre auprès de l\'Aldor. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec Aldor ou Les clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin.\nLes joueurs de Draenei seront directement amicaux avec Aldor et hostiles avec les Clairvoyants, alors que les joueurs Elfe du sang seront hostiles à l\'Aldor et amicaux envers les Clairvoyants.\n\n[npc=19321] et [npc=20807] sont situés dans la banque Aldor, sur le bord nord de la terrasse de la lumière. Le sanctuaire de la lumière sans fin sur léminence de l\'Aldor abrite [npc=20616] [petit][/small] et [npc=21906] [petit][/small], qui échangent, respectivement, des jetons épiques d\'armure contre des pièces de set de [url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec Aldor correspondent à une perte de réputation de 10% plus élevée chez les Clairvoyants. La plupart des gains de réputation avec Aldor accorderont également 50% de la réputation avec le Sha\'tar.[/i]\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29425][/span] à [npc=18537] dans léminence de l\'Aldor accordera 250 points de réputation pour l\'Aldor. Il existe également une quête répétable où donner une unique marque accorde 25 points de réputation. Ces marques tombent sur des membres inférieurs de la Légion Ardente trouvés dans la plupart des zones de Outreterre, y compris les deux camps au nord d\'Auchindoun dans les déchets osseux de [zone=3519].\nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique marque.\n\nLes joueurs qui souhaitent également faire la réputation des factions [faction=978] ou [faction=941] iront tuer des Orcs à la forteresse de Kil\'Sorrow dans le sud-est de [zone=3518], car ils donnent des marques ainsi que 10 points de réputation auprès des Kurenai ou des Mag\'har.\n\n[b]Jusqu\'à Exalté[/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1] [item=30809][/span], c\'est le même principe que les marques de Kil\'jaeden mais ceux-ci tombent sur des partisans de haut rang de la Légion Ardente. Si vous le souhaitez, vous pouvez transformer les marques de niveau supérieur avant la réputation honorée. Dans [zone=3522], la porte de la mort dispose du plus grand nombre de membre avec ce grade.\n\n[b]Arme gangrenée[/b]\n\n[span class=q2][item=29740][/span] peut être donné à tout moment à [npc=18538] [small][/small] à léminence de l\'Aldor. Cela augmentera votre réputation avec l\'Aldor de 350 par arme gangrenée.\nEn plus des gains de réputation, vous recevrez [span class=q1][item=29735][/span], qui est la condition pour acheter lenchantement d\'épaule à [npc=20807] dans la banque de l\'Aldor.\n\n[h3]Passer à la réputation de l\'Aldor[/h3]\n\nPour changer votre faction des Claivoyants vers l\'Aldor et donc pour accéder à leurs recettes d\'artisanat (et annuler toutes les réputations que vous avez faites), trouvez [npc=18597], un membre de l\'Aldor dans la ville basse. Elle propose une quête répétable où pour 8x [span class=q1][item=25802][/span] vous montez la réputation Aldor. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.',NULL),(8,922,2,NULL,0,2,'[b]Tranquilliens[/b] a été reprise par les Réprouvés et les Elfes de sang puis est devenu une faction des [zone=3433].\n\n[h3]Histoire[/h3]\n\nAlors que l\'armée du Fléau faisait son chemin vers le Puit-du-Soleil, les elfes n\'avaient pas d\'autre choix que de se retirer, Tranquillien fût donc abandonnée. La ville est maintenant utilisée par les Elfes de sang et les Réprouvés comme base d\'opération pour lancer des attaques visant à reprendre les Terres Fantômes. Cependant, la ville est entourée par le fléau, même les courriers ont du mal à traverser l\'ennemi pour atteindre la ville. Les forces mortels de Mortholme sont la menace la plus dangereuse pour la ville.\n\n[h3]Réputation[/h3]\n\nContrairement à la plupart des zones de départ, la ville de Tranquillien a sa propre faction.\nToutes les quêtes que vous effectuez pour eux accumuleront au moins 1000 points de réputation. [npc=16528] agit comme lintendant des Tranquilliens. Vredigar peut être trouvé près de l\'auberge et vendra divers éléments [span class=q2]commun[/span], et même un manteau [span class=q3]rare[/span] lorsque vous atteignez la réputation exaltée.\n\nSi vous complétez toutes les quêtes des Tranquilliens, vous devriez être exalté.\nIl existe une variété de quêtes concernant principalement la récupération des villages envahis, l\'enquête sur les morts-vivants et l\'aide apportée à la population. La suite de quête prend « fin » avec la quête où il faut tuer [npc=16329].',NULL),(8,910,2,NULL,0,2,'La [b]Progéniture de Nozdormu[/b] est une faction composée du vol Draconique de bronze. Leur chef, [npc=15192], se trouve à l\'extérieur des [b]Grottes du temps[/b], avec beaucoup de ses agents volant dans le ciel de [zone=1377].\n\nPour ouvrir les portes d[b]Ahn\'Qiraj[/b], un champion doit compléter une longue ligne de quête pour le dragon de bronze Anachronos. Cette réputation est également présente dans [zone=3428]; Elle permet dobtenir des équipements et des bagues épiques.\n\n[h3]Réputation [/h3]\n\nLes joueurs commencent leur réputation au plus bas niveau possible, cestà-dire 0/36000 de détestés.\n\nLa réputation de la Progéniture de Nozdormu peut être gagnée en tuant des monstres à l\'intérieur du temple d\'Ahn\'Qiraj et en faisant des quêtes liées. Vous pouvez également exploiter [item=20384], cela prend beaucoup plus de temps et nécessite l\'obtention de [item=20383] dans [zone=2677] pour la suite de quête [item=21175].\n\nTuer des monstres dans le temple d\'Ahn\'Qiraj ne permet que datteindre une réputation de 2999/3000 de neutre, la réputation ne peut donc être avancée que par des quêtes et la remise de [item=21229] et [item=21230]. \nUn conseil, gardez tous les insignes jusqu\'à ce que vous soyez à une réputation neutre, car à ce moment-là, cela devient beaucoup plus difficile.',NULL),(8,749,2,NULL,0,2,'Les [b]Hydraxiens[/b] sont des élémentaires qui se sont installés sur les îles à l\'est de [zone=16]. Les ennemis jurés des armées de [npc=11502]. Historiquement serviteurs des Anciens Dieux, les quatre Lords Élémentaires ont servi les dieux avec une loyauté éternelle. Les minions de Neptulon, le chasse-marée, étaient nombreux et insensés. On ne sait pas encore comment le [npc=13278] a libéré le contrôle de son seigneur ou quels sont ses objectifs ultimes, mais les élémentaires deau sont les seuls éléments qui n\'attaquent pas les races mortelles.\n\nSitué sur une île éloignée dans l\'extrême est d\'Azshara, le Duke Hydraxis propose des quêtes. Les deux premiers nécessitent de tuer divers élémentaires dans les [zone=139] et en [zone=1377]. Une réputation accrue avec les Hydraxiens ouvre des quêtes supplémentaires menant à [zone=2717]. Tous les objets obtenus auprès des Hydraxiens sont gagnés à partir de différentes missions.\n\nL\'achèvement de la suite de quête permet aux joueurs d\'obtenir [item=17333] utilisé pour endommager les runes trouvées près de la plupart des boss dans Cur de Magma. Ceci est nécessaire pour convoquer [npc=12018], l\'avant-dernier boss, et, après sa défaite, pour convoquer Ragnaros lui-même. Comme il y a sept runes, tout raid nécessite au moins sept joueurs qui apportent une quintessence s\'ils souhaitent terminer l\'instance. Comme la majeure partie de la suite de quête a lieu au sein de Cur de Magma, toutes personnes du raid peuvent compléter cette tâche avec un peu plus que quelques voyages et une course au [zone=1583].\n\n[h3] Réputation [/h3]\n\nLa réputation des Hydraxiens est obtenue en tuant les ennemis élémentaires suivants :\n[ul][li] [npc=11746] - 5 points de réputation, jusqu\'à l\'Honoré. [/li]\n[li] [npc=11744] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=7032] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=9017] - 15 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=14478] - 25 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=9816] - 50 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11658], [npc=11673], [npc=12101] et [npc=11668] - 20 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11659], [npc=12100], [npc=12076], [npc=11667] et [npc=11666] - 40 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264] et [npc=12098] - 100 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11988] - 150 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11502] - 200 points de réputation, jusqu\'à Exalté. [/li][/ul]\n\nLa réputation au statut de Révéré avec les Hydraxiens permet aux joueurs dobtenir le [item=22754], qui se recharge. Et donc évite la nécessité de retourner à Hydraxis pour obtenir une nouvelle quintessence chaque semaine.',NULL),(8,609,2,NULL,0,2,'Le [b]Cercle Cénarien [/b] est une organisation de druides, à la fois tauren et elfe de nuit, nommé d\'après Cénarius. Ses membres se consacrent à la protection de la nature et à la restauration de celle-ci suite aux dégâts subis par des forces malveillantes.\n\nLe Cercle a de nombreux sites, mais leur ville principale est la ville de Havre- nuit dans la [zone=493]. Les druides apprennent le sort [sortilège=18960] au niveau 10, mais il est aussi possible dy arriver par [zone=361] via le tunnel des Grumegueles.\n\nLe cercle Cénarien est aussi beaucoup présent en [zone=1377], où ils combattent les Silithides, les Qirajis et larmée du crépuscule. Le repos du vaillant et le Fort Cénarien servent de base dans ces terres hostiles et offrent de nombreuses opportunités aux aventuriers qui cherchent à aider les druides.\n\n[h3]Membres notables[/h3]\n\n[ul][li][npc=11832], fils de Cenarius [/li]\n[li][npc=3516], chef des druides - elfes de la nuit [/li]\n[li][npc=5769], chef des druides - Taurens [/li][/ul]\n\n[h3]Réputation[/h3]\n\nIl existe plusieurs façons de se faire connaître avec le cercle Cénarien.\nMise à part les [url=?Quests&filter=cr=1;crs=609;crv=0]quêtes[/url], vous pouvez faire ce qui suit pour gagner en réputation: \n[ul]\n[li]Le raid des [zone=3429] est de loin le moyen le plus rapide de gagner en réputation, car un clean complet peut dépasser 2000 points de réputation. [/li]\n[li] Tuez larmée du crépuscule. Elle cesse daugmenter une fois que vous atteignez la réputation Honoré pour [npc=11880] et [npc=11881], et Révéré pour [npc=15201].[/li]\n[li] Trouvez des [item=20404 ]. Ceux-ci se trouvent sur larmée du crépuscule et produisent 250 points de réputation pour 10 textes.[/li]\n[li] Trouvez des [item=20513], [item=20514] et [item=20515]. Ceux-ci se trouvent sur les mini-boss qui sont convoqués aux pierres de vent en utilisant [itemset=492]. [/li]\n[li] Effectuez la quête : [quest=8507]. Ce sont soit des [url=?search=logistique+Briefing] Quêtes de logistique [/url], des [url=?search=combat+Briefing]quêtes de Combat[/url] ou des [url=?search=tactique+Briefing] Quêtes tactiques [/url]. Les badges que vous gagnez de ces quêtes peuvent être transformés en réputation supplémentaire, si vous choisissez d\'abandonner les récompenses. [/li]\n[li] Collectez les [object=181598] de la zone et rendez les à votre faction.[/li]\n[/ul]',NULL),(8,589,2,NULL,0,2,'Les [b]Éleveurs de sabres-d\'hiver[/b] est une faction de l\'Alliance composée de deux Elfes de la nuit qui peuvent être trouvés au [zone=618]. À l\'heure actuelle, le seul donneur de quête est [npc=10618], qui est situé au sommet du Rocher des Sabres-d\'hiver au Berceau-de-lhiver. En atteignant un niveau de réputation exalté avec cette faction, Rivern vendra une monture spéciale, le [item=13086].\n\nLa monture de cette faction est la seule monture épique, ayant une vitesse de 100%, utilisable avec une compétence en équitation de 75. La faction est connue pour ne pas avoir déquivalant côté Horde et être la plus longue et la plus répétitive des réputations à monter dans l\'ensemble du jeu. La première quête peut être faite au niveau 58, tandis que les deux autres sont réalisables quau niveau 60.\n\n[h3]Réputation[/h3]\n\nLa réputation avec les Éleveurs de sabres-d\'hiver ne peut être obtenue que par trois quêtes répétables. Il n\'y a pas d\'objets de faction ni de mobs qui récompensent la réputation directement.\n\n[b]De neutre 0 à 1500[/b]\n\nUne seule quête répétable sera disponible jusqu\'à ce quune réputation de 1500/3000 soit atteinte, la quête : [quest=4970] doit donc être répétée. Tous les [url=?npcs&filter=cr=6;crs=618;crv=0;na=Croc%20acéré]Ours[/url] et [url=?npcs&filter=cr=6;crs=618;crv=0;na=Noroît]Noroît[/url] au Berceau-de-lhivers peuvent looter les objets de quête. Cette quête doit être effectuée en solo, car les taux de loot sont faibles et ne sont pas partageables si d\'autres ont la quête.\n\n[b]De neutre 1500 à exalté [/b]\n\nÀ mi-chemin du neutre, la quête : [quest=5201] sera disponible. Cette quête nécessite de tuer 10 Tombe-hivers dans le village Tombe-hivers, juste à l\'est de Long-guet. Si la quête : [quest=8464] a été effectuée pour [faction=576], les [item=21383] peuvent tomber sur les Tombe-hivers. Si un joueur veut les deux réputations, il préférable quil les gardes jusquà ce quil soit Révéré avec les Grumegueules. Ce qui entraînera beaucoup de réputation \"gratuite\".\n\nCette quête peut se faire en groupes pour aller plus vite. Les joueurs qui augmentent les réputations des Éleveurs de sabres-d\'hiver et des Grumegueules peuvent être trouvés dans le village des Tombe-hivers. Même en épique, le voyage vers le village Tombe-hivers prend beaucoup de temps. Il y a des tigres sur la route qui vous étourdiront, ce qui entraînera un désarçonnement, cela devrait être évité (mais peut être difficile car ils vont vous rattraper sur une monture de 60%). \n\n[b]De honoré à exalté[/b]\n\nA partir dhonoré, la troisième quête : [quest=5981] est disponible. La quête exige que le joueur tue 8 géants. Ils sont beaucoup plus difficiles que les Tombe-hivers et le trajet est assez long. Cette quête est généralement ignorée.\n\nEn raison de certains joueurs qui augmentent la réputation des Grumegueules, dans le village de Tombe-hivers, cette quête peut effectivement se révéler une récompense de réputation plus rapide que [quest=5201].',NULL),(8,576,2,NULL,0,2,'[b]Les Grumegueules[/b], dernière tribu furbolg non-corrompue (au moins dans leur point de vue), cherchent à conserver leurs voies spirituelles et à mettre fin à la souffrance de leurs frères.\n\nLes Grumegueules habitent deux zones : [zone=16] et [zone=361]. Ils sont présumés être la seule tribu furbolg à échapper à la corruption démoniaque, mais ce n\'est peut-être pas vrai, en raison de l\'existence de [npc=3897], furbolg de tribu inconnue, et la tribu Stillpine sur [zone=3524]. Cependant, de nombreuses autres races tuent les furbolgs aveuglément maintenant, sans savoir si elles sont alliées ou non. Pour cette raison, les Grumegueles ne se montrent pratiquement pas.\n\nLes aventuriers qui recherchent les Grumegueules dans le nord de Gangrebois et s\'aventurent chez eux apprendront quil faut mieux être leurs alliés. Bien qu\'ils ne possèdent pas de bijoux fins ou de richesses mondaines, la tradition chamanique des Grumegueules est encore forte. Ils connaissent bien l\'art de fabriquer des armures à partir de peaux d\'animaux, et ils sont plus qu\'heureux de partager leurs connaissances de guérison avec des amis de leur tribu. En outre, à partir dune réputation inamical, les Grumegueules vous accorderont également un accès sans problème à [zone=493] et [zone=618] dans leurs tunnels.\n\n[h3] Réputation[/h3]\n\nLa réputation avec la faction des Grumegueules est principalement acquise grâce à des quêtes. Les membres de la tribu Mort-bois, une autre tribu de Furbolg à Gangrebois, sont les principaux ennemis des Grumegueules et peuvent être tué pour gagner de la réputation.\n\n[ul]\n[li] Tuer des furbolgs [url=?Npcs&filter=na=Tombe-hivers]Tombe-hivers[/url] ou [url=?Npcs&filter=na=Mort-bois]Mort-bois[/url], donne 10 points de réputation. Les gains s\'arrêtent à révéré. [/li]\n[li] Tuer [npc=9464] ou [npc=9462], donne 60 points de réputation.[/li]\n[li] Tuer [npc=10738], située dans une grotte à l\'est de [faction=577], donne 50 points de réputation. Son taux de réapparition est de 6 à 8 minutes. [/li]\n[li] Tuer [npc=14342], élite rare, donne 50 points de réputation. Il se situe au village des Mort-bois à Gangrebois. Donne de la réputation jusquà exalté. [/ Li]\n[li] Tuer [npc=10199], élite rare, donne 50 points de réputation. Il se situe dans le village des Tombe-hivers au Berceau-de-lHivers. Donne de la réputation jusquà exalté. [/li]\n[li] Après avoir terminé la quête : [quest=8460], avec les [item=21377] ramassés sur les Furbolgs Mort-bois, la réputation augmente de 150 points. [/li]\n[li] Après avoir terminé la quête : [quest=8464], avec les [item=21383] ramassés sur les furbolgs Tombe-hivers, la réputation augmente de 150 points.[/li]\n[/ul]',NULL),(8,890,2,NULL,0,2,'[b]Les Sentinelles d\'Aile-argent[/b] représente la faction de l\'Alliance sur le champ de bataille [zone=3277]. Les elfes de la nuit, qui ont commencé une avancée massive pour reprendre les forêts de [zone=331], concentrent leur attention sur le débarquement sur leur terre de la [faction=889] une fois pour toutes. Et ainsi, les Sentinelles d\'Aile-argent ont répondu à l\'appel et ont juré qu\'ils ne vont pas se reposer avant que tous les orcs soient vaincus et expulsés du Goulet des Chanteguerres.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois quil est exalté avec Les Sentinelles d\'Aile-argent et les deux autres factions des champs de bataille, [faction=730] et [faction=509].',NULL),(8,889,2,NULL,0,2,'[b]Les Voltigeurs Chanteguerre[/b] est un clan orc précédemment dirigé par [npc=18076], daprès lequel le clan a été nommé. Les Voltigeurs Chanteguerre représentent la faction de la Horde sur le champ de bataille [zone=3277], où ils tentent de défendre leurs opérations d\'enregistrement dans [zone=331] de la [faction=890].\n\nCest l\'un des clans les plus forts et les plus violents, le clan de Chanteguerre était également l\'un des clans les plus distingués de Draenor, ce clan a pu échapper aux forces de l\'expédition de l\'Alliance à chaque tournant. Formés comme Grunts, ils ont maîtrisé l\'utilisation d\'épées et de lames et quelques-uns ont même atteint le rang de Maître-lames.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois quil est exalté avec Les Voltigeurs Chanteguerre et les deux autres factions des champs de bataille, [faction=510] et [faction=729].',NULL),(8,729,2,NULL,0,2,'[b]Le Clan Loup-de-givre[/b], ainsi que [npc=11946], ont vécu dans [zone=36] et ont des Loups de givre comme compagnons. Des nains, connue sous le nom de [faction=730], ont commencé une expédition dans le territoire des Loup-de-givre pour creuser la vallée et miner les veines. Une transgression envers les Orcs qui habitaient en Alterac. Cela a provoqué lextermination de la première expédition et la bataille pour [zone=2597] a commencé.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée dAlterac, en effectuant diverses tâches et en tuant les membres de la faction opposée, les Gardes Foudrepiques.\n\nOn vous accorde le titre : [title=47] au joueur une fois quil est exalté avec le clan Loup-de-givre et les deux autres factions des champs de bataille, [faction=889] et [faction=510].',NULL),(8,935,2,NULL,0,2,'[b]Les Sha\'tar[/b], ou \"né de la lumière\", sont des naaru qui ont aidé [faction=932], l\'ordre des prêtres draenei précédemment dirigés par [npc=17468], en reconstruction à [zone=3703]. La ville a été détruite par les Orcs pendant leur fuite à travers Draenor avant la Première Guerre mondiale. \nLa défaite de la Légion ardente est le but ultime des Sha\'tar. Les Sha\'tar sont aidés dans cette guerre par l\'Aldor et leurs rivaux, la faction des elfes du sang connue sous le nom : [faction=934]. \nL\'Aldor et les Clairvoyants se battent pour la faveur du Sha\'tar afin qu\'ils puissent être aidés dans leur guerre pour les pouvoirs des naaru. L\'entité qui dirige le Sha\'tar est connue sous le nom de [npc=18481] ; Il peut être trouvé sur la terrasse de la lumière dans la ville de Shattrath.\n\nLes joueurs de l\'Alliance et de la Horde commencent avec une réputation neutre auprès des Sha\'tar. Les joueurs peuvent augmenter leur réputation, Sha\'tar, à travers diverses quêtes, en élevant leur réputation avec lAldor ou les clairvoyants, ou en s\'aventurant dans le [url=?search=donjon+tempête]donjon des tempêtes [/url].\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLa réputation peut être obtenue à partir de divers objets. Ce qui suit n\'accordera que de la réputation de Sha\'tar jusqu\'à ce que vous obteniez un statut honoré : \n[li]Pour une réputation envers les Clairvoyants : [item=29426], [item=30810] et [item=29739][/li]\n[li]Pour une réputation envers l\'Aldor : [item=29425], [item=30809] et [item=29740][/li]\n\n[i]Notez que ce gain de réputation ne s\'affiche pas dans le journal de combat, mais peut être vérifié en regardant votre panneau de réputation.[/i]\n\nLa réputation peut également être obtenue en faisant le temple des tempêtes : [zone=3847], [zone=3846] et [zone=3849].\n\n[b]Jusquà exalté [/b]\n\nAprès avoir épuisé les récompenses de réputation de Aldor ou des Clairvoyants, les joueurs souhaiteront peut-être compléter les quelques quêtes de Sha\'tar disponibles. En plus des quêtes, les instances qui se trouvent au temple des tempêtes : Botanica, Arcatraz et Mechanar continueront à accorder de la réputation. À ce stade, il est probablement plus utile d\'exécuter ces instances en mode héroïque.',NULL),(8,934,2,NULL,0,2,'[b]Les Clairvoyants[/b] sont des elfes de sang qui résident dans [zone=3703] dirigé par [npc=18530]. Le groupe s\'est éloigné de [npc=19622] et a offert de leur aide au Naaru de Shattrath. Ils sont en désaccord avec [faction=932], et rivalisent avec eux pour le pouvoir de Shattrath et la faveur du Naaru. \n\nLa plupart des joueurs commenceront avec une réputation neutre auprès des Clairvoyants. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec lAldor ou Les Clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin. \nLes joueurs delfes de sang seront amicaux avec les Clairvoyants et hostiles avec l\'Aldor, alors que les joueurs draenei seront hostiles aux Clairvoyants et amicaux envers lAldor.\n\n[npc=19331] et [npc=20808] sont situés dans la banque des Clairvoyants, sur le bord sud de la terrasse de lumière. La Bibliothèque du Visiteur abrite [npc=20613] [small][/small] et [npc=21905] [small][/small], qui échangent des pièces d\'armure épique contre des pièces de set de[url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec les Clairvoyants correspondent à une perte de réputation de 10% plus élevée chez lAldor. La plupart des gains de réputation avec les Clairvoyants accorderont également 50% de la réputation avec [faction=935].[/i]\n\n[h3]Tradition [/h3]\n\nAprès avoir subi des assauts implacables de leurs ennemis, les gardes harassés de Sha\'tar et de lAldor se sont regroupés pour la prochaine attaque alors qu\'elle marchait sur l\'horizon. Cette fois, l\'attaque provenait des armées de [npc=22917]. Un grand régiment d\'elfes de sang avait été envoyé par l\'allié d\'Illidan, le prince Kael\'thas pour détruit la ville. Alors que le régiment d\'elfes de sang traversait le pont, les exarques et les vindicateurs de lAldor se sont alignés pour défendre la Terrasse de Lumière. Alors l\'inattendu arriva, les elfes de sang déposèrent leurs armes devant les défenseurs de la ville.\nLeur chef, un ainé de sang connu sous le nom de Voren\'thal, a exigé de parler au naaru [npc=18481]. À mesure que le naaru s\'approchait de lui, Voren\'thal s\'agenouilla et prononça les mots suivants : « Je vous ai vu dans une vision, naaru. Le seul espoir de survie de ma race est avec vous. Mes disciples et moi-même sommes là pour vous servir ».\nLa défection de Voren\'thal et de ses partisans a été la plus grande perte jamais subie par les forces de Kael\'thas. Beaucoup des plus forts et les plus brillants parmi les savants et les magistrats de Kael\'thas ont été influencés par l\'influence de Voren\'thal. Le naaru a accepté les déflecteurs qui sont devenus connus sous le nom de Clairvoyant.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29426][/span] à [npc=18531] dans la bibliothèque du Visiteur des Clairvoyants accordera une réputation de 250 points de réputation pour les Clairvoyants. Il existe également une quête répétable où donner une unique chevalière accorde 25 points de réputation. Ces chevalières tombent sur des membres Aile-de feu dans la partie nord-est de la forêt de Terrokar. \nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique chevalière.\n\n[b]Jusqu\'à exalté [/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1][item=30810][/span], cest le même principe que les chevalières mais ceux-ci tombent sur des elfes de sang Solfurie de haut rang. Si vous le souhaitez, vous pouvez transformer les chevalières de niveau supérieur avant une réputation honorée. Vous les trouverez dans [zone=3523], [zone=3520] et les instances du [url=?Search=tempête+donjon]donjon de la tempêtes[/url].\n\n[b]Tome des Arcanes[/b]\n\n[span class=q2][item=29739][/span] peut être donné à tout moment à [npc=18530] à l\'intérieur la Bibliothèque du Visiteur. Cela augmentera votre réputation avec les Clairvoyants de 350 par Tome des Arcane.\nEn plus des gains de réputation, vous recevrez une [span class=q1][item=29736][/span], qui est la condition pour acheter l\'enchantements d\'épaule à [npc=20808], qui réside dans la banque des Claivoyants.\n\n[h3]Passer à la réputation des Claivoyants[/h3]\n\nPour changer votre faction d\'Aldor vers Claivoyants et donc accéder à leurs recettes d\'artisanat (et annuler toutes les avancées de réputation que vous avez faites), trouvez [npc=18596], membre des Claivroyants dans la ville basse. Elle vous propose une quête répétable, [quest=10024], où pour huit [span class=q1][item=25744][/span] vous montez la réputation Claivoyant. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.',NULL),(8,942,2,NULL,0,2,'L[b]Expédition Cénarienne[/b] a été envoyé par [faction=609], lors de la réouverture de la porte des ténèbres vers l\'Outreterre, pour explorer ce monde inconnu. Tout comme le cercle, il s\'agit d\'une coalition de forces entre les Elfes de la nuit et les Taurens. Depuis l\'ouverture de la porte, l\'expédition Cénarienne a rapidement gagné en taille et en autonomie, obtenant suffisamment de puissance pour être considérée comme une propre et unique faction. L\'expédition maintient sa base principale au refuge Cénarien dans [zone=3521], située immédiatement à louest de la péninsule des flammes infernales. Elle est aussi présente sur [zone=3483], dans [zone=3519], et dans [zone=3522]. \n\nLe Refuge est situé dans le marécage de Zangar afin détudier la faune riche située là-bas. Cependant, l\'expédition a révélé des retombées inquiétantes dans le marais. Les niveaux d\'eau dans de nombreuses régions du marécage diminuent, et certaines régions comme Morte-bourbe ont déjà beaucoup souffert de ce phénomène étrange. On sait que cette diminution des niveaux d\'eau peut être attribuée aux pompes qui ont été construites dans le marécage par les naga. Leur but est de créer un nouveau puits d\'éternité pour [npc=22917].\nCependant, l\'expédition ne peut pas se permettre une confrontation directe avec le naga si nombreux dans le marécage de Zangar et le [url=?Search=Glissecroc#c0z]Réservoir de Glissecroc [/url]. Elle a besoin de l\'aide daventurier qui veulent soutenir les druides dans leur dangereuse bataille contre les Nagas qui cherchent à perturber l\'équilibre naturel du marais. Naturellement, ceux assez héroïques pour combattre au réservoir de Glissecroc seront bien récompensés.\n\n[h3]Réputation[/h3]\n\n[b]De neutre à honoré[/b]\n\nTuez des Nagas chaque fois que vous le pouvez. Le mieux sera de parcourir les instances, la réputation monte plus rapidement.\nAlternativement, le joueur peut commencer à trouver des [item=24401] pour avoir une chance davoir des [item=24407], qui peuvent être transformé en 500 points de réputation. Il est suggéré que le joueur garde ses espèces non cataloguées jusqu\'à ce que son statut honoré soit atteint, car la quête ne peut pas être poursuivie après ce point, alors que les espèces non cataloguées peuvent être utilisées jusqu\'à Exalté.\n\nSi vous êtes un herboriste et que vous êtes intéressé par la réputation [faction=970], vous voudrez peut-être trouver les [url=?Npcs&filter=na=Seigneur+tourbe]Seigneurs-tourbes[/url] qui se trouve dans lEst, et le coin Sud-ouest du Marécage de Zangar. Leurs corps peuvent être «récoltés» par les herboristes et produisent souvent des végétaux non identifiées, alors que chaque monstre tué donne 15 points de réputation chez Sporeggar. \n\n[b]De honoré à révéré[/b]\n\nUne fois que le joueur est honoré, faire lenclos aux esclaves et [zone=3716] (à l\'exception de [npc=17770] et de certains géants), n\'accorderont plus de réputation. Vous devriez maintenant faire des quêtes de l\'Expédition Cénarienne dans la péninsule des flammes infernal, le marécage de Zangar, la forêt de Terokkar et les Tranchantes. Il est également temps de transformer toutes les espèces non cataloguées que vous avez trouvées. Faire cela devrait vous faire passer révérer.\n\nAlternativement, vous pouvez, en étant niveau 70, faire [zone=3715]. Chaque donjon donne un peu plus de 1500 points de réputation si vous tuez toutes les mobs.\nDans le Caveau de la vapeur, se trouve, aussi, une quête répétable, [quest=9764], qui commence par [item=24367]. Vous pourrez ensuite donner les [item=24368], qui tombe à la fois dans le caveau de la vapeur et lenclos aux esclaves, recevant 250 points de réputation pour les premières armes et 75 points de réputation par la suite. Cette quête est disponible jusqu\'à exalté.\n\nUne fois que vous avez le niveau 70 et que vous avez amélioré votre équipement, vous pouvez choisir d\'entrer dans lenclos des esclaves, le caveau de la vapeur et basse-tourbière en mode héroïque avec l\'achat de la [item=30623]. Ils accordent une réputation importante : les mobs ordinaires valent 15 points de réputation, 2 pour les non élites et 150 à 250 pour les boss. Cette méthode fonctionne jusqu\'à exalté.\n\n[b]De révéré à exalté [/b]\n\nContinuez avec la même stratégie que ci-dessus : terminez toutes les requêtes restantes, faites caveau de la vapeur et continuez avec la quête des [item=24368].\n\nIl est également possible de faire lenclos des esclaves, Basse-tourbière et caveau de la vapeur en mode héroïque. La réputation acquise n\'est pas beaucoup plus intéressante que le caveau de la vapeur en mode normal, alors que l\'investissement dans le temps pour les donjons héroïques est beaucoup plus élevé, le butin est mieux et vous recevrez [item=29434] sur les boss qui peuvent être utilisés pour acheter des équipements épiques de haute qualité.',NULL),(8,941,2,NULL,0,2,'Les [b]Mag\'har[/b] sont la faction d\'orcs à peau brune qui sont restées en Outreterre et se sont séparés des autres clans orcs restants qui ont été victimes de [npc=17257] et qui sont maintenant dirigés par le puissant [npc=16808]. Les Mag\'har sont présent dans la forteresse de Garadar dans le magnifique pays de [zone=3518], une fois bien installés, la majorité des orcs sont retournés dans [zone=3519] et [zone=3522].\n\nLes Maghar n\'ont jamais été corrompus par Mannoroth ou Magtheridon. Contrairement à dautres anciens clans qui vivent dans les ruines de leurs ancêtres, les Mag\'har sont composés de membres de différents clans d\'orc qui ont échappé à la corruption. Le chef actuel des Mag\'har, la vénérable [npc=18141], est une orc ancienne et sage, mais elle est tombée récemment extrêmement malade. [npc=18063], fils du puissant Grom hurlenfer, sert de chef militaire aux Mag\'har, aidé par [npc=18106], fils du vénérable chef du clan Orbite-Sanglante, Kilrogg Deadeye. En outre, il existe un orc dans un camp de Mag\'har à l\'ouest connu sous le nom [npc=18229].\n\nIl n\'est pas clair comment le Mag\'har a réussi à conserver sa peau marron d\'origine. La peau orque devient verte lorsqu\'elle est exposée à la magie du sorcier, indépendamment des croyances ou des pratiques de l\'individu ; Garrosh et Jorin auraient certainement été exposés, compte tenu de la position hiérarchique de leurs pères.\n\nLes joueurs de la Horde commencent inamical avec le Mag\'har. Les joueurs de l\'Alliance seront toujours traités comme hostiles. La contrepartie de l\'Alliance à cette faction est la faction des : [faction=978].\n\n[h3]Quête[/h3]\n\nLes quêtes pour les Mag\'har commencent dans [zone=3483] avec [quest=9400] de [faction=947]. Cette quête vous mènera à un petit avant-poste Mag\'har au nord de la Citadelle des flammes infernales. Une fois à Nagrand, les joueurs trouveront la principale ville de Mag\'har, Garadar. La ville détient la plupart des quêtes restantes qui récompenseront la réputation de Mag\'har.\n\n[i]Note : Vous DEVEZ compléter la suite de quête de \"lassassin\" jusqu\'à la quête [quest=9410] (où vous devenez neutre) afin que vous puissiez parler à la plupart des gens de Garadar.[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x[item=25433], qui tombent de ces ogres.\n\nLes joueurs qui recherchent la réputation : [faction=933] peuvent vouloir garder leurs perles, car la réputation Mag\'har est généralement plus facile à obtenir. \nLes joueurs qui recherchent la réputation :[faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, car ils donnent aussi des [item=29425] pour la réputation Aldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent une réputation jusquà exalté![/i]',NULL),(8,946,2,NULL,0,2,'Le [b]Bastion de lHonneur[/b], refuge des explorateurs humains, élu, draenei et nains, est la première grande ville que les explorateurs de l\'Alliance rencontreront en traversant la porte des ténèbres. Les vestiges des fils de Lothar, anciens combattants de l\'Alliance qui sont venus à Draenor, se sont tenus fermement dans cet avant-poste des flammes infernales. Ils sont maintenant rejoints par les armées de Hurlevent et Forgefer.\n\n[h3]Réputation[/h3]\n\nLa réputation du Bastion de l\'Honneur est gagnée par divers moyens dans la péninsule des flammes infernales. Les PNJs, dans et autour, de la citadelle donnent en récompensés de quêtes de l\'honneur et de la réputation. En raison du manque de représentants dans d\'autres endroits dOutreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation au bastion de lhonneur une fois que vous partez de la péninsule.\n\n[b]Jusquà Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes du Bastion de l\'honneur, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à lextérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée sarrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10106]. Cette quête accorde 70 points d\'honneur et 150 points de réputation au Bastion de lHonneur, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24579][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=17657] et [npc=18266] au Bastion de lHonneur ainsi que [npc=18581] aux marécages de Zangar.\n\n[b]Jusquà Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). Linstance des salles brisées abrite des PNJs qui donnent de la réputation jusquà Exalté.[/li]\n[li]Après avoir obtenu le statut dhonoré, vous pouvez acheter [span class=q1][item=30622][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusquà Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24520][/span] à l\'adjudant Tracy Proudwell et augmenter le montant gagné de réputation (et dexpérience) acquise lors de l\'exécution de ces instances.[/i]',NULL),(8,967,2,NULL,0,2,'[b]L\'Oeil Pourpre[/b] est une secte secrète fondée par le Kirin Tor de Dalaran pour espionner le gardien de Tirisfal, [npc=15608], dans la tour de [zone=2562]. Bien que Medivh soit mort, l\'il pourpre reste dans Karazhan, défendant le mal qui semble lenvahir en l\'absence de son maître.\n\nOn ignore si l\'apprenti de Medivh, [npc=18166], était membre de lOeil Pourpre, ou s\'il connaissait leurs activités à l\'époque.\n\n[h3]Réputation[/h3]\n\nLa réputation de lil pourpre est obtenue en tuant des mobs à l\'intérieur de Karazhan et en complétant les quêtes liées à Karazhan. La réputation grâce aux mobs de Karazhan peut être acquise à partir d\'une position neutre jusquà une réputation exalté. Chaque mob apporte une réputation d\'environ 15 points, les boss accordent davantage de réputation.\n\n[npc=18253] propose une chaîne de quête assez longue commençant par [quest=9824] et [quest=9825]. Cette suite de quête se termine par [quest=9644] et récompense les joueurs avec [span class=q1][item=24490][/span]. L\'achèvement complet de cette suite de quête récompense le joueur avec 10 270 point de réputation d\'environ.\n\n[h3]Récompenses de la réputation[/h3]\n\n[npc=18253] offrira aux joueurs des bagues en récompenses pour chaque niveau de réputation sous forme de quêtes. La première de ces quêtes est disponible dès la réputation neutre. Vous recevrez une version nouvelle et améliorée de la bague que vous avez choisi chaque fois que vous entrez dans un nouveau niveau de réputation. Les anneaux sont triés dans les 4 catégories suivantes :\n[ul]\n[li][quest=10731] : [item=29280], [item=29281], [item=29282] et [item=29283][/li]\n[li][quest=10729] : [item=29284], [item=29285], [item=29286] et [item=29287][/li]\n[li][quest=10732] : [item=29276], [item=29277], [item=29278] et [item=29279][/li]\n[li][quest=10730] : [item=29288], [item=29289], [item=29291] et [item=29290][/li]\n[/ul]\n\n[npc=16388], un forgeron situé à l\'intérieur de Karazhan juste après [npc=15550], offre aux joueurs ayant une réputation assez élevée la possibilité d\'acheter des plans de forge épique. Les joueurs honorés ou au-dessus pourront également réparer des armures et des armes chez ce fournisseur.\n\n[npc=18255], qui se trouve juste à l\'extérieur des portes principales de Karazhan, vendra une recette de joaillerie épique et un enchantement d\'épaule aux joueurs qui ont une haute réputation avec lOeil Pourpre.',NULL),(8,970,2,NULL,0,2,'Les[b]Sporeggar[/b] sont une race de champignons essentiellement pacifique originaire d\'Outreterre. Ils vivent dans une ville située dans les tourbières occidentales de [zone=3521].\n\n[h3]Réputation [/h3]\n\nLes joueurs de l\'Alliance et de la Horde commencent amicalement avec Sporeggar. Il existe de nombreuses façons d\'augmenter votre réputation au début : \n[ul]\n[li]Apporter 10 [span class=q1][item=24290][/ span] à [npc=17923] pour compléter [quest=9739][/li]\n[li]Apporter 6 [span class=q1][item=24291][/span] à Fahssn pour compléter [quest=9743][/li]\n[i]Ces deux quêtes ne seront disponibles que si vous avez une réputation au minimum amical[/i]\n[li]Tuer [url=?Search=seigneurs +tourbes+-hungry #z0z]Seigneurs tourbes[/url] [i](jusqu\'à honoré)[/i][/li]\n[li]Tuer [npc=18137] et [npc=18136] [i](jusqu\'à révéré)[/i][/li]\n[li]Apporter 10 [span class=q1][item=24245][/span] à [npc=17924] dans Sporeggar[i] (jusquà amical)[/i][/li]\n[/ul]\n\nAprès avoir une réputation [b]amicale[/b], de nouvelles quêtes répétitives s\'ouvrent en même temps que les quêtes de Fahssn, notamment :\n[ul]\n[li]Tuer 12 [npc=18088] et [npc=18089] pour [npc=17856] pour compléter [quest=9726][/li]\n[li]Apporter 10 [span class=q1][item=24449][/span] à [npc=17925] pour compléter [quest=9806][/li]\n[li] S\'aventurer dans [zone=3716] pour rassembler 5 [span class=q1][item=24246][/span] pour terminer [quest=9715][/li]\n[/ul]\nCes 3 quêtes sont répétables et seront disponibles jusquà la réputation exalté.\nLes joueurs qui sont exaltés avec Sporeggar devraient parler à [npc=17877] pour une dernière quête.',NULL),(8,978,2,NULL,0,2,'Les Kurenaï, pour « racheté », ont échappé à lesclavage en Outreterre et ont fait leur maison à Telaar dans le sud de [zone=3518]. C\'est là qu\'ils cherchent à redécouvrir leur destinée. Ils conservent également une petite présence en [zone=3521]. Leur intendant, [npc=20240], est situé à l\'extérieur de l\'auberge à Telaar, en dessous du point de vol.\n\nLes joueurs de l\'Alliance commencent à faire preuve d\'hostilité avec les Kurenai. Les joueurs de la Horde seront toujours traités comme hostiles. La contrepartie de la Horde à cette faction est [faction=941].\n\n[i]Kurenai est le japonais pour « cramoisi ».[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?Npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x [item=25433], qui tombent de ces ogres.\n\nLes joueurs qui cherchent la réputation de la faction [faction=933] peuvent vouloir garder leurs perles, car la réputation de Kurenai est généralement plus facile à obtenir.\n\nLes joueurs qui cherchent la réputation de la faction [faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, alors qu\'ils donnent des [item=29425] pour la réputation de lAldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent de la réputation jusquà exalté.[/i]',NULL),(8,989,2,NULL,0,2,'Les [b]Gardiens du Temps[/b] sont des dragons de bronze sélectionnés par Nozdormu pour surveiller les grottes du temps. Ils sont dirigés par [npc=19932] et [npc=19933], qui remplacent également Nozdormu en son absence.\n\n[h3]Réputation[/h3]\n\nActuellement, la seule façon d\'obtenir la faveur des dragons de bronze est de faire les instances : [zone=2367] et [zone=2366]. Lintendant des Gardiens du Temps, [npc=21643], se situe au quartier-intendant dans les grottes du temps. Les Gardiens vous demanderont d\'être au minimum niveau 66 et de compléter la courte quête [quest=10277] avant d\'autoriser le passage dans Les contreforts dHautebande dantan pour accomplir la destinée du Chef de la Horde, [npc=17876].',NULL),(8,990,2,NULL,0,2,'La [b]Balance des sables[/b] est un sous-groupe secret du vol des Dragons de bronze, dirigé par [npc=19935], premier partenaire de [npc=15185]. Leur chef, Nozdormu, a envoyé ces factions gardiennes à [zone=3606] où ils gardent l\'Arbre Monde d\'une autre attaque par les démons, contribuent à restaurer le temps et à préserver l\'avenir du monde.\n\n[h3]Réputation[/h3]\n\nTuer les boss et monstres du Fléau font monter la réputation. [npc=17968], le boss final, récompense de 1 500 points de réputation tandis que les quatre autres boss donnent 375 points de réputations. La réputation général des montres du Fléau donnent 12 points de réputation, tandis que [npc=17907] donnent 60 points de réputation. En produisant une moyenne de 7 800 points de réputations par raid, 6 raids sont nécessaires pour atteindre la réputation exaltée.\n\nActuellement, la réputation permet davoir lune des meilleurs [span class=q4][url=?Items=4.-2&filter=na=bague+éternel]Bagues[/url][/span] pour les raids. Afin de recevoir ces anneaux, vous devez compléter la quête précédemment requise, [quest=10445]. Chaque nouveau niveau de réputation accorde une bague améliorée.',NULL),(8,1012,2,NULL,0,2,'Les [b]Ligemorts Cendrelangues[/b] sont l\'élite de la tribu Kurenaï connue sous le nom de Cendrelangue. La tribu Cendrelangue est dirigée par la sage aînée [npc=21700]. Les Ligemorts sont [i]officiellement[/i] alignés avec [npc=22917] [small][/small]. Les Ligemorts sont les lieutenants les plus dignes d\'Akama et sont au courant des motivations mystérieuses de leur chef.\n\nPour découvrir les Ligemorts Centrelangues en tant que faction, le joueur doit commencer et compléter la majorité de la suite de quête qui commence par [quest=10568] ou [quest=10683]. Finalement, vous parlerez avec Akama, après quoi vous deviendrez neutre avec les Ligemorts Cendrelangues.',NULL),(8,947,2,NULL,0,2,'[b]Thrallmar[/b], expédition envoyée par le Portail des Ténèbres par Thrall, a construit un bastion dans la péninsule des flammes infernales qui sert de base d\'opérations pour une grande partie des activités de la Horde en Outreterre.\n\n[h3]Réputation[/h3]\n\nLa réputation de Thrallmar jusqu\'à l\'honorée est relativement facile à gagner. Même les quêtes les plus faciles (celles qui vous emmènent d\'un fournisseur de quête à la prochaine, par exemple) peuvent produire 75 points de réputation, alors que ceux qui nécessitent plus defforts pour compléter ont généralement 250 points de réputation ou plus. Certaines quêtes de groupe impliquant de tuer un élite peuvent donner jusqu\'à 1 000 points de réputation.\n\nSi vous faites la majeure partie des quêtes de Thrallmar au lieu de passer rapidement à la prochaine zone, vous pourriez vous attendre à être honoré après 1 ou 2 niveaux de jeu. En raison du manque de représentants dans d\'autres endroits dOutreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation à Thrallmar une fois que vous partez de la péninsule. Cest seulement au niveau 68 que vous pouvez commencer à regagner des points dans le donjon [zone=3714].\n\n[b]Jusquà Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes de Thrallmar, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à lextérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée sarrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10110]. Cette quête accorde 70 points d\'honneur et 150 points de réputation à Thrallmar, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24581][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=18267] et [npc=18564] à Thrallmar et près de Zabrajin dans [zone=3521].\n\n[b]Jusquà Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). Linstance des salles brisées abrite des PNJs qui donnent de la réputation jusquà Exalté.[/li]\n[li]Après avoir obtenu le statut dhonoré, vous pouvez acheter [span class=q1][item=30637][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusquà Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24522][/span] au Crieur-de-guerre Coquard et augmenter le montant gagné de réputation (et dexpérience) acquise lors de l\'exécution de ces instances.[/i]',NULL),(8,1011,2,NULL,0,2,'[b]Ville Basse[/b] de [zone=3703] est l\'endroit où les réfugiés se rassemblent et saident par leurs propres moyens. Lorsque vous aidez l\'une des races qui ont fui la guerre, la réputation se débrouille rapidement. Leur intendant, [npc=21655], est situé sur le marché dans la ville basse.\n\nLa ville basse de Shattrath contient de nombreux artisans qui possèdent de vastes connaissances :\n[ul]\n[li][npc=19187], [small]< Maître des travailleurs du cuirs >[/ small].[/li]\n[li][npc=19180], [small]< Maître des dépeceurs >[/small].[/li]\n[li][npc=19052], [small]< Maître des alchimistes >[/small]. Il donne la quête [quest=10902] (pour une spécialisation). Un laboratoire dalchimiste se trouve également à son côté.[/li]\n[li]Trois tailleurs qui vous permettent de se spécialiser et d\'acheter de nouvelles recettes de couture épiques pour des ensembles d\'armures et des sacs spéciaux :\n[ul][li][npc=22212], [small]< Spécialiste de couture de tisse-ombre >[/small] vend des recettes pour [itemset=553][/li]\n[li][npc=22213], [small]< Spécialiste de couture de feu-sorcier >[/small] vend des recettes pour [itemset=552].[/li]\n[li][npc=22208], [small]< Spécialiste de couture détoffe lunaire > [/small] vend des recettes pour [itemset=554].[/li][/ul]\n[/ul]\n\nLes maîtres de guerre, Alliance et Horde, des quatre [zones=6] peuvent également être trouvés ici, ainsi que la Tavernes de la Fin du Monde.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré [/b]\n[ul]\n[li]Faire [zone=3790] en [i]mode normal[/i], vous récompense denvirons 750 points de réputation.[/li]\n[li]Faire [zone=3791] en [i]mode normal[/i], vous récompense denvirons 1 250 points de réputation.[/li]\n[li]Faire [zone=3789] en [i]mode normal[/i], vous récompense denvirons 2 000 points de réputation.[/li]\n[li]Fournir 30 x [item=25719] à [npc=22429], vous récompense de 250 points de réputations par quête.[/li]\n[/ul]\n[i]Note : Les joueurs qui visent une faction supérieure à Honorée devraient attendre jusqu\'à dêtre honoré avant de compléter les quêtes de la Ville Basse.[/i]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire de Labyrinthe des ombres en [i]mode normal[/i], vous récompense de 2 000 points de réputation.[/li]\n[li]Terminer toutes les [url=?quests&filter=cr=1;crs=1011;crv=0]quête de la Ville-Basse[/url].[/li]\n[/ul]\n[b]De révéré à exalté[/b]\n[ul]\n[li]Faire les Cryptages Auchenai en [i]mode héroïque[/i], vous récompense denvirons 750 points de réputation.[/li]\n[li]Faire les salles de Sethekk en [i]mode héroïque[/i], vous récompense denvirons 1 250 points de réputation.[/li]\n[li]Faire le Labyrinthe des ombres en [i]mode normal[/i] ou en [i]mode héroïque[/i], vous récompense denvirons 2 000 points de réputation.[/li]\n[/ul]\n\n[h3]Anecdotes[/h3]\n\n[npc=19227], un vendeur dans la ville basse, vend des amulettes qui sont très ... intéressantes. Il vend des articles comme [item=27940], qui vous permettent de revenir à la vie lorsque vous retournez à l\'endroit où vous êtes mort. [i]Buyer se méfiez-vous![/i]\n\nEn tant quexalté, vous pouvez acheter un [item=31778]. Curieusement, aucun des habitants de la Ville Basse na été vu avec un tel objet. Peut-être qu\'ils ne peuvent pas se le permettre',NULL),(8,1015,2,NULL,0,2,'L[b]Aile-du-Néant[/b] est une faction de dragons situés en Outreterre. La couvée inhabituelle a été engendrée par les ufs du vol de dragon noir dAile-de-Mort et infusée d\'énergies brutes. Maintenant, ils cherchent à trouver leur identité au-delà de l\'ombre du patrimoine destructeur de leur père.\n\n[h3]Réputation[/h3]\n\nLes joueurs, au commencement, sont haïe à la faction Aile-du-Néant et doivent être exaltés pour recevoir des [span class=q4][url=?Items=15.-7&filter=na=Aile-du-Néant+Drake]Drakes Aile-du-Néant[/url][/spanclass]. La suite de quête de la réputation est une suite qui se fait en solitaire impliquant des quêtes journalières, une quête de groupes (5 joueurs) pour passer Neutre et les quêtes journalières de groupe (3 joueurs) après être passer Révéré.\nUne monture volante est requise pour cette réputation et 300 compétences de monte sont nécessaires pour passer neutre.\n\n[b]De Haïe à Neutre[/b]\n\nLes joueurs de niveau 70 commenceront leur voyage pour une réputation exaltée en choisissant la suite de quête offerte par [npc=22113], un elfe du sang errant la surface des champs dAile-du-Néant, dans le coin sud-est de [zone=3520]. La suite de quête commence par [quest=10804]. L\'achèvement de cette suite fournira une réputation instantanée neutre et le choix de l\'un de [span class=q3][url=?Items&filter=qu=18;cr=1;crv=0;na=Aile%20néant;qu=3]ces 5 items[/url][/span].\n\n[h3]Après Neutre [/h3]\n\nAprès avoir terminé la suite de quête, Mordenai sassurera qui vous ayez acquis 300 compétences [spell=34091] et que vous ayez une réputation neutre auprsè de lAile-de-Néant.\nCela vous accordera un déguisement dOrc Gueule-de-Dragon lorsque vous entrez dans la zone Aile-du-Néant et vous permettra de communiquer et de travailler pour les Gueules-de-Dragon stationné là-bas.\n\nMordenai vous enverra d\'abord à [npc=23139] avec un ensemble de faux papiers. L\'achèvement de cette quête débloque le début des quêtes Gueule-de-Dragon sur lesquelles vous travaillerez pour augmenter votre réputation Aile-du-Néant.\n\nLa plupart de ces quêtes seront journalières (ajoutée à la 2.1). Les quêtes journalières diffèrent des quêtes régulières car elles sont infiniment repérables, mais vous ne pouvez compléter chaque quête journalière qu\'une fois par jour et se limiter à 25 quêtes journalières par jour.\n[i]Remarque : De nouvelles quêtes seront débloquées après chaque niveau de réputation, et toutes les quêtes journalières des niveaux précédents seront toujours disponibles.[/i]\n\n[b][toggler id=Neutralcaché]Neutre[/toggler][/b]\n\n[div id=Neutralcaché] \nAprès avoir donné la [item=32469] à [npc=23139] pour compléter [quest=11013], votre première suite de quêtes sera disponible pour accéder au prochain niveau de réputation avec Aile-du-Néant.\n\nMor\'ghor vous indiquera daller voir le maître d\'uvre afin de commencer votre travail, et [npc=23141] se révélera comme un allié déguisé et vous proposera dautres quêtes.\nL\'une d\'entre elles est [quest=11049]. Les joueurs pourront trouver, avec un peu de chance (1% de loot), l[item=32506] sur presque toutes les créatures de lescarpement dAile-du-Néant et sur un [item=185881] ou un [item=185877].\nYarzill voudra aussi une trouvaille rare, l[item=185915], trouvée n\'importe où sur le rebord dAile-du-Néant et dans la forteresse Gueule-de-Dragon, coin sud-est de la vallée de dOmbrelune. Cette quête n\'est pas étiquetée comme journalière et peut donc être effectuée autant de fois que vous voulez, du moment que vous pouvez trouver des ufs. Cette quête nest pas comprise dans votre limite de quête journalière.\n\nAutres quêtes disponibles dès le début:\n[ul]\n[li][i][small]Journalière[/small][/i] - [quest=11018], [quest=11016], [quest=11017] Nest disponible que pour les joueurs qui possèdent la profession adaptée pour rassembler chaque élément.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11015] - Une quête de collecte simple ouverte à tous les joueurs indépendamment de leur profession.[/li] \n[li][i][small]Journalière[/small][/i] - [quest=11020] - Yarzill vous demandera de collecter des [item=32502]s et de les utiliser afin dempoisonner les péons qui travaillent pour rassembler des ressources pour Gueule-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11035] - Vous devrez voler vers le coin nord-est de lescarpement dAile-du-Néant et vous positionner sur une des roches flottantes pour intercepter le [npc= 23188] et récupérer 10 x [item=32509].[/li]\n[/ul]\n[/div]\n[b][toggler id=Friendlyhidden]Amical[/toggler][/b]\n\n[div id=Friendlyhidden]\nMor\'ghor vous donnera un [item=32694] pour circuler avec votre nouveau rang parmi les Gueules-de-Dragon.\n[ul]\n[li][quest=11083] - [npc=23166] vous enverra tuer des bourbesangs qui sont stationné profondément dans les mines.[/li]\n[li][quest=11081] - Après avoir trouvé les [item=32726] dans un [item=32724], vous révélerez ce qui se passe réellement avec les bourbesangs dans la mine.[/li]\n[li][quest=11054] - [npc=23291] vous donnera vos propres [item=32680] pour garder les pétons Gueules-de-Dragon en ligne et travailler avec efficacité[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11076] - La [npc=23149] vous demandera de vous aventurer dans les mines Ailes-du-Néant et de récupérer la cargaison contenue dans les chars de la mine qui est jetée au hasard dans l\'intérieur de la mine.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11077] - L\'un des [npc=23376] vous informera que des créatures plus profondes dans la mine interrompent la production et vous demandent de réduire leur nombre.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11055] - Cette quête humoristique commence chez le [npc=23291] après que vous lui apportiez le matériel requis. Vous pourrez survoler lescarpement Aile-du-Néant et lancer le Booterang à n\'importe quel [npc=23311] qui sy trouve autour des cris-taux.[/li]\n[/ul]\n[/div]\n[b][toggler id=Honorécaché]Honoré[/toggler][/b]\n\n[div id=Honorécaché]\nMor\'ghor vous donnera votre nouveau [item=32695], qui est maintenant utilisable n\'importe où, tant que vous êtes à l\'extérieur.\n[ul]\n[li][quest=11063] - Cette quête en six parties est une course aérienne contre les autres maîtres de vol Gueule-de-Dragon. Ils tenteront tous de vous renverser, vous et votre monture, avec des attaques aériennes habilement placées, vous devez rester visible et sur votre monture jusqu\'à leur atterrissage, si vous échouez, vous devez redémarrer la quête. Après avoir vaincu le dernier des six coureurs, vous recevrez un [item=32863], qui fonctionne exactement comme une [item=25653]. Les effets des deux bijoux ne sadditionnent pas.[/li]\n[li][quest=11089] Le [npc=23427] demandera un ensemble de matériaux pour créer un dispositif spécial pour détruire son frère et entraver les avancées de la légion dans l\'ouest de [zone=3518].[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11086] - Mor\'ghor Vous enverra au Portal de Nagrand pour tuer 20 [url=?npcs=7&filter=na=ombremort] Agents Ombremort[/url]. Attention aux seigneurs, ils patrouillent dans la région et peuvent vous tuer dcoup de poing.[/li]\n[/ul]\n[/div]\n[b][toggler id=Révéréhidden]Révéré[/toggler][/b]\n\n[div id=Révéréhidden]\nMor\'ghor vous donnera votre nouveau [item=32864], le plus haut bijou.\n[ul]\n[li][url=?quests&filter=na=tuez%20les%20tous;minle=70;maxle=70] Tuez-les tous ![/url] - Mor\'ghor vous ordonnera de commencer l\'attaque la base d\'opérations de votre faction dans la vallée de Sombrelune. De toute évidence, vous n\'allez pas autoriser les Gueules-de-Dragon à attaquer vos alliés, alors vous informerez au leader approprié et débloquerez votre dernière quête journalière pour les Gueules-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] [url=?quests&filter=na=le%20plus%20mortel%20des%20pièges]Le plus mortel des pièges[/url] - Les forces Gueules-de-Dragon vont attaquer la base des opérations. Apportez des alliés, car il s\'agit d\'une grande bataille.[/li]\n[/ul]\n[/div]\n[b][toggler id=Exaltécaché]Exalté[/toggler][/b]\n\n[div id=Exaltécaché]\nAprès de nombreux jours de travail, finalement le dénouement de la suite des quêtes Aile-du-Néant / Gueule-de-Dragon, vous dirigera à Mor\'ghor une dernière fois, qui vous informera que vous serez promu par [npc=22917] lui-même.\nSans gâcher les événements qui s\'ensuivent, vous vous retrouverez à Shattrath avec une sélection de montures épiques Aile-du-Néant. Vous pouvez en choisir un gratuitement, et si vous décidez d\'une couleur différente plus tard, vous pouvez acheter un autre drake chez [npc=23489] dans le camp de Gueule-de-Dragon pour 200 or.\n[/div]',NULL),(8,1031,2,NULL,0,2,'Les [b]Gardes-ciel sha\'tari[/b] sont les gardiens aériens de [zone=3703], défendant la capitale des assaillants dans les collines ainsi que la lutte contre les Arakkoas de Terokk dans les sommets de Skettis. [faction=935] dirigent les gardes-ciel shatari.\nIls ont deux avant-postes, l\'un au nord des montages de Skettis et un près d[faction=1038]. Les joueurs commencent avec une réputation neutre chez les Gardes-ciel sha\'tari.\n\n[h3]Réputation[/h3]\n\n[b]Quêtes journalières[/b]\n[ul]\n[li][quest=11008] - [npc=23048] vous accordera un paquet d\'explosifs pour détruire les oeufs qui reposent au sommet des structures de Skettis. [/li]\n[li][quest=11085] - Le [npc=23383] peut être trouvé au sommet de certaines structures, les joueurs l\'escorteront pour la réputation, l\'or et un choix entre deux potions : [item=28100] ou [item=28101].[/li]\n[li][quest=11065] - [npc=23335] vous informera que les bombardements, de lavant-poste de la garde-ciel, ont coûté la vie de leurs montures et vous demandent de rassembler des Raies de léther pour compléter leurs forces aériennes.[/li]\n[li][quest=11010] - [npc=23120] vous demande de détruire les munitions pour les canons de la Légion afin que les gardes-ciel puissent continuer leur travail.[/li]\n[li][quest=11004] - Après avoir recueilli 6 [item=32388], [npc=23042] fera une potion qui permettra de voir l\'arakkoa le plus puissant, tel que [npc=23066].[i][small] Note : cette quête n\'est pas une quête journalière, mais peut être répété autant de fois que nécessaire. [/small][/i][/li]\n[/ul]\n\n[b]Créatures[/b]\n\n[ul]\n[li][npc=21804] - 5 points de réputation, jusqu\'à la fin de Révéré[/li]\n[li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70] Tous les Arakkoa de Skettis[/url] - 10 points de réputation.[/li]\n[li][npc=23029] - 30 points de réputation.[/li]\n[/ul]',NULL),(NULL,NULL,0,'new',0,2,'Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft\'s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow\'s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don\'t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won\'t risk losing them.[/li]\n[li][b]Make sure it\'s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]',NULL),(NULL,NULL,0,'edit',0,2,'Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft\'s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow\'s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don\'t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won\'t risk losing them.[/li]\n[li][b]Make sure it\'s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]',NULL),(13,1,3,NULL,0,2,'[b][color=c1]Krieger[/color][/b] sind eine sehr mächtige Klasse, die sowohl tanken als auch im Nahkampf erheblichen Schaden anrichten kann. Der [icon name=ability_warrior_defensivestance][url=?spells=7.1.257]Schutz[/url][/icon]-Talentbaum des Kriegers enthält viele Talente, um seine Überlebensfähigkeit zu verbessern und Bedrohung gegenüber Monstern zu erzeugen. Schutz-Krieger sind eine der wichtigsten Tank-Klassen des Spiels.\n\nAußerdem verfügen Krieger über zwei schadensorientierte Talentbäume - [icon name=ability_rogue_eviscerate][url=?spells=7.1.26]Waffen[/url][/icon] und [icon name=ability_warrior_innerrage][url=?spells=7.1.256]Furor[/url][/icon]. Der Furor-Talentbaum enthält das Talent [spell=46917], das es dem Krieger erlaubt, zwei Zweihandwaffen gleichzeitig zu führen! Krieger sind in der Lage, mit Fähigkeiten wie [spell=845], [spell=1680] und [spell=46924] starken Flächenschaden im Nahkampf zu verursachen. Ein Krieger kämpft in einer bestimmten [i]Haltung[/i], die ihm Boni und Zugang zu verschiedenen Fähigkeiten gewährt. Zu Beginn verfügen Krieger nur über die [spell=2457], erlernen aber mit Level 10 [spell=71] und mit Level 30 [spell=2458]. Die Verteidigungshaltung wird zum Tanken, die Kampfhaltung oder Berserkerhaltung für erheblichen Nahkampfschaden verwendet.\n\n[ul][li]Alle Krieger können ihren Schlachtzug oder ihre Gruppe mit einem [spell=6673] oder [spell=469] verstärken. Furor-Krieger besitzen den passiven Stärkungszauber [spell=29801], der die Chance auf kritische Treffer im Nah- und Fernkampf für ihre Verbündeten deutlich erhöht.[/li][li]Krieger haben zahlreiche nützliche Fähigkeiten, um schnell an ihr Ziel zu gelangen! Alle Krieger können [spell=100] oder [spell=20252] benutzen, um einen Gegner zu erreichen. Zudem können sie schnell [spell=3411], um ein befreundetes Ziel vor einem Angriff zu schützen.[/li][/ul]',NULL),(13,2,3,NULL,0,2,'[b][color=c2]Paladine[/color][/b] unterstützen ihre Verbündeten mit heiligen Auren und Segen, um sie vor Schaden zu bewahren und ihre Kräfte zu stärken. Sie tragen Plattenrüstungen und können in den härtesten Schlachten verheerenden Schlägen standhalten, während sie ihre Verwundeten heilen und die Gefallenen wiederbeleben. Im Kampf können sie mächtige Zweihandwaffen führen, ihre Feinde betäuben, Untote und Dämonen vernichten und ihre Feinde mit heiliger Vergeltung richten. Paladine sind eine defensive Klasse, die in erster Linie darauf ausgelegt ist, ihre Gegner zu überdauern.\n\nDer Paladin ist hauptsächlich ein Nahkämpfer und in geringem Maße Zauberer, der aufgrund seiner [url=?spells=7.2&filter=cr=109:12;crs=10:1;crv=0:0]Heilzauber[/url], [url=?spells=7.2&filter=na=Segen]Segen[/url] und anderen Fähigkeiten sehr nützlich für die Gruppe ist. Sie können eine aktive [url=?spells=7.2&filter=na=Aura]Aura[/url] pro Paladin auf alle Gruppen- und Schlachtzugsmitglieder legen und bestimmte Segen für bestimmte Spieler verwenden. Dank ihrer zahlreichen defensiven Fähigkeiten vergessen Paladine einfach unglaublich oft zu sterben. Mit ihrer Fähigkeit [spell=25780] sind sie außerdem ausgezeichnete Tanks.\n\n[ul][li]Paladine können effektiv [icon name=spell_holy_holybolt][url=?spells=7.2.594]heilen[/url][/icon], [icon name=spell_holy_devotionaura][url=?spells=7.2.267]tanken[/url][/icon] und im Nahkampf [icon name=spell_holy_auraoflight][url=?spells=7.2.184]Schaden[/url][/icon] verursachen.[/li][li]Sie besitzen eine große Auswahl an Segen, Auren und anderen Verstärkungszaubern.[/li][li]Der Paladin ist die einzige Klasse mit Zugang zu einem echten Unverwundbarkeitszauber: [spell=642].[/li][/ul]',NULL),(13,3,3,NULL,0,2,'[b][color=c3]Jäger[/color][/b] sind eine besonders einzigartige Klasse in World of Warcraft. Sie sind die einzigen nicht-magischen Fernkämpfer, die mit Bögen und Gewehren kämpfen. Jäger verfügen über verschiedene Arten von Schüssen und Bissen zur Schwächung ihrer Gegner und können [url=?spells=7.3&filter=cr=4;crs=1;crv=0;na=Falle]Fallen[/url] legen, um Schaden zu verursachen oder den Gegner auf andere Weise zu verlangsamen oder kampfunfähig zu machen.\n\nJäger [icon name=ability_hunter_beasttaming][url=?spell=1515]zähmen Wildtiere[/url][/icon], damit diese sie als [url=?pets]Begleiter[/url] im Kampf unterstützen. Zwar sind Jäger nicht die einzige Klasse, die Begleiter einsetzen kann. Ihre Tierbegleiter sind aber insofern einzigartig, als jede Spezies einen [url=?petcalc]eigenen Talentbaum[/url] hat, den der Jäger nutzen kann, um Punkte auf verschiedene Fähigkeiten zu verteilen.\n\nDarüber hinaus hat jede Spezies eine einzigartige Spezialfähigkeit. Jäger können sich die begehrtesten Begleiter aufgrund ihres Aussehens oder ihrer Fähigkeiten aussuchen. Und wenn sie genug Talentpunkte in den Baum der [icon name=ability_hunter_beasttaming][url=?spells=7.3.50]Tierherrschaft[/url][/icon] investieren, können sie besondere, "exotische" Bestien zähmen, wie [url=?pet=46]Geisterbestien[/url] oder [url=?pet=39]Teufelssaurier[/url]!\n\n[ul][li]Jäger haben Zugriff auf 25 (32 als [icon name=ability_hunter_beastmastery][url=?spell=53270]Meister der Tierherrschaft[/url][/icon]) verschiedene Arten von Begleitern mit über 150 verschiedenen Erscheinungsbildern![/li][li]Jäger haben eine Reihe von überlebensorientierten Fähigkeiten, die sie einsetzen können, um potentiellen Gefahren zu entkommen oder ihnen auszuweichen, wie z.B. [spell=5384] und [spell=781].[/li][li]Auf das [icon name=ability_hunter_swiftstrike][url=?spells=7.3.51]Überleben[/url][/icon] spezialisierte Jäger können in ihrem Talentbaum Punkte in das Talent [icon name=ability_hunter_huntingparty][url=?spells=-2.3&filter=na=jagdgesellschaft rel=spell=53292]Jagdgesellschaft[/url][/icon] investieren, welches es ihnen ermöglicht, ihre Gruppen- und Schlachtzugsmitglieder mit dem Stärkungszauber [spell=57669] zu versorgen.[/li][/ul]',NULL),(13,4,3,NULL,0,2,'[b][color=c4]Schurken[/color][/b] sind eine Nahkampfklasse, die Lederrüstungen trägt und ihren Feinden mit sehr schnellen Angriffen großen Schaden zufügen kann. Sie sind Meister der Verstohlenheit und des Meuchelns, die sich ungesehen an Feinden vorbeischleichen, aus den Schatten heraus zuschlagen und dann blitzschnell aus dem Kampf verschwinden.\n\nSie sind in der Lage, [url=?items=0.-3&filter=cr=152;crs=4;crv=0;ty=-3#0+1-2]Gifte[/url] einzusetzen, um ihre Gegner zu verkrüppeln und sie so im Kampf massiv zu schwächen. Schurken verfügen über ein mächtiges Arsenal an Fähigkeiten, von denen viele dadurch verstärkt werden, dass sie in [spell=1784] schleichen und ihre Opfer kampfunfähig machen können.\n\nSchurken können sich auf drei unterschiedliche Kampfstile mithilfe ihrer Talentbäume Meucheln, Kampf und Täuschung spezialisieren.\n\nAuf das [icon name=ability_rogue_eviscerate][url=?spells=7.4.253]Meucheln[/url][/icon] spezialisierte Schurken sind [icon name=ability_creature_poison_06][url=?spells=-2&filter=na=meister+der+gifte rel=spell=58410]Meister der Gifte[/url][/icon] und [icon name=ability_rogue_disembowel][url=?spell=57993]vergiften[/url][/icon] ihre Gegner mit schnellen Dolchen, die mit [icon name=ability_rogue_feigndeath][url=?spells=-2.4.253&filter=na=Üble+Gifte rel=spell=16515]üblen[/url][/icon] und [icon name=ability_poisons][url=?spells=-2.4.253&filter=na=Verbesserte+Gifte rel=spell=14117]verbesserten[/url][/icon] Giften versehen sind.\n\nAuf den [icon name=ability_backstab][url=?spells=7.4.38]Kampf[/url][/icon] spezialisierte Schurken können den Umgang mit [icon name=inv_sword_27][url=?spells=-2&filter=na=Niedermetzeln rel=spell=13964]Axt und Schwert[/url][/icon] oder [icon name=inv_mace_01][url=?spells=-2&filter=na=Streitkolben-Spezialisierung;cl=4 rel=spell=13803]Streitkolben[/url][/icon] meistern und haben mithilfe ihrer Talente auch in langwierigen Kämpfen eine verbesserte Energiezufuhr, um zuverlässig ihre Angriffscombos durchzuführen.\n\nAuf die [icon name=ability_stealth][url=?spells=7.4.39]Täuschung[/url][/icon] spezialisierte Schurken besitzen Fähigkeiten, die unvorhergesehene Aktionen ermöglichen. So können sie dank [spell=51713] etwa kurzzeitig Fähigkeiten nutzen, die eigentlich nur aus der Verstohlenheit heraus nutzbar wären, mit [spell=36554] plötzlich hinter einem Gegner auftauchen oder mit [icon name=ability_rogue_cheatdeath][url=?spells=-2.4.39&filter=cr=15;crs=0;crv=ability_rogue_cheatdeath rel=spell=31230]Von der Schippe springen[/url][/icon] einen sicheren Todesstoß überleben.',NULL),(13,5,3,NULL,0,2,'[b][color=c5]Priester[/color][/b] gelten allgemein als eine der Standard-Heilerklassen in World of Warcraft, da sie über zwei Talentspezialisierungen zur effektiven Heilung verfügen.\n\nIhr [icon name=spell_holy_holybolt][url=?spells=7.5.56]Heilig[/url][/icon]-Talentbaum enthält Talente, die die Heilung auf ihre Verbündeten erheblich verstärken - einschließlich Zaubern, mit denen mehrere Spieler gleichzeitig geheilt werden können, wie z.B. [spell=48089].\nDer [icon name=spell_holy_wordfortitude][url=?spells=7.5.613]Disziplin[/url][/icon]-Talentbaum ist zwar auch in der Lage, eine beträchtliche Menge an Heilung zu bewirken, konzentriert sich aber in erster Linie auf die Schadensabsorption und -verminderung durch den Einsatz von [spell=48066] und [icon name=spell_holy_devineaegis][url=?spells=-2.5&filter=cr=15;crs=0;crv=spell_holy_devineaegis rel=spell=47515]Göttliche Aegis[/url][/icon].\nPriester können außerdem mit ihren [icon name=spell_shadow_shadowwordpain][url=?spells=7.5.78]Schatten[/url][/icon]fähigkeiten sehr mächtigen Fernkampfschaden verursachen. Insbesondere wenn sie ihre [spell=15473] annehmen, erhöht sich ihr Schattenschaden erheblich, aber sie verlieren ihre Fähigkeit, Heiligzauber zu wirken.\n\n[ul][li]Der Disziplin-Talentbaum wird in der Regel zur Heilung verwendet, enthält aber auch einige Talente zur Erhöhung des Schadens des Priesters, wobei Schattenzauber und -fähigkeiten in erster Linie zur Verursachung von Fernkampfschaden verwendet werden sollten.[/li][li]Priester verfügen über einen der am meisten geschätzten Stärkungszauber im Spiel - [spell=48161], welcher allen befreundeten Mitspielern eine unverzichtbare Erhöhung ihrer Ausdauer gewährt. Außerdem können sie ihre Mitspieler mit [spell=48073] und [spell=48169] verstärken und mit einzigartigen Hymnen das [icon name=spell_holy_divinehymn][url=?spell=64843]Leben[/url][/icon] und [icon name=spell_holy_symbolofhope][url=?spell=64901]Mana[/url][/icon] ihres Schlachtzug signifikant wiederherstellen![/li][li]Schattenpriester unterstützen, zusätzlich zu ihrem Schaden, jeden Schlachtzug mit dem beliebten Stärkungszauber [spell=57669] zur Erhöhung der Manaregeneration und mit ihrer [spell=15286], die ihre gesamte Gruppe passiv heilt.[/li][/ul]',NULL),(13,6,3,NULL,0,2,'Die [b][color=c6]Todesritter[/color][/b] wurden in der Erweiterung Wrath of the Lich King eingeführt und sind die erste Heldenklasse von World of Warcraft. Todesritter beginnen auf Stufe 55 in einer speziellen, instanzierten Zone, die für andere Klassen unzugänglich ist: [url=?maps=4298:511346]Acherus, die Schwarze Festung[/url] in der Scharlachroten Enklave der Östlichen Pestländer. Hier erhalten sie ihre Talentpunkte durch Questbelohnungen und bekommen sogar ein besonderes beschworenes Reittier: das [spell=48778]!\n\nTodesritter haben mehrere sehr starke Möglichkeiten zur Schadensverursachung, da jeder ihrer Talentbäume es erlaubt mit einer Vielfalt an Nahkampffähigkeiten, Zaubern und Schaden-über-Zeit verursachenden Krankheiten überragende Leistung zu erbringen. Sie sind auch sehr fähige Tanks, wobei sowohl ihr Blut- als auch ihr Frost-Talentbaum einzigartige Optionen bietet. [icon name=spell_deathknight_bloodpresence][url=?spells=7.6.770]Blut[/url][/icon] bietet mehr Selbstheilungsfähigkeiten, [icon name=spell_deathknight_frostpresence][url=?spells=7.6.771]Frost[/url][/icon] bietet erhebliche Schadensminderung und starken Flächenschaden.\n\nTodesritter kämpfen mit einem besonderen Verstärkungszauber, der [url=?spells=7&filter=na=präsenz]Präsenz[/url] genannt wird (ähnlich wie die Haltungen eines Kriegers), der ihnen besondere Boni für ihre Rollen verleiht. Todesritter verwenden ein einzigartiges Ressourcensystem, bei dem die meisten Zauber entweder [url=?spells=7.6&filter=cr=45;crs=10;crv=0#50+1+13+3]Runen[/url] kosten, die während des Kampfes wieder aufgefüllt werden, oder [url=?spells=7.6&filter=cr=45;crs=11;crv=0]Runenmacht[/url], die durch verschiedene Fähigkeiten erzeugt werden kann.\n\n[ul][li]Auf [icon name=spell_deathknight_unholypresence][url=?spells=7.6.772]Unheilig[/url][/icon] spezialisierte Todesritter können sich in [spell=52143] spezialisieren, was ihren beschworenen Ghul-Wächter zu einem permanenten Begleiter macht, der sie im Kampf unterstützt![/li][li]Die Klasse der Todesritter verfügt über eine eigene spezielle Waffenverzauberungsfähigkeit namens [spell=53428], die herkömmliche Waffenverzauberungen überflüssig macht.[/li][li]Todesritter sind eine Schadensklasse, die ihren Schaden sowohl durch Nahkampffähigkeiten als auch durch Zauber verursacht![/li][/ul]',NULL),(13,7,3,NULL,0,2,'[b][color=c7]Schamanen[/color][/b] beherrschen Elementar- und Naturmagie und bringen einer (Schlachtzugs-) Gruppe die größte Vielfalt an potenziellen Stärkungszaubern in Form von [url=?spells=7&filter=na=Totem;cl=7]Totems[/url]. Ein Schamane kann für jedes Element - Erde, Feuer, Luft und Wasser - ein Totem beschwören, welches zu seinen Füßen erscheint und allen Mitgliedern seiner (Schlachtzugs-) Gruppe in Reichweite einen Stärkungszauber verleiht. Einige Totems, insbesondere Feuer-Totems, fügen Gegnern auch Schaden zu. Der Trick beim Spielen jeder Art von Schamanen besteht darin, zu wissen, welche Totems in welcher Situation beschworen werden müssen, um den verursachten Schaden und die Überlebensfähigkeit ihrer Gruppe zu maximieren.\n\nSchamanen sind in erster Linie Zauberer, wobei ein auf [icon name=spell_nature_lightningshield][url=?spells=7.7.373]Verstärkung[/url][/icon] spezialisierter Schamane Schaden in Nahkampfreichweite verursacht. Ein solcher Schamane erlernt das Führen zweier Waffen durch [spell=30798] und kann mit [spell=51533] zwei Schattenwölfe zur Unterstützung im Kampf beschwören. Obwohl sie hauptsächlich im Nahkampf eingesetzt werden, können auf Verstärkung spezialisierte Schamanen dennoch einen gewissen Nutzen aus ihrer Zaubermacht ziehen und spontane [icon name=spell_nature_lightning][url=?spell=49238]Blitzschläge[/url][/icon] oder [url=?spells=7&filter=cr=109:12:14;crs=10:1:5;crv=0:0:60000;cl=7]Heilungen[/url] durch [icon name=spell_shaman_maelstromweapon][url=?spells=-2&filter=na=waffe+des+mahlstroms rel=spell=51532]Waffe des Mahlstroms[/url][/icon] wirken.\n\nAuf [icon name=spell_nature_lightning][url=?spells=7.7.375]Elementarkampf[/url][/icon] spezialisierte Schamanen wirken Feuer- und Blitzzauber auf Distanz und verursachen so großen Schaden. Sie können Gegner durch [spell=59159] zurückstoßen und mit [icon name=spell_shaman_stormearthfire][url=?spells=-2&filter=na=sturm%2C+erde+und+feuer rel=spell=51486]Sturm, Erde und Feuer[/url][/icon] alle Feinde in einem Gebiet festwurzeln. Außerdem gewähren sie durch [spell=57722] und [icon name=spell_shaman_elementaloath][url=?spells=-2&filter=na=Elementarer+Schwur rel=spell=51470]Elementarer Schwur[/url][/icon] begehrte Stärkungszauber für Zauberer ihres Schlachtzugs.\n\nEin auf [icon name=spell_nature_magicimmunity][url=?spells=7.7.374]Wiederherstellung[/url][/icon] spezialisierter Schamane erhält verbesserte Heilzauber und kann ein ausgezeichneter Schlachtzugs- oder Tankheiler sein. Sie sind bekannt für ihre mächtige Fähigkeit [spell=55459] und dafür, dass sie ein [spell=16190] zur Verfügung stellen, welches der Gruppe hilft Mana wiederherzustellen. Sie erhalten außerdem ein mächtiges [spell=49284], können mit [spell=51886] Flüche entfernen und verfügen durch [spell=61301] über einen Spontanheilungseffekt, der zusätzlich eine Heilung über Zeit verursacht.\n\n[ul][li]Es gibt über zwanzig verschiedene Totems, die ein Schamane erlernen kann![/li][li]Schamanen der Horde können [spell=2825] und Schamanen der Allianz [spell=32182] wirken, wodurch der verursachte Schaden und die gewirkte Heilung der gesamten Gruppe erhöht wird. Dieser Stärkungszauber ist einzigartig und in jeder Schlachtzugsgruppe sehr begehrt.[/li][li]Ein Schamane kann sich ab Stufe 16 in einen [spell=2645] verwandeln und dies mit dem Talent [spell=16287] sogar als Spontanzauber wirken. Dieser Zauber kann im Kampf eingesetzt werden, aber nicht in geschlossenen Räumen.[/li][li]Schamanen können immer nur einen Elementarschild - [spell=49281] oder [spell=57960] - gleichzeitig benutzen. Auf Wiederherstellung spezialisierte Schamanen können zudem [spell=49284] auf einen anderen Spieler wirken.[/li][/ul]',NULL),(13,8,3,NULL,0,2,'[b][color=c8]Magier[/color][/b] bändigen die Elemente Feuer, Frost und Arkan, um ihre Feinde zu vernichten oder unter Kontrolle zu halten. Dazu besitzen sie ein Arsenal voller Zauber zu unterschiedlichen Zwecken.\nStärkungszauber, [icon name=ability_mage_conjurefoodrank10][url=?spell=42956]herbeigezauberte Erfrischungen[/url][/icon] oder arkane [url=?spells=7&filter=na=Portal]Portale[/url] zur schnellen Weltreise in ferne Länder machen einen Magier zu einem idealen Weggefährten.\nUnd wenn man eine Klasse sucht, die Gegner in eine Welt des Schmerzes einführt, ist der Magier eine gute Wahl. Ihren Gegnern können Magier mit verschiedensten Schwächungszaubern die Bedingungen eines jeden Kampfes diktieren, mit Elementarblitzen massiven Schaden aus der Ferne anrichten, oder Zerstörung in einem großen Wirkungsbereich niederregnen lassen.\n\nAuf [icon name=spell_holy_magicalsentry][url=?spells=7.8.237]Arkan[/url][/icon] spezialisierte Magier haben das Potenzial, mit [icon name=spell_arcane_blast][url=?spell=42897]Arkanschlägen[/url][/icon] und [icon name=ability_mage_missilebarrage][url=?spells=-2&filter=na=Geschosssalve rel=spell=54490]Salven[/url][/icon] an [icon name=spell_nature_starfall][url=?spell=42846]Arkanen Geschossen[/url][/icon] in kurzer Zeit enormen Schaden zu verursachen. Das Bändigen der reinen arkanen Mächte hat jedoch ihre Kehrseite: einem unerfahrenen Arkanmagier verzehrt es schon nach kurzer Zeit seine gesamten Kräfte.\n\nAuf [icon name=spell_fire_flamebolt][url=?spells=7.8.8]Feuer[/url][/icon] spezialisierte Magier verfallen durch kritische Treffer mit Feuerzaubern in [icon name=ability_mage_hotstreak][url=?spells=-2&filter=na=Kampfeshitze rel=spell=44448]Kampfeshitze[/url][/icon] und äschern so ihre Gegner mit verheerenden [icon name=spell_fire_fireball02][url=?spell=42891]Pyroschlägen[/url][/icon] ein. Zudem verwandeln sie ihre Gegner in [icon name=ability_mage_livingbomb][url=?spell=55360]Lebende Bomben[/url][/icon] und verursachen dadurch explosiven Flächenschaden.\n\n[icon name=spell_frost_frostbolt02][url=?spells=7.8.6]Frost[/url][/icon]magier können ihre Gegner [icon name=ability_mage_deepfreeze][url=?spell=44572]in Eis erstarren[/url][/icon] lassen. Ihre Spezialisierung auf Kälteeffekte erlaubt ihnen eine starke Kontrolle über ihre Gegner und erhöht dadurch ihre Überlebensfähigkeit enorm.\n\n[ul][li]Magier können Erfrischungen herbeizaubern, um die Gesundheit und das Mana ihrer Verbündeten wiederherzustellen.[/li][li]Sie sind die einzige Klasse, die Portale erschaffen kann, um andere Spieler zu transportieren. Sie können jedoch keine Spieler von einem entfernten Ort herbeirufen - das ist die Aufgabe eines [class=9]![/li][li]Der verursachte Fernkampfschaden von Magiern ist einer der höchsten im Spiel und macht sie zu einem unverzichtbaren Verbündeten in jedem Schlachtzug.[/li][/ul]',NULL),(13,9,3,NULL,0,2,'[b][color=c9]Hexenmeister[/color][/b] sind Meister der dämonischen Künste. Gekleidet in Gewänder sind sie Meister im Wirken von [url=?spells=7&filter=cr=12;crs=1;crv=0;na=Fluch+der;cl=9]Flüchen[/url], dem Schleudern von Feuer- oder Schattenblitzen und der Beschwörung von [url=?spells=7&filter=cr=14;crs=6;crv=48018;na=beschwören;cl=9]Dämonen[/url] unter ihre Kontrolle zur Unterstützung im Kampf. Die Kombination ihrer Flüche und direkten Schadenszauber richten Verwüstung und Zerstörung an und machen Hexenmeister zu sehr gefürchteten Gegnern.\n\nNeben Mana als primäre Ressource können Hexenmeister Gegnern Teile ihrer [icon name=spell_shadow_haunting][url=?spell=47855]Seele stehlen[/url][/icon] und dadurch [item=6265] erzeugen. Seelensplitter ermöglichen mächtige rituelle Magie, etwa zur [icon name=spell_shadow_twilight][url=?spell=698]Beschwörung von anderen Spielern[/url][/icon] oder von [icon name=spell_shadow_shadesofdarkness][url=?spell=58887]Gesundheitssteinen[/url][/icon] mit heilenden Kräften. Insbesondere kann jedoch ein Hexenmeister mit ihnen die Seele eines Verbündeten in einem [icon name=spell_shadow_soulgem][url=?spell=47884]Seelenstein[/url][/icon] speichern, sodass dieser im Todesfall sich selbst wiederbeleben kann.\n\n[ul][li]Hexenmeister können durch ein Ritual der Beschwörung ein Portal erschaffen, um einen anderen Spieler an den Ort des Portals zu beschwören.[/li][li]Sie können Gesundheitssteine beschwören, die den Anwender heilen.[/li][li]Die Flüche eines Hexenmeisters können ihre Feinde schwächen oder ihnen Schaden zufügen.[/li][/ul]',NULL),(13,11,3,NULL,0,2,'[b][color=c11]Druiden[/color][/b] sind die "Alleskönner"-Klasse in World of Warcraft - das heißt, sie können in einer Vielzahl von verschiedenen Rollen agieren und bieten daher einen der vielfältigsten Spielstile. Durch das [i]Annehmen der Gestalt von verschiedenen Kreaturen[/i] kann der Druide heilen, Schaden im Nah- und Fernkampf verursachen oder als Tank agieren. Mit steigenden Stufen kann der Druide neue, immer mächtigere Gestaltwandlungen erlernen, um sich in eine Kreatur passend zu seiner Rolle zu verwandeln.\n\nAuf niedrigeren Stufen wird ein Druide in seiner humanoiden Gestalt heilen oder im Fernkampf Schaden verursachen. Auf späteren Stufen jedoch erhalten Druiden durch die spezialisierten Talentbäume Zugang zu zwei besonderen Gestalten für jede unterschiedliche Rolle.\n\nAuf [icon name=spell_nature_healingtouch][url=?spells=7.11.573]Wiederherstellung[/url][/icon] spezialisierte Druiden erlernen den [spell=33891], der die Manakosten ihrer Heilzauber reduziert und jegliche Heilung auf ihre Verbündeten verstärkt.\nAuf [icon name=spell_nature_starfall][url=?spells=7.11.574]Gleichgewicht[/url][/icon] spezialisierte Druiden verursachen Schaden im Fernkampf und erlernen die [spell=24858], die ihre Rüstung sowie die Chance auf kritische Treffer mit Zaubern bei ihnen und ihren Verbündeten erhöht.\nEs gibt auch zwei Druidenformen für den [icon name=ability_racial_bearform][url=?spells=7.11.134]Wilden Kampf[/url][/icon]. Zum einen die mächtige [spell=5487] (und [spell=9634] ab einer höheren Stufe) - eine auf das Tanken ausgelegte Gestalt, die zusätzliche Rüstung, Gesundheit und Zugang zu einem Arsenal von Fähigkeiten zur Erhöhung der Bedrohung und Schadensverminderung gewährt. Zum anderen die schurkenähnliche [spell=768], die erheblichen Nahkampfschaden verursachen kann.\n\n[ul][li]Druiden erlernen ihre verschiedenen Gestalten durch das Abschließen von Quests oder durch Training. Einige Gestalten können nur durch Talente erlernt werden.[/li][li]Es gibt einige Gestalten, die alle Druiden erlernen können. Die Bärengestalt erhält man ab Stufe 10, die [spell=1066] und [spell=783] ab Stufe 16, die Katzengestalt ab Stufe 20 und die Terrorbärengestalt ab Stufe 40.[/li][li]Druiden haben sogar ihre eigene fliegende Reisegestalt: die [spell=33943] kann ab Stufe 60 und die [spell=40120] ab Stufe 71 erlernt werden, sofern der Spieler bereits [icon name=spell_nature_swiftness][url=?spell=34093]Gekonntes Reiten[/url][/icon] erlernt hat.[/li][li]Einige Druidengestalten können nur über Talente erlernt werden - die Mondkingestalt kann ab Stufe 40 erlernt werden, wenn ein Spieler viele Talentpunkte im Gleichgewicht-Talentbaum verteilt, und Baum des Lebens ab Stufe 50, wenn er viele Talentpunkte im Wiederherstellung-Talentbaum verteilt.[/li][li]Druiden haben ihre eigene, klassenspezifische [icon name=spell_arcane_teleportmoonglade][url=?spell=18960]Teleportationsfähigkeit[/url][/icon], die es ihnen erlaubt, zur [zone=493] zu reisen - praktisch, wenn sie trainieren müssen![/li][li]Druiden in (Terror-) Bärengestalt oder Katzengestalt schwingen zur Verursachung von Nahkampfschaden keine Waffen. Stattdessen erhalten sie einen speziellen Wert für jede ausgerüstete Nahkampfwaffe: die "Angriffskraft in Tiergestalt". Dieser Wert ist eine Umwandlung des "Schaden pro Sekunde"-Wertes einer Waffe in einen Wert, der Angriffskraft verleiht und den verursachten Schaden des Druiden in Katzen- oder (Terror-) Bärengestalt beeinflusst.[/li][/ul]',NULL); /*!40000 ALTER TABLE `aowow_articles` ENABLE KEYS */; UNLOCK TABLES; @@ -3322,7 +3322,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1753369289,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1753563162,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/updates/1753563161_01.sql b/setup/updates/1753563161_01.sql new file mode 100644 index 00000000..cae5801d --- /dev/null +++ b/setup/updates/1753563161_01.sql @@ -0,0 +1 @@ +UPDATE aowow_articles SET `article` = '[menu tab=2 path=2,8]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.png border=0 margin=5 float=left]Firefox[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-firefox.png][/div]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n if (typeof window.external.AddSearchProvider == "function")\r\n window.external.AddSearchProvider("STATIC_URL/download/searchplugins/aowow.xml");\r\n else\r\n alert("This feature is unavailable.");\r\n}\r\n[/script]\r\n[pad]\r\nEither\r\n[ul]\r\n[li]Click on the button below to install the search plugin in your browser or[/li]\r\n[li]Right-click your address bar and then clck on "Add AoWoW" or[/li]\r\n[li]Click on the [img src=STATIC_URL/images/icons/add.png border=0] on the browser search bar and then on [img src=STATIC_URL/images/icons/add.png border=0] "Add search engine"[/li]\r\n[/ul]\r\n\r\n[pad]\r\n[html]Install pluginInstall plugin[/html]\r\n[div clear=both][/div]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/edge.png border=0 float=left][img src=STATIC_URL/images/help/searchplugins/chrome.png border=0 float=left]MS Edge / Google Chrome[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-edge.png][/div]\r\n[pad]\r\nFor Chrome-based browsers go to settings and fill in the add search engine form as shown.\r\n[pad]\r\n[div width=500px]\r\n[pre]HOST_URL/?search=%s[img src=STATIC_URL/images/icons/pages.gif float=right][/pre]\r\n[/div]\r\n[script]\r\nsetTimeout(() => $WH.clickToCopy($WH.qs("pre > img"), "HOST_URL/?search=%s"), 100);\r\n[/script]\r\n[pad]\r\nSave your changes, and you\'ll be able to perform Aowow searches by typing "db" followed by the search terms in the address bar (e.g. db sword).\r\n[div clear=both][/div]\r\n' WHERE `url` = 'searchplugins' AND `locale` = 0; diff --git a/static/images/help/searchplugins/chrome.png b/static/images/help/searchplugins/chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..da28ab3ae739bef98d38927d7024c989e29d67bc GIT binary patch literal 2498 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�?{ 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 literal 0 HcmV?d00001 diff --git a/static/images/help/searchplugins/os-firefox.png b/static/images/help/searchplugins/os-firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..f46d539243e26e1a7c389f78274960f5cd8b600c GIT binary patch literal 48416 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 literal 0 HcmV?d00001 diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 194e5e0c..eda14775 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -1086,7 +1086,7 @@ var mn_more = [ [7,"What's New","?whats-new"], [,"Goodies"], [16, "Search Box","?searchbox"], - [8,"Search Plugins (FF, IE7, ...)","?searchplugins"], + [8,"Search Plugins","?searchplugins"], [10,"Tooltips","?tooltips"] ]; From 056298919607b73bd82835806ab4607b08cc0e7f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 26 Jul 2025 23:12:06 +0200 Subject: [PATCH 617/957] Fix LocString serialization .. just implement __serialize, d'uh! --- includes/components/locstring.class.php | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/includes/components/locstring.class.php b/includes/components/locstring.class.php index 90408532..646c1fd4 100644 --- a/includes/components/locstring.class.php +++ b/includes/components/locstring.class.php @@ -29,11 +29,33 @@ class LocString return $str; foreach (Locale::cases() as $l) // desired loc not set, use any other - if ($str = $this->store[$l]) - return Cfg::get('DEBUG') ? '['.$str.']' : $str; + if (isset($this->store[$l])) + return Cfg::get('DEBUG') ? '['.$this->store[$l].']' : $this->store[$l]; return Cfg::get('DEBUG') ? '[LOCSTRING]' : ''; } + + public function __serialize(): array + { + $data = []; + foreach (Locale::cases() as $l) + if (isset($this->store[$l])) + $data[$l->value] = $this->store[$l]; + + return ['store' => $data]; + } + + public function __unserialize(array $data): void + { + $this->store = new \WeakMap(); + + if (empty($data['store'])) + return; + + foreach ($data['store'] as $locId => $str) + if (($l = Locale::tryFrom($locId))?->validate()) + $this->store[$l] = (string)$str; + } } ?> From 0928b1b43064e40c9654ff752131c1c78658651f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 26 Jul 2025 23:18:59 +0200 Subject: [PATCH 618/957] User * slightly modernize static class --- includes/user.class.php | 252 ++++++++++++++++++++++------------------ pages/account.php | 2 +- 2 files changed, 141 insertions(+), 113 deletions(-) diff --git a/includes/user.class.php b/includes/user.class.php index abb56582..739c8a9c 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -47,11 +47,11 @@ class User return false; // check IP bans - if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = 0', self::$ip)) + if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = 0', self::$ip)) { - if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['unbanDate'] > time()) + if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) return false; - else if ($ipBan['unbanDate'] <= time()) + else if (!$ipBan['active']) DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `ip` = ?', self::$ip); } @@ -97,26 +97,23 @@ class User self::$dailyVotes = $uData['dailyVotes']; self::$excludeGroups = $uData['excludeGroups']; - $conditions = array( - [['cuFlags', PROFILER_CU_DELETED, '&'], 0], - ['OR', ['user', self::$id], ['ap.accountId', self::$id]] - ); - - if (self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - array_shift($conditions); + $conditions = [['OR', ['user', self::$id], ['ap.accountId', self::$id]]]; + if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; self::$profiles = (new LocalProfileList($conditions)); if ($uData['avatar']) self::$avatar = $uData['avatar']; + // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) // - conscutive visits // - votes per day // - reputation for daily visit if (self::isLoggedIn()) { - $lastLogin = DB::Aowow()->selectCell('SELECT curLogin FROM ?_account WHERE id = ?d', self::$id); + $lastLogin = DB::Aowow()->selectCell('SELECT `curLogin` FROM ?_account WHERE `id` = ?d', self::$id); // either the day changed or the last visit was >24h ago if (date('j', $lastLogin) != date('j') || (time() - $lastLogin) > 1 * DAY) { @@ -204,116 +201,124 @@ class User /* auth mechanisms */ /*******************/ - public static function Auth($name, $pass) + public static function authenticate(string $name, string $password) : int { - $user = 0; - $hash = ''; + $userId = 0; + $hash = ''; - switch (Cfg::get('ACC_AUTH_MODE')) + $result = match (Cfg::get('ACC_AUTH_MODE')) { - case AUTH_MODE_SELF: - { - if (!self::$ip) - return AUTH_INTERNAL_ERR; + AUTH_MODE_SELF => self::authSelf($name, $password, $userId, $hash), + AUTH_MODE_REALM => self::authRealm($name, $password, $userId, $hash), + AUTH_MODE_EXTERNAL => self::authExtern($name, $password, $userId, $hash), + default => AUTH_INTERNAL_ERR + }; - // handle login try limitation - $ip = DB::Aowow()->selectRow('SELECT `ip`, `count`, `unbanDate` FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); - if (!$ip || $ip['unbanDate'] < time()) // no entry exists or time expired; set count to 1 - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK')); - else // entry already exists; increment count - DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip); + if ($result == AUTH_OK) + { + session_unset(); + $_SESSION['user'] = $userId; + $_SESSION['hash'] = self::hashCrypt($hash); + } - if ($ip && $ip['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ip['unbanDate'] >= time()) - return AUTH_IPBANNED; + return $result; + } - $query = DB::Aowow()->SelectRow( - 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status` - FROM ?_account a - LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() - WHERE a.`user` = ? - GROUP BY a.`id`', - $name - ); - if (!$query) - return AUTH_WRONGUSER; + private static function authSelf(string $name, string $password, int &$userId, string &$hash) : int + { + if (!self::$ip) + return AUTH_INTERNAL_ERR; - self::$passHash = $query['passHash']; - if (!self::verifyCrypt($pass)) - return AUTH_WRONGPASS; + // handle login try limitation + $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); + if (!$ipBan || !$ipBan['active']) // no entry exists or time expired; set count to 1 + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK')); + else // entry already exists; increment count + DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip); - // successfull auth; clear bans for this IP - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); + if ($ipBan && $ipBan['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) + return AUTH_IPBANNED; - if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP)) - return AUTH_BANNED; + $query = DB::Aowow()->SelectRow( + 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status` + FROM ?_account a + LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() + WHERE a.`user` = ? + GROUP BY a.`id`', + $name + ); - $user = $query['id']; - $hash = $query['passHash']; - break; - } - case AUTH_MODE_REALM: - { - if (!DB::isConnectable(DB_AUTH)) - return AUTH_INTERNAL_ERR; + if (!$query) + return AUTH_WRONGUSER; - $wow = DB::Auth()->selectRow('SELECT a.id, a.salt, a.verifier, ab.active AS hasBan FROM account a LEFT JOIN account_banned ab ON ab.id = a.id AND active <> 0 WHERE username = ? LIMIT 1', $name); - if (!$wow) - return AUTH_WRONGUSER; + self::$passHash = $query['passHash']; + if (!self::verifyCrypt($password)) + return AUTH_WRONGPASS; - if (!self::verifySRP6($name, $pass, $wow['salt'], $wow['verifier'])) - return AUTH_WRONGPASS; + // successfull auth; clear bans for this IP + DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); - if ($wow['hasBan']) - return AUTH_BANNED; + if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP)) + return AUTH_BANNED; - if ($_ = self::checkOrCreateInDB($wow['id'], $name)) - $user = $_; - else - return AUTH_INTERNAL_ERR; + $userId = $query['id']; + $hash = $query['passHash']; - break; - } - case AUTH_MODE_EXTERNAL: - { - if (!file_exists('config/extAuth.php')) - { - trigger_error('config/extAuth.php not found'); - return AUTH_INTERNAL_ERR; - } + return AUTH_OK; + } - require 'config/extAuth.php'; + private static function authRealm(string $name, string $password, int &$userId, string &$hash) : int + { + if (!DB::isConnectable(DB_AUTH)) + return AUTH_INTERNAL_ERR; - if (!function_exists('\extAuth')) - { - trigger_error('external auth function extAuth() not defined in config/extAuth.php'); - return AUTH_INTERNAL_ERR; - } + $wow = DB::Auth()->selectRow('SELECT a.id, a.salt, a.verifier, ab.active AS hasBan FROM account a LEFT JOIN account_banned ab ON ab.id = a.id AND active <> 0 WHERE username = ? LIMIT 1', $name); + if (!$wow) + return AUTH_WRONGUSER; - $extGroup = -1; - $result = \extAuth($name, $pass, $extId, $extGroup); + if (!self::verifySRP6($name, $password, $wow['salt'], $wow['verifier'])) + return AUTH_WRONGPASS; - if ($result == AUTH_OK && $extId) - { - if ($_ = self::checkOrCreateInDB($extId, $name, $extGroup)) - $user = $_; - else - return AUTH_INTERNAL_ERR; + if ($wow['hasBan']) + return AUTH_BANNED; - break; - } + if ($_ = self::checkOrCreateInDB($wow['id'], $name)) + $userId = $_; + else + return AUTH_INTERNAL_ERR; - return $result; - } - default: + return AUTH_OK; + } + + private static function authExtern(string $name, string $password, int &$userId, string &$hash) : int + { + if (!file_exists('config/extAuth.php')) + { + trigger_error('User::authExtern - AUTH_MODE_EXTERNAL is selected but config/extAuth.php does not exist!', E_USER_ERROR); + return AUTH_INTERNAL_ERR; + } + + require 'config/extAuth.php'; + + if (!function_exists('\extAuth')) + { + trigger_error('User::authExtern - AUTH_MODE_EXTERNAL is selected but function extAuth() is not defined!', E_USER_ERROR); + return AUTH_INTERNAL_ERR; + } + + $extGroup = -1; + $extId = 0; + $result = \extAuth($name, $password, $extId, $extGroup); + + if ($result == AUTH_OK && $extId) + { + if ($_ = self::checkOrCreateInDB($extId, $name, $extGroup)) + $userId = $_; + else return AUTH_INTERNAL_ERR; } - // kickstart session - session_unset(); - $_SESSION['user'] = $user; - $_SESSION['hash'] = $hash; - - return AUTH_OK; + return $result; } // create a linked account for our settings if necessary @@ -339,10 +344,10 @@ class User if ($newId) Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); - return $newId; + return $newId ?: 0; } - private static function createSalt() + private static function createSalt() : string { $algo = '$2a'; $strength = '$09'; @@ -352,18 +357,18 @@ class User } // crypt used by aowow - public static function hashCrypt($pass) + public static function hashCrypt(string $pass) : string { return crypt($pass, self::createSalt()); } - public static function verifyCrypt($pass, $hash = '') + public static function verifyCrypt(string $pass, string $hash = '') : string { $_ = $hash ?: self::$passHash; return $_ === crypt($pass, $_); } - private static function verifySRP6($user, $pass, $salt, $verifier) + private static function verifySRP6(string $user, string $pass, string $salt, string $verifier) : bool { $g = gmp_init(7); $N = gmp_init('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16); @@ -376,7 +381,7 @@ class User return ($verifier === str_pad(gmp_export($v, 1, GMP_LSW_FIRST), 32, chr(0), STR_PAD_RIGHT)); } - public static function isValidName($name, &$errCode = 0) + public static function isValidName(string $name, int &$errCode = 0) : bool { $errCode = 0; @@ -402,7 +407,7 @@ class User return $errCode == 0; } - public static function isValidPass($pass, &$errCode = 0) + public static function isValidPass(string $pass, int &$errCode = 0) : bool { $errCode = 0; @@ -420,9 +425,9 @@ class User /* access management */ /*********************/ - public static function isInGroup($group) : bool + public static function isInGroup(int $group) : bool { - return (self::$groups & $group) != 0; + return $group == U_GROUP_NONE || (self::$groups & $group) != U_GROUP_NONE; } public static function canComment() : bool @@ -511,25 +516,38 @@ class User public static function decrementDailyVotes() : void { + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) + return; + self::$dailyVotes--; DB::Aowow()->query('UPDATE ?_account SET `dailyVotes` = ?d WHERE `id` = ?d', self::$dailyVotes, self::$id); } public static function getCurrentDailyVotes() : int { + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::$dailyVotes < 0) + return 0; + return self::$dailyVotes; } public static function getMaxDailyVotes() : int { - if (!self::isLoggedIn() || self::isBanned()) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) return 0; - return Cfg::get('USER_MAX_VOTES') + (self::$reputation >= Cfg::get('REP_REQ_VOTEMORE_BASE') ? 1 + intVal((self::$reputation - Cfg::get('REP_REQ_VOTEMORE_BASE')) / Cfg::get('REP_REQ_VOTEMORE_ADD')) : 0); + $threshold = Cfg::get('REP_REQ_VOTEMORE_BASE'); + $extra = Cfg::get('REP_REQ_VOTEMORE_ADD'); + $base = Cfg::get('USER_MAX_VOTES'); + + return $base + max(0, intVal((self::$reputation - $threshold + $extra) / $extra)); } public static function getReputation() : int { + if (!self::isLoggedIn() || self::$reputation < 0) + return 0; + return self::$reputation; } @@ -555,10 +573,10 @@ class User $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE'); $gUser['characters'] = self::getCharacters(); $gUser['excludegroups'] = self::$excludeGroups; - $gUser['settings'] = (new \StdClass); // profiler requires this to be set; has property premiumborder (NYI) + $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; has property premiumborder (NYI) if (Cfg::get('DEBUG') && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_TESTER)) - $gUser['debug'] = true; // csv id-list output option on listviews + $gUser['debug'] = true; // csv id-list output option on listviews; todo - set on per user basis if ($_ = self::getProfilerExclusions()) $gUser = array_merge($gUser, $_); @@ -582,6 +600,9 @@ class User { $result = []; + if (!self::isLoggedIn() || self::isBanned()) + return $result; + $res = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `name` FROM ?_account_weightscales WHERE `userId` = ?d', self::$id); if (!$res) return $result; @@ -596,6 +617,10 @@ class User public static function getProfilerExclusions() : array { $result = []; + + if (!self::isLoggedIn() || self::isBanned()) + return $result; + $modes = [1 => 'excludes', 2 => 'includes']; foreach ($modes as $mode => $field) if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ?_account_excludes WHERE `mode` = ?d AND `userId` = ?d', $mode, self::$id)) @@ -644,10 +669,13 @@ class User { $result = []; + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE)) + return $result; + if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GUIDE_STATUS_ARCHIVED)) { // fix url - array_walk($guides, fn(&$x) => $x['url'] = '?guide='.($x['url'] ?? $x['id'])); + array_walk($guides, fn(&$x) => $x['url'] = '?guide='.($x['url'] ?: $x['id'])); $result = $guides; } @@ -664,7 +692,7 @@ class User public static function getFavorites() : array { - if (!self::isLoggedIn()) + if (!self::isLoggedIn() || self::isBanned()) return []; $res = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ?_account_favorites WHERE `userId` = ?d', self::$id); diff --git a/pages/account.php b/pages/account.php index ff814d32..4d856ef1 100644 --- a/pages/account.php +++ b/pages/account.php @@ -358,7 +358,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup if (!User::isValidPass($this->_post['password'])) return Lang::account('wrongPass'); - switch (User::Auth($this->_post['username'], $this->_post['password'])) + switch (User::authenticate($this->_post['username'], $this->_post['password'])) { case AUTH_OK: if (!User::$ip) From 3f0d6c2de652cb5730358a07a8b5da4d8041d797 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 12 Jul 2025 04:05:07 +0200 Subject: [PATCH 619/957] JS * add clickToCopy functionality --- static/css/global.css | 21 ++++++ static/js/basic.js | 42 +++++++++++- static/js/global.js | 143 ++++++++++++++++++++++++++++++++------- static/js/locale_dede.js | 6 ++ static/js/locale_enus.js | 6 ++ static/js/locale_eses.js | 6 ++ static/js/locale_frfr.js | 6 ++ static/js/locale_ruru.js | 6 ++ static/js/locale_zhcn.js | 6 ++ 9 files changed, 216 insertions(+), 26 deletions(-) diff --git a/static/css/global.css b/static/css/global.css index 64360ae1..b80fe0f7 100644 --- a/static/css/global.css +++ b/static/css/global.css @@ -802,3 +802,24 @@ div.screenshotviewer-caption { .options-menu-widget.open { color:#fff; } + +.click-to-copy { + cursor: pointer; +} + +.fade-out { + opacity: 0; + pointer-events: none; + transition: opacity 1s; + transition-timing-function: ease-in; +} + +.hidden-element { + height: 0; + left: 0; + opacity: 0; + padding: 0; + pointer-events: none; + position: fixed; + top: 1px; +} diff --git a/static/js/basic.js b/static/js/basic.js index 4163d42a..05d30daf 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -2077,6 +2077,12 @@ $WH.Tooltip = { $WH.Tooltip.move(x, y, 0, 0, paddX, paddY); }, + showFadingTooltipAtCursor: function (text, ev, className, noWrap, maxWidth) { + text = $WH.Tooltip.prepareTooltipHtml(text, noWrap, maxWidth, ev); + $WH.Tooltip.showAtCursor(ev, text, undefined, undefined, className); + requestAnimationFrame(function () { $WH.Tooltip.tooltip.classList.add('fade-out'); }); + }, + cursorUpdate: function(e, x, y) { // Used along with showAtCursor if ($WH.Tooltip.disabled || !$WH.Tooltip.tooltip) { return; @@ -2126,12 +2132,42 @@ $WH.Tooltip = { $WH.Tooltip.iconVisible = icon ? 1 : 0; }, - simple: function(element, text, className, fixed) { + prepareTooltipHtml: function (textOrFn, noWrap, maxWidth, ev) { + textOrFn = typeof textOrFn === "function" ? textOrFn.call(ev.target, ev) : textOrFn; + if (typeof textOrFn === "string") { + if (noWrap === undefined && textOrFn.length < 30) + noWrap = true; + + let attr = []; + if (noWrap) + attr.push(' class="no-wrap"'); + + if (maxWidth && !isNaN(maxWidth)) + attr.push(' style="max-width:' + maxWidth + 'px"'); + + if (attr.length) + textOrFn = "" + textOrFn + ""; + } + + return textOrFn; + }, + + simple: function(element, textOrFn, className, fixed) { if (fixed) - element.onmouseover = function(x) { $WH.Tooltip.show(element, text, false, false, className) }; + { + element.onmouseover = function(ev) + { + let text = $WH.Tooltip.prepareTooltipHtml(textOrFn, null, null, ev); + $WH.Tooltip.show(element, text, false, false, className); + }; + } else { - element.onmouseover = function(x) { $WH.Tooltip.showAtCursor(x, text, false, false, className) }; + element.onmouseover = function(ev) + { + let text = $WH.Tooltip.prepareTooltipHtml(textOrFn, null, null, ev); + $WH.Tooltip.showAtCursor(ev, text, false, false, className); + }; element.onmousemove = $WH.Tooltip.cursorUpdate; } diff --git a/static/js/global.js b/static/js/global.js index 574eaa0b..cead8fe9 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -5346,32 +5346,23 @@ function Listview(opt) { id: 'debug-id', compute: function(data, td) { if (data.id) { - $WH.ae(td, $WH.ct(data.id)); + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); } }, getVisibleText: function(data) { - if (data.id) { - return data.id; - } - else { - return ''; - } + return data.id || ''; }, getValue: function(data) { - if (data.id) { - return data.id; - } - else { - return 0; - } + return data.id || 0; }, sortFunc: function(a, b, col) { - if (a.id == null) { + if (a.id == null) return -1; - } - else if (b.id == null) { + + if (b.id == null) return 1; - } return $WH.strcmp(a.id, b.id); }, @@ -5379,19 +5370,17 @@ function Listview(opt) { width: '5%', tooltip: 'ID' }); + this.visibility.splice(0, 0, -1); - for (var i = 0, len = this.visibility.length; i < len; ++i) { + for (var i = 0, len = this.visibility.length; i < len; ++i) this.visibility[i] = this.visibility[i] + 1; - } for (var i = 0, len = this.sort.length; i < len; ++i) { - if (this.sort[i] < 0) { + if (this.sort[i] < 0) this.sort[i] = this.sort[i] - 1; - } - else { + else this.sort[i] = this.sort[i] + 1; - } } } @@ -22637,6 +22626,114 @@ function CreateAjaxLoader() { } +$WH.clickToCopy = function (el, textOrFn, opt) { + opt = opt || {}; + + $WH.aE(el, 'click', $WH.clickToCopy.copy.bind(null, el, textOrFn, opt)); + // $WH.preventSelectStart(el); + + el.classList.add('click-to-copy'); + + if (opt.modifyTooltip) { + el._fixTooltip = function (e) { + return e + '
' + $WH.ce('span', { className: 'q2', innerHTML: $WH.clickToCopy.getTooltip(false, opt) }).outerHTML; + }; + + opt.overrideOtherTooltips = false; + } + + // Aowow - fitted to old system + // $WH.Tooltips.attach( + $WH.Tooltip.simple( + el, + $WH.clickToCopy.getTooltip.bind(null, false, opt), + undefined, + // { + /* byCursor: */ !opt.attachToElement, + // stopPropagation: opt.overrideOtherTooltips + // } + ); +}; +$WH.clickToCopy.copy = function (el, textOrFn, opt, ev) { + ev.preventDefault(); + ev.stopPropagation(); + + if (textOrFn === undefined) { + if (!el.childNodes[0] || !el.childNodes[0].textContent) { + let text = 'Could not find text to copy.'; + // $WH.error(text, el); + + if (opt.attachToElement) + $WH.Tooltip.show(el, text, 'q10'); + else + $WH.Tooltip.showAtCursor(ev, text, 'q10'); + + return; + } + + textOrFn = el.childNodes[0].textContent; + } + else if (typeof textOrFn === 'function') + textOrFn = textOrFn(); + + $WH.copyToClipboard(textOrFn); + + if (opt.attachToElement) + $WH.Tooltip.show(el, $WH.clickToCopy.getTooltip(true, opt)); + else + $WH.Tooltip.showAtCursor(ev, $WH.clickToCopy.getTooltip(true, opt)); +}; +$WH.clickToCopy.getTooltip = function (clicked, opt) { + let txt = ''; + let attr = undefined; + if (clicked) { + txt = ' ' + LANG.copied; + attr = { className: 'q1 icon-tick' }; + } + else + txt = LANG.clickToCopy; + + let tt = $WH.ce('div', attr, $WH.ct(txt)); + + if (opt.prefix) { + tt.style.marginTop = '10px'; + let prefix = typeof opt.prefix === 'function' ? opt.prefix() : opt.prefix; + return prefix + tt.outerHTML; + } + + return tt.outerHTML; +}; +$WH.copyToClipboard = function (text, t) { + if (!$WH.copyToClipboard.hiddenInput) { + $WH.copyToClipboard.hiddenInput = $WH.ce('textarea', { className: 'hidden-element' }); + $WH.ae(document.body, $WH.copyToClipboard.hiddenInput); + } + + $WH.copyToClipboard.hiddenInput.value = text; + + let isEmpty = $WH.copyToClipboard.hiddenInput.value === ''; + if (isEmpty) + $WH.copyToClipboard.hiddenInput.value = LANG.nothingToCopy_tip; + + $WH.copyToClipboard.hiddenInput.focus(); + $WH.copyToClipboard.hiddenInput.select(); + + if (!document.execCommand('copy')) + prompt(null, text); + + $WH.copyToClipboard.hiddenInput.blur(); + + if (t) { + if (isEmpty) + $WH.Tooltips.showFadingTooltipAtCursor(LANG.nothingToCopy_tip, t, 'q10'); + else { + let e = $WH.ce('span', { className: 'q1 icon-tick' }, $WH.ct(' ' + LANG.copied)); + $WH.Tooltips.showFadingTooltipAtCursor(e.outerHTML, t) + } + } +}; + + /* Global utility functions related to arrays, format validation, regular expressions, and strings */ diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 83692e30..1564acb3 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -4896,6 +4896,12 @@ var LANG = { /* AoWoW: start custom */ + // click to copy fn + copied: 'Kopiert', + clickToCopy: 'Klicke zum Kopieren', + nothingToCopy_tip: 'Nichts zu kopieren!', + + // TC conditions display tab_conditions: 'Konditionen', tab_condition_for: 'Kondition für', cnd_either: 'Entweder', diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index eda14775..012bb207 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -4944,6 +4944,12 @@ var LANG = { /* AoWoW: start custom */ + // click to copy fn + copied: 'Copied', + clickToCopy: 'Click to Copy', + nothingToCopy_tip: 'Nothing to copy!', + + // TC conditions display tab_conditions: 'Conditions', tab_condition_for: 'Condition for', cnd_either: 'Either', diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index da7e06bc..d9f78932 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -4898,6 +4898,12 @@ var LANG = { /* AoWoW: start custom */ + // click to copy fn + copied: 'Copiado', + clickToCopy: 'Click para copiar', + nothingToCopy_tip: '[Nothing to copy!]', + + // TC conditions display tab_conditions: 'Condiciones', tab_condition_for: 'Condición para', cnd_either: 'Cualquiera', diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index fa562af5..a27b4eae 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -4898,6 +4898,12 @@ var LANG = { /* AoWoW: start custom */ + // click to copy fn + copied: 'Copié', + clickToCopy: 'Cliquer pour Copier', + nothingToCopy_tip: 'Rien à copier !', + + // TC conditions display tab_conditions: '[Conditions]', tab_condition_for: '[Condition for]', cnd_either: '[Either]', diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index b2aea8e5..6825f61d 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -4900,6 +4900,12 @@ var LANG = { /* AoWoW: start custom */ + // click to copy fn + copied: 'Скопировано', + clickToCopy: 'Нажмите, чтобы скопировать', + nothingToCopy_tip: 'Нет данных для копирования!', + + // TC conditions display tab_conditions: '[Conditions]', tab_condition_for: '[Condition for]', cnd_either: '[Either]', diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index d6a21db6..b5bce119 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -4923,6 +4923,12 @@ var LANG = { /* AoWoW: start custom */ + // click to copy fn + copied: '已复制', + clickToCopy: '点击复制', + nothingToCopy_tip: '[Nothing to copy!]', + + // TC conditions display tab_conditions: '[Conditions]', tab_condition_for: '[Condition for]', cnd_either: '[Either]', From 967841fcb93ea1dca4c56f44b4ff291c04486ffb Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 25 Jul 2025 03:51:16 +0200 Subject: [PATCH 620/957] Spell/DetailPage * finally set GCDCat var * (a smooth decade later and it turns out it was StartRecoveryCategory all along) --- includes/types/spell.class.php | 1 + localization/locale_dede.php | 6 +++++- localization/locale_enus.php | 6 +++++- localization/locale_eses.php | 6 +++++- localization/locale_frfr.php | 6 +++++- localization/locale_ruru.php | 6 +++++- localization/locale_zhcn.php | 6 +++++- pages/spell.php | 14 +++++++++++++- template/pages/spell.tpl.php | 12 ++++++------ 9 files changed, 50 insertions(+), 13 deletions(-) diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php index 54dcb4f6..d6c7c993 100644 --- a/includes/types/spell.class.php +++ b/includes/types/spell.class.php @@ -2427,6 +2427,7 @@ class SpellListFilter extends Filter 19 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION ], // scaling 20 => [parent::CR_CALLBACK, 'cbReagents', ], // has Reagents [yn] 22 => [parent::CR_CALLBACK, 'cbProficiency', null, null ], // proficiencytype [proficiencytype] + // 26 => [parent::CR_NUMERIC, 'startRecoveryCategory', NUM_CAST_INT, false ], // gcd-cat 25 => [parent::CR_BOOLEAN, 'skillLevelYellow' ], // rewardsskillups 27 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CHANNELED_1, true ], // channeled [yn] 28 => [parent::CR_NUMERIC, 'castTime', NUM_CAST_FLOAT ], // casttime [num] diff --git a/localization/locale_dede.php b/localization/locale_dede.php index a7ec2b48..c69dfd03 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -46,7 +46,6 @@ $lang = array( 'gains' => "Belohnungen", 'login' => "Login", 'forum' => "Forum", - 'n_a' => "n. v.", 'siteRep' => "Ruf", 'yourRepHistory'=> "Dein Ruf-Verlauf", 'aboutUs' => "Über Aowow", @@ -1490,6 +1489,11 @@ $lang = array( '_seeMore' => "Mehr anzeigen", '_rankRange' => "Rang: %d - %d", '_showXmore' => "Zeige %d weitere", + + 'n_a' => "n. v.", + 'normal' => "Normal", + 'special' => "Besonders", + 'currentArea' => '<Momentanes Gebiet>', 'discovered' => "Durch Geistesblitz erlernt", 'ppm' => "(%s Auslösungen pro Minute)", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 35e78121..dca37d71 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -46,7 +46,6 @@ $lang = array( 'gains' => "Gains", 'login' => "Login", 'forum' => "Forum", - 'n_a' => "n/a", 'siteRep' => "Reputation", 'yourRepHistory'=> "Your Reputation History", 'aboutUs' => "About us & contact", @@ -1490,6 +1489,11 @@ $lang = array( '_seeMore' => "See more", '_rankRange' => "Rank: %d - %d", '_showXmore' => "Show %d More", + + 'n_a' => "n/a", + 'normal' => "Normal", + 'special' => "Special", + 'currentArea' => '<current area>', 'discovered' => "Learned via discovery", 'ppm' => "(%s procs per minute)", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index c92bc30a..477d8120 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -46,7 +46,6 @@ $lang = array( 'gains' => "Ganancias", 'login' => "Ingresar", 'forum' => "Foro", - 'n_a' => "n/d", 'siteRep' => "Reputación", 'yourRepHistory'=> "Tu Historial de Reputación", 'aboutUs' => "Sobre Aowow", @@ -1490,6 +1489,11 @@ $lang = array( '_seeMore' => "Más información", '_rankRange' => "Rango: %d - %d", '_showXmore' => "Mostrar %d más", + + 'n_a' => "n/d", + 'normal' => "Normal", + 'special' => "Especial", + 'currentArea' => '<current area>', 'discovered' => "Aprendido via descubrimiento", 'ppm' => "(%s procs por minuto)", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 1c7845b2..f31c917b 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -46,7 +46,6 @@ $lang = array( 'gains' => "Gains", 'login' => "[Login]", 'forum' => "Forum", - 'n_a' => "n/d", 'siteRep' => "Réputation", 'yourRepHistory'=> "Votre historique de réputation", 'aboutUs' => "À propos de Aowow", @@ -1490,6 +1489,11 @@ $lang = array( '_seeMore' => "[See more]", '_rankRange' => "Rang : %d - %d", '_showXmore' => "En afficher %d de plus", + + 'n_a' => "n/d", + 'normal' => "Standard", + 'special' => "Spécial", + 'currentArea' => '<current area>', 'discovered' => "Appris via une découverte", 'ppm' => "(%s déclenchements par minute)", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 8f1d8a5d..f212cf23 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -46,7 +46,6 @@ $lang = array( 'gains' => "Бонус", 'login' => "[Login]", 'forum' => "Форум", - 'n_a' => "нет", 'siteRep' => "Репутация", 'yourRepHistory'=> "История вашей репутации", 'aboutUs' => "О Aowow", @@ -1490,6 +1489,11 @@ $lang = array( '_seeMore' => "[See more]", '_rankRange' => "Ранг: %d - %d", '_showXmore' => "Показать на %d больше", + + 'n_a' => "нет", + 'normal' => "Обычный", + 'special' => "Особый", + 'currentArea' => '<current area>', 'discovered' => "Изучается путём освоения местности", 'ppm' => "(Срабатывает %s раз в минуту)", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 2cb39fff..a14fefb9 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -47,7 +47,6 @@ $lang = array( 'gains' => "获得", 'login' => "登录", 'forum' => "论坛", - 'n_a' => "n/a", 'siteRep' => "站点声望", 'yourRepHistory'=> "您的声望历史", 'aboutUs' => "关于我们 & 联系我们", @@ -1490,6 +1489,11 @@ $lang = array( '_seeMore' => "[See more]", '_rankRange' => "排名: %d - %d", '_showXmore' => "[Show %d More]", + + 'n_a' => "n/a", + 'normal' => "普通", + 'special' => "特殊", + 'currentArea' => '<当前区域>', 'discovered' => "通过发现学习", 'ppm' => "%s每分钟触发几率", diff --git a/pages/spell.php b/pages/spell.php index fb6e2ab9..77ebcdbe 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -24,7 +24,7 @@ class SpellPage extends GenericPage protected $rangeName = ''; protected $range = ''; protected $gcd = ''; - protected $gcdCat = ''; // todo (low): nyi; find out how this works [n/a; normal; ..] + protected $gcdCat = ''; protected $school = ''; protected $dispel = ''; protected $mechanic = ''; @@ -352,6 +352,18 @@ class SpellPage extends GenericPage $this->headIcons = [$this->subject->getField('iconString'), $this->subject->getField('stackAmount') ?: ($this->subject->getField('procCharges') > 1 ? $this->subject->getField('procCharges') : '')]; $this->redButtons = $redButtons; $this->infobox = $infobox; + $this->gcdCat = match((int)$this->subject->getField('startRecoveryCategory')) + { + 133 => Lang::spell('normal'), + 330, // Mounts + 1156, // Heart of the Phoenix + 1159, // Ignis Grab and Slag Pot + 1164, // Kessel Run Elek + 1173, // Birmingham Test Spells + 1178, // Stealth (Druid Cat, Rogue, Hunter Cat Pets) + Charge (Warrior) + 1244 => Lang::spell('special'), // Argent Tournament Vehicle Jousting Abilities + default => '' // n/a + }; // minRange exists.. prepend if ($_ = $this->subject->getField('rangeMinHostile')) diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index 7804e999..9f7b023c 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -88,23 +88,23 @@ endif; - duration ?: ''.Lang::main('n_a').'');?> + duration ?: ''.Lang::spell('n_a').'');?> - school ?: ''.Lang::main('n_a').'');?> + school ?: ''.Lang::spell('n_a').'');?> - mechanic ?:''.Lang::main('n_a').'');?> + mechanic ?:''.Lang::spell('n_a').'');?> - dispel ?: ''.Lang::main('n_a').'');?> + dispel ?: ''.Lang::spell('n_a').'');?> - gcdCat ?: ''.Lang::main('n_a').'');?> + gcdCat ?: ''.Lang::spell('n_a').'');?> @@ -123,7 +123,7 @@ endif; - cooldown ?: ''.Lang::main('n_a').'');?> + cooldown ?: ''.Lang::spell('n_a').'');?> '.Lang::spell('_gcd');?> From 5de9759b90f6cb645f25aa6e6ada697fa07c57d7 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 27 Jul 2025 00:16:58 +0200 Subject: [PATCH 621/957] DBType * extend functions * FilterFactory * test ::hasIcon() * test ::isRandomSearchable() --- includes/type.class.php | 82 +++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/includes/type.class.php b/includes/type.class.php index 0c7a2349..7644b594 100644 --- a/includes/type.class.php +++ b/includes/type.class.php @@ -82,8 +82,9 @@ abstract class Type public const FLAG_NONE = 0x0; public const FLAG_RANDOM_SEARCHABLE = 0x1; - /* public const FLAG_SEARCHABLE = 0x2 general search? */ + public const FLAG_FILTRABLE = 0x2; public const FLAG_DB_TYPE = 0x4; + public const FLAG_HAS_ICON = 0x8; public const IDX_LIST_OBJ = 0; public const IDX_FILE_STR = 1; @@ -91,34 +92,34 @@ abstract class Type public const IDX_FLAGS = 3; private static array $data = array( - self::NPC => [__NAMESPACE__ . '\CreatureList', 'npc', 'g_npcs', 0x5], - self::OBJECT => [__NAMESPACE__ . '\GameObjectList', 'object', 'g_objects', 0x5], - self::ITEM => [__NAMESPACE__ . '\ItemList', 'item', 'g_items', 0x5], - self::ITEMSET => [__NAMESPACE__ . '\ItemsetList', 'itemset', 'g_itemsets', 0x5], - self::QUEST => [__NAMESPACE__ . '\QuestList', 'quest', 'g_quests', 0x5], - self::SPELL => [__NAMESPACE__ . '\SpellList', 'spell', 'g_spells', 0x5], - self::ZONE => [__NAMESPACE__ . '\ZoneList', 'zone', 'g_gatheredzones', 0x5], - self::FACTION => [__NAMESPACE__ . '\FactionList', 'faction', 'g_factions', 0x5], - self::PET => [__NAMESPACE__ . '\PetList', 'pet', 'g_pets', 0x5], - self::ACHIEVEMENT => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x5], - self::TITLE => [__NAMESPACE__ . '\TitleList', 'title', 'g_titles', 0x5], - self::WORLDEVENT => [__NAMESPACE__ . '\WorldEventList', 'event', 'g_holidays', 0x5], - self::CHR_CLASS => [__NAMESPACE__ . '\CharClassList', 'class', 'g_classes', 0x5], - self::CHR_RACE => [__NAMESPACE__ . '\CharRaceList', 'race', 'g_races', 0x5], - self::SKILL => [__NAMESPACE__ . '\SkillList', 'skill', 'g_skills', 0x5], - self::STATISTIC => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x0], // alias for achievements; exists only for Markup - self::CURRENCY => [__NAMESPACE__ . '\CurrencyList', 'currency', 'g_gatheredcurrencies',0x5], - self::SOUND => [__NAMESPACE__ . '\SoundList', 'sound', 'g_sounds', 0x5], - self::ICON => [__NAMESPACE__ . '\IconList', 'icon', 'g_icons', 0x5], - self::GUIDE => [__NAMESPACE__ . '\GuideList', 'guide', '', 0x0], - self::PROFILE => [__NAMESPACE__ . '\ProfileList', '', '', 0x0], // x - not known in javascript - self::GUILD => [__NAMESPACE__ . '\GuildList', '', '', 0x0], // x - self::ARENA_TEAM => [__NAMESPACE__ . '\ArenaTeamList', '', '', 0x0], // x - self::USER => [__NAMESPACE__ . '\UserList', 'user', 'g_users', 0x0], // x - self::EMOTE => [__NAMESPACE__ . '\EmoteList', 'emote', 'g_emotes', 0x5], - self::ENCHANTMENT => [__NAMESPACE__ . '\EnchantmentList', 'enchantment', 'g_enchantments', 0x5], - self::AREATRIGGER => [__NAMESPACE__ . '\AreatriggerList', 'areatrigger', '', 0x4], - self::MAIL => [__NAMESPACE__ . '\MailList', 'mail', '', 0x5] + self::NPC => [__NAMESPACE__ . '\CreatureList', 'npc', 'g_npcs', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::OBJECT => [__NAMESPACE__ . '\GameObjectList', 'object', 'g_objects', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::ITEM => [__NAMESPACE__ . '\ItemList', 'item', 'g_items', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::ITEMSET => [__NAMESPACE__ . '\ItemsetList', 'itemset', 'g_itemsets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::QUEST => [__NAMESPACE__ . '\QuestList', 'quest', 'g_quests', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::SPELL => [__NAMESPACE__ . '\SpellList', 'spell', 'g_spells', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::ZONE => [__NAMESPACE__ . '\ZoneList', 'zone', 'g_gatheredzones', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::FACTION => [__NAMESPACE__ . '\FactionList', 'faction', 'g_factions', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::PET => [__NAMESPACE__ . '\PetList', 'pet', 'g_pets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::ACHIEVEMENT => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::TITLE => [__NAMESPACE__ . '\TitleList', 'title', 'g_titles', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::WORLDEVENT => [__NAMESPACE__ . '\WorldEventList', 'event', 'g_holidays', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::CHR_CLASS => [__NAMESPACE__ . '\CharClassList', 'class', 'g_classes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::CHR_RACE => [__NAMESPACE__ . '\CharRaceList', 'race', 'g_races', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::SKILL => [__NAMESPACE__ . '\SkillList', 'skill', 'g_skills', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::STATISTIC => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', self::FLAG_NONE], // alias for achievements; exists only for Markup + self::CURRENCY => [__NAMESPACE__ . '\CurrencyList', 'currency', 'g_gatheredcurrencies', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::SOUND => [__NAMESPACE__ . '\SoundList', 'sound', 'g_sounds', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::ICON => [__NAMESPACE__ . '\IconList', 'icon', 'g_icons', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::GUIDE => [__NAMESPACE__ . '\GuideList', 'guide', '', self::FLAG_NONE], + self::PROFILE => [__NAMESPACE__ . '\ProfileList', 'profile', '', self::FLAG_FILTRABLE], // x - not known in javascript + self::GUILD => [__NAMESPACE__ . '\GuildList', 'guild', '', self::FLAG_FILTRABLE], // x + self::ARENA_TEAM => [__NAMESPACE__ . '\ArenaTeamList', 'arena-team', '', self::FLAG_FILTRABLE], // x + self::USER => [__NAMESPACE__ . '\UserList', 'user', 'g_users', self::FLAG_NONE], // x + self::EMOTE => [__NAMESPACE__ . '\EmoteList', 'emote', 'g_emotes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::ENCHANTMENT => [__NAMESPACE__ . '\EnchantmentList', 'enchantment', 'g_enchantments', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::AREATRIGGER => [__NAMESPACE__ . '\AreatriggerList', 'areatrigger', '', self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::MAIL => [__NAMESPACE__ . '\MailList', 'mail', '', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE] ); @@ -134,6 +135,15 @@ abstract class Type return new (self::$data[$type][self::IDX_LIST_OBJ])($conditions); } + public static function newFilter(string $fileStr, array|string $data, array $opts = []) : ?Filter + { + $x = self::getFileStringsFor(self::FLAG_FILTRABLE); + if ($type = array_search($fileStr, $x)) + return new (self::$data[$type][self::IDX_LIST_OBJ].'Filter')($data, $opts); + + return null; + } + public static function validateIds(int $type, int|array $ids) : array { if (!self::exists($type)) @@ -145,6 +155,16 @@ abstract class Type return DB::Aowow()->selectCol('SELECT `id` FROM ?# WHERE `id` IN (?a)', self::$data[$type][self::IDX_LIST_OBJ]::$dataTable, (array)$ids); } + public static function hasIcon(int $type) : bool + { + return self::exists($type) && self::$data[$type][self::IDX_FLAGS] & self::FLAG_HAS_ICON; + } + + public static function isRandomSearchable(int $type) : bool + { + return self::exists($type) && self::$data[$type][self::IDX_FLAGS] & self::FLAG_RANDOM_SEARCHABLE; + } + public static function getFileString(int $type) : string { if (!self::exists($type)) @@ -186,9 +206,9 @@ abstract class Type return (self::$data[$type][self::IDX_LIST_OBJ])::$$attr ?? null; } - public static function exists(int $type) : bool + public static function exists(int $type) : ?int { - return !empty(self::$data[$type]); + return !empty(self::$data[$type]) ? $type : null; } public static function getIndexFrom(int $idx, string $match) : int From 58412e0491ad203615f5ad08a69378981f76c8ac Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 27 Jul 2025 02:44:31 +0200 Subject: [PATCH 622/957] Titles/Name * partially revert 398b93e9a734dd44a03379b557a716cfbdf25b36 * generic "name" is required by CommunityContent system --- includes/types/title.class.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/includes/types/title.class.php b/includes/types/title.class.php index f182d12e..5777ebde 100644 --- a/includes/types/title.class.php +++ b/includes/types/title.class.php @@ -46,14 +46,11 @@ class TitleList extends BaseType unset($_curTpl['moreTypeId']); unset($_curTpl['src3']); - // shorthand for more generic access - // i don't see it being used anywhere..? - /* + // shorthand for more generic access; required by CommunityContent to determine subject foreach (Locale::cases() as $loc) if ($loc->validate()) $_curTpl['name'] = new LocString($_curTpl, 'male', fn($x) => trim(str_replace('%s', '', $x))); // $_curTpl['name_loc'.$loc->value] = trim(str_replace('%s', '', $_curTpl['male_loc'.$loc->value])); - */ } } From bffdb9672eea0236735507b8a910ac45119b69c4 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 26 Jul 2025 23:47:41 +0200 Subject: [PATCH 623/957] Future/Frontend * create php classes, each mirroring a js object * each frontend class implements __toString and json_serialize and as such can be directly used by the template * also allows for sane object creation before js screams in agony * usage TBD --- includes/ajaxHandler/getdescription.class.php | 2 +- .../components/communitycontent.class.php | 6 +- .../frontend/announcement.class.php | 69 +++++ includes/components/frontend/book.class.php | 50 +++ .../components/frontend/iconelement.class.php | 159 ++++++++++ .../frontend/infoboxmarkup.class.php | 49 +++ .../components/frontend/listview.class.php | 174 +++++++++++ includes/components/frontend/markup.class.php | 291 ++++++++++++++++++ .../components/frontend/summary.class.php | 70 +++++ includes/components/frontend/tabs.class.php | 142 +++++++++ .../components/frontend/tooltip.class.php | 60 ++++ includes/components/markup.class.php | 150 --------- includes/kernel.php | 2 + includes/types/guide.class.php | 2 +- pages/genericPage.class.php | 4 +- pages/guide.php | 2 +- pages/home.php | 2 +- 17 files changed, 1075 insertions(+), 159 deletions(-) create mode 100644 includes/components/frontend/announcement.class.php create mode 100644 includes/components/frontend/book.class.php create mode 100644 includes/components/frontend/iconelement.class.php create mode 100644 includes/components/frontend/infoboxmarkup.class.php create mode 100644 includes/components/frontend/listview.class.php create mode 100644 includes/components/frontend/markup.class.php create mode 100644 includes/components/frontend/summary.class.php create mode 100644 includes/components/frontend/tabs.class.php create mode 100644 includes/components/frontend/tooltip.class.php delete mode 100644 includes/components/markup.class.php diff --git a/includes/ajaxHandler/getdescription.class.php b/includes/ajaxHandler/getdescription.class.php index 41802c13..cabf84d9 100644 --- a/includes/ajaxHandler/getdescription.class.php +++ b/includes/ajaxHandler/getdescription.class.php @@ -28,7 +28,7 @@ class AjaxGetdescription extends AjaxHandler if (!User::canWriteGuide()) return ''; - $desc = (new Markup($this->_post['description']))->stripTags(); + $desc = Markup::stripTags($this->_post['description']); return Lang::trimTextClean($desc, 120); } diff --git a/includes/components/communitycontent.class.php b/includes/components/communitycontent.class.php index 2fc8843f..3cd88627 100644 --- a/includes/components/communitycontent.class.php +++ b/includes/components/communitycontent.class.php @@ -198,7 +198,7 @@ class CommunityContent foreach ($results as $r) { - (new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals); + Markup::parseTags($r['body'], self::$jsGlobals); $reply = array( 'commentid' => $commentId, @@ -359,7 +359,7 @@ class CommunityContent $i = 0; foreach ($results as $r) { - (new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals); + Markup::parseTags($r['body'], self::$jsGlobals); self::$jsGlobals[Type::USER][$r['userId']] = $r['userId']; @@ -384,7 +384,7 @@ class CommunityContent $c['responseroles'] = $r['responseRoles']; $c['responseuser'] = $r['responseUser']; - (new Markup($r['responseBody']))->parseGlobalsFromText(self::$jsGlobals); + Markup::parseTags($r['responseBody'], self::$jsGlobals); } if ($r['editCount']) // lastEdit diff --git a/includes/components/frontend/announcement.class.php b/includes/components/frontend/announcement.class.php new file mode 100644 index 00000000..8823428f --- /dev/null +++ b/includes/components/frontend/announcement.class.php @@ -0,0 +1,69 @@ +editable = true; + + if ($this->mode != self::MODE_PAGE_TOP && $this->mode != self::MODE_CONTENT_TOP) + $this->mode = self::MODE_PAGE_TOP; + + if ($status != self::STATUS_DISABLED && $status != self::STATUS_ENABLED && $status != self::STATUS_DELETED) + $this->status = self::STATUS_DELETED; + else + $this->status = $status; + } + + public function jsonSerialize() : array + { + $json = array( + 'parent' => 'announcement-' . abs($this->id), + 'id' => $this->editable ? -$this->id : $this->id, + 'mode' => $this->mode, + 'status' => $this->status, + 'name' => $this->name, + 'text' => (string)$this->text // force LocString to naive string for display + ); + + if ($this->style) + $json['style'] = $this->style; + + return $json; + } + + public function __toString() : string + { + if ($this->status == self::STATUS_DELETED) + return ''; + + return "new Announcement(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/book.class.php b/includes/components/frontend/book.class.php new file mode 100644 index 00000000..b1ef62e7 --- /dev/null +++ b/includes/components/frontend/book.class.php @@ -0,0 +1,50 @@ +parent) + trigger_error(self::class.'::__construct - initialized without parent element', E_USER_WARNING); + + if (!$this->pages) + trigger_error(self::class.'::__construct - initialized without content', E_USER_WARNING); + else + $this->pages = Util::parseHtmlText($this->pages); + } + + public function &iterate() : \Generator + { + reset($this->pages); + + foreach ($this->pages as $idx => &$page) + yield $idx => $page; + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + return "new Book(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/iconelement.class.php b/includes/components/frontend/iconelement.class.php new file mode 100644 index 00000000..a9fdac67 --- /dev/null +++ b/includes/components/frontend/iconelement.class.php @@ -0,0 +1,159 @@ +quality = 'q'.$quality; + else if ($quality !== null) + $this->quality = 'q'; + else + $this->quality = ''; + + if ($size < self::SIZE_SMALL || $size > self::SIZE_LARGE) + { + trigger_error('IconElement::__construct - invalid icon size '.$size.'. Normalied to 1 [small]', E_USER_WARNING); + $this->size = self::SIZE_SMALL; + } + else + $this->size = $size; + + if ($align && !in_array($align, ['left', 'right', 'center', 'justify'])) + { + trigger_error('IconElement::__construct - unset invalid align value "'.$align.'".', E_USER_WARNING); + $this->align = null; + } + else + $this->align = $align; + + if ($type && $typeId && !Type::validateIds($type, $typeId)) + { + $link = false; + trigger_error('IconElement::__construct - invalid typeId '.$typeId.' for '.Type::getFileString($type).'.', E_USER_WARNING); + } + else if (!$type || !$typeId) + $link = false; + + if ($link || $url) + $this->href = $url ?: '?'.Type::getFileString($this->type).'='.$this->typeId; + + // see Spell/Tools having icon container but no actual icon and having to be inline with other IconElements + $this->noIcon = !$typeId || !Type::hasIcon($type); + } + + public function renderContainer(int $lpad = 0, int &$iconIdxOffset = 0, bool $rowWrap = false) : string + { + if (!$this->noIcon) + $this->idx = ++$iconIdxOffset; + + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $td = $dom->createElement('td'); + $th = $dom->createElement('th'); + + if ($this->noIcon) // see Spell/Tools or AchievementCriteria having no actual icon, but placeholder + { + $ul = $dom->createElement('ul'); + $li = $dom->createElement('li'); + $var = $dom->createElement('var', ' '); + $li->appendChild($var); + $ul->appendChild($li); + $th->appendChild($ul); + } + else + { + $th->setAttribute('id', $this->element . $this->idx); + if ($this->align) + $th->setAttribute('align', $this->align); + } + + if ($this->href) + ($a = $dom->createElement('a', $this->text))->setAttribute('href', $this->href); + else + $a = $dom->createTextNode($this->text); + + if ($this->quality) + { + ($sp = $dom->createElement('span'))->setAttribute('class', $this->quality); + $sp->appendChild($a); + $td->appendChild($sp); + } + else + $td->appendChild($a); + + // extraText can be HTML, so import it as a fragment + if ($this->extraText) + { + $fragment = $dom->createDocumentFragment(); + $fragment->appendXML(' '.$this->extraText); + $td->appendChild($fragment); + } + // only for objectives list..? + if ($this->num && $this->size == self::SIZE_SMALL) + $td->appendChild($dom->createTextNode(' ('.$this->num.')')); + + if ($rowWrap) + { + $tr = $dom->createElement('tr'); + $tr->appendChild($th); + $tr->appendChild($td); + $dom->append($tr); + } + else + $dom->append($th, $td); + + return str_repeat(' ', $lpad) . $dom->saveHTML(); + } + + // $WH.ge('icontab-icon1').appendChild(g_spells.createIcon(40120, 1, '1-4', 0)); + + public function renderJS(int $lpad = 0) : string + { + if ($this->noIcon) + return ''; + + $params = [$this->typeId, $this->size]; + if ($this->num || $this->qty) + $params[] = is_numeric($this->num) ? $this->num : "'".$this->num."'"; + if ($this->qty) + $params[] = is_numeric($this->qty) ? $this->qty : "'".$this->qty."'"; + + return str_repeat(' ', $lpad) . sprintf(self::CREATE_ICON_TPL, $this->element, $this->idx, Type::getJSGlobalString($this->type), implode(', ', $params)); + } +} + +?> diff --git a/includes/components/frontend/infoboxmarkup.class.php b/includes/components/frontend/infoboxmarkup.class.php new file mode 100644 index 00000000..c88972b9 --- /dev/null +++ b/includes/components/frontend/infoboxmarkup.class.php @@ -0,0 +1,49 @@ += count($this->items)) + $this->items[] = $item; + else + array_splice($this->items, $pos, 0, $item); + } + + public function append(string $text) : self + { + if ($this->items && !$this->__text) + $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + + return parent::append($text); + } + + public function __toString() : string + { + if ($this->items && !$this->__text) + $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + + return parent::__toString(); + } + + public function getJsGlobals() : array + { + if ($this->items && !$this->__text) + $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + + return parent::getJsGlobals(); + } +} + +?> diff --git a/includes/components/frontend/listview.class.php b/includes/components/frontend/listview.class.php new file mode 100644 index 00000000..7180d6ba --- /dev/null +++ b/includes/components/frontend/listview.class.php @@ -0,0 +1,174 @@ + ['template' => 'achievement', 'id' => 'achievements', 'name' => '$LANG.tab_achievements' ], + 'areatrigger' => ['template' => 'areatrigger', 'id' => 'areatrigger', ], + 'calendar' => ['template' => 'holidaycal', 'id' => 'calendar', 'name' => '$LANG.tab_calendar' ], + 'class' => ['template' => 'classs', 'id' => 'classes', 'name' => '$LANG.tab_classes' ], + 'commentpreview' => ['template' => 'commentpreview', 'id' => 'comments', 'name' => '$LANG.tab_comments' ], + 'npc' => ['template' => 'npc', 'id' => 'npcs', 'name' => '$LANG.tab_npcs' ], + 'currency' => ['template' => 'currency', 'id' => 'currencies', 'name' => '$LANG.tab_currencies' ], + 'emote' => ['template' => 'emote', 'id' => 'emotes', ], + 'enchantment' => ['template' => 'enchantment', 'id' => 'enchantments', ], + 'event' => ['template' => 'holiday', 'id' => 'holidays', 'name' => '$LANG.tab_holidays' ], + 'faction' => ['template' => 'faction', 'id' => 'factions', 'name' => '$LANG.tab_factions' ], + 'genericmodel' => ['template' => 'genericmodel', 'id' => 'same-model-as', 'name' => '$LANG.tab_samemodelas' ], + 'icongallery' => ['template' => 'icongallery', 'id' => 'icons', ], + 'item' => ['template' => 'item', 'id' => 'items', 'name' => '$LANG.tab_items' ], + 'itemset' => ['template' => 'itemset', 'id' => 'itemsets', 'name' => '$LANG.tab_itemsets' ], + 'mail' => ['template' => 'mail', 'id' => 'mails', ], + 'model' => ['template' => 'model', 'id' => 'gallery', 'name' => '$LANG.tab_gallery' ], + 'object' => ['template' => 'object', 'id' => 'objects', 'name' => '$LANG.tab_objects' ], + 'pet' => ['template' => 'pet', 'id' => 'hunter-pets', 'name' => '$LANG.tab_pets' ], + 'profile' => ['template' => 'profile', 'id' => 'profiles', 'name' => '$LANG.tab_profiles' ], + 'quest' => ['template' => 'quest', 'id' => 'quests', 'name' => '$LANG.tab_quests' ], + 'race' => ['template' => 'race', 'id' => 'races', 'name' => '$LANG.tab_races' ], + 'replypreview' => ['template' => 'replypreview', 'id' => 'comment-replies', 'name' => '$LANG.tab_commentreplies'], + 'reputationhistory' => ['template' => 'reputationhistory', 'id' => 'reputation', 'name' => '$LANG.tab_reputation' ], + 'screenshot' => ['template' => 'screenshot', 'id' => 'screenshots', 'name' => '$LANG.tab_screenshots' ], + 'skill' => ['template' => 'skill', 'id' => 'skills', 'name' => '$LANG.tab_skills' ], + 'sound' => ['template' => 'sound', 'id' => 'sounds', 'name' => '$LANG.types[19][2]' ], + 'spell' => ['template' => 'spell', 'id' => 'spells', 'name' => '$LANG.tab_spells' ], + 'title' => ['template' => 'title', 'id' => 'titles', 'name' => '$LANG.tab_titles' ], + 'topusers' => ['template' => 'topusers', 'id' => 'topusers', 'name' => '$LANG.topusers' ], + 'video' => ['template' => 'video', 'id' => 'videos', 'name' => '$LANG.tab_videos' ], + 'zone' => ['template' => 'zone', 'id' => 'zones', 'name' => '$LANG.tab_zones' ], + 'guide' => ['template' => 'guide', 'id' => 'guides', ] + ); + + private string $id = ''; + private ?string $name = null; + private ?array $data = null; // js:array of object + private ?string $tabs = null; // js:Object; instance of "Tabs" + private ?string $parent = 'lv-generic'; // HTMLNode.id; can be null but is pretty much always 'lv-generic' + private ?string $template = null; + private ?int $mode = null; // js:int; defaults to MODE_DEFAULT + private ?string $note = null; // text in top band + + private ?int $poundable = null; // 0 (no); 1 (always); 2 (yes, w/o sorting); defaults to 1 + private ?int $searchable = null; // js:bool; defaults to FALSE + private ?int $filtrable = null; // js:bool; defaults to FALSE + private ?int $sortable = null; // js:bool; defaults to FALSE + private ?int $searchDelay = null; // in ms; defalts to 333 + private ?int $clickable = null; // js:bool; defaults to TRUE + private ?int $hideBands = null; // js:int; 1:top, 2:bottom, 3:both; + private ?int $hideNav = null; // js:int; 1:top, 2:bottom, 3:both; + private ?int $hideHeader = null; // js:bool + private ?int $hideCount = null; // js:bool + private ?int $debug = null; // js:bool + private ?int $_truncated = null; // js:bool; adds predefined note to top band, because there was too much data to display + private ?int $_errors = null; // js:bool; adds predefined note to top band, because there was an error + private ?int $_petTalents = null; // js:bool; applies modifier for talent levels + + private ?int $nItemsPerPage = null; // js:int; defaults to 50 + private ?int $_totalCount = null; // js:int; used by loot and comments + private ?array $clip = null; // js:array of int {w:, h:} + private ?string $customPound = null; + private ?string $genericlinktype = null; // sometimes set when expecting to display model + private ?array $_upgradeIds = null; // js:array of int (itemIds) + + private null|array|string $extraCols = null; // js:callable or js:array of object + private null|array|string $visibleCols = null; // js:callable or js:array of string + private null|array|string $hiddenCols = null; // js:callable or js:array of string + private null|array|string $sort = null; // js:callable or js:array of colIndizes + + private ?string $onBeforeCreate = null; // js:callable + private ?string $onAfterCreate = null; // js:callable + private ?string $onNoData = null; // js:callable + private ?string $computeDataFunc = null; // js:callable + private ?string $onSearchSubmit = null; // js:callable + private ?string $createNote = null; // js:callable + private ?string $createCbControls = null; // js:callable + private ?string $customFilter = null; // js:callable + private ?string $getItemLink = null; // js:callable + private ?array $sortOptions = null; // js:array of object {id:, name:, hidden:, type:"text", sortFunc:} + + private string $__addIn = ''; + + public function __construct(array $opts, string $template = '', string $addIn = '') + { + if ($template && isset(self::TEMPLATES[$template])) + foreach (self::TEMPLATES[$template] as $k => $v) + $this->$k = $v; + + foreach ($opts as $k => $v) + { + if (property_exists($this, $k)) + { + // reindex arrays to force json_encode to treat them as arrays + if (is_array($v)) // in_array($k, ['data', 'extraCols', 'visibleCols', 'hiddenCols', 'sort', 'sortOptions'])) + $v = array_values($v); + $this->$k = $v; + } + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + + if ($addIn && !Template\PageTemplate::test('listviews/', $addIn.'.tpl')) + trigger_error('Nonexistent Listview addin requested: template/listviews/'.$addIn.'.tpl', E_USER_ERROR); + else if ($addIn) + $this->__addIn = 'template/listviews/'.$addIn.'.tpl'; + } + + public function &iterate() : \Generator + { + reset($this->data); + + foreach ($this->data as $idx => &$row) + yield $idx => $row; + } + + public function getTemplate() : string + { + return $this->template; + } + + public function setTabs(string $tabVar) : void + { + if ($tabVar[0] !== '$') // expects a jsVar, which we denote with a prefixed $ + $tabVar = '$' . $tabVar; + + $this->tabs = $tabVar; + } + + public function setError() : void + { + $this->_errors = 1; + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && substr($prop, 0, 2) != '__') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + if ($this->__addIn) + include($this->__addIn); + + return "new Listview(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/markup.class.php b/includes/components/frontend/markup.class.php new file mode 100644 index 00000000..4c07861a --- /dev/null +++ b/includes/components/frontend/markup.class.php @@ -0,0 +1,291 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + + $this->__text = $text; + + if ($parent) + $this->__parent = $parent; + } + + public function getJsGlobals() : array + { + return $this->_parseTags(); + } + + public function getParent() : string + { + return $this->__parent; + } + + + /***********************/ + /* Markup tag handling */ + /***********************/ + + private function _parseTags(array &$jsg = []) : array + { + return self::parseTags($this->__text, $jsg); + } + + public static function parseTags(string $text, array &$jsg = []) : array + { + $jsGlobals = []; + + if (preg_match_all(self::DB_TAG_PATTERN, $text, $matches, PREG_SET_ORDER)) + { + foreach ($matches as $match) + { + if ($match[1] == 'statistic') + $match[1] = 'achievement'; + else if ($match[1] == 'icondb') + $match[1] = 'icon'; + + if ($match[1] == 'money') + { + if (stripos($match[0], 'items')) + { + if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i+=2) + $jsGlobals[Type::ITEM][$sm[$i]] = $sm[$i]; + } + } + + if (stripos($match[0], 'currency')) + { + if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i+=2) + $jsGlobals[Type::CURRENCY][$sm[$i]] = $sm[$i]; + } + } + } + else if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1])) + $jsGlobals[$type][$match[2]] = $match[2]; + } + } + + Util::mergeJsGlobals($jsg, $jsGlobals); + + return $jsGlobals; + } + + private function _stripTags(array $jsgData = []) : string + { + return self::stripTags($this->__text, $jsgData); + } + + public static function stripTags(string $text, array $jsgData = []) : string + { + // replace DB Tags + $text = preg_replace_callback(self::DB_TAG_PATTERN, function ($match) use ($jsgData) { + if ($match[1] == 'statistic') + $match[1] = 'achievement'; + else if ($match[1] == 'icondb') + $match[1] = 'icon'; + else if ($match[1] == 'money') + { + $moneys = []; + if (stripos($match[0], 'items')) + { + if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i += 2) + { + if (!empty($jsgData[Type::ITEM][1][$sm[$i]])) + $moneys[] = $jsgData[Type::ITEM][1][$sm[$i]]['name']; + else + $moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i]; + } + } + } + + if (stripos($match[0], 'currency')) + { + if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i += 2) + { + if (!empty($jsgData[Type::CURRENCY][1][$sm[$i]])) + $moneys[] = $jsgData[Type::CURRENCY][1][$sm[$i]]['name']; + else + $moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i]; + } + } + } + + return Lang::concat($moneys); + } + if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1])) + { + if (!empty($jsgData[$type][1][$match[2]])) + return $jsgData[$type][1][$match[2]]['name']; + else + return Util::ucFirst(Lang::game($match[1])).' #'.$match[2]; + } + + trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match)); + return ''; + }, $text); + + // replace line endings + $text = str_replace('[br]', "\n", $text); + + // strip other Tags + $stripped = ''; + $inTag = false; + for ($i = 0; $i < strlen($text); $i++) + { + if ($text[$i] == '[' && (!$i || $text[$i - 1] != '\\')) + $inTag = true; + if (!$inTag) + $stripped .= $text[$i]; + if ($inTag && $text[$i] == ']' && (!$i || $text[$i - 1] != '\\')) + $inTag = false; + } + + return $stripped; + } + + + /*********************/ + /* String Operations */ + /*********************/ + + public function append(string $text) : self + { + $this->__text .= $text; + return $this; + } + + public function prepend(string $text) : self + { + $this->__text = $text . $this->__text; + return $this; + } + + public function apply(\Closure $fn) : void + { + $this->__text = $fn($this->__text); + } + + public function replace(string $middle, int $offset = 0, ?int $len = null) : self + { + // y no mb_substr_replace >:( + $start = $end = ''; + + if ($offset < 0) + $offset = mb_strlen($this->__text) + $offset; + + $start = mb_substr($this->__text, 0, $offset); + + if (!is_null($len) && $len >= 0) + $end = mb_substr($this->__text, $offset + $len); + else if (!is_null($len) && $len < 0) + $end = mb_substr($this->__text, $offset + mb_strlen($this->__text) + $len); + + $this->__text = $start . $middle . $end; + return $this; + } + + private function cleanText() : string + { + // break script-tags, unify newlines + $val = preg_replace(['/script\s*\>/i', "/\r\n/", "/\r/"], ['script>', "\n", "\n"], $this->__text); + + return strtr(Util::jsEscape($val), ['script>' => 'scr"+"ipt>']); + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + if ($this->jsonSerialize()) + return 'Markup.printHtml("'.$this->cleanText().'", "'.$this->__parent.'", '.Util::toJSON($this).");\n"; + + return 'Markup.printHtml("'.$this->cleanText().'", "'.$this->__parent."\");\n"; + } +} + +?> diff --git a/includes/components/frontend/summary.class.php b/includes/components/frontend/summary.class.php new file mode 100644 index 00000000..62ad87e5 --- /dev/null +++ b/includes/components/frontend/summary.class.php @@ -0,0 +1,70 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + + if (!$this->template) + trigger_error(self::class.'::__construct - initialized without template', E_USER_WARNING); + if (!$this->id) + trigger_error(self::class.'::__construct - initialized without HTMLNode#id to reference', E_USER_WARNING); + } + + public function &iterate() : \Generator + { + reset($this->groups); + + foreach ($this->groups as $idx => &$group) + yield $idx => $group; + } + + public function addGroup(array $group) : void + { + $this->groups[] = $group; + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + return "new Summary(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/tabs.class.php b/includes/components/frontend/tabs.class.php new file mode 100644 index 00000000..2563d7a9 --- /dev/null +++ b/includes/components/frontend/tabs.class.php @@ -0,0 +1,142 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + } + + public function &iterate() : \Generator + { + reset($this->__tabs); + + foreach ($this->__tabs as $idx => &$tab) + yield $idx => $tab; + } + + public function addListviewTab(Listview $lv) : void + { + $this->__tabs[] = $lv; + } + + public function addDataTab(string $id, string $name, string $data) : void + { + $this->__tabs[] = ['id' => $id, 'name' => $name, 'data' => $data]; + $this->__forceTabs = true; // otherwise a single DataTab could not be accessed + } + + public function getDataContainer() : \Generator + { + foreach ($this->__tabs as $tab) + if (is_array($tab)) + yield '

'; + } + + public function getFlush() : string + { + if ($this->isTabbed()) + return $this->__tabVar.".flush();"; + + return ''; + } + + public function isTabbed() : bool + { + return count($this->__tabs) > 1 || $this->__forceTabs; + } + + + /***********************/ + /* enable deep cloning */ + /***********************/ + + public function __clone() + { + foreach ($this->__tabs as $idx => $tab) + { + if (is_array($tab)) + continue; + + $this->__tabs[$idx] = clone $tab; + } + } + + + /******************/ + /* make countable */ + /******************/ + + public function count() : int + { + return count($this->__tabs); + } + + + /************************/ + /* make Tabs stringable */ + /************************/ + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + $result = ''; + + if ($this->isTabbed()) + $result .= "var ".$this->__tabVar." = new Tabs(".Util::toJSON($this).");\n"; + + foreach ($this->__tabs as $tab) + { + if (is_array($tab)) + { + $n = $tab['name'][0] == '$' ? substr($tab['name'], 1) : "'".$tab['name']."'"; + $result .= $this->__tabVar.".add(".$n.", { id: '".$tab['id']."' });\n"; + } + else + { + if ($this->isTabbed()) + $tab->setTabs($this->__tabVar); + + $result .= $tab; // Listview::__toString here + } + } + + return $result . "\n"; + } +} + +?> diff --git a/includes/components/frontend/tooltip.class.php b/includes/components/frontend/tooltip.class.php new file mode 100644 index 00000000..b9c51634 --- /dev/null +++ b/includes/components/frontend/tooltip.class.php @@ -0,0 +1,60 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + } + + public function jsonSerialize() : array + { + $out = []; + + $locString = Lang::getLocale()->json(); + + foreach ($this as $k => $v) + { + if ($v === null || $k[0] == '_') + continue; + + if ($k == 'icon') + $out[$k] = rawurldecode($v); + else if ($k == 'quality' || $k == 'map' || $k == 'daily') + $out[$k] = $v; + else + $out[$k . '_' . $locString] = $v; + } + + return $out; + } + + public function __toString() : string + { + return sprintf($this->__powerTpl, Util::toJSON($this->__subject, JSON_AOWOW_POWER), Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n"; + } +} + +?> diff --git a/includes/components/markup.class.php b/includes/components/markup.class.php deleted file mode 100644 index ff8b652a..00000000 --- a/includes/components/markup.class.php +++ /dev/null @@ -1,150 +0,0 @@ -text = $text; - } - - public function parseGlobalsFromText(&$jsg = []) - { - if (preg_match_all(self::$dbTagPattern, $this->text, $matches, PREG_SET_ORDER)) - { - foreach ($matches as $match) - { - if ($match[1] == 'statistic') - $match[1] = 'achievement'; - else if ($match[1] == 'icondb') - $match[1] = 'icon'; - - if ($match[1] == 'money') - { - if (stripos($match[0], 'items')) - { - if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i+=2) - $this->jsGlobals[Type::ITEM][$sm[$i]] = $sm[$i]; - } - } - - if (stripos($match[0], 'currency')) - { - if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i+=2) - $this->jsGlobals[Type::CURRENCY][$sm[$i]] = $sm[$i]; - } - } - } - else if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1])) - $this->jsGlobals[$type][$match[2]] = $match[2]; - } - } - - Util::mergeJsGlobals($jsg, $this->jsGlobals); - - return $this->jsGlobals; - } - - public function stripTags($globals = []) - { - // since this is an article the db-tags should already be parsed - $text = preg_replace_callback(self::$dbTagPattern, function ($match) use ($globals) { - if ($match[1] == 'statistic') - $match[1] = 'achievement'; - else if ($match[1] == 'icondb') - $match[1] = 'icon'; - else if ($match[1] == 'money') - { - $moneys = []; - if (stripos($match[0], 'items')) - { - if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i += 2) - { - if (!empty($globals[Type::ITEM][1][$sm[$i]])) - $moneys[] = $globals[Type::ITEM][1][$sm[$i]]['name']; - else - $moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i]; - } - } - } - - if (stripos($match[0], 'currency')) - { - if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i += 2) - { - if (!empty($globals[Type::CURRENCY][1][$sm[$i]])) - $moneys[] = $globals[Type::CURRENCY][1][$sm[$i]]['name']; - else - $moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i]; - } - } - } - - return Lang::concat($moneys); - } - if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1])) - { - if (!empty($globals[$type][1][$match[2]])) - return $globals[$type][1][$match[2]]['name']; - else - return Util::ucFirst(Lang::game($match[1])).' #'.$match[2]; - } - - trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match)); - return ''; - }, $this->text); - - $text = str_replace('[br]', "\n", $text); - $stripped = ''; - - $inTag = false; - for ($i = 0; $i < strlen($text); $i++) - { - if ($text[$i] == '[') - $inTag = true; - if (!$inTag) - $stripped .= $text[$i]; - if ($text[$i] == ']') - $inTag = false; - } - - return $stripped; - } - - public function fromHtml() - { - } - - public function toHtml() - { - } -} - -?> diff --git a/includes/kernel.php b/includes/kernel.php index 8da5f9b6..b9e050a1 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -65,6 +65,8 @@ spl_autoload_register(function ($class) if (file_exists('includes/components/'.strtolower($class).'.class.php')) require_once 'includes/components/'.strtolower($class).'.class.php'; + else if (file_exists('includes/components/frontend/'.strtolower($class).'.class.php')) + require_once 'includes/components/frontend/'.strtolower($class).'.class.php'; }); // TC systems in components diff --git a/includes/types/guide.class.php b/includes/types/guide.class.php index 61f314df..dc980582 100644 --- a/includes/types/guide.class.php +++ b/includes/types/guide.class.php @@ -72,7 +72,7 @@ class GuideList extends BaseType $this->article[$a['rev']] = $a['article']; if ($this->article[$a['rev']]) { - (new Markup($this->article[$a['rev']]))->parseGlobalsFromText($this->jsGlobals); + Markup::parseTags($this->article[$a['rev']], $this->jsGlobals); return $this->article[$a['rev']]; } else diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index 66e92447..1919a90f 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -559,9 +559,9 @@ class GenericPage if ($article) { if ($article['article']) - (new Markup($article['article']))->parseGlobalsFromText($this->jsgBuffer); + Markup::parseTags($article['article'], $this->jsgBuffer); if ($article['quickInfo']) - (new Markup($article['quickInfo']))->parseGlobalsFromText($this->jsgBuffer); + Markup::parseTags($article['quickInfo'], $this->jsgBuffer); $this->article = array( 'text' => Util::jsEscape(Util::defStatic($article['article'])), diff --git a/pages/guide.php b/pages/guide.php index e002809c..f48d0374 100644 --- a/pages/guide.php +++ b/pages/guide.php @@ -195,7 +195,7 @@ class GuidePage extends GenericPage 'specId' => $this->_post['specId'], 'title' => $this->_post['title'], 'name' => $this->_post['name'], - 'description' => $this->_post['description'] ?: Lang::trimTextClean((new Markup($this->_post['body']))->stripTags(), 120), + 'description' => $this->_post['description'] ?: Lang::trimTextClean(Markup::stripTags($this->_post['body']), 120), 'locale' => $this->_post['locale'], 'roles' => User::$groups, 'status' => GUIDE_STATUS_DRAFT diff --git a/pages/home.php b/pages/home.php index a1714d66..366a94ea 100644 --- a/pages/home.php +++ b/pages/home.php @@ -39,7 +39,7 @@ class HomePage extends GenericPage $this->featuredBox['text'] = Util::localizedString($this->featuredBox, 'text', true); - if ($_ = (new Markup($this->featuredBox['text']))->parseGlobalsFromText()) + if ($_ = Markup::parseTags($this->featuredBox['text'])) $this->extendGlobalData($_); if (empty($this->featuredBox['boxBG'])) From 086760b9b1eb0011fb1bf93b0ef408743c8033bf Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 27 Jul 2025 01:34:22 +0200 Subject: [PATCH 624/957] User/Cleanup * the great unfuckening of user and displayName * `login` is purely used as login with AUTH_MODE_SELF * `email` may now also be used to log in (if the system knows it) * `username` is purely used for display around the site, and lookups from web context * both must exist because of external logins a) that may be not unique b) you may not want to share with the rest of the world * todo: implement rename ( because of b) ) --- includes/ajaxHandler/admin.class.php | 2 +- includes/ajaxHandler/edit.class.php | 2 +- includes/ajaxHandler/profile.class.php | 28 +++--- .../components/communitycontent.class.php | 54 +++++------ includes/types/guide.class.php | 4 +- includes/types/user.class.php | 12 +-- includes/user.class.php | 93 +++++++++++-------- pages/account.php | 20 ++-- pages/admin.php | 2 +- pages/guide.php | 2 +- pages/more.php | 28 +++--- pages/screenshot.php | 2 +- pages/user.php | 8 +- setup/db_structure.sql | 11 ++- setup/tools/clisetup/account.us.php | 8 +- setup/updates/1753572319_01.sql | 12 +++ template/bricks/headerMenu.tpl.php | 2 +- template/pages/acc-dashboard.tpl.php | 6 +- template/pages/user.tpl.php | 2 +- 19 files changed, 158 insertions(+), 140 deletions(-) create mode 100644 setup/updates/1753572319_01.sql diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php index 7992ead2..78af780d 100644 --- a/includes/ajaxHandler/admin.class.php +++ b/includes/ajaxHandler/admin.class.php @@ -123,7 +123,7 @@ class AjaxAdmin extends AjaxHandler if ($this->_get['type'] && $this->_get['type'] && $this->_get['typeid'] && $this->_get['typeid']) $res = CommunityContent::getScreenshotsForManager($this->_get['type'], $this->_get['typeid']); else if ($this->_get['user']) - if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', $this->_get['user'])) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) $res = CommunityContent::getScreenshotsForManager(0, 0, $uId); return 'ssm_screenshotData = '.Util::toJSON($res); diff --git a/includes/ajaxHandler/edit.class.php b/includes/ajaxHandler/edit.class.php index fc5c753d..1275b7c4 100644 --- a/includes/ajaxHandler/edit.class.php +++ b/includes/ajaxHandler/edit.class.php @@ -41,7 +41,7 @@ class AjaxEdit extends AjaxHandler $targetPath = 'static/uploads/guide/images/'; $tmpPath = 'static/uploads/temp/'; - $tmpFile = User::$displayName.'-'.Type::GUIDE.'-0-'.Util::createHash(16); + $tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16); $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); $result = $uploader->handleUpload($tmpPath, $tmpFile, true); diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php index 382b37e2..86e668a9 100644 --- a/includes/ajaxHandler/profile.class.php +++ b/includes/ajaxHandler/profile.class.php @@ -112,7 +112,7 @@ class AjaxProfile extends AjaxHandler $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 user = ?', $this->_get['user']))) + 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; @@ -120,12 +120,12 @@ class AjaxProfile extends AjaxHandler } if ($this->undo) - DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE accountId = ?d AND profileId IN (?a)', $uid, $this->_get['id']); + 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)) + 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 { @@ -152,7 +152,7 @@ class AjaxProfile extends AjaxHandler $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 user = ?', $this->_get['user']))) + 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; @@ -160,10 +160,10 @@ class AjaxProfile extends AjaxHandler } // 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); + 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); + 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 @@ -182,7 +182,7 @@ class AjaxProfile extends AjaxHandler $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 user = ?', $this->_get['user']))) + 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; @@ -191,13 +191,13 @@ class AjaxProfile extends AjaxHandler 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); + 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); + 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); } } @@ -323,7 +323,7 @@ class AjaxProfile extends AjaxHandler // todo (med): detail check this post-data $cuProfile = array( 'user' => User::$id, - // 'userName' => User::$displayName, + // 'userName' => User::$username, 'name' => $this->_post['name'], 'level' => $this->_post['level'], 'class' => $this->_post['class'], @@ -557,7 +557,7 @@ class AjaxProfile extends AjaxHandler $profile['sourcename'] = $pBase['sourceName']; $profile['description'] = $pBase['description']; $profile['user'] = $pBase['user']; - $profile['username'] = DB::Aowow()->selectCell('SELECT displayName FROM ?_account WHERE id = ?d', $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 :( @@ -572,7 +572,7 @@ class AjaxProfile extends AjaxHandler if ($_ = DB::Aowow()->selectCol('SELECT accountId FROM ?_account_profiles WHERE profileId = ?d', $pBase['id'])) $profile['bookmarks'] = $_; - // arena teams - [size(2|3|5) => DisplayName]; DisplayName gets urlized to use as link + // 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; diff --git a/includes/components/communitycontent.class.php b/includes/components/communitycontent.class.php index 3cd88627..8655d4f9 100644 --- a/includes/components/communitycontent.class.php +++ b/includes/components/communitycontent.class.php @@ -31,13 +31,13 @@ class CommunityContent private static string $coQuery = 'SELECT c.*, - a1.`displayName` AS `user`, - a2.`displayName` AS `editUser`, - a3.`displayName` AS `deleteUser`, - a4.`displayName` AS `responseUser`, - IFNULL(SUM(ur.`value`), 0) AS `rating`, - SUM(IF(ur.`userId` > 0 AND ur.`userId` = ?d, ur.`value`, 0)) AS `userRating`, - IF(r.`id` IS NULL, 0, 1) AS `userReported` + a1.`username` AS "user", + a2.`username` AS "editUser", + a3.`username` AS "deleteUser", + a4.`username` AS "responseUser", + IFNULL(SUM(ur.`value`), 0) AS "rating", + SUM(IF(ur.`userId` > 0 AND ur.`userId` = ?d, ur.`value`, 0)) AS "userRating", + IF(r.`id` IS NULL, 0, 1) AS "userReported" FROM ?_comments c JOIN ?_account a1 ON c.`userId` = a1.`id` LEFT JOIN ?_account a2 ON c.`editUserId` = a2.`id` @@ -51,7 +51,7 @@ class CommunityContent ORDER BY c.`date` ASC'; private static string $ssQuery = - 'SELECT s.`id` AS ARRAY_KEY, s.`id`, a.`displayName` AS `user`, s.`date`, s.`width`, s.`height`, s.`caption`, IF(s.`status` & ?d, 1, 0) AS "sticky", s.`type`, s.`typeId` + 'SELECT s.`id` AS ARRAY_KEY, s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`caption`, IF(s.`status` & ?d, 1, 0) AS "sticky", s.`type`, s.`typeId` FROM ?_screenshots s LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id` WHERE { s.`userIdOwner` = ?d AND }{ s.`type` = ? AND }{ s.`typeId` = ? AND } s.`status` & ?d AND (s.`status` & ?d) = 0 @@ -59,7 +59,7 @@ class CommunityContent { LIMIT ?d }'; private static string $viQuery = - 'SELECT v.`id` AS ARRAY_KEY, v.`id`, a.`displayName` AS `user`, v.`date`, v.`videoId`, v.`caption`, IF(v.`status` & ?d, 1, 0) AS "sticky", v.`type`, v.`typeId` + 'SELECT v.`id` AS ARRAY_KEY, v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`caption`, IF(v.`status` & ?d, 1, 0) AS "sticky", v.`type`, v.`typeId` FROM ?_videos v LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id` WHERE { v.`userIdOwner` = ?d AND }{ v.`type` = ? AND }{ v.`typeId` = ? AND } v.`status` & ?d AND (v.`status` & ?d) = 0 @@ -68,14 +68,14 @@ class CommunityContent private static string $previewQuery = 'SELECT c.`id`, - c.`body` AS `preview`, + c.`body` AS "preview", c.`date`, - c.`replyTo` AS `commentid`, - IF(c.`flags` & ?d, 1, 0) AS `deleted`, - IF(c.`type` <> 0, c.`type`, c2.`type`) AS `type`, - IF(c.`typeId` <> 0, c.`typeId`, c2.`typeId`) AS `typeId`, - IFNULL(SUM(ur.`value`), 0) AS `rating`, - a.`displayName` AS `user` + c.`replyTo` AS "commentid", + IF(c.`flags` & ?d, 1, 0) AS "deleted", + IF(c.`type` <> 0, c.`type`, c2.`type`) AS "type", + IF(c.`typeId` <> 0, c.`typeId`, c2.`typeId`) AS "typeId", + IFNULL(SUM(ur.`value`), 0) AS "rating", + a.`username` AS "user" FROM ?_comments c JOIN ?_account a ON c.`userId` = a.`id` LEFT JOIN ?_user_ratings ur ON ur.`entry` = c.`id` AND ur.`userId` <> 0 AND ur.`type` = 1 @@ -228,14 +228,14 @@ class CommunityContent public static function getScreenshotsForManager($type, $typeId, $userId = 0) { - $screenshots = DB::Aowow()->select(' - SELECT s.id, a.displayName AS user, s.date, s.width, s.height, s.type, s.typeId, s.caption, s.status, s.status AS "flags" + $screenshots = DB::Aowow()->select( + 'SELECT s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`type`, s.`typeId`, s.`caption`, s.`status`, s.`status` AS "flags" FROM ?_screenshots s - LEFT JOIN ?_account a ON s.userIdOwner = a.id + LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id` WHERE - { s.type = ?d} - { AND s.typeId = ?d} - { s.userIdOwner = ?d} + { s.`type` = ?d} + { AND s.`typeId` = ?d} + { s.`userIdOwner` = ?d} LIMIT 100', $userId ? DBSIMPLE_SKIP : $type, $userId ? DBSIMPLE_SKIP : $typeId, @@ -300,11 +300,11 @@ class CommunityContent { // i GUESS .. ss_getALL ? everything : pending $nFound = 0; - $pages = DB::Aowow()->select(' - SELECT s.`type`, s.`typeId`, count(1) AS "count", MIN(s.`date`) AS "date" - FROM ?_screenshots s - {WHERE (s.status & ?d) = 0} - GROUP BY s.`type`, s.`typeId`', + $pages = DB::Aowow()->select( + 'SELECT s.`type`, s.`typeId`, COUNT(1) AS "count", MIN(s.`date`) AS "date" + FROM ?_screenshots s + { WHERE (s.`status` & ?d) = 0 } + GROUP BY s.`type`, s.`typeId`', $all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED | CC_FLAG_DELETED ); diff --git a/includes/types/guide.class.php b/includes/types/guide.class.php index dc980582..4fc7942a 100644 --- a/includes/types/guide.class.php +++ b/includes/types/guide.class.php @@ -29,8 +29,8 @@ class GuideList extends BaseType protected $queryBase = 'SELECT g.*, g.id AS ARRAY_KEY FROM ?_guides g'; protected $queryOpts = array( 'g' => [['a', 'c'], 'g' => 'g.`id`'], - 'a' => ['j' => ['?_account a ON a.id = g.userId', true], 's' => ', IFNULL(a.displayName, "") AS author'], - 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS `comments`'] + 'a' => ['j' => ['?_account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], + 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'] ); public function __construct(array $conditions = [], array $miscData = []) diff --git a/includes/types/user.class.php b/includes/types/user.class.php index f547ef8d..0373c5bb 100644 --- a/includes/types/user.class.php +++ b/includes/types/user.class.php @@ -18,7 +18,7 @@ class UserList extends BaseType protected $queryBase = 'SELECT *, a.id AS ARRAY_KEY FROM ?_account a'; protected $queryOpts = array( 'a' => [['r']], - 'r' => ['j' => ['?_account_reputation r ON r.userId = a.id', true], 's' => ', IFNULL(SUM(r.amount), 0) AS reputation', 'g' => 'a.id'] + 'r' => ['j' => ['?_account_reputation r ON r.`userId` = a.`id`', true], 's' => ', IFNULL(SUM(r.`amount`), 0) AS "reputation"', 'g' => 'a.`id`'] ); public function getListviewData() { } @@ -29,7 +29,7 @@ class UserList extends BaseType foreach ($this->iterate() as $__) { - $data[$this->curTpl['displayName']] = array( + $data[$this->curTpl['username']] = array( 'border' => 0, // border around avatar (rarityColors) 'roles' => $this->curTpl['userGroups'], 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), @@ -40,14 +40,14 @@ class UserList extends BaseType 'reputation' => $this->curTpl['reputation'] ); - // custom titles (only ssen on user page..?) + // custom titles (only seen on user page..?) if ($_ = $this->curTpl['title']) - $data[$this->curTpl['displayName']]['title'] = $_; + $data[$this->curTpl['username']]['title'] = $_; if ($_ = $this->curTpl['avatar']) { - $data[$this->curTpl['displayName']]['avatar'] = is_numeric($_) ? 2 : 1; - $data[$this->curTpl['displayName']]['avatarmore'] = $_; + $data[$this->curTpl['username']]['avatar'] = is_numeric($_) ? 2 : 1; + $data[$this->curTpl['username']]['avatarmore'] = $_; } // more optional data diff --git a/includes/user.class.php b/includes/user.class.php index 739c8a9c..48a8c14b 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -8,23 +8,22 @@ if (!defined('AOWOW_REVISION')) class User { - public static int $id = 0; - public static string $displayName = ''; - public static int $banStatus = 0x0; // see ACC_BAN_* defines - public static int $groups = 0x0; - public static int $perms = 0; - public static string $avatar = 'inv_misc_questionmark'; - public static int $dailyVotes = 0; - public static $ip = null; + public static int $id = 0; + public static string $username = ''; + public static int $banStatus = 0x0; // see ACC_BAN_* defines + public static int $groups = 0x0; + public static int $perms = 0; + public static ?string $email = null; + public static int $dailyVotes = 0; + public static ?string $ip = null; + public static Locale $preferedLoc; - private static int $reputation = 0; - private static string $dataKey = ''; - private static bool $expires = false; - private static string $passHash = ''; - private static int $excludeGroups = 1; - - public static Locale $preferedLoc; - private static ?LocalProfileList $profiles = null; + private static int $reputation = 0; + private static string $dataKey = ''; + private static bool $expires = false; + private static string $passHash = ''; + private static int $excludeGroups = 1; + private static ?LocalProfileList $profiles = null; public static function init() { @@ -64,7 +63,7 @@ class User return false; $uData = DB::Aowow()->SelectRow( - 'SELECT a.`id`, a.`passHash`, a.`displayName`, a.`locale`, a.`userGroups`, a.`userPerms`, a.`allowExpire`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`avatar`, a.`dailyVotes`, a.`excludeGroups` + 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, a.`allowExpire`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` @@ -87,7 +86,7 @@ class User } self::$id = intVal($uData['id']); - self::$displayName = $uData['displayName']; + self::$username = $uData['username']; self::$passHash = $uData['passHash']; self::$expires = (bool)$uData['allowExpire']; self::$reputation = $uData['reputation']; @@ -103,9 +102,6 @@ class User self::$profiles = (new LocalProfileList($conditions)); - if ($uData['avatar']) - self::$avatar = $uData['avatar']; - // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) // - conscutive visits @@ -190,10 +186,10 @@ class User $_SESSION['locale'] = self::$preferedLoc; // keep locale $_SESSION['dataKey'] = self::$dataKey; // keep dataKey - self::$id = 0; - self::$displayName = ''; - self::$perms = 0; - self::$groups = U_GROUP_NONE; + self::$id = 0; + self::$username = ''; + self::$perms = 0; + self::$groups = U_GROUP_NONE; } @@ -201,16 +197,16 @@ class User /* auth mechanisms */ /*******************/ - public static function authenticate(string $name, string $password) : int + public static function authenticate(string $login, string $password) : int { $userId = 0; $hash = ''; $result = match (Cfg::get('ACC_AUTH_MODE')) { - AUTH_MODE_SELF => self::authSelf($name, $password, $userId, $hash), - AUTH_MODE_REALM => self::authRealm($name, $password, $userId, $hash), - AUTH_MODE_EXTERNAL => self::authExtern($name, $password, $userId, $hash), + AUTH_MODE_SELF => self::authSelf($login, $password, $userId, $hash), + AUTH_MODE_REALM => self::authRealm($login, $password, $userId, $hash), + AUTH_MODE_EXTERNAL => self::authExtern($login, $password, $userId, $hash), default => AUTH_INTERNAL_ERR }; @@ -224,7 +220,7 @@ class User return $result; } - private static function authSelf(string $name, string $password, int &$userId, string &$hash) : int + private static function authSelf(string $nameOrEmail, string $password, int &$userId, string &$hash) : int { if (!self::$ip) return AUTH_INTERNAL_ERR; @@ -239,13 +235,16 @@ class User if ($ipBan && $ipBan['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) return AUTH_IPBANNED; + $email = filter_var($nameOrEmail, FILTER_VALIDATE_EMAIL); + $query = DB::Aowow()->SelectRow( 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() - WHERE a.`user` = ? + WHERE { a.`email` = ? } { a.`login` = ? } GROUP BY a.`id`', - $name + $email ?: DBSIMPLE_SKIP, + !$email ? $nameOrEmail : DBSIMPLE_SKIP ); if (!$query) @@ -290,7 +289,7 @@ class User return AUTH_OK; } - private static function authExtern(string $name, string $password, int &$userId, string &$hash) : int + private static function authExtern(string $nameOrEmail, string $password, int &$userId, string &$hash) : int { if (!file_exists('config/extAuth.php')) { @@ -308,11 +307,15 @@ class User $extGroup = -1; $extId = 0; - $result = \extAuth($name, $password, $extId, $extGroup); + $result = \extAuth($nameOrEmail, $password, $extId, $extGroup); + + // assert we don't have an email passed back from extAuth + if (filter_var($nameOrEmail, FILTER_VALIDATE_EMAIL)) + return AUTH_WRONGUSER; if ($result == AUTH_OK && $extId) { - if ($_ = self::checkOrCreateInDB($extId, $name, $extGroup)) + if ($_ = self::checkOrCreateInDB($extId, $nameOrEmail, $extGroup)) $userId = $_; else return AUTH_INTERNAL_ERR; @@ -331,10 +334,9 @@ class User return $_; } - $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (`extId`, `user`, `passHash`, `displayName`, `email`, `joinDate`, `allowExpire`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (?d, ?, "", ?, "", UNIX_TIMESTAMP(), 0, ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)', + $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (`extId`, `login`, `passHash`, `username`, `email`, `joinDate`, `allowExpire`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (?d, "", "", ?, "", UNIX_TIMESTAMP(), 0, ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)', $extId, $name, - Util::ucFirst($name), $_SERVER["REMOTE_ADDR"] ?? '', self::$preferedLoc->value, ACC_STATUS_OK, @@ -555,7 +557,7 @@ class User { $gUser = array( 'id' => self::$id, - 'name' => self::$displayName, + 'name' => self::$username, 'roles' => self::$groups, 'permissions' => self::$perms, 'cookies' => [] @@ -573,11 +575,18 @@ class User $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE'); $gUser['characters'] = self::getCharacters(); $gUser['excludegroups'] = self::$excludeGroups; - $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; has property premiumborder (NYI) if (Cfg::get('DEBUG') && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_TESTER)) $gUser['debug'] = true; // csv id-list output option on listviews; todo - set on per user basis + if (self::getPremiumBorder()) + $gUser['settings'] = ['premiumborder' => 1]; + else + $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied + + if (self::isPremium()) + $gUser['premium'] = 1; + if ($_ = self::getProfilerExclusions()) $gUser = array_merge($gUser, $_); @@ -716,6 +725,12 @@ class User return $data; } + + // not sure what to set .. user selected? + public static function getPremiumBorder() : bool + { + return self::isInGroup(U_GROUP_PREMIUM); + } } ?> diff --git a/pages/account.php b/pages/account.php index 4d856ef1..a1391810 100644 --- a/pages/account.php +++ b/pages/account.php @@ -142,7 +142,7 @@ class AccountPage extends GenericPage header('Location: '.$this->getNext(true), true, 302); } } - else if ($this->_get['token'] && ($_ = DB::Aowow()->selectCell('SELECT user FROM ?_account WHERE status IN (?a) AND token = ? AND statusTimer > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token']))) + else if ($this->_get['token'] && ($_ = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE `status` IN (?a) AND `token` = ? AND `statusTimer` > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token']))) $this->user = $_; break; @@ -203,8 +203,8 @@ class AccountPage extends GenericPage if (!User::isLoggedIn()) $this->forwardToSignIn('account'); - $user = DB::Aowow()->selectRow('SELECT * FROM ?_account WHERE id = ?d', User::$id); - $bans = DB::Aowow()->select('SELECT ab.*, a.displayName, ab.id AS ARRAY_KEY FROM ?_account_banned ab LEFT JOIN ?_account a ON a.id = ab.staffId WHERE ab.userId = ?d', User::$id); + $user = DB::Aowow()->selectRow('SELECT * FROM ?_account WHERE `id` = ?d', User::$id); + $bans = DB::Aowow()->select('SELECT ab.*, a.`username`, ab.`id` AS ARRAY_KEY FROM ?_account_banned ab LEFT JOIN ?_account a ON a.`id` = ab.`staffId` WHERE ab.`userId` = ?d', User::$id); /***********/ /* Infobox */ @@ -236,7 +236,7 @@ class AccountPage extends GenericPage continue; $this->banned = array( - 'by' => [$b['staffId'], $b['displayName']], + 'by' => [$b['staffId'], $b['username']], 'end' => $b['end'], 'reason' => $b['reason'] ); @@ -365,7 +365,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup return Lang::main('intError'); // reset account status, update expiration - DB::Aowow()->query('UPDATE ?_account SET prevIP = IF(curIp = ?, prevIP, curIP), curIP = IF(curIp = ?, curIP, ?), allowExpire = ?d, status = IF(status = ?d, status, 0), statusTimer = IF(status = ?d, statusTimer, 0), token = IF(status = ?d, token, "") WHERE user = ?', + DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `allowExpire` = ?d, `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE LOWER(`username`) = LOWER(?)', User::$ip, User::$ip, User::$ip, $this->_post['remember_me'] != 'yes', ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW, @@ -419,23 +419,23 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup return Lang::main('intError'); // limit account creation - $ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 1 AND ip = ?', User::$ip); + $ip = DB::Aowow()->selectRow('SELECT `ip`, `count`, `unbanDate` FROM ?_account_bannedips WHERE `type` = 1 AND `ip` = ?', User::$ip); if ($ip && $ip['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ip['unbanDate'] >= time()) { - DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip); + DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip); return sprintf(Lang::account('signupExceeded'), Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)); } // username taken - if ($_ = DB::Aowow()->SelectCell('SELECT user FROM ?_account WHERE (user = ? OR email = ?) AND (status <> ?d OR (status = ?d AND statusTimer > UNIX_TIMESTAMP()))', $this->_post['username'], $this->_post['email'], ACC_STATUS_NEW, ACC_STATUS_NEW)) + if ($_ = DB::Aowow()->SelectCell('SELECT `username` FROM ?_account WHERE (`username` = ? OR `email` = ?) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $this->_post['username'], $this->_post['email'], ACC_STATUS_NEW, ACC_STATUS_NEW)) return $_ == $this->_post['username'] ? Lang::account('nameInUse') : Lang::account('mailInUse'); // create.. $token = Util::createHash(); - $ok = DB::Aowow()->query('REPLACE INTO ?_account (user, passHash, displayName, email, joindate, curIP, allowExpire, locale, userGroups, status, statusTimer, token) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', + $ok = DB::Aowow()->query('REPLACE INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `allowExpire`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', $this->_post['username'], User::hashCrypt($this->_post['password']), - Util::ucFirst($this->_post['username']), + $this->_post['username'], $this->_post['email'], User::$ip, $this->_post['remember_me'] != 'yes', diff --git a/pages/admin.php b/pages/admin.php index 3e665095..e6627efc 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -224,7 +224,7 @@ class AdminPage extends GenericPage { if (mb_strlen($this->_get['user']) >= 3) { - if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', ucFirst($this->_get['user']))) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) { $ssData = CommunityContent::getScreenshotsForManager(0, 0, $uId); $nMatches = count($ssData); diff --git a/pages/guide.php b/pages/guide.php index f48d0374..a371694f 100644 --- a/pages/guide.php +++ b/pages/guide.php @@ -378,7 +378,7 @@ class GuidePage extends GenericPage $buff = ''; @@ -100,25 +92,14 @@ if ($this->reputation): endforeach; endif; -if (isset($this->smartAI)): -?> -
- +$this->brick('markup', ['markup' => $this->smartAI]); -
-

brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/npcs.tpl.php b/template/pages/npcs.tpl.php index cb732ed6..1ba1cd19 100644 --- a/template/pages/npcs.tpl.php +++ b/template/pages/npcs.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,67 +13,62 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [4]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); ?> - -
+
+
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

h1; ?>

+
-
+
petFamPanel): ?>
-
+
- + - + '; + + if (!$rows) + continue; + + $this->lvTabs->addDataTab(Profiler::urlize($catName), $catName, '
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
 />  /> />  />
 /> - /> /> - /> - - +
         - > - - - + + +
@@ -84,7 +80,7 @@ endforeach;
- /> /> + /> />
@@ -98,7 +94,7 @@ endforeach;
-brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From 253cbcb4d982f3f9b4b3a6d99262e3a5251ce5be Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 15:36:25 +0200 Subject: [PATCH 673/957] Template/Update (Part 30) * convert dbtype 'object' --- {pages => endpoints/object}/object.php | 343 ++++++++++++------------- endpoints/object/object_power.php | 50 ++++ endpoints/objects/objects.php | 109 ++++++++ localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- localization/locale_zhcn.php | 2 +- pages/objects.php | 90 ------- template/pages/object.tpl.php | 30 +-- template/pages/objects.tpl.php | 30 ++- 12 files changed, 366 insertions(+), 298 deletions(-) rename {pages => endpoints/object}/object.php (54%) create mode 100644 endpoints/object/object_power.php create mode 100644 endpoints/objects/objects.php delete mode 100644 pages/objects.php diff --git a/pages/object.php b/endpoints/object/object.php similarity index 54% rename from pages/object.php rename to endpoints/object/object.php index 762a47aa..203fa6a0 100644 --- a/pages/object.php +++ b/endpoints/object/object.php @@ -6,57 +6,62 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 5: Object g_initPath() -// tabId 0: Database g_initHeader() -class ObjectPage extends GenericPage +class ObjectBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $pageText = []; - protected $relBoss = null; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $type = Type::OBJECT; - protected $typeId = 0; - protected $tpl = 'object'; - protected $path = [0, 5]; - protected $tabId = 0; - protected $mode = CACHE_TYPE_PAGE; - protected $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; + protected string $template = 'object'; + protected string $pageName = 'object'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 5]; - protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; + protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - private $powerTpl = '$WowheadPower.registerObject(%d, %d, %s);'; + public int $type = Type::OBJECT; + public int $typeId = 0; + public ?Book $book = null; + public ?array $relBoss = null; - public function __construct($pageCall, $id) + private GameObjectList $subject; + + public function __construct(string $id) { - parent::__construct($pageCall, $id); + parent::__construct($id); - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - $this->typeId = intVal($id); + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + protected function generate() : void + { $this->subject = new GameObjectList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('object'), Lang::gameObject('notFound')); + $this->generateNotFound(Lang::game('object'), Lang::gameObject('notFound')); - $this->name = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML); - } + $this->h1 = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML); - protected function generatePath() - { - $this->path[] = $this->subject->getField('typeCat'); - } + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('typeCat'); + + + /**************/ + /* Page Title */ + /**************/ - protected function generateTitle() - { array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('object'))); - } - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); /***********/ /* Infobox */ @@ -65,48 +70,48 @@ class ObjectPage extends GenericPage $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // Event (ignore events, where the object only gets removed) - if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.eventEntry FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.eventEntry = geg.eventEntry AND g.guid = geg.guid AND g.id = ?d', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.`eventEntry` = geg.`eventEntry` AND g.`guid` = geg.`guid` AND g.`id` = ?d', $this->typeId)) { $this->extendGlobalIds(Type::WORLDEVENT, ...$_); $ev = []; foreach ($_ as $i => $e) $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; - $infobox[] = Util::ucFirst(Lang::game('eventShort')).Lang::main('colon').implode(',', $ev); + $infobox[] = Lang::game('eventShort', [implode(',', $ev)]); } // Faction - if ($_ = DB::Aowow()->selectCell('SELECT factionId FROM ?_factiontemplate WHERE id = ?d', $this->subject->getField('faction'))) + if ($_ = DB::Aowow()->selectCell('SELECT `factionId` FROM ?_factiontemplate WHERE `id` = ?d', $this->subject->getField('faction'))) { $this->extendGlobalIds(Type::FACTION, $_); $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$_.']'; } // Reaction - $_ = function ($r) + $color = fn (int $r) : string => match($r) { - if ($r == 1) return 2; // q2 green - if ($r == -1) return 10; // q10 red - return; // q yellow + 1 => 'q2', // q2 green + -1 => 'q10', // q10 red + default => 'q' // q yellow }; - $infobox[] = Lang::npc('react').Lang::main('colon').'[color=q'.$_($this->subject->getField('A')).']A[/color] [color=q'.$_($this->subject->getField('H')).']H[/color]'; + $infobox[] = Lang::npc('react', ['[color='.$color($this->subject->getField('A')).']A[/color] [color='.$color($this->subject->getField('H')).']H[/color]']); // reqSkill + difficulty switch ($this->subject->getField('typeCat')) { - case -3: // Herbalism - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 2).' ('.$this->subject->getField('reqSkill').')'); + case -3: // Herbalism + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 2).' ('.$this->subject->getField('reqSkill').')']); $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_HERBALISM, $this->subject->getField('reqSkill'))); break; - case -4: // Mining - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 3).' ('.$this->subject->getField('reqSkill').')'); + case -4: // Mining + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 3).' ('.$this->subject->getField('reqSkill').')']); $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_MINING, $this->subject->getField('reqSkill'))); break; - case -5: // Lockpicking - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 1).' ('.$this->subject->getField('reqSkill').')'); + case -5: // Lockpicking + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 1).' ('.$this->subject->getField('reqSkill').')']); $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_LOCKPICKING, $this->subject->getField('reqSkill'))); break; - default: // requires key .. maybe + default: // requires key .. maybe { $locks = Lang::getLocks($this->subject->getField('lockId'), $ids, true, Lang::FMT_MARKUP); $l = []; @@ -119,7 +124,7 @@ class ObjectPage extends GenericPage if ($idx > 0) $l[] = Lang::gameObject('key').Lang::main('colon').$str; else if ($idx < 0) - $l[] = sprintf(Lang::game('requires'), $str); + $l[] = Lang::game('requires', [$str]); } if ($l) @@ -138,114 +143,113 @@ class ObjectPage extends GenericPage // SpellFocus if ($_ = $this->subject->getField('spellFocusId')) - if ($sfo = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE id = ?d', $_)) + if ($sfo = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE `id` = ?d', $_)) $infobox[] = '[tooltip name=focus]'.Lang::gameObject('focusDesc').'[/tooltip][span class=tip tooltip=focus]'.Lang::gameObject('focus').Lang::main('colon').Util::localizedString($sfo, 'name').'[/span]'; // lootinfo: [min, max, restock] - if (($_ = $this->subject->getField('lootStack')) && $_[0]) + if ($this->subject->getField('lootStack')) { - $buff = Lang::spell('spellModOp', 4).Lang::main('colon').$_[0]; - if ($_[0] < $_[1]) - $buff .= Lang::game('valueDelim').$_[1]; + [$min, $max, $restock] = $this->subject->getField('lootStack'); + $buff = Lang::spell('spellModOp', 4).Lang::main('colon').$min; + if ($min < $max) + $buff .= Lang::game('valueDelim').$max; // since Veins don't have charges anymore, the timer is questionable - $infobox[] = $_[2] > 1 ? '[tooltip name=restock]'.sprintf(Lang::gameObject('restock'), Util::formatTime($_[2] * 1000)).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; + $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [Util::formatTime($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; } // meeting stone [minLevel, maxLevel, zone] - if ($this->subject->getField('type') == OBJECT_MEETINGSTONE) + if ($this->subject->getField('type') == OBJECT_MEETINGSTONE && $this->subject->getField('mStone')) { - if ($_ = $this->subject->getField('mStone')) - { - $this->extendGlobalIds(Type::ZONE, $_[2]); - $m = Lang::game('meetingStone').Lang::main('colon').'[zone='.$_[2].']'; + [$minLevel, $maxLevel, $zone] = $this->subject->getField('mStone'); - $l = $_[0]; - if ($_[0] > 1 && $_[1] > $_[0]) - $l .= Lang::game('valueDelim').min($_[1], MAX_LEVEL); + $this->extendGlobalIds(Type::ZONE, $zone); + $m = Lang::game('meetingStone').'[zone='.$zone.']'; - $infobox[] = $l ? '[tooltip name=meetingstone]'.sprintf(Lang::game('reqLevel'), $l).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; - } + $l = $minLevel; + if ($minLevel > 1 && $maxLevel > $minLevel) + $l .= Lang::game('valueDelim').min($maxLevel, MAX_LEVEL); + + $infobox[] = $l ? '[tooltip name=meetingstone]'.Lang::game('reqLevel', [$l]).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; } - // capture area [minPlayer, maxPlayer, minTime, maxTime, radius] - if ($this->subject->getField('type') == OBJECT_CAPTURE_POINT) + // capture area + if ($this->subject->getField('type') == OBJECT_CAPTURE_POINT && $this->subject->getField('capture')) { - if ($_ = $this->subject->getField('capture')) - { - $buff = Lang::gameObject('capturePoint'); + [$minPlayer, $maxPlayer, $minTime, $maxTime, $radius] = $this->subject->getField('capture'); - if ($_[2] > 1 || $_[0]) - $buff .= Lang::main('colon').'[ul]'; + $buff = Lang::gameObject('capturePoint'); - if ($_[2] > 1) - $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($_[3] > $_[2] ? Util::FormatTime($_[3] * 1000, true).' - ' : null).Util::FormatTime($_[2] * 1000, true).'[/li]'; + if ($minTime > 1 || $minPlayer || $radius) + $buff .= Lang::main('colon').'[ul]'; - if ($_[1]) - $buff .= '[li]'.Lang::main('players').Lang::main('colon').$_[0].($_[1] > $_[0] ? ' - '.$_[1] : null).'[/li]'; + if ($minTime > 1) + $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($maxTime > $minTime ? Util::FormatTime($maxTime * 1000, true).' - ' : '').Util::FormatTime($minTime * 1000, true).'[/li]'; - if ($_[4]) - $buff .= '[li]'.sprintf(Lang::spell('range'), $_[4]).'[/li]'; + if ($minPlayer) + $buff .= '[li]'.Lang::main('players').Lang::main('colon').$minPlayer.($maxPlayer > $minPlayer ? ' - '.$maxPlayer : '').'[/li]'; - if ($_[2] > 1 || $_[0]) - $buff .= '[/ul]'; - } + if ($radius) + $buff .= '[li]'.Lang::spell('range', [$radius]).'[/li]'; + + if ($minTime > 1 || $minPlayer || $radius) + $buff .= '[/ul]'; $infobox[] = $buff; } // AI if (User::isInGroup(U_GROUP_EMPLOYEE)) - { if ($_ = $this->subject->getField('ScriptOrAI')) - { - if ($_ == 'SmartGameObjectAI') - $infobox[] = 'AI'.Lang::main('colon').$_; - else - $infobox[] = 'Script'.Lang::main('colon').$_; - } - } + $infobox[] = ($_ == 'SmartGameObjectAI' ? 'AI' : 'Script').Lang::main('colon').$_; + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); /****************/ /* Main Content */ /****************/ - // pageText - if ($this->pageText = Game::getBook($this->subject->getField('pageTextId'))) + // pageText / book + if ($this->book = Game::getBook($this->subject->getField('pageTextId'))) $this->addScript( [SC_JS_FILE, 'js/Book.js'], [SC_CSS_FILE, 'css/Book.css'] ); // get spawns and path - $map = null; if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns, 'foundIn' => Lang::gameObject('foundIn')]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::gameObject('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); } // todo (low): consider pooled spawns - $relBoss = null; if ($ll = DB::Aowow()->selectRow('SELECT * FROM ?_loot_link WHERE `objectId` = ?d ORDER BY `priority` DESC LIMIT 1', $this->typeId)) { // group encounter if ($ll['encounterId']) - $relBoss = [$ll['npcId'], Lang::profiler('encounterNames', $ll['encounterId'])]; + $this->relBoss = [$ll['npcId'], Lang::profiler('encounterNames', $ll['encounterId'])]; // difficulty dummy else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_creature WHERE `difficultyEntry1` = ?d OR `difficultyEntry2` = ?d OR `difficultyEntry3` = ?d', $ll['npcId'], $ll['npcId'], $ll['npcId'])) - $relBoss = [$c['id'], Util::localizedString($c, 'name')]; + $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; // base creature else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_creature WHERE `id` = ?d', $ll['npcId'])) - $relBoss = [$c['id'], Util::localizedString($c, 'name')]; + $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; } - // smart AI + // Smart AI $sai = null; if ($this->subject->getField('ScriptOrAI') == 'SmartGameObjectAI') { @@ -263,15 +267,14 @@ class ObjectPage extends GenericPage } if ($sai->prepare()) + { $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } else trigger_error('Gameobject has AIName set in template but no SmartAI defined.'); } - $this->map = $map; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->relBoss = $relBoss; - $this->smartAI = $sai ? $sai->getMarkup() : null; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], @@ -283,12 +286,22 @@ class ObjectPage extends GenericPage /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: summoned by + $summonEffects = array( + SPELL_EFFECT_TRANS_DOOR, + SPELL_EFFECT_SUMMON_OBJECT_WILD, + SPELL_EFFECT_SUMMON_OBJECT_SLOT1, + SPELL_EFFECT_SUMMON_OBJECT_SLOT2, + SPELL_EFFECT_SUMMON_OBJECT_SLOT3, + SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + ); $conditions = array( 'OR', - ['AND', ['effect1Id', [50, 76, 104, 105, 106, 107]], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', [50, 76, 104, 105, 106, 107]], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', [50, 76, 104, 105, 106, 107]], ['effect3MiscValue', $this->typeId]] + ['AND', ['effect1Id', $summonEffects], ['effect1MiscValue', $this->typeId]], + ['AND', ['effect2Id', $summonEffects], ['effect2MiscValue', $this->typeId]], + ['AND', ['effect3Id', $summonEffects], ['effect3MiscValue', $this->typeId]] ); $summons = new SpellList($conditions); @@ -296,11 +309,11 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($summons->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($summons->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $summons->getListviewData(), 'id' => 'summoned-by', 'name' => '$LANG.tab_summonedby' - )]; + ), SpellList::$brickFile)); } // tab: related spells @@ -315,13 +328,13 @@ class ObjectPage extends GenericPage foreach ($data as $relId => $d) $data[$relId]['trigger'] = array_search($relId, $_); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($data), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, 'id' => 'spells', 'name' => '$LANG.tab_spells', 'hiddenCols' => ['skill'], 'extraCols' => ["\$Listview.funcBox.createSimpleCol('trigger', 'Condition', '10%', 'trigger')"] - )]; + ), SpellList::$brickFile)); } } @@ -331,11 +344,11 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($acvs->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $acvs->getListviewData(), 'id' => 'criteria-of', 'name' => '$LANG.tab_criteriaof' - )]; + ), AchievementList::$brickFile)); } // tab: starts quest @@ -345,30 +358,29 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($startEnd->getJSGlobals()); $lvData = $startEnd->getListviewData(); - $_ = [[], []]; + $start = $end = []; foreach ($startEnd->iterate() as $id => $__) { - $m = $startEnd->getField('method'); - if ($m & 0x1) - $_[0][] = $lvData[$id]; - if ($m & 0x2) - $_[1][] = $lvData[$id]; + if ($startEnd->getField('method') & 0x1) + $start[] = $lvData[$id]; + if ($startEnd->getField('method') & 0x2) + $end[] = $lvData[$id]; } - if ($_[0]) - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($_[0]), + if ($start) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $start, 'name' => '$LANG.tab_starts', 'id' => 'starts' - )]; + ), QuestList::$brickFile)); - if ($_[1]) - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($_[1]), + if ($end) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $end, 'name' => '$LANG.tab_ends', 'id' => 'ends' - )]; + ), QuestList::$brickFile)); } // tab: related quests @@ -379,11 +391,11 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($relQuest->getJSGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($relQuest->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $relQuest->getListviewData(), 'name' => '$LANG.tab_quests', 'id' => 'quests' - )]; + ), QuestList::$brickFile)); } } @@ -402,24 +414,20 @@ class ObjectPage extends GenericPage foreach ($hiddenCols as $k => $str) { - if ($k == 1 && array_filter(array_column($lootResult, $str), function ($x) { return $x != SIDE_BOTH; })) + if ($k == 1 && array_filter(array_column($lootResult, $str), fn ($x) => $x != SIDE_BOTH)) unset($hiddenCols[$k]); - else if ($k != 1 && array_column($lootResult, $str)) + else if ($k != 1 && !array_filter(array_column($lootResult, $str))) unset($hiddenCols[$k]); } - $tabData = array( - 'data' => array_values($lootResult), - 'id' => 'contains', - 'name' => '$LANG.tab_contains', - 'sort' => ['-percent', 'name'], - 'extraCols' => array_unique($extraCols) - ); - - if ($hiddenCols) - $tabData['hiddenCols'] = array_values($hiddenCols); - - $this->lvTabs[] = [ItemList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lootResult, + 'id' => 'contains', + 'name' => '$LANG.tab_contains', + 'sort' => ['-percent', 'name'], + 'extraCols' => array_unique($extraCols), + 'hiddenCols' => $hiddenCols ?: null + ), ItemList::$brickFile)); } } @@ -430,7 +438,7 @@ class ObjectPage extends GenericPage if (!$focusSpells->error) { $tabData = array( - 'data' => array_values($focusSpells->getListviewData()), + 'data' => $focusSpells->getListviewData(), 'name' => Lang::gameObject('focus'), 'id' => 'focus-for' ); @@ -440,11 +448,11 @@ class ObjectPage extends GenericPage // create note if search limit was exceeded if ($focusSpells->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) { - $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); $tabData['_truncated'] = 1; } - $this->lvTabs[] = [SpellList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); } } @@ -454,25 +462,27 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($trigger->getJSGlobals()); - $this->lvTabs[] = [GameObjectList::$brickFile, array( - 'data' => array_values($trigger->getListviewData()), + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $trigger->getListviewData(), 'name' => Lang::gameObject('triggeredBy'), 'id' => 'triggerd-by', 'note' => sprintf(Util::$filterResultString, '?objects=6') - )]; + ), GameObjectList::$brickFile)); } - // tab: Same model as .. whats the fucking point..? + // tab: Same model as $sameModel = new GameObjectList(array(['displayId', $this->subject->getField('displayId')], ['id', $this->typeId, '!'])); if (!$sameModel->error) { $this->extendGlobalData($sameModel->getJSGlobals()); - $this->lvTabs[] = [GameObjectList::$brickFile, array( - 'data' => array_values($sameModel->getListviewData()), + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sameModel->getListviewData(), 'name' => '$LANG.tab_samemodelas', 'id' => 'same-model-as' - )]; + ), GameObjectList::$brickFile)); } // tab: condition-for @@ -481,21 +491,10 @@ class ObjectPage extends GenericPage if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) { $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - } - - protected function generateTooltip() - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW); - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - $power->map = $this->subject->getSpawns(SPAWNINFO_SHORT); + $this->lvTabs->addDataTab(...$tab); } - return sprintf($this->powerTpl, $this->typeId, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); + parent::generate(); } } diff --git a/endpoints/object/object_power.php b/endpoints/object/object_power.php new file mode 100644 index 00000000..bfd73a60 --- /dev/null +++ b/endpoints/object/object_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $object = new GameObjectList(array(['id', $this->typeId])); + if ($object->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => Lang::unescapeUISequences($object->getField('name', true), Lang::FMT_RAW), + 'tooltip' => $object->renderTooltip(), + 'map' => $object->getSpawns(SPAWNINFO_SHORT) + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/objects/objects.php b/endpoints/objects/objects.php new file mode 100644 index 00000000..e08e5782 --- /dev/null +++ b/endpoints/objects/objects.php @@ -0,0 +1,109 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [-2, -3, -4, -5, -6, 0, 3, 6, 9, 25]; + + public bool $petFamPanel = false; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new GameObjectListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('objects')); + + $conditions = []; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->filter->evalCriteria(); + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + if ($this->category) + $conditions[] = ['typeCat', (int)$this->category[0]]; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::gameObject('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $tabData = ['data' => []]; + $objects = new GameObjectList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + if (!$objects->error) + { + $tabData['data'] = $objects->getListviewData(); + if ($objects->hasSetFields('reqSkill')) + $tabData['visibleCols'] = ['skill']; + + // create note if search limit was exceeded + if ($objects->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/localization/locale_dede.php b/localization/locale_dede.php index e37a8832..2cd40810 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Stufe", 'mechanic' => "Auswirkung", 'mechAbbr' => "Ausw.: ", - 'meetingStone' => "Versammlungsstein", + 'meetingStone' => "Versammlungsstein: ", 'requires' => "Benötigt %s", 'requires2' => "Benötigt", 'reqLevel' => "Benötigt Stufe %s", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 6670ad1b..ba489eef 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Level", 'mechanic' => "Mechanic", 'mechAbbr' => "Mech.: ", - 'meetingStone' => "Meeting Stone", + 'meetingStone' => "Meeting Stone: ", 'requires' => "Requires %s", 'requires2' => "Requires", 'reqLevel' => "Requires Level %s", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 65e22870..84353d5f 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Nivel", 'mechanic' => "Mecanica", 'mechAbbr' => "Mec.: ", - 'meetingStone' => "Roca de encuentro", + 'meetingStone' => "Roca de encuentro: ", 'requires' => "Requiere %s", 'requires2' => "Requiere", 'reqLevel' => "Necesitas ser de nivel %s", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index d0ad5a56..a1f7d19a 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Niveau", 'mechanic' => "Mécanique", 'mechAbbr' => "Mécan. : ", - 'meetingStone' => "Pierre de rencontre", + 'meetingStone' => "Pierre de rencontre : ", 'requires' => "%s requis", 'requires2' => "Requiert", 'reqLevel' => "Niveau %s requis", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 6034134e..a16699f7 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Уровень", 'mechanic' => "Механика", 'mechAbbr' => "Механика: ", - 'meetingStone' => "Камень встреч", + 'meetingStone' => "Камень встреч: ", 'requires' => "Требует %s", 'requires2' => "Требуется:", 'reqLevel' => "Требуется уровень: %s", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 876b8c90..2098281e 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -338,7 +338,7 @@ $lang = array( 'level' => "等级", 'mechanic' => "机制", 'mechAbbr' => "机制:", - 'meetingStone' => "集合石", + 'meetingStone' => "集合石:", 'requires' => "需要%s", 'requires2' => "需要", 'reqLevel' => "需要等级%s", diff --git a/pages/objects.php b/pages/objects.php deleted file mode 100644 index 5d516ea9..00000000 --- a/pages/objects.php +++ /dev/null @@ -1,90 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new GameObjectListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); - - $this->name = Util::ucFirst(Lang::game('objects')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); - - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['typeCat', (int)$this->category[0]]; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = ['data' => []]; - $objects = new GameObjectList($conditions, ['extraOpts' => $this->filterObj->extraOpts, 'calcTotal' => true]); - if (!$objects->error) - { - $tabData['data'] = array_values($objects->getListviewData()); - if ($objects->hasSetFields('reqSkill')) - $tabData['visibleCols'] = ['skill']; - - // create note if search limit was exceeded - if ($objects->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - - $this->lvTabs[] = [GameObjectList::$brickFile, $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::gameObject('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/template/pages/object.tpl.php b/template/pages/object.tpl.php index 5bd28b69..b3e076e5 100644 --- a/template/pages/object.tpl.php +++ b/template/pages/object.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
@@ -17,17 +20,17 @@
brick('redButtons'); ?> -

name; ?>

+

h1; ?>

brick('article'); + $this->brick('markup', ['markup' => $this->article]); if ($this->relBoss): - echo "
".sprintf(Lang::gameObject('npcLootPH'), $this->name, $this->relBoss[0], $this->relBoss[1])."
\n"; + echo "
".sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1])."
\n"; echo '
'; endif; -if (!empty($this->map)): +if ($this->map): $this->brick('mapper'); else: echo Lang::gameObject('unkPosition'); @@ -35,26 +38,15 @@ endif; $this->brick('book'); -if (isset($this->smartAI)): -?> -
- +$this->brick('markup', ['markup' => $this->smartAI]); -
-

brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/objects.tpl.php b/template/pages/objects.tpl.php index ffc6f356..d5055534 100644 --- a/template/pages/objects.tpl.php +++ b/template/pages/objects.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,20 +13,27 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [5]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); ?> - -
+
+
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

h1; ?>

+
- +
 />
ucFirst(Lang::main('name')).Lang::main('colon'); ?> />
- /> /> + /> />
@@ -39,7 +47,7 @@ $this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem'
-brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From e876463f3be24fcf06ffe6d45cebfea3234b687a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 17:02:52 +0200 Subject: [PATCH 674/957] Template/Update (Part 31) * convert dbtype 'quest' * make use of separate GlobalStrings for spell rewards --- {pages => endpoints/quest}/quest.php | 840 ++++++++++++++------------- endpoints/quest/quest_power.php | 50 ++ endpoints/quests/quests.php | 142 +++++ includes/dbtypes/quest.class.php | 2 +- includes/game/misc.php | 2 +- localization/lang.class.php | 2 +- localization/locale_dede.php | 37 +- localization/locale_enus.php | 39 +- localization/locale_eses.php | 37 +- localization/locale_frfr.php | 37 +- localization/locale_ruru.php | 41 +- localization/locale_zhcn.php | 37 +- pages/quests.php | 125 ---- pages/zone.php | 2 +- setup/tools/filegen/profiler.ss.php | 2 +- template/pages/quest.tpl.php | 168 +++--- template/pages/quests.tpl.php | 50 +- 17 files changed, 834 insertions(+), 779 deletions(-) rename {pages => endpoints/quest}/quest.php (65%) create mode 100644 endpoints/quest/quest_power.php create mode 100644 endpoints/quests/quests.php delete mode 100644 pages/quests.php diff --git a/pages/quest.php b/endpoints/quest/quest.php similarity index 65% rename from pages/quest.php rename to endpoints/quest/quest.php index dfc37210..305900d6 100644 --- a/pages/quest.php +++ b/endpoints/quest/quest.php @@ -6,85 +6,88 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 3: Quest g_initPath() -// tabId 0: Database g_initHeader() -class QuestPage extends GenericPage +class QuestBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $objectiveList = []; - protected $providedItem = []; - protected $series = []; - protected $gains = []; - protected $mail = []; - protected $rewards = []; - protected $objectives = ''; - protected $details = ''; - protected $offerReward = ''; - protected $requestItems = ''; - protected $completed = ''; - protected $end = ''; - protected $suggestedPl = 1; - protected $unavailable = false; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $type = Type::QUEST; - protected $typeId = 0; - protected $tpl = 'quest'; - protected $path = [0, 3]; - protected $tabId = 0; - protected $mode = CACHE_TYPE_PAGE; - protected $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js'], [SC_CSS_FILE, 'css/Book.css']]; + protected string $template = 'quest'; + protected string $pageName = 'quest'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 3]; - protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; + protected array $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js']]; - private $powerTpl = '$WowheadPower.registerQuest(%d, %d, %s);'; + public int $type = Type::QUEST; + public int $typeId = 0; + public array $objectiveList = []; + public ?IconElement $providedItem = null; + public array $mail = []; + public ?array $gains = null; // why array|null ? because destructuring an array with less elements than expected is an error, destructuring null just returns false + public ?array $rewards = null; // so " if ([$spells, $items, $choice, $money] = $this->rewards): " will either work or cleanly branch to else + public string $objectives = ''; + public string $details = ''; + public string $offerReward = ''; + public string $requestItems = ''; + public string $completed = ''; + public string $end = ''; + public int $suggestedPl = 1; + public bool $unavailable = false; - public function __construct($pageCall, $id) + private QuestList $subject; + + public function __construct(string $id) { - parent::__construct($pageCall, $id); + parent::__construct($id); - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - $this->typeId = intVal($id); + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + protected function generate() : void + { $this->subject = new QuestList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('quest'), Lang::quest('notFound')); + $this->generateNotFound(Lang::game('quest'), Lang::quest('notFound')); - // may contain htmlesque tags - $this->name = Lang::unescapeUISequences(Util::htmlEscape($this->subject->getField('name', true)), Lang::FMT_HTML); - } + $this->h1 = Lang::unescapeUISequences(Util::htmlEscape($this->subject->getField('name', true)), Lang::FMT_HTML); - protected function generatePath() - { - // recreate path - $this->path[] = $this->subject->getField('cat2'); - if ($cat = $this->subject->getField('cat1')) - { - foreach (Game::$questSubCats as $parent => $children) - if (in_array($cat, $children)) - $this->path[] = $parent; + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML) + ); - $this->path[] = $cat; - } - } - - protected function generateTitle() - { - // page title already escaped - array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('quest'))); - } - - protected function generateContent() - { $_level = $this->subject->getField('level'); $_minLevel = $this->subject->getField('minLevel'); $_flags = $this->subject->getField('flags'); $_specialFlags = $this->subject->getField('specialFlags'); $_side = ChrRace::sideFromMask($this->subject->getField('reqRaceMask')); + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('cat2'); + if ($cat = $this->subject->getField('cat1')) + { + foreach (Game::$questSubCats as $parent => $children) + if (in_array($cat, $children)) + $this->breadcrumb[] = $parent; + + $this->breadcrumb[] = $cat; + } + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('quest'))); + + /***********/ /* Infobox */ /***********/ @@ -95,7 +98,7 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('eventId')) { $this->extendGlobalIds(Type::WORLDEVENT, $_); - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$_.']'; + $infobox[] = Lang::game('eventShort', ['[event='.$_.']']); } // level @@ -109,7 +112,7 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('maxLevel')) $lvl .= ' - '.$_; - $infobox[] = sprintf(Lang::game('reqLevel'), $lvl); + $infobox[] = Lang::game('reqLevel', [$lvl]); } // loremaster (i dearly hope those flags cover every case...) @@ -128,10 +131,10 @@ class QuestPage extends GenericPage case 0: break; case 1: - $infobox[] = Lang::quest('loremaster').Lang::main('colon').'[achievement='.$loremaster->id.']'; + $infobox[] = Lang::quest('loremaster').'[achievement='.$loremaster->id.']'; break; default: - $lm = Lang::quest('loremaster').Lang::main('colon').'[ul]'; + $lm = Lang::quest('loremaster').'[ul]'; foreach ($loremaster->iterate() as $id => $__) $lm .= '[li][achievement='.$id.'][/li]'; @@ -153,16 +156,15 @@ class QuestPage extends GenericPage $_[] = Lang::quest('questInfo', $t); if ($_) - $infobox[] = Lang::game('type').Lang::main('colon').implode(' ', $_); + $infobox[] = Lang::game('type').implode(' ', $_); // side - $_ = Lang::main('side').Lang::main('colon'); - switch ($_side) + $infobox[] = Lang::main('side') . match ($this->subject->getField('faction')) { - case 3: $infobox[] = $_.Lang::game('si', 3); break; - case 2: $infobox[] = $_.'[span class=icon-horde]'.Lang::game('si', 2).'[/span]'; break; - case 1: $infobox[] = $_.'[span class=icon-alliance]'.Lang::game('si', 1).'[/span]'; break; - } + SIDE_ALLIANCE => '[span class=icon-alliance]'.Lang::game('si', SIDE_ALLIANCE).'[/span]', + SIDE_HORDE => '[span class=icon-horde]'.Lang::game('si', SIDE_HORDE).'[/span]', + default => Lang::game('si', SIDE_BOTH) // 0, 3 + }; $jsg = []; // races @@ -189,17 +191,17 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('reqSkillPoints')) $sk .= ' ('.$_.')'; - $infobox[] = Lang::quest('profession').Lang::main('colon').$sk; + $infobox[] = Lang::quest('profession').$sk; } // timer if ($_ = $this->subject->getField('timeLimit')) - $infobox[] = Lang::quest('timer').Lang::main('colon').Util::formatTime($_ * 1000); + $infobox[] = Lang::quest('timer').Util::formatTime($_ * 1000); - $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE questId = ?d', $this->typeId); + $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE `questId` = ?d', $this->typeId); // start - $start = '[icon name=quest_start'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('start').Lang::main('colon').'[/icon]'; + $start = '[icon name=quest_start'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('start').'[/icon]'; $s = []; foreach ($startEnd as $se) { @@ -214,7 +216,7 @@ class QuestPage extends GenericPage $infobox[] = implode('[br]', $s); // end - $end = '[icon name=quest_end'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('end').Lang::main('colon').'[/icon]'; + $end = '[icon name=quest_end'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('end').'[/icon]'; $e = []; foreach ($startEnd as $se) { @@ -266,98 +268,13 @@ class QuestPage extends GenericPage $_[] = '[color=r4]'.($_level + 3 + ceil(12 * $_level / MAX_LEVEL)).'[/color]'; if ($_) - $infobox[] = Lang::game('difficulty').Lang::main('colon').implode('[small]  [/small]', $_); + $infobox[] = Lang::game('difficulty').implode('[small]  [/small]', $_); } - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); - /**********/ - /* Series */ - /**********/ - - // Assumption - // a chain always ends in a single quest, but can have an arbitrary amount of quests leading into it. - // so we fast forward to the last quest and go backwards from there. - - $lastQuestId = $this->subject->getField('nextQuestIdChain'); - while ($newLast = DB::Aowow()->selectCell('SELECT `nextQuestIdChain` FROM ?_quests WHERE `id` = ?d AND `id` <> `nextQuestIdChain`', $lastQuestId)) - $lastQuestId = $newLast; - - $end = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `id` = ?d', $lastQuestId ?: $this->typeId); - $chain = array(array(array( // series / step / quest - 'side' => ChrRace::sideFromMask($end['reqRaceMask']), - 'typeStr' => Type::getFileString(Type::QUEST), - 'typeId' => $end['id'], - 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($end, 'name'), 40)), - ))); - - $prevStepIds = [$lastQuestId ?: $this->typeId]; - while ($prevQuests = DB::Aowow()->select('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `nextQuestIdChain` IN (?a) AND `id` <> `nextQuestIdChain`', $prevStepIds)) - { - $step = []; - foreach ($prevQuests as $pQuest) - $step[$pQuest['id']] = array( - 'side' => ChrRace::sideFromMask($pQuest['reqRaceMask']), - 'typeStr' => Type::getFileString(Type::QUEST), - 'typeId' => $pQuest['id'], - 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($pQuest, 'name'), 40)), - ); - - $prevStepIds = array_keys($step); - $chain[] = $step; - } - - if (count($chain) > 1) - $this->series[] = [array_reverse($chain), null]; - - - // todo (low): sensibly merge the following lists into 'series' - $listGen = function($cnd) - { - $chain = []; - $list = new QuestList($cnd); - if ($list->error) - return null; - - foreach ($list->iterate() as $id => $__) - { - $n = $list->getField('name', true); - $chain[] = array(array( - 'side' => ChrRace::sideFromMask($list->getField('reqRaceMask')), - 'typeStr' => Type::getFileString(Type::QUEST), - 'typeId' => $id, - 'name' => Util::htmlEscape(Lang::trimTextClean($n, 40)) - )); - } - - return $chain; - }; - - $extraLists = array( - // Requires all of these quests (Quests that you must follow to get this quest) - ['reqQ', array('OR', ['AND', ['nextQuestId', $this->typeId], ['exclusiveGroup', 0, '<']], ['AND', ['id', $this->subject->getField('prevQuestId')], ['nextQuestIdChain', $this->typeId, '!']])], - - // Requires one of these quests (Requires one of the quests to choose from) - ['reqOneQ', array('OR', ['AND', ['exclusiveGroup', 0, '>'], ['nextQuestId', $this->typeId]], ['breadCrumbForQuestId', $this->typeId])], - - // Opens Quests (Quests that become available only after complete this quest (optionally only one)) - ['opensQ', array('OR', ['AND', ['prevQuestId', $this->typeId], ['id', $this->subject->getField('nextQuestIdChain'), '!']], ['id', $this->subject->getField('nextQuestId')], ['id', $this->subject->getField('breadcrumbForQuestId')])], - - // Closes Quests (Quests that become inaccessible after completing this quest) - ['closesQ', array(['exclusiveGroup', 0, '>'], ['exclusiveGroup', $this->subject->getField('exclusiveGroup')], ['id', $this->typeId, '!'])], - - // During the quest available these quests (Quests that are available only at run time this quest) - ['enablesQ', array(['prevQuestId', -$this->typeId])], - - // Requires an active quest (Quests during the execution of which is available on the quest) - ['enabledByQ', array(['id', -$this->subject->getField('prevQuestId')])] - ); - - foreach ($extraLists as $el) - if ($_ = $listGen($el[1])) - $this->series[] = [$_, sprintf(Util::$dfnString, Lang::quest($el[0].'Desc'), Lang::quest($el[0]))]; - /*******************/ /* Objectives List */ /*******************/ @@ -391,31 +308,45 @@ class QuestPage extends GenericPage $providedRequired = false; foreach ($olItems as $i => [$itemId, $qty, $provided]) { - if (!$i || !$itemId || !in_array($itemId, $olItemData->getFoundIDs())) + if (!$i || !$itemId) continue; if ($provided) $providedRequired = true; - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $itemId, - 'name' => Lang::unescapeUISequences($olItemData->json[$itemId]['name'], Lang::FMT_HTML), - 'qty' => $qty > 1 ? $qty : 0, - 'quality' => 7 - $olItemData->json[$itemId]['quality'], - 'extraText' => $provided ? ' ('.Lang::quest('provided').')' : '' - ); + if (!$olItemData->getEntry($itemId)) + { + $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $qty > 1 ? $qty : '', extraText: $provided ? Lang::quest('provided') : null)]; + continue; + } + + $this->objectiveList[] = [0, new IconElement( + Type::ITEM, + $itemId, + Lang::unescapeUISequences($olItemData->json[$itemId]['name'], Lang::FMT_HTML), + num: $qty > 1 ? $qty : '', + quality: 7 - $olItemData->json[$itemId]['quality'], + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: $provided ? Lang::quest('provided') : null + )]; } // if providd item is not required by quest, list it below other requirements - if (!$providedRequired && $olItems[0][0] && in_array($olItems[0][0], $olItemData->getFoundIDs())) + if (!$providedRequired && $olItems[0][0]) { - $this->providedItem = array( - 'id' => $olItems[0][0], - 'name' => Lang::unescapeUISequences($olItemData->json[$olItems[0][0]]['name'], Lang::FMT_HTML), - 'qty' => $olItems[0][1] > 1 ? $olItems[0][1] : 0, - 'quality' => 7 - $olItemData->json[$olItems[0][0]]['quality'] - ); + if (!$olItemData->getEntry($olItems[0][0])) + $this->providedItem = new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $olItems[0][1] > 1 ? $olItems[0][1] : ''); + else + $this->providedItem = new IconElement( + Type::ITEM, + $olItems[0][0], + Lang::unescapeUISequences($olItemData->json[$olItems[0][0]]['name'], Lang::FMT_HTML), + num: $olItems[0][1] > 1 ? $olItems[0][1] : '', + quality: 7 - $olItemData->json[$olItems[0][0]]['quality'], + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon' + ); } } @@ -449,24 +380,40 @@ class QuestPage extends GenericPage $olNPCs[$p][2][$id] = $olNPCData->getField('name', true); } - foreach ($olNPCs as $i => $pair) + foreach ($olNPCs as $i => [$qty, $altText, $proxies]) { - if (!$i || !in_array($i, $olNPCData->getFoundIDs())) + if (!$i) continue; - $ol = array( - 'typeStr' => Type::getFileString(Type::NPC), - 'id' => $i, - 'name' => $pair[1] ?: Util::localizedString($olNPCData->getEntry($i), 'name'), - 'qty' => $pair[0] > 1 ? $pair[0] : 0, - 'extraText' => (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $pair[1]) ? '' : ' '.Lang::achievement('slain'), - 'proxy' => $pair[2] - ); + if ($proxies) // has proxies assigned, add yourself as another proxy + { + $proxies[$i] = Util::localizedString($olNPCData->getEntry($i), 'name'); - if ($pair[2]) // has proxies assigned, add yourself as another proxy - $ol['proxy'][$i] = Util::localizedString($olNPCData->getEntry($i), 'name'); + // split in two blocks for display + $proxies = array( + array_slice($proxies, 0, ceil(count($proxies) / 2), true), + array_slice($proxies, ceil(count($proxies) / 2), null, true) + ); - $this->objectiveList[] = $ol; + $this->objectiveList[] = [2, array( + 'id' => $i, + 'text' => ($altText ?: Util::localizedString($olNPCData->getEntry($i), 'name')) . ((($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : ' '.Lang::achievement('slain')), + 'qty' => $qty > 1 ? $qty : 0, + 'proxy' => array_filter($proxies) + )]; + } + else if (!$olNPCData->getEntry($i)) + $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('npc')).' #'.$i, $qty > 1 ? $qty : '')]; + else + $this->objectiveList[] = [0, new IconElement( + Type::NPC, + $i, + $altText ?: Util::localizedString($olNPCData->getEntry($i), 'name'), + $qty > 1 ? $qty : '', + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : Lang::achievement('slain'), + )]; } } @@ -476,18 +423,22 @@ class QuestPage extends GenericPage $olGOData = new GameObjectList(array(['id', $ids])); $this->extendGlobalData($olGOData->getJSGlobals(GLOBALINFO_SELF)); - foreach ($olGOs as $i => $pair) + foreach ($olGOs as $i => [$qty, $altText]) { - if (!$i || !in_array($i, $olGOData->getFoundIDs())) + if (!$i) continue; - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::OBJECT), - 'id' => $i, - 'name' => $pair[1] ?: Lang::unescapeUISequences(Util::localizedString($olGOData->getEntry($i), 'name'), Lang::FMT_HTML), - 'qty' => $pair[0] > 1 ? $pair[0] : 0, - 'extraText' => '' - ); + if (!$olGOData->getEntry($i)) + $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('object')).' #'.$i, $qty > 1 ? $qty : '')]; + else + $this->objectiveList[] = [0, new IconElement( + Type::OBJECT, + $i, + $altText ?: Lang::unescapeUISequences(Util::localizedString($olGOData->getEntry($i), 'name'), Lang::FMT_HTML), + $qty > 1 ? $qty : '', + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + )]; } } @@ -512,13 +463,14 @@ class QuestPage extends GenericPage if (!$i || !in_array($i, $olFactionsData->getFoundIDs())) continue; - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::FACTION), - 'id' => $i, - 'name' => Util::localizedString($olFactionsData->getEntry($i), 'name'), - 'qty' => sprintf(Util::$dfnString, $val.' '.Lang::achievement('points'), Lang::getReputationLevelForPoints($val)), - 'extraText' => '' - ); + $this->objectiveList[] = [0, new IconElement( + Type::FACTION, + $i, + Util::localizedString($olFactionsData->getEntry($i), 'name'), + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: sprintf(Util::$dfnString, $val.' '.Lang::achievement('points'), '('.Lang::getReputationLevelForPoints($val).')') + )]; } } @@ -526,29 +478,22 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('sourceSpellId')) { $this->extendGlobalIds(Type::SPELL, $_); - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $_, - 'name' => SpellList::getName($_), - 'qty' => 0, - 'extraText' => ' ('.Lang::quest('provided').')' - ); + $this->objectiveList[] = [0, new IconElement(Type::SPELL, $_, SpellList::getName($_), extraText: Lang::quest('provided'), element: 'iconlist-icon')]; } // required money if ($this->subject->getField('rewardOrReqMoney') < 0) - $this->objectiveList[] = ['text' => Lang::quest('reqMoney').Lang::main('colon').Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))]; + $this->objectiveList[] = [1, Lang::quest('reqMoney', [Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))])]; // required pvp kills if ($_ = $this->subject->getField('reqPlayerKills')) - $this->objectiveList[] = ['text' => Lang::quest('playerSlain').' ('.$_.')']; + $this->objectiveList[] = [1, Lang::quest('playerSlain', [$_])]; + /**********/ /* Mapper */ /**********/ - $this->addScript([SC_JS_FILE, '?data=zones']); - // gather points of interest $mapNPCs = $mapGOs = []; // [typeId, start|end|objective, startItemId] @@ -560,7 +505,7 @@ class QuestPage extends GenericPage { /* todo (med): sanity check: - there are loot templates that are absolute tosh, containing hundrets of random items (e.g. Peacebloom for Quest "The Horde Needs Peacebloom!") + there are loot templates that are absolute tosh, containing hundreds of random items (e.g. Peacebloom for Quest "The Horde Needs Peacebloom!") even without these .. consider quests like "A Donation of Runecloth" .. oh my ..... should we... .. display only a maximum of sources? @@ -637,7 +582,7 @@ class QuestPage extends GenericPage $mObjectives[$zoneId]['levels'][$floor][] = $processing($objId, $objData); } } - }; + }; // POI: start + end @@ -674,10 +619,13 @@ class QuestPage extends GenericPage if ($_specialFlags & QUEST_FLAG_SPECIAL_EXT_COMPLETE) { // areatrigger - if ($atir = DB::Aowow()->selectCol('SELECT id FROM ?_areatrigger WHERE type = ?d AND quest = ?d', AT_TYPE_OBJECTIVE, $this->typeId)) + if ($atir = DB::Aowow()->selectCol('SELECT `id` FROM ?_areatrigger WHERE `type` = ?d AND `quest` = ?d', AT_TYPE_OBJECTIVE, $this->typeId)) { - if ($atSpawns = DB::AoWoW()->select('SELECT typeId AS ARRAY_KEY, posX, posY, floor, areaId FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a)', Type::AREATRIGGER, $atir)) + if ($atSpawns = DB::AoWoW()->select('SELECT `typeId` AS ARRAY_KEY, `posX`, `posY`, `floor`, `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a)', Type::AREATRIGGER, $atir)) { + if (User::isInGroup(U_GROUP_STAFF)) + $endTextWrapper = '%s'; + foreach ($atSpawns as $atId => $atsp) { $atSpawn = array ( @@ -705,7 +653,7 @@ class QuestPage extends GenericPage } } // complete-spell - else if ($endSpell = new SpellList(array('OR', ['AND', ['effect1Id', 16], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', 16], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', 16], ['effect3MiscValue', $this->typeId]]))) + else if ($endSpell = new SpellList(array('OR', ['AND', ['effect1Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect3MiscValue', $this->typeId]]))) if (!$endSpell->error) $endTextWrapper = '%s'; } @@ -901,24 +849,30 @@ class QuestPage extends GenericPage } } - $this->map = $mObjectives ? array( - 'mapperData' => [], // always empty - 'data' => array( - 'parent' => 'mapper-generic', - 'objectives' => $mObjectives, - 'zoneparent' => 'mapper-zone-generic', - 'zones' => $mZones, - 'missing' => count($mZones) > 1 || $hasStartEnd != 0x3 ? 1 : 0 // 0 if everything happens in one zone, else 1 - ) - ) : null; + if ($mObjectives) + { + $this->addDataLoader('zones'); + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'objectives' => $mObjectives, + 'zoneparent' => 'mapper-zone-generic', + 'zones' => $mZones, + 'missing' => count($mZones) > 1 || $hasStartEnd != 0x3 ? 1 : 0 // 0 if everything happens in one zone, else 1 + ), + new \StdClass(), // mapperData + null, // ShowOnMap + null // foundIn + ); + } /****************/ /* Main Content */ /****************/ + $this->series = $this->createSeries($_side); $this->gains = $this->createGains(); - $this->mail = $this->createMail($startEnd); $this->rewards = $this->createRewards($_side); $this->objectives = $this->subject->parseText('objectives', false); $this->details = $this->subject->parseText('details', false); @@ -939,36 +893,41 @@ class QuestPage extends GenericPage ) ); + if ($this->createMail($startEnd)) + $this->addScript([SC_CSS_FILE, 'css/Book.css']); + // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(horde_id = ?d, alliance_id, -horde_id) FROM player_factionchange_quests WHERE alliance_id = ?d OR horde_id = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_quests WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) { $altQuest = new QuestList(array(['id', abs($pendant)])); if (!$altQuest->error) { - $this->transfer = sprintf( - Lang::quest('_transfer'), + $this->transfer = Lang::quest('_transfer', array( $altQuest->id, $altQuest->getField('name', true), $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); } } + /**************/ /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: see also $seeAlso = new QuestList(array(['name_loc'.Lang::getLocale()->value, '%'.Util::htmlEscape($this->subject->getField('name', true)).'%'], ['id', $this->typeId, '!'])); if (!$seeAlso->error) { $this->extendGlobalData($seeAlso->getJSGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($seeAlso->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $seeAlso->getListviewData(), 'name' => '$LANG.tab_seealso', 'id' => 'see-also' - )]; + ), QuestList::$brickFile)); } // tab: criteria of @@ -976,27 +935,27 @@ class QuestPage extends GenericPage if (!$criteriaOf->error) { $this->extendGlobalData($criteriaOf->getJSGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($criteriaOf->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $criteriaOf->getListviewData(), 'name' => '$LANG.tab_criteriaof', 'id' => 'criteria-of' - )]; + ), AchievementList::$brickFile)); } // tab: spawning pool (for the swarm) - if ($qp = DB::World()->selectCol('SELECT qpm2.questId FROM quest_pool_members qpm1 JOIN quest_pool_members qpm2 ON qpm1.poolId = qpm2.poolId WHERE qpm1.questId = ?d', $this->typeId)) + if ($qp = DB::World()->selectCol('SELECT qpm2.`questId` FROM quest_pool_members qpm1 JOIN quest_pool_members qpm2 ON qpm1.`poolId` = qpm2.`poolId` WHERE qpm1.`questId` = ?d', $this->typeId)) { - $max = DB::World()->selectCell('SELECT numActive FROM quest_pool_template qpt JOIN quest_pool_members qpm ON qpm.poolId = qpt.poolId WHERE qpm.questId = ?d', $this->typeId); + $max = DB::World()->selectCell('SELECT `numActive` FROM quest_pool_template qpt JOIN quest_pool_members qpm ON qpm.`poolId` = qpt.`poolId` WHERE qpm.`questId` = ?d', $this->typeId); $pooledQuests = new QuestList(array(['id', $qp])); if (!$pooledQuests->error) { $this->extendGlobalData($pooledQuests->getJSGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($pooledQuests->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $pooledQuests->getListviewData(), 'name' => 'Quest Pool', 'id' => 'quest-pool', 'note' => Lang::quest('questPoolDesc', [$max]) - )]; + ), QuestList::$brickFile)); } } @@ -1015,27 +974,15 @@ class QuestPage extends GenericPage if ($tab = $cnd->toListviewTab()) { $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - } - - protected function generateTooltip() - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW); - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - if ($this->subject->isDaily()) - $power->daily = 1; + $this->lvTabs->addDataTab(...$tab); } - return sprintf($this->powerTpl, $this->typeId, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); + parent::generate(); } - private function createRewards($side) + private function createRewards(int $side) : ?array { - $rewards = []; + $rewards = [[], [], [], '']; // [spells, items, choice, money] // moneyReward / maxLevelCompensation $comp = $this->subject->getField('rewardMoneyMaxLevel'); @@ -1043,75 +990,68 @@ class QuestPage extends GenericPage $realComp = max($comp, $questMoney); if ($questMoney > 0) { - $rewards['money'] = Util::formatMoney($questMoney); + $rewards[3] = Util::formatMoney($questMoney); if ($realComp > $questMoney) - $rewards['money'] .= ' ' . sprintf(Lang::quest('expConvert'), Util::formatMoney($realComp), MAX_LEVEL); + $rewards[3] .= ' ' . Lang::quest('expConvert', [Util::formatMoney($realComp), MAX_LEVEL]); } else if ($questMoney <= 0 && $realComp > 0) - $rewards['money'] = sprintf(Lang::quest('expConvert2'), Util::formatMoney($realComp), MAX_LEVEL); + $rewards[3] = Lang::quest('expConvert2', [Util::formatMoney($realComp), MAX_LEVEL]); // itemChoices if (!empty($this->subject->choices[$this->typeId][Type::ITEM])) { - $c = $this->subject->choices[$this->typeId][Type::ITEM]; - $choiceItems = new ItemList(array(['id', array_keys($c)])); + $choices = $this->subject->choices[$this->typeId][Type::ITEM]; + $choiceItems = new ItemList(array(['id', array_keys($choices)])); if (!$choiceItems->error) { $this->extendGlobalData($choiceItems->getJSGlobals()); - foreach ($choiceItems->Iterate() as $id => $__) - { - $rewards['choice'][] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $id, - 'name' => $choiceItems->getField('name', true), - 'quality' => $choiceItems->getField('quality'), - 'qty' => $c[$id], - 'globalStr' => Type::getJSGlobalString(Type::ITEM) - ); - } + foreach ($choices as $id => $num) // itr over $choices to preserve display order + if ($choiceItems->getEntry($id)) + $rewards[2][] = new IconElement( + Type::ITEM, + $id, + Lang::unescapeUISequences($choiceItems->getField('name', true), Lang::FMT_HTML), + quality: $choiceItems->getField('quality'), + num: $num + ); } } // itemRewards if (!empty($this->subject->rewards[$this->typeId][Type::ITEM])) { - $ri = $this->subject->rewards[$this->typeId][Type::ITEM]; - $rewItems = new ItemList(array(['id', array_keys($ri)])); + $reward = $this->subject->rewards[$this->typeId][Type::ITEM]; + $rewItems = new ItemList(array(['id', array_keys($reward)])); if (!$rewItems->error) { $this->extendGlobalData($rewItems->getJSGlobals()); - foreach ($rewItems->Iterate() as $id => $__) - { - $rewards['items'][] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $id, - 'name' => Lang::unescapeUISequences($rewItems->getField('name', true), Lang::FMT_HTML), - 'quality' => $rewItems->getField('quality'), - 'qty' => $ri[$id], - 'globalStr' => Type::getJSGlobalString(Type::ITEM) - ); - } + foreach ($reward as $id => $num) // itr over $reward to preserve display order + if ($rewItems->getEntry($id)) + $rewards[1][] = new IconElement( + Type::ITEM, + $id, + Lang::unescapeUISequences($rewItems->getField('name', true), Lang::FMT_HTML), + quality: $rewItems->getField('quality'), + num: $num + ); } } if (!empty($this->subject->rewards[$this->typeId][Type::CURRENCY])) { - $rc = $this->subject->rewards[$this->typeId][Type::CURRENCY]; - $rewCurr = new CurrencyList(array(['id', array_keys($rc)])); + $currency = $this->subject->rewards[$this->typeId][Type::CURRENCY]; + $rewCurr = new CurrencyList(array(['id', array_keys($currency)])); if (!$rewCurr->error) { $this->extendGlobalData($rewCurr->getJSGlobals()); - foreach ($rewCurr->Iterate() as $id => $__) - { - $rewards['items'][] = array( - 'typeStr' => Type::getFileString(Type::CURRENCY), - 'id' => $id, - 'name' => $rewCurr->getField('name', true), - 'quality' => 1, - 'qty' => $rc[$id] * ($side == 2 ? -1 : 1), // toggles the icon - 'globalStr' => Type::getJSGlobalString(Type::CURRENCY) + foreach ($rewCurr->iterate() as $id => $__) + $rewards[1][] = new IconElement( + Type::CURRENCY, + $id, + $rewCurr->getField('name', true), + quality: ITEM_QUALITY_NORMAL, + num: $currency[$id] * ($side == SIDE_HORDE ? -1 : 1), // toggles the icon ); - } } } @@ -1133,18 +1073,14 @@ class QuestPage extends GenericPage { $extra = null; if ($_ = $rewSpells->getEntry($displ)) - $extra = sprintf(Lang::quest('spellDisplayed'), $displ, Util::localizedString($_, 'name')); + $extra = Lang::quest('spellDisplayed', [$displ, Util::localizedString($_, 'name')]); if ($_ = $rewSpells->getEntry($cast)) - { - $rewards['spells']['extra'] = $extra; - $rewards['spells']['cast'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $cast, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) + $rewards[0] = array( + 'title' => Lang::quest('rewardAura'), + 'cast' => [new IconElement(Type::SPELL, $cast, Util::localizedString($_, 'name'))], + 'extra' => $extra ); - } } else // if it has effect:learnSpell display the taught spell instead { @@ -1154,114 +1090,102 @@ class QuestPage extends GenericPage foreach ($_ as $idx) $teach[$rewSpells->getField('effect'.$idx.'TriggerSpell')] = $id; - if ($_ = $rewSpells->getEntry($displ)) - { - $rewards['spells']['extra'] = null; - $rewards['spells'][$teach ? 'learn' : 'cast'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $displ, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) - ); - } - else if (($_ = $rewSpells->getEntry($cast)) && !$teach) - { - $rewards['spells']['extra'] = null; - $rewards['spells']['cast'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $cast, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) - ); - } - else + if ($teach) { $taught = new SpellList(array(['id', array_keys($teach)])); if (!$taught->error) { $this->extendGlobalData($taught->getJSGlobals()); - $rewards['spells']['extra'] = null; + $rewards[0] = ['cast' => [], 'extra' => null]; + + $isTradeSkill = 0; foreach ($taught->iterate() as $id => $__) { - $rewards['spells']['learn'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $id, - 'name' => $taught->getField('name', true), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) - ); + $isTradeSkill |= array_intersect($taught->getField('skillLines'), array_merge(SKILLS_TRADE_PRIMARY, SKILLS_TRADE_SECONDARY)) ? 1 : 0; + $rewards[0]['cast'][] = new IconElement(Type::SPELL, $id, $taught->getField('name', true)); } + + $rewards[0]['title'] = $isTradeSkill ? Lang::quest('rewardTradeSkill') : Lang::quest('rewardSpell'); } } - } - } - - return $rewards; - } - - private function createMail($startEnd) - { - $mail = []; - - if ($rmtId = $this->subject->getField('rewardMailTemplateId')) - { - $delay = $this->subject->getField('rewardMailDelay'); - $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE id = ?d', $rmtId); - - $mail = array( - 'id' => $rmtId, - 'delay' => $delay ? sprintf(Lang::mail('mailIn'), Util::formatTime($delay * 1000)) : null, - 'sender' => null, - 'attachments' => [], - 'text' => $letter ? Util::parseHtmlText(Util::localizedString($letter, 'text')) : null, - 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject')) - ); - - $senderTypeId = 0; - if ($_= DB::World()->selectCell('SELECT RewardMailSenderEntry FROM quest_mail_sender WHERE QuestId = ?d', $this->typeId)) - $senderTypeId = $_; - else - foreach ($startEnd as $se) - if (($se['method'] & 0x2) && $se['type'] == Type::NPC) - $senderTypeId = $se['typeId']; - - if ($ti = CreatureList::getName($senderTypeId)) - $mail['sender'] = sprintf(Lang::mail('mailBy'), $senderTypeId, $ti); - - // while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item. - $mailLoot = new Loot(); - if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId)) - { - $this->extendGlobalData($mailLoot->jsGlobals); - foreach ($mailLoot->getResult() as $loot) + else if (($_ = $rewSpells->getEntry($displ)) || ($_ = $rewSpells->getEntry($cast))) { - $mail['attachments'][] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $loot['id'], - 'name' => substr($loot['name'], 1), - 'quality' => 7 - $loot['name'][0], - 'qty' => $loot['stack'][0], - 'globalStr' => Type::getJSGlobalString(Type::ITEM) + $rewards[0] = array( + 'title' => Lang::quest('rewardAura'), + 'cast' => [new IconElement(Type::SPELL, $cast, Util::localizedString($_, 'name'))], + 'extra' => null ); } } } - return $mail; + if (!array_filter($rewards)) + return null; + + return $rewards; } - private function createGains() + private function createMail(array $startEnd) : bool + { + $rmtId = $this->subject->getField('rewardMailTemplateId'); + if (!$rmtId) + return false; + + $delay = $this->subject->getField('rewardMailDelay'); + $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE `id` = ?d', $rmtId); + + $this->mail = array( + 'attachments' => [], + 'text' => $letter ? Util::parseHtmlText(Util::localizedString($letter, 'text')) : null, + 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject')), + 'header' => array( + $rmtId, + null, + $delay ? Lang::mail('mailIn', [Util::formatTime($delay * 1000)]) : null, + ) + ); + + $senderTypeId = 0; + if ($_= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = ?d', $this->typeId)) + $senderTypeId = $_; + else + foreach ($startEnd as $se) + if (($se['method'] & 0x2) && $se['type'] == Type::NPC) + $senderTypeId = $se['typeId']; + + if ($ti = CreatureList::getName($senderTypeId)) + $this->mail['header'][1] = Lang::mail('mailBy', [$senderTypeId, $ti]); + + // while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item. + $mailLoot = new Loot(); + if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId)) + { + $this->extendGlobalData($mailLoot->jsGlobals); + foreach ($mailLoot->getResult() as $loot) + $this->mail['attachments'][] = new IconElement(Type::ITEM, $loot['id'], substr($loot['name'], 1), $loot['stack'][0], quality: 7 - $loot['name'][0]); + } + + return true; + } + + private function createGains() : ?array { $gains = []; // xp - if ($_ = $this->subject->getField('rewardXP')) - $gains['xp'] = $_; + $gains[0] = $this->subject->getField('rewardXP'); // talent points - if ($_ = $this->subject->getField('rewardTalents')) - $gains['tp'] = $_; + $gains[3] = $this->subject->getField('rewardTalents'); + + // title + if ($tId = $this->subject->getField('rewardTitleId')) + $gains[2] = [$tId, (new TitleList(array(['id', $tId])))->getHtmlizedName()]; + else + $gains[2] = null; // reputation + $repGains = []; for ($i = 1; $i < 6; $i++) { $fac = $this->subject->getField('rewardFactionId'.$i); @@ -1275,32 +1199,114 @@ class QuestPage extends GenericPage 'name' => FactionList::getName($fac) ); - if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE faction = ?d', $fac)) + if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE `faction` = ?d', $fac)) { - if ($dailyType = $this->subject->isDaily()) - { - if ($dailyType == 1 && $cuRates['quest_daily_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_daily_rate'] - 1); - else if ($dailyType == 2 && $cuRates['quest_weekly_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_weekly_rate'] - 1); - else if ($dailyType == 3 && $cuRates['quest_monthly_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_monthly_rate'] - 1); - } - else if ($this->subject->isRepeatable() && $cuRates['quest_repeatable_rate'] != 1.0) + if ($this->subject->isRepeatable()) $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_repeatable_rate'] - 1); - else if ($cuRates['quest_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_rate'] - 1); + else + $rep['qty'][1] = $rep['qty'][0] * match ($this->subject->isDaily()) + { + 1 => $cuRates['quest_daily_rate'] - 1, + 2 => $cuRates['quest_weekly_rate'] - 1, + 3 => $cuRates['quest_monthly_rate'] - 1, + default => $cuRates['quest_rate'] - 1 + }; } - $gains['rep'][] = $rep; - } + if (User::isInGroup(U_GROUP_STAFF)) + $rep['qty'][1] = $rep['qty'][0] . ($rep['qty'][1] ? $this->fmtStaffTip(($rep['qty'][1] > 0 ? '+' : '').$rep['qty'][1], Lang::faction('customRewRate')) : ''); + else + $rep['qty'][1] += $rep['qty'][0]; - // title - if ($_ = (new TitleList(array(['id', $this->subject->getField('rewardTitleId')])))->getHtmlizedName()) - $gains['title'] = $_; + $repGains[] = $rep; + } + $gains[1] = $repGains; + + if (!array_filter($gains)) + return null; return $gains; } + + private function createSeries() : array + { + $series = []; + + $makeSeriesItem = function (array $questData) : array + { + return array( + 'side' => ChrRace::sideFromMask($questData['reqRaceMask']), + 'typeStr' => Type::getFileString(Type::QUEST), + 'typeId' => $questData['id'], + 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($questData, 'name'), 40)), + ); + }; + + // Assumption + // a chain always ends in a single quest, but can have an arbitrary amount of quests leading into it. + // so we fast forward to the last quest and go backwards from there. + + $lastQuestId = $this->subject->getField('nextQuestIdChain'); + while ($newLast = DB::Aowow()->selectCell('SELECT `nextQuestIdChain` FROM ?_quests WHERE `id` = ?d AND `id` <> `nextQuestIdChain`', $lastQuestId)) + $lastQuestId = $newLast; + + $end = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `id` = ?d', $lastQuestId ?: $this->typeId); + $chain = array(array($makeSeriesItem($end))); // series / step / quest + + $prevStepIds = [$lastQuestId ?: $this->typeId]; + while ($prevQuests = DB::Aowow()->select('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `nextQuestIdChain` IN (?a) AND `id` <> `nextQuestIdChain`', $prevStepIds)) + { + $step = []; + foreach ($prevQuests as $pQuest) + $step[$pQuest['id']] = $makeSeriesItem($pQuest); + + $prevStepIds = array_keys($step); + $chain[] = $step; + } + + if (count($chain) > 1) + $series[] = [array_reverse($chain), null]; + + // todo (low): sensibly merge the following lists into 'series' + $listGen = function($cnd) use ($makeSeriesItem) + { + $chain = []; + $list = new QuestList($cnd); + if ($list->error) + return null; + + foreach ($list->iterate() as $tpl) + $chain[] = [$makeSeriesItem($tpl)]; + + return $chain; + }; + + $extraLists = array( + // Requires all of these quests (Quests that you must follow to get this quest) + ['reqQ', array('OR', ['AND', ['nextQuestId', $this->typeId], ['exclusiveGroup', 0, '<']], ['AND', ['id', $this->subject->getField('prevQuestId')], ['nextQuestIdChain', $this->typeId, '!']])], + + // Requires one of these quests (Requires one of the quests to choose from) + ['reqOneQ', array('OR', ['AND', ['exclusiveGroup', 0, '>'], ['nextQuestId', $this->typeId]], ['breadCrumbForQuestId', $this->typeId])], + + // Opens Quests (Quests that become available only after complete this quest (optionally only one)) + ['opensQ', array('OR', ['AND', ['prevQuestId', $this->typeId], ['id', $this->subject->getField('nextQuestIdChain'), '!']], ['id', $this->subject->getField('nextQuestId')], ['id', $this->subject->getField('breadcrumbForQuestId')])], + + // Closes Quests (Quests that become inaccessible after completing this quest) + ['closesQ', array(['exclusiveGroup', 0, '>'], ['exclusiveGroup', $this->subject->getField('exclusiveGroup')], ['id', $this->typeId, '!'])], + + // During the quest available these quests (Quests that are available only at run time this quest) + ['enablesQ', array(['prevQuestId', -$this->typeId])], + + // Requires an active quest (Quests during the execution of which is available on the quest) + ['enabledByQ', array(['id', -$this->subject->getField('prevQuestId')])] + ); + + foreach ($extraLists as [$section, $condition]) + if ($_ = $listGen($condition)) + $series[] = [$_, sprintf(Util::$dfnString, Lang::quest($section.'Desc'), Lang::quest($section))]; + + return $series; + } } ?> diff --git a/endpoints/quest/quest_power.php b/endpoints/quest/quest_power.php new file mode 100644 index 00000000..d0eccf52 --- /dev/null +++ b/endpoints/quest/quest_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $quest = new QuestList(array(['id', $this->typeId])); + if ($quest->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => Lang::unescapeUISequences($quest->getField('name', true), Lang::FMT_RAW), + 'tooltip' => $quest->renderTooltip(), + 'daily' => $quest->isDaily() ? 1 : null + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/quests/quests.php b/endpoints/quests/quests.php new file mode 100644 index 00000000..fb4d944f --- /dev/null +++ b/endpoints/quests/quests.php @@ -0,0 +1,142 @@ + 3519, 4024 => 3537, 25 => 46, 1769 => 361, + // Startzones: Horde + 132 => 1, 9 => 12, 3431 => 3430, 154 => 85, + // Startzones: Alliance + 3526 => 3524, 363 => 14, 220 => 215, 188 => 141, + // Group: Caverns of Time + 2366 => 1941, 2367 => 1941, 4100 => 1941, + // Group: Hellfire Citadell + 3562 => 3535, 3713 => 3535, 3714 => 3535, + // Group: Auchindoun + 3789 => 3688, 3790 => 3688, 3791 => 3688, 3792 => 3688, + // Group: Tempest Keep + 3847 => 3842, 3848 => 3842, 3849 => 3842, + // Group: Coilfang Reservoir + 3715 => 3905, 3716 => 3905, 3717 => 3905, + // Group: Icecrown Citadel + 4809 => 4522, 4813 => 4522, 4820 => 4522 + ); + + protected int $type = Type::QUEST; + protected int $cacheType = CACHE_TYPE_PAGE; + + protected string $template = 'quests'; + protected string $pageName = 'quests'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 3]; + + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = Game::QUEST_CLASSES; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new QuestListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('quests')); + + $conditions = []; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->filter->evalCriteria(); + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + if (isset($this->category[1])) + $conditions[] = ['zoneOrSort', $this->category[1]]; + else if (isset($this->category[0])) + $conditions[] = ['zoneOrSort', $this->validCats[$this->category[0]]]; + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + if (isset($this->category[1]) && isset(self::SUB_SUB_CAT[$this->category[1]])) + array_splice($this->breadcrumb, 3, 0, self::SUB_SUB_CAT[$this->category[1]]); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if (isset($this->category[1])) + array_unshift($this->title, Lang::quest('cat', $this->category[0], $this->category[1])); + else if (isset($this->category[0])) + { + $c0 = Lang::quest('cat', $this->category[0]); + array_unshift($this->title, is_array($c0) ? $c0[0] : $c0); + } + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $quests = new QuestList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + + $this->extendGlobalData($quests->getJSGlobals()); + + $tabData = ['data' => $quests->getListviewData()]; + + if ($rc = $this->filter->fiReputationCols) + $tabData['extraCols'] = '$fi_getReputationCols('.json_encode($rc, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; + else if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + // create note if search limit was exceeded + if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + else if (isset($this->category[1]) && $this->category[1] > 0) + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questgivers, '.$this->category[1].', g_zones['.$this->category[1].'], '.$this->category[1].')'; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index f46a6ec5..5cd13bb7 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -36,7 +36,7 @@ class QuestList extends DBTypeList $_curTpl['cat1'] = $_curTpl['zoneOrSort']; // should probably be in a method... $_curTpl['cat2'] = 0; - foreach (Game::$questClasses as $k => $arr) + foreach (Game::QUEST_CLASSES as $k => $arr) { if (in_array($_curTpl['cat1'], $arr)) { diff --git a/includes/game/misc.php b/includes/game/misc.php index 44d6727d..67bfc553 100644 --- a/includes/game/misc.php +++ b/includes/game/misc.php @@ -31,7 +31,7 @@ class Game 1 => ['ability_rogue_eviscerate', 'ability_warrior_innerrage', 'ability_warrior_defensivestance' ] ); - public static $questClasses = array( + public const /* array */ QUEST_CLASSES = array( -2 => [ 0], 0 => [ 1, 3, 4, 8, 9, 10, 11, 12, 25, 28, 33, 36, 38, 40, 41, 44, 45, 46, 47, 51, 85, 130, 132, 139, 154, 267, 1497, 1519, 1537, 2257, 3430, 3431, 3433, 3487, 4080, 4298], 1 => [ 14, 15, 16, 17, 141, 148, 188, 215, 220, 331, 357, 361, 363, 400, 405, 406, 440, 490, 493, 618, 1377, 1637, 1638, 1657, 1769, 3524, 3525, 3526, 3557], diff --git a/localization/lang.class.php b/localization/lang.class.php index 62a54838..1bde86d4 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -490,7 +490,7 @@ class Lang public static function formatSkillBreakpoints(array $bp, int $fmt = self::FMT_MARKUP) : string { - $tmp = self::game('difficulty').self::main('colon'); + $tmp = self::game('difficulty'); $base = match ($fmt) { diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 2cd40810..ccf542bb 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Briefe", 'cooldown' => "%s Abklingzeit", - 'difficulty' => "Modus", + 'difficulty' => "Modus: ", 'dispelType' => "Bannart", 'duration' => "Dauer", 'eventShort' => "Ereignis: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Dieses Weltereignis existiert nicht.", - 'start' => "Anfang", - 'end' => "Ende", + 'start' => "Anfang: ", + 'end' => "Ende: ", 'interval' => "Intervall", 'inProgress' => "Ereignis findet gerade statt", 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Dieses Quest wird mit %s vertauscht, wenn Ihr zur %s wechselt.', 'questLevel' => "Stufe %s", 'requirements' => "Anforderungen", - 'reqMoney' => "Benötigtes Geld", + 'reqMoney' => "Benötigtes Geld: %s", 'money' => "Geld", 'additionalReq' => "Zusätzliche Anforderungen um das Quest zu erhalten", 'reqRepWith' => 'Eure Reputation mit %s %s %s sein', 'reqRepMin' => "muss mindestens", 'reqRepMax' => "darf höchstens", 'progress' => "Fortschritt", - 'provided' => "Bereitgestellt", + 'provided' => "(Bereitgestellt)", 'providedItem' => "Bereitgestellter Gegenstand", 'completion' => "Abschluss", 'description' => "Beschreibung", - 'playerSlain' => "Spieler getötet", - 'profession' => "Beruf", - 'timer' => "Zeitbegrenzung", - 'loremaster' => "Meister der Lehren", - 'suggestedPl' => "Empfohlene Spielerzahl", + 'playerSlain' => "Spieler getötet (%d)", + 'profession' => "Beruf: ", + 'timer' => "Zeitbegrenzung: ", + 'loremaster' => "Meister der Lehren: ", + 'suggestedPl' => "Empfohlene Spielerzahl: %d", 'keepsPvpFlag' => "Hält Euch im PvP", 'daily' => 'Täglich', 'weekly' => "Wöchentlich", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Aktiviert durch", 'enabledByQDesc'=> "Ihr könnt diese Quest nur annehmen, wenn eins der nachfolgenden Quests aktiv ist", 'gainsDesc' => "Bei Abschluss dieser Quest erhaltet Ihr", - 'theTitle' => 'den Titel "%s"', 'unavailable' => "Diese Quest wurde als nicht genutzt markiert und kann weder erhalten noch vollendet werden.", 'experience' => "Erfahrung", 'expConvert' => "(oder %s, wenn auf Stufe %d vollendet)", 'expConvert2' => "%s, wenn auf Stufe %d vollendet", - 'chooseItems' => "Auf Euch wartet eine dieser Belohnungen", - 'receiveItems' => "Ihr bekommt", - 'receiveAlso' => "Ihr bekommt außerdem", - 'spellCast' => "Der folgende Zauber wird auf Euch gewirkt", - 'spellLearn' => "Ihr erlernt", + 'rewardChoices' => "Auf Euch wartet eine dieser Belohnungen:", // REWARD_CHOICES + 'rewardItems' => "Ihr bekommt:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Ihr bekommt außerdem:", // REWARD_ITEMS + 'rewardSpell' => "Ihr erlernt:", // REWARD_SPELL + 'rewardAura' => "Der folgende Zauber wird auf Euch gewirkt:", // REWARD_AURA + 'rewardTradeSkill'=>"Ihr erlernt die Herstellung von:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Euch wird folgender Titel verliehen: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4Talentpunkt:Talentpunkte;", 'spellDisplayed'=> ' (%s wird angezeigt)', 'questPoolDesc' => 'Nur %d |4Quest kann:Quests können; aus diesem Tab gleichzeitig aktiv sein', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => 'Ihr werdet diesen Brief%s%s erhalten', 'mailBy' => ' von %s', 'mailIn' => " nach %s", - 'delay' => "Verzögerung", - 'sender' => "Absender", + 'delay' => "Verzögerung: %s", + 'sender' => "Absender: %s", 'untitled' => "Unbetitelter Brief #%d" ), 'pet' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index ba489eef..e250a297 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "%s cooldown", - 'difficulty' => "Difficulty", + 'difficulty' => "Difficulty: ", 'dispelType' => "Dispel type", 'duration' => "Duration", 'eventShort' => "Event: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "This world event doesn't exist.", - 'start' => "Start", - 'end' => "End", + 'start' => "Start: ", + 'end' => "End: ", 'interval' => "Interval", 'inProgress' => "Event is currently in progress", 'category' => ["Uncategorized", "Holidays", "Recurring", "Player vs. Player"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'This quest will be converted to %s if you transfer to %s.', 'questLevel' => "Level %s", 'requirements' => "Requirements", - 'reqMoney' => "Required money", // REQUIRED_MONEY + 'reqMoney' => "Required money: %s", // REQUIRED_MONEY 'money' => "Money", 'additionalReq' => "Additional requirements to obtain this quest", 'reqRepWith' => 'Your reputation with %s must be %s %s', 'reqRepMin' => "at least", 'reqRepMax' => "lower than", 'progress' => "Progress", - 'provided' => "Provided", + 'provided' => "(Provided)", 'providedItem' => "Provided item", 'completion' => "Completion", 'description' => "Description", - 'playerSlain' => "Players slain", - 'profession' => "Profession", - 'timer' => "Timer", - 'loremaster' => "Loremaster", - 'suggestedPl' => "Suggested players", + 'playerSlain' => "Players slain (%d)", + 'profession' => "Profession: ", + 'timer' => "Timer: ", + 'loremaster' => "Loremaster: ", + 'suggestedPl' => "Suggested players: %d", 'keepsPvpFlag' => "Keeps you PvP flagged", 'daily' => "Daily", 'weekly' => "Weekly", @@ -1308,17 +1308,18 @@ $lang = array( 'enabledByQ' => "Enabled by", 'enabledByQDesc'=> "This quest is available only, when one of these quests are active", 'gainsDesc' => "Upon completion of this quest you will gain", - 'theTitle' => 'the title "%s"', // partly REWARD_TITLE 'unavailable' => "This quest was marked obsolete and cannot be obtained or completed.", 'experience' => "experience", 'expConvert' => "(or %s if completed at level %d)", 'expConvert2' => "%s if completed at level %d", - 'chooseItems' => "You will be able to choose one of these rewards", // REWARD_CHOICES - 'receiveItems' => "You will receive", // REWARD_ITEMS_ONLY - 'receiveAlso' => "You will also receive", // REWARD_ITEMS - 'spellCast' => "The following spell will be cast on you", // REWARD_AURA - 'spellLearn' => "You will learn", // REWARD_SPELL - 'bonusTalents' => "%d talent |4point:points;", // partly LEVEL_UP_CHAR_POINTS + 'rewardChoices' => "You will be able to choose one of these rewards:", // REWARD_CHOICES + 'rewardItems' => "You will receive:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "You will also receive:", // REWARD_ITEMS + 'rewardSpell' => "You will learn:", // REWARD_SPELL + 'rewardAura' => "The following spell will be cast on you:", // REWARD_AURA + 'rewardTradeSkill'=>"You will learn how to create:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'You shall be granted the title: "%s"', // REWARD_TITLE + 'bonusTalents' => "%d talent |4point:points;", // partly LEVEL_UP_CHAR_POINTS 'spellDisplayed'=> ' (%s is displayed)', 'questPoolDesc' => 'Only %d |4Quest:Quests; from this tab will be available at a time', 'autoaccept' => 'Auto Accept', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => 'You will receive this letter%s%s', 'mailBy' => ' by %s', 'mailIn' => " after %s", - 'delay' => "Delay", - 'sender' => "Sender", + 'delay' => "Delay: %s", + 'sender' => "Sender: %s", 'untitled' => "Untitled Mail #%d" ), 'pet' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 84353d5f..b38316b9 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "%s de reutilización", - 'difficulty' => "Dificultad", + 'difficulty' => "Dificultad: ", 'dispelType' => "Tipo de disipación", 'duration' => "Duración", 'eventShort' => "Evento: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Este evento del mundo no existe.", - 'start' => "Empieza", - 'end' => "Termina", + 'start' => "Empieza: ", + 'end' => "Termina: ", 'interval' => "Intervalo", 'inProgress' => "El evento está en progreso actualmente", 'category' => ["Sin categoría", "Vacacionales", "Periódicos", "Jugador contra Jugador"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Esta misión será convertido a %s si lo transfieres a la %s.', 'questLevel' => 'Nivel %s', 'requirements' => 'Requisitos', - 'reqMoney' => 'Dinero necesario', + 'reqMoney' => 'Dinero necesario: %s', 'money' => 'Dinero', 'additionalReq' => "Requerimientos adicionales para obtener esta misión", 'reqRepWith' => 'Tu reputación con %s debe ser %s %s', 'reqRepMin' => "de al menos", 'reqRepMax' => "menor que", 'progress' => "Progreso", - 'provided' => "Provisto", + 'provided' => "(Provisto)", 'providedItem' => "Objeto provisto", 'completion' => "Terminación", 'description' => "Descripción", - 'playerSlain' => "Jugadores derrotados", - 'profession' => "Profesión", - 'timer' => "Tiempo", - 'loremaster' => "Maestro cultural", - 'suggestedPl' => "Jugadores sugeridos", + 'playerSlain' => "Jugadores derrotados (%d)", + 'profession' => "Profesión: ", + 'timer' => "Tiempo: ", + 'loremaster' => "Maestro cultural: ", + 'suggestedPl' => "Jugadores sugeridos: %d", 'keepsPvpFlag' => "Mantiene el JcJ activado", 'daily' => 'Diaria', 'weekly' => "Semanal", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Activada por", 'enabledByQDesc'=> "Para aceptar esta misión debes haber tener activa alguna de estas misiones", 'gainsDesc' => "Cuando completes esta misión ganarás", - 'theTitle' => 'el título "%s"', 'unavailable' => "Esta misión fue marcada como obsoleta y no puede ser obtenida o completada.", 'experience' => "experiencia", 'expConvert' => "(o %s si se completa al nivel %d)", 'expConvert2' => "%s si se completa al nivel %d", - 'chooseItems' => "Podrás elegir una de estas recompensas", - 'receiveItems' => "Recibirás", - 'receiveAlso' => "También recibirás", - 'spellCast' => "Te van a lanzar el siguiente hechizo", - 'spellLearn' => "Aprenderás", + 'rewardChoices' => "Podrás elegir una de estas recompensas:", // REWARD_CHOICES + 'rewardItems' => "Recibirás:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "También recibirás:", // REWARD_ITEMS + 'rewardSpell' => "Aprenderás:", // REWARD_SPELL + 'rewardAura' => "Te van a lanzar el siguiente hechizo:", // REWARD_AURA + 'rewardTradeSkill'=>"Aprenderás a crear:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Se te otorga el título de: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4punto:puntos; de talento", 'spellDisplayed'=> ' (mostrando %s)', 'questPoolDesc' => 'Solo %d |4misión:misiones; de esta pestaña estarán disponibles a la vez', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => "Usted recibirá esta carta%s%s", 'mailBy' => ' del %s', 'mailIn' => " después de %s", - 'delay' => "Retraso", - 'sender' => "Remitente", + 'delay' => "Retraso: %s", + 'sender' => "Remitente: %s", 'untitled' => "Correo sin título #%d" ), 'pet' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index a1f7d19a..bf728dd6 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "%s de recharge", - 'difficulty' => "Difficulté", + 'difficulty' => "Difficulté : ", 'dispelType' => "Type de dissipation", 'duration' => "Durée", 'eventShort' => "Évènement : %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Cet évènement mondial n'existe pas.", - 'start' => "Début", - 'end' => "Fin", + 'start' => "Début : ", + 'end' => "Fin : ", 'interval' => "Intervalle", 'inProgress' => "L'évènement est présentement en cours", 'category' => ["Non classés", "Vacances", "Récurrent", "Joueur ctr. Joueur"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Cette quête sera converti en %s si vous transférez en %s.', 'questLevel' => "Niveau %s", 'requirements' => "Conditions", - 'reqMoney' => "Argent requis", + 'reqMoney' => "Argent requis : %s", 'money' => "Argent", 'additionalReq' => "Conditions additionnelles requises pour obtenir cette quête", 'reqRepWith' => 'Votre reputation avec %s doît être %s %s', 'reqRepMin' => "d'au moins", 'reqRepMax' => "moins que", 'progress' => "Progrès", - 'provided' => "Fourni", + 'provided' => "(Fourni)", 'providedItem' => "Objet fourni", 'completion' => "Achèvement", 'description' => "Description", - 'playerSlain' => "Joueurs tués", - 'profession' => "Métier", - 'timer' => "Temps", - 'loremaster' => "Maitre des traditions", - 'suggestedPl' => "Joueurs suggérés", + 'playerSlain' => "Joueurs tués (%d)", + 'profession' => "Métier : ", + 'timer' => "Temps : ", + 'loremaster' => "Maitre des traditions : ", + 'suggestedPl' => "Joueurs suggérés : %d", 'keepsPvpFlag' => "Vous garde en mode JvJ", 'daily' => "Journalière", 'weekly' => "Chaque semaine", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Autorisée par", 'enabledByQDesc'=> "Vous pouvez faire cette quête seulement quand cette quête est active", 'gainsDesc' => "Lors de l'achèvement de cette quête vous gagnerez", - 'theTitle' => '"%s"', // empty on purpose! 'unavailable' => "Cette quête est marquée comme obsolète et ne peut être obtenue ou accomplie.", 'experience' => "points d'expérience", 'expConvert' => "(ou %s si completé au niveau %d)", 'expConvert2' => "%s si completé au niveau %d", - 'chooseItems' => "Vous pourrez choisir une de ces récompenses", - 'receiveItems' => "Vous recevrez", - 'receiveAlso' => "Vous recevrez également", - 'spellCast' => "Vous allez être la cible du sort suivant", - 'spellLearn' => "Vous apprendrez", + 'rewardChoices' => "Vous pourrez choisir une de ces récompenses :", // REWARD_CHOICES + 'rewardItems' => "Vous recevrez :", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Vous recevrez également :", // REWARD_ITEMS (Ainsi que :) + 'rewardSpell' => "Vous apprendrez :", // REWARD_SPELL + 'rewardAura' => "Vous allez être la cible du sort suivant :", // REWARD_AURA + 'rewardTradeSkill'=>"Vous apprendrez comment créer :", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Vous allez recevoir le titre suivant : "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4point:points; de talent", 'spellDisplayed'=> ' (%s affichés)', 'questPoolDesc' => 'Only %d |4Quest:Quests; from this tab will be available at a time', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => "Vous recevrez cette lettre%s%s", 'mailBy' => ' de %s', 'mailIn' => " après %s", - 'delay' => "Delay", - 'sender' => "Sender", + 'delay' => "Delay : %s", + 'sender' => "Sender : %s", 'untitled' => "Untitled Mail #%d" ), 'pet' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index a16699f7..8adbaec2 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "Восстановление: %s", - 'difficulty' => "Сложность", + 'difficulty' => "Сложность: ", 'dispelType' => "Тип рассеивания", 'duration' => "Длительность", 'eventShort' => "Игровое событие: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Это игровое событие не существует.", - 'start' => "Начало", - 'end' => "Конец", + 'start' => "Начало: ", + 'end' => "Конец: ", 'interval' => "[Interval]", 'inProgress' => "Событие активно в данный момент", 'category' => array("Разное", "Праздники", "Периодические", "PvP") @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Этот предмет превратится в %s, если вы перейдете за %s.', 'questLevel' => "%s-го уровня", 'requirements' => "Требования", - 'reqMoney' => "Требуется денег", + 'reqMoney' => "Требуется денег: %s", 'money' => "Деньги", 'additionalReq' => "Дополнительные условия для получения данного задания", 'reqRepWith' => 'Ваша репутация с %s должна быть %s %s', 'reqRepMin' => "не менее", 'reqRepMax' => "меньше чем", 'progress' => "Прогресс", - 'provided' => "Прилагается", + 'provided' => "(Прилагается)", 'providedItem' => "Прилагается предмет", 'completion' => "Завершение", 'description' => "Описание", - 'playerSlain' => "Убито игроков", - 'profession' => "Профессия", - 'timer' => "Таймер", - 'loremaster' => "Хранитель мудрости", - 'suggestedPl' => "Рекомендуемое количество игроков", + 'playerSlain' => "Убито игроков (%d)", + 'profession' => "Профессия: ", + 'timer' => "Таймер: ", + 'loremaster' => "Хранитель мудрости: ", + 'suggestedPl' => "Рекомендуемое количество игроков: %d", 'keepsPvpFlag' => "Включает доступность PvP", 'daily' => "Ежедневно", 'weekly' => "Раз в неделю", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Включена по", 'enabledByQDesc'=> "Вы можете получить это задание, только когда эти задания доступны", 'gainsDesc' => "По завершении этого задания, вы получите", - 'theTitle' => '"%s"', // empty on purpose! 'unavailable' => "пометили это задание как устаревшее — его нельзя получить или выполнить.", 'experience' => "опыта", 'expConvert' => "(или %s на %d-м уровне)", 'expConvert2' => "%s на %d-м уровне", - 'chooseItems' => "Вам дадут возможность выбрать одну из следующих наград", - 'receiveItems' => "Вы получите", - 'receiveAlso' => "Вы также получите", - 'spellCast' => "Следующее заклинание будет наложено на вас", - 'spellLearn' => "Вы изучите", + 'rewardChoices' => "Вы сможете выбрать одну из наград:", // REWARD_CHOICES + 'rewardItems' => "Вы получите:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Вы также получите:", // REWARD_ITEMS + 'rewardSpell' => "Вы узнаете:", // REWARD_SPELL + 'rewardAura' => "На вас будет наложено заклинание:", // REWARD_AURA + 'rewardTradeSkill'=>"Вы узнаете, как создавать:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Вам будет присвоено звание: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4очко талантов:очка талантов:очков талантов;", 'spellDisplayed'=> ' (показано: %s)', 'questPoolDesc' => 'Only %d |4Quest:Quests; from this tab will be available at a time', @@ -1439,14 +1440,14 @@ $lang = array( ) ), 'mail' => array( - 'notFound' => "This mail doesn't exist.", + 'notFound' => "[This mail doesn't exist].", 'attachment' => "[Attachment]", 'mailDelivery' => "Вы получите это письмо%s%s", 'mailBy' => ' от %s', 'mailIn' => " через %s", - 'delay' => "Delay", - 'sender' => "Sender", - 'untitled' => "Untitled Mail #%d" + 'delay' => "[Delay]: %s", + 'sender' => "[Sender]: %s", + 'untitled' => "[Untitled Mail] #%d" ), 'pet' => array( 'notFound' => "Такой породы питомцев не существует.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 2098281e..0ea4e318 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -329,7 +329,7 @@ $lang = array( 'mails' => "邮件", 'cooldown' => "%s冷却时间", - 'difficulty' => "难度", + 'difficulty' => "难度:", 'dispelType' => "驱散类型", 'duration' => "持续时间", 'eventShort' => "事件:%s", @@ -1134,8 +1134,8 @@ $lang = array( ), 'event' => array( 'notFound' => "这个世界事件不存在。", - 'start' => "开始", - 'end' => "结束", + 'start' => "开始:", + 'end' => "结束:", 'interval' => "间隔", 'inProgress' => "事件正在进行中", 'category' => ["未分类", "节日", "循环", "PvP"] @@ -1271,22 +1271,22 @@ $lang = array( '_transfer' => '这个任务将被转换到%s,如果你转移到%s。', 'questLevel' => "等级%s", 'requirements' => "要求", - 'reqMoney' => "需要金钱", + 'reqMoney' => "需要金钱:%s", 'money' => "金钱", 'additionalReq' => "获得这个任务的额外要求", 'reqRepWith' => '你%s的声望需要%s %s', 'reqRepMin' => "至少", 'reqRepMax' => "低于", 'progress' => "进行", - 'provided' => "提供的", + 'provided' => "(提供的)", 'providedItem' => "提供的物品", 'completion' => "完成", 'description' => "描述", - 'playerSlain' => "玩家被杀", - 'profession' => "专业", - 'timer' => "计时器", - 'loremaster' => "博学者", - 'suggestedPl' => "建议玩家数", + 'playerSlain' => "玩家被杀(%d)", + 'profession' => "专业:", + 'timer' => "计时器:", + 'loremaster' => "博学者:", + 'suggestedPl' => "建议玩家数:%d", 'keepsPvpFlag' => "保持你的PvP标记", 'daily' => "每日", 'weekly' => "每周", @@ -1307,16 +1307,17 @@ $lang = array( 'enabledByQ' => "启用自", 'enabledByQDesc'=> "只有当这些任务中的一个活跃时,这个任务才可用", 'gainsDesc' => "完成这个任务后,你将获得", - 'theTitle' => '头衔 "%s"', 'unavailable' => "这项任务已被标记为过时,无法获得或完成。", 'experience' => "经验", 'expConvert' => "(或%s如果在等级%d完成)", 'expConvert2' => "%s如果在等级%d完成", - 'chooseItems' => "你可以从这些奖励品中选择一件", - 'receiveItems' => "你将得到", - 'receiveAlso' => "你还将得到", - 'spellCast' => "该法术将被施放在你身上", - 'spellLearn' => "你将学会", + 'rewardChoices' => "你可以从这些奖励品中选择一件:", // REWARD_CHOICES + 'rewardItems' => "你将得到:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "你还将得到:", // REWARD_ITEMS + 'rewardSpell' => "你将学会:", // REWARD_SPELL + 'rewardAura' => "该法术将被施放在你身上:", // REWARD_AURA + 'rewardTradeSkill'=>"你将学会如何制造:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => '你将获得头衔:"%s"', // REWARD_TITLE 'bonusTalents' => "%d天赋|4点数:点数;", 'spellDisplayed'=> ' (%s 已显示)', 'questPoolDesc' => '每次只能同时提供 %d 个任务', @@ -1443,8 +1444,8 @@ $lang = array( 'mailDelivery' => '你将收到 这封信%s%s', // "你会收到这封信%s%s", 'mailBy' => '发件人:%s', 'mailIn' => "在 %s 后", - 'delay' => "延迟", - 'sender' => "寄件人", + 'delay' => "延迟:%s", + 'sender' => "寄件人:%s", 'untitled' => "无标题邮件 #%d" ), 'pet' => array( diff --git a/pages/quests.php b/pages/quests.php deleted file mode 100644 index a69f6210..00000000 --- a/pages/quests.php +++ /dev/null @@ -1,125 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->validCats = Game::$questClasses; // not allowed to set this as default - - $this->getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new QuestListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); - - $this->name = Util::ucFirst(Lang::game('quests')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if (isset($this->category[1])) - $conditions[] = ['zoneOrSort', $this->category[1]]; - else if (isset($this->category[0])) - $conditions[] = ['zoneOrSort', $this->validCats[$this->category[0]]]; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $quests = new QuestList($conditions, ['extraOpts' => $this->filterObj->extraOpts, 'calcTotal' => true]); - - $this->extendGlobalData($quests->getJSGlobals()); - - $tabData = ['data' => array_values($quests->getListviewData())]; - - if ($rCols = $this->filterObj->fiReputationCols) // never use pretty-print - $tabData['extraCols'] = '$fi_getReputationCols('.Util::toJSON($rCols, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; - else if ($this->filterObj->fiExtraCols) - $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - - // create note if search limit was exceeded - if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - else if (isset($this->category[1]) && $this->category[1] > 0) - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questgivers, '.$this->category[1].', g_zones['.$this->category[1].'], '.$this->category[1].')'; - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = [QuestList::$brickFile, $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - if (isset($this->category[1])) - array_unshift($this->title, Lang::quest('cat', $this->category[0], $this->category[1])); - else if (isset($this->category[0])) - { - $c0 = Lang::quest('cat', $this->category[0]); - array_unshift($this->title, is_array($c0) ? $c0[0] : $c0); - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - - $hubs = array( - // Quest Hubs - 3679 => 3519, 4024 => 3537, 25 => 46, 1769 => 361, - // Startzones: Horde - 132 => 1, 9 => 12, 3431 => 3430, 154 => 85, - // Startzones: Alliance - 3526 => 3524, 363 => 14, 220 => 215, 188 => 141, - // Group: Caverns of Time - 2366 => 1941, 2367 => 1941, 4100 => 1941, - // Group: Hellfire Citadell - 3562 => 3535, 3713 => 3535, 3714 => 3535, - // Group: Auchindoun - 3789 => 3688, 3790 => 3688, 3791 => 3688, 3792 => 3688, - // Group: Tempest Keep - 3847 => 3842, 3848 => 3842, 3849 => 3842, - // Group: Coilfang Reservoir - 3715 => 3905, 3716 => 3905, 3717 => 3905, - // Group: Icecrown Citadel - 4809 => 4522, 4813 => 4522, 4820 => 4522 - ); - - if (isset($this->category[1]) && isset($hubs[$this->category[1]])) - array_splice($this->path, 3, 0, $hubs[$this->category[1]]); - } -} - -?> diff --git a/pages/zone.php b/pages/zone.php index 0469ac78..03020585 100644 --- a/pages/zone.php +++ b/pages/zone.php @@ -592,7 +592,7 @@ class ZonePage extends GenericPage { $tabData = ['data' => array_values($questsLV)]; - foreach (Game::$questClasses as $parent => $children) + foreach (Game::QUEST_CLASSES as $parent => $children) { if (!in_array($this->typeId, $children)) continue; diff --git a/setup/tools/filegen/profiler.ss.php b/setup/tools/filegen/profiler.ss.php index 8adf0f32..576d057b 100644 --- a/setup/tools/filegen/profiler.ss.php +++ b/setup/tools/filegen/profiler.ss.php @@ -63,7 +63,7 @@ CLISetup::registerSetup("build", new class extends SetupScript [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_DUNGEON_FINDER | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0] ]; - foreach (Game::$questClasses as $cat2 => $cat) + foreach (Game::QUEST_CLASSES as $cat2 => $cat) { if ($cat2 < 0) continue; diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index 6343a1f8..95c1a2d7 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
@@ -17,7 +20,7 @@
brick('redButtons'); ?> -

name; ?>

+

h1; ?>

unavailable): ?>
@@ -35,80 +38,61 @@ elseif ($this->offerReward): echo $this->offerReward."\n"; endif; +$iconOffset = 0; if ($this->end || $this->objectiveList): ?> objectiveList as [$type, $data]): + switch ($type): + case 1: // just text line + echo ' \n"; + break; + case 2: // proxy npc data + ['id' => $id, 'text' => $text, 'qty' => $qty, 'proxy' => $proxies] = $data; + echo ' \n"; + break; + default: // has icon set (spell / item / ...) or unordered linked list + echo $data->renderContainer(20, $iconOffset, true); + endswitch; + endforeach; + if ($this->end): echo " \n"; endif; - if ($o = $this->objectiveList): - foreach ($o as $i => $ol): - if (isset($ol['text'])): - echo ' \n"; - elseif (!empty($ol['proxy'])): // this implies creatures - echo ' \n"; - elseif (isset($ol['typeStr'])): - if (in_array($ol['typeStr'], ['item', 'spell'])): - echo ' '; - else /* if (in_array($ol['typeStr'], ['npc', 'object', 'faction'])) */: - echo ' '; - endif; - - echo '\n"; - endif; - endforeach; - endif; - if ($this->suggestedPl): - echo ' \n"; + echo ' \n"; endif; ?>

 

'.$data."

 

'.$text.''.($qty ? ' ('.$qty.')' : '').'
\n"; + foreach ($proxies as $block): + echo "
\n"; + foreach ($block as $pId => $pName): + echo ' \n"; + endforeach; + echo "
  •  
'.$pName."
\n"; + endforeach; + echo "

 

".$this->end."

 

'.$ol['text']."

 

'.$ol['name'].$ol['extraText'].''.($ol['qty'] > 1 ? ' ('.$ol['qty'].')' : null).'
\n"; - - $block1 = array_slice($ol['proxy'], 0, ceil(count($ol['proxy']) / 2), true); - $block2 = array_slice($ol['proxy'], ceil(count($ol['proxy']) / 2), null, true); - - echo "
\n"; - foreach ($block1 as $pId => $name): - echo ' \n"; - endforeach; - echo "
  •  
'.$name."
\n"; - - if ($block2): // may be empty - echo "
\n"; - foreach ($block2 as $pId => $name): - echo ' \n"; - endforeach; - echo "
  •  
'.$name."
\n"; - endif; - - echo "
  •  
'.$ol['name'].''.($ol['extraText']).(!empty($ol['qty']) ? ' ('.$ol['qty'].')' : null)."

 

'.Lang::quest('suggestedPl').Lang::main('colon').$this->suggestedPl."

 

'.Lang::quest('suggestedPl', [$this->suggestedPl])."
providedItem): - echo "
\n"; - echo ' '.Lang::quest('providedItem').Lang::main('colon')."\n"; - echo " \n"; - echo ' '; - echo '\n"; + if ($this->providedItem): ?> +
+ +
'.$p['name'].''.($p['qty'] ? ' ('.$ol['qty'].')' : null)."
+ providedItem->renderContainer(20, $iconOffset, true); ?>
offerReward && ($this->requestItems || $this->objectives)): rewards): +if ([$spells, $items, $choice, $money] = $this->rewards): echo '

'.Lang::main('rewards')."

\n"; - if (!empty($r['choice'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('chooseItems'), 'rewards' => $r['choice'], 'offset' => $offset]); - $offset += count($r['choice']); + if ($choice): + $this->brick('rewards', ['rewTitle' => Lang::quest('rewardChoices'), 'rewards' => $choice, 'offset' => $iconOffset]); + $iconOffset += count($choice); endif; - if (!empty($r['spells'])): - if (!empty($r['choice'])): + if ($spells): + if ($choice): echo "
\n"; endif; - if (!empty($r['spells']['learn'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('spellLearn'), 'rewards' => $r['spells']['learn'], 'offset' => $offset, 'extra' => $r['spells']['extra']]); - $offset += count($r['spells']['learn']); - elseif (!empty($r['spells']['cast'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('spellCast'), 'rewards' => $r['spells']['cast'], 'offset' => $offset, 'extra' => $r['spells']['extra']]); - $offset += count($r['spells']['cast']); - endif; + $this->brick('rewards', ['rewTitle' => $spells['title'], 'rewards' => $spells['cast'], 'offset' => $iconOffset, 'extra' => $spells['extra']]); + $iconOffset += count($spells['cast']); endif; - if (!empty($r['items']) || !empty($r['money'])): - if (!empty($r['choice']) || !empty($r['spells'])): + if ($items || $money): + if ($choice || $spells): echo "
\n"; endif; - $addData = ['rewards' => !empty($r['items']) ? $r['items'] : null, 'offset' => $offset, 'extra' => !empty($r['money']) ? $r['money'] : null]; - $addData['rewTitle'] = empty($r['choice']) ? Lang::quest('receiveItems') : Lang::quest('receiveAlso'); - - $this->brick('rewards', $addData); + $this->brick('rewards', array( + 'rewTitle' => $choice ? Lang::quest('rewardAlso') : Lang::quest('rewardItems'), + 'rewards' => $items ?: null, + 'offset' => $iconOffset, + 'extra' => $money ?: null + )); endif; endif; -if ($g = $this->gains): - echo '

'.Lang::main('gains')."

\n"; - echo ' '.Lang::quest('gainsDesc').Lang::main('colon')."\n"; - echo "
    \n"; - - if (!empty($g['xp'])): - echo '
  • '.Lang::nf($g['xp']).' '.Lang::quest('experience')."
  • \n"; +if ([$xp, $rep, $title, $tp] = $this->gains): +?> +

    + +
      +
      '.Lang::nf($xp).' '.Lang::quest('experience')."
      \n"; endif; - if (!empty($g['rep'])): - foreach ($g['rep'] as $r): - if ($r['qty'][1] && User::isInGroup(U_GROUP_EMPLOYEE)) - $qty = $r['qty'][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($r['qty'][1] > 0 ? '+' : '').$r['qty'][1]); - else - $qty = array_sum($r['qty']); - - echo '
    • '.($r['qty'][0] < 0 ? ''.$qty.'' : $qty).' '.Lang::npc('repWith').' '.$r['name']."
    • \n"; + if ($rep): + foreach ($rep as $r): + echo '
    • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name']."
    • \n"; endforeach; endif; - if (!empty($g['title'])): - echo '
    • '.Lang::quest('theTitle', [$g['title']])."
    • \n"; + if ($title): + echo '
    • '.Lang::quest('rewardTitle', $title)."
    • \n"; endif; - if (!empty($g['tp'])): - echo '
    • '.Lang::quest('bonusTalents', [$g['tp']])."
    • \n"; + if ($tp): + echo '
    • '.Lang::quest('bonusTalents', [$tp])."
    • \n"; endif; echo "
    \n"; endif; -$this->brick('mail', ['offset' => ++$offset]); +$this->brickIf($this->mail, 'mail', ['offset' => ++$iconOffset]); -if (!empty($this->transfer)): +if ($this->transfer): echo "
    "; echo "
    \n ".$this->transfer."\n"; endif; @@ -213,7 +189,7 @@ endif;
brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index ef5fca5b..e7de3d19 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.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,41 +13,44 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [3]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); ?> - -
+
+
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

h1; ?>

+
- + - + @@ -54,11 +58,7 @@ endforeach;
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
 />  /> />  />
 /> - /> /> - /> - +
    /> - /> /> - />
 
@@ -66,7 +66,7 @@ endforeach;
- /> /> + /> />
@@ -80,7 +80,7 @@ endforeach;
-brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From d66a863f55fc5f93192bb207ac26220e4eae9109 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 18:07:35 +0200 Subject: [PATCH 675/957] Template/Update (Part 32) * convert dbtype 'event' --- endpoints/event/event.php | 337 ++++++++++++++++++++++++ endpoints/event/event_power.php | 79 ++++++ endpoints/events/events.php | 96 +++++++ includes/dbtypes/worldevent.class.php | 11 +- localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- localization/locale_zhcn.php | 2 +- pages/event.php | 365 -------------------------- pages/events.php | 109 -------- 12 files changed, 519 insertions(+), 490 deletions(-) create mode 100644 endpoints/event/event.php create mode 100644 endpoints/event/event_power.php create mode 100644 endpoints/events/events.php delete mode 100644 pages/event.php delete mode 100644 pages/events.php diff --git a/endpoints/event/event.php b/endpoints/event/event.php new file mode 100644 index 00000000..5e1530b8 --- /dev/null +++ b/endpoints/event/event.php @@ -0,0 +1,337 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new WorldEventList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('event'), Lang::event('notFound')); + + $this->h1 = $this->subject->getField('name', true); + $this->dates = array( + 'firstDate' => $this->subject->getField('startTime'), + 'lastDate' => $this->subject->getField('endTime'), + 'length' => $this->subject->getField('length'), + 'rec' => $this->subject->getField('occurence') + ); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_holidayId = $this->subject->getField('holidayId'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = match ($this->subject->getField('scheduleType')) + { + -1 => 1, + 0, 1 => 2, + 2 => 3, + '' => 0, + default => 0 + }; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucWords(Lang::game('event'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // boss + if ($_ = $this->subject->getField('bossCreature')) + { + $this->extendGlobalIds(Type::NPC, $_); + $infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; + } + + // display internal id to staff + if (User::isInGroup(U_GROUP_STAFF)) + $infobox[] = 'Event-Id'.Lang::main('colon').$this->typeId; + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + + /****************/ + /* Main Content */ + /****************/ + + // no entry in ?_articles? use default HolidayDescription + if ($_holidayId && empty($this->article)) + $this->article = new Markup($this->subject->getField('description', true), ['dbpage' => true]); + + if ($_holidayId) + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'event=', $_holidayId); + + $this->headIcons = [$this->subject->getField('iconString')]; + $this->redButtons = array( + BUTTON_WOWHEAD => $_holidayId > 0, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + parent::generate(); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: npcs + if ($npcIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(ec.`eventEntry` > 0, 1, 0) AS "added" FROM creature c, game_event_creature ec WHERE ec.`guid` = c.`guid` AND ABS(ec.`eventEntry`) = ?d', $this->typeId)) + { + $creatures = new CreatureList(array(['id', array_keys($npcIds)])); + if (!$creatures->error) + { + $data = $creatures->getListviewData(); + foreach ($data as &$d) + $d['method'] = $npcIds[$d['id']]; + + $tabData = ['data' => $data]; + + if ($_holidayId && CreatureListFilter::getCriteriaIndex(38, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=38;crs='.$_holidayId.';crv=0'); + + $this->result->addDataLoader('zones'); // req. by secondary tooltip in this tab + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + } + } + + // tab: objects + if ($objectIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(eg.`eventEntry` > 0, 1, 0) AS "added" FROM gameobject g, game_event_gameobject eg WHERE eg.`guid` = g.`guid` AND ABS(eg.`eventEntry`) = ?d', $this->typeId)) + { + $objects = new GameObjectList(array(['id', array_keys($objectIds)])); + if (!$objects->error) + { + $data = $objects->getListviewData(); + foreach ($data as &$d) + $d['method'] = $objectIds[$d['id']]; + + $tabData = ['data' => $data]; + + if ($_holidayId && GameObjectListFilter::getCriteriaIndex(16, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=16;crs='.$_holidayId.';crv=0'); + + $this->result->addDataLoader('zones'); // req. by secondary tooltip in this tab + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + } + } + + // tab: achievements + if ($_ = $this->subject->getField('achievementCatOrId')) + { + $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; + $acvs = new AchievementList($condition); + if (!$acvs->error) + { + $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $tabData = array( + 'data' => $acvs->getListviewData(), + 'visibleCols' => ['category'] + ); + + if ($_holidayId && AchievementListFilter::getCriteriaIndex(11, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?achievements&filter=cr=11;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, AchievementList::$brickFile)); + } + } + + $itemCnd = []; + if ($_holidayId) + { + $itemCnd = array( + 'OR', + ['eventId', $this->typeId], // direct requirement on item + ); + + // tab: quests (by table, go & creature) + $quests = new QuestList(array(['eventId', $this->typeId])); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $tabData = ['data'=> $quests->getListviewData()]; + + if (QuestListFilter::getCriteriaIndex(33, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); + + $questItems = []; + foreach (array_column($quests->rewards, Type::ITEM) as $arr) + $questItems = array_merge($questItems, array_keys($arr)); + + foreach (array_column($quests->choices, Type::ITEM) as $arr) + $questItems = array_merge($questItems, array_keys($arr)); + + foreach (array_column($quests->requires, Type::ITEM) as $arr) + $questItems = array_merge($questItems, $arr); + + if ($questItems) + $itemCnd[] = ['id', $questItems]; + } + } + + // items from creature + if ($npcIds && !$creatures->error) + { + // vendor + $cIds = $creatures->getFoundIDs(); + if ($sells = DB::World()->selectCol( + 'SELECT `item` FROM npc_vendor nv WHERE `entry` IN (?a) UNION + SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` IN (?a) UNION + SELECT `item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` IN (?a)', + $cIds, $cIds, $cIds + )) + $itemCnd[] = ['id', $sells]; + } + + // tab: items + // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot + if ($itemCnd) + { + $eventItems = new ItemList($itemCnd); + if (!$eventItems->error) + { + $this->extendGlobalData($eventItems->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = ['data'=> $eventItems->getListviewData()]; + + if ($_holidayId && ItemListFilter::getCriteriaIndex(160, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=160;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + } + + // tab: see also (event conditions) + if ($rel = DB::World()->selectCol('SELECT IF(`eventEntry` = `prerequisite_event`, NULL, IF(`eventEntry` = ?d, `prerequisite_event`, -`eventEntry`)) FROM game_event_prerequisite WHERE `prerequisite_event` = ?d OR `eventEntry` = ?d', $this->typeId, $this->typeId, $this->typeId)) + { + if (array_filter($rel, fn($x) => $x === null)) + trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); + + if ($seeAlso = array_filter($rel, fn($x) => $x > 0)) + { + $relEvents = new WorldEventList(array(['id', $seeAlso])); + $this->extendGlobalData($relEvents->getJSGlobals()); + $relData = $relEvents->getListviewData(); + foreach ($relEvents->getFoundIDs() as $id) + Conditions::extendListviewRow($relData[$id], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $this->typeId]); + + $this->extendGlobalData($this->subject->getJSGlobals()); + $d = $this->subject->getListviewData(); + foreach ($rel as $r) + if ($r > 0) + if (Conditions::extendListviewRow($d[$this->typeId], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $r])) + $this->extendGlobalIds(Type::WORLDEVENT, $r); + + $tabData = array( + 'data' => array_merge($relData, $d), + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso', + 'hiddenCols' => ['date'], + 'extraCols' => ['$Listview.extraCols.condition'] + ); + $this->lvTabs->addListviewTab(new Listview($tabData, WorldEventList::$brickFile)); + } + } + + // tab: condition for + $cnd = new Conditions(); + $cnd->getByCondition(Type::WORLDEVENT, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + } + + // update dates to now() + public static function tabsHook(Template\PageTemplate &$pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if (is_object($listview) && $listview?->getTemplate() == 'holiday') + WorldEventList::updateListview($listview); + } + + /* finalize infobox */ + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$markup) : void + { + WorldEventList::updateDates($pt->dates, $start, $end, $rec); + $infobox = []; + + // start + if ($start) + $infobox[] = Lang::event('start').date(Lang::main('dateFmtLong'), $start); + + // end + if ($end) + $infobox[] = Lang::event('end').date(Lang::main('dateFmtLong'), $end); + + // interval + if ($rec > 0) + $infobox[] = Lang::event('interval').Util::formatTime($rec * 1000); + + // in progress + if ($start < time() && $end > time()) + $infobox[] = '[span class=q2]'.Lang::event('inProgress').'[/span]'; + + if ($infobox && !$markup) + $markup = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + else if ($markup) + foreach ($infobox as $ib) + $markup->addItem($ib); + } +} + +?> diff --git a/endpoints/event/event_power.php b/endpoints/event/event_power.php new file mode 100644 index 00000000..48c75683 --- /dev/null +++ b/endpoints/event/event_power.php @@ -0,0 +1,79 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private array $dates = []; + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $worldevent = new WorldEventList(array(['id', $this->typeId])); + if ($worldevent->error) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $icon = $worldevent->getField('iconString'); + if ($icon == 'trade_engineering') + $icon = null; + + $opts = array( + 'name' => $worldevent->getField('name', true), + 'tooltip' => $worldevent->renderTooltip(), + 'icon' => $icon + ); + + $this->dates = array( + 'firstDate' => $worldevent->getField('startTime'), + 'lastDate' => $worldevent->getField('endTime'), + 'length' => $worldevent->getField('length'), + 'rec' => $worldevent->getField('occurence') + ); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay'], $this->dates); + } + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } + + public static function onBeforeDisplay(string $tooltip, array $dates) : string + { + // update dates to now() + WorldEventList::updateDates($dates, $start, $end); + + return sprintf( + $tooltip, + $start ? date(Lang::main('dateFmtLong'), $start) : null, + $end ? date(Lang::main('dateFmtLong'), $end) : null + ); + } +} + +?> diff --git a/endpoints/events/events.php b/endpoints/events/events.php new file mode 100644 index 00000000..2ae4a9b9 --- /dev/null +++ b/endpoints/events/events.php @@ -0,0 +1,96 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('events')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::event('category')[$this->category[0]]); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $condition = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $condition[] = match ($this->category[0]) + { + 1 => ['h.scheduleType', -1], + 2 => ['h.scheduleType', [0, 1]], + 3 => ['h.scheduleType', 2], + default => ['e.holidayId', 0] // also cat 0 + }; + + $events = new WorldEventList($condition); + $this->extendGlobalData($events->getJSGlobals()); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $events->getListviewData()], WorldEventList::$brickFile)); + + if ($_ = array_filter($events->getListviewData(), fn($x) => $x['category'] > 0)) + $this->lvTabs->addListviewTab(new Listview(['data' => $_, 'hideCount' => 1], 'calendar')); + + parent::generate(); + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + } + + // recalculate dates with now() + public static function tabsHook(Template\PageTemplate &$pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if (is_object($listview) && ($listview?->getTemplate() == 'holiday' || $listview?->getTemplate() == 'holidaycal')) + WorldEventList::updateListview($listview); + } +} + +?> diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php index 18ba67c6..831b57c3 100644 --- a/includes/dbtypes/worldevent.class.php +++ b/includes/dbtypes/worldevent.class.php @@ -113,7 +113,7 @@ class WorldEventList extends DBTypeList } } - public function getListviewData(bool $forNow = false) : array + public function getListviewData() : array { $data = []; @@ -132,15 +132,6 @@ class WorldEventList extends DBTypeList ); } - if ($forNow) - { - foreach ($data as &$d) - { - self::updateDates($d['_date'], $d['startDate'], $d['endDate'], $d['rec']); - unset($d['_date']); - } - } - return $data; } diff --git a/localization/locale_dede.php b/localization/locale_dede.php index ccf542bb..9369227d 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Dieses Weltereignis existiert nicht.", 'start' => "Anfang: ", 'end' => "Ende: ", - 'interval' => "Intervall", + 'interval' => "Intervall: ", 'inProgress' => "Ereignis findet gerade statt", 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] ), diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e250a297..4628acc2 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "This world event doesn't exist.", 'start' => "Start: ", 'end' => "End: ", - 'interval' => "Interval", + 'interval' => "Interval: ", 'inProgress' => "Event is currently in progress", 'category' => ["Uncategorized", "Holidays", "Recurring", "Player vs. Player"] ), diff --git a/localization/locale_eses.php b/localization/locale_eses.php index b38316b9..82ba9a10 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Este evento del mundo no existe.", 'start' => "Empieza: ", 'end' => "Termina: ", - 'interval' => "Intervalo", + 'interval' => "Intervalo: ", 'inProgress' => "El evento está en progreso actualmente", 'category' => ["Sin categoría", "Vacacionales", "Periódicos", "Jugador contra Jugador"] ), diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index bf728dd6..96e90a94 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Cet évènement mondial n'existe pas.", 'start' => "Début : ", 'end' => "Fin : ", - 'interval' => "Intervalle", + 'interval' => "Intervalle : ", 'inProgress' => "L'évènement est présentement en cours", 'category' => ["Non classés", "Vacances", "Récurrent", "Joueur ctr. Joueur"] ), diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 8adbaec2..930b776a 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Это игровое событие не существует.", 'start' => "Начало: ", 'end' => "Конец: ", - 'interval' => "[Interval]", + 'interval' => "[Interval]: ", 'inProgress' => "Событие активно в данный момент", 'category' => array("Разное", "Праздники", "Периодические", "PvP") ), diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 0ea4e318..a5c5912c 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1136,7 +1136,7 @@ $lang = array( 'notFound' => "这个世界事件不存在。", 'start' => "开始:", 'end' => "结束:", - 'interval' => "间隔", + 'interval' => "间隔:", 'inProgress' => "事件正在进行中", 'category' => ["未分类", "节日", "循环", "PvP"] ), diff --git a/pages/event.php b/pages/event.php deleted file mode 100644 index 2af3aefb..00000000 --- a/pages/event.php +++ /dev/null @@ -1,365 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; - - private $powerTpl = '$WowheadPower.registerHoliday(%d, %d, %s);'; - private $hId = 0; - private $eId = 0; - - public function __construct($pageCall, $id) - { - parent::__construct($pageCall, $id); - - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - $this->typeId = intVal($id); - - $this->subject = new WorldEventList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('event'), Lang::event('notFound')); - - $this->hId = $this->subject->getField('holidayId'); - $this->eId = $this->typeId; - $this->name = $this->subject->getField('name', true); - $this->dates = array( - 'firstDate' => $this->subject->getField('startTime'), - 'lastDate' => $this->subject->getField('endTime'), - 'length' => $this->subject->getField('length'), - 'rec' => $this->subject->getField('occurence') - ); - } - - protected function generatePath() - { - switch ($this->subject->getField('scheduleType')) - { - case '': $this->path[] = 0; break; - case -1: $this->path[] = 1; break; - case 0: - case 1: $this->path[] = 2; break; - case 2: $this->path[] = 3; break; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('event'))); - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); - - /***********/ - /* Infobox */ - /***********/ - - $this->infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // boss - if ($_ = $this->subject->getField('bossCreature')) - { - $this->extendGlobalIds(Type::NPC, $_); - $this->infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; - } - - // display internal id to staff - if (User::isInGroup(U_GROUP_STAFF)) - $this->infobox[] = 'Event-Id'.Lang::main('colon').$this->eId; - - /****************/ - /* Main Content */ - /****************/ - - // no entry in ?_articles? use default HolidayDescription - if ($this->hId && empty($this->article)) - $this->article = ['text' => Util::jsEscape($this->subject->getField('description', true)), 'params' => []]; - - $this->headIcons = [$this->subject->getField('iconString')]; - $this->redButtons = array( - BUTTON_WOWHEAD => $this->hId > 0, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - /**************/ - /* Extra Tabs */ - /**************/ - - $hasFilter = in_array($this->hId, [372, 283, 285, 353, 420, 400, 284, 201, 374, 409, 141, 324, 321, 424, 335, 327, 341, 181, 404, 398, 301]); - - // tab: npcs - if ($npcIds = DB::World()->selectCol('SELECT id AS ARRAY_KEY, IF(ec.eventEntry > 0, 1, 0) AS added FROM creature c, game_event_creature ec WHERE ec.guid = c.guid AND ABS(ec.eventEntry) = ?d', $this->eId)) - { - $creatures = new CreatureList(array(['id', array_keys($npcIds)])); - if (!$creatures->error) - { - $data = $creatures->getListviewData(); - foreach ($data as &$d) - $d['method'] = $npcIds[$d['id']]; - - $tabData = ['data' => array_values($data)]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=38;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [CreatureList::$brickFile, $tabData]; - } - } - - // tab: objects - if ($objectIds = DB::World()->selectCol('SELECT id AS ARRAY_KEY, IF(eg.eventEntry > 0, 1, 0) AS added FROM gameobject g, game_event_gameobject eg WHERE eg.guid = g.guid AND ABS(eg.eventEntry) = ?d', $this->eId)) - { - $objects = new GameObjectList(array(['id', array_keys($objectIds)])); - if (!$objects->error) - { - $data = $objects->getListviewData(); - foreach ($data as &$d) - $d['method'] = $objectIds[$d['id']]; - - $tabData = ['data' => array_values($data)]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=16;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [GameObjectList::$brickFile, $tabData]; - } - } - - // tab: achievements - if ($_ = $this->subject->getField('achievementCatOrId')) - { - $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; - $acvs = new AchievementList($condition); - if (!$acvs->error) - { - $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $tabData = array( - 'data' => array_values($acvs->getListviewData()), - 'visibleCols' => ['category'] - ); - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?achievements&filter=cr=11;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [AchievementList::$brickFile, $tabData]; - } - } - - $itemCnd = []; - if ($this->hId) - { - $itemCnd = array( - 'OR', - ['eventId', $this->eId], // direct requirement on item - ); - - // tab: quests (by table, go & creature) - $quests = new QuestList(array(['eventId', $this->eId])); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $tabData = ['data'=> array_values($quests->getListviewData())]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [QuestList::$brickFile, $tabData]; - - $questItems = []; - foreach (array_column($quests->rewards, Type::ITEM) as $arr) - $questItems = array_merge($questItems, array_keys($arr)); - - foreach (array_column($quests->choices, Type::ITEM) as $arr) - $questItems = array_merge($questItems, array_keys($arr)); - - foreach (array_column($quests->requires, Type::ITEM) as $arr) - $questItems = array_merge($questItems, $arr); - - if ($questItems) - $itemCnd[] = ['id', $questItems]; - } - } - - // items from creature - if ($npcIds && !$creatures->error) - { - // vendor - $cIds = $creatures->getFoundIDs(); - if ($sells = DB::World()->selectCol( - 'SELECT `item` FROM npc_vendor nv WHERE `entry` IN (?a) UNION - SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_Vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` IN (?a) UNION - SELECT `item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` IN (?a)', - $cIds, $cIds, $cIds - )) - $itemCnd[] = ['id', $sells]; - } - - // tab: items - // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot - if ($itemCnd) - { - $eventItems = new ItemList($itemCnd); - if (!$eventItems->error) - { - $this->extendGlobalData($eventItems->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = ['data'=> array_values($eventItems->getListviewData())]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=160;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [ItemList::$brickFile, $tabData]; - } - } - - // tab: see also (event conditions) - if ($rel = DB::World()->selectCol('SELECT IF(eventEntry = prerequisite_event, NULL, IF(eventEntry = ?d, prerequisite_event, -eventEntry)) FROM game_event_prerequisite WHERE prerequisite_event = ?d OR eventEntry = ?d', $this->eId, $this->eId, $this->eId)) - { - $list = []; - array_walk($rel, function($v, $k) use (&$list) { - if ($v > 0) - $list[] = $v; - else if ($v === null) - trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); - }); - - if ($list) - { - $relEvents = new WorldEventList(array(['id', $list])); - $this->extendGlobalData($relEvents->getJSGlobals()); - $relData = $relEvents->getListviewData(); - foreach ($relEvents->getFoundIDs() as $id) - Conditions::extendListviewRow($relData[$id], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $this->eId]); - - $this->extendGlobalData($this->subject->getJSGlobals()); - $d = $this->subject->getListviewData(); - foreach ($rel as $r) - if ($r > 0) - if (Conditions::extendListviewRow($d[$this->eId], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $r])) - $this->extendGlobalIds(Type::WORLDEVENT, $r); - - $relData = array_merge($relData, $d); - - $this->lvTabs[] = [WorldEventList::$brickFile, array( - 'data' => array_values($relData), - 'id' => 'see-also', - 'name' => '$LANG.tab_seealso', - 'hiddenCols' => ['date'], - 'extraCols' => ['$Listview.extraCols.condition'] - )]; - } - } - - // tab: condition for - $cnd = new Conditions(); - $cnd->getByCondition(Type::WORLDEVENT, $this->typeId)->prepare(); - if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) - { - $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - } - - protected function generateTooltip() : string - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); - - if ($this->subject->getField('iconString') != 'trade_engineering') - $power->icon = rawurlencode($this->subject->getField('iconString', true, true)); - - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - } - - return sprintf($this->powerTpl, $this->typeId, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); - } - - protected function postCache() - { - // update dates to now() - WorldEventList::updateDates($this->dates, $start, $end, $rec); - - if ($this->mode == CACHE_TYPE_TOOLTIP) - { - return array( - date(Lang::main('dateFmtLong'), $start), - date(Lang::main('dateFmtLong'), $end) - ); - } - else - { - if ($this->hId) - $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'event', $this->hId); - - /********************/ - /* finalize infobox */ - /********************/ - - // start - if ($start) - array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $start)); - - // end - if ($end) - array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $end)); - - // occurence - if ($rec > 0) - array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($rec * 1000)); - - // in progress - if ($start < time() && $end > time()) - array_push($this->infobox, '[span class=q2]'.Lang::event('inProgress').'[/span]'); - - $this->infobox = '[ul][li]'.implode('[/li][li]', $this->infobox).'[/li][/ul]'; - - /***************************/ - /* finalize related events */ - /***************************/ - - foreach ($this->lvTabs as &$view) - { - if ($view[0] != WorldEventList::$brickFile) - continue; - - foreach ($view[1]['data'] as &$data) - { - WorldEventList::updateDates($data['_date'], $start, $end, $rec); - unset($data['_date']); - $data['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : false; - $data['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : false; - $data['rec'] = $rec; - } - } - } - } -} - -?> diff --git a/pages/events.php b/pages/events.php deleted file mode 100644 index 4ed714c1..00000000 --- a/pages/events.php +++ /dev/null @@ -1,109 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('events')); - } - - protected function generateContent() - { - $condition = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - switch ($this->category[0]) - { - case 0: $condition[] = ['e.holidayId', 0]; break; - case 1: $condition[] = ['h.scheduleType', -1]; break; - case 2: $condition[] = ['h.scheduleType', [0, 1]]; break; - case 3: $condition[] = ['h.scheduleType', 2]; break; - } - } - - $events = new WorldEventList($condition); - $this->extendGlobalData($events->getJSGlobals()); - - foreach ($events->iterate() as $__) - if ($d = $events->getField('requires')) - $this->dependency[$events->id] = $d; - - $data = array_values($events->getListviewData()); - - $this->lvTabs[] = [WorldEventList::$brickFile, ['data' => $data]]; - - if ($_ = array_values(array_filter($data, function($x) {return $x['category'] > 0;}))) - { - $this->lvTabs[] = ['calendar', array( - 'data' => $_, - 'hideCount' => 1 - )]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::event('category')[$this->category[0]]); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } - - protected function postCache() - { - // recalculate dates with now() - foreach ($this->lvTabs as &$views) - { - foreach ($views[1]['data'] as &$data) - { - // is a followUp-event - if (!empty($this->dependency[$data['id']])) - { - $data['startDate'] = $data['endDate'] = false; - unset($data['_date']); - continue; - } - - WorldEventList::updateDates($data['_date'], $start, $end, $rec); - unset($data['_date']); - $data['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : false; - $data['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : false; - $data['rec'] = $rec; - } - } - } -} - -?> From 3f7f522d5087e2fd5de50e0e43ee0322d3995580 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 18:49:27 +0200 Subject: [PATCH 676/957] Template/Update (Part 33) * convert dbtype 'zone' --- {pages => endpoints/zone}/zone.php | 377 ++++++++++++++--------------- endpoints/zones/zones.php | 188 ++++++++++++++ pages/zones.php | 185 -------------- 3 files changed, 374 insertions(+), 376 deletions(-) rename {pages => endpoints/zone}/zone.php (72%) create mode 100644 endpoints/zones/zones.php delete mode 100644 pages/zones.php diff --git a/pages/zone.php b/endpoints/zone/zone.php similarity index 72% rename from pages/zone.php rename to endpoints/zone/zone.php index 03020585..548b4a0d 100644 --- a/pages/zone.php +++ b/endpoints/zone/zone.php @@ -6,39 +6,68 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 6: Zone g_initPath() -// tabId 0: Database g_initHeader() -class ZonePage extends GenericPage +class ZoneBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $path = [0, 6]; - protected $tabId = 0; - protected $type = Type::ZONE; - protected $typeId = 0; - protected $tpl = 'detail-page-generic'; - protected $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js']]; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $zoneMusic = []; + protected string $template = 'detail-page-generic'; + protected string $pageName = 'zone'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 6]; - public function __construct($pageCall, $id) + protected array $dataLoader = ['zones']; + protected array $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js']]; + + public int $type = Type::ZONE; + public int $typeId = 0; + public array $zoneMusic = []; + public ?string $expansion = null; + + private ZoneList $subject; + + public function __construct(string $id) { - $this->typeId = intVal($id); + parent::__construct($id); - parent::__construct($pageCall, $id); - - $this->subject = new ZoneList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('zone'), Lang::zone('notFound')); - - $this->name = $this->subject->getField('name', true); + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; } - protected function generateContent() + protected function generate() : void { - $this->addScript([SC_JS_FILE, '?data=zones']); + $this->subject = new ZoneList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('zone'), Lang::zone('notFound')); - $parentArea = $this->subject->getField('parentArea'); + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_parentArea = $this->subject->getField('parentArea'); + $_type = $this->subject->getField('type'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('category'); + + if (in_array($this->subject->getField('category'), [MAP_TYPE_DUNGEON, MAP_TYPE_RAID])) + $this->breadcrumb[] = $this->subject->getField('expansion'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('zone'))); /***********/ @@ -63,11 +92,11 @@ class ZonePage extends GenericPage $infobox = array_merge($infobox, $topRows); // City - if ($this->subject->getField('flags') & 0x8 && !$parentArea) + if ($this->subject->getField('flags') & AREA_FLAG_SLAVE_CAPITAL && !$_parentArea) $infobox[] = Lang::zone('city'); // Auto repop - if ($this->subject->getField('flags') & 0x1000 && !$parentArea) + if ($this->subject->getField('flags') & AREA_FLAG_NEED_FLY && !$_parentArea) $infobox[] = Lang::zone('autoRez'); // Level @@ -83,7 +112,7 @@ class ZonePage extends GenericPage if ($_ = $this->subject->getField('levelReq')) { if ($__ = $this->subject->getField('levelReqLFG')) - $buff = sprintf(Lang::zone('reqLevels'), $_, $__); + $buff = Lang::zone('reqLevels', [$_, $__]); else $buff = Lang::main('_reqLevel').Lang::main('colon').$_; @@ -92,12 +121,12 @@ class ZonePage extends GenericPage // Territory $faction = $this->subject->getField('faction'); - $wrap = match ((int)$faction) + $wrap = match ($faction) { - 0 => '[span class=icon-alliance]%s[/span]', - 1 => '[span class=icon-horde]%s[/span]', - 4, 5 => '[span class=icon-ffa]%s[/span]', - default => '%s' + TEAM_ALLIANCE => '[span class=icon-alliance]%s[/span]', + TEAM_HORDE => '[span class=icon-horde]%s[/span]', + 4, 5 => '[span class=icon-ffa]%s[/span]', + default => '%s' }; $infobox[] = Lang::zone('territory').sprintf($wrap, Lang::zone('territories', $faction)); @@ -132,6 +161,8 @@ class ZonePage extends GenericPage $infobox[] = Lang::concat($_, Lang::CONCAT_NONE, fn($x) => '[race='.$x.']').' '.Lang::race('startZone'); } + parent::generate(); // calls applyGlobals .. probably too early here, but addMoveLocationMenu requires PageTemplate to be initialized + // location (if instance) if ($pa = DB::Aowow()->selectRow('SELECT `areaId`, `posX`, `posY`, `floor` FROM ?_spawns WHERE `type`= ?d AND `typeId` = ?d ', Type::ZONE, $this->typeId)) { @@ -160,12 +191,15 @@ class ZonePage extends GenericPage if ($botRows = array_filter($quickFactsRows, fn($x) => $x > 0, ARRAY_FILTER_USE_KEY)) $infobox = array_merge($infobox, $botRows); + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + /****************/ /* Main Content */ /****************/ - $addToSOM = function ($what, $entry) use (&$som) + $addToSOM = function (string $what, array $entry) use (&$som) : void { // entry always contains: type, id, name, level, coords[] if (!isset($som[$what][$entry['name']])) // not found yet @@ -188,10 +222,10 @@ class ZonePage extends GenericPage } }; - if ($parentArea) + if ($_parentArea) { - $this->extraText = sprintf(Lang::zone('zonePartOf'), $parentArea); - $this->extendGlobalIds(Type::ZONE, $parentArea); + $this->extraText = new Markup(Lang::zone('zonePartOf', [$_parentArea]), ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + $this->extendGlobalIds(Type::ZONE, $_parentArea); } // we cannot fetch spawns via lists. lists are grouped by entry @@ -219,13 +253,22 @@ class ZonePage extends GenericPage } // see if we can actually display a map - $hasMap = file_exists('static/images/wow/maps/'.Lang::getLocale()->json().'/normal/'.$this->typeId.'.jpg'); - if (!$hasMap) // try multilayered - $hasMap = file_exists('static/images/wow/maps/'.Lang::getLocale()->json().'/normal/'.$this->typeId.'-1.jpg'); - if (!$hasMap) // try english fallback - $hasMap = file_exists('static/images/wow/maps/enus/normal/'.$this->typeId.'.jpg'); - if (!$hasMap) // try english fallback, multilayered - $hasMap = file_exists('static/images/wow/maps/enus/normal/'.$this->typeId.'-1.jpg'); + $mapFilePath = 'static/images/wow/maps/%s/normal/%d%s.jpg'; + $options = array( + [Lang::getLocale()->json(), ''], // default case + [Lang::getLocale()->json(), '-1'], // try multifloor + ['enus', ''], // try english fallback + ['enus', '-1'] // try english fallback, multifloor + ); + $hasMap = false; + foreach ($options as [$lang, $floor]) + { + if (!file_exists(sprintf($mapFilePath, $lang, $this->typeId, $floor))) + continue; + + $hasMap = true; + break; + } if ($hasMap) { @@ -238,33 +281,16 @@ class ZonePage extends GenericPage $n = Util::localizedString($tpl, 'name'); - $what = ''; - switch ($tpl['typeCat']) + $what = match ((int)$tpl['typeCat']) { - case -3: - $what = 'herb'; - break; - case -4: - $what = 'vein'; - break; - case 9: - $what = 'book'; - break; - case 25: - $what = 'pool'; - break; - case 0: - if ($tpl['type'] == 19) - $what = 'mail'; - break; - case -6: - if ($tpl['spellFocusId'] == 1) - $what = 'anvil'; - else if ($tpl['spellFocusId'] == 3) - $what = 'forge'; - - break; - } + -3 => 'herb', + -4 => 'vein', + 9 => 'book', + 25 => 'pool', + 0 => $tpl['type'] == 19 ? 'mail' : '', + -6 => $tpl['spellFocusId'] == 1 ? 'anvil' : ($tpl['spellFocusId'] == 3 ? 'forge' : ''), + default => '' + }; if ($what) { @@ -339,36 +365,33 @@ class ZonePage extends GenericPage $n = Util::localizedString($tpl, 'name'); $sn = Util::localizedString($tpl, 'subname'); - $what = ''; - if ($tpl['npcflag'] & NPC_FLAG_REPAIRER) - $what = 'repair'; - else if ($tpl['npcflag'] & NPC_FLAG_AUCTIONEER) - $what = 'auctioneer'; - else if ($tpl['npcflag'] & NPC_FLAG_BANKER) - $what = 'banker'; - else if ($tpl['npcflag'] & NPC_FLAG_BATTLEMASTER) - $what = 'battlemaster'; - else if ($tpl['npcflag'] & NPC_FLAG_INNKEEPER) - $what = 'innkeeper'; - else if ($tpl['npcflag'] & NPC_FLAG_TRAINER) - $what = 'trainer'; - else if ($tpl['npcflag'] & NPC_FLAG_VENDOR) - $what = 'vendor'; - else if ($tpl['npcflag'] & NPC_FLAG_FLIGHT_MASTER) - { - $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; - $what = 'flightmaster'; - } - else if ($tpl['npcflag'] & NPC_FLAG_STABLE_MASTER) - $what = 'stablemaster'; - else if ($tpl['npcflag'] & NPC_FLAG_GUILD_MASTER) - $what = 'guildmaster'; - else if ($tpl['npcflag'] & (NPC_FLAG_SPIRIT_HEALER | NPC_FLAG_SPIRIT_GUIDE)) - $what = 'spirithealer'; - else if ($creatureSpawns->isBoss()) + $flagsMap = array( + NPC_FLAG_REPAIRER => 'repair', + NPC_FLAG_AUCTIONEER => 'auctioneer', + NPC_FLAG_BANKER => 'banker', + NPC_FLAG_BATTLEMASTER => 'battlemaster', + NPC_FLAG_INNKEEPER => 'innkeeper', + NPC_FLAG_TRAINER => 'trainer', + NPC_FLAG_VENDOR => 'vendor', + NPC_FLAG_FLIGHT_MASTER => 'flightmaster', + NPC_FLAG_STABLE_MASTER => 'stablemaster', + NPC_FLAG_GUILD_MASTER => 'guildmaster', + NPC_FLAG_SPIRIT_HEALER | + NPC_FLAG_SPIRIT_GUIDE => 'spirithealer', + 0 => '' // set 'unused' if no match + ); + + if ($creatureSpawns->isBoss()) $what = 'boss'; else if ($tpl['rank'] == 2 || $tpl['rank'] == 4) $what = 'rare'; + else + foreach ($flagsMap as $flag => $what) + if ($tpl['npcflag'] & $flag) + break; + + if ($what == 'flightmaster') + $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; if ($what) $addToSOM($what, array( @@ -448,7 +471,7 @@ class ZonePage extends GenericPage 'name' => Util::localizedString($tpl, 'name', true, true), 'type' => Type::AREATRIGGER, 'id' => $spawn['typeId'], - 'description' => Lang::game('type').Lang::main('colon').Lang::areatrigger('types', $tpl['type']) + 'description' => Lang::game('type').Lang::areatrigger('types', $tpl['type']) )); } @@ -479,19 +502,13 @@ class ZonePage extends GenericPage { // neutral nodes come last as the line is colored by the node it's attached to usort($som['flightmaster'], function($a, $b) { - $n1 = $a['reactalliance'] == $a['reacthorde']; - $n2 = $b['reactalliance'] == $b['reacthorde']; + $n1 = (int)$a['reactalliance'] == $a['reacthorde']; + $n2 = (int)$b['reactalliance'] == $b['reacthorde']; - if ($n1 && !$n2) - return 1; - - if (!$n1 && $n2) - return -1; - - return 0; + return $n1 <=> $n2; }); - $paths = DB::Aowow()->select('SELECT n1.typeId AS "0", n2.typeId AS "1" FROM ?_taxipath p JOIN ?_taxinodes n1 ON n1.id = p.startNodeId JOIN ?_taxinodes n2 ON n2.id = p.endNodeId WHERE n1.typeId IN (?a) AND n2.typeId IN (?a)', array_keys($flightNodes), array_keys($flightNodes)); + $paths = DB::Aowow()->select('SELECT n1.`typeId` AS "0", n2.`typeId` AS "1" FROM ?_taxipath p JOIN ?_taxinodes n1 ON n1.`id` = p.`startNodeId` JOIN ?_taxinodes n2 ON n2.`id` = p.`endNodeId` WHERE n1.`typeId` IN (?a) AND n2.`typeId` IN (?a)', array_keys($flightNodes), array_keys($flightNodes)); foreach ($paths as $k => $path) { @@ -513,37 +530,39 @@ class ZonePage extends GenericPage } // preselect bosses for raids/dungeons - if (in_array($this->subject->getField('type'), [2, 3, 4, 5, 7, 8])) + if (in_array($_type, [MAP_TYPE_DUNGEON, MAP_TYPE_RAID, MAP_TYPE_BATTLEGROUND, MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC])) $som['instance'] = true; $this->map = array( - 'data' => ['parent' => 'mapper-generic', 'zone' => $this->typeId, 'zoneLink' => false], - 'som' => $som + array( // Mapper + 'parent' => 'mapper-generic', + 'zone' => $this->typeId, + 'zoneLink' => false + ), + null, // mapperData + $som, // ShowOnMap + null // foundIn ); } - else - $this->map = false; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] ); - /* - - associated with holiday? - */ /**************/ /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: NPCs if ($cSpawns && !$creatureSpawns->error) { $tabData = array( - 'data' => array_values($creatureSpawns->getListviewData()), + 'data' => $creatureSpawns->getListviewData(), 'note' => sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0') ); @@ -552,14 +571,14 @@ class ZonePage extends GenericPage $this->extendGlobalData($creatureSpawns->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [CreatureList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); } // tab: Objects if ($oSpawns && !$objectSpawns->error) { $tabData = array( - 'data' => array_values($objectSpawns->getListviewData()), + 'data' => $objectSpawns->getListviewData(), 'note' => sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0') ); @@ -568,7 +587,7 @@ class ZonePage extends GenericPage $this->extendGlobalData($objectSpawns->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [GameObjectList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); } $quests = new QuestList(array(['zoneOrSort', $this->typeId])); @@ -590,7 +609,7 @@ class ZonePage extends GenericPage // tab: Quests [including data collected by SOM-routine] if ($questsLV) { - $tabData = ['data' => array_values($questsLV)]; + $tabData = ['data' => $questsLV]; foreach (Game::QUEST_CLASSES as $parent => $children) { @@ -601,15 +620,15 @@ class ZonePage extends GenericPage break; } - $this->lvTabs[] = [QuestList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); } // tab: item-quest starter // select every quest starter, that is a drop - $questStartItem = DB::Aowow()->select(' - SELECT qse.typeId AS ARRAY_KEY, moreType, moreTypeId, moreZoneId - FROM ?_quests_startend qse JOIN ?_source src ON src.type = qse.type AND src.typeId = qse.typeId - WHERE src.src2 IS NOT NULL AND qse.type = ?d AND (moreZoneId = ?d OR (moreType = ?d AND moreTypeId IN (?a)) OR (moreType = ?d AND moreTypeId IN (?a)))', + $questStartItem = DB::Aowow()->select( + 'SELECT qse.`typeId` AS ARRAY_KEY, `moreType`, `moreTypeId`, `moreZoneId` + FROM ?_quests_startend qse JOIN ?_source src ON src.`type` = qse.`type` AND src.`typeId` = qse.`typeId` + WHERE src.`src2` IS NOT NULL AND qse.`type` = ?d AND (`moreZoneId` = ?d OR (`moreType` = ?d AND `moreTypeId` IN (?a)) OR (`moreType` = ?d AND `moreTypeId` IN (?a)))', Type::ITEM, $this->typeId, Type::NPC, array_unique(array_column($cSpawns, 'typeId')) ?: [0], Type::OBJECT, array_unique(array_column($oSpawns, 'typeId')) ?: [0] @@ -620,11 +639,11 @@ class ZonePage extends GenericPage $qsiList = new ItemList(array(['id', array_keys($questStartItem)])); if (!$qsiList->error) { - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($qsiList->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $qsiList->getListviewData(), 'name' => '$LANG.tab_startsquest', 'id' => 'starts-quest' - )]; + ), ItemList::$brickFile)); $this->extendGlobalData($qsiList->getJSGlobals(GLOBALINFO_SELF)); } @@ -636,12 +655,12 @@ class ZonePage extends GenericPage $rewards = new ItemList(array(['id', array_unique($rewardsLV)])); if (!$rewards->error) { - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($rewards->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $rewards->getListviewData(), 'name' => '$LANG.tab_questrewards', 'id' => 'quest-rewards', 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0') - )]; + ), ItemList::$brickFile)); $this->extendGlobalData($rewards->getJSGlobals(GLOBALINFO_SELF)); } @@ -656,28 +675,24 @@ class ZonePage extends GenericPage $this->extendGlobalData($fish->jsGlobals); $xCols = array_merge(['$Listview.extraCols.percent'], $fish->extraCols); - $note = ''; + $note = null; if ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $this->typeId)) $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); - else if ($parentArea && ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $parentArea))) + else if ($_parentArea && ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $_parentArea))) $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); - $tabData = array( - 'data' => array_values($fish->getResult()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $fish->getResult(), 'name' => '$LANG.tab_fishing', 'id' => 'fishing', 'extraCols' => array_unique($xCols), - 'hiddenCols' => ['side'] - ); - - if ($note) - $tabData['note'] = $note; - - $this->lvTabs[] = [ItemList::$brickFile, $tabData]; + 'hiddenCols' => ['side'], + 'note' => $note + ), ItemList::$brickFile)); } // tab: spells - if ($saData = DB::World()->select('SELECT * FROM spell_area WHERE area = ?d', $this->typeId)) + if ($saData = DB::World()->select('SELECT * FROM spell_area WHERE `area` = ?d', $this->typeId)) { $spells = new SpellList(array(['id', array_column($saData, 'spell')])); if (!$spells->error) @@ -710,15 +725,11 @@ class ZonePage extends GenericPage if ($cnd->toListviewColumn($lvSpells, $extraCols)) $this->extendGlobalData($cnd->getJsGlobals()); - $tabData = array( - 'data' => array_values($lvSpells), - 'hiddenCols' => ['skill'] - ); - - if ($extraCols) - $tabData['extraCols'] = $extraCols; - - $this->lvTabs[] = [SpellList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvSpells, + 'hiddenCols' => ['skill'], + 'extraCols' => $extraCols ?: null + ), SpellList::$brickFile)); } } @@ -726,12 +737,12 @@ class ZonePage extends GenericPage $subZones = new ZoneList(array(['parentArea', $this->typeId])); if (!$subZones->error) { - $this->lvTabs[] = [ZoneList::$brickFile, array( - 'data' => array_values($subZones->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $subZones->getListviewData(), 'name' => '$LANG.tab_zones', 'id' => 'subzones', 'hiddenCols' => ['territory', 'instancetype'] - )]; + ), ZoneList::$brickFile)); $this->extendGlobalData($subZones->getJSGlobals(GLOBALINFO_SELF)); } @@ -745,12 +756,12 @@ class ZonePage extends GenericPage $soundIds = []; $zoneMusic = DB::Aowow()->select( - 'SELECT x.soundId AS ARRAY_KEY, x.soundId, x.worldStateId, x.worldStateValue, x.type - FROM (SELECT `ambienceDay` AS soundId, `worldStateId`, `worldStateValue`, 1 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceDay` > 0 UNION - SELECT `ambienceNight` AS soundId, `worldStateId`, `worldStateValue`, 1 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceNight` > 0 UNION - SELECT `musicDay` AS soundId, `worldStateId`, `worldStateValue`, 2 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicDay` > 0 UNION - SELECT `musicNight` AS soundId, `worldStateId`, `worldStateValue`, 2 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicNight` > 0 UNION - SELECT `intro` AS soundId, `worldStateId`, `worldStateValue`, 3 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `intro` > 0) x + 'SELECT x.`soundId` AS ARRAY_KEY, x.`soundId`, x.`worldStateId`, x.`worldStateValue`, x.`type` + FROM (SELECT `ambienceDay` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceDay` > 0 UNION + SELECT `ambienceNight` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceNight` > 0 UNION + SELECT `musicDay` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicDay` > 0 UNION + SELECT `musicNight` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicNight` > 0 UNION + SELECT `intro` AS "soundId", `worldStateId`, `worldStateValue`, 3 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `intro` > 0) x GROUP BY x.soundId, x.worldStateId, x.worldStateValue', $areaIds, $areaIds, $areaIds, $areaIds, $areaIds ); @@ -772,40 +783,38 @@ class ZonePage extends GenericPage if (array_filter(array_column($zoneMusic, 'worldStateId'))) { - $tabData['extraCols'] = ['$Listview.extraCols.condition']; + $tabData['extraCols'] = ['$Listview.extraCols.condition']; foreach ($soundIds as $sId) if (!empty($zoneMusic[$sId]['worldStateId'])) Conditions::extendListviewRow($data[$sId], Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $zoneMusic[$sId]['worldStateId'], $zoneMusic[$sId]['worldStateValue']]); } - $tabData['data'] = array_values($data); + $tabData['data'] = $data; - $this->lvTabs[] = [SoundList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); $this->extendGlobalData($music->getJSGlobals(GLOBALINFO_SELF)); $typeFilter = function(array $music, int $type) use ($data) : array { $result = []; - foreach (array_filter($music, function ($x) use ($type) { return $x['type'] == $type; } ) as $sId => $_) + foreach (array_filter($music, fn ($x) => $x['type'] == $type) as $sId => $_) $result = array_merge($result, $data[$sId]['files'] ?? []); return $result; }; - // audio controls - // ambience - if ($_ = $typeFilter($zoneMusic, 1)) - $this->zoneMusic['ambience'] = $_; - - // music + // audio controls (order how it appears on page) + // [title, data, divID, options] if ($_ = $typeFilter($zoneMusic, 2)) - $this->zoneMusic['music'] = $_; + $this->zoneMusic[] = [Lang::sound('music'), $_, 'zonemusic', (object)['loop' => true]]; - // intro if ($_ = $typeFilter($zoneMusic, 3)) - $this->zoneMusic['intro'] = $_; + $this->zoneMusic[] = [Lang::sound('intro'), $_, 'zonemusicintro', (object)[]]; + + if ($_ = $typeFilter($zoneMusic, 1)) + $this->zoneMusic[] = [Lang::sound('ambience'), $_, 'soundambience', (object)['loop' => true]]; } } @@ -815,11 +824,11 @@ class ZonePage extends GenericPage if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) { $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; + $this->lvTabs->addDataTab(...$tab); } } - private function addMoveLocationMenu($parentArea, $parentFloor) + private function addMoveLocationMenu(int $_parentArea, int $parentFloor) : void { // hide for non-staff if (!User::isInGroup(U_GROUP_EMPLOYEE)) @@ -829,7 +838,7 @@ class ZonePage extends GenericPage if (!$worldPos) return; - $menu = Util::buildPosFixMenu($worldPos[-$this->typeId]['mapId'], $worldPos[-$this->typeId]['posX'], $worldPos[-$this->typeId]['posY'], Type::ZONE, -$this->typeId, $parentArea, $parentFloor); + $menu = Util::buildPosFixMenu($worldPos[-$this->typeId]['mapId'], $worldPos[-$this->typeId]['posX'], $worldPos[-$this->typeId]['posY'], Type::ZONE, -$this->typeId, $_parentArea, $parentFloor); if (!$menu) return; @@ -837,20 +846,6 @@ class ZonePage extends GenericPage $this->addScript([SC_JS_STRING, '$(document).ready(function () { mn_staff.push('.Util::toJSON(array_values($menu)).'); });']); } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('category'); - - if (in_array($this->subject->getField('category'), [2, 3])) - $this->path[] = $this->subject->getField('expansion'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('zone'))); - } - } ?> diff --git a/endpoints/zones/zones.php b/endpoints/zones/zones.php new file mode 100644 index 00000000..77247254 --- /dev/null +++ b/endpoints/zones/zones.php @@ -0,0 +1,188 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('zones')); + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + + /**************/ + /* Page Title */ + /**************/ + + if (isset($this->category[1])) + array_unshift($this->title, Lang::game('expansions', $this->category[1])); + + if (isset($this->category[0])) + array_unshift($this->title, Lang::zone('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Cfg::get('SQL_LIMIT_NONE')]; + $visibleCols = []; + $hiddenCols = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) // sub-areas and unused zones + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + { + $conditions[] = ['z.category', $this->category[0]]; + $hiddenCols[] = 'category'; + + if (isset($this->category[1]) && in_array($this->category[0], [2, 3])) + $conditions[] = ['z.expansion', $this->category[1]]; + + switch ($this->category[0]) + { + case 6: + case 2: + case 3: + array_push($visibleCols, 'level', 'players'); + case 9: + $hiddenCols[] = 'territory'; + break; + } + } + + $zones = new ZoneList($conditions); + + if (!$zones->hasSetFields('type')) + $hiddenCols[] = 'instancetype'; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $zones->getListviewData(), + 'visibleCols'=> $visibleCols ?: null, + 'hiddenCols' => $hiddenCols ?: null + ), ZoneList::$brickFile)); + + + /**************/ + /* Flight Map */ + /**************/ + + [$mapFile, $spawnMap] = match ($this->category[0] ?? null) + { + 0 => [-3, 0], + 1 => [-6, 1], + 8 => [-2, 530], + 10 => [-5, 571], + default => [ 0, -1] + }; + + if ($mapFile) + { + $somData = ['flightmaster' => []]; + $nodes = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, tn.* FROM ?_taxinodes tn WHERE `mapId` = ?d AND `type` <> 0 AND `typeId` <> 0', $spawnMap); + $paths = DB::Aowow()->select( + 'SELECT IF(tn1.`reactA` = tn1.`reactH` AND tn2.`reactA` = tn2.`reactH`, 1, 0) AS "neutral", + tp.`startNodeId` AS "startId", tn1.`posX` AS "startPosX", tn1.`posY` AS "startPosY", + tp.`endNodeId` AS "endId", tn2.`posX` AS "endPosX", tn2.`posY` AS "endPosY" + FROM ?_taxipath tp, ?_taxinodes tn1, ?_taxinodes tn2 + WHERE tn1.`Id` = tp.`endNodeId` AND tn2.`Id` = tp.`startNodeId` AND + tn1.`type` <> 0 AND tn2.`type` <> 0 AND + (tp.`startNodeId` IN (?a) OR tp.`EndNodeId` IN (?a))', + array_keys($nodes), array_keys($nodes) + ); + + foreach ($nodes as $i => $n) + { + $neutral = $n['reactH'] == $n['reactA']; + + $data = array( + 'coords' => [[$n['posX'], $n['posY']]], + 'level' => 0, // floor + 'name' => Util::localizedString($n, 'name'), + 'type' => $n['type'], + 'id' => $n['typeId'], + 'reacthorde' => $n['reactH'], + 'reactalliance' => $n['reactA'], + 'paths' => [] + ); + + foreach ($paths as $j => $p) + { + if ($i != $p['startId'] && $i != $p['endId']) + continue; + + if ($i == $p['startId'] && (!$neutral || $p['neutral'])) + { + $data['paths'][] = [$p['startPosX'], $p['startPosY']]; + unset($paths[$j]); + } + else if ($i == $p['endId'] && (!$neutral || $p['neutral'])) + { + $data['paths'][] = [$p['endPosX'], $p['endPosY']]; + unset($paths[$j]); + } + } + + if (empty($data['paths'])) + unset($data['paths']); + + $somData['flightmaster'][] = $data; + } + + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'zone' => $mapFile, + 'zoom' => 1, + 'overlay' => true, + 'zoomable' => false + ), + null, // mapperData + $somData, // ShowOnMap + null // foundIn + ); + } + + parent::generate(); + } +} + +?> diff --git a/pages/zones.php b/pages/zones.php deleted file mode 100644 index eb5e6efe..00000000 --- a/pages/zones.php +++ /dev/null @@ -1,185 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('zones')); - } - - protected function generateContent() - { - $conditions = [Cfg::get('SQL_LIMIT_NONE')]; - $visibleCols = []; - $hiddenCols = []; - $mapFile = 0; - $spawnMap = -1; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) // sub-areas and unused zones - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - $conditions[] = ['z.category', $this->category[0]]; - $hiddenCols[] = 'category'; - - if (isset($this->category[1]) && in_array($this->category[0], [2, 3])) - $conditions[] = ['z.expansion', $this->category[1]]; - - if (empty($this->category[1])) - { - switch ($this->category[0]) - { - case 0: $mapFile = -3; $spawnMap = 0; break; - case 1: $mapFile = -6; $spawnMap = 1; break; - case 8: $mapFile = -2; $spawnMap = 530; break; - case 10: $mapFile = -5; $spawnMap = 571; break; - } - } - - switch ($this->category[0]) - { - case 6: - case 2: - case 3: - array_push($visibleCols, 'level', 'players'); - case 9: - $hiddenCols[] = 'territory'; - break; - } - } - - $zones = new ZoneList($conditions); - - if (!$zones->hasSetFields('type')) - $hiddenCols[] = 'instancetype'; - - $tabData = ['data' => array_values($zones->getListviewData())]; - - if ($visibleCols) - $tabData['visibleCols'] = $visibleCols; - - if ($hiddenCols) - $tabData['hiddenCols'] = $hiddenCols; - - $this->lvTabs[] = [ZoneList::$brickFile, $tabData]; - - // create flight map - if ($mapFile) - { - $somData = ['flightmaster' => []]; - $nodes = DB::Aowow()->select('SELECT id AS ARRAY_KEY, tn.* FROM ?_taxinodes tn WHERE mapId = ?d AND type <> 0 AND typeId <> 0', $spawnMap); - $paths = DB::Aowow()->select(' - SELECT IF(tn1.reactA = tn1.reactH AND tn2.reactA = tn2.reactH, 1, 0) AS neutral, - tp.startNodeId AS startId, - tn1.posX AS startPosX, - tn1.posY AS startPosY, - tp.endNodeId AS endId, - tn2.posX AS endPosX, - tn2.posY AS endPosY - FROM ?_taxipath tp, - ?_taxinodes tn1, - ?_taxinodes tn2 - WHERE tn1.Id = tp.endNodeId AND - tn2.Id = tp.startNodeId AND - tn1.type <> 0 AND - tn2.type <> 0 AND - (tp.startNodeId IN (?a) OR tp.EndNodeId IN (?a)) - ', array_keys($nodes), array_keys($nodes)); - - foreach ($nodes as $i => $n) - { - $neutral = $n['reactH'] == $n['reactA']; - - $data = array( - 'coords' => [[$n['posX'], $n['posY']]], - 'level' => 0, // floor - 'name' => Util::localizedString($n, 'name'), - 'type' => $n['type'], - 'id' => $n['typeId'], - 'reacthorde' => $n['reactH'], - 'reactalliance' => $n['reactA'], - 'paths' => [] - ); - - foreach ($paths as $j => $p) - { - if ($i != $p['startId'] && $i != $p['endId']) - continue; - - if ($i == $p['startId'] && (!$neutral || $p['neutral'])) - { - $data['paths'][] = [$p['startPosX'], $p['startPosY']]; - unset($paths[$j]); - } - else if ($i == $p['endId'] && (!$neutral || $p['neutral'])) - { - $data['paths'][] = [$p['endPosX'], $p['endPosY']]; - unset($paths[$j]); - } - } - - if (empty($data['paths'])) - unset($data['paths']); - - $somData['flightmaster'][] = $data; - } - - $this->map = array( - 'data' => array( - 'zone' => $mapFile, - 'zoom' => 1, - 'overlay' => true, - 'zoomable' => false, - 'parent' => 'mapper-generic' - ), - 'som' => $somData, - 'mapperData' => null - ); - } - } - - protected function generateTitle() - { - if ($this->category) - { - if (isset($this->category[1])) - array_unshift($this->title, Lang::game('expansions', $this->category[1])); - - array_unshift($this->title, Lang::zone('cat', $this->category[0])); - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - } -} - - -?> From 1672883186cea4ad551fe1c0d0b422e3477ad95f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 19:23:10 +0200 Subject: [PATCH 677/957] Template/Update (Part 34) * convert dbtype 'areatrigger' --- endpoints/areatrigger/areatrigger.php | 141 ++++++++++++++++++++++++ endpoints/areatriggers/areatriggers.php | 100 +++++++++++++++++ pages/areatrigger.php | 125 --------------------- pages/areatriggers.php | 85 -------------- template/listviews/areatrigger.tpl | 11 +- template/pages/areatriggers.tpl.php | 40 ++++--- 6 files changed, 273 insertions(+), 229 deletions(-) create mode 100644 endpoints/areatrigger/areatrigger.php create mode 100644 endpoints/areatriggers/areatriggers.php delete mode 100644 pages/areatrigger.php delete mode 100644 pages/areatriggers.php diff --git a/endpoints/areatrigger/areatrigger.php b/endpoints/areatrigger/areatrigger.php new file mode 100644 index 00000000..b0ce07ca --- /dev/null +++ b/endpoints/areatrigger/areatrigger.php @@ -0,0 +1,141 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new AreaTriggerList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('areatrigger'), Lang::areatrigger('notFound')); + + $this->h1 = $this->subject->getField('name') ?: 'Areatrigger #'.$this->typeId; + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('type'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('areatrigger'))); + + + /****************/ + /* Main Content */ + /****************/ + + $_type = $this->subject->getField('type'); + + // get spawns + if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) + { + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::areatrigger('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + // Smart AI + if ($_type == AT_TYPE_SMART) + { + $sai = new SmartAI(SmartAI::SRC_TYPE_AREATRIGGER, $this->typeId, ['teleportTargetArea' => $this->subject->getField('areaId')]); + if ($sai->prepare()) + { + $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } + } + + $this->redButtons = array( + BUTTON_LINKS => false, + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: conditions + $cnd = new Conditions(); + $cnd->getBySourceEntry($this->typeId, Conditions::SRC_AREATRIGGER_CLIENT)->prepare(); + if ($tab = $cnd->toListviewTab()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + if ($_type == AT_TYPE_OBJECTIVE) + { + $relQuest = new QuestList(array(['id', $this->subject->getField('quest')])); + if (!$relQuest->error) + { + $this->extendGlobalData($relQuest->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + $this->lvTabs->addListviewTab(new Listview(['data' => $relQuest->getListviewData()], QuestList::$brickFile)); + } + } + else if ($_type == AT_TYPE_TELEPORT) + { + $relZone = new ZoneList(array(['id', $this->subject->getField('areaId')])); + if (!$relZone->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $relZone->getListviewData()], ZoneList::$brickFile)); + } + else if ($_type == AT_TYPE_SCRIPT) + { + $relTrigger = new AreaTriggerList(array(['id', $this->typeId, '!'], ['name', $this->subject->getField('name')])); + if (!$relTrigger->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $relTrigger->getListviewData(), 'name' => Util::ucFirst(Lang::game('areatrigger'))]), AreaTriggerList::$brickFile, 'areatrigger'); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/areatriggers/areatriggers.php b/endpoints/areatriggers/areatriggers.php new file mode 100644 index 00000000..04b06d46 --- /dev/null +++ b/endpoints/areatriggers/areatriggers.php @@ -0,0 +1,100 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]]; + protected array $validCats = [0, 1, 2, 3, 4, 5]; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + if (isset($this->category[0])) + $this->forward('?areatriggers&filter=ty='.$this->category[0]); + + parent::__construct($pageParam); + + $this->filter = new AreaTriggerListFilter($this->_get['filter'] ?? ''); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('areatriggers')); + + $this->filter->evalCriteria(); + + $fiForm = $this->filter->values; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if (count($fiForm['ty']) == 1) + array_unshift($this->title, Lang::areatrigger('types', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty']; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = false; + + $conditions = []; + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + $tabData = []; + $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); + if (!$trigger->error) + { + $tabData['data'] = $trigger->getListviewData(); + + // create note if search limit was exceeded; overwriting 'note' is intentional + if ($trigger->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, AreaTriggerList::$brickFile, 'areatrigger')); + + parent::generate(); + } +} + +?> diff --git a/pages/areatrigger.php b/pages/areatrigger.php deleted file mode 100644 index ecae884d..00000000 --- a/pages/areatrigger.php +++ /dev/null @@ -1,125 +0,0 @@ -typeId = intVal($id); - - $this->subject = new AreaTriggerList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('areatrigger'), Lang::areatrigger('notFound')); - - $this->name = $this->subject->getField('name') ?: 'AT #'.$this->typeId; - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('type'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('areatrigger'))); - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); - - $_type = $this->subject->getField('type'); - - - /****************/ - /* Main Content */ - /****************/ - - // get spawns - $map = null; - if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) - { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns, 'foundIn' => Lang::areatrigger('foundIn')]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); - } - - // smart AI - $sai = null; - if ($_type == AT_TYPE_SMART) - { - $sai = new SmartAI(SmartAI::SRC_TYPE_AREATRIGGER, $this->typeId, ['teleportTargetArea' => $this->subject->getField('areaId')]); - if ($sai->prepare()) - $this->extendGlobalData($sai->getJSGlobals()); - } - - $this->map = $map; - $this->infobox = false; - $this->smartAI = $sai?->getMarkup(); - $this->redButtons = array( - BUTTON_LINKS => false, - BUTTON_WOWHEAD => false - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: conditions - $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_AREATRIGGER_CLIENT)->prepare(); - if ($tab = $cnd->toListviewTab()) - { - $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - - if ($_type == AT_TYPE_OBJECTIVE) - { - $relQuest = new QuestList(array(['id', $this->subject->getField('quest')])); - if (!$relQuest->error) - { - $this->extendGlobalData($relQuest->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - $this->lvTabs[] = [QuestList::$brickFile, ['data' => array_values($relQuest->getListviewData())]]; - } - } - else if ($_type == AT_TYPE_TELEPORT) - { - $relZone = new ZoneList(array(['id', $this->subject->getField('areaId')])); - if (!$relZone->error) - { - $this->lvTabs[] = [ZoneList::$brickFile, ['data' => array_values($relZone->getListviewData())]]; - } - } - else if ($_type == AT_TYPE_SCRIPT) - { - $relTrigger = new AreaTriggerList(array(['id', $this->typeId, '!'], ['name', $this->subject->getField('name')])); - if (!$relTrigger->error) - { - $this->lvTabs[] = [AreaTriggerList::$brickFile, ['data' => array_values($relTrigger->getListviewData()), 'name' => Util::ucFirst(Lang::game('areatrigger'))], 'areatrigger']; - } - } - } -} - -?> diff --git a/pages/areatriggers.php b/pages/areatriggers.php deleted file mode 100644 index 7c32bd24..00000000 --- a/pages/areatriggers.php +++ /dev/null @@ -1,85 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - if (isset($this->category[0])) - header('Location: ?areatriggers&filter=ty='.$this->category[0], true, 302); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new AreaTriggerListFilter($this->_get['filter'] ?? ''); - - $this->name = Util::ucFirst(Lang::game('areatriggers')); - } - - protected function generateContent() - { - $this->filterObj->evalCriteria(); - - $conditions = []; - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = []; - $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); - if (!$trigger->error) - { - $tabData['data'] = array_values($trigger->getListviewData()); - - // create note if search limit was exceeded; overwriting 'note' is intentional - if ($trigger->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - } - - $this->lvTabs[] = [AreaTriggerList::$brickFile, $tabData, 'areatrigger']; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - array_unshift($this->title, Lang::areatrigger('types', $form['ty'][0])); - } - - protected function generatePath() - { - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - $this->path[] = $form['ty']; - } -} - -?> diff --git a/template/listviews/areatrigger.tpl b/template/listviews/areatrigger.tpl index c9ee2816..0a428c0b 100644 --- a/template/listviews/areatrigger.tpl +++ b/template/listviews/areatrigger.tpl @@ -11,7 +11,9 @@ Listview.templates.areatrigger = { value: 'id', compute: function(data, td) { if (data.id) { - $WH.ae(td, $WH.ct(data.id)); + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); } } }, @@ -75,5 +77,12 @@ Listview.templates.areatrigger = { ], getItemLink: function(areatrigger) { return '?areatrigger=' + areatrigger.id; + }, + onBeforeCreate : function() { + // hide duplicate id col + if (this.debug || g_user?.debug) { + let colId = this.columns.findIndex(x => x.id == 'id'); + this.visibility = this.visibility.filter(x => x != colId); + } } } diff --git a/template/pages/areatriggers.tpl.php b/template/pages/areatriggers.tpl.php index 5709d0e3..3f187918 100644 --- a/template/pages/areatriggers.tpl.php +++ b/template/pages/areatriggers.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,29 +13,32 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [101]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [102]]); ?> -

name; ?> brick('redButtons'); ?>

-
+
+
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

h1; ?>

+
-
+
- + @@ -43,7 +47,7 @@ endforeach;
- /> /> + /> />
@@ -57,7 +61,7 @@ endforeach;
-brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From 0cf9069eb1fd71803f18f0194a715f12f414badf Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 21:23:09 +0200 Subject: [PATCH 678/957] Template/Update (Part 35) * convert dbtype 'icon' * improve on IconlistFilter --- endpoints/icon/icon.php | 158 ++++++++++++++++++++++++++++++++ endpoints/icons/icons.php | 109 ++++++++++++++++++++++ includes/dbtypes/icon.class.php | 120 ++++++++++-------------- pages/icon.php | 120 ------------------------ pages/icons.php | 96 ------------------- template/pages/icon.tpl.php | 13 ++- template/pages/icons.tpl.php | 32 ++++--- 7 files changed, 344 insertions(+), 304 deletions(-) create mode 100644 endpoints/icon/icon.php create mode 100644 endpoints/icons/icons.php delete mode 100644 pages/icon.php delete mode 100644 pages/icons.php diff --git a/endpoints/icon/icon.php b/endpoints/icon/icon.php new file mode 100644 index 00000000..2a67b53e --- /dev/null +++ b/endpoints/icon/icon.php @@ -0,0 +1,158 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new IconList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('icon'), Lang::icon('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = $this->subject->getField('name'); + $this->icon = $this->subject->getField('name', true, true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $cats = [1 => 'nItems', 2 => 'nSpells', 3 => 'nAchievements', 6 => 'nCurrencies', 9 => 'nPets'/* , 11 => '' */]; + $crumb = ''; + foreach ($cats as $cat => $field) + { + if (!$this->subject->getField($field)) + continue; + + if ($crumb) + { + $crumb = 0; + break; + } + + $crumb = $cat; + } + + if ($crumb) + $this->breadcrumb[] = $crumb; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('icon'))); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // used by: spell + $ubSpells = new SpellList(array(['iconId', $this->typeId])); + if (!$ubSpells->error) + { + $this->extendGlobalData($ubSpells->getJsGlobals(GLOBALINFO_RELATED | GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubSpells->getListviewData(), + 'id' => 'used-by-spell' + ), SpellList::$brickFile)); + } + + // used by: item + $ubItems = new ItemList(array(['iconId', $this->typeId])); + if (!$ubItems->error) + { + $this->extendGlobalData($ubItems->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubItems->getListviewData(), + 'id' => 'used-by-item' + ), ItemList::$brickFile)); + } + + // used by: achievement + $ubAchievements = new AchievementList(array(['iconId', $this->typeId])); + if (!$ubAchievements->error) + { + $this->extendGlobalData($ubAchievements->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubAchievements->getListviewData(), + 'id' => 'used-by-achievement' + ), AchievementList::$brickFile)); + } + + // used by: currency + $ubCurrencies = new CurrencyList(array(['iconId', $this->typeId])); + if (!$ubCurrencies->error) + { + $this->extendGlobalData($ubCurrencies->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubCurrencies->getListviewData(), + 'id' => 'used-by-currency' + ), CurrencyList::$brickFile)); + } + + // used by: hunter pet + $ubPets = new PetList(array(['iconId', $this->typeId])); + if (!$ubPets->error) + { + $this->extendGlobalData($ubPets->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubPets->getListviewData(), + 'id' => 'used-by-pet' + ), PetList::$brickFile)); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/icons/icons.php b/endpoints/icons/icons.php new file mode 100644 index 00000000..7613240e --- /dev/null +++ b/endpoints/icons/icons.php @@ -0,0 +1,109 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [0, 1, 2, 3]; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new IconListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('icons')); + + $conditions = [600]; // LIMIT 600 - fits better onto the grid + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->filter->evalCriteria(); + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + + /**************/ + /* Page Title */ + /**************/ + + $title = $this->h1; + $setCr = $this->filter->getSetCriteria(1, 2, 3, 6, 9, 11); + if (count($setCr) == 1) + $title = match ($setCr[0]) + { + 1 => Util::ucFirst(Lang::game('item')), + 2 => Util::ucFirst(Lang::game('spell')), + 3 => Util::ucFirst(Lang::game('achievement')), + 6 => Util::ucFirst(Lang::game('currency')), + 9 => Util::ucFirst(Lang::game('pet')), + 11 => Util::ucFirst(Lang::game('class')), + } . ' ' . $this->h1; + + array_unshift($this->title, $title); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($setCr) == 1) + $this->breadcrumb[] = $setCr[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $icons = new IconList($conditions, ['calcTotal' => true]); + + $tabData['data'] = $icons->getListviewData(); + $this->extendGlobalData($icons->getJSGlobals()); + + if ($icons->getMatches() > $conditions[0]) // LIMIT + { + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $icons->getMatches(), 'LANG.types[29][3]', $conditions[0]); + $tabData['_truncated'] = 1; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, IconList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php index 0ec46b4e..c324847f 100644 --- a/includes/dbtypes/icon.class.php +++ b/includes/dbtypes/icon.class.php @@ -101,7 +101,7 @@ class IconList extends DBTypeList class IconListFilter extends Filter { - private array $totalUses = []; + private array $iconTotals = []; private array $criterion2field = array( 1 => '?_items', // items [num] 2 => '?_spell', // spells [num] @@ -119,13 +119,13 @@ class IconListFilter extends Filter protected string $type = 'icons'; protected static array $genericFilter = array( - 1 => [parent::CR_CALLBACK, 'cbUseAny' ], // items [num] - 2 => [parent::CR_CALLBACK, 'cbUseAny' ], // spells [num] - 3 => [parent::CR_CALLBACK, 'cbUseAny' ], // achievements [num] - 6 => [parent::CR_CALLBACK, 'cbUseAny' ], // currencies [num] - 9 => [parent::CR_CALLBACK, 'cbUseAny' ], // hunterpets [num] - 11 => [parent::CR_NYI_PH, null, 0], // classes [num] - 13 => [parent::CR_CALLBACK, 'cbUseAll' ] // used [num] + 1 => [parent::CR_CALLBACK, 'cbUsedBy' ], // items [num] + 2 => [parent::CR_CALLBACK, 'cbUsedBy' ], // spells [num] + 3 => [parent::CR_CALLBACK, 'cbUsedBy' ], // achievements [num] + 6 => [parent::CR_CALLBACK, 'cbUsedBy' ], // currencies [num] + 9 => [parent::CR_CALLBACK, 'cbUsedBy' ], // hunterpets [num] + 11 => [parent::CR_NYI_PH, null, 0 ], // classes [num] + 13 => [parent::CR_CALLBACK, 'cbUsedBy', true] // used [num] ); protected static array $inputFields = array( @@ -138,35 +138,6 @@ class IconListFilter extends Filter public array $extraOpts = []; - private function _getCnd(string $op, int $val, string $tbl) : ?array - { - switch ($op) - { - case '>': - case '>=': - case '=': - $ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl); - return $ids ? ['id', array_keys($ids)] : [1]; - case '<=': - if ($val) - $op = '>'; - break; - case '<': - if ($val) - $op = '>='; - break; - case '!=': - if ($val) - $op = '='; - break; - default: - return null; - } - - $ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl); - return $ids ? ['id', array_keys($ids), '!'] : [1]; - } - protected function createSQLForValues() : array { $parts = []; @@ -180,47 +151,54 @@ class IconListFilter extends Filter return $parts; } - protected function cbUseAny(int $cr, int $crs, string $crv) : ?array + protected function cbUsedBy(int $cr, int $crs, string $crv, ?bool $all = false) : ?array { - if (Util::checkNumeric($crv, NUM_CAST_INT) && $this->int2Op($crs)) - return $this->_getCnd($crs, $crv, $this->criterion2field[$cr]); - - return null; - } - - protected function cbUseAll(int $cr, int $crs, string $crv) : ?array - { - if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + if (!Util::checkNumeric($crv, NUM_CAST_INT) || ![$filter, $negate] = $this->int2Filter($crs, $crv)) return null; - if (!$this->totalUses) - { - foreach ($this->criterion2field as $tbl) - { - if (!$tbl) - continue; + $total = $this->prepareIconTotals($all ? 0 : $cr); - $res = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl); - Util::arraySumByKey($this->totalUses, $res); - } - } + $ids = array_filter($total, $filter); - if ($crs == '=') - $crs = '=='; - - $op = $crs; - if ($crs == '<=' && $crv) - $op = '>'; - else if ($crs == '<' && $crv) - $op = '>='; - else if ($crs == '!=' && $crv) - $op = '=='; - $ids = array_filter($this->totalUses, fn($x) => eval('return '.$x.' '.$op.' '.$crv.';')); - - if ($crs != $op) + if ($negate) return $ids ? ['id', array_keys($ids), '!'] : [1]; else - return $ids ? ['id', array_keys($ids)] : ['id', array_keys($this->totalUses), '!']; + return $ids ? ['id', array_keys($ids)] : ['id', array_keys($total), '!']; + } + + private function int2Filter(mixed $op, int $y) : ?array + { + return match ($op) { + 1 => [fn($x) => $x > $y, false], + 2 => [fn($x) => $x >= $y, false], + 3 => [fn($x) => $x == $y, false], + 4 => [fn($x) => $x > $y, true], + 5 => [fn($x) => $x >= $y, true], + 6 => [fn($x) => $x == $y, true], + default => null + }; + } + + private function prepareIconTotals(int $forCr = 0) : array + { + foreach ($this->criterion2field as $cr => $tbl) + { + if (!$tbl || isset($this->iconTotals[$cr]) || ($forCr && $forCr != $cr)) + continue; + + $this->iconTotals[$cr] = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl); + } + + if ($forCr) + return $this->iconTotals[$forCr]; + + if (!isset($this->iconTotals['all'])) + { + $this->iconTotals['all'] = []; + Util::arraySumByKey($this->iconTotals['all'], ...$this->iconTotals); + } + + return $this->iconTotals['all']; } } diff --git a/pages/icon.php b/pages/icon.php deleted file mode 100644 index cb56cc95..00000000 --- a/pages/icon.php +++ /dev/null @@ -1,120 +0,0 @@ -typeId = intVal($id); - - $this->subject = new IconList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('icon'), Lang::icon('notFound')); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - $this->name = $this->subject->getField('name'); - $this->icon = $this->subject->getField('name', true, true); - } - - protected function generateContent() - { - /****************/ - /* Main Content */ - /****************/ - - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // used by: spell - $ubSpells = new SpellList(array(['iconId', $this->typeId])); - if (!$ubSpells->error) - { - $this->extendGlobalData($ubSpells->getJsGlobals(GLOBALINFO_RELATED | GLOBALINFO_SELF)); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($ubSpells->getListviewData()), - 'id' => 'used-by-spell' - )]; - } - - // used by: item - $ubItems = new ItemList(array(['iconId', $this->typeId])); - if (!$ubItems->error) - { - $this->extendGlobalData($ubItems->getJsGlobals()); - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($ubItems->getListviewData()), - 'id' => 'used-by-item' - )]; - } - - // used by: achievement - $ubAchievements = new AchievementList(array(['iconId', $this->typeId])); - if (!$ubAchievements->error) - { - $this->extendGlobalData($ubAchievements->getJsGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($ubAchievements->getListviewData()), - 'id' => 'used-by-achievement' - )]; - } - - // used by: currency - $ubCurrencies = new CurrencyList(array(['iconId', $this->typeId])); - if (!$ubCurrencies->error) - { - $this->extendGlobalData($ubCurrencies->getJsGlobals()); - $this->lvTabs[] = [CurrencyList::$brickFile, array( - 'data' => array_values($ubCurrencies->getListviewData()), - 'id' => 'used-by-currency' - )]; - } - - // used by: hunter pet - $ubPets = new PetList(array(['iconId', $this->typeId])); - if (!$ubPets->error) - { - $this->extendGlobalData($ubPets->getJsGlobals()); - $this->lvTabs[] = [PetList::$brickFile, array( - 'data' => array_values($ubPets->getListviewData()), - 'id' => 'used-by-pet' - )]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('icon'))); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/icons.php b/pages/icons.php deleted file mode 100644 index 52e9d218..00000000 --- a/pages/icons.php +++ /dev/null @@ -1,96 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall) - { - parent::__construct($pageCall); - - $this->filterObj = new IconListFilter($this->_get['filter'] ?? ''); - - $this->name = Util::ucFirst(Lang::game('icons')); - } - - protected function generateContent() - { - $tabData = array( - 'data' => [], - ); - - $sqlLimit = 600; // fits better onto the grid - - $conditions = [$sqlLimit]; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $icons = new IconList($conditions, ['calcTotal' => true]); - - $tabData['data'] = array_values($icons->getListviewData()); - $this->extendGlobalData($icons->getJSGlobals()); - - if ($icons->getMatches() > $sqlLimit) - { - $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $icons->getMatches(), 'LANG.types[29][3]', $sqlLimit); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = [IconList::$brickFile, $tabData]; - } - - protected function generateTitle() - { - $title = $this->name; - $setCr = $this->filterObj->getSetCriteria(1, 2, 3, 6, 9, 11); - if (count($setCr) == 1) - { - $title = match($setCr[0]) - { - 1 => Util::ucFirst(Lang::game('item')), - 2 => Util::ucFirst(Lang::game('spell')), - 3 => Util::ucFirst(Lang::game('achievement')), - 6 => Util::ucFirst(Lang::game('currency')), - 9 => Util::ucFirst(Lang::game('pet')), - 11 => Util::ucFirst(Lang::game('class')) - } . ' ' . $title; - } - - array_unshift($this->title, $title); - } - - protected function generatePath() - { - $setCr = $this->filterObj->getSetCriteria(1, 2, 3, 6, 9, 11); - if (count($setCr) == 1) - $this->path[] = $setCr[0]; - } -} - -?> diff --git a/template/pages/icon.tpl.php b/template/pages/icon.tpl.php index 727c1ade..5ec5f25a 100644 --- a/template/pages/icon.tpl.php +++ b/template/pages/icon.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
@@ -19,20 +22,20 @@ $this->brick('redButtons'); ?> -

name; ?>

+

h1; ?>

brick('article'); + $this->brick('markup', ['markup' => $this->article]); ?>

brick('lvTabs', ['relTabs' => true]); + $this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/icons.tpl.php b/template/pages/icons.tpl.php index 11f6c9c9..2ab62c48 100644 --- a/template/pages/icons.tpl.php +++ b/template/pages/icons.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,17 +13,24 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [101]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); ?> - -
+
+
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

h1; ?>

+
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
 /> />
- + @@ -31,7 +39,7 @@ $this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem'
- /> /> + /> />
@@ -45,7 +53,7 @@ $this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem'
-brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From 3f8d5d90e1df5a7d12f8af3319eeee6afac7789f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 22:20:24 +0200 Subject: [PATCH 679/957] Template/Update (Part 36) * convert dbtype 'mail' --- endpoints/mail/mail.php | 178 +++++++++++++++++++++++++ endpoints/mails/mails.php | 59 ++++++++ pages/mail.php | 169 ----------------------- pages/mails.php | 48 ------- setup/tools/sqlgen/mailtemplate.ss.php | 24 ++-- template/listviews/mail.tpl | 11 +- 6 files changed, 259 insertions(+), 230 deletions(-) create mode 100644 endpoints/mail/mail.php create mode 100644 endpoints/mails/mails.php delete mode 100644 pages/mail.php delete mode 100644 pages/mails.php diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php new file mode 100644 index 00000000..705c6aca --- /dev/null +++ b/endpoints/mail/mail.php @@ -0,0 +1,178 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new MailList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('mail'), Lang::mail('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = Util::htmlEscape($this->subject->getField('name', true)); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->subject->getField('name', true) + ); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('mail'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // sender + delay + if ($this->typeId < 0) // def. achievement + { + if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `ID` = ?d', -$this->typeId)) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + } + else if ($mlr = DB::World()->selectRow('SELECT * FROM mail_level_reward WHERE `mailTemplateId` = ?d', $this->typeId)) // level rewards + { + if ($mlr['level']) + $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; + + if ($r = Lang::getRaceString($mlr['raceMask'], $rIds, Lang::FMT_MARKUP)) + { + $infobox[] = Lang::game('races').Lang::main('colon').$r; + $this->extendGlobalIds(Type::CHR_RACE, ...$rIds); + } + + $infobox[] = Lang::mail('sender', ['[npc='.$mlr['senderEntry'].']']); + $this->extendGlobalIds(Type::NPC, $mlr['senderEntry']); + } + else // achievement or quest + { + if ($q = DB::Aowow()->selectRow('SELECT `id`, `rewardMailDelay` FROM ?_quests WHERE `rewardMailTemplateId` = ?d', $this->typeId)) + { + if ($npcId= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = ?d', $q['id'])) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + else if ($npcId = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_quests_startend WHERE `questId` = ?d AND `type` = ?d AND `method` & ?d', $q['id'], Type::NPC, 0x2)) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + + if ($q['rewardMailDelay'] > 0) + $infobox[] = Lang::mail('delay', [Util::formatTime($q['rewardMailDelay'] * 1000)]); + } + else if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId)) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + } + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + $this->extraText = new Markup(Util::parseHtmlText($this->subject->getField('text', true), true), ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: attachment + if ($itemId = $this->subject->getField('attachment')) + { + $attachment = new ItemList(array(['id', $itemId])); + if (!$attachment->error) + { + $this->extendGlobalData($attachment->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $attachment->getListviewData(), + 'name' => Lang::mail('attachment'), + 'id' => 'attachment' + ), ItemList::$brickFile)); + } + } + + if ($this->typeId < 0 || // used by: achievement + ($acvId = DB::World()->selectCell('SELECT `ID` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId))) + { + $ubAchievements = new AchievementList(array(['id', $this->typeId < 0 ? -$this->typeId : $acvId])); + if (!$ubAchievements->error) + { + $this->extendGlobalData($ubAchievements->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubAchievements->getListviewData(), + 'id' => 'used-by-achievement' + ), AchievementList::$brickFile)); + } + } + else // used by: quest + { + $ubQuests = new QuestList(array(['rewardMailTemplateId', $this->typeId])); + if (!$ubQuests->error) + { + $this->extendGlobalData($ubQuests->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubQuests->getListviewData(), + 'id' => 'used-by-quest' + ), QuestList::$brickFile)); + } + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/mails/mails.php b/endpoints/mails/mails.php new file mode 100644 index 00000000..e892948f --- /dev/null +++ b/endpoints/mails/mails.php @@ -0,0 +1,59 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('mails')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $tabData = []; + $mails = new MailList(); + if (!$mails->error) + $tabData['data'] = $mails->getListviewData(); + + $this->extendGlobalData($mails->getJsGlobals()); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $mails->getListviewData()], MailList::$brickFile, 'mail')); + + parent::generate(); + } +} + +?> diff --git a/pages/mail.php b/pages/mail.php deleted file mode 100644 index 157188f4..00000000 --- a/pages/mail.php +++ /dev/null @@ -1,169 +0,0 @@ -typeId = intVal($id); - - $this->subject = new MailList(array(['id', $this->typeId])); - - if ($this->subject->error) - $this->notFound(lang::game('mail'), Lang::mail('notFound')); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - $this->name = Util::htmlEscape(Util::ucFirst($this->subject->getField('name', true))); - } - - protected function generateContent() - { - /***********/ - /* Infobox */ - /***********/ - - $infobox = []; - - // sender + delay - if ($this->typeId < 0) // def. achievement - { - if ($npcId = DB::World()->selectCell('SELECT Sender FROM achievement_reward WHERE ID = ?d', -$this->typeId)) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - } - else if ($mlr = DB::World()->selectRow('SELECT * FROM mail_level_reward WHERE mailTemplateId = ?d', $this->typeId)) // level rewards - { - if ($mlr['level']) - $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; - - $rIds = []; - if ($r = Lang::getRaceString($mlr['raceMask'], $rIds, Lang::FMT_MARKUP)) - { - $infobox[] = Lang::game('races').Lang::main('colon').$r; - $this->extendGlobalIds(Type::CHR_RACE, ...$rIds); - } - - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$mlr['senderEntry'].']'; - $this->extendGlobalIds(Type::NPC, $mlr['senderEntry']); - } - else // achievement or quest - { - if ($q = DB::Aowow()->selectRow('SELECT id, rewardMailDelay FROM ?_quests WHERE rewardMailTemplateId = ?d', $this->typeId)) - { - if ($npcId= DB::World()->selectCell('SELECT RewardMailSenderEntry FROM quest_mail_sender WHERE QuestId = ?d', $q['id'])) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - else if ($npcId = DB::Aowow()->selectCell('SELECT typeId FROM ?_quests_startend WHERE questId = ?d AND type = ?d AND method & ?d', $q['id'], Type::NPC, 0x2)) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - - if ($q['rewardMailDelay'] > 0) - $infobox[] = Lang::mail('delay').Lang::main('colon').''.Util::formatTime($q['rewardMailDelay'] * 1000); - } - else if ($npcId = DB::World()->selectCell('SELECT Sender FROM achievement_reward WHERE MailTemplateId = ?d', $this->typeId)) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - } - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : ''; - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - $this->extraText = Util::parseHtmlText($this->subject->getField('text', true), true); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: attachment - if ($itemId = $this->subject->getField('attachment')) - { - $attachment = new ItemList(array(['id', $itemId])); - if (!$attachment->error) - { - $this->extendGlobalData($attachment->getJsGlobals()); - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($attachment->getListviewData()), - 'name' => Lang::mail('attachment'), - 'id' => 'attachment' - )]; - } - } - - - if ($this->typeId < 0 || // used by: achievement - ($acvId = DB::World()->selectCell('SELECT ID FROM achievement_reward WHERE MailTemplateId = ?d', $this->typeId))) - { - $ubAchievements = new AchievementList(array(['id', $this->typeId < 0 ? -$this->typeId : $acvId])); - if (!$ubAchievements->error) - { - $this->extendGlobalData($ubAchievements->getJsGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($ubAchievements->getListviewData()), - 'id' => 'used-by-achievement' - )]; - } - } - else if ($npcId = DB::World()->selectCell('SELECT ID FROM achievement_reward WHERE MailTemplateId = ?d', $this->typeId)) - { - $infobox[] = '[Sender]: [npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - - else // used by: quest - { - $ubQuests = new QuestList(array(['rewardMailTemplateId', $this->typeId])); - if (!$ubQuests->error) - { - $this->extendGlobalData($ubQuests->getJsGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($ubQuests->getListviewData()), - 'id' => 'used-by-quest' - )]; - } - } - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst($this->subject->getField('name', true)), Util::ucFirst(Lang::game('mail'))); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/mails.php b/pages/mails.php deleted file mode 100644 index 391b575f..00000000 --- a/pages/mails.php +++ /dev/null @@ -1,48 +0,0 @@ -name = Util::ucFirst(Lang::game('mails')); - } - - protected function generateContent() - { - $tabData = []; - $mails = new MailList(); - if (!$mails->error) - $tabData['data'] = array_values($mails->getListviewData()); - - $this->extendGlobalData($mails->getJsGlobals()); - - $this->lvTabs[] = [MailList::$brickFile, $tabData, 'mail']; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - protected function generatePath() { } -} - -?> diff --git a/setup/tools/sqlgen/mailtemplate.ss.php b/setup/tools/sqlgen/mailtemplate.ss.php index 32c21948..d2e17d1e 100644 --- a/setup/tools/sqlgen/mailtemplate.ss.php +++ b/setup/tools/sqlgen/mailtemplate.ss.php @@ -28,17 +28,17 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[mails] - loading data from achievement_reward'); $acvMail = DB::World()->select( - 'SELECT -ar.ID, 0, - IFNULL(ar.Subject, "") AS s0, IFNULL(arl2.Subject, "") AS s2, IFNULL(arl3.Subject, "") AS s3, IFNULL(arl4.Subject, "") AS s4, IFNULL(arl6.Subject, "") AS s6, IFNULL(arl8.Subject, "") AS s8, - IFNULL(ar.Body, "") AS t0, IFNULL(arl2.Body, "") AS t2, IFNULL(arl3.Body, "") AS t3, IFNULL(arl4.Body, "") AS t4, IFNULL(arl6.Body, "") AS t6, IFNULL(arl8.Body, "") AS t8, - ItemID + 'SELECT -ar.`ID`, 0, + IFNULL(ar.`Subject`, "") AS "s0", IFNULL(arl2.`Subject`, "") AS "s2", IFNULL(arl3.`Subject`, "") AS "s3", IFNULL(arl4.`Subject`, "") AS "s4", IFNULL(arl6.`Subject`, "") AS "s6", IFNULL(arl8.`Subject`, "") AS "s8", + IFNULL(ar.`Body`, "") AS "t0", IFNULL(arl2.`Body`, "") AS "t2", IFNULL(arl3.`Body`, "") AS "t3", IFNULL(arl4.`Body`, "") AS "t4", IFNULL(arl6.`Body`, "") AS "t6", IFNULL(arl8.`Body`, "") AS "t8", + `ItemID` FROM achievement_reward ar - LEFT JOIN achievement_reward_locale arl2 ON ar.ID = arl2.ID AND arl2.Locale = "frFR" - LEFT JOIN achievement_reward_locale arl3 ON ar.ID = arl3.ID AND arl3.Locale = "deDE" - LEFT JOIN achievement_reward_locale arl4 ON ar.ID = arl4.ID AND arl4.Locale = "zhCN" - LEFT JOIN achievement_reward_locale arl6 ON ar.ID = arl6.ID AND arl6.Locale = "esES" - LEFT JOIN achievement_reward_locale arl8 ON ar.ID = arl8.ID AND arl8.Locale = "ruRU" - WHERE ar.MailTemplateID = 0 AND ar.Body <> ""' + LEFT JOIN achievement_reward_locale arl2 ON ar.`ID` = arl2.`ID` AND arl2.`Locale` = "frFR" + LEFT JOIN achievement_reward_locale arl3 ON ar.`ID` = arl3.`ID` AND arl3.`Locale` = "deDE" + LEFT JOIN achievement_reward_locale arl4 ON ar.`ID` = arl4.`ID` AND arl4.`Locale` = "zhCN" + LEFT JOIN achievement_reward_locale arl6 ON ar.`ID` = arl6.`ID` AND arl6.`Locale` = "esES" + LEFT JOIN achievement_reward_locale arl8 ON ar.`ID` = arl8.`ID` AND arl8.`Locale` = "ruRU" + WHERE ar.`MailTemplateID` = 0 AND ar.`Body` <> ""' ); DB::Aowow()->query('INSERT INTO ?_mails VALUES (?a)', array_values($acvMail)); @@ -46,9 +46,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[mails] - loading data from mail_loot_template'); // assume mails to only contain one single item, wich works for an unmodded installation - $mlt = DB::World()->selectCol('SELECT Entry AS ARRAY_KEY, Item FROM mail_loot_template'); + $mlt = DB::World()->selectCol('SELECT `Entry` AS ARRAY_KEY, `Item` FROM mail_loot_template'); foreach ($mlt as $k => $v) - DB::Aowow()->query('UPDATE ?_mails SET attachment = ?d WHERE id = ?d', $v, $k); + DB::Aowow()->query('UPDATE ?_mails SET `attachment` = ?d WHERE `id` = ?d', $v, $k); return true; } diff --git a/template/listviews/mail.tpl b/template/listviews/mail.tpl index 2ebe19f4..72e05fda 100644 --- a/template/listviews/mail.tpl +++ b/template/listviews/mail.tpl @@ -11,7 +11,9 @@ Listview.templates.mail = { value: 'id', compute: function(data, td) { if (data.id) { - $WH.ae(td, $WH.ct(data.id)); + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); } } }, @@ -89,5 +91,12 @@ Listview.templates.mail = { ], getItemLink: function(mail) { return '?mail=' + mail.id; + }, + onBeforeCreate : function() { + // hide duplicate id col + if (this.debug || g_user?.debug) { + let colId = this.columns.findIndex(x => x.id == 'id'); + this.visibility = this.visibility.filter(x => x != colId); + } } } From 3d3e2211e50d8fc1a298cfb2bcc37d411dff1a89 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 23:12:34 +0200 Subject: [PATCH 680/957] Template/Update (Part 37) * convert dbtype 'sound' --- {pages => endpoints/sound}/sound.php | 159 ++++++++++++-------------- endpoints/sound/sound_playlist.php | 26 +++++ endpoints/sounds/sounds.php | 117 +++++++++++++++++++ pages/sounds.php | 93 --------------- template/pages/sound-playlist.tpl.php | 68 +++++++++++ template/pages/sound.tpl.php | 71 ++---------- template/pages/sounds.tpl.php | 38 +++--- 7 files changed, 315 insertions(+), 257 deletions(-) rename {pages => endpoints/sound}/sound.php (72%) create mode 100644 endpoints/sound/sound_playlist.php create mode 100644 endpoints/sounds/sounds.php delete mode 100644 pages/sounds.php create mode 100644 template/pages/sound-playlist.tpl.php diff --git a/pages/sound.php b/endpoints/sound/sound.php similarity index 72% rename from pages/sound.php rename to endpoints/sound/sound.php index c90a0a69..6207cf2a 100644 --- a/pages/sound.php +++ b/endpoints/sound/sound.php @@ -6,97 +6,82 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 19: Sound g_initPath() -// tabId 0: Database g_initHeader() -class SoundPage extends GenericPage +class SoundBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $type = Type::SOUND; - protected $typeId = 0; - protected $tpl = 'sound'; - protected $path = [0, 19]; - protected $tabId = 0; - protected $mode = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $special = false; - protected $_get = ['playlist' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet']]; + protected string $template = 'sound'; + protected string $pageName = 'sound'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 19]; - private $cat = 0; + public int $type = Type::SOUND; + public int $typeId = 0; - public function __construct($pageCall, $id) + private SoundList $subject; + + public function __construct(string $id) { - parent::__construct($pageCall, $id); + parent::__construct($id); - // special case - if (!$id && $this->_get['playlist']) - { - $this->special = true; - $this->name = Lang::sound('cat', 1000); - $this->cat = 1000; - $this->articleUrl = 'sound&playlist'; - $this->contribute = CONTRIBUTE_NONE; - $this->mode = CACHE_TYPE_NONE; - } - // regular case - else - { - $this->typeId = intVal($id); - - $this->subject = new SoundList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('sound'), Lang::sound('notFound')); - - $this->name = $this->subject->getField('name'); - $this->cat = $this->subject->getField('cat'); - } + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; } - protected function generatePath() + protected function generate() : void { - $this->path[] = $this->cat; - } + $this->subject = new SoundList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('sound'), Lang::sound('notFound')); - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('sound'))); - } + $this->h1 = $this->subject->getField('name'); - protected function generateContent() - { - if ($this->special) - $this->generatePlaylistContent(); - else - $this->generateDefaultContent(); - } + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); - private function generatePlaylistContent() - { + $_cat = $this->subject->getField('cat'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $_cat; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('sound'))); - } - private function generateDefaultContent() - { /****************/ /* Main Content */ /****************/ - $this->addScript([SC_JS_FILE, '?data=zones']); - // get spawns - $map = null; if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns, 'foundIn' => Lang::sound('foundIn')]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::sound('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $__) + $this->map[3][$areaId] = ZoneList::getName($areaId); } - // get full path ingame for sound (workaround for missing PlaySoundKit()) + // get full path in-game for sound (workaround for missing PlaySoundKit()) $fullpath = DB::Aowow()->selectCell('SELECT IF(sf.`path` <> "", CONCAT(sf.`path`, "\\\\", sf.`file`), sf.`file`) FROM ?_sounds_files sf JOIN ?_sounds s ON s.`soundFile1` = sf.`id` WHERE s.`id` = ?d', $this->typeId); - $this->map = $map; - $this->headIcons = [$this->subject->getField('iconString')]; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_PLAYLIST => true, @@ -109,11 +94,15 @@ class SoundPage extends GenericPage $this->extendGlobalData($this->subject->getJSGlobals()); + parent::generate(); + /**************/ /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: Spells // skipping (always empty): ready, castertargeting, casterstate, targetstate $displayIds = DB::Aowow()->selectCol( @@ -134,9 +123,9 @@ class SoundPage extends GenericPage $cnd = array( 'OR', - ['AND', ['effect1Id', 132], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', 132], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', 132], ['effect3MiscValue', $this->typeId]] + ['AND', ['effect1Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect1MiscValue', $this->typeId]], + ['AND', ['effect2Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect2MiscValue', $this->typeId]], + ['AND', ['effect3Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect3MiscValue', $this->typeId]] ); if ($displayIds) @@ -145,19 +134,18 @@ class SoundPage extends GenericPage if ($seMiscValues) $cnd[] = array( 'OR', - ['AND', ['effect1AuraId', 260], ['effect1MiscValue', $seMiscValues]], - ['AND', ['effect2AuraId', 260], ['effect2MiscValue', $seMiscValues]], - ['AND', ['effect3AuraId', 260], ['effect3MiscValue', $seMiscValues]] + ['AND', ['effect1AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect1MiscValue', $seMiscValues]], + ['AND', ['effect2AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect2MiscValue', $seMiscValues]], + ['AND', ['effect3AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect3MiscValue', $seMiscValues]] ); $spells = new SpellList($cnd); if (!$spells->error) { $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [SpellList::$brickFile, ['data' => array_values($spells->getListviewData())]]; + $this->lvTabs->addListviewTab(new Listview(['data' => $spells->getListviewData()], SpellList::$brickFile)); } - // tab: Items $subClasses = []; if ($subClassMask = DB::Aowow()->selectCell('SELECT `subClassMask` FROM ?_items_sounds WHERE `soundId` = ?d', $this->typeId)) @@ -179,11 +167,10 @@ class SoundPage extends GenericPage if (!$items->error) { $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [ItemList::$brickFile, ['data' => array_values($items->getListviewData())]]; + $this->lvTabs->addListviewTab(new Listview(['data' => $items->getListviewData()], ItemList::$brickFile)); } } - // tab: Zones if ($zoneIds = DB::Aowow()->select('SELECT `id`, `worldStateId`, `worldStateValue` FROM ?_zones_sounds WHERE `ambienceDay` = ?d OR `ambienceNight` = ?d OR `musicDay` = ?d OR `musicNight` = ?d OR `intro` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId)) { @@ -240,14 +227,13 @@ class SoundPage extends GenericPage } } - $tabData['data'] = array_values($zoneData); + $tabData['data'] = $zoneData; $tabData['hiddenCols'] = ['territory']; - $this->lvTabs[] = [ZoneList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, ZoneList::$brickFile)); } } - // tab: Races (VocalUISounds (containing error voice overs)) if ($vo = DB::Aowow()->selectCol('SELECT `raceId` FROM ?_races_sounds WHERE `soundId` = ?d GROUP BY `raceId`', $this->typeId)) { @@ -255,11 +241,10 @@ class SoundPage extends GenericPage if (!$races->error) { $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [CharRaceList::$brickFile, ['data' => array_values($races->getListviewData())]]; + $this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile)); } } - // tab: Emotes (EmotesTextSound (containing emote audio)) if ($em = DB::Aowow()->selectCol('SELECT `emoteId` FROM ?_emotes_sounds WHERE `soundId` = ?d GROUP BY `emoteId` UNION SELECT `id` FROM ?_emotes WHERE `soundId` = ?d', $this->typeId, $this->typeId)) { @@ -267,10 +252,10 @@ class SoundPage extends GenericPage if (!$races->error) { $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [EmoteList::$brickFile, array( - 'data' => array_values($races->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $races->getListviewData(), 'name' => Util::ucFirst(Lang::game('emotes')) - ), 'emote']; + ), EmoteList::$brickFile, 'emote')); } } @@ -289,7 +274,7 @@ class SoundPage extends GenericPage `injury` = ?d OR `injurycritical` = ?d OR `death` = ?d OR `stun` = ?d OR `stand` = ?d OR `aggro` = ?d OR `wingflap` = ?d OR `wingglide` = ?d OR `alert` = ?d OR `fidget` = ?d OR `customattack` = ?d OR `loop` = ?d OR `jumpstart` = ?d OR `jumpend` = ?d OR `petattack` = ?d OR - `petorder` = ?d OR `petdismiss` = ?d OR `birth` = ?d OR `spellcast` = ?d OR `submerge` = ?d OR `submerged` = ?d', + `petorder` = ?d OR `petdismiss` = ?d OR `birth` = ?d OR `spellcast` = ?d OR `submerge` = ?d OR `submerged` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, @@ -319,10 +304,10 @@ class SoundPage extends GenericPage $npcs = new CreatureList($cnds); if (!$npcs->error) { - $this->addScript([SC_JS_FILE, '?data=zones']); - $this->extendGlobalData($npcs->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [CreatureList::$brickFile, ['data' => array_values($npcs->getListviewData())]]; + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(['data' => $npcs->getListviewData()], CreatureList::$brickFile)); } } } diff --git a/endpoints/sound/sound_playlist.php b/endpoints/sound/sound_playlist.php new file mode 100644 index 00000000..334117ae --- /dev/null +++ b/endpoints/sound/sound_playlist.php @@ -0,0 +1,26 @@ +h1 = Lang::sound('cat', 1000); + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('sound'))); + + parent::generate(); + } +} + +?> diff --git a/endpoints/sounds/sounds.php b/endpoints/sounds/sounds.php new file mode 100644 index 00000000..3ed6eaa6 --- /dev/null +++ b/endpoints/sounds/sounds.php @@ -0,0 +1,117 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [1, 2, 3, 4, 6, 9, 10, 12, 13, 14, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 50, 52, 53]; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + if ($this->category) + $this->forward('?sounds&filter=ty='.$this->category[0]); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new SoundListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('sounds')); + + $this->filter->evalCriteria(); + + $conditions = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + + /**************/ + /* Page Title */ + /**************/ + + $fiForm = $this->filter->values; + + array_unshift($this->title, $this->h1); + if (count($fiForm['ty']) == 1) + array_unshift($this->title, Lang::sound('cat', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty'][0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_PLAYLIST => true + ); + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $tabData = []; + $sounds = new SoundList($conditions, ['calcTotal' => true]); + if (!$sounds->error) + { + $tabData['data'] = $sounds->getListviewData(); + + // create note if search limit was exceeded; overwriting 'note' is intentional + if ($sounds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); + + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + public static function onBeforeDisplay() + { + // sort for dropdown-menus in filter + Lang::sort('sound', 'cat'); + } +} + +?> diff --git a/pages/sounds.php b/pages/sounds.php deleted file mode 100644 index ea064f36..00000000 --- a/pages/sounds.php +++ /dev/null @@ -1,93 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - if (isset($this->category[0])) - header('Location: ?sounds&filter=ty='.$this->category[0], true, 302); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new SoundListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); - - $this->name = Util::ucFirst(Lang::game('sounds')); - } - - protected function generateContent() - { - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_PLAYLIST => true - ); - - $this->filterObj->evalCriteria(); - - $conditions = []; - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = []; - $sounds = new SoundList($conditions, ['calcTotal' => true]); - if (!$sounds->error) - { - $tabData['data'] = array_values($sounds->getListviewData()); - - // create note if search limit was exceeded; overwriting 'note' is intentional - if ($sounds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - $this->lvTabs[] = [SoundList::$brickFile, $tabData]; - } - - protected function postCache() - { - // sort for dropdown-menus - Lang::sort('sound', 'cat'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - array_unshift($this->title, Lang::sound('cat', $form['ty'][0])); - } - - protected function generatePath() - { - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - $this->path[] = $form['ty'][0]; - } -} - -?> diff --git a/template/pages/sound-playlist.tpl.php b/template/pages/sound-playlist.tpl.php new file mode 100644 index 00000000..1d561627 --- /dev/null +++ b/template/pages/sound-playlist.tpl.php @@ -0,0 +1,68 @@ +brick('header'); +?> +
+
+
+ +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
+

h1; ?>

+ +brick('markup', ['markup' => $this->article]); ?> + +
+
+ +
+
+
+ +brick('footer'); ?> diff --git a/template/pages/sound.tpl.php b/template/pages/sound.tpl.php index b52e3841..6dbb1a32 100644 --- a/template/pages/sound.tpl.php +++ b/template/pages/sound.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
@@ -17,68 +20,19 @@ $this->brick('redButtons'); ?> -

name; ?>

+

h1; ?>

brick('article'); + $this->brick('markup', ['markup' => $this->article]); - if ($this->special): -?> -
-
- -
- -map)): - $this->brick('mapper'); - endif; + $this->brickIf($this->map, 'mapper'); ?>
    From cb523353fd448a86d10d2b02677f4f39f06eebbd Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 14 Aug 2025 01:12:42 +0200 Subject: [PATCH 682/957] Template/Update (Part 39) * implement video suggestion & management --- endpoints/admin/videos.php | 68 ++ endpoints/admin/videos_approve.php | 46 + endpoints/admin/videos_delete.php | 43 + endpoints/admin/videos_edittitle.php | 31 + endpoints/admin/videos_list.php | 23 + endpoints/admin/videos_manage.php | 31 + endpoints/admin/videos_order.php | 57 ++ endpoints/admin/videos_relocate.php | 49 + endpoints/admin/videos_sticky.php | 56 ++ endpoints/video/add.php | 124 +++ endpoints/video/complete.php | 92 ++ endpoints/video/confirm.php | 81 ++ endpoints/video/thankyou.php | 60 ++ includes/components/videomgr.class.php | 229 +++++ includes/defines.php | 4 +- includes/utilities.php | 4 +- localization/lang.class.php | 1 + localization/locale_dede.php | 14 + localization/locale_enus.php | 14 + localization/locale_eses.php | 14 + localization/locale_frfr.php | 14 + localization/locale_ruru.php | 14 + localization/locale_zhcn.php | 15 + setup/updates/1758578400_07.sql | 8 + setup/updates/1758578400_08.sql | 8 + setup/updates/1758578400_09.sql | 4 + static/js/global.js | 13 +- static/js/locale_dede.js | 2 +- static/js/locale_enus.js | 2 +- static/js/locale_eses.js | 2 +- static/js/locale_frfr.js | 2 +- static/js/locale_ruru.js | 2 +- static/js/locale_zhcn.js | 2 +- static/js/video.js | 1135 ++++++++++++++++++++++++ template/pages/admin/videos.tpl.php | 135 +++ template/pages/video.tpl.php | 82 ++ 36 files changed, 2465 insertions(+), 16 deletions(-) create mode 100644 endpoints/admin/videos.php create mode 100644 endpoints/admin/videos_approve.php create mode 100644 endpoints/admin/videos_delete.php create mode 100644 endpoints/admin/videos_edittitle.php create mode 100644 endpoints/admin/videos_list.php create mode 100644 endpoints/admin/videos_manage.php create mode 100644 endpoints/admin/videos_order.php create mode 100644 endpoints/admin/videos_relocate.php create mode 100644 endpoints/admin/videos_sticky.php create mode 100644 endpoints/video/add.php create mode 100644 endpoints/video/complete.php create mode 100644 endpoints/video/confirm.php create mode 100644 endpoints/video/thankyou.php create mode 100644 includes/components/videomgr.class.php create mode 100644 setup/updates/1758578400_07.sql create mode 100644 setup/updates/1758578400_08.sql create mode 100644 setup/updates/1758578400_09.sql create mode 100644 static/js/video.js create mode 100644 template/pages/admin/videos.tpl.php create mode 100644 template/pages/video.tpl.php diff --git a/endpoints/admin/videos.php b/endpoints/admin/videos.php new file mode 100644 index 00000000..10beece8 --- /dev/null +++ b/endpoints/admin/videos.php @@ -0,0 +1,68 @@ + Content > Videos + + protected array $scripts = array( + [SC_JS_FILE, 'js/video.js'], + [SC_CSS_STRING, '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'], + [SC_CSS_STRING, '#highlightedRow { background-color: #322C1C; }'] + ); + protected array $expectedGET = array( + 'action' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']], + 'type' => ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode' ] + ); + + public ?bool $getAll = null; + public array $viPages = []; + public array $viData = []; + public int $viNFound = 0; + public array $pageTypes = []; + + protected function generate() : void + { + $this->h1 = 'Video Manager'; + + // types that can have videos + foreach (Type::getClassesFor(0, 'contribute', CONTRIBUTE_SS) as $type => $obj) + $this->pageTypes[$type] = Util::ucWords(Lang::game(Type::getFileString($type))); + + $viGetAll = $this->_get['all']; + $viPages = []; + $viData = []; + $nMatches = 0; + + if ($this->_get['type'] && $this->_get['typeid']) + $viData = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid'], nFound: $nMatches); + else if ($this->_get['user']) + { + if (mb_strlen($this->_get['user']) >= 3) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + $viData = VideoMgr::getVideos(userId: $uId, nFound: $nMatches); + } + else + $viPages = VideoMgr::getPages($viGetAll, $nMatches); + + $this->getAll = $viGetAll; + $this->viPages = $viPages; + $this->viData = $viData; + $this->viNFound = $nMatches; // ssm_numPagesFound + + parent::generate(); + } +} diff --git a/endpoints/admin/videos_approve.php b/endpoints/admin/videos_approve.php new file mode 100644 index 00000000..32e866e9 --- /dev/null +++ b/endpoints/admin/videos_approve.php @@ -0,0 +1,46 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionApproveResponse - videoId empty', E_USER_ERROR); + return; + } + + $viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_APPROVED, $this->_get['id']); + foreach ($viEntries as $id => $viData) + { + // set as approved in DB + DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + + // gain siterep + Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]); + + // flag DB entry as having videos + if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable')) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + + unset($viEntries[$id]); + } + + if (!$viEntries) + trigger_error('AdminVideosActionApproveResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or already approved', E_USER_WARNING); + } +} diff --git a/endpoints/admin/videos_delete.php b/endpoints/admin/videos_delete.php new file mode 100644 index 00000000..d3b6c19e --- /dev/null +++ b/endpoints/admin/videos_delete.php @@ -0,0 +1,43 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + // 2 steps: 1) remove from sight, 2) remove from disk + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionDeleteResponse - videoId empty', E_USER_ERROR); + return; + } + + // irrevocably purge files already flagged as deleted (should only exist as pending) + if (User::isInGroup(U_GROUP_ADMIN)) + DB::Aowow()->selectCell('SELECT 1 FROM ?_videos WHERE `status` & ?d AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + + // flag as deleted if not aready + $oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ?_videos WHERE `id` IN (?a) GROUP BY `type`', $this->_get['id']); + DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdDelete` = ?d WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, User::$id, CC_FLAG_DELETED, $this->_get['id']); + + // deflag db entry as having videos + foreach ($oldEntries as $type => $typeIds) + { + $typeIds = explode(',', $typeIds); + $toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds); + if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable'))) + DB::Aowow()->query('UPDATE ?# SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', $tbl, CUSTOM_HAS_VIDEO, array_keys($toUnflag)); + } + } +} diff --git a/endpoints/admin/videos_edittitle.php b/endpoints/admin/videos_edittitle.php new file mode 100644 index 00000000..d9c92dfe --- /dev/null +++ b/endpoints/admin/videos_edittitle.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + protected array $expectedPOST = array( + 'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + return; + + $caption = $this->handleCaption($this->_post['title']); + + DB::Aowow()->query('UPDATE ?_videos SET `caption` = ? WHERE `id` = ?d', $caption, $this->_get['id'][0]); + } +} diff --git a/endpoints/admin/videos_list.php b/endpoints/admin/videos_list.php new file mode 100644 index 00000000..7b02d12b --- /dev/null +++ b/endpoints/admin/videos_list.php @@ -0,0 +1,23 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + protected function generate() : void + { + $pages = VideoMgr::getPages($this->_get['all'], $nPages); + $this->result = 'vim_videoPages = '.Util::toJSON($pages).";\n"; + $this->result .= 'vim_numPagesFound = '.$nPages.';'; + } +} diff --git a/endpoints/admin/videos_manage.php b/endpoints/admin/videos_manage.php new file mode 100644 index 00000000..0bd4e31e --- /dev/null +++ b/endpoints/admin/videos_manage.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] + ); + + protected function generate() : void + { + $res = []; + + if ($this->_get['type'] && $this->_get['typeid']) + $res = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid']); + else if ($this->_get['user']) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + $res = VideoMgr::getVideos(userId: $uId); + + $this->result = 'vim_videoData = '.Util::toJSON($res); + } +} diff --git a/endpoints/admin/videos_order.php b/endpoints/admin/videos_order.php new file mode 100644 index 00000000..f11f64eb --- /dev/null +++ b/endpoints/admin/videos_order.php @@ -0,0 +1,57 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'] ], + 'move' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -1, 'max_range' => 1]] // -1 = up, 1 = down + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'move') || $this->_get['move'] === 0) + { + trigger_error('AdminVideosActionOrderResponse - id or move empty', E_USER_ERROR); + return; + } + + $id = $this->_get['id'][0]; + + $videos = DB::Aowow()->selectCol('SELECT a.`id` AS ARRAY_KEY, a.`pos` FROM ?_videos a, ?_videos b WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND (a.`status` & ?d) = 0 AND b.`id` = ?d ORDER BY a.`pos` ASC', CC_FLAG_DELETED, $id); + if (!$videos || count($videos) == 1) + { + trigger_error('AdminVideosActionOrderResponse - not enough videos to sort', E_USER_WARNING); + return; + } + + $dir = $this->_get['move']; + $curPos = $videos[$id]; + + if ($dir == -1 && $curPos == 0) + { + trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in top position', E_USER_WARNING); + return; + } + + if ($dir == 1 && $curPos + 1 == count($videos)) + { + trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in bottom position', E_USER_WARNING); + return; + } + + $oldKey = array_search($curPos + $dir, $videos); + $videos[$oldKey] -= $dir; + $videos[$id] += $dir; + + foreach ($videos as $id => $pos) + DB::Aowow()->query('UPDATE ?_videos SET `pos` = ?d WHERE `id` = ?d', $pos, $id); + } +} diff --git a/endpoints/admin/videos_relocate.php b/endpoints/admin/videos_relocate.php new file mode 100644 index 00000000..aa2bd6a1 --- /dev/null +++ b/endpoints/admin/videos_relocate.php @@ -0,0 +1,49 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ] + // (but not type..?) + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'typeid')) + { + trigger_error('AdminVideosActionRelocateResponse - videoId or typeId empty', E_USER_ERROR); + return; + } + + $id = $this->_get['id'][0]; + [$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ?_videos WHERE `id` = ?d', $id)); + $typeId = $this->_get['typeid']; + + if (Type::validateIds($type, $typeId)) + { + $tbl = Type::getClassAttrib($type, 'dataTable'); + + // move video + DB::Aowow()->query('UPDATE ?_videos SET `typeId` = ?d WHERE `id` = ?d', $typeId, $id); + + // flag target as having video + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $typeId); + + // deflag source for having had videos (maybe) + $viInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `status`& ?d AND `type` = ?d AND `typeId` = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId); + if ($viInfo || !$viInfo['hasMore']) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $oldTypeId); + } + else + trigger_error('AdminVideosActionRelocateResponse - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR); + } +} diff --git a/endpoints/admin/videos_sticky.php b/endpoints/admin/videos_sticky.php new file mode 100644 index 00000000..ea79ac4b --- /dev/null +++ b/endpoints/admin/videos_sticky.php @@ -0,0 +1,56 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionStickyResponse - videoId empty', E_USER_ERROR); + return; + } + + // this one is a bit strange: as far as i've seen, the only thing a 'sticky' video does is show up in the infobox + // this also means, that only one video per page should be sticky + // so, handle it one by one and the last one affecting one particular type/typId-key gets the cake + $viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + foreach ($viEntries as $id => $viData) + { + // approve yet unapproved videos + if (!($viData['status'] & CC_FLAG_APPROVED)) + { + // set as approved in DB + DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + + // gain siterep + Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]); + + // flag DB entry as having videos + if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable')) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + } + + // reset all others + DB::Aowow()->query('UPDATE ?_videos a, ?_videos b SET a.`status` = a.`status` & ~?d WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = ?d', CC_FLAG_STICKY, $id); + + // toggle sticky status + DB::Aowow()->query('UPDATE ?_videos SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE `id` = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); + + unset($viEntries[$id]); + } + + if ($viEntries) + trigger_error('AdminVideosActionStickyResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or flagged as deleted', E_USER_WARNING); + } +} diff --git a/endpoints/video/add.php b/endpoints/video/add.php new file mode 100644 index 00000000..36c67ebc --- /dev/null +++ b/endpoints/video/add.php @@ -0,0 +1,124 @@ + 1. =add: receives user upload + 1.1. checks and processing on the upload + 1.2. forward to =confirm or blank response + 2. =confirm: user edites upload + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoAddResponse extends TextResponse +{ + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'videourl' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private string $videoHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + + // only accept/expect hash for confirm & complete + if ($videoHash) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleAdd()) + $this->redirectTo = '?video=confirm&'.$this->destType.'.'.$this->destTypeId.'.'.$this->videoHash; + else if ($this->destType && $this->destTypeId) + $this->redirectTo = '?'.Type::getFileString($this->destType).'='.$this->destTypeId.'#suggest-a-video'; + else + $this->generate404(); + } + + private function handleAdd() : bool + { + if (!User::canSuggestVideo()) + { + $_SESSION['error']['vi'] = Lang::video('error', 'notAllowed'); + return false; + } + + if (!$this->assertPOST('videourl')) + { + $_SESSION['error']['vi'] = Lang::video('error', 'selectVI'); + return false; + } + + $videoId = ''; + if (preg_match('/^https?:\/\/(www\.)?youtu(\.be|be\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/', $this->_post['videourl'], $m)) + $videoId = $m[3]; + else + { + $_SESSION['error']['vi'] = Lang::video('error', 'selectVI'); + return false; + } + + $curl = curl_init('https://youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v='.$videoId); + if (!$curl) + { + trigger_error('VideoAddResponse - curl_init fail', E_USER_ERROR); + $_SESSION['error']['vi'] = Lang::main('intError'); + return false; + } + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + $ytOembed = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + curl_close($curl); + + if ($status == 401) + { + $_SESSION['error']['vi'] = Lang::video('error', 'isPrivate'); + return false; + } + else if ($status != 200) // 404, 500 seen .. does it matter why its inaccessible? + { + $_SESSION['error']['vi'] = Lang::video('error', 'noExist'); + return false; + } + + $videoInfo = json_decode($ytOembed); + $videoInfo->id = $videoId; + + if (!VideoMgr::saveSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + return true; + } +} + +?> diff --git a/endpoints/video/complete.php b/endpoints/video/complete.php new file mode 100644 index 00000000..46cf9d0b --- /dev/null +++ b/endpoints/video/complete.php @@ -0,0 +1,92 @@ + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoCompleteResponse extends TextResponse +{ + use TrCommunityHelper; + + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'caption' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private string $videoHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $this->videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleComplete()) + $this->forward('?video=thankyou&'.$this->destType.'.'.$this->destTypeId); + else + $this->generate404(); + } + + private function handleComplete() : bool + { + if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + $this->generate404(); + + $pos = DB::Aowow()->selectCell('SELECT MAX(`pos`) FROM ?_videos WHERE `type` = ?d AND `typeId` = ?d AND (`status` & ?d) = 0', $this->destType, $this->destTypeId, CC_FLAG_DELETED); + if (!is_int($pos)) + $pos = -1; + + // write to db + $newId = DB::Aowow()->query( + 'INSERT INTO ?_videos (`type`, `typeId`, `userIdOwner`, `date`, `videoId`, `pos`, `url`, `width`, `height`, `name`, `caption`, `status`) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?, ?d, ?, ?d, ?d, ?, ?, 0)', + $this->destType, $this->destTypeId, User::$id, + $videoInfo->id, + $pos + 1, + $videoInfo->thumbnail_url, + $videoInfo->thumbnail_width, + $videoInfo->thumbnail_height, + $videoInfo->title, + $this->handleCaption($this->_post['caption']) + ); + + if (!is_int($newId)) // 0 is valid, NULL or FALSE is not + { + trigger_error('VideoCompleteResponse - video query failed', E_USER_ERROR); + return false; + } + + VideoMgr::dropTempFile(); + + return true; + } +} + +?> diff --git a/endpoints/video/confirm.php b/endpoints/video/confirm.php new file mode 100644 index 00000000..055670ef --- /dev/null +++ b/endpoints/video/confirm.php @@ -0,0 +1,81 @@ + 2. =crop: user edites upload + 2.1. just show edit page + 2.2. user submits coords and description to =complete + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoConfirmResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'video'; + protected string $pageName = 'video'; + + public ?Markup $infobox = null; + public string $videoHash = ''; + public int $destType = 0; + public int $destTypeId = 0; + public string $url = ''; + public int $width = 0; + public int $height = 0; + public array $video = []; + public string $viTitle = ''; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId, , $this->videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::video('submission'); + array_unshift($this->title, $this->h1); + + if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + $this->generateError(); + + $this->viTitle = $videoInfo->title; + $this->url = $videoInfo->thumbnail_url; + $this->width = $videoInfo->thumbnail_width; + $this->height = $videoInfo->thumbnail_height; + $this->video = [[ + 'videoType' => VideoMgr::TYPE_YOUTUBE, + 'videoId' => $videoInfo->id, + 'caption' => $videoInfo->title + ]]; + + // target + $this->infobox = new Markup(Lang::screenshot('displayOn', [Lang::typeName($this->destType), Type::getFileString($this->destType), $this->destTypeId]), ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->extendGlobalIds($this->destType, $this->destTypeId); + + parent::generate(); + } +} + +?> diff --git a/endpoints/video/thankyou.php b/endpoints/video/thankyou.php new file mode 100644 index 00000000..c90e7922 --- /dev/null +++ b/endpoints/video/thankyou.php @@ -0,0 +1,60 @@ + 4. =thankyou +*/ + +class VideoThankyouResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'video'; + + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&. + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::video('submission'); + + array_unshift($this->title, $this->h1); + + $this->extraHTML = Lang::video('thanks', 'contrib').'

    '; + $this->extraHTML .= Lang::video('thanks', 'goBack', [Type::getFileString($this->destType), $this->destTypeId])."

    \n"; + $this->extraHTML .= ''.Lang::video('thanks', 'note').''; + + parent::generate(); + } +} + +?> diff --git a/includes/components/videomgr.class.php b/includes/components/videomgr.class.php new file mode 100644 index 00000000..a9b6e07f --- /dev/null +++ b/includes/components/videomgr.class.php @@ -0,0 +1,229 @@ +id . PHP_EOL); + fwrite($tmpFile, $videoInfo->title . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_url . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_height . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_width . PHP_EOL); + + return fclose($tmpFile); + } + + public static function loadSuggestion(?\stdClass &$videoInfo, int $destType, int $destTypeId, ?string $uid) : bool + { + self::$tmpFile = sprintf(self::PATH_TEMP, User::$username.'-'.$destType.'-'.$destTypeId.'-'.$uid); + + if (!file_exists(self::$tmpFile)) + return false; + + if ($info = file(self::$tmpFile, FILE_IGNORE_NEW_LINES)) + { + $videoInfo = new \stdClass; + $videoInfo->id = $info[0]; + $videoInfo->title = $info[1]; + $videoInfo->thumbnail_url = $info[2]; + $videoInfo->thumbnail_height = (int)$info[3]; + $videoInfo->thumbnail_width = (int)$info[4]; + + return true; + } + + return false; + } + + public static function dropTempFile() + { + if (!self::$tmpFile || !file_exists(self::$tmpFile)) + return; + + unlink(self::$tmpFile); + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + public static function getVideos(int $type = 0, int $typeId = 0, $userId = 0, ?int &$nFound = 0) : array + { + /* VideoData + * caption: caption + * date: isodate + * height: ytPreviewImgHeight? + * width: ytPreviewImgWidth? + * id: id + * next: idx || null + * prev: idx || null + * name: ytTitle? + * pending: bool + * status: statusCode + * type: dbType + * typeId: typeId + * user: userName + * url: ytPreviewImg? + * videoType: always 1 + * videoId: videoId + * unique: bool || null + */ + + $videos = DB::Aowow()->select( + 'SELECT v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`type`, v.`typeId`, v.`caption`, v.`status` AS "flags", v.`url`, v.`name` + FROM ?_videos v + LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id` + WHERE + { v.`type` = ?d } + { AND v.`typeId` = ?d } + { v.`userIdOwner` = ?d } + { LIMIT ?d } + ORDER BY `type`, `typeId`, `pos` ASC', + $userId ? DBSIMPLE_SKIP : $type, + $userId ? DBSIMPLE_SKIP : $typeId, + $userId ? $userId : DBSIMPLE_SKIP, + $userId || $type ? DBSIMPLE_SKIP : 100 + ); + + $num = []; + foreach ($videos as $v) + { + if (empty($num[$v['type']][$v['typeId']])) + $num[$v['type']][$v['typeId']] = 1; + else + $num[$v['type']][$v['typeId']]++; + } + + $nFound = 0; + + // format data to meet requirements of the js + foreach ($videos as $i => &$v) + { + $nFound++; + + $v['date'] = date(Util::$dateFormatInternal, $v['date']); + $v['videoType'] = self::TYPE_YOUTUBE; + + if ($i > 0) + $v['prev'] = $i - 1; + + if (($i + 1) < count($videos)) + $v['next'] = $i + 1; + + // order gives priority for 'status' + if (!($v['flags'] & CC_FLAG_APPROVED)) + { + $v['pending'] = 1; + $v['status'] = self::STATUS_PENDING; + } + else + $v['status'] = self::STATUS_APPROVED; + + if ($v['flags'] & CC_FLAG_STICKY) + { + $v['sticky'] = 1; + $v['status'] = self::STATUS_STICKY; + } + + if ($v['flags'] & CC_FLAG_DELETED) + { + $v['deleted'] = 1; + $v['status'] = self::STATUS_DELETED; + } + + // something todo with massSelect .. am i doing this right? + if ($num[$v['type']][$v['typeId']] == 1) + $v['unique'] = 1; + + if (!$v['user']) + unset($v['user']); + } + + return $videos; + } + + public static function getPages(?bool $all, ?int &$nFound) : array + { + // i GUESS .. vi_getALL ? everything : pending + $nFound = 0; + $pages = DB::Aowow()->select( + 'SELECT v.`type`, v.`typeId`, COUNT(1) AS "count", MIN(v.`date`) AS "date" + FROM ?_videos v + { WHERE (v.`status` & ?d) = 0 } + GROUP BY v.`type`, v.`typeId`', + $all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED | CC_FLAG_DELETED + ); + + if ($pages) + { + // limit to one actually existing type each + foreach (array_unique(array_column($pages, 'type')) as $t) + { + $ids = []; + foreach ($pages as $row) + if ($row['type'] == $t) + $ids[] = $row['typeId']; + + if (!$ids) + continue; + + $obj = Type::newList($t, [Cfg::get('SQL_LIMIT_NONE'), ['id', $ids]]); + if (!$obj || $obj->error) + continue; + + foreach ($pages as &$p) + if ($p['type'] == $t) + if ($obj->getEntry($p['typeId'])) + $p['name'] = $obj->getField('name', true); + } + + foreach ($pages as &$p) + { + if (empty($p['name'])) + { + trigger_error('VideoMgr::getPages - video linked to nonexistent type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); + unset($p); + } + else + { + $nFound += $p['count']; + $p['date'] = date(Util::$dateFormatInternal, $p['date']); + } + } + } + + return $pages; + } +} + +?> diff --git a/includes/defines.php b/includes/defines.php index 49e18275..a9f440ef 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -90,7 +90,7 @@ define('SITEREP_ACTION_DAILYVISIT', 2); // Daily visit define('SITEREP_ACTION_COMMENT', 3); // Posted comment define('SITEREP_ACTION_UPVOTED', 4); // Your comment was upvoted define('SITEREP_ACTION_DOWNVOTED', 5); // Your comment was downvoted -define('SITEREP_ACTION_SUBMIT_SCREENSHOT', 6); // Submitted screenshot (suggested video) +define('SITEREP_ACTION_SUBMIT_SCREENSHOT', 6); // Submitted screenshot // Cast vote // Uploaded data define('SITEREP_ACTION_GOOD_REPORT', 9); // Report accepted @@ -98,7 +98,7 @@ define('SITEREP_ACTION_BAD_REPORT', 10); // Report declined // Copper Achievement // Silver Achievement // Gold Achievement - // Test 1 +define('SITEREP_ACTION_SUGGEST_VIDEO', 14); // repurposed, originally: Test 1 // Test 2 define('SITEREP_ACTION_ARTICLE', 16); // Guide approved (article approved) define('SITEREP_ACTION_USER_WARNED', 17); // Moderator Warning diff --git a/includes/utilities.php b/includes/utilities.php index 5ead86dc..10d84995 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -727,12 +727,12 @@ abstract class Util $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? Cfg::get('REP_REWARD_UPVOTED') : Cfg::get('REP_REWARD_DOWNVOTED'); break; case SITEREP_ACTION_SUBMIT_SCREENSHOT: + case SITEREP_ACTION_SUGGEST_VIDEO: if (empty($miscData['id']) || empty($miscData['what'])) return false; $x['sourceA'] = $miscData['id']; // screenshotId or videoId - $x['sourceB'] = $miscData['what']; // screenshot:1 - $x['amount'] = Cfg::get('REP_REWARD_UPLOAD'); + $x['amount'] = $action == SITEREP_ACTION_SUBMIT_SCREENSHOT ? Cfg::get('REP_REWARD_SUBMIT_SCREENSHOT') : Cfg::get('REP_REWARD_SUGGEST_VIDEO'); break; case SITEREP_ACTION_GOOD_REPORT: // NYI case SITEREP_ACTION_BAD_REPORT: diff --git a/localization/lang.class.php b/localization/lang.class.php index 1bde86d4..e496c36b 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -16,6 +16,7 @@ class Lang private static $maps; private static $profiler; private static $screenshot; + private static $video; private static $privileges; private static $smartAI; private static $unit; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 9369227d..c3e0007b 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "Es ist euch nicht erlaubt einen Screenshot hochzuladen!", ) ), + 'video' => array( + 'submission' => "Video-Einsendung", + 'thanks' => array( + 'contrib' => "Vielen Dank für Euren Beitrag!", + 'goBack' => 'Klickt hier, um zu der vorherigen Seite zurückzukehren.', + 'note' => "Hinweis: Euer Video muss zunächst zugelassen werden, bevor es auf der Seite erscheint. Dies kann bis zu 72 Stunden dauern." + ), + 'error' => array( + 'isPrivate' => "Das vorgeschlagene Video ist privat.", + 'noExist' => "An der eingereichten Url existiert kein Video.", + 'selectVI' => "Bitte gebt gültige Videoinformationen ein.", + 'notAllowed' => "Es ist euch nicht erlaubt Videos vorzuschlagen!" + ) + ), 'game' => array( // type strings 'npc' => "NPC", // 1 diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 4628acc2..e4a7fe76 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "You are not allowed to upload screenshots!", ) ), + 'video' => array( + 'submission' => "Video Suggestion", + 'thanks' => array( + 'contrib' => "Thanks a lot for your contribution!", + 'goBack' => 'Click here to go back to the page you came from.', + 'note' => "Note: Your video will need to be approved before appearing on the site. This can take up to 72 hours." + ), + 'error' => array( + 'isPrivate' => "The suggested video is private.", + 'noExist' => "No video found at the provided Url.", + 'selectVI' => "Please enter valid video information.", // message_novideo + 'notAllowed' => "You are not allowed to suggest videos!", + ) + ), 'game' => array( // type strings 'npc' => "NPC", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 82ba9a10..95a75707 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "¡No estás permitido para subir capturas de pantalla!", ) ), + 'video' => array( + 'submission' => "Sugerencia de video", + 'thanks' => array( + 'contrib' => "¡Muchísimas gracias por tu aportación!", + 'goBack' => 'aquí vuelve a la página de la que viniste.', + 'note' => "Nota: Tu video tiene que ser aprobado antes de que pueda aparecer en el sitio. Esto puede tomar hasta 72 horas." + ), + 'error' => array( + 'isPrivate' => "El video sugerido es privado.", + 'noExist' => "No se encontró ningún video en la URL proporcionada.", + 'selectVI' => "Por favor, introduce información válida del vídeo.", // message_novideo + 'notAllowed' => "¡No tienes permiso para sugerir videos!", + ) + ), 'game' => array( // type strings 'npc' => "PNJ", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 96e90a94..7123dde1 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "Vous n'êtes pas autorisés à exporter des captures d'écran.", ) ), + 'video' => array( + 'submission' => "Suggestion de vidéo", + 'thanks' => array( + 'contrib' => "Merci beaucoup de votre contribution!", + 'goBack' => 'ici pour retourner à la page d\'où vous venez.', + 'note' => "Note : Votre vidéo devra être approuvée avant d'apparaître sur le site. Cela peut prendre jusqu'à 72 heures." + ), + 'error' => array( + 'isPrivate' => "La vidéo suggérée est privée.", + 'noExist' => "Aucune vidéo trouvée à l'URL fournie.", + 'selectVI' => "Veuillez entrer des informations valides pour la vidéo.", // message_novideo + 'notAllowed' => "Vous n'êtes pas autorisé à suggérer des vidéos!", + ) + ), 'game' => array( // type strings 'npc' => "PNJ", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 930b776a..6e0de9a1 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "[You are not allowed to upload screenshots!]", ) ), + 'video' => array( + 'submission' => "Предложить видео", + 'thanks' => array( + 'contrib' => "Спасибо за ваш вклад!", + 'goBack' => 'здесь чтобы перейти к предыдущей странице.', + 'note' => "Примечание: Ваше видео должно быть одобрено, прежде чем появится на сайте. Это может занять до 72 часов." + ), + 'error' => array( + 'isPrivate' => "Предложенное видео является приватным.", + 'noExist' => "Видео по предоставленной ссылке не найдено.", + 'selectVI' => "введите корректную информацию о видео.", // message_novideo + 'notAllowed' => "У вас нет прав предлагать видео!", + ) + ), 'game' => array( // type strings 'npc' => "НИП", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index a5c5912c..42e090cb 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -278,7 +278,22 @@ $lang = array( 'notAllowed' => "你不允许上传截图!", ) ), + 'video' => array( + 'submission' => "视频建议", + 'thanks' => array( + 'contrib' => "非常感谢你的贡献!", + 'goBack' => '点击这里返回上一页。', + 'note' => "注意:您的视频需要经过审核后才能显示在网站上。这需要最多72小时。" + ), + 'error' => array( + 'isPrivate' => "建议的视频为私有。", + 'noExist' => "在提供的链接中未找到视频。", + 'selectVI' => "请输入有效的视频信息。", // message_novideo + 'notAllowed' => "您没有权限建议视频!", + ) + ), 'game' => array( + // type strings 'npc' => "NPC", 'npcs' => "NPC", 'object' => "对象", diff --git a/setup/updates/1758578400_07.sql b/setup/updates/1758578400_07.sql new file mode 100644 index 00000000..4ba7523f --- /dev/null +++ b/setup/updates/1758578400_07.sql @@ -0,0 +1,8 @@ +-- `key` is too small for our new configs +ALTER TABLE `aowow_config` + MODIFY COLUMN `key` varchar(50) NOT NULL; + +-- split generic upload in ss / vi +UPDATE `aowow_config` SET `key` = 'rep_reward_submit_screenshot', `comment` = 'uploaded screenshot was approved' WHERE `key` = 'rep_reward_upload'; +DELETE FROM `aowow_config` WHERE `key` = 'rep_reward_suggest_video'; +INSERT INTO `aowow_config` VALUES ('rep_reward_suggest_video', '10', '10', 5, 129, 'suggested video was approved'); diff --git a/setup/updates/1758578400_08.sql b/setup/updates/1758578400_08.sql new file mode 100644 index 00000000..f7ad1861 --- /dev/null +++ b/setup/updates/1758578400_08.sql @@ -0,0 +1,8 @@ +-- update video storage +ALTER TABLE `aowow_videos` + ADD COLUMN `pos` tinyint unsigned NOT NULL AFTER `videoId`, + ADD COLUMN `url` varchar(64) NOT NULL COMMENT 'preview thumb' AFTER `pos`, + ADD COLUMN `width` smallint unsigned NOT NULL AFTER `url`, + ADD COLUMN `height` smallint unsigned NOT NULL AFTER `width`, + ADD COLUMN `name` varchar(64) DEFAULT NULL AFTER `height`, + MODIFY COLUMN `caption` varchar(200) DEFAULT NULL; diff --git a/setup/updates/1758578400_09.sql b/setup/updates/1758578400_09.sql new file mode 100644 index 00000000..4af1c23d --- /dev/null +++ b/setup/updates/1758578400_09.sql @@ -0,0 +1,4 @@ +-- update article affected by cfg change +UPDATE `aowow_acticles` SET + `article` = '[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]REP_REWARD_SUBMIT_SCREENSHOT reputation[/td]\r\n[/tr]\r\n[tr][td]Suggesting a video[/td]\r\n[td align=right class=no-wrap]REP_REWARD_SUGGEST_VIDEO reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n' +WHERE `url` = 'reputation' AND `locale` = 0; diff --git a/static/js/global.js b/static/js/global.js index 91d49851..453f644e 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -2831,9 +2831,10 @@ var vi_siteurls = { 1: 'https://www.youtube.com/watch?v=$1' // YouTube }; -var vi_sitevalidation = { - 1: /^https?:\/\/www\.youtube\.com\/watch\?v=([^& ]{11})/ // YouTube -}; +var vi_sitevalidation = [ + /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([^& ]{11})/i, + /https?:\/\/(?:www\.)?youtu\.be\/([^& ]{11})/i +]; function vi_submitAVideo() { tabsContribute.focus(2); @@ -2882,7 +2883,7 @@ function vi_appendSticky() { }; var img = $WH.ce('img'); - img.src = $WH.sprintf(vi_thumbnails[video.videoType], video.videoId); + img.src = $WH.sprintf(vi_thumbnails[video.videoType].replace(/\/default\.jpg/, '/mqdefault.jpg'), video.videoId); img.className = 'border'; $WH.ae(a, img); @@ -3227,7 +3228,7 @@ var VideoViewer = new function() { aCover.onclick = Lightbox.hide; var foo = $WH.ce('span'); var b = $WH.ce('b'); - $WH.ae(b, $WH.ct(LANG.close)); + // $WH.ae(b, $WH.ct(LANG.close)); $WH.ae(foo, b); $WH.ae(aCover, foo); @@ -3312,7 +3313,7 @@ var VideoViewer = new function() { onShow: onShow, onHide: onHide, onResize: onResize - },opt); + }, opt); return false; } diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 7ee34a44..32d67761 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Bronzeerfolg", "Silbererfolg", "Golderfolg", - 'Test 1', + 'Video vorgeschlagen', // aowow - originally: Test 1 'Test 2', "Leitfaden zugelassen", "Warnung durch Moderator", diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index a06c0c6f..4aacd372 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Copper Achievement", "Silver Achievement", "Gold Achievement", - 'Test 1', + 'Video suggested', // aowow - originally: Test 1 'Test 2', "Guide approved", "Moderator Warning", diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index e4f82e5a..6cbf3d68 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Logro de Cobre", "Logro de Plata", "Logro de Oro", - 'Test 1', + "Vídeo sugerido", // aowow - originally: Test 1 'Test 2', "Guía aprobada", "Aviso de moderador", diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 42128086..747196f1 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Haut-fait de bronze", "Haut-fait d'argent", "Haut-fait d'or", - 'Test 1', + "Vidéo suggérée", // aowow - originally: Test 1 'Test 2', "Guide approuvé", "Avertissement d'un modérateur", diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 8851c21b..0211ba2c 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Бронзовое достижение", "Серебряное достижение", "Золотое достижение", - "Test 1", + "Видео предложено", // aowow - originally: Test 1 "Test 2", "Гайд одобрен", "Пожаловаться модератору", diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 53d7cfda..186bb754 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "铜牌成就", "银牌成就", "金牌成就", - 'Test 1', + '建议视频', // aowow - originally: Test 1 'Test 2', "指南通过审核", "管理员警告", diff --git a/static/js/video.js b/static/js/video.js new file mode 100644 index 00000000..545ca01c --- /dev/null +++ b/static/js/video.js @@ -0,0 +1,1135 @@ +var vi_managedRow = null; +var vi_getAll = false; +var vim_ViewedRow = null; +var vim_videoData = []; +var vim_videoPages = []; +var vim_numPagesFound = 0; +var vim_numPages = 0; +var vim_numPending = 0; +var vim_statuses = { + 0 : 'Pending', + 999: 'Deleted', + 100: 'Approved', + 105: 'Sticky' +}; + +function makePipe() { + var sp = $WH.ce('span'); + $WH.ae(sp, $WH.ct(' ')); + + var b = $WH.ce('small'); + b.className = 'q0'; + $WH.ae(b, $WH.ct('|')); + + $WH.ae(sp, b); + $WH.ae(sp, $WH.ct(' ')); + + return sp; +} + +function vi_OnResize() { + var a = Math.max(100, Math.min($WH.g_getWindowSize().h - 50, 700)); + + $WH.ge('menu-container').style.height = $WH.ge('pages-container').style.height = a + 'px'; + $WH.ge('data-container').style.height = a + 'px'; +} + +$WH.aE(window, 'resize', vi_OnResize); + +function vi_Refresh(openNext, type, typeId) { + new Ajax('?admin=videos&action=list' + (vi_getAll ? '&all': ''), { + method: 'get', + onSuccess: function (xhr) { + eval(xhr.responseText); + + if (vim_videoPages.length > 0) { + $WH.ge('show-all-pages').innerHTML = ' – Show All (' + vim_numPagesFound + ')'; + + vim_UpdatePages(); + + if (openNext) + vi_Manage($WH.ge('pages-container').firstChild.firstChild, vim_videoPages[0].type, vim_videoPages[0].typeId, true); + else if (type && typeId) + vi_Manage(null, type, typeId, true); + } + else { + $WH.ee($WH.ge('show-all-pages')); + $WH.ge('pages-container').innerHTML = 'NO VIDEOZ NEEDS 2 BE APPRVED NOW KTHX. :)'; + if (type && typeId) + vi_Manage(null, type, typeId, true); + } + } + }) +} + +function vi_Manage(_this, type, typeId, openNext) { + new Ajax('?admin=videos&action=manage&type=' + type + '&typeid=' + typeId, { + method: 'get', + onSuccess: function (xhr) { + + eval(xhr.responseText); + vim_numPending = 0; + + for (var i in vim_videoData) + if (vim_videoData[i].pending) + vim_numPending++; + + var nRows = vim_videoData.length; + $WH.ge('videoTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); + + vim_UpdateList(openNext); + vim_UpdateMassLinks(); + + if (vi_managedRow != null) + vi_ColorizeRow('transparent'); + + vi_managedRow = _this; + + if (vi_managedRow != null) + vi_ColorizeRow('#282828'); + } + }); +} + +function vi_ManageUser() { + var username = $WH.ge('usermanage'); + username.value = $WH.trim(username.value); + + if (username.value.length < 4) { + alert('Username must be at least 4 characters long.'); + username.focus(); + + return false + } + + if (username.value.match(/[^a-z0-9]/i) != null) { + alert('Username can only contain letters and numbers.'); + username.focus(); + + return false + } + + new Ajax('?admin=videos&action=manage&user=' + username.value, { + method: 'get', + onSuccess: function (xhr) { + eval(xhr.responseText); + + var nRows = vim_videoData.length; + $WH.ge('videoTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); + + vim_UpdateList(); + vim_UpdateMassLinks(); + + if (vi_managedRow != null) + vi_ColorizeRow('transparent'); + } + }); + + return true +} + +function vi_ColorizeRow(color) { + for (var i = 0; i < vi_managedRow.childNodes.length; ++i) + vi_managedRow.childNodes[i].style.backgroundColor = color; +} + +function vim_GetVideo(id) { + for (var i in vim_videoData) + if (vim_videoData[i].id == id) + return vim_videoData[i]; + + return null +} + +function vim_View(row, id) { + if (vim_ViewedRow != null) + vim_ColorizeRow('transparent'); + + vim_ViewedRow = row; + vim_ColorizeRow('#282828'); + + var video = vim_GetVideo(id); + if (video != null) + VideoManager.show(video); +} + +function vim_ColorizeRow(color) { + for (var i = 0; i < vim_ViewedRow.childNodes.length; ++i) + vim_ViewedRow.childNodes[i].style.backgroundColor = color; +} + +function vim_ConfirmMassApprove() { + ajaxAnchor(this); // aowow custom - same endpoint gets used as ajax and page .. what? + + return false; + // return true; +} + +function vim_ConfirmMassDelete() { + if (confirm('Delete selected video(s)?')) // aowow custom - see above + ajaxAnchor(this); + + return false; + // return confirm('Delete selected video(s)?'); +} + +function vim_ConfirmMassSticky() { + if (confirm('Sticky selected video(s)?')) // aowow custom - see above + ajaxAnchor(this); + + return false; + // return confirm('Sticky selected video(s)?'); +} + +function vim_UpdatePages(UNUSED) { + var pc = $WH.ge('pages-container'); + $WH.ee(pc); + + var tbl = $WH.ce('table'); + tbl.className = 'grid'; + tbl.style.width = '400px'; + + var tr = $WH.ce('tr'); + + var th = $WH.ce('th'); + $WH.ae(th, $WH.ct('Page')); + $WH.ae(tr, th); + + th = $WH.ce('th'); + $WH.ae(th, $WH.ct('Submitted')); + $WH.ae(tr, th); + + th = $WH.ce('th'); + th.align = 'right'; + $WH.ae(th, $WH.ct('#')); + $WH.ae(tr, th); + + $WH.ae(tbl, tr); + + var now = new Date(); + for (var i in vim_videoPages) { + var viPage = vim_videoPages[i]; + tr = $WH.ce('tr'); + tr.onclick = vi_Manage.bind(tr, tr, viPage.type, viPage.typeId, true, i); + + var td = $WH.ce('td'); + var a = $WH.ce('a'); + a.href = '?' + g_types[viPage.type] + '=' + viPage.typeId; + a.target = '_blank'; + $WH.ae(a, $WH.ct(viPage.name)); + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var elapsed = new Date(viPage.date); + $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); + $WH.ae(tr, td); + + td = $WH.ce('td'); + td.align = 'right'; + $WH.ae(td, $WH.ct(viPage.count)); + $WH.ae(tr, td); + + $WH.ae(tbl, tr); + } + + $WH.ae(pc, tbl); +} + +function vim_UpdateList(k) { + var tbl = $WH.ge('theVideosList'); + var tBody = false; + var i = 1; + + while (tbl.childNodes.length > i) { + if (tbl.childNodes[i].nodeName == 'TR' && tBody) + $WH.de(tbl.childNodes[i]); + else if (tbl.childNodes[i].nodeName == 'TR') + tBody = true; + else + i++; + } + + var now = new Date(); + var viId = 0; + for (var i in vim_videoData) { + var video = vim_videoData[i]; + var tr = $WH.ce('tr'); + if (viId == 0 && video.pending) { + viId = video.id; + tr.id = 'highlightedRow'; + } + + var td = $WH.ce('td'); + td.align = 'center'; + + // if (video.status != 999 && !video.pending) { // Aowow - removed + var a = $WH.ce('a'); + a.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId); + a.target = '_blank'; + a.onclick = function (id, e) { + $WH.sp(e); + (vim_View.bind(null, this, id))(); + return false; + }.bind(tr, video.id); + + var previewImg = $WH.ce('img'); + previewImg.src = $WH.sprintf(vi_thumbnails[video.videoType], video.videoId); + previewImg.height = 50; + $WH.ae(a, previewImg); + $WH.ae(td, a); + // } + $WH.ae(tr, td); + + td = $WH.ce('td'); + if (video.status != 999 && !video.pending) { + var a = $WH.ce('a'); + a.href = '?' + g_types[video.type] + '=' + video.typeId + '#videos:id=' + video.id; + a.target = '_blank'; + a.onclick = function (a) { $WH.sp(a); }; + $WH.ae(a, $WH.ct(video.id)); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct(video.id)); + + $WH.ae(tr, td); + + td = $WH.ce('td'); + td.id = 'title-' + video.id; + + var sp = $WH.ce('span'); + sp.style.paddingRight = '8px'; + if (video.caption) { + var sp2 = $WH.ce('span'); + sp2.className = 'q2'; + var b = $WH.ce('b'); + $WH.ae(b, $WH.ct(video.caption)); + $WH.ae(sp2, b); + $WH.ae(sp, sp2); + } + else { + var it = $WH.ce('i'); + it.className = 'q0'; + $WH.ae(it, $WH.ct('NULL')); + $WH.ae(sp, it); + } + $WH.ae(td, sp); + + sp = $WH.ce('span'); + sp.style.whiteSpace = 'nowrap'; + + var a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (vi, e) { + $WH.sp(e); + (vim_ShowEdit.bind(this, vi))(); + }.bind(a, video); + $WH.ae(a, $WH.ct('Edit')); + $WH.ae(sp, a); + $WH.ae(sp, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (vi, e) { + $WH.sp(e); + (vim_Clear.bind(this, vi))(); + }.bind(a, video); + $WH.ae(a, $WH.ct('Clear')); + $WH.ae(sp, a); + $WH.ae(td, sp); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var elapsed = new Date(video.date); + $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); + $WH.ae(tr, td); + + td = $WH.ce('td'); + a = $WH.ce('a'); + a.href = '?user=' + video.user; + a.target = '_blank'; + a.onclick = function (a) { $WH.sp(a); }; + $WH.ae(a, $WH.ct(video.user)); + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + $WH.ae(td, $WH.ct(vim_statuses[video.status])); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var cb = $WH.ce('input'); + cb.type = 'checkbox'; + cb.value = video.id; + cb.onclick = function (e) { + $WH.sp(e); + (vim_UpdateMassLinks.bind(this))(); + }.bind(cb); + $WH.ae(td, cb); + + $WH.ae(td, $WH.ct(' ')); + + if (video.status != 999) { + tr.onclick = function (id) { + vim_View(this, id); + return false; + }.bind(tr, video.id); + + if (video.id == viId && k) + vim_View(tr, video.id); + + if (video.pending) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Approve.bind(this, false))(); + }.bind(video); + $WH.ae(a, $WH.ct('Approve')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Approve')); + + $WH.ae(td, makePipe()); + + if (video.status != 105) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Sticky.bind(this, false))(); + }.bind(video); + $WH.ae(a, $WH.ct('Make sticky')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Make sticky')); + + $WH.ae(td, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Delete.bind(this, false))(); + }.bind(video); + $WH.ae(a, $WH.ct('Delete')); + $WH.ae(td, a); + + $WH.ae(td, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + var a = prompt('Enter the ID to move this video to:'); + (vim_Relocate.bind(this, a))(); + }.bind(video); + $WH.ae(a, $WH.ct('Relocate')); + $WH.ae(td, a); + + $WH.ae(td, makePipe()); + + if (i > 0) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Move.bind(this, -1))() + }.bind(video); + $WH.ae(a, $WH.ct('Move up')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Move up')); + + $WH.ae(td, makePipe()); + + if (i < vim_videoData.length - 1) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Move.bind(this, 1))(); + }.bind(video); + $WH.ae(a, $WH.ct('Move down')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Move down')); + } + + $WH.ae(tr, td); + $WH.ae(tbl, tr); + } +} + +function vim_UpdateMassLinks() { + var idBuff = ''; + var i = 0; + var e = $WH.ge('theVideosList'); + var inp = $WH.gE(e, 'input'); + + $WH.array_walk(inp, function (i) { + if (i.checked) { + idBuff += i.value + ','; ++i + } + }); + + idBuff = $WH.rtrim(idBuff, ','); + + var selCnt = $WH.ge('withselected'); + if (i > 0) { + selCnt.style.display = ''; + $WH.gE(selCnt, 'b')[0].firstChild.nodeValue = '(' + i + ')'; + + var c = $WH.ge('massapprove'); + var b = $WH.ge('massdelete'); + var a = $WH.ge('masssticky'); + + c.href = '?admin=videos&action=approve&id=' + idBuff; + c.onclick = vim_ConfirmMassApprove; + + b.href = '?admin=videos&action=delete&id=' + idBuff; + b.onclick = vim_ConfirmMassDelete; + + a.href = '?admin=videos&action=sticky&id=' + idBuff; + a.onclick = vim_ConfirmMassSticky; + } + else + selCnt.style.display = 'none'; +} + +function vim_MassSelect(action) { + var tbl = $WH.ge('theVideosList'); + var inp = $WH.gE(tbl, 'input'); + + switch (parseInt(action)) { + case 1: + $WH.array_walk(inp, function (x) { x.checked = true; }); + break; + case 0: + $WH.array_walk(inp, function (x) { x.checked = false; }); + break; + case -1: + $WH.array_walk(inp, function (x) { x.checked = !x.checked; }); + break; + case 2: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).status == 0; }); + break; + case 5: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).unique == 1 && vim_GetVideo(x.value).status == 0; }); + break; + case 3: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).status == 100; }); + break; + case 4: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).status == 105; }); + break; + default: + return; + } + + vim_UpdateMassLinks(); +} + +function vim_ShowEdit(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var sp = $WH.gE(node, 'span')[0]; + var div = $WH.ce('div'); + div.style.whiteSpace = 'nowrap'; + var iCaption = $WH.ce('input'); + iCaption.type = 'text'; + iCaption.value = video.caption; + iCaption.maxLength = 200; + iCaption.size = 35; + iCaption.onclick = function (e) { $WH.sp(e); } // aowow - custom to inhibit screenshot popup, when clicking into input element + div.appendChild(iCaption); + + var btn = $WH.ce('input'); + btn.type = 'button'; + btn.value = 'Update'; + btn.onclick = function (vi, isAlt, e) { + if (!isAlt) + $WH.sp(e); + + (vim_Edit.bind(this, vi, isAlt))(); + }.bind(btn, video, isAlt); + div.appendChild(btn); + + var sp2 = $WH.ce('span'); + sp2.appendChild($WH.ct(' ')); + div.appendChild(sp2); + + btn = $WH.ce('input'); + btn.type = 'button'; + btn.value = 'Cancel'; + btn.onclick = function (vi, isAlt, e) { + if (!isAlt) + $WH.sp(e); + + (vim_CancelEdit.bind(this, vi, isAlt))(); + }.bind(btn, video, isAlt); + div.appendChild(btn); + + sp.style.display = 'none'; + sp.nextSibling.style.display = 'none'; + node.insertBefore(div, sp); + + iCaption.focus(); +} + +function vim_CancelEdit(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var b = $WH.gE(node, 'span')[1]; + b.style.display = ''; + b.nextSibling.style.display = ''; + + node.removeChild(node.firstChild); +} + +function vim_Edit(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var desc = node.firstChild.childNodes; + if (desc[0].value == video.caption) { + vim_CancelEdit(video, isAlt); + return; + } + + video.caption = desc[0].value; + + vim_CancelEdit(video, isAlt); + + node = node.firstChild; + while (node.childNodes.length > 0) + node.removeChild(node.firstChild); + + $WH.ae(node, $WH.ct(video.caption)); + + new Ajax('?admin=videos&action=edittitle&id=' + video.id, { + method: 'POST', + params: 'title=' + $WH.urlencode(video.caption) + }); +} + +function vim_Clear(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var sp = $WH.gE(node, 'span'); + var a = $WH.gE(sp[1], 'a'); + sp = sp[0]; + + if (video.caption == '') + return; + + video.caption = ''; + sp.innerHTML = "NULL"; + + new Ajax('?admin=videos&action=edittitle&id=' + video.id, { + method: 'POST', + params: 'title=' + $WH.urlencode('') + }); +} + +function vim_Approve(openNext) { + var vi = this; + new Ajax('?admin=videos&action=approve&id=' + vi.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (vim_numPending == 1 && vi.pending) + vi_Refresh(true); + else { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId, openNext, 0); + } + } + }); +} + +function vim_Sticky(openNext) { + var vi = this; + new Ajax('?admin=videos&action=sticky&id=' + vi.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (vim_numPending == 1 && vi.pending) + vi_Refresh(true); + else { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId, openNext, 0); + } + } + }); +} + +function vim_Delete(openNext) { + var vi = this; + new Ajax('?admin=videos&action=delete&id=' + vi.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (vim_numPending == 1 && vi.pending) + vi_Refresh(true); + else { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId, openNext, 0); + } + } + }); +} + +function vim_Relocate(typeid) { + var vi = this; + new Ajax('?admin=videos&action=relocate&id=' + vi.id + '&typeid=' + typeid, { + method: 'get', + onSuccess: function (x) { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, typeid); + } + }); +} + +function vim_Move(direction) { + var vi = this; + new Ajax('?admin=videos&action=order&id=' + vi.id + '&move=' + direction, { + method: 'get', + onSuccess: function (x) { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId); + } + }); +} + +var VideoManager = new +function () { + var + video, + pos, + prevImgWidth, + prevImgHeight, + scale, + desiredScale, + container, screen, + prevImgDiv, + aPrev, aNext, aCover, + aOriginal, + divFrom, + spCaption, + divCaption, + h2Name, + controlsCOPY, + aEdit, + aClear, + spApprove, + aApprove, + aMakeSticky, + aDelete, + loadingImage, + lightboxComponents; + + function computeDimensions(captionExtraHeight) { + var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 122 - captionExtraHeight)); + + if (video.id) { + desiredScale = Math.min(772 / video.width, 618 / video.height); + scale = Math.min(772 / video.width, availHeight / video.height) + } + else + desiredScale = scale = 1; + + if (desiredScale > 1) + desiredScale = 1; + + if (scale > 1) + scale = 1; + + prevImgWidth = Math.round(scale * video.width); + prevImgHeight = Math.round(scale * video.height); + var M = Math.max(480, prevImgWidth); + + Lightbox.setSize(M + 20, prevImgHeight + 116 + captionExtraHeight); + + if (captionExtraHeight) { + prevImgDiv.firstChild.width = prevImgWidth; + prevImgDiv.firstChild.height = prevImgHeight; + } + } + + function render(resizing) { + if (resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) + return; + + container.style.visibility = 'hidden'; + + var resized = (video.width > 772 || video.height > 618); + + computeDimensions(0); + + // Aowow - /uploads/videos/ not seen on server + // var url = g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'; + var url = video.url; + + var html = ''; + + spCaption.innerHTML = html; + } + else + spCaption.innerHTML = "NULL"; + + divCaption.id = 'title2-' + video.id; + + aEdit.onclick = vim_ShowEdit.bind(aEdit, video, true); + aClear.onclick = vim_Clear.bind(aClear, video, true); + + if (video.next !== undefined) { + aPrev.style.display = aNext.style.display = ''; + aCover.style.display = 'none'; + } + else { + aPrev.style.display = aNext.style.display = 'none'; + aCover.style.display = ''; + } + } + + Lightbox.reveal(); + + if (spCaption.offsetHeight > 18) + computeDimensions(spCaption.offsetHeight - 18); + + container.style.visibility = 'visible'; + } + + function nextVideo() { + if (video.next !== undefined) + video = vim_videoData[video.next]; + + onRender(); + } + + function prevVideo() { + if (video.prev !== undefined) + video = vim_videoData[video.prev]; + + onRender(); + } + function onResize() { + render(1); + } + + function onHide() { + aApprove.onclick = aMakeSticky.onclick = aDelete.onclick = null; + cancelImageLoading(); + } + + function onShow(dest, first, opt) { + video = opt; + container = dest; + + if (first) { + dest.className = 'screenshotviewer'; + + screen = $WH.ce('div'); + screen.className = 'screenshotviewer-screen'; + + aPrev = $WH.ce('a'); + aNext = $WH.ce('a'); + aPrev.className = 'screenshotviewer-prev'; + aNext.className = 'screenshotviewer-next'; + aPrev.href = 'javascript:;'; + aNext.href = 'javascript:;'; + + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aPrev, foo); + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aNext, foo); + + aPrev.onclick = prevVideo; + aNext.onclick = nextVideo; + + aCover = $WH.ce('a'); + aCover.className = 'screenshotviewer-cover'; + aCover.href = 'javascript:;'; + aCover.onclick = Lightbox.hide; + + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aCover, foo); + $WH.ae(screen, aPrev); + $WH.ae(screen, aNext); + $WH.ae(screen, aCover); + + var _div = $WH.ce('div'); + _div.className = 'text'; + h2Name = $WH.ce('h2'); + h2Name.className = 'first'; + $WH.ae(h2Name, $WH.ct(video.name)); + $WH.ae(_div, h2Name); + $WH.ae(dest, _div); + + prevImgDiv = $WH.ce('div'); + $WH.ae(screen, prevImgDiv); + + $WH.ae(dest, screen); + + var _div = $WH.ce('div'); + _div.style.paddingTop = '6px'; + _div.style.cssFloat = _div.style.styleFloat = 'right'; + _div.className = 'bigger-links'; + + aApprove = $WH.ce('a'); + aApprove.href = 'javascript:;'; + $WH.ae(aApprove, $WH.ct('Approve')); + $WH.ae(_div, aApprove); + + spApprove = $WH.ce('span'); + spApprove.style.display = 'none'; + $WH.ae(spApprove, $WH.ct('Approve')); + $WH.ae(_div, spApprove); + + $WH.ae(_div, makePipe()); + + aMakeSticky = $WH.ce('a'); + aMakeSticky.href = 'javascript:;'; + $WH.ae(aMakeSticky, $WH.ct('Make sticky')); + $WH.ae(_div, aMakeSticky); + + $WH.ae(_div, makePipe()); + + aDelete = $WH.ce('a'); + aDelete.href = 'javascript:;'; + $WH.ae(aDelete, $WH.ct('Delete')); + $WH.ae(_div, aDelete); + + controlsCOPY = _div; + + $WH.ae(dest, _div); + + divFrom = $WH.ce('div'); + divFrom.className = 'screenshotviewer-from'; + + var sp = $WH.ce('span'); + $WH.ae(sp, $WH.ct(LANG.lvvideo_from)); + $WH.ae(sp, $WH.ce('a')); + $WH.ae(sp, $WH.ct(' ')); + $WH.ae(sp, $WH.ce('span')); + $WH.ae(divFrom, sp); + $WH.ae(dest, divFrom); + + _div = $WH.ce('div'); + _div.className = 'clear'; + $WH.ae(dest, _div); + + var aClose = $WH.ce('a'); + aClose.className = 'screenshotviewer-close'; + aClose.href = 'javascript:;'; + aClose.onclick = Lightbox.hide; + $WH.ae(aClose, $WH.ce('span')); + $WH.ae(dest, aClose); + + aOriginal = $WH.ce('a'); + aOriginal.className = 'screenshotviewer-original'; + aOriginal.href = 'javascript:;'; + aOriginal.target = '_blank'; + $WH.ae(aOriginal, $WH.ce('span')); + $WH.ae(dest, aOriginal); + + divCaption = $WH.ce('div'); + spCaption = $WH.ce('span'); + spCaption.style.paddingRight = '8px'; + $WH.ae(divCaption, spCaption); + + var sp = $WH.ce('span'); + sp.style.whiteSpace = 'nowrap'; + aEdit = $WH.ce('a'); + aEdit.href = 'javascript:;'; + $WH.ae(aEdit, $WH.ct('Edit')); + $WH.ae(sp, aEdit); + + $WH.ae(sp, makePipe()); + + aClear = $WH.ce('a'); + aClear.href = 'javascript:;'; + $WH.ae(aClear, $WH.ct('Clear')); + $WH.ae(sp, aClear); + $WH.ae(divCaption, sp); + $WH.ae(dest, divCaption); + + _div = $WH.ce('div'); + _div.className = 'clear'; + $WH.ae(dest, _div); + } + else { + $WH.ee(h2Name); + $WH.ae(h2Name, $WH.ct(video.name)); + } + + onRender(); + } + function onRender() { + if (video.pending) { + aApprove.onclick = vim_Approve.bind(video, true); + aMakeSticky.onclick = vim_Sticky.bind(video, true); + aDelete.onclick = vim_Delete.bind(video, true); + } + else { + aMakeSticky.onclick = vim_Sticky.bind(video, true); + aDelete.onclick = vim_Delete.bind(video, true); + } + aApprove.style.display = video.pending ? '' : 'none'; + spApprove.style.display = video.pending ? 'none' : ''; + + if (!video.width || !video.height) { + if (loadingImage) { + loadingImage.onload = null; + loadingImage.onerror = null; + } + else { + container.className = ''; + lightboxComponents = []; + + while (container.firstChild) { + lightboxComponents.push(container.firstChild); + $WH.de(container.firstChild); + } + } + + var lightboxTimer = setTimeout(function () { + video.width = 126; + video.height = 22; + + computeDimensions(0); + + video.width = null; + video.height = null; + + var div = $WH.ce('div'); + div.style.margin = '0 auto'; + div.style.width = '126px'; + + var img = $WH.ce('img'); + img.src = g_staticUrl + '/images/ui/misc/progress-anim.gif'; + img.width = 126; + img.height = 22; + + $WH.ae(div, img); + $WH.ae(container, div); + + Lightbox.reveal(); + container.style.visiblity = 'visible' + }, 150); + + loadingImage = new Image(); + loadingImage.onload = (function (vi, timer) { + clearTimeout(timer); + vi.width = this.width; + vi.height = this.height; + loadingImage = null; + restoreLightbox(); + render() + }).bind(loadingImage, video, lightboxTimer); + + loadingImage.onerror = (function (timer) { + clearTimeout(timer); + loadingImage = null; + Lightbox.hide(); + restoreLightbox() + }).bind(loadingImage, lightboxTimer); + + loadingImage.src = (video.url ? video.url : g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'); + } + else + render(); + } + + function cancelImageLoading() { + if (!loadingImage) + return; + + loadingImage.onload = null; + loadingImage.onerror = null; + loadingImage = null; + + restoreLightbox(); + } + + function restoreLightbox() { + if (!lightboxComponents) + return; + + $WH.ee(container); + container.className = 'screenshotviewer'; + for (var K = 0; K < lightboxComponents.length; ++K) + $WH.ae(container, lightboxComponents[K]); + + lightboxComponents = null; + } + + this.show = function (opt) { + Lightbox.show('videomanager', { + onShow: onShow, + onHide: onHide, + onResize: onResize + }, opt); + } +}; diff --git a/template/pages/admin/videos.tpl.php b/template/pages/admin/videos.tpl.php new file mode 100644 index 00000000..da89e0a6 --- /dev/null +++ b/template/pages/admin/videos.tpl.php @@ -0,0 +1,135 @@ +brick('header'); +?> +
    +
    +
    + +brick('announcement'); + +$this->brick('pageTemplate'); +?> +
    +

    h1; ?>

    + +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
     /> />
    + + + + + + + + + + + +
    User: » Search by User
    Page: + + #» Search by Page
    +
    + + + + + + + +
    Menu
    PagesVideos:
    + + + + + + + +
    VideoIdTitleDateUploaderStatusOptions
    + + +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/video.tpl.php b/template/pages/video.tpl.php new file mode 100644 index 00000000..f3cded7d --- /dev/null +++ b/template/pages/video.tpl.php @@ -0,0 +1,82 @@ +brick('header'); +?> +
    +
    +
    + +brick('announcement'); + +$this->brick('pageTemplate'); + +$this->brick('infobox'); + +?> +
    +

    h1; ?>

    + +

    viTitle;?>

    +
    +
    + + +
    + + +
    +
    + + + + + +
    +
    +
    + +brick('footer'); ?> From fef27c58e60333f3e4f44a66d7dd1488c6c949b8 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 18 Aug 2025 00:22:24 +0200 Subject: [PATCH 683/957] Template/Update (Part 40) * convert 'guides' (listing, viewing, writing & management) * don't allow comments on WIP guides --- endpoints/admin/guide.php | 81 +++ endpoints/admin/guides.php | 46 ++ endpoints/edit/image.php | 48 ++ endpoints/get-description/get-description.php | 35 ++ endpoints/guide/changelog.php | 104 ++++ endpoints/guide/edit.php | 214 +++++++ endpoints/guide/guide.php | 250 ++++++++ endpoints/guide/guide_power.php | 59 ++ endpoints/guide/new.php | 66 +++ endpoints/guide/vote.php | 50 ++ endpoints/guides/guides.php | 73 +++ endpoints/my-guides/my-guides.php | 52 ++ endpoints/user/user.php | 2 +- includes/ajaxHandler/admin.class.php | 60 +- includes/ajaxHandler/edit.class.php | 82 --- includes/ajaxHandler/getdescription.class.php | 37 -- includes/ajaxHandler/guide.class.php | 63 -- includes/components/guidemgr.class.php | 106 ++++ includes/components/pagetemplate.class.php | 1 - .../response/templateresponse.class.php | 6 +- includes/dbtypes/guide.class.php | 35 +- includes/defines.php | 7 - includes/libs/qqFileUploader.class.php | 2 +- includes/type.class.php | 2 +- includes/user.class.php | 2 +- includes/utilities.php | 2 - localization/locale_dede.php | 28 +- localization/locale_enus.php | 28 +- localization/locale_eses.php | 28 +- localization/locale_frfr.php | 28 +- localization/locale_ruru.php | 28 +- localization/locale_zhcn.php | 28 +- pages/admin.php | 2 +- pages/guide.php | 549 ------------------ pages/guides.php | 102 ---- static/js/global.js | 235 ++++---- template/listviews/guideAdminCol.tpl | 4 +- template/pages/guide-edit.tpl.php | 57 +- 38 files changed, 1437 insertions(+), 1165 deletions(-) create mode 100644 endpoints/admin/guide.php create mode 100644 endpoints/admin/guides.php create mode 100644 endpoints/edit/image.php create mode 100644 endpoints/get-description/get-description.php create mode 100644 endpoints/guide/changelog.php create mode 100644 endpoints/guide/edit.php create mode 100644 endpoints/guide/guide.php create mode 100644 endpoints/guide/guide_power.php create mode 100644 endpoints/guide/new.php create mode 100644 endpoints/guide/vote.php create mode 100644 endpoints/guides/guides.php create mode 100644 endpoints/my-guides/my-guides.php delete mode 100644 includes/ajaxHandler/edit.class.php delete mode 100644 includes/ajaxHandler/getdescription.class.php delete mode 100644 includes/ajaxHandler/guide.class.php create mode 100644 includes/components/guidemgr.class.php delete mode 100644 pages/guide.php delete mode 100644 pages/guides.php diff --git a/endpoints/admin/guide.php b/endpoints/admin/guide.php new file mode 100644 index 00000000..8a6d93d8 --- /dev/null +++ b/endpoints/admin/guide.php @@ -0,0 +1,81 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => GuideMgr::STATUS_APPROVED, 'max_range' => GuideMgr::STATUS_REJECTED]], + 'msg' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'status')) + { + trigger_error('AdminGuideResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + $guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']); + if (!$guide) + { + trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' not found', E_USER_ERROR); + $this->result = self::ERR_GUIDE; + return; + } + + if ($this->_post['status'] == $guide['status']) + { + trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' already has status #'.$this->_post['status'], E_USER_ERROR); + $this->result = self::ERR_STATUS; + return; + } + + // status can only be APPROVED or REJECTED due to input validation + if (!$this->update($this->_post['id'], $this->_post['status'], $this->_post['msg'])) + { + trigger_error('AdminGuideResponse - write to db failed for guide #'.$this->_post['id'], E_USER_ERROR); + $this->result = self::ERR_WRITE_DB; + return; + } + + if ($this->_post['status'] == GuideMgr::STATUS_APPROVED) + Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]); + + $this->result = self::ERR_NONE; + } + + private function update(int $id, int $status, ?string $msg = null) : bool + { + if ($status == GuideMgr::STATUS_APPROVED) // set display rev to latest + $ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d, `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', $status, Type::GUIDE, $id, User::$id, time(), $id); + else + $ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id); + + if (!$ok) + return false; + + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status); + if ($msg) + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg); + + return true; + } +} + +?> diff --git a/endpoints/admin/guides.php b/endpoints/admin/guides.php new file mode 100644 index 00000000..16aa460b --- /dev/null +++ b/endpoints/admin/guides.php @@ -0,0 +1,46 @@ + Content > Guides Awaiting Approval + + protected function generate() : void + { + $this->h1 = 'Pending Guides'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); + if ($pending->error) + $data = []; + else + { + $data = $pending->getListviewData(); + $latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ?_articles WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs()); + foreach ($latest as $id => $rev) + $data[$id]['rev'] = $rev; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => array_values($data), + 'hiddenCols' => ['patch', 'comments', 'views', 'rating'], + 'extraCols' => '$_' + ), GuideList::$brickFile, 'guideAdminCol')); + } +} + +?> diff --git a/endpoints/edit/image.php b/endpoints/edit/image.php new file mode 100644 index 00000000..587d5a5e --- /dev/null +++ b/endpoints/edit/image.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'guide' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]] + ); + + /* + success: bool + id: image enumerator + type: 3 ? png : jpg + name: old filename + error: errString + */ + protected function generate() : void + { + if (!$this->assertGET('qqfile', 'guide')) + { + $this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]); + return; + } + + if (!User::canWriteGuide()) + { + $this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]); + return; + } + + $this->result = GuideMgr::handleUpload(); + + if (isset($this->result['success'])) + $this->result += ['name' => $this->_get['qqfile']]; + + $this->result = Util::toJSON($this->result); + } +} + +?> diff --git a/endpoints/get-description/get-description.php b/endpoints/get-description/get-description.php new file mode 100644 index 00000000..139c6949 --- /dev/null +++ b/endpoints/get-description/get-description.php @@ -0,0 +1,35 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + public function __construct(string $param) + { + if ($param) // should be empty + $this->generate404(); + + parent::__construct($param); + } + + protected function generate() : void + { + if (!User::canWriteGuide()) + return; + + $this->result = GuideMgr::createDescription($this->_post['description']); + } +} + +?> diff --git a/endpoints/guide/changelog.php b/endpoints/guide/changelog.php new file mode 100644 index 00000000..685dfad3 --- /dev/null +++ b/endpoints/guide/changelog.php @@ -0,0 +1,104 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + // main container should be tagged:
    + + if (!$this->assertGET('id')) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + $guide = new GuideList(array(['id', $this->_get['id']])); + if ($guide->error) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + if (!$guide->canBeViewed() && !$guide->userCanView()) + $this->forward('?guides='.$guide->getField('category')); + + $this->h1 = lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); + if (!$this->h1) + $this->h1 = $guide->getField('name'); + + $this->gPageInfo += ['name' => $guide->getField('name')]; + + + $this->breadcrumb[] = $guide->getField('category'); + + + parent::generate(); + + /* - NYI (see "&& false") + $this->addScript([SC_JS_STRING, + + <<= parseInt(e.value)); + }); + + }; + + radios.each(function (i, e) { + e.onchange = limit.bind(this, e.name, parseInt(e.value)); + + if (i < 2 && e.name == "b") // first pair + $(e).trigger("click"); + else if (e.value == 0 && e.name == "a") // last pair + $(e).trigger("click"); + }); + }); + JS + ]); + */ + + $buff = '
      '; + $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) && false ? ($rev !== null ? '' : '') : ''; + + $logEntries = DB::Aowow()->select('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->_get['id']); + foreach ($logEntries as $log) + { + if ($log['status'] != GuideMgr::STATUS_NONE) + $buff .= '
    • '.$inp($log['rev']).''.Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).''.Util::formatTimeDiff($log['date'])."
    • \n"; + else if ($log['msg']) + $buff .= '
    • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; + else + $buff .= '
    • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; + } + + // append creation + $buff .= '
    • '.$inp(0).''.Lang::guide('clCreated').''.Util::formatTimeDiff($guide->getField('date'))."
    • \n
    \n"; + + if (User::isInGroup(U_GROUP_STAFF) && false) + $buff .= ''; + + $this->extraHTML = $buff; + } +} + +?> diff --git a/endpoints/guide/edit.php b/endpoints/guide/edit.php new file mode 100644 index 00000000..b2897167 --- /dev/null +++ b/endpoints/guide/edit.php @@ -0,0 +1,214 @@ + span { display: block; height: 22px; } + #upload-result { display: inline-block; text-align: right; } + #upload-progress { display: inline-block; margin-right: 8px; } + + CSS] + ); + protected array $expectedPOST = array( + 'save' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], // saved for more editing + 'submit' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], // submitted for review + 'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'description' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkDescription'] ], + 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom'] ], + 'category' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 9] ], + 'specId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => -1, 'max_value' => 2, 'default' => -1]], + 'classId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 11, 'default' => 0]] + ); + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_VALIDATE_INT], + 'rev' => ['filter' => FILTER_VALIDATE_INT] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + if (!User::canWriteGuide()) + $this->generateError(); + + if (!is_int($this->_get['id'])) // edit existing guide + return; + + $this->typeId = $this->_get['id']; // just to display sensible not-found msg + $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_guides WHERE `id` = ?d AND `status` <> ?d { AND `userId` = ?d }', $this->typeId, GuideMgr::STATUS_ARCHIVED, User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : User::$id); + if (!$status && $this->typeId) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + else if (!$this->typeId) + return; + + // just so we don't have to access GuideMgr from template + $this->isDraft = $status == GuideMgr::STATUS_DRAFT; + $this->editStatus = $status; + $this->editRev = DB::Aowow()->selectCell('SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC', Type::GUIDE, $this->typeId); + } + + protected function generate() : void + { + if ($this->_post['save'] || $this->_post['submit']) + { + if (!$this->saveGuide()) + $this->error = Lang::main('intError'); + else if ($this->_get['id'] === 0) + $this->forward('?guide=edit&id='.$this->typeId); + } + + $guide = new GuideList(array(['id', $this->typeId])); + + $this->h1 = Lang::guide('editTitle'); + array_unshift($this->title, $this->h1.Lang::main('colon').$guide->getField('title'), Lang::game('guides')); + + Lang::sort('guide', 'category'); + + // init required template vars + $this->editCategory = $this->_post['category'] ?? $guide->getField('category'); + $this->editTitle = $this->_post['title'] ?? $guide->getField('title'); + $this->editName = $this->_post['name'] ?? $guide->getField('name'); + $this->editDescription = $this->_post['description'] ?? $guide->getField('description'); + $this->editText = $this->_post['body'] ?? $guide->getArticle(); + $this->editClassId = $this->_post['classId'] ?? $guide->getField('classId'); + $this->editSpecId = $this->_post['specId'] ?? $guide->getField('specId'); + $this->editLocale = $this->_post['locale'] ?? Locale::tryFrom($guide->getField('locale')); + $this->editStatus = $this->editStatus ?: $guide->getField('status'); + $this->editStatusColor = GuideMgr::STATUS_COLORS[$this->editStatus]; + + $this->extendGlobalData($guide->getJSGlobals()); + + parent::generate(); + } + + private function saveGuide() : bool + { + // test requiered fields set + if (!$this->assertPOST('title', 'name', 'body', 'locale', 'category')) + { + trigger_error('GuideEditResponse::saveGuide - received malformed request', E_USER_ERROR); + return false; + } + + // test required fields context + if (!$this->_post['locale']->validate()) + return false; + + // sanitize: spec / class + if ($this->_post['category'] == 1) // Classes + { + if ($this->_post['classId'] && !ChrClass::tryFrom($this->_post['classId'])) + $this->_post['classId'] = 0; + + if ($this->_post['specId'] > -1 && !$this->_post['classId']) + $this->_post['specId'] = -1; + } + else + { + $this->_post['classId'] = 0; + $this->_post['specId'] = -1; + } + + $guideData = array( + 'category' => $this->_post['category'], + 'classId' => $this->_post['classId'], + 'specId' => $this->_post['specId'], + 'title' => $this->_post['title'], + 'name' => $this->_post['name'], + 'description' => $this->_post['description'] ?: GuideMgr::createDescription($this->_post['body']), + 'locale' => $this->_post['locale']->value, + 'roles' => User::$groups, + 'status' => $this->_post['submit'] ? GuideMgr::STATUS_REVIEW : GuideMgr::STATUS_DRAFT, + 'date' => time() + ); + + // new guide > reload editor + if ($this->_get['id'] === 0) + { + $guideData += ['userId' => User::$id]; + if (!($this->typeId = (int)DB::Aowow()->query('INSERT INTO ?_guides (?#) VALUES (?a)', array_keys($guideData), array_values($guideData)))) + { + trigger_error('GuideEditResponse::saveGuide - failed to save guide to db', E_USER_ERROR); + return false; + } + } + // existing guide > :shrug: + else if (DB::Aowow()->query('UPDATE ?_guides SET ?a WHERE `id` = ?d', $guideData, $this->typeId)) + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?d, ?)', $this->typeId, $this->editRev, time(), User::$id, $this->_post['changelog']); + else + { + trigger_error('GuideEditResponse::saveGuide - failed to update guide in db', E_USER_ERROR); + return false; + } + + // insert Article + $articleId = DB::Aowow()->query( + 'INSERT INTO ?_articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (?d, ?d, ?d, ?d, ?d, ?)', + Type::GUIDE, + $this->typeId, + $this->_post['locale']->value, + ++$this->editRev, + User::$groups & U_GROUP_STAFF ? User::$groups : User::$groups | U_GROUP_BLOGGER, + $this->_post['body'] + ); + + if (!is_int($articleId)) + { + if ($this->_get['id'] === 0) + DB::Aowow()->query('DELETE FROM ?_guides WHERE `id` = ?d', $this->typeId); + + trigger_error('GuideEditResponse::saveGuide - failed to save article to db', E_USER_ERROR); + return false; + } + + if ($this->_post['submit'] && $this->editStatus != GuideMgr::STATUS_REVIEW) + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $this->typeId, time(), User::$id, GuideMgr::STATUS_REVIEW); + + $this->editStatus = $guideData['status']; + + return true; + } + + protected static function checkDescription(string $str) : string + { + // run checkTextBlob and also replace \n => \s and \s+ => \s + $str = preg_replace(parent::PATTERN_TEXT_BLOB, '', $str); + + $str = strtr($str, ["\n" => ' ', "\r" => ' ']); + + return preg_replace('/\s+/', ' ', trim($str)); + } +} + +?> diff --git a/endpoints/guide/guide.php b/endpoints/guide/guide.php new file mode 100644 index 00000000..90de7eb8 --- /dev/null +++ b/endpoints/guide/guide.php @@ -0,0 +1,250 @@ + ['filter' => FILTER_VALIDATE_INT], + 'rev' => ['filter' => FILTER_VALIDATE_INT] + ); + + public int $type = Type::GUIDE; + public int $typeId = 0; + public int $guideStatus = 0; + public array $guideRating = []; + public ?int $guideRevision = null; + + private GuideList $subject; + + public function __construct(string $nameOrId) + { + parent::__construct($nameOrId); + + /**********************/ + /* get mode + guideId */ + /**********************/ + + if (Util::checkNumeric($nameOrId, NUM_CAST_INT)) + $this->typeId = $nameOrId; + else if (preg_match(GuideMgr::VALID_URL, $nameOrId)) + { + if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($nameOrId))) + { + $this->typeId = intVal($id); + $this->articleUrl = Util::lower($nameOrId); + } + } + + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new GuideList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + if (!$this->subject->canBeViewed() && !$this->subject->userCanView()) + $this->forward('?guides='.$this->subject->getField('category')); + + $this->guideStatus = $this->subject->getField('status'); + if ($this->guideStatus != GuideMgr::STATUS_APPROVED && $this->guideStatus != GuideMgr::STATUS_ARCHIVED) + { + $this->cacheType = CACHE_TYPE_NONE; + $this->contribute = CONTRIBUTE_NONE; + } + + if ($this->articleUrl) + $this->guideRevision = $this->subject->getField('rev'); + else if ($this->subject->userCanView()) + $this->guideRevision = $this->_get['rev'] ?? $this->subject->getField('latest'); + else + $this->subject->getField('rev'); + + $this->h1 = $this->subject->getField('name'); + + $this->gPageInfo += array( + 'name' => $this->h1, + 'author' => $this->subject->getField('author') + ); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($x = $this->subject?->getField('category')) + $this->breadcrumb[] = $x; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('title'), Lang::game('guides')); + + + /***********/ + /* Infobox */ + /***********/ + + if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_QUICKFACTS)) + $this->generateInfobox(); + + // needs post-cache updating + if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_RATING)) + $this->guideRating = array( + $this->subject->getField('rating'), // avg rating + User::canUpvote() && User::canDownvote() ? 'true' : 'false', + $this->subject->getField('_self'), // my rating amt; 0 = no vote + $this->typeId // guide Id + ); + + + /****************/ + /* Main Content */ + /****************/ + + if ($this->subject->userCanView()) + $this->redButtons[BUTTON_GUIDE_EDIT] = User::canWriteGuide() && $this->guideStatus != GuideMgr::STATUS_ARCHIVED; + + $this->redButtons[BUTTON_GUIDE_LOG] = true; + $this->redButtons[BUTTON_GUIDE_REPORT] = $this->subject->canBeReported(); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + // the article text itself is added by PageTemplate::addArticle() + parent::generate(); + + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + if ($this->guideRating) + $this->result->registerDisplayHook('guideRating', [self::class, 'starsHook']); + } + + private function generateInfobox() : void + { + $infobox = []; + + if ($this->subject->getField('cuFlags') & CC_FLAG_STICKY) + $infobox[] = '[span class=guide-sticky]'.Lang::guide('sticky').'[/span]'; + + $infobox[] = Lang::guide('author').'[url=?user='.$this->subject->getField('author').']'.$this->subject->getField('author').'[/url]'; + + if ($this->subject->getField('category') == 1) + { + $c = $this->subject->getField('classId'); + $s = $this->subject->getField('specId'); + if ($c > 0) + { + $this->extendGlobalIds(Type::CHR_CLASS, $c); + $infobox[] = Util::ucFirst(Lang::game('class')).Lang::main('colon').'[class='.$c.']'; + } + if ($s > -1) + $infobox[] = Lang::guide('spec').'[icon class="c'.$c.' icontiny" name='.Game::$specIconStrings[$c][$s].']'.Lang::game('classSpecs', $c, $s).'[/icon]'; + } + + // $infobox[] = Lang::guide('patch').Lang::main('colon').'3.3.5'; // replace with date + $infobox[] = Lang::guide('added').'[tooltip name=added]'.date('l, G:i:s', $this->subject->getField('date')).'[/tooltip][span class=tip tooltip=added]'.date(Lang::main('dateFmtShort'), $this->subject->getField('date')).'[/span]'; + + if ($this->guideStatus == GuideMgr::STATUS_ARCHIVED) + $infobox[] = Lang::guide('status', GuideMgr::STATUS_ARCHIVED); + + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + if ($this->guideStatus == GuideMgr::STATUS_REVIEW && User::isInGroup(U_GROUP_STAFF) && $this->_get['rev']) + { + $this->addScript([SC_JS_STRING, <<infobox->append('[h3 style="text-align:center"]Admin[/h3]'); + $this->infobox->append('[div style="text-align:center"][url=# id="btn-accept" class=icon-tick]Approve[/url][url=# style="margin-left:20px" id="btn-reject" class=icon-delete]Reject[/url][/div]'); + } + } + + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$infobox) : void + { + if ($pt->guideStatus != GuideMgr::STATUS_APPROVED) + return; + + // increment and display views + DB::Aowow()->query('UPDATE ?_guides SET `views` = `views` + 1 WHERE `id` = ?d', $pt->typeId); + + $nViews = DB::Aowow()->selectCell('SELECT `views` FROM ?_guides WHERE `id` = ?d', $pt->typeId); + + $infobox->addItem(Lang::guide('views').'[n5='.$nViews.']'); + + // should we have a rating item in the lv? + if (!$pt->guideRating) + return; + + $rating = GuideMgr::getRatings([$pt->typeId]); + if ($rating[$pt->typeId]['nvotes'] < 5) + $infobox->addItem(Lang::guide('rating').Lang::guide('noVotes')); + else + $infobox->addItem(Lang::guide('rating').Lang::guide('votes', [round($rating[$pt->typeId]['rating'], 1), $rating[$pt->typeId]['nvotes']])); + } + + public static function starsHook(Template\PageTemplate &$pt, ?array &$guideRating) : void + { + if ($pt->guideStatus != GuideMgr::STATUS_APPROVED) + return; + + $rating = GuideMgr::getRatings([$pt->typeId]); + $guideRating = array( + $rating[$pt->typeId]['rating'], + User::canUpvote() && User::canDownvote() ? 'true' : 'false', + $rating[$pt->typeId]['_self'] ?? 0, + $pt->typeId + ); + } +} + +?> diff --git a/endpoints/guide/guide_power.php b/endpoints/guide/guide_power.php new file mode 100644 index 00000000..99fd4dba --- /dev/null +++ b/endpoints/guide/guide_power.php @@ -0,0 +1,59 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private string $url = ''; + + public function __construct(string $idOrName) + { + parent::__construct($idOrName); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + if (Util::checkNumeric($idOrName, NUM_CAST_INT)) + $this->typeId = $idOrName; + else if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($idOrName))) + { + $this->typeId = intVal($id); + $this->url = Util::lower($idOrName); + } + } + + protected function generate() : void + { + $opts = []; + if ($this->typeId) + if (!($guide = new GuideList(array(['id', $this->typeId])))->error) + $opts = array( + 'name' => $guide->getField('name', true), + 'tooltip' => $guide->renderTooltip() + ); + + if (!$opts) + $this->cacheType = CACHE_TYPE_NONE; + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->url ?: $this->typeId, $opts); + } +} + +?> diff --git a/endpoints/guide/new.php b/endpoints/guide/new.php new file mode 100644 index 00000000..95de5c4d --- /dev/null +++ b/endpoints/guide/new.php @@ -0,0 +1,66 @@ + span { display: block; height: 22px; } + #upload-result { display: inline-block; text-align: right; } + #upload-progress { display: inline-block; margin-right: 8px; } + + CSS] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + if (!User::canWriteGuide()) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::guide('newTitle'); + + array_unshift($this->title, $this->h1, Lang::game('guides')); + + Lang::sort('guide', 'category'); + + // update required template vars + $this->editLocale = Lang::getLocale(); + + parent::generate(); + } +} + +?> diff --git a/endpoints/guide/vote.php b/endpoints/guide/vote.php new file mode 100644 index 00000000..ca7ee945 --- /dev/null +++ b/endpoints/guide/vote.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 5]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'rating')) + { + trigger_error('GuideVoteResponse - malformed request received', E_USER_ERROR); + $this->generate404(); + } + + if (!User::canUpvote() || !User::canDownvote()) // same logic as comments? + $this->generate403(); + + // by id, not own, published + $points = $votes = 0; + if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ?_guides WHERE `id` = ?d AND (`status` = ?d OR `rev` > 0)', $this->_post['id'], GuideMgr::STATUS_APPROVED)) + { + // apparently you are allowed to vote on your own guide + if ($g['cuFlags'] & GUIDE_CU_NO_RATING) + $this->generate403(); + + if (!$this->_post['rating']) + DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_GUIDE, $this->_post['id'], User::$id); + else + DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']); + + [$points, $votes] = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1" FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_GUIDE, $this->_post['id']); + } + + $this->result = Util::toJSON($votes ? ['rating' => $points / $votes, 'nvotes' => $votes] : ['rating' => 0, 'nvotes' => 0]); + } +} + +?> diff --git a/endpoints/guides/guides.php b/endpoints/guides/guides.php new file mode 100644 index 00000000..9c0db0ee --- /dev/null +++ b/endpoints/guides/guides.php @@ -0,0 +1,73 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('guides')); + + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::guide('category', $this->category[0])); + + + $conditions = array( + ['locale', Lang::getLocale()->value], + ['status', GuideMgr::STATUS_ARCHIVED, '!'], // never archived guides + [ + 'OR', + ['status', GuideMgr::STATUS_APPROVED], // currently approved + ['rev', 0, '>'] // has previously approved revision + ] + ); + if ($this->category) + $conditions[] = ['category', $this->category[0]]; + + $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; + + $guides = new GuideList($conditions); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'name' => Util::ucFirst(Lang::game('guides')), + 'hiddenCols' => ['patch'], // pointless: display date instead + 'extraCols' => ['$Listview.extraCols.date'] // ok + ), GuideList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/my-guides/my-guides.php b/endpoints/my-guides/my-guides.php new file mode 100644 index 00000000..288d87aa --- /dev/null +++ b/endpoints/my-guides/my-guides.php @@ -0,0 +1,52 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::guide('myGuides')); + + array_unshift($this->title, $this->h1); + + $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; + + $guides = new GuideList(array(['userId', User::$id])); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'name' => Util::ucFirst(Lang::game('guides')), + 'hiddenCols' => ['patch', 'author'], + 'visibleCols' => ['status'], + 'extraCols' => ['$Listview.extraCols.date'] + ), GuideList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php index 7f4fee64..b3e8f7b8 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -253,7 +253,7 @@ class UserBaseResponse extends TemplateResponse } // My Guides - $guides = new GuideList(['status', [GUIDE_STATUS_APPROVED, GUIDE_STATUS_ARCHIVED]], ['userId', $this->user['id']]); + $guides = new GuideList(['status', [GuideMgr::STATUS_APPROVED, GuideMgr::STATUS_ARCHIVED]], ['userId', $this->user['id']]); if (!$guides->error) { $this->lvTabs->addListviewTab(new Listview(array( diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php index 899c607e..1b2b6f95 100644 --- a/includes/ajaxHandler/admin.class.php +++ b/includes/ajaxHandler/admin.class.php @@ -7,7 +7,7 @@ if (!defined('AOWOW_REVISION')) class AjaxAdmin extends AjaxHandler { - protected $validParams = ['siteconfig', 'weight-presets', 'spawn-override', 'guide', 'comment']; + protected $validParams = ['siteconfig', 'weight-presets', 'spawn-override', 'comment']; protected $_get = array( 'action' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ], 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], @@ -64,13 +64,6 @@ class AjaxAdmin extends AjaxHandler $this->handler = 'spawnPosFix'; } - else if ($this->params[0] == 'guide') - { - if (!User::isInGroup(U_GROUP_STAFF)) - return; - - $this->handler = 'guideManage'; - } else if ($this->params[0] == 'comment') { if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD)) @@ -207,57 +200,6 @@ class AjaxAdmin extends AjaxHandler return '-1'; } - protected function guideManage() : string - { - $update = function (int $id, int $status, ?string $msg = null) : bool - { - if (!DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id)) - return false; - - // set display rev to latest - if ($status == GUIDE_STATUS_APPROVED) - DB::Aowow()->query('UPDATE ?_guides SET `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', Type::GUIDE, $id, User::$id, time(), $id); - - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status); - if ($msg) - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg); - return true; - }; - - if (!$this->_post['id']) - trigger_error('AjaxHander::guideManage - malformed request: id: '.$this->_post['id'].', status: '.$this->_post['status']); - else - { - $guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']); - if (!$guide) - trigger_error('AjaxHander::guideManage - guide #'.$this->_post['id'].' not found'); - else - { - if ($this->_post['status'] == $guide['status']) - trigger_error('AjaxHander::guideManage - guide #'.$this->_post['id'].' already has status #'.$this->_post['status']); - else - { - if ($this->_post['status'] == GUIDE_STATUS_APPROVED) - { - if ($update($this->_post['id'], GUIDE_STATUS_APPROVED, $this->_post['msg'])) - { - Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]); - return '1'; - } - else - return '-2'; - } - else if ($this->_post['status'] == GUIDE_STATUS_REJECTED) - return $update($this->_post['id'], GUIDE_STATUS_REJECTED, $this->_post['msg']) ? '1' : '-2'; - else - trigger_error('AjaxHander::guideManage - unhandled status change request'); - } - } - } - - return '-1'; - } - protected function commentOutOfDate() : string { $ok = false; diff --git a/includes/ajaxHandler/edit.class.php b/includes/ajaxHandler/edit.class.php deleted file mode 100644 index 1275b7c4..00000000 --- a/includes/ajaxHandler/edit.class.php +++ /dev/null @@ -1,82 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], - 'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$params) - return; - - if ($params[0] == 'image') - $this->handler = 'handleUpload'; - else if ($params[0] == 'article') // has it's own editor page - $this->handler = null; - } - - /* - success: bool - id: image enumerator - type: 3 ? png : jpg - name: old filename - error: errString - */ - protected function handleUpload() : string - { - if (!User::canWriteGuide() || $this->_get['guide'] != 1) - return Util::toJSON(['success' => false, 'error' => '']); - - require_once('includes/libs/qqFileUploader.class.php'); - - $targetPath = 'static/uploads/guide/images/'; - $tmpPath = 'static/uploads/temp/'; - $tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16); - - $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); - $result = $uploader->handleUpload($tmpPath, $tmpFile, true); - - if (isset($result['success'])) - { - $finfo = new \finfo(FILEINFO_MIME); - $mime = $finfo->file($tmpPath.$result['newFilename']); - if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) - { - $i = 1; // image index - if ($files = scandir($targetPath, SCANDIR_SORT_DESCENDING)) - if (rsort($files, SORT_NATURAL) && $files[0] != '.' && $files[0] != '..') - $i = explode('.', $files[0])[0] + 1; - - $targetFile = $i . ($m[1] == 'png' ? '.png' : '.jpg'); - - // move to final location - if (!rename($tmpPath.$result['newFilename'], $targetPath.$targetFile)) - return Util::toJSON(['error' => Lang::main('intError')]); - - // send success - return Util::toJSON(array( - 'success' => true, - 'id' => $i, - 'type' => $m[1] == 'png' ? 3 : 2, - 'name' => $this->_get['qqfile'] - )); - } - - return Util::toJSON(['error' => Lang::screenshot('error', 'unkFormat')]); - } - - return Util::toJSON($result); - } -} - -?> diff --git a/includes/ajaxHandler/getdescription.class.php b/includes/ajaxHandler/getdescription.class.php deleted file mode 100644 index cabf84d9..00000000 --- a/includes/ajaxHandler/getdescription.class.php +++ /dev/null @@ -1,37 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'Aowow\AjaxHandler::checkTextBlob']] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$params || $params[0]) // should be empty - return; - - $this->handler = 'handleDescription'; - } - - protected function handleDescription() : string - { - $this->contentType = MIME_TYPE_TEXT; - - if (!User::canWriteGuide()) - return ''; - - $desc = Markup::stripTags($this->_post['description']); - - return Lang::trimTextClean($desc, 120); - } -} - -?> diff --git a/includes/ajaxHandler/guide.class.php b/includes/ajaxHandler/guide.class.php deleted file mode 100644 index eb425f15..00000000 --- a/includes/ajaxHandler/guide.class.php +++ /dev/null @@ -1,63 +0,0 @@ - [FILTER_SANITIZE_NUMBER_INT, null], - 'rating' => [FILTER_SANITIZE_NUMBER_INT, null] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params || count($this->params) != 1) - return; - - $this->contentType = MIME_TYPE_TEXT; - - // select handler - if ($this->params[0] == 'vote') - $this->handler = 'voteGuide'; - } - - protected function voteGuide() : string - { - if (!$this->_post['id'] || $this->_post['rating'] < 0 || $this->_post['rating'] > 5) - { - header('HTTP/1.0 404 Not Found', true, 404); - return ''; - } - else if (!User::canUpvote() || !User::canDownvote()) // same logic as comments? - { - header('HTTP/1.0 403 Forbidden', true, 403); - return ''; - } - // by id, not own, published - if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ?_guides WHERE `id` = ?d AND (`status` = ?d OR `rev` > 0)', $this->_post['id'], GUIDE_STATUS_APPROVED)) - { - if ($g['cuFlags'] & GUIDE_CU_NO_RATING || $g['userId'] == User::$id) - { - header('HTTP/1.0 403 Forbidden', true, 403); - return ''; - } - - if (!$this->_post['rating']) - DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_GUIDE, $this->_post['id'], User::$id); - else - DB::Aowow()->query('REPLACE INTO ?_user_ratings VALUES (?d, ?d, ?d, ?d)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']); - - $res = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS `t`, IFNULL(COUNT(*), 0) AS `n` FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_GUIDE, $this->_post['id']); - return Util::toJSON($res['n'] ? ['rating' => $res['t'] / $res['n'], 'nvotes' => $res['n']] : ['rating' => 0, 'nvotes' => 0]); - } - - return Util::toJSON(['rating' => 0, 'nvotes' => 0]); - } -} - -?> diff --git a/includes/components/guidemgr.class.php b/includes/components/guidemgr.class.php new file mode 100644 index 00000000..3b738404 --- /dev/null +++ b/includes/components/guidemgr.class.php @@ -0,0 +1,106 @@ + '#71D5FF', + self::STATUS_REVIEW => '#FFFF00', + self::STATUS_APPROVED => '#1EFF00', + self::STATUS_REJECTED => '#FF4040', + self::STATUS_ARCHIVED => '#FFD100' + ); + + private static array $ratingsStore = []; + private static ?int $imgUploadIdx = null; + + public static function createDescription(string $text) : string + { + return Lang::trimTextClean(Markup::stripTags($text), 120); + } + + public static function getRatings(array $guideIds) : array + { + if (!$guideIds) + return []; + + if (array_keys(self::$ratingsStore) == $guideIds) + return self::$ratingsStore; + + self::$ratingsStore = array_fill_keys($guideIds, ['nvotes' => 0, 'rating' => -1]); + + $ratings = DB::Aowow()->select('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1", IFNULL(MAX(IF(`userId` = ?d, `value`, 0)), 0) AS "2" FROM ?_user_ratings WHERE `type` = ?d AND `entry` IN (?a) GROUP BY `entry`', User::$id, RATING_GUIDE, $guideIds); + foreach ($ratings as $id => [$total, $count, $self]) + { + self::$ratingsStore[$id]['nvotes'] = (int)$count; + self::$ratingsStore[$id]['_self'] = (int)$self; + if ($count >= 5 ) + self::$ratingsStore[$id]['rating'] = $total / $count; + } + + return self::$ratingsStore; + } + + public static function handleUpload() : array + { + require_once('includes/libs/qqFileUploader.class.php'); + + $tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16); + + $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); + $result = $uploader->handleUpload(self::IMG_TMP_DIR, $tmpFile, true); + + if (isset($result['error'])) + return $result; + + $mime = (new \finfo(FILEINFO_MIME))?->file(self::IMG_TMP_DIR . $result['newFilename']); + + if (!preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) + return ['error' => Lang::screenshot('error', 'unkFormat')]; + + // find next empty image name (an int) + if (is_null(self::$imgUploadIdx)) + { + if ($files = scandir(self::IMG_DEST_DIR, SCANDIR_SORT_DESCENDING)) + if (rsort($files, SORT_NATURAL) && $files[0] != '.' && $files[0] != '..') + $i = explode('.', $files[0])[0] + 1; + + self::$imgUploadIdx = $i ?? 1; + } + + $targetFile = self::$imgUploadIdx . ($m[1] == 'png' ? '.png' : '.jpg'); + + // move to final location + if (!rename(self::IMG_TMP_DIR.$result['newFilename'], self::IMG_DEST_DIR.$targetFile)) + { + trigger_error('GuideMgr::handleUpload - failed to move file', E_USER_ERROR); + return ['error' => Lang::main('intError')]; + } + + return array( + 'success' => true, + 'id' => self::$imgUploadIdx, + 'type' => $m[1] == 'png' ? 3 : 2 + ); + } +} + +?> diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index c53c58b5..1923a192 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -27,7 +27,6 @@ class PageTemplate private array $pageData = []; // processed by display hooks // template data that needs further processing .. ! WARNING ! they will not get aut fetched from $context as they are already defined here - protected array $guideRating = []; private string $gStaticUrl; private string $gHost; private string $gServerTime; diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index 76fb8672..bef62121 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -71,13 +71,15 @@ trait TrGuideEditor public int $editClassId = 0; public int $editSpecId = 0; public int $editRev = 0; - public int $editStatus = GUIDE_STATUS_DRAFT; - public string $editStatusColor = GuideMgr::STATUS_COLORS[GUIDE_STATUS_DRAFT]; + public int $editStatus = GuideMgr::STATUS_DRAFT; + public string $editStatusColor = GuideMgr::STATUS_COLORS[GuideMgr::STATUS_DRAFT]; public string $editTitle = ''; public string $editName = ''; public string $editDescription = ''; public string $editText = ''; + public string $error = ''; public Locale $editLocale = Locale::EN; + public bool $isDraft = false; } class TemplateResponse extends BaseResponse diff --git a/includes/dbtypes/guide.class.php b/includes/dbtypes/guide.class.php index 5f262f55..492b47a0 100644 --- a/includes/dbtypes/guide.class.php +++ b/includes/dbtypes/guide.class.php @@ -10,14 +10,6 @@ class GuideList extends DBTypeList { use ListviewHelper; - public const /* array */ STATUS_COLORS = array( - GUIDE_STATUS_DRAFT => '#71D5FF', - GUIDE_STATUS_REVIEW => '#FFFF00', - GUIDE_STATUS_APPROVED => '#1EFF00', - GUIDE_STATUS_REJECTED => '#FF4040', - GUIDE_STATUS_ARCHIVED => '#FFD100' - ); - public static int $type = Type::GUIDE; public static string $brickFile = 'guide'; public static string $dataTable = '?_guides'; @@ -28,9 +20,10 @@ class GuideList extends DBTypeList protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ?_guides g'; protected array $queryOpts = array( - 'g' => [['a', 'c'], 'g' => 'g.`id`'], - 'a' => ['j' => ['?_account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], - 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'] + 'g' => [['a', 'c', 'ar'], 'g' => 'g.`id`'], + 'a' => ['j' => ['?_account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], + 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'], + 'ar' => ['j' => ['?_articles ar ON ar.`type` = 300 AND ar.`typeId` = g.`id`'], 's' => ', MAX(ar.`rev`) AS "latest"'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -40,23 +33,11 @@ class GuideList extends DBTypeList if ($this->error) return; - $ratings = DB::Aowow()->select('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS `t`, IFNULL(COUNT(*), 0) AS `n`, IFNULL(MAX(IF(`userId` = ?d, `value`, 0)), 0) AS `s` FROM ?_user_ratings WHERE `type` = ?d AND `entry` IN (?a)', User::$id, RATING_GUIDE, $this->getFoundIDs()); + $ratings = GuideMgr::getRatings($this->getFoundIDs()); // post processing foreach ($this->iterate() as $id => &$_curTpl) - { - if (isset($ratings[$id])) - { - $_curTpl['nvotes'] = $ratings[$id]['n']; - $_curTpl['rating'] = $ratings[$id]['n'] < 5 ? -1 : $ratings[$id]['t'] / $ratings[$id]['n']; - $_curTpl['_self'] = $ratings[$id]['s']; - } - else - { - $_curTpl['nvotes'] = 0; - $_curTpl['rating'] = -1; - } - } + $_curTpl = array_merge($_curTpl, $ratings[$id]); } public static function getName(int $id) : ?LocString @@ -133,13 +114,13 @@ class GuideList extends DBTypeList public function canBeViewed() : bool { // currently approved || has prev. approved version - return $this->getField('status') == GUIDE_STATUS_APPROVED || $this->getField('rev') > 0; + return $this->getField('status') == GuideMgr::STATUS_APPROVED || $this->getField('rev') > 0; } public function canBeReported() : bool { // not own guide && is not archived - return $this->getField('userId') != User::$id && $this->getField('status') != GUIDE_STATUS_ARCHIVED; + return $this->getField('userId') != User::$id && $this->getField('status') != GuideMgr::STATUS_ARCHIVED; } public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array diff --git a/includes/defines.php b/includes/defines.php index a9f440ef..1c423f5e 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -227,13 +227,6 @@ define('STR_ALLOW_SHORT', 0x4); define('RATING_COMMENT', 1); define('RATING_GUIDE', 2); -define('GUIDE_STATUS_NONE', 0); -define('GUIDE_STATUS_DRAFT', 1); -define('GUIDE_STATUS_REVIEW', 2); -define('GUIDE_STATUS_APPROVED', 3); -define('GUIDE_STATUS_REJECTED', 4); -define('GUIDE_STATUS_ARCHIVED', 5); - define('DEFAULT_ICON', 'inv_misc_questionmark'); define('MENU_IDX_ID', 0); // ID: A number or string; null makes the menu item a separator diff --git a/includes/libs/qqFileUploader.class.php b/includes/libs/qqFileUploader.class.php index 27b27ab8..dae92a58 100644 --- a/includes/libs/qqFileUploader.class.php +++ b/includes/libs/qqFileUploader.class.php @@ -132,7 +132,7 @@ class qqFileUploader private function toBytes(string $str) : int { - $val = trim($str); + $val = substr(trim($str), 0, -1); $last = strtolower(substr($str, -1, 1)); switch ($last) { diff --git a/includes/type.class.php b/includes/type.class.php index 189e7cea..822b31f1 100644 --- a/includes/type.class.php +++ b/includes/type.class.php @@ -111,7 +111,7 @@ abstract class Type self::CURRENCY => [CurrencyList::class, 'currency', 'g_gatheredcurrencies', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], self::SOUND => [SoundList::class, 'sound', 'g_sounds', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], self::ICON => [IconList::class, 'icon', 'g_icons', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], - self::GUIDE => [GuideList::class, 'guide', '', self::FLAG_NONE], + self::GUIDE => [GuideList::class, 'guide', '', self::FLAG_DB_TYPE], self::PROFILE => [ProfileList::class, 'profile', '', self::FLAG_FILTRABLE], // x - not known in javascript self::GUILD => [GuildList::class, 'guild', '', self::FLAG_FILTRABLE], // x self::ARENA_TEAM => [ArenaTeamList::class, 'arena-team', '', self::FLAG_FILTRABLE], // x diff --git a/includes/user.class.php b/includes/user.class.php index dc9fe853..46d11f6d 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -681,7 +681,7 @@ class User if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE)) return $result; - if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GUIDE_STATUS_ARCHIVED)) + if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GuideMgr::STATUS_ARCHIVED)) { // fix url array_walk($guides, fn(&$x) => $x['url'] = '?guide='.($x['url'] ?: $x['id'])); diff --git a/includes/utilities.php b/includes/utilities.php index 10d84995..b435c06b 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -75,8 +75,6 @@ abstract class Util public static $mapSelectorString = '%s (%d)'; - public static $guideratingString = " $(document).ready(function() {\n $('#guiderating').append(GetStars(%.10F, %s, %u, %u));\n });"; - public static $expansionString = [null, 'bc', 'wotlk']; public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index c3e0007b..d934a107 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Meine Leitfäden", 'editTitle' => "Eigenen Leitfaden bearbeiten", 'newTitle' => "Leitfaden erstellen", - 'author' => "Autor", - 'spec' => "Spezialisierung", + 'author' => "Autor: ", + 'spec' => "Spezialisierung: ", 'sticky' => "Angeheftet", - 'views' => "Ansichten", + 'views' => "Ansichten: ", 'patch' => "Patch", - 'added' => "Hinzugefügt", - 'rating' => "Wertung", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Bewertungen) [span id=guiderating][/span]", + 'added' => "Hinzugefügt: ", + 'rating' => "Wertung: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Bewertungen) [span id=guiderating][/span]", 'noVotes' => "nicht genug Bewertungen [span id=guiderating][/span]", 'byAuthor' => "Von %s", 'notFound' => "Dieser Leitfaden existiert nicht.", 'clTitle' => 'Änderungsprotokoll für "%2$s"', - 'clStatusSet' => 'Status gesetzt auf %s', - 'clCreated' => 'Erstellt', + 'clStatusSet' => 'Status gesetzt auf %s: ', + 'clCreated' => 'Erstellt: ', 'clMinorEdit' => 'Kleinere Bearbeitung', 'editor' => array( 'fullTitle' => 'Ganze Überschrift', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Name', 'nameTip' => 'Dies sollte ein einfacher und klarer Name für den Leitfaden sein, der an Orten wie Menüs und Leitfadenlisten verwendet werden kann.', 'description' => 'Beschreibung', - 'descriptionTip' => 'Beschreibung, die für Suchmaschinen verwendet wird.<br /><br />Wenn leer, wird es automatisch generiert.', + 'descriptionTip' => 'Beschreibung, die für Suchmaschinen verwendet wird.

    Wenn leer, wird es automatisch generiert.', // 'commentEmail' => 'Emailbenachrichtigung', // 'commentEmailTip' => 'Soll der Autor darüber benachrichtigt werden, dass Nutzer diesen Guide kommentieren?', 'changelog' => 'Änderungsprotokoll für diese Änderung', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Sehen Sie, wie Ihr Leitfaden aussehen wird', 'images' => 'Bilder', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Ihr Leitfaden ist im "Entwurfs"-Status und Sie sind der einzige der ihn sehen kann. Bearbeiten Sie ihn so lange Sie wollen und wenn Sie fertig sind reichen Sie ihn zur Überprüfung ein.', - GUIDE_STATUS_REVIEW => 'Ihr Leitfaden wird überprüft.', - GUIDE_STATUS_APPROVED => 'Ihr Leitfaden wurde veröffentlicht.', - GUIDE_STATUS_REJECTED => 'Ihr Leitfaden wurde abgewiesen. Nachdem die Mängel behoben wurde kann er erneut zur Überprüfung eingereicht werden.', - GUIDE_STATUS_ARCHIVED => 'Ihr Leitfaden ist veraltet und wurde archiviert. Er wird nicht mehr in der Übersicht gelistet und ist kann nicht mehr bearbeitet werden.]', + GuideMgr::STATUS_DRAFT => 'Ihr Leitfaden ist im "Entwurfs"-Status und Sie sind der einzige der ihn sehen kann. Bearbeiten Sie ihn so lange Sie wollen und wenn Sie fertig sind reichen Sie ihn zur Überprüfung ein.', + GuideMgr::STATUS_REVIEW => 'Ihr Leitfaden wird überprüft.', + GuideMgr::STATUS_APPROVED => 'Ihr Leitfaden wurde veröffentlicht.', + GuideMgr::STATUS_REJECTED => 'Ihr Leitfaden wurde abgewiesen. Nachdem die Mängel behoben wurde kann er erneut zur Überprüfung eingereicht werden.', + GuideMgr::STATUS_ARCHIVED => 'Ihr Leitfaden ist veraltet und wurde archiviert. Er wird nicht mehr in der Übersicht gelistet und ist kann nicht mehr bearbeitet werden.]', ) ), 'category' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e4a7fe76..bc8d7ff0 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "My Guides", 'editTitle' => "Edit your Guide", 'newTitle' => "Create New Guide", - 'author' => "Author", - 'spec' => "Specialization", + 'author' => "Author: ", + 'spec' => "Specialization: ", 'sticky' => "Sticky Status", - 'views' => "Views", + 'views' => "Views: ", 'patch' => "Patch", - 'added' => "Added", - 'rating' => "Rating", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] votes) [span id=guiderating][/span]", + 'added' => "Added: ", + 'rating' => "Rating: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] votes) [span id=guiderating][/span]", 'noVotes' => "not enough votes [span id=guiderating][/span]", 'byAuthor' => "By %s", 'notFound' => "This guide doesn't exist.", 'clTitle' => 'Changelog For "%2$s"', - 'clStatusSet' => 'Status set to %s', - 'clCreated' => 'Created', + 'clStatusSet' => 'Status set to %s: ', + 'clCreated' => 'Created: ', 'clMinorEdit' => 'Minor Edit', 'editor' => array( 'fullTitle' => 'Full Title', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Name', 'nameTip' => 'This should be a simple and clear name of what the guide is, for use in places like menus and guide lists.', 'description' => 'Description', - 'descriptionTip' => 'Description that will be used for search engines.<br><br>If left empty, it will be generated automatically.', + 'descriptionTip' => "Description that will be used for search engines.

    If left empty, it will be generated automatically.", // 'commentEmail' => 'Comment Emails', // 'commentEmailTip' => 'Should the author get emailed whenever a user comments on this guide?', 'changelog' => 'Changelog For This Edit', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'See how your guide will look', 'images' => 'Images', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Your guide is in "Draft" status and you are the only one able to see it. Keep editing it as long as you like, and when you feel it's ready submit it for review.', - GUIDE_STATUS_REVIEW => 'Your guide is being reviewed.', - GUIDE_STATUS_APPROVED => 'Your guide has been published.', - GUIDE_STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', - GUIDE_STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + GuideMgr::STATUS_DRAFT => 'Your guide is in "Draft" status and you are the only one able to see it. Keep editing it as long as you like, and when you feel it's ready submit it for review.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', ) ), 'category' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 95a75707..1b8ac56b 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Mis Guías", 'editTitle' => "Editar tu Guía", 'newTitle' => "Crear Nueva Guía", - 'author' => "Autor", - 'spec' => "Especialización", + 'author' => "Autor: ", + 'spec' => "Especialización: ", 'sticky' => "Estado Fijo", - 'views' => "Visto", + 'views' => "Visto: ", 'patch' => "Parche", - 'added' => "Añadido", - 'rating' => "Valoración", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votos) [span id=guiderating][/span]", + 'added' => "Añadido: ", + 'rating' => "Valoración: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votos) [span id=guiderating][/span]", 'noVotes' => "necesita más votaciones [span id=guiderating][/span]", 'byAuthor' => "Por %s", 'notFound' => "Este/a guía no existe.", 'clTitle' => 'Historial de cambios para "%2$s"', - 'clStatusSet' => 'Estado cambiado a %s', - 'clCreated' => 'Creado', + 'clStatusSet' => 'Estado cambiado a %s: ', + 'clCreated' => 'Creado: ', 'clMinorEdit' => 'Modificación menor', 'editor' => array( 'fullTitle' => 'Título completo', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Nombre', 'nameTip' => 'Este debe ser un nombre simple y claro de lo que es la guía, para usar en menús y listas de guías.', 'description' => 'Descripción', - 'descriptionTip' => 'Descripción utilizada para los motores de búsqueda.<br /><br />Si se deja vacío, se generará automáticamente.', + 'descriptionTip' => 'Descripción utilizada para los motores de búsqueda.

    Si se deja vacío, se generará automáticamente.', // 'commentEmail' => 'Enviar comentarios por email', // 'commentEmailTip' => '¿El autor debería ser notificado por correo cuando un usuario escriba comentarios en esta guía?', 'changelog' => 'Historial de cambios para esta modificación', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Mira el aspecto de tu guía.', 'images' => 'Imágenes', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Tu guía está en estado "borrador" y solo tú puedes verla. Tienes todo el tiempo del mundo para editarla y, cuando creas que ya está lista, envíala para su revisión.', - GUIDE_STATUS_REVIEW => 'Tu guía está siendo revisada.', - GUIDE_STATUS_APPROVED => 'Tu guía ha sido publicada.', - GUIDE_STATUS_REJECTED => 'Tu guía ha sido rechazada. Una vez que se hayan corregido las deficiencias, puedes volver a enviarla para revisión.', - GUIDE_STATUS_ARCHIVED => 'Tu guía está desactualizada y ha sido archivada. Ya no aparecerá en la lista y no se puede editar.', + GuideMgr::STATUS_DRAFT => 'Tu guía está en estado "borrador" y solo tú puedes verla. Tienes todo el tiempo del mundo para editarla y, cuando creas que ya está lista, envíala para su revisión.', + GuideMgr::STATUS_REVIEW => 'Tu guía está siendo revisada.', + GuideMgr::STATUS_APPROVED => 'Tu guía ha sido publicada.', + GuideMgr::STATUS_REJECTED => 'Tu guía ha sido rechazada. Una vez que se hayan corregido las deficiencias, puedes volver a enviarla para revisión.', + GuideMgr::STATUS_ARCHIVED => 'Tu guía está desactualizada y ha sido archivada. Ya no aparecerá en la lista y no se puede editar.', ) ), 'category' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 7123dde1..8d94d2c5 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Mes guides", 'editTitle' => "Editez votre Guide", 'newTitle' => "Créer un nouveau Guide", - 'author' => "Auteur", - 'spec' => "Spécialisation", + 'author' => "Auteur : ", + 'spec' => "Spécialisation : ", 'sticky' => "Statut coller", - 'views' => "Vues", + 'views' => "Vues : ", 'patch' => "Patch", - 'added' => "Ajouté", - 'rating' => "Note", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votes) [span id=guiderating][/span]", + 'added' => "Ajouté : ", + 'rating' => "Note : ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votes) [span id=guiderating][/span]", 'noVotes' => "pas assez de votes [span id=guiderating][/span]", 'byAuthor' => "Par %s", 'notFound' => "Ce guide n'existe pas.", 'clTitle' => 'Journal des changements pour "%2$s"', - 'clStatusSet' => 'Statut défini comme %s', - 'clCreated' => 'Créé', + 'clStatusSet' => 'Statut défini comme %s : ', + 'clCreated' => 'Créé : ', 'clMinorEdit' => 'Modification mineure', 'editor' => array( 'fullTitle' => 'Titre complet', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Nom', 'nameTip' => 'Ceci devrait être un nom clair et concis de ce en quoi consiste le guide, qui sera affiché dans les menus et listes de guides.', 'description' => 'Description', - 'descriptionTip' => 'Description qui sera utilisée par les moteurs de recherche.<br /><br />S'il est laissé vide, le résumé sera généré automatiquement.', + 'descriptionTip' => 'Description qui sera utilisée par les moteurs de recherche.

    S'il est laissé vide, le résumé sera généré automatiquement.', // 'commentEmail' => 'Recevoir les commentaires par courriel', // 'commentEmailTip' => 'L'auteur doit-il recevoir un courriel chaque fois qu'un utilisateur commente ce guide ?', 'changelog' => 'Journal des changements pour cette modification', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Ayez un aperçu de votre guide', 'images' => 'Images', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Votre guide est en statut "Brouillon" et vous êtes le seul à pouvoir le lire. Continuez de l'écrire comme vous le voulez, et quand vous sentez qu'il est prêt, soumettez-le pour approbation.', - GUIDE_STATUS_REVIEW => 'Your guide is being reviewed.', - GUIDE_STATUS_APPROVED => 'Your guide has been published.', - GUIDE_STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', - GUIDE_STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + GuideMgr::STATUS_DRAFT => 'Votre guide est en statut "Brouillon" et vous êtes le seul à pouvoir le lire. Continuez de l'écrire comme vous le voulez, et quand vous sentez qu'il est prêt, soumettez-le pour approbation.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', ) ), 'category' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 6e0de9a1..7304a86d 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Мои руководства", 'editTitle' => "Редактировать руководство", 'newTitle' => "Написать новое руководство", - 'author' => "Автор", - 'spec' => "Спек", + 'author' => "Автор: ", + 'spec' => "Спек: ", 'sticky' => "Закрепленный", - 'views' => "Просмотры", + 'views' => "Просмотры: ", 'patch' => "Обновление", - 'added' => "Добавлено", - 'rating' => "Рейтинг", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] проголосовало) [span id=guiderating][/span]", + 'added' => "Добавлено: ", + 'rating' => "Рейтинг: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] проголосовало) [span id=guiderating][/span]", 'noVotes' => "недостаточно голосов [span id=guiderating][/span]", 'byAuthor' => "От %s", 'notFound' => "Такого руководство не существует.", 'clTitle' => 'История изменений «%2$s»', - 'clStatusSet' => 'Присвоен статус «%s»', - 'clCreated' => 'Создано', + 'clStatusSet' => 'Присвоен статус «%s»: ', + 'clCreated' => 'Создано: ', 'clMinorEdit' => 'Небольшое изменение', 'editor' => array( 'fullTitle' => 'Полный заголовок', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Имя', 'nameTip' => 'Укажите краткое и понятное название руководства. Оно будет использоватья в меню и перечнях руководств.', 'description' => 'Описание', - 'descriptionTip' => 'Описание для поисковых систем.<br><br>Если поле будет оставлено пустым, то сайт сгенерирует описание автоматически.', + 'descriptionTip' => 'Описание для поисковых систем.

    Если поле будет оставлено пустым, то сайт сгенерирует описание автоматически.', // 'commentEmail' => 'E-mail уведомления', // 'commentEmailTip' => 'Должен ли автор руководства получать e-mail оповещения, когда к руководству оставляют комментарий?', 'changelog' => 'История изменений, внесенных этой правкой', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Посмотрите, как будет выглядеть руководство', 'images' => 'Images', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Руководство сохранено как "Черновик" — видеть его можете только вы. Правьте руководство так долго, как сочтете нужным, а когда решите, что оно готово — отправьте на одобрение.', - GUIDE_STATUS_REVIEW => 'Your guide is being reviewed.', - GUIDE_STATUS_APPROVED => 'Your guide has been published.', - GUIDE_STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', - GUIDE_STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + GuideMgr::STATUS_DRAFT => 'Руководство сохранено как "Черновик" — видеть его можете только вы. Правьте руководство так долго, как сочтете нужным, а когда решите, что оно готово — отправьте на одобрение.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', ) ), 'category' => array( diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 42e090cb..2ebf767e 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "我的指南", 'editTitle' => "编辑你的指南", 'newTitle' => "创建新指南", - 'author' => "作者", - 'spec' => "专精", + 'author' => "作者:", + 'spec' => "专精:", 'sticky' => "置顶状态", - 'views' => "浏览量", + 'views' => "浏览量:", 'patch' => "补丁", - 'added' => "已添加", - 'rating' => "评分", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] 投票)[span id=guiderating][/span]", + 'added' => "已添加:", + 'rating' => "评分:", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] 投票)[span id=guiderating][/span]", 'noVotes' => "投票数量不足 [span id=guiderating][/span]", 'byAuthor' => "来自 %s", 'notFound' => "该指南不存在。", 'clTitle' => '修改日志 "%2$s"', - 'clStatusSet' => '状态已设置为 %s', - 'clCreated' => '已创建', + 'clStatusSet' => '状态已设置为 %s:', + 'clCreated' => '已创建:', 'clMinorEdit' => '小修改', 'editor' => array( 'fullTitle' => '完整标题', @@ -188,7 +188,7 @@ $lang = array( 'name' => '名称', 'nameTip' => '这应该是一个简单明了的指南名称,用于菜单和指南列表', 'description' => '描述', - 'descriptionTip' => '描述将用于说明片段<br /><br />如果不填,则自动生成。', + 'descriptionTip' => '描述将用于说明片段

    如果不填,则自动生成。', // 'commentEmail' => '评论电子邮件', // 'commentEmailTip' => '当用户对此指南发表评论时,作者是否收到电子邮件通知?', 'changelog' => '当前编辑的修改日志', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => '自我浏览你的指南', 'images' => '图片', 'statusTip' => array( - GUIDE_STATUS_DRAFT => '你的指南目前是草稿状态,只有你自己可见。试着输入更多的文字,当你觉得可以了的时候就提交送审吧。', - GUIDE_STATUS_REVIEW => '你的指南正在审核中', - GUIDE_STATUS_APPROVED => '你的指南已发布', - GUIDE_STATUS_REJECTED => '你的指南已被拒绝。在修正问题后,你可以重新提交审核。', - GUIDE_STATUS_ARCHIVED => '你的指南已过时,并已归档。它将不再列出,也无法编辑。', + GuideMgr::STATUS_DRAFT => '你的指南目前是草稿状态,只有你自己可见。试着输入更多的文字,当你觉得可以了的时候就提交送审吧。', + GuideMgr::STATUS_REVIEW => '你的指南正在审核中', + GuideMgr::STATUS_APPROVED => '你的指南已发布', + GuideMgr::STATUS_REJECTED => '你的指南已被拒绝。在修正问题后,你可以重新提交审核。', + GuideMgr::STATUS_ARCHIVED => '你的指南已过时,并已归档。它将不再列出,也无法编辑。', ) ), 'category' => array( diff --git a/pages/admin.php b/pages/admin.php index a69209a0..6709c008 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -225,7 +225,7 @@ class AdminPage extends GenericPage private function handleGuideApprove() : void { - $pending = new GuideList([['status', GUIDE_STATUS_REVIEW]]); + $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); if ($pending->error) $data = []; else diff --git a/pages/guide.php b/pages/guide.php deleted file mode 100644 index a371694f..00000000 --- a/pages/guide.php +++ /dev/null @@ -1,549 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'rev' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'] - ); - - protected /* array */ $_post = array( - 'save' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], - 'submit' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], - 'title' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'description' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GuidePage::checkDescription'], - 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'], - 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'], - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'category' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'specId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'classId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'] - ); - - public function __construct($pageCall, $pageParam) - { - $guide = explode( "&", $pageParam, 2); - - parent::__construct($pageCall, $pageParam); - - if (isset($guide[1]) && preg_match(self::VALID_URL, $guide[1])) - $this->extra = $guide[1]; - - - /**********************/ - /* get mode + guideId */ - /**********************/ - - if (Util::checkNumeric($guide[0], NUM_CAST_INT)) - $this->typeId = $guide[0]; - else if (preg_match(self::VALID_URL, $guide[0])) - { - switch ($guide[0]) - { - case 'changelog': - if (!$this->_get['id']) - break; - - $this->show = self::SHOW_CHANGELOG; - $this->tpl = 'text-page-generic'; - $this->article = false; // do not include article from db - - // main container should be tagged:
    - // why is this here: is there a mediawiki like diff function for staff? - $this->addScript([SC_CSS_STRING, 'li input[type="radio"] {margin:0}']); - - $this->typeId = $this->_get['id']; // just to display sensible not-found msg - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `id` = ?d', $this->typeId)) - $this->typeId = intVal($id); - - break; - case 'new': - if (User::canWriteGuide()) - { - $this->show = self::SHOW_NEW; - $this->guideRevision = null; - - $this->initNew(); - return; // do not create new GuideList - } - break; - case 'edit': - if (User::canWriteGuide()) - { - if (!$this->initEdit()) - $this->notFound(Lang::game('guide'), Lang::guide('notFound')); - - $this->show = self::SHOW_EDITOR; - } - break; - default: - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($guide[0]))) - { - $this->typeId = intVal($id); - $this->guideRevision = null; - $this->articleUrl = Util::lower($guide[0]); - } - } - } - - - /*********************/ - /* load actual guide */ - /*********************/ - - $this->subject = new GuideList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('guide'), Lang::guide('notFound')); - - if (!$this->subject->canBeViewed() && !$this->subject->userCanView()) - header('Location: ?guides='.$this->subject->getField('category'), true, 302); - - if ($this->show == self::SHOW_GUIDE && $this->_get['rev'] !== null && !$this->articleUrl && $this->subject->userCanView()) - $this->guideRevision = $this->_get['rev']; - else if ($this->show == self::SHOW_GUIDE && !$this->articleUrl) - $this->guideRevision = $this->subject->getField('rev'); - else - $this->guideRevision = null; - - if (!$this->name) - $this->name = $this->subject->getField('name'); - } - - protected function generateContent() : void - { - match ($this->show) - { - self::SHOW_NEW => $this->displayNew(), - self::SHOW_EDITOR => $this->displayEditor(), - self::SHOW_GUIDE => $this->displayGuide(), - self::SHOW_CHANGELOG => $this->displayChangelog(), - default => trigger_error('GuidePage::generateContent - what content!?') - }; - } - - private function displayNew() : void - { - // init required template vars - $this->editorFields = array( - 'locale' => Lang::getLocale()->value, - 'status' => GUIDE_STATUS_DRAFT - ); - } - - private function displayEditor() : void - { - // can't check in init as subject is unknown - if ($this->subject->getField('status') == GUIDE_STATUS_ARCHIVED) - $this->notFound(Lang::game('guide'), Lang::guide('notFound')); - - $status = GUIDE_STATUS_NONE; - $rev = DB::Aowow()->selectCell('SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1', Type::GUIDE, $this->typeId); - $curStatus = DB::Aowow()->selectCell('SELECT `status` FROM ?_guides WHERE `id` = ?d ', $this->typeId); - if ($rev === null) - $rev = 0; - - if ($this->save) - { - $rev++; - - // insert Article - DB::Aowow()->query('INSERT INTO ?_articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (?d, ?d, ?d, ?d, ?d, ?)', - Type::GUIDE, $this->typeId, $this->_post['locale'], $rev, User::$groups & U_GROUP_STAFF ? User::$groups : User::$groups | U_GROUP_BLOGGER, $this->_post['body']); - - // link to Guide - $guideData = array( - 'category' => $this->_post['category'], - 'classId' => $this->_post['classId'], - 'specId' => $this->_post['specId'], - 'title' => $this->_post['title'], - 'name' => $this->_post['name'], - 'description' => $this->_post['description'] ?: Lang::trimTextClean(Markup::stripTags($this->_post['body']), 120), - 'locale' => $this->_post['locale'], - 'roles' => User::$groups, - 'status' => GUIDE_STATUS_DRAFT - ); - - DB::Aowow()->query('UPDATE ?_guides SET ?a WHERE `id` = ?d', $guideData, $this->typeId); - - // new guide -> reload editor - if ($this->_get['id'] === 0) - header('Location: ?guide=edit&id='.$this->typeId, true, 302); - else - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?d, ?)', $this->typeId, $rev, time(), User::$id, $this->_post['changelog']); - - if ($this->_post['submit']) - { - $status = GUIDE_STATUS_REVIEW; - if ($curStatus != GUIDE_STATUS_REVIEW) - { - DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', GUIDE_STATUS_REVIEW, $this->typeId); - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $this->typeId, time(), User::$id, GUIDE_STATUS_REVIEW); - } - } - } - - // init required template vars - $this->editorFields = array( - 'category' => $this->_post['category'] ?? $this->subject->getField('category'), - 'title' => $this->_post['title'] ?? $this->subject->getField('title'), - 'name' => $this->_post['name'] ?? $this->subject->getField('name'), - 'description' => $this->_post['description'] ?? $this->subject->getField('description'), - 'text' => $this->_post['body'] ?? $this->subject->getArticle(), - 'status' => $status ?: $this->subject->getField('status'), - 'classId' => $this->_post['classId'] ?? $this->subject->getField('classId'), - 'specId' => $this->_post['specId'] ?? $this->subject->getField('specId'), - 'locale' => $this->_post['locale'] ?? $this->subject->getField('locale'), - 'rev' => $rev - ); - - $this->extendGlobalData($this->subject->getJSGlobals()); - } - - private function displayGuide() : void - { - if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_QUICKFACTS)) - { - $qf = []; - if ($this->subject->getField('cuFlags') & CC_FLAG_STICKY) - $qf[] = '[span class=guide-sticky]'.Lang::guide('sticky').'[/span]'; - - $qf[] = Lang::guide('author').Lang::main('colon').'[url=?user='.$this->subject->getField('author').']'.$this->subject->getField('author').'[/url]'; - - if ($this->subject->getField('category') == 1) - { - $c = $this->subject->getField('classId'); - $s = $this->subject->getField('specId'); - if ($c > 0) - { - $this->extendGlobalIds(Type::CHR_CLASS, $c); - $qf[] = Util::ucFirst(Lang::game('class')).Lang::main('colon').'[class='.$c.']'; - } - if ($s > -1) - $qf[] = Lang::guide('spec').Lang::main('colon').'[icon class="c'.$c.' icontiny" name='.Game::$specIconStrings[$c][$s].']'.Lang::game('classSpecs', $c, $s).'[/icon]'; - } - - // $qf[] = Lang::guide('patch').Lang::main('colon').'3.3.5'; // replace with date - $qf[] = Lang::guide('added').Lang::main('colon').'[tooltip name=added]'.date('l, G:i:s', $this->subject->getField('date')).'[/tooltip][span class=tip tooltip=added]'.date(Lang::main('dateFmtShort'), $this->subject->getField('date')).'[/span]'; - - switch ($this->subject->getField('status')) - { - case GUIDE_STATUS_APPROVED: - $qf[] = Lang::guide('views').Lang::main('colon').'[n5='.$this->subject->getField('views').']'; - - if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_RATING)) - { - $this->guideRating = array( - $this->subject->getField('rating'), // avg rating - User::canUpvote() && User::canDownvote() ? 'true' : 'false', - $this->subject->getField('_self'), // my rating amt; 0 = no vote - $this->typeId // guide Id - ); - - if ($this->subject->getField('nvotes') < 5) - $qf[] = Lang::guide('rating').Lang::main('colon').Lang::guide('noVotes'); - else - $qf[] = Lang::guide('rating').Lang::main('colon').Lang::guide('votes', [round($this->subject->getField('rating'), 1), $this->subject->getField('nvotes')]); - } - break; - case GUIDE_STATUS_ARCHIVED: - $qf[] = Lang::guide('status', GUIDE_STATUS_ARCHIVED); - break; - } - - $qf = '[ul][li]'.implode('[/li][li]', $qf).'[/li][/ul]'; - - if ($this->subject->getField('status') == GUIDE_STATUS_REVIEW && User::isInGroup(U_GROUP_STAFF) && $this->_get['rev']) - { - $this->addScript([SC_JS_STRING, ' - DomContentLoaded.addEvent(function() { - let send = function (status) - { - let message = ""; - let id = $WH.g_getGets().guide; - if (status == 4) // rejected - { - while (message === "") - message = prompt("Please provide your reasoning."); - - if (message === null) - return false; - } - - $.ajax({cache: false, url: "?admin=guide", type: "POST", - error: function() { - alert("Operation failed."); - }, - success: function(json) { - if (json != 1) - alert("Operation failed."); - else - window.location.href = "?admin=guides"; - }, - data: { id: id, status: status, msg: message } - }) - - return true; - }; - - $WH.ge("btn-accept").onclick = send.bind(null, 3); - $WH.ge("btn-reject").onclick = send.bind(null, 4); - }); - ']); - - $qf .= '[h3 style="text-align:center"]Admin[/h3]'; - - $qf .= '[div style="text-align:center"][url=# id="btn-accept" class=icon-tick]Approve[/url][url=# style="margin-left:20px" id="btn-reject" class=icon-delete]Reject[/url][/div]'; - } - } - - $this->redButtons[BUTTON_GUIDE_LOG] = true; - $this->redButtons[BUTTON_GUIDE_REPORT] = $this->subject->canBeReported(); - - $this->infobox = $qf ?? ''; - $this->author = $this->subject->getField('author'); // add to g_pageInfo in GenericPage:prepareContent() - - if ($this->subject->userCanView()) - $this->redButtons[BUTTON_GUIDE_EDIT] = User::canWriteGuide() && $this->subject->getField('status') != GUIDE_STATUS_ARCHIVED; - - // the article text itself is added by GenericPage::addArticle() - } - - private function displayChangelog() : void - { - $this->addScript([SC_JS_STRING, ' - $(document).ready(function() { - var radios = $("input[type=radio]"); - function limit(col, val) { - radios.each(function(i, e) { - if (col == e.name) - return; - - if (col == "b") - e.disabled = (val <= parseInt(e.value)); - else if (col == "a") - e.disabled = (val >= parseInt(e.value)); - }); - - }; - - radios.each(function (i, e) { - e.onchange = limit.bind(this, e.name, parseInt(e.value)); - - if (i < 2 && e.name == "b") // first pair - $(e).trigger("click"); - else if (e.value == 0 && e.name == "a") // last pair - $(e).trigger("click"); - }); - }); - ']); - - $buff = '
      '; - $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) ? ($rev !== null ? '' : '') : ''; - - $logEntries = DB::Aowow()->select('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->typeId); - foreach ($logEntries as $log) - { - if ($log['status'] != GUIDE_STATUS_NONE) - $buff .= '
    • '.$inp($log['rev']).Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).Lang::main('colon').''.Util::formatTimeDiff($log['date'])."
    • \n"; - else if ($log['msg']) - $buff .= '
    • '.$inp($log['rev']).Util::formatTimeDiff($log['date']).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; - else - $buff .= '
    • '.$inp($log['rev']).Util::formatTimeDiff($log['date']).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; - } - - // append creation - $buff .= '
    • '.$inp(0).''.Lang::guide('clCreated').Lang::main('colon').''.Util::formatTimeDiff($this->subject->getField('date'))."
    • \n
    \n"; - - - if (User::isInGroup(U_GROUP_STAFF)) - $buff .= ''; - - $this->name = lang::guide('clTitle', [$this->typeId, $this->subject->getField('title')]); - $this->extraHTML = $buff; - } - - private function initNew() : void - { - $this->addScript( - [SC_JS_FILE, 'js/article-description.js'], - [SC_JS_FILE, 'js/article-editing.js'], - [SC_JS_FILE, 'js/guide-editing.js'], - [SC_JS_FILE, 'js/fileuploader.js'], - [SC_JS_FILE, 'js/toolbar.js'], - [SC_JS_FILE, 'js/AdjacentPreview.js'], - [SC_CSS_FILE, 'css/article-editing.css'], - [SC_CSS_FILE, 'css/fileuploader.css'], - [SC_CSS_FILE, 'css/guide-edit.css'], - [SC_CSS_FILE, 'css/AdjacentPreview.css'], - - [SC_CSS_STRING, '#upload-result input[type=text] { padding: 0px 2px; font-size: 12px; }'], - [SC_CSS_STRING, '#upload-result > span { display:block; height: 22px; }'], - [SC_CSS_STRING, '#upload-result { display: inline-block; text-align:right; }'], - [SC_CSS_STRING, '#upload-progress { display: inline-block; margin-right:8px; }'] - ); - - $this->articleUrl = 'new'; - $this->tpl = 'guide-edit'; - $this->name = Lang::guide('newTitle'); - - Lang::sort('guide', 'category'); - - $this->typeId = 0; // signals 'edit' to create new guide - } - - private function initEdit() : bool - { - $this->addScript( - [SC_JS_FILE, 'js/article-description.js'], - [SC_JS_FILE, 'js/article-editing.js'], - [SC_JS_FILE, 'js/guide-editing.js'], - [SC_JS_FILE, 'js/fileuploader.js'], - [SC_JS_FILE, 'js/toolbar.js'], - [SC_JS_FILE, 'js/AdjacentPreview.js'], - [SC_CSS_FILE, 'css/article-editing.css'], - [SC_CSS_FILE, 'css/fileuploader.css'], - [SC_CSS_FILE, 'css/guide-edit.css'], - [SC_CSS_FILE, 'css/AdjacentPreview.css'], - - [SC_CSS_STRING, '#upload-result input[type=text] { padding: 0px 2px; font-size: 12px; }'], - [SC_CSS_STRING, '#upload-result > span { display:block; height: 22px; }'], - [SC_CSS_STRING, '#upload-result { display: inline-block; text-align:right; }'], - [SC_CSS_STRING, '#upload-progress { display: inline-block; margin-right:8px; }'] - ); - - $this->articleUrl = 'edit'; - $this->tpl = 'guide-edit'; - $this->name = Lang::guide('editTitle'); - $this->save = $this->_post['save'] || $this->_post['submit']; - - // reject inconsistent guide data - if ($this->save) - { - // req: set data - if (!$this->_post['title'] || !$this->_post['name'] || !$this->_post['body'] || $this->_post['locale'] === null) - return false; - - // req: valid data - if (!in_array($this->_post['category'], $this->validCats) || !(Cfg::get('LOCALES') & (1 << $this->_post['locale']))) - return false; - - // sanitize: spec / class - if ($this->_post['category'] == 1) // Classes - { - if ($this->_post['classId'] && !ChrClass::tryFrom($this->_post['classId'])) - $this->_post['classId'] = 0; - - if (!in_array($this->_post['specId'], [-1, 0, 1, 2])) - $this->_post['specId'] = -1; - if ($this->_post['specId'] > -1 && !$this->_post['classId']) - $this->_post['specId'] = -1; - } - else - { - $this->_post['classId'] = 0; - $this->_post['specId'] = -1; - } - } - - if ($this->_get['id']) // edit existing guide - { - $this->typeId = $this->_get['id']; // just to display sensible not-found msg - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `id` = ?d AND `status` <> ?d {AND `userId` = ?d}', $this->typeId, GUIDE_STATUS_ARCHIVED, User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : User::$id)) - $this->typeId = intVal($id); - } - else if ($this->_get['id'] === 0) // create new guide and load in editor - $this->typeId = DB::Aowow()->query('INSERT INTO ?_guides (`userId`, `date`, `status`) VALUES (?d, ?d, ?d)', User::$id, time(), GUIDE_STATUS_DRAFT); - - return $this->typeId > 0; - } - - protected function editorFields(string $field, bool $asInt = false) : string|int - { - return $this->editorFields[$field] ?? ($asInt ? 0 : ''); - } - - protected function generateTooltip() - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = strip_tags($this->name); - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - } - - return sprintf($this->powerTpl, Util::toJSON($this->articleUrl ?: $this->typeId), Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); - } - - protected function generatePath() : void - { - if ($x = $this->subject?->getField('category')) - $this->path[] = $x; - } - - protected function generateTitle() : void - { - if ($this->show == self::SHOW_EDITOR) - array_unshift($this->title, Lang::guide('editTitle').Lang::main('colon').$this->subject->getField('title'), Lang::game('guides')); - if ($this->show == self::SHOW_NEW) - array_unshift($this->title, Lang::guide('newTitle'), Lang::game('guides')); - else - array_unshift($this->title, $this->subject->getField('title'), Lang::game('guides')); - } - - protected function postCache() : void - { - // increment views of published guide; ignore caching - if ($this->subject?->getField('status') == GUIDE_STATUS_APPROVED) - DB::Aowow()->query('UPDATE ?_guides SET `views` = `views` + 1 WHERE `id` = ?d', $this->typeId); - } - - protected static function checkDescription(string $str) : string - { - // run checkTextBlob and also replace \n => \s and \s+ => \s - $str = preg_replace(parent::PATTERN_TEXT_BLOB, '', $str); - - $str = strtr($str, ["\n" => ' ', "\r" => ' ']); - - return preg_replace('/\s+/', ' ', trim($str)); - } -} - -?> diff --git a/pages/guides.php b/pages/guides.php deleted file mode 100644 index a2194a6c..00000000 --- a/pages/guides.php +++ /dev/null @@ -1,102 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - if ($pageCall == 'my-guides') - { - if (!User::isLoggedIn()) - $this->error(); - - $this->name = Util::ucFirst(Lang::guide('myGuides')); - $this->myGuides = true; - } - else - $this->name = Util::ucFirst(Lang::game('guides')); - } - - protected function generateContent() - { - $hCols = ['patch']; // pointless: display date instead - $vCols = []; - $xCols = ['$Listview.extraCols.date']; // ok - - if ($this->myGuides) - { - $conditions = [['userId', User::$id]]; - $hCols[] = 'author'; - $vCols[] = 'status'; - } - else - { - $conditions = array( - ['locale', Lang::getLocale()->value], - ['status', GUIDE_STATUS_ARCHIVED, '!'], // never archived guides - [ - 'OR', - ['status', GUIDE_STATUS_APPROVED], // currently approved - ['rev', 0, '>'] // has previously approved revision - ] - ); - if (isset($this->category[0])) - $conditions[] = ['category', $this->category]; - } - - $data = []; - $guides = new GuideList($conditions); - if (!$guides->error) - $data = array_values($guides->getListviewData()); - - $tabData = array( - 'data' => $data, - 'name' => Util::ucFirst(Lang::game('guides')), - 'hiddenCols' => $hCols, - 'visibleCols' => $vCols, - 'extraCols' => $xCols - ); - - $this->lvTabs[] = [GuideList::$brickFile, $tabData]; - - $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if (isset($this->category[0])) - array_unshift($this->title, Lang::guide('category', $this->category[0])); - - } - - protected function generatePath() - { - if (isset($this->category[0])) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/static/js/global.js b/static/js/global.js index 453f644e..3c8741aa 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -22987,17 +22987,14 @@ function g_modifyUrl(url, params, opt) { } function g_enhanceTextarea (ta, opt) { - if (!(ta instanceof jQuery)) { + if (!(ta instanceof jQuery)) ta = $(ta); - } - if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") { + if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") return; - } - if (typeof opt != "object") { + if (typeof opt != "object") opt = {}; - } var canResize = (function(el) { if (!el.dynamicResizeOption) @@ -23016,63 +23013,66 @@ function g_enhanceTextarea (ta, opt) { var wrapper = $("
    ", { "class": "enhanced-textarea-wrapper" }).insertBefore(ta).append(ta); if (!opt.hasOwnProperty("color")) - wrapper.addClass("enhanced-textarea-dark") + wrapper.addClass("enhanced-textarea-dark"); else if (opt.color) - wrapper.addClass("enhanced-textarea-" + opt.color) + wrapper.addClass("enhanced-textarea-" + opt.color); if (!opt.hasOwnProperty("dynamicSizing") || opt.dynamicSizing || opt.dynamicResizeOption) { var expander = $("
    ", { "class": "enhanced-textarea-expander" }).prependTo(wrapper); - var n = function(E, D, F) { - if (!F()) + var dynamicResize = function(textarea, exactHeight, canResizeFn) { + if (!canResizeFn()) return; - // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
    ") + "
    ").height() + (D ? 14 : 34) + "px") - E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()) + "
    ").height() + (D ? 14 : 34) + "px") + // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
    ") + "
    ").height() + (D ? 14 : 34) + "px"); + textarea.css("height", textarea.siblings(".enhanced-textarea-expander").html($WH.htmlentities(textarea.val()) + "
    ").height() + (exactHeight ? 14 : 34) + "px"); }; - ta.bind("keydown keyup change", n.bind(this, ta, opt.exactLineHeights, canResize)); - n(ta, opt.exactLineHeights, canResize); - var setWidth = function(D) { - D.css("width", D.parent().width() + "px") - }; + ta.bind("keydown keyup change", dynamicResize.bind(this, ta, opt.exactLineHeights, canResize)); + dynamicResize(ta, opt.exactLineHeights, canResize); + + var setWidth = function(el) { el.css("width", el.parent().width() + "px"); }; + setWidth(expander); setTimeout(setWidth.bind(null, expander), 1); - if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) { - wrapper.addClass("enhanced-textarea-dynamic-sizing") - } - } - if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) { - wrapper.addClass("enhanced-textarea-focus-changes") + + if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) + wrapper.addClass("enhanced-textarea-dynamic-sizing"); } + + if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) + wrapper.addClass("enhanced-textarea-focus-changes"); + if (opt.markup) { - var w = $("
    ", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); - var y = $("
    ", { "class": "enhanced-textarea-markup" }).appendTo(w); - var z = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(y); - var k = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(y); + var _markupMenu = $("
    ", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); + var _segments = $("
    ", { "class": "enhanced-textarea-markup" }).appendTo(_markupMenu); + var _toolbar = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _menu = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); if (opt.markup == "inline") - ar_AddInlineToolbar(ta.get(0), z.get(0), k.get(0)); + ar_AddInlineToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); else - ar_AddToolbar(ta.get(0), z.get(0), k.get(0)); + ar_AddToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); if (opt.dynamicResizeOption) { - var t = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(y); - var C = $("
    new configuration
    ' . $head . $rows . '
    '); + } + } + + private function buildRow(string $key, string $value, int $flags, ?string $default, string $comment) : string + { + $buff = ''; + $info = explode(' - ', $comment); + $key = $flags & Cfg::FLAG_PHP ? strtolower($key) : strtoupper($key); + + // name + if (!empty($info[0])) + $buff .= ''.sprintf(Util::$dfnString, $info[0], $key).''; + else + $buff .= ''.$key.''; + + // value + if ($flags & Cfg::FLAG_TYPE_BOOL) + $buff .= '
    '; + else if ($flags & Cfg::FLAG_OPT_LIST && !empty($info[1])) + { + $buff .= ''; + } + else if ($flags & Cfg::FLAG_BITMASK && !empty($info[1])) + { + $buff .= '
    '; + foreach (explode(', ', $info[1]) as $option) + { + [$idx, $name] = explode(':', $option); + $buff .= ''; + } + $buff .= '
    '; + } + else + $buff .= ''; + + // actions + $buff .= ''; + + $buff .= ''; + + if ($default) + $buff .= '|'; + else + $buff .= '|'; + + if (!($flags & Cfg::FLAG_PERSISTENT)) + $buff .= '|'; + + $buff .= ''; + + return $buff; + } +} + +?> diff --git a/endpoints/admin/siteconfig_add.php b/endpoints/admin/siteconfig_add.php new file mode 100644 index 00000000..a99e77a2 --- /dev/null +++ b/endpoints/admin/siteconfig_add.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertGET('key', 'val')) + { + trigger_error('AdminSiteconfigActionAddResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $key = trim($this->_get['key']); + $val = trim(urldecode($this->_get['val'])); + + $this->result = Cfg::add($key, $val); + } +} + +?> diff --git a/endpoints/admin/siteconfig_remove.php b/endpoints/admin/siteconfig_remove.php new file mode 100644 index 00000000..cef906d0 --- /dev/null +++ b/endpoints/admin/siteconfig_remove.php @@ -0,0 +1,30 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]] + ); + + protected function generate() : void + { + if (!$this->assertGET('key')) + { + trigger_error('AdminSiteconfigActionRemoveResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $this->result = Cfg::delete($this->_get['key']); + } +} + +?> diff --git a/endpoints/admin/siteconfig_update.php b/endpoints/admin/siteconfig_update.php new file mode 100644 index 00000000..5afe0bec --- /dev/null +++ b/endpoints/admin/siteconfig_update.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertGET('key', 'val')) + { + trigger_error('AdminSiteconfigActionUpdateResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $key = trim($this->_get['key']); + $val = trim(urldecode($this->_get['val'])); + + $this->result = Cfg::set($key, $val); + } +} + +?> diff --git a/includes/cfg.class.php b/includes/cfg.class.php index 2ddffd10..138e541d 100644 --- a/includes/cfg.class.php +++ b/includes/cfg.class.php @@ -7,8 +7,8 @@ if (!defined('AOWOW_REVISION')) class Cfg { - public const PATTERN_CONF_KEY = '/[a-z0-9_\.\-]/i'; - public const PATTERN_INV_CONF_KEY = '/[^a-z0-9_\.\-]/i'; + public const PATTERN_CONF_KEY_CHAR = '/[a-z0-9_\.\-]/i'; + public const PATTERN_CONF_KEY_FULL = '/^[a-z0-9_\.\-]+$/i'; public const PATTERN_INVALID_CHARS = '/\p{C}/ui'; // config flags @@ -116,7 +116,7 @@ class Cfg $key = strtolower($key); - if (preg_match(self::PATTERN_INV_CONF_KEY, $key)) + if (!preg_match(self::PATTERN_CONF_KEY_FULL, $key)) return 'invalid chars in option name: [a-z 0-9 _ . -] are allowed'; if (isset(self::$store[$key])) @@ -129,7 +129,7 @@ class Cfg return 'this configuration option cannot be set'; $flags = self::FLAG_TYPE_STRING | self::FLAG_PHP; - if (!DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, ?d, ?d)', $key, $value, self::CAT_MISCELLANEOUS, $flags)) + if (!is_int(DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, ?d, ?d)', $key, $value, self::CAT_MISCELLANEOUS, $flags))) return 'internal error'; self::$store[$key] = [$value, $flags, self::CAT_MISCELLANEOUS, null, null]; @@ -349,7 +349,7 @@ class Cfg } if ($flags & self::FLAG_TYPE_BOOL) - $value = (bool)$value; + $value = $value ? 1 : 0; return ''; } @@ -384,7 +384,17 @@ class Cfg trigger_error($msg, E_USER_ERROR); } - private static function locales(/*int|string*/ $value, ?string &$msg = '') : bool + private static function useSSL() : bool + { + return (($_SERVER['HTTPS'] ?? 'off') != 'off') || (self::$store['force_ssl'][self::IDX_VALUE] ?? 0); + } + + + /***************************/ + /* onSet/onLoad validators */ + /***************************/ + + private static function locales(int|string $value, ?string &$msg = '') : bool { if (!CLI) return true; @@ -397,7 +407,7 @@ class Cfg return false; } - private static function acc_auth_mode(/*int|string*/ $value, ?string &$msg = '') : bool + private static function acc_auth_mode(int|string $value, ?string &$msg = '') : bool { if ($value == 1 && !extension_loaded('gmp')) { @@ -408,7 +418,7 @@ class Cfg return true; } - private static function profiler_enable(/*int|string*/ $value, ?string &$msg = '') : bool + private static function profiler_enable(int|string $value, ?string &$msg = '') : bool { if ($value != 1) return true; @@ -416,7 +426,7 @@ class Cfg return Profiler::queueStart($msg); } - private static function static_host(/*int|string*/ $value, ?string &$msg = '') : bool + private static function static_host(int|string $value, ?string &$msg = '') : bool { self::$store['static_url'] = array( // points js to images & scripts (self::useSSL() ? 'https://' : 'http://').$value, @@ -429,7 +439,7 @@ class Cfg return true; } - private static function site_host(/*int|string*/ $value, ?string &$msg = '') : bool + private static function site_host(int|string $value, ?string &$msg = '') : bool { self::$store['host_url'] = array( // points js to executable files (self::useSSL() ? 'https://' : 'http://').$value, @@ -442,9 +452,15 @@ class Cfg return true; } - private static function useSSL() : bool + private static function cache_mode(int|string $value, ?string &$msg = '') : bool { - return (($_SERVER['HTTPS'] ?? 'off') != 'off') || (self::$store['force_ssl'][self::IDX_VALUE] ?? 0); + if ($value & 0x2 && !class_exists('\Memcached')) + { + $msg .= 'PHP extension Memcached is not enabled.'; + return false; + } + + return true; } private static function screenshot_min_size(int|string $value, ?string &$msg = '') : bool diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 2be3beb7..3bc02c3b 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -217,6 +217,12 @@ trait TrCache private function memcached() : ?\Memcached { + if (!class_exists('\Memcached')) + { + trigger_error('Memcached is enabled by us but not in php!', E_USER_ERROR); + return null; + } + if (!$this->memcached && (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED)) { $this->memcached = new \Memcached(); diff --git a/setup/tools/clisetup/siteconfig.us.php b/setup/tools/clisetup/siteconfig.us.php index 17dcf0c8..405061b7 100644 --- a/setup/tools/clisetup/siteconfig.us.php +++ b/setup/tools/clisetup/siteconfig.us.php @@ -114,7 +114,7 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); } - if (CLI::read(['idx' => ['', false, false, Cfg::PATTERN_CONF_KEY]], $uiIndex) && $uiIndex && $uiIndex['idx'] !== '') + if (CLI::read(['idx' => ['', false, false, Cfg::PATTERN_CONF_KEY_CHAR]], $uiIndex) && $uiIndex && $uiIndex['idx'] !== '') { $idx = array_search(strtolower($uiIndex['idx']), $cfgList); if ($idx === false) @@ -147,7 +147,7 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); $setting = array( - 'key' => ['option name', false, false, Cfg::PATTERN_CONF_KEY], + 'key' => ['option name', false, false, Cfg::PATTERN_CONF_KEY_CHAR], 'val' => ['value'] ); if (CLI::read($setting, $uiSetting) && $uiSetting) @@ -443,7 +443,13 @@ CLISetup::registerUtility(new class extends UtilityScript private function testCase(&$protocol, &$host, $testFile, &$status) : bool { - $res = get_headers($protocol.$host.$testFile, true); + // https://stackoverflow.com/questions/14279095/allow-self-signed-certificates-for-https-wrapper + $ctx = stream_context_create(array( + 'ssl' => ['verify_peer' => false, + 'allow_self_signed' => true] + )); + + $res = get_headers($protocol.$host.$testFile, true, $ctx); if (!preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) return false; diff --git a/setup/updates/1758578400_10.sql b/setup/updates/1758578400_10.sql new file mode 100644 index 00000000..5e7cd68d --- /dev/null +++ b/setup/updates/1758578400_10.sql @@ -0,0 +1,2 @@ +-- set on_set_fn check +UPDATE `aowow_config` SET `flags` = `flags` | 1024 WHERE `key` = 'cache_mode'; diff --git a/template/pages/admin/siteconfig.tpl.php b/template/pages/admin/siteconfig.tpl.php index ed8a9238..70324333 100644 --- a/template/pages/admin/siteconfig.tpl.php +++ b/template/pages/admin/siteconfig.tpl.php @@ -1,6 +1,8 @@ - +brick('header'); ?> + $this->brick('header'); +?> \n\n"; + + parent::generate(); + } +} + +?> diff --git a/endpoints/admin/weight-presets_save.php b/endpoints/admin/weight-presets_save.php new file mode 100644 index 00000000..50038a54 --- /dev/null +++ b/endpoints/admin/weight-presets_save.php @@ -0,0 +1,74 @@ + ['filter' => FILTER_VALIDATE_INT ], + '__icon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'scale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkScale'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', '__icon', 'scale')) + { + trigger_error('AdminWeightpresetsActionSaveResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + // save to db + DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $this->_post['id']); + DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']); + + foreach (explode(',', $this->_post['scale']) as $s) + { + [$k, $v] = explode(':', $s); + + if (!in_array($k, Util::$weightScales) || $v < 1) + continue; + + if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null) + { + trigger_error('AdminWeightpresetsActionSaveResponse - failed to write to database', E_USER_ERROR); + $this->result = self::ERR_WRITE_DB; + return; + } + } + + // write dataset + exec('php aowow --build=weightPresets', $out); + foreach ($out as $o) + if (strstr($o, 'ERR')) + { + trigger_error('AdminWeightpresetsActionSaveResponse - failed to write dataset' . $o, E_USER_ERROR); + $this->result = self::ERR_WRITE_FILE; + return; + } + + // all done + $this->result = self::ERR_NONE; + } + + protected static function checkScale(string $val) : string + { + if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) + return $val; + + return ''; + } +} + +?> diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php deleted file mode 100644 index 1b2b6f95..00000000 --- a/includes/ajaxHandler/admin.class.php +++ /dev/null @@ -1,258 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], - 'key' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ], - 'all' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet' ], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkUser' ], - 'val' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'guid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'area' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'floor' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ] - ); - protected $_post = array( - 'alt' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkScale' ], - '__icon' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ], - 'status' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'msg' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params) - return; - - if ($this->params[0] == 'siteconfig' && $this->_get['action']) - { - if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) - return; - - if ($this->_get['action'] == 'add') - $this->handler = 'confAdd'; - else if ($this->_get['action'] == 'remove') - $this->handler = 'confRemove'; - else if ($this->_get['action'] == 'update') - $this->handler = 'confUpdate'; - } - else if ($this->params[0] == 'weight-presets' && $this->_get['action']) - { - if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_BUREAU)) - return; - - if ($this->_get['action'] == 'save') - $this->handler = 'wtSave'; - } - else if ($this->params[0] == 'spawn-override') - { - if (!User::isInGroup(U_GROUP_MODERATOR)) - return; - - $this->handler = 'spawnPosFix'; - } - else if ($this->params[0] == 'comment') - { - if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD)) - return; - - $this->handler = 'commentOutOfDate'; - } - } - - protected function confAdd() : string - { - $key = trim($this->_get['key']); - $val = trim(urldecode($this->_get['val'])); - - return Cfg::add($key, $val); - } - - protected function confRemove() : string - { - if (!$this->reqGET('key')) - return 'invalid configuration option given'; - - return Cfg::delete($this->_get['key']); - } - - protected function confUpdate() : string - { - $key = trim($this->_get['key']); - $val = trim(urldecode($this->_get['val'])); - - return Cfg::set($key, $val); - } - - protected function wtSave() : string - { - if (!$this->reqPOST('id', '__icon')) - return '3'; - - // save to db - DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE id = ?d', $this->_post['id']); - DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']); - - foreach (explode(',', $this->_post['scale']) as $s) - { - [$k, $v] = explode(':', $s); - - if (!in_array($k, Util::$weightScales) || $v < 1) - continue; - - if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null) - return '1'; - } - - // write dataset - exec('php aowow --build=weightPresets', $out); - foreach ($out as $o) - if (strstr($o, 'ERR')) - return '2'; - - // all done - return '0'; - } - - protected function spawnPosFix() : string - { - if (!$this->reqGET('type', 'guid', 'area', 'floor')) - return '-4'; - - $guid = $this->_get['guid']; - $type = $this->_get['type']; - $area = $this->_get['area']; - $floor = $this->_get['floor']; - - if (!in_array($type, [Type::NPC, Type::OBJECT, Type::SOUND, Type::AREATRIGGER, Type::ZONE])) - return '-3'; - - DB::Aowow()->query('REPLACE INTO ?_spawns_override VALUES (?d, ?d, ?d, ?d, ?d)', $type, $guid, $area, $floor, AOWOW_REVISION); - - if ($wPos = WorldPosition::getForGUID($type, $guid)) - { - if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor)) - { - $updGUIDs = [$guid]; - $newPos = array( - 'posX' => $point[0]['posX'], - 'posY' => $point[0]['posY'], - 'areaId' => $point[0]['areaId'], - 'floor' => $point[0]['floor'] - ); - - // if creature try for waypoints - if ($type == Type::NPC) - { - $jobs = array( - 'SELECT -w.id AS `entry`, w.point AS `pointId`, w.position_x AS `posX`, w.position_y AS `posY` FROM creature_addon ca JOIN waypoint_data w ON w.id = ca.path_id WHERE ca.guid = ?d AND ca.path_id <> 0', - 'SELECT `entry`, `pointId`, `location_x` AS `posX`, `location_y` AS `posY` FROM `script_waypoint` WHERE `entry` = ?d', - 'SELECT `entry`, `pointId`, `position_x` AS `posX`, `position_y` AS `posY` FROM `waypoints` WHERE `entry` = ?d' - ); - - foreach ($jobs as $idx => $job) - { - if ($swp = DB::World()->select($job, $idx ? $wPos[$guid]['id'] : $guid)) - { - foreach ($swp as $w) - { - if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor)) - { - $p = array( - 'posX' => $point[0]['posX'], - 'posY' => $point[0]['posY'], - 'areaId' => $point[0]['areaId'], - 'floor' => $point[0]['floor'] - ); - - DB::Aowow()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']); - } - } - } - } - - // also move linked vehicle accessories (on the very same position) - $updGUIDs = array_merge($updGUIDs, DB::Aowow()->selectCol('SELECT s2.guid FROM ?_spawns s1 JOIN ?_spawns s2 ON s1.posX = s2.posX AND s1.posY = s2.posY AND - s1.areaId = s2.areaId AND s1.floor = s2.floor AND s2.guid < 0 WHERE s1.guid = ?d', $guid)); - } - - DB::Aowow()->query('UPDATE ?_spawns SET ?a WHERE `type` = ?d AND `guid` IN (?a)', $newPos, $type, $updGUIDs); - - return '1'; - } - - return '-2'; - } - - return '-1'; - } - - protected function commentOutOfDate() : string - { - $ok = false; - switch ($this->_post['status']) - { - case 0: // up to date - if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id'])) - if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) - $rep->close(Report::STATUS_CLOSED_WONTFIX); - break; - case 1: // outdated, mark as deleted and clear other flags (sticky + outdated) - if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = ?d, `deleteUserId` = ?d, `deleteDate` = ?d WHERE `id` = ?d', CC_FLAG_DELETED, User::$id, time(), $this->_post['id'])) - if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) - $rep->close(Report::STATUS_CLOSED_SOLVED); - break; - default: - trigger_error('AjaxHandler::comentOutOfDate - called with invalid status'); - } - - return $ok ? '1' : '0'; - } - - - /***************************/ - /* additional input filter */ - /***************************/ - - protected static function checkKey(string $val) : string - { - // expecting string - if (preg_match(Cfg::PATTERN_INV_CONF_KEY, $val)) - return ''; - - return strtolower($val); - } - - protected static function checkUser($val) : string - { - $n = Util::lower(trim(urldecode($val))); - - if (User::isValidName($n)) - return $n; - - return ''; - } - - protected static function checkScale($val) : string - { - if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) - return $val; - - return ''; - } -} - -?> diff --git a/pages/admin.php b/pages/admin.php deleted file mode 100644 index 6709c008..00000000 --- a/pages/admin.php +++ /dev/null @@ -1,322 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] - ); - - private $generator = ''; - - public function __construct($pageCall, $pageParam) - { - switch ($pageParam) - { - case 'phpinfo': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV; - $this->generator = 'handlePhpInfo'; - $this->tpl = 'list-page-generic'; - - array_push($this->path, 2, 21); - $this->name = 'PHP Information'; - break; - case 'siteconfig': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV; - $this->generator = 'handleConfig'; - $this->tpl = 'admin/siteconfig'; - - array_push($this->path, 2, 18); - $this->name = 'Site Configuration'; - break; - case 'weight-presets': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV | U_GROUP_BUREAU; - $this->generator = 'handleWeightPresets'; - $this->tpl = 'admin/weight-presets'; - - array_push($this->path, 2, 16); - $this->name = 'Weight Presets'; - break; - case 'guides': - $this->reqUGroup = U_GROUP_STAFF; - $this->generator = 'handleGuideApprove'; - $this->tpl = 'list-page-generic'; - - array_push($this->path, 1, 25); - $this->name = 'Pending Guides'; - break; - case 'out-of-date': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD; - $this->generator = 'handleOutOfDate'; - $this->tpl = 'list-page-generic'; - - array_push($this->path, 1, 23); - $this->name = 'Out of Date Comments'; - break; - case 'reports': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_EDITOR | U_GROUP_MOD | U_GROUP_LOCALIZER | U_GROUP_SCREENSHOT | U_GROUP_VIDEO; - $this->generator = 'handleReports'; - $this->tpl = 'admin/reports'; - - array_push($this->path, 5); - $this->name = 'Reports'; - break; - default: // error out through unset template - } - - parent::__construct($pageCall, $pageParam); - } - - protected function generateContent() : void - { - if (!$this->generator || function_exists($this->generator)) - return; - - $this->{$this->generator}(); - } - - private function handleConfig() : void - { - $this->addScript( - [SC_CSS_STRING, '.grid input[type=\'text\'], .grid input[type=\'number\'] { width:250px; text-align:left; }'], - [SC_CSS_STRING, '.grid input[type=\'button\'] { width:65px; padding:2px; }'], - [SC_CSS_STRING, '.grid a.tip { margin:0px 5px; opacity:0.8; }'], - [SC_CSS_STRING, '.grid a.tip:hover { opacity:1; }'], - [SC_CSS_STRING, '.grid tr { height:30px; }'], - [SC_CSS_STRING, '.grid .disabled { opacity:0.4 !important; }'], - [SC_CSS_STRING, '.grid .status { position:absolute; right:5px; }'] - ); - - $head = 'KeyValueOptions'; - foreach (Cfg::$categories as $idx => $catName) - { - $rows = ''; - foreach (Cfg::forCategory($idx) as $key => [$value, $flags, , $default, $comment]) - $rows .= $this->configAddRow($key, $value, $flags, $default, $comment); - - if ($idx == Cfg::CAT_MISCELLANEOUS) - $rows .= 'new configuration'; - - if (!$rows) - continue; - - $this->lvTabs[] = [null, array( - 'data' => '' . $head . $rows . '
    ', - 'name' => $catName, - 'id' => Profiler::urlize($catName) - )]; - } - } - - private function handlePhpInfo() : void - { - $this->addScript([ - SC_CSS_STRING, "\npre {margin: 0px; font-family: monospace;}\n" . - "td, th { border: 1px solid #000000; vertical-align: baseline;}\n" . - ".p {text-align: left;}\n" . - ".e {background-color: #ccccff; font-weight: bold; color: #000000;}\n" . - ".h {background-color: #9999cc; font-weight: bold; color: #000000;}\n" . - ".v {background-color: #cccccc; color: #000000;}\n" . - ".vr {background-color: #cccccc; text-align: right; color: #000000;}\n" - ]); - - $bits = [INFO_GENERAL, INFO_CONFIGURATION, INFO_ENVIRONMENT, INFO_MODULES]; - $names = ['General', '', '', 'Module']; - foreach ($bits as $i => $b) - { - ob_start(); - phpinfo($b); - $buff = ob_get_contents(); - ob_end_clean(); - - $buff = explode('
    ', $buff)[1]; - $buff = explode('
    ', $buff); - array_pop($buff); // remove last from stack - $buff = implode('
    ', $buff); // sew it together - - if (strpos($buff, '

    ')) - $buff = explode('

    ', $buff)[1]; - - if (strpos($buff, '

    ')) - { - $parts = explode('

    ', $buff); - foreach ($parts as $p) - { - if (!preg_match('/\w/i', $p)) - continue; - - $p = explode('

    ', $p); - - $body = substr($p[1], 0, -7); // remove trailing "
    \n" - $name = $names[$i] ? $names[$i].': ' : ''; - if (preg_match('/]*>([\w\s\d]+)<\/a>/i', $p[0], $m)) - $name .= $m[1]; - else - $name .= $p[0]; - - $this->lvTabs[] = [null, array( - 'data' => $body, - 'id' => strtolower(strtr($name, [' ' => ''])), - 'name' => $name - )]; - } - } - else - { - $this->lvTabs[] = [null, array( - 'data' => $buff, - 'id' => strtolower($names[$i]), - 'name' => $names[$i] - )]; - } - } - } - - private function handleWeightPresets() : void - { - $this->addScript( - [SC_JS_FILE, 'js/filters.js'], - [SC_CSS_STRING, '.wt-edit {display:inline-block; vertical-align:top; width:350px;}'] - ); - - $head = $body = ''; - - $scales = DB::Aowow()->select('SELECT `class` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `name`, `icon` FROM ?_account_weightscales WHERE `userId` = 0 ORDER BY `class`, `orderIdx` ASC'); - $weights = DB::Aowow()->selectCol('SELECT awd.`id` AS ARRAY_KEY, awd.`field` AS ARRAY_KEY2, awd.`val` FROM ?_account_weightscale_data awd JOIN ?_account_weightscales ad ON awd.`id` = ad.`id` WHERE ad.`userId` = 0'); - foreach ($scales as $cl => $data) - { - $ul = ''; - foreach ($data as $id => $s) - { - $weights[$id]['__icon'] = $s['icon']; - $ul .= '[url=# onclick="loadScale.bind(this, '.$id.')();"]'.$s['name'].'[/url][br]'; - } - - $head .= '[td=header]'.Lang::game('cl', $cl).'[/td]'; - $body .= '[td valign=top]'.$ul.'[/td]'; - } - - $this->extraText = '[table class=grid][tr]'.$head.'[/tr][tr]'.$body.'[/tr][/table]'; - - $this->extraHTML = '\n\n"; - } - - private function handleGuideApprove() : void - { - $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); - if ($pending->error) - $data = []; - else - { - $data = $pending->getListviewData(); - $latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ?_articles WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs()); - foreach ($latest as $id => $rev) - $data[$id]['rev'] = $rev; - } - - $this->lvTabs[] = [GuideList::$brickFile, array( - 'data' => array_values($data), - 'hiddenCols' => ['patch', 'comments', 'views', 'rating'], - 'extraCols' => '$_' - ), 'guideAdminCol']; - } - - private function handleOutOfDate() : void - { - $data = CommunityContent::getCommentPreviews(['flags' => CC_FLAG_OUTDATED]); - - $this->lvTabs[] = ['commentpreview', array( - 'data' => $data, - 'extraCols' => '$_' - ), 'commentAdminCol']; - } - - private function handleReports() : void - { - // todo: handle reports listing - // - } - - private function configAddRow($key, $value, $flags, $default, $comment) - { - $buff = ''; - $info = explode(' - ', $comment); - $key = $flags & Cfg::FLAG_PHP ? strtolower($key) : strtoupper($key); - - // name - if (!empty($info[0])) - $buff .= ''.sprintf(Util::$dfnString, $info[0], $key).''; - else - $buff .= ''.$key.''; - - // value - if ($flags & Cfg::FLAG_TYPE_BOOL) - $buff .= '
    '; - else if ($flags & Cfg::FLAG_OPT_LIST && !empty($info[1])) - { - $buff .= ''; - } - else if ($flags & Cfg::FLAG_BITMASK && !empty($info[1])) - { - $buff .= '
    '; - foreach (explode(', ', $info[1]) as $option) - { - [$idx, $name] = explode(':', $option); - $buff .= ''; - } - $buff .= '
    '; - } - else - $buff .= ''; - - // actions - $buff .= ''; - - $buff .= ''; - - if (isset($default)) - $buff .= '|'; - else - $buff .= '|'; - - if (!($flags & Cfg::FLAG_PERSISTENT)) - $buff .= '|'; - - $buff .= ''; - - return $buff; - } - - protected function generateTitle() {} - protected function generatePath() {} -} - -?> diff --git a/template/pages/admin/reports.tpl.php b/template/pages/admin/reports.tpl.php index 1731d07c..d35577e2 100644 --- a/template/pages/admin/reports.tpl.php +++ b/template/pages/admin/reports.tpl.php @@ -1,6 +1,8 @@ - +brick('header'); ?> + $this->brick('header'); +?> @@ -15,39 +17,15 @@ $this->brick('announcement'); $this->brick('pageTemplate'); ?>
    -

    name;?>

    +

    h1;?>

    brick('article'); + $this->brick('markup', ['markup' => $this->article]); - if (isset($this->extraText)): + $this->brick('markup', ['markup' => $this->extraText]); + + echo $this->extraHTML ?? ''; ?> -
    - - -
    -extraHTML)): - echo $this->extraHTML; - endif; -?> -

    Edit

    -
    -
    Icon
    -
    -
    -
    -
    Scale
    -
    -
    -
    diff --git a/template/pages/admin/weight-presets.tpl.php b/template/pages/admin/weight-presets.tpl.php index e383307d..68a25ea1 100644 --- a/template/pages/admin/weight-presets.tpl.php +++ b/template/pages/admin/weight-presets.tpl.php @@ -1,6 +1,8 @@ - +brick('header'); ?> + $this->brick('header'); +?> + $this->brick('markup', ['markup' => $this->extraText]); -
    -extraHTML)): - echo $this->extraHTML; - endif; + echo $this->extraHTML ?? ''; ?>

    Edit

    From 155bf1e4a37ac6e0f35d6ed16e633250f0a5efef Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:33:03 +0200 Subject: [PATCH 689/957] Template/Update (Part 46 - I) * account management rework: Base * create proper account settings page - modelviewer preferences - show ids in lists - announcement purge - public description * fix broken FKs between aowow_user_ratings and aowow_account --- endpoints/account/account.php | 168 +++++++ endpoints/account/signin.php | 4 +- .../account/update-community-settings.php | 48 ++ endpoints/account/update-general-settings.php | 60 +++ .../response/baseresponse.class.php | 4 +- includes/defines.php | 12 +- includes/user.class.php | 34 +- includes/utilities.php | 11 +- localization/locale_dede.php | 131 ++++- localization/locale_enus.php | 133 ++++- localization/locale_eses.php | 129 ++++- localization/locale_frfr.php | 131 ++++- localization/locale_ruru.php | 149 ++++-- localization/locale_zhcn.php | 133 ++++- pages/account.php | 465 ------------------ setup/updates/1758578400_11.sql | 3 + setup/updates/1758578400_12.sql | 11 + static/css/aowow.css | 11 +- static/js/account.js | 462 +++++++++++++++++ static/js/locale_dede.js | 1 + static/js/locale_enus.js | 1 + static/js/locale_eses.js | 1 + static/js/locale_frfr.js | 1 + static/js/locale_ruru.js | 1 + static/js/locale_zhcn.js | 1 + template/bricks/inputbox-form-signin.tpl.php | 2 +- template/pages/acc-dashboard.tpl.php | 141 ------ template/pages/account.tpl.php | 291 +++++++++++ 28 files changed, 1735 insertions(+), 804 deletions(-) create mode 100644 endpoints/account/account.php create mode 100644 endpoints/account/update-community-settings.php create mode 100644 endpoints/account/update-general-settings.php delete mode 100644 pages/account.php create mode 100644 setup/updates/1758578400_11.sql create mode 100644 setup/updates/1758578400_12.sql create mode 100644 static/js/account.js delete mode 100644 template/pages/acc-dashboard.tpl.php create mode 100644 template/pages/account.tpl.php diff --git a/endpoints/account/account.php b/endpoints/account/account.php new file mode 100644 index 00000000..cd57c720 --- /dev/null +++ b/endpoints/account/account.php @@ -0,0 +1,168 @@ +forwardToSignIn('account'); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('settings')); + + $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon` FROM ?_account WHERE `id` = ?d', User::$id); + + Lang::sort('game', 'ra'); + + parent::generate(); + + + /*************/ + /* Ban Popup */ + /*************/ + + $b = DB::Aowow()->select( + 'SELECT ab.`end` AS "0", ab.`reason` AS "1", a.`username` AS "2" + FROM ?_account_banned ab + LEFT JOIN ?_account a ON a.`id` = ab.`staffId` + WHERE ab.`userId` = ?d AND ab.`typeMask` & ?d AND (ab.`end` = 0 OR ab.`end` > UNIX_TIMESTAMP())', + User::$id, ACC_BAN_TEMP | ACC_BAN_PERM + ); + + $this->bans = $b ?: null; + + + /*******************/ + /* Status Messages */ + /*******************/ + + if (isset($_SESSION['msg'])) + { + [$var, $status, $msg] = $_SESSION['msg']; + if (property_exists($this, $var.'Message')) + $this->{$var.'Message'} = [$status, $msg]; + else + trigger_error('AccountBaseResponse::generate - unknown var in $_SESSION msg: '.$var, E_USER_WARNING); + + unset($_SESSION['msg']); + } + + + /*************/ + /* Form Data */ + /*************/ + + /* GENERAL */ + + // Modelviewer + if ($_ = DB::Aowow()->selectCell('SELECT `data` FROM ?_account_cookies WHERE `name` = ? AND `userId` = ?d', 'default_3dmodel', User::$id)) + [$this->modelrace, $this->modelgender] = explode(',', $_); + + // Lists + $this->idsInLists = $user['debug'] ? 1 : 0; + + /* PERSONAL */ + + // Email address + $this->curEmail = $user['email'] ?? ''; + + // Username + $this->curName = User::$username; + + // todo localize date format; store time + // $this->renameCD = date('F j, o', time() + 7 * DAY); + + /* COMMUNITY */ + + // Public Description + $this->description = ['body' => $user['description']]; + + // Forum Signature + // $this->signature = ['body' => $user['signature']]; + + // Avatar + $this->wowicon = $user['wowicon']; + $this->avMode = $user['avatar']; + + // status [reviewing, ok, rejected]? (only 2: rejected processed in js) + if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d AND `status` > 0', User::$id))) + { + array_walk($cuAvatars, function (&$x) { + $x['when'] *= 1000; // uploaded timestamp expected as msec for some reason + $x['caption'] = $x['name']; // only used for getVisibleText, duplicates name? + $x['type'] = 1; // always 1 ?, Dialog-popup doesn't work without it + }); + + foreach ($cuAvatars as $a) + if ($a['status'] != 2) + $this->customicons[$a['id']] = $a['name']; + + // TODO - replace with array_find in PHP 8.4 + if ($x = array_filter($cuAvatars, fn($x) => $x['current'] > 0 )) + $this->customicon = array_pop($x)['id']; + } + + /* PREMIUM */ + + $this->premium = User::isPremium(); + + if (!$this->premium) + return; + + // Avatar Manager + $this->avatarManager = new Listview([ + 'template' => 'avatar', + 'id' => 'avatar', + 'name' => '$LANG.tab_avatars', + 'parent' => 'avatar-manage', + 'hideNav' => 1 | 2, // top | bottom + 'data' => $cuAvatars ?? [] + ]); + + // Premium Border Selector + // ??? + } +} + +?> diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index b0fe5a6e..dce60520 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -62,7 +62,7 @@ class AccountSigninResponse extends TemplateResponse $this->forward($this->getNext(true)); $this->inputbox = ['inputbox-form-signin', array( - 'head' => Lang::account('doSignIn'), + 'head' => Lang::account('inputbox', 'head', 'signin'), 'action' => '?account=signin&next='.$this->getNext(), 'error' => $message, 'username' => $username, @@ -90,7 +90,7 @@ class AccountSigninResponse extends TemplateResponse // AUTH_BANNED => Lang::account('accBanned'); // ToDo: should this return an error? the actual account functionality should be blocked elsewhere AUTH_WRONGUSER => Lang::account('userNotFound'), AUTH_WRONGPASS => Lang::account('wrongPass'), - AUTH_IPBANNED => Lang::account('loginExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), + AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), AUTH_INTERNAL_ERR => Lang::main('intError'), default => Lang::main('intError') }; diff --git a/endpoints/account/update-community-settings.php b/endpoints/account/update-community-settings.php new file mode 100644 index 00000000..a09921f8 --- /dev/null +++ b/endpoints/account/update-community-settings.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($message = $this->updateSettings()) + $_SESSION['msg'] = ['community', $this->success, $message]; + } + + protected function updateSettings() + { + if (is_null($this->_post['desc'])) // assertPOST tests for empty string which is valid here + return Lang::main('genericError'); + + // description - 0 modified rows is still success + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `description` = ? WHERE `id` = ?d', $this->_post['desc'], User::$id))) + return Lang::main('genericError'); + + $this->success = true; + return Lang::account('updateMessage', 'community'); + } +} + +?> diff --git a/endpoints/account/update-general-settings.php b/endpoints/account/update-general-settings.php new file mode 100644 index 00000000..6e56ce3a --- /dev/null +++ b/endpoints/account/update-general-settings.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 11]], + 'modelgender' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 2] ], + 'idsInLists' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox'] ] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($message = $this->updateGeneral()) + $_SESSION['msg'] = ['general', $this->success, $message]; + } + + private function updateGeneral() : string + { + if (!$this->assertPOST('modelrace', 'modelgender')) + return Lang::main('genericError'); + + if ($this->_post['modelrace'] && !ChrRace::tryFrom($this->_post['modelrace'])) + return Lang::main('genericError'); + + // js handles this as cookie, so saved as cookie; Q - also save in ?_account table? + if (!DB::Aowow()->query('REPLACE INTO ?_account_cookies (`userId`, `name`, `data`) VALUES (?d, ?, ?)', User::$id, 'default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'])) + return Lang::main('genericError'); + + if (!setcookie('default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'], 0, '/')) + return Lang::main('intError'); + + // int > number of edited rows > no changes is still success + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `debug` = ?d WHERE `id` = ?d', $this->_post['idsInLists'] ? 1 : 0, User::$id))) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('updateMessage', 'general'); + } +} + +?> diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 3bc02c3b..986fa509 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -19,7 +19,7 @@ trait TrRecoveryHelper // check if already processing if ($_ = DB::Aowow()->selectCell('SELECT `statusTimer` - UNIX_TIMESTAMP() FROM ?_account WHERE `email` = ? AND `status` > ?d AND `statusTimer` > UNIX_TIMESTAMP()', $email, ACC_STATUS_NEW)) - return sprintf(Lang::account('isRecovering'), Util::formatTime($_ * 1000)); + return Lang::account('inputbox', 'error', 'isRecovering', [Util::formatTime($_ * 1000)]); // create new token and write to db $token = Util::createHash(); @@ -28,7 +28,7 @@ trait TrRecoveryHelper // send recovery mail if (!Util::sendMail($email, $mailTemplate, [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return sprintf(Lang::main('intError2'), 'send mail'); + return Lang::main('intError2', ['send mail']); return ''; } diff --git a/includes/defines.php b/includes/defines.php index 1c423f5e..29900e2e 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -62,10 +62,14 @@ define('DB_AUTH', 2); define('DB_CHARACTERS', 3); // Account Status -define('ACC_STATUS_OK', 0); // nothing special +define('ACC_STATUS_NONE', 0); // nothing special define('ACC_STATUS_NEW', 1); // just created, awaiting confirmation define('ACC_STATUS_RECOVER_USER', 2); // currently recovering username define('ACC_STATUS_RECOVER_PASS', 3); // currently recovering password +define('ACC_STATUS_CHANGE_EMAIL', 4); // currently changing contact email +define('ACC_STATUS_CHANGE_PASS', 5); // currently changing password +define('ACC_STATUS_CHANGE_USERNAME', 6); // currently changing username +define('ACC_STATUS_DELETED', 7); // is deleted - only a stub remains // Session Status define('SESSION_ACTIVE', 1); @@ -84,6 +88,12 @@ define('ACC_BAN_VIDEO', 0x0040); // cannot suggest vi define('ACC_BAN_GUIDE', 0x0080); // cannot write a guide define('ACC_BAN_FORUM', 0x0100); // cannot post on forums [not used here] +define('IP_BAN_TYPE_LOGIN_ATTEMPT', 0); +define('IP_BAN_TYPE_REGISTRATION_ATTEMPT', 1); +define('IP_BAN_TYPE_EMAIL_RECOVERY', 2); +define('IP_BAN_TYPE_PASSWORD_RECOVERY', 3); +define('IP_BAN_TYPE_USERNAME_RECOVERY', 4); + // Site Reputation/Privileges define('SITEREP_ACTION_REGISTER', 1); // Registered account define('SITEREP_ACTION_DAILYVISIT', 2); // Daily visit diff --git a/includes/user.class.php b/includes/user.class.php index 46d11f6d..5fca60fe 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -48,7 +48,7 @@ class User return false; // check IP bans - if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = 0', self::$ip)) + if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT)) { if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) return false; @@ -62,7 +62,7 @@ class User $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id()); $userData = DB::Aowow()->selectRow( - 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email` + 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` @@ -97,10 +97,10 @@ class User self::$preferedLoc = $loc; // reset expired account statuses - if ($userData['statusTimer'] < time() && $userData['status'] > ACC_STATUS_NEW) + if ($userData['statusTimer'] && $userData['statusTimer'] < time() && $userData['status'] != ACC_STATUS_NEW) { - DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = ?d', ACC_STATUS_OK, User::$id); - $userData['status'] = ACC_STATUS_OK; + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = ?d', ACC_STATUS_NONE, User::$id); + $userData['status'] = ACC_STATUS_NONE; } @@ -117,7 +117,7 @@ class User self::$dailyVotes = $userData['dailyVotes']; self::$excludeGroups = $userData['excludeGroups']; self::$status = $userData['status']; - // self::$debug = $userData['debug']; // TBD + self::$debug = $userData['debug']; self::$email = $userData['email']; if (Cfg::get('PROFILER_ENABLE')) @@ -251,9 +251,9 @@ class User return AUTH_INTERNAL_ERR; // handle login try limitation - $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); + $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); if (!$ipBan || !$ipBan['active']) // no entry exists or time expired; set count to 1 - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK')); + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK')); else // entry already exists; increment count DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip); @@ -279,7 +279,7 @@ class User return AUTH_WRONGPASS; // successfull auth; clear bans for this IP - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); + DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP)) return AUTH_BANNED; @@ -362,7 +362,7 @@ class User $name, $_SERVER["REMOTE_ADDR"] ?? '', self::$preferedLoc->value, - ACC_STATUS_OK, + ACC_STATUS_NONE, $userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE ); @@ -497,7 +497,7 @@ class User public static function isRecovering() : bool { - return self::$status == ACC_STATUS_RECOVER_USER || self::$status == ACC_STATUS_RECOVER_PASS; + return self::$status != ACC_STATUS_NONE && self::$status != ACC_STATUS_NEW; } @@ -565,21 +565,13 @@ class User $gUser['characters'] = self::getCharacters(); $gUser['excludegroups'] = self::$excludeGroups; - if (Cfg::get('DEBUG') && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_TESTER)) + if (self::$debug) $gUser['debug'] = true; // csv id-list output option on listviews if (self::getPremiumBorder()) $gUser['settings'] = ['premiumborder' => 1]; else - $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied - - if (self::isPremium()) - $gUser['premium'] = 1; - - if (self::getPremiumBorder()) - $gUser['settings'] = ['premiumborder' => 1]; - else - $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied + $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ? if (self::isPremium()) $gUser['premium'] = 1; diff --git a/includes/utilities.php b/includes/utilities.php index b435c06b..61084146 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1212,12 +1212,15 @@ abstract class Util $body = Util::defStatic($body); + if ($expiration) + { + $vars += array_fill(0, 9, null); // vsprintf requires all unused indizes to also be set... + $vars[9] = Util::formatTime($expiration * 1000); + } + if ($vars) $body = vsprintf($body, $vars); - if ($expiration) - $body .= "\n\n".Lang::account('tokenExpires', [Util::formatTime($expiration * 1000)])."\n"; - $subject = Cfg::get('NAME_SHORT').Lang::main('colon') . $subject; $header = 'From: ' . Cfg::get('CONTACT_EMAIL') . "\n" . 'Reply-To: ' . Cfg::get('CONTACT_EMAIL') . "\n" . @@ -1225,7 +1228,7 @@ abstract class Util if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) { - Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_DEV | U_GROUP_ADMIN, LOG_LEVEL_INFO); + Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_NONE, LOG_LEVEL_INFO); return true; } diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 5c2867c5..71e46780 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Anzahl an SQL-Queries", 'timeSQL' => "Zeit für SQL-Queries", 'noJScript' => 'Diese Seite macht ausgiebigen Gebrauch von JavaScript.
    Bitte aktiviert JavaScript in Eurem Browser.', - 'userProfiles' => "Deine Charaktere", + // 'userProfiles' => "Deine Charaktere", 'pageNotFound' => "Dies %s existiert nicht.", 'gender' => "Geschlecht", 'sex' => [null, "Mann", "Frau"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Seite: ", 'related' => "Weiterführende Informationen", 'contribute' => "Beitragen", - // 'replyingTo' => "Antwort zu einem Kommentar von", + // 'replyingTo' => "Antwort zu einem Kommentar von", 'submit' => "Absenden", + 'save' => 'Speichern', 'cancel' => "Abbrechen", 'rewards' => "Belohnungen", 'gains' => "Belohnungen", - 'login' => "Login", + // 'login' => "Login", 'forum' => "Forum", 'siteRep' => "Ruf: ", 'yourRepHistory'=> "Dein Ruf-Verlauf", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", + 'dateFmtUntil' => "j. F Y", 'timeAgo' => 'vor %s', 'nfSeparators' => ['.', ','], @@ -900,7 +902,6 @@ $lang = array( "Screenshot-Verwalter", "Video-Verwalter", "API-Partner", "Ausstehend" ), // signIn - 'doSignIn' => "Mit Eurem Konto anmelden", 'signIn' => "Anmelden", 'user' => "Benutzername", 'pass' => "Kennwort", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Benutzername", 'forgotPass' => "Kennwort", 'accCreate' => 'Noch kein Konto? Jetzt eins erstellen!', - 'resendMail' => "Bestätigungsmail erneut senden", - 'resendHint' => "Wenn Sie sich registriert haben, aber keine Bestätigungs-E-Mail erhalten haben, geben Sie Ihre E-Mail-Adresse unten ein und senden Sie das Formular ab. (Bitte überprüfen Sie Ihre Spam- oder Papierkorb-Ordner, um sicherzustellen, dass die E-Mail nicht versehentlich an der falschen Stelle abgelegt wurde!)", // recovery - 'recoverUser' => "Benutzernamenanfrage", - 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", - 'newPass' => "Neues Kennwort", - 'tokenExpires' => "Das Token wird in %s verfallen.", + 'newPass' => "Neues Kennwort:", + 'confNewPass' => "Neues Kennwort bestätigen:", + 'passResetHint' => 'Wenn ihr euer Kennwort nicht mehr wisst, könnt ihr es auf dieser Seite zurücksetzen.', + // 'tokenExpires' => "Das Token wird in %s verfallen.", // creation - 'register' => "Registrierung: Schritt %s von 2", - 'passConfirm' => "Kennwort bestätigen", + 'passConfirm' => "Kennwort bestätigen:", // dashboard 'ipAddress' => "IP-Adresse: ", 'lastIP' => "Letzte bekannte IP: ", - // 'myAccount' => "Mein Account", - // 'editAccount' => "Benutze die folgenden Formulare um deine Account-Informationen zu aktualisieren", - // 'viewPubDesc' => 'Die Beschreibung in deinem öffentlichen Profil ansehen', + // 'myAccount' => "Mein Account", + // 'editAccount' => "Benutze die folgenden Formulare um deine Account-Informationen zu aktualisieren", + // 'viewPubDesc' => 'Die Beschreibung in deinem öffentlichen Profil ansehen', // bans 'accBanned' => "Dieses Konto wurde geschlossen", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "Diese E-Mail-Adresse ist ungültig.", // message_emailnotvalid - 'emailNotFound' => "Die E-Mail-Adresse, die Ihr eingegeben habt, ist mit keinem Konto verbunden.

    Falls Ihr die E-Mail-Adresse vergessen habt, mit der Ihr Euer Konto erstellt habt, kontaktiert Ihr bitte CFG_CONTACT_EMAIL für Hilfestellung.", - 'createAccSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euer Konto zu erstellen.", - 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euren Benutzernamen zu erhalten.", - 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euer Kennwort zurückzusetzen.", - 'accActivated' => 'Euer Konto wurde soeben aktiviert.
    Ihr könnt euch nun anmelden', 'userNotFound' => "Ein Konto mit diesem Namen existiert nicht.", 'wrongPass' => "Dieses Kennwort ist ungültig.", - // 'accInactive' => "Dieses Konto wurde bisher nicht aktiviert.", - 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", - 'signupExceeded'=> "Die maximale Anzahl an Regustrierungen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + // 'accInactive' => "Dieses Konto wurde bisher nicht aktiviert.", 'errNameLength' => "Euer Benutzername muss mindestens 4 Zeichen lang sein.", // message_usernamemin 'errNameChars' => "Euer Benutzername kann nur aus Buchstaben und Zahlen bestehen.", // message_usernamenotvalid 'errPassLength' => "Euer Kennwort muss mindestens 6 Zeichen lang sein.", // message_passwordmin 'passMismatch' => "Die eingegebenen Kennworte stimmen nicht überein.", 'nameInUse' => "Es existiert bereits ein Konto mit diesem Namen.", 'mailInUse' => "Diese E-Mail-Adresse ist bereits mit einem Konto verbunden.", - 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", 'passCheckFail' => "Die Kennwörter stimmen nicht überein.", // message_passwordsdonotmatch - 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden." // message_newpassdifferent + 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent + 'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent + + // settings + 'settings' => "Kontoeinstellungen", + 'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.", + 'tabGeneral' => "Allgemein", + 'tabPersonal' => "Persönliches", + 'tabCommunity' => "Community", + 'tabPremium' => "Premium", + 'preferences' => "Voreinstellungen", + 'modelviewer' => "Modellviewer", + 'mvNote' => "Vorgegebenes Charaktermodell:", + 'lists' => "Listen", + 'listsNote' => "Zeigt IDs in unterstützten Listen", + 'announcements' => "Bekanntmachungen", + 'annNote' => "Entfernt die Daten von Bekanntmachungen, die du geschlossen hast, damit sie wieder sichtbar werden.", + 'purge' => "Löschen", + 'curPass' => "Derzeitiges Kennwort:", + 'globalLogout' => "Von allen Browsern/Geräten abmelden", + 'curEmail' => "Momentane E-Mail-Adresse:", + 'newEmail' => "Neue E-Mail-Adresse:", + 'userPage' => "Benutzerseite", + 'publicDesc' => "Öffentliche Beschreibung", + 'publicDescNote'=> 'Erzähl uns etwas über dich und deine WoW-Charaktere. Alles, was du hier eingibst, erscheint auf deiner Benutzerseite.', + 'forums' => "Foren", + 'signature' => "Signatur", + 'signatureNote' => "Deine Signatur erscheint unter all deinen Forenbeiträgen.", + 'usernameNote' => "Nutzernamen können nur einmal alle %s geändert werden und müssen 4-16 Zeichen lang sein. Sonderzeichen sind nicht erlaubt.", + 'curName' => "Aktueller Nutzername:", + 'newName' => "Neuer Nutzername:", + 'accDelete' => "Konto löschen", + 'accDeleteNote' => 'Wenn du dein Konto und alle persönlichen Daten vollständig löschen möchtest, dann geh zu unserer Kontolöschung.', + 'avatar' => "Avatar", + 'avatarNote' => "Dein Avatar wird neben all deinen Forenbeiträgen angezeigt.", + 'avWowIcon' => "World of Warcraft-Icon", + 'avWowIconNote' => 'z.B. INV_Axe_54
    Tipp: Um den Namen eines Symbols herauszufinden, doppelklickt einfach auf das große Symbol, während ihr auf einer Gegenstands- oder Zauberseite seid. Kopiert den Text anschließend und fügt ihn oben ein.', + 'avIconName' => "Symbolname:", + 'none' => "Keins", + 'preview' => "Vorschau", + 'custom' => "Benutzerdefiniert", + 'premiumStatus' => "Premium Status", + 'status' => "Status", + 'active' => "Activ", + 'inactive' => "Inaktiv", + 'activeCD' => "Ihr müsst bis zum %s warten um euren Nutzernamen erneut zu ändern.", + 'updateMessage' => array( + 'general' => "Deine Einstellungen wurden aktualisiert.", + 'community' => "Eure öffentliche Beschreibung und Forensignatur wurden erfolgreich aktualisiert.", + 'personal' => "Eine Bestätigungsnachricht wurde an %s versandt.", + 'username' => 'Nutzername von %1$s zu %2$s geändert.', + 'avNotFound' => "Symbol nicht gefunden.", + 'avSuccess' => "Euer Avatar wurde erfolgreich aktualisiert.", + 'avNoChange' => "Es wurden keine Änderungen durchgeführt.", + 'av1stUser' => "Glückwunsch! Ihr habt eine einzigartige Auswahl getroffen! /jubeln", + 'avNthUser' => "Zur Eurer Information, Euer Symbol wird bereits von %d anderen Benutzer(n) benutzt." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Erfolg", + 'error' => "Hoppla!", + 'register' => "Registrierung: Schritt %s von 2", + 'recoverUser' => "Benutzernamenanfrage", + 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", + 'resendMail' => "Bestätigungsmail erneut senden", + 'signin' => "Mit Eurem Konto anmelden" + ), + 'message' => array( + 'accActivated' => 'Euer Konto wurde soeben aktiviert.
    Ihr könnt euch nun anmelden', + 'resendMail' => "Wenn Sie sich registriert haben, aber keine Bestätigungs-E-Mail erhalten haben, geben Sie Ihre E-Mail-Adresse unten ein und senden Sie das Formular ab. (Bitte überprüfen Sie Ihre Spam- oder Papierkorb-Ordner, um sicherzustellen, dass die E-Mail nicht versehentlich an der falschen Stelle abgelegt wurde!)", + 'mailChangeOk' => "Ihre E-Mail-Adresse wurde erfolgreich geändert.", + 'mailRevertOk' => "Ihre Anfrage zur Änderung der E-Mail-Adresse wurde storniert/zurückgesetzt.", + 'passChangeOk' => "Ihr Kennwort wurde erfolgreich geändert.", + 'deleteAccSent' => "Eine E-Mail mit einem Bestätigungslink wurde an %s gesendet.", + 'deleteOk' => "Ihr Konto wurde erfolgreich entfernt. Wir hoffen, Sie bald wiederzusehen!

    Sie können dieses Fenster jetzt schließen.", + 'createAccSent' => 'Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um Euer Konto zu erstellen.

    Falls du keine Bestätigungsnachricht erhalten hast klicke hier um eine neue zu senden.
    ', + 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euren Benutzernamen zu erhalten.", + 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euer Kennwort zurückzusetzen.", + ), + 'error' => array( + 'mailTokenUsed' => 'Dieser Schlüssel zur Änderung der E-Mail-Adresse wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'passTokenUsed' => 'Dieser Schlüssel zur Änderung des Kennworts wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'passTokenLost' => "Kein Token wurde bereitgestellt. Wenn Sie in einer E-Mail einen Link zum Zurücksetzen des Kennworts erhalten haben, kopieren Sie die gesamte URL (einschließlich des Tokens am Ende) in die Adressleiste Ihres Browsers.", + 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", + 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + 'signupExceeded' => "Die maximale Anzahl an Registrierungen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + // 'emailNotFound' => "Die E-Mail-Adresse, die Ihr eingegeben habt, ist mit keinem Konto verbunden.

    Falls Ihr die E-Mail-Adresse vergessen habt, mit der Ihr Euer Konto erstellt habt, kontaktiert Ihr bitte CFG_CONTACT_EMAIL für Hilfestellung.", + 'emailNotFound' => "Diese E-Mail-Adresse wurde in unserem System nicht gefunden.", + ) + ) ), 'user' => array( 'notFound' => "Der Benutzer \"%s\" wurde nicht gefunden!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "%d. Stockwerk" ), 'privileges' => array( - 'main' => "Auf unserer Seite könnt Ihr Ruf erringen. Hauptsächlich erringt man Ruf dadurch, dass Eure Kommentare positiv bewertet werden.

    Das heißt, Euer Ruf hängt in gewissem Maße davon ab, wie sehr Ihr der Community beiträgt.

    Mit dem Sammeln von Ruf verdient Ihr Euch auch das Vertrauen der Gemeinschaft ein, und Ihr erhält Privilegien. Unten könnt Ihr eine vollständige Liste einsehen.", + 'main' => "Auf unserer Seite könnt Ihr Ruf erringen. Hauptsächlich erringt man Ruf dadurch, dass Eure Kommentare positiv bewertet werden.

    Das heißt, Euer Ruf hängt in gewissem Maße davon ab, wie sehr Ihr der Community beiträgt.

    Mit dem Sammeln von Ruf verdient Ihr Euch auch das Vertrauen der Gemeinschaft ein, und Ihr erhält Privilegien. Unten könnt Ihr eine vollständige Liste einsehen.", 'privilege' => "Privileg", 'privileges' => "Privilegien", 'requiredRep' => "Benötigter Ruf", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index edc82692..812c3606 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Number of SQL queries", 'timeSQL' => "Time of SQL queries", 'noJScript' => 'This site makes extensive use of JavaScript.
    Please enable JavaScript in your browser.', - 'userProfiles' => "My Profiles", + // 'userProfiles' => "My Profiles", 'pageNotFound' => "This %s doesn't exist.", 'gender' => "Gender", 'sex' => [null, "Male", "Female"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Side: ", 'related' => "Related", 'contribute' => "Contribute", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "Submit", + 'save' => 'Save', 'cancel' => "Cancel", 'rewards' => "Rewards", 'gains' => "Gains", - 'login' => "Login", + // 'login' => "Login", 'forum' => "Forum", 'siteRep' => "Reputation: ", 'yourRepHistory'=> "Your Reputation History", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", + 'dateFmtUntil' => "F j, Y", 'timeAgo' => "%s ago", 'nfSeparators' => [',', '.'], @@ -900,7 +902,6 @@ $lang = array( "Screenshot manager", "Video manager", "API partner", "Pending" ), // signIn - 'doSignIn' => "Log in to your Account", 'signIn' => "Log In", 'user' => "Username", 'pass' => "Password", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Username", 'forgotPass' => "Password", 'accCreate' => 'Don\'t have an account? Create one now!', - 'resendMail' => "Re-Send Verification Email", - 'resendHint' => "If you registered but did not receive a verification email, enter your email address below and submit the form. (Please be sure to check your spam or trash folders to make sure the email didn't accidentally get put in the wrong place!)", // recovery - 'recoverUser' => "Username Request", - 'recoverPass' => "Password Reset: Step %s of 2", - 'newPass' => "New Password", - 'tokenExpires' => "This token expires in %s.", + 'newPass' => "New Password:", + 'confNewPass' => "Confirm new password:", + 'passResetHint' => 'If you don\'t know your password, visit the password reset page to reset it.', + // 'tokenExpires' => "This token expires in %s.", // previously appended to all emails, now it's part of the mail template // creation - 'register' => "Registration - Step %s of 2", - 'passConfirm' => "Confirm password", + 'passConfirm' => "Confirm password:", // dashboard 'ipAddress' => "IP address: ", 'lastIP' => "last used IP: ", - // 'myAccount' => "My Account", - // 'editAccount' => "Simply use the forms below to update your account information", - // 'viewPubDesc' => 'View your Public Description in your Profile Page', + // 'myAccount' => "My Account", + // 'editAccount' => "Simply use the forms below to update your account information.", + // 'viewPubDesc' => 'View your Public Description in your Profile Page', // bans 'accBanned' => "This account was closed", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "That email address is not valid.", // message_emailnotvalid - 'emailNotFound' => "The email address you entered is not associated with any account.

    If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", - 'createAccSent' => "An email was sent to %s. Simply follow the instructions to create your account.", - 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", - 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password.", - 'accActivated' => 'Your account has been activated.
    Proceed to sign in', 'userNotFound' => "The username you entered does not exists.", 'wrongPass' => "That password is not vaild.", - // 'accInactive' => "That account has not yet been confirmed active.", - 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", - 'signupExceeded'=> "The maximum number of signups from this IP has been exceeded. Please try again in %s.", + // 'accInactive' => "That account has not yet been confirmed active.", 'errNameLength' => "Your username must be at least 4 characters long.", // message_usernamemin 'errNameChars' => "Your username can only contain letters and numbers.", // message_usernamenotvalid 'errPassLength' => "Your password must be at least 6 characters long.", // message_passwordmin 'passMismatch' => "The passwords you entered do not match.", - 'nameInUse' => "That username is already taken.", + 'nameInUse' => "This username is already in use.", 'mailInUse' => "That email is already registered to an account.", - 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Passwords do not match.", // message_passwordsdonotmatch - 'newPassDiff' => "Your new password must be different than your previous one." // message_newpassdifferent + 'newPassDiff' => "Your new password must be different than your previous one.", // message_newpassdifferent + 'newMailDiff' => "Your new email address must be different than your previous one.", // message_newemaildifferent + + // settings + 'settings' => "Account Settings", + 'settingsNote' => "Simply use the forms below to update your account information.", + 'tabGeneral' => "General", + 'tabPersonal' => "Personal", + 'tabCommunity' => "Community", + 'tabPremium' => "Premium", + 'preferences' => "Preferences", + 'modelviewer' => "Model Viewer", + 'mvNote' => "Default character model:", + 'lists' => "Lists", + 'listsNote' => "Show IDs in supported lists", + 'announcements' => "Announcements", + 'annNote' => "Removes data related to announcements you have closed so that they may be viewed again.", + 'purge' => "Purge", + 'curPass' => "Current password:", + 'globalLogout' => "Log me out of all other browsers/devices", + 'curEmail' => "Current email address:", + 'newEmail' => "New email address:", + 'userPage' => "User Page", + 'publicDesc' => "Public Description", + 'publicDescNote'=> 'Tell us more about yourself and your WoW characters. Whatever you type here will appear on your user page.', + 'forums' => "Forums", + 'signature' => "Signature", + 'signatureNote' => "Your signature will appear beneath all of your posts in the forums.", + 'usernameNote' => "Usernames can only be changed once every %s and must be between 4-16 characters. No special characters are permitted.", + 'curName' => "Current Username:", + 'newName' => "New Username:", + 'accDelete' => "Delete Account", + 'accDeleteNote' => "If you'd like to completely delete your account and all its personal information, visit our account deletion page.", + 'avatar' => "Avatar", + 'avatarNote' => "Your avatar will appear next to all of your posts in the forums.", + 'avWowIcon' => "Icon from World of Warcraft", + 'avWowIconNote' => 'e.g. INV_Axe_54
    Tip: To find the name of an icon, simply double-click the big icon while
    browsing an item or spell page. Then copy and paste it above.', + 'avIconName' => "Icon name:", + 'none' => "None", + 'preview' => "Preview", + 'custom' => "Custom", + 'premiumStatus' => "Premium Status", + 'status' => "Status", + 'active' => "Active", + 'inactive' => "Inactive", + 'activeCD' => "You must wait until %s to change your username again.", + 'updateMessage' => array( + 'general' => "Updated your preferences.", + 'community' => "Your public description and forum signature have been updated successfully.", + 'personal' => "A confirmation email was sent to %s.", + 'username' => 'Username changed from %1$s to %2$s.', + 'avNotFound' => "Icon not found.", + 'avSuccess' => "Your avatar has been updated successfully.", + 'avNoChange' => "No changes were made.", + 'av1stUser' => "Congratulations for picking one that is unique! /cheer", + 'avNthUser' => "FYI, your icon is also used by %d other user(s)." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Success", + 'error' => "Oops!", + 'register' => "Registration - Step %s of 2", + 'recoverUser' => "Username Request", + 'recoverPass' => "Password Reset: Step %s of 2", + 'resendMail' => "Re-Send Verification Email", + 'signin' => "Log in to your Account" + ), + 'message' => array( + 'accActivated' => 'Your account has been activated.
    Proceed to sign in', + 'resendMail' => "If you registered but did not receive a verification email, enter your email address below and submit the form. (Please be sure to check your spam or trash folders to make sure the email didn't accidentally get put in the wrong place!)", + 'mailChangeOk' => "Your email address has been changed successfully.", + 'mailRevertOk' => "Your email change request has been cancelled/reverted.", + 'passChangeOk' => "Your password has been changed successfully.", + 'deleteAccSent' => "An email has been sent to %s with confirmation link attached.", + 'deleteOk' => "Your account has been successfully removed. We hope to see you again soon!

    You may now close this window.", + 'createAccSent' => 'An email was sent to %s. Simply follow the instructions to create your account.

    If you don\'t receive the verification email, click here to send another one.
    ', + 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", + 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password." + ), + 'error' => array( + 'mailTokenUsed' => 'Either that email change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', + 'passTokenUsed' => 'Either that password change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', + 'passTokenLost' => "No token was provided. If you received a reset password link in an email, please copy and paste the entire URL (including the token at the end) into your browser's location bar.", + 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", + 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", + 'signupExceeded' => "The maximum number of signups from this IP has been exceeded. Please try again in %s.", + // 'emailNotFound' => "The email address you entered is not associated with any account.

    If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", + 'emailNotFound' => "That email address wasn't found in our system." + ) + ) ), 'user' => array( 'notFound' => "User \"%s\" not found!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "Level %d" ), 'privileges' => array( - 'main' => "Here on our Site you can generate reputation. The main way to generate it is to get your comments upvotes.

    So, reputation is a rough measure of how much you contributed to the community.

    As you amass reputation you earn the community's trust and you will be granted with additional privileges. You can find a full list below.", + 'main' => "Here on our Site you can generate reputation. The main way to generate it is to get your comments upvotes.

    So, reputation is a rough measure of how much you contributed to the community.

    As you amass reputation you earn the community's trust and you will be granted with additional privileges. You can find a full list below.", 'privilege' => "Privilege", 'privileges' => "Privileges", 'requiredRep' => "Reputation Required", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 979a603b..385861b1 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Número de consultas de SQL", 'timeSQL' => "El tiempo para las consultas de SQL", 'noJScript' => 'Este sitio hace uso intenso de JavaScript.
    Por favor habilita JavaScript en tu navegador.', - 'userProfiles' => "Tus personajes", + // 'userProfiles' => "Tus personajes", 'pageNotFound' => "Este %s no existe.", 'gender' => "Género", 'sex' => [null, "Hombre", "Mujer"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Lado: ", 'related' => "Información relacionada", 'contribute' => "Contribuir", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "Enviar", + 'save' => 'Guardar', 'cancel' => "Cancelar", 'rewards' => "Recompensas", 'gains' => "Ganancias", - 'login' => "Ingresar", + // 'login' => "Ingresar", 'forum' => "Foro", 'siteRep' => "Reputación: ", 'yourRepHistory'=> "Tu Historial de Reputación", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d/m/Y", 'dateFmtLong' => "d/m/Y \a \l\a\s g:i A", + 'dateFmtUntil' => "j \d\\e F \d\\e Y", 'timeAgo' => 'hace %s', 'nfSeparators' => ['.', ','], @@ -900,7 +902,6 @@ $lang = array( "Gestor de Capturas de pantalla","Gestor de vídeos", "Partner de API", "Pendiente" ), // signIn - 'doSignIn' => "Iniciar sesión con tu cuenta", 'signIn' => "Iniciar sesión", 'user' => "Nombre de usuario", 'pass' => "Contraseña", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Nombre de usuario", 'forgotPass' => "Contraseña", 'accCreate ' => '¿No tienes una cuenta? ¡Crea una ahora!', - 'resendMail' => "Reenviar correo de verificación", - 'resendHint' => "Si te has registrado pero no recibiste un correo de verificación, introduce tu dirección de correo más abajo y completa el formulario. (¡Por favor, asegúrate de comprobar tus directorios de correo no deseado o papelera por si el correo acabara en el lugar equivocado!)", // recovery - 'recoverUser' => "Pedir nombre de usuario", - 'recoverPass' => "Reiniciar contraseña: Paso %s de 2", - 'newPass' => "Nueva Contraseña", - 'tokenExpires' => "Este token expira en %s", + 'newPass' => "Nueva Contraseña:", + 'confNewPass' => "Confirmar contraseña nueva:", + 'passResetHint' => 'Si no sabes tu contraseña, visita la página de restablecimiento de contraseña para restablecerla.', + // 'tokenExpires' => "Este token expira en %s", // creation - 'register' => "Inscripción: Paso %s de 2", - 'passConfirm' => "Confirmar contraseña", + 'passConfirm' => "Confirmar contraseña:", // dashboard 'ipAddress' => "Dirección IP: ", 'lastIP' => "Última IP usada: ", - // 'myAccount' => "Mi cuenta", - // 'editAccount' => "Use el formulario siguienta para actualizar la información de la cuenta.", - // 'viewPubDesc' => 'Mira tu descripción pública en tu Página de perfil', + // 'myAccount' => "Mi cuenta", + // 'editAccount' => "Use el formulario siguienta para actualizar la información de la cuenta.", + // 'viewPubDesc' => 'Mira tu descripción pública en tu Página de perfil', // bans 'accBanned' => "Esta cuenta fue cerrada.", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "Esa dirección de correo electrónico no es válida.", // message_emailnotvalid - 'emailNotFound' => "El correo electrónico que ingresaste no está asociado con ninguna cuenta.

    Si olvistaste el correo electronico con el que registraste la cuenta, escribe a CFG_CONTACT_EMAIL para asistencia.", - 'createAccSent' => "Un correo fue enviado a %s. Siga las instrucciones para crear su cuenta.", - 'recovUserSent' => "Un correo fue enviado a %s. Siga las instrucciones para recuperar su nombre de usuario.", - 'recovPassSent' => "Un correo fue enviado a %s. Siga las instrucciones para reiniciar su contraseña.", - 'accActivated' => 'Su cuenta ha sido activada.
    Ingrese a para ingresar', 'userNotFound' => "El usuario que ha ingresado no existe", 'wrongPass' => "La contraseña no es valida.", - // 'accInactive' => "That account has not yet been confirmed active.", - 'loginExceeded' => "Ha excedido la cantidad de inicios de sesion con esta IP. Por favor intente en %s", - 'signupExceeded'=> "Ha excedido la cantidad de creaciones de cuentas con esta IP. Por favor intente en %s.", + // 'accInactive' => "That account has not yet been confirmed active.", 'errNameLength' => "Tu nombre de usuario tiene que tener por lo menos cuatro caracteres.", // message_usernamemin 'errNameChars' => "Tu nombre de usuario solo puede contener números y letras.", // message_usernamenotvalid 'errPassLength' => "Tu contraseña tiene que tener por lo menos seis caracteres.", // message_passwordmin 'passMismatch' => "La contraseña que ingresó no concuerdan.", 'nameInUse' => "El nombre de usuario ya se encuentra utilzado", 'mailInUse' => "El correo electrónico ya se encuentra registrado a una cuenta", - 'isRecovering' => "Esta cuenta ya se encuentra en proceso de recuperación. Siga las intrucciones en su correo o espere %s para que el token expire ", 'passCheckFail' => "Las contraseñas no son iguales.", // message_passwordsdonotmatch - 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior." // message_newpassdifferent + 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior.",// message_newpassdifferent + 'newMailDiff' => "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", // message_newemaildifferent + + // settings + 'settings' => "Mi cuenta", + 'settingsNote' => "Simplemente usa el siguiente formulario para actualizar la información de tu cuenta.", + 'tabGeneral' => "General", + 'tabPersonal' => "Personal", + 'tabCommunity' => "Comunidad", + 'tabPremium' => "Premium", + 'preferences' => "Preferencias", + 'modelviewer' => "Visualizador de modelos", + 'mvNote' => "Modelo de personaje por defecto:", + 'lists' => "Listas", + 'listsNote' => "Mostrar IDs en listas soportadas", + 'announcements' => "Anuncios", + 'annNote' => "Elimina datos relacionados con anuncios que hayas cerrado para que puedan ser vistos de nuevo.", + 'purge' => "Purgar", + 'curPass' => "Contraseña actual:", + 'globalLogout' => "Cerrar sesión en todos los otros navegadores/dispositivos", + 'curEmail' => "Dirección de correo electrónico actual", + 'newEmail' => "Dirección de correo electrónico nueva", + 'userPage' => "Página de usuario", + 'publicDesc' => "Descripción pública", + 'publicDescNote'=> 'Dinos más sobre ti y tus personajes de WoW. Lo que escribas aquí aparecerá en tu página de usuario.', + 'forums' => "Foros", + 'signature' => "Firma", + 'signatureNote' => "Tu firma aparecerá debajo de todos tus mensajes en los foros.", + 'usernameNote' => "Los nombres de usuario solo pueden cambiarse una vez cada %s y deben tener entre 4 y 16 caracteres. No se permiten caracteres especiales.", + 'curName' => "Nombre de Usuario Actual:", + 'newName' => "Nuevo Nombre de Usuario:", + 'accDelete' => "Eliminar Cuenta", + 'accDeleteNote' => 'Si quieres eliminar completamente tu cuenta y toda tu información personal, visita nuestra página de eliminación de cuenta.', + 'avatar' => "Avatar", + 'avatarNote' => "Tu avatar aparecerá al lado de todos tus mensajes en los foros.", + 'avWowIcon' => "Ícono de World of Warcraft", + 'avWowIconNote' => 'ej. INV_Axe_54
    Sugerencia: Para encontrar el nombre de un icono, simplemente haz doble-clic en el icono grande mientras estás viendo una página de un objeto o un hechizo. Después cópialo arriba.', + 'avIconName' => "Nombre de ícono:", + 'none' => "Ninguno", + 'preview' => "Visualizar", + 'custom' => "Personalizado", + 'premiumStatus' => "Suscripción Premium", + 'status' => "Estado", + 'active' => "Activo", + 'inactive' => "Inactivo", + 'activeCD' => "Debes esperar hasta %s para cambiar tu nombre de usuario nuevamente.", + 'updateMessage' => array( + 'general' => "Preferencias actualizadas.", + 'community' => "Tu descripción pública y tu firma en el foro se han actualizado correctamente.", + 'personal' => "Se envió un correo electrónico de confirmación a %s.", + 'username' => 'Nombre de usuario cambiado de %1$s a %2$s.', + 'avNotFound' => "No se encontró este avatar.", + 'avSuccess' => "Tu avatar ha sido actualizado correctamente.", + 'avNoChange' => "No se hicieron cambios.", + 'av1stUser' => "¡Felicidades, tienes un avatar único! /hurra", + 'avNthUser' => "Para tu información, tu avatar también está siendo usado por %d otros usuarios." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Éxito", + 'error' => "¡Ups!", + 'register' => "Inscripción: Paso %s de 2", + 'recoverUser' => "Solicitar nombre de usuario", + 'recoverPass' => "Restablecer contraseña: Paso %s de 2", + 'resendMail' => "Reenviar correo de verificación", + 'signin' => "Iniciar sesión con tu cuenta" + ), + 'message' => array( + 'accActivated' => 'Su cuenta ha sido activada.
    Ingrese a para ingresar', + 'resendMail' => "Si te has registrado pero no recibiste un correo de verificación, introduce tu dirección de correo más abajo y completa el formulario. (¡Por favor, asegúrate de comprobar tus directorios de correo no deseado o papelera por si el correo acabara en el lugar equivocado!)", + 'mailChangeOk' => "Tu dirección de correo electrónico ha sido cambiada correctamente.", + 'mailRevertOk' => "Tu solicitud de cambio de correo electrónico ha sido cancelada/revertida.", + 'passChangeOk' => "Tu contraseña ha sido cambiada correctamente.", + 'deleteAccSent' => "Se ha enviado un correo electrónico a %s con el enlace de confirmación adjunto.", + 'deleteOk' => "Tu cuenta ha sido eliminada correctamente. ¡Esperamos verte de nuevo pronto!

    Ahora puedes cerrar esta ventana.", + 'createAccSent' => 'Un correo fue enviado a %s. Sigue las instrucciones para crear tu cuenta.

    Si no recibes el correo de verificación, haz clic aquí para enviar otro.', + 'recovUserSent' => "Un correo fue enviado a %s. Sigue las instrucciones para recuperar tu nombre de usuario.", + 'recovPassSent' => "Un correo fue enviado a %s. Sigue las instrucciones para restablecer tu contraseña." + ), + 'error' => array( + 'mailTokenUsed' => 'Ese código de cambio de correo electrónico ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', + 'passTokenUsed' => 'Ese código de cambio de contraseña ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', + 'passTokenLost' => "No se recibió ningún código de petición. Si recibiste un enlace para restablecer tu contraseña por correo, por favor copia y pega la dirección completa (incluyendo el código del final) en la barra de dirección de tu navegador.", + 'isRecovering' => "Esta cuenta ya se encuentra en proceso de recuperación. Sigue las instrucciones en tu correo o espera %s para que el token expire.", + 'loginExceeded' => "Has excedido la cantidad de inicios de sesión con esta IP. Por favor intenta en %s.", + 'signupExceeded' => "Has excedido la cantidad de creaciones de cuentas con esta IP. Por favor intenta en %s.", + // 'emailNotFound' => "El correo electrónico que ingresaste no está asociado con ninguna cuenta.

    Si olvistaste el correo electronico con el que registraste la cuenta, escribe a CFG_CONTACT_EMAIL para asistencia.", + 'emailNotFound' => "Esa dirección de correo electrónico no fue encontrada en nuestro sistema." + ) + ) ), 'user' => array( 'notFound' => "¡No se encontró el usuario \"%s\"!", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 9f9acf20..1974541e 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Nombre de requêtes SQL", 'timeSQL' => "Temps d'exécution des requêtes SQL", 'noJScript' => "Ce site requiert JavaScript pour fonctionner.
    Veuillez activer JavaScript dans votre navigateur.", - 'userProfiles' => "Vos personnages", // translate.google :x + // 'userProfiles' => "Vos personnages", // translate.google :x 'pageNotFound' => "Ce %s n'existe pas.", 'gender' => "Genre", 'sex' => [null, "Homme", "Femme"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Coté : ", 'related' => "Informations connexes", 'contribute' => "Contribuer", - // 'replyingTo' => "En réponse au commentaire de", + // 'replyingTo' => "En réponse au commentaire de", 'submit' => "Soumettre", + 'save' => 'Sauver', 'cancel' => "Annuler", 'rewards' => "Récompenses", 'gains' => "Gains", - 'login' => "[Login]", + // 'login' => "[Login]", 'forum' => "Forum", 'siteRep' => "Réputation : ", 'yourRepHistory'=> "Votre historique de réputation", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ' : ', 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d à g:i A", + 'dateFmtUntil' => "j F Y", 'timeAgo' => 'il y a %s', 'nfSeparators' => [' ', ','], @@ -900,7 +902,6 @@ $lang = array( "Gestionnaire de capture d'écran","Gestionnaire de vidéos", "Partenaire API", "En attente" ), // signIn - 'doSignIn' => "Connexion à votre compte", 'signIn' => "Connexion", 'user' => "Nom d'utilisateur", 'pass' => "Mot de passe", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Nom d'utilisateur", 'forgotPass' => "Mot de passe", 'accCreate' => 'Vous n\'avez pas encore de compte ? Créez-en un maintenant !', - 'resendMail' => "Renvoyer le courriel de vérification", - 'resendHint' => "Si vous vous êtes enregistré mais n'avez pas reçu de courriel de vérification, entrez votre adresse électronique ci-dessous et validez le formulaire. (Assurez-vous de vérifier vos dossiers de courrier indésirable et votre corbeille pour vous assurer que le courriel ne s'y soit pas perdu !)", // recovery - 'recoverUser' => "Demande de nom d'utilisateur", - 'recoverPass' => "Changement de mot de passe : Étape %s de 2", - 'newPass' => "Nouveau mot de passe", - 'tokenExpires' => "This token expires in %s.", + 'newPass' => "Nouveau mot de passe :", + 'confNewPass' => "Confirm new password:", + 'passResetHint' => 'Si vous ne connaissez pas votre mot de passe, rendez-vous sur la page de réinitialisation du mot de passe pour le réinitialiser.', + // 'tokenExpires' => "This token expires in %s.", // creation - 'register' => "Enregistrement : Étape %s de 2", - 'passConfirm' => "Confirmez", + 'passConfirm' => "Confirmez :", // dashboard 'ipAddress' => "Addresse IP : ", 'lastIP' => "Dernière IP utilisée : ", - // 'myAccount' => "Mon compte", - // 'editAccount' => "Utilisez les formulaires ci-dessous pour mettre à jour vos informations.", - // 'viewPubDesc' => 'Voyez vos informations publiques dans votre Profile Page', + // 'myAccount' => "Mon compte", + // 'editAccount' => "Utilisez les formulaires ci-dessous pour mettre à jour vos informations.", + // 'viewPubDesc' => 'Voyez vos informations publiques dans votre Profile Page', // bans 'accBanned' => "Ce compte a été fermé.", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "Cette adresse courriel est invalide.", // message_emailnotvalid - 'emailNotFound' => "L'address email que vous avez entrée n'est pas associée à un compte.

    Si vous avez oublié l'address email avec laquelle vous avez enregistré votre compteCFG_CONTACT_EMAIL pour obtenir de l'aide.", - 'createAccSent' => "Un email a été envoyé à %s. Suivez les instructions pour créer votre compte.", - 'recovUserSent' => "Un email a été envoyé à %s. Suivez les instructions pour récupérer votre nom d'utilisateur.", - 'recovPassSent' => "Un email a été envoyé à %s. Suivez les instructions pour réinitialiser votre mot de passe.", - 'accActivated' => 'Votre compte a été activé.
    Vous pouvez maintenant vous connecter', 'userNotFound' => "Le nom d'utilisateur que vous avez saisi n'éxiste pas.", 'wrongPass' => "Ce mot de passe est invalide.", - // 'accInactive' => "Ce compte n'a pas encore été activé.", - 'loginExceeded' => "Le nombre maximum de connections depuis cette IP a été dépassé. Essayez de nouevau dans %s.", - 'signupExceeded'=> "Le nombre maximum d'inscriptions depuis cette IP a été dépassé. Essayez de nouveau dans %s.", + // 'accInactive' => "Ce compte n'a pas encore été activé.", 'errNameLength' => "Votre nom d'utilisateur doit faire au moins 4 caractères de long.", // message_usernamemin 'errNameChars' => "Votre nom d'utilisateur doit contenir seulement des lettres et des chiffres.", // message_usernamenotvalid 'errPassLength' => "Votre mot de passe doit faire au moins 6 caractères de long.", // message_passwordmin 'passMismatch' => "Les mots de passe que vous avez saisis ne correspondent pas.", 'nameInUse' => "Ce nom d'utilisateur est déjà utilisé.", 'mailInUse' => "Cette addresse email est déjà liée à un compte.", - 'isRecovering' => "Ce compte est déjà en train d'être récupéré. Suivez les instruction dans l'email reçu ou attendez %s pour que le token expire.", 'passCheckFail' => "Les mots de passe ne correspondent pas.", // message_passwordsdonotmatch - 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien." // message_newpassdifferent + 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien.", // message_newpassdifferent + 'newMailDiff' => "Votre nouvelle adresse courriel doit être différente de l'ancienne.", // message_newemaildifferent + + // settings + 'settings' => "Mon compte", + 'settingsNote' => "Veuillez utiliser les formulaires ci-dessous pour apporter des changements.", + 'tabGeneral' => "Général", + 'tabPersonal' => "Personnel", + 'tabCommunity' => "Communauté", + 'tabPremium' => "Premium", + 'preferences' => "Préférences", + 'modelviewer' => "Visionneuse 3D", + 'mvNote' => "Modèle de personnage par défaut :", + 'lists' => "Listes", + 'listsNote' => "Afficher les IDs dans les listes supportées", + 'announcements' => "Annonces", + 'annNote' => "Supprimer les données relatives aux annonces que vous avez fermées pour qu'elles puissent être vues à nouveau.", + 'purge' => "Effacer", + 'curPass' => "Mot de passe actuel :", + 'globalLogout' => "Me déconnecter de tous les autres navigateurs/appareils", + 'curEmail' => "Adresse courriel actuelle :", + 'newEmail' => "Nouvelle adresse e-mail :", + 'userPage' => "Page d'utilisateur", + 'publicDesc' => "Description publique", + 'publicDescNote'=> 'Dites-nous en un peu plus sur vous et vos persos de WoW. Tout ce que vous écrivez ici apparaîtra dans votre page d\'utilisateur.', + 'forums' => "Forum", + 'signature' => "Signature", + 'signatureNote' => "Votre signature apparaîtra en dessous de chacun de vos messages dans le forum.", + 'usernameNote' => "Les noms d'utilisateur ne peuvent être changés qu'une fois tous les %s et doivent comporter entre 4 et 16 caractères. Aucun caractère spécial n'est autorisé.", + 'curName' => "Nom d'utilisateur actuel :", + 'newName' => "Nouveau nom d'utilisateur :", + 'accDelete' => "Supprimer le compte", + 'accDeleteNote' => 'Si vous voulez complètement supprimer votre compte et toutes ses informations personnelles, visitez notre page de suppression de compte.', + 'avatar' => "Avatar", + 'avatarNote' => "Votre avatar apparaîtra à côté de chacun de vos messages dans le forum.", + 'avWowIcon' => "Icône de World of Warcraft ", + 'avWowIconNote' => 'ex. INV_Axe_54
    Astuce : Pour trouver le nom d\'une icône, vous n\'avez qu\'à double-cliquer sur la grosse icône lorsque vous naviguez sur une page d\'objet ou de sort. Ensuite copiez-collez le nom ci-dessous.', + 'avIconName' => "Nom de l'icône :", + 'none' => "Aucun", + 'preview' => "Aperçu", + 'custom' => "Personnalisé", + 'premiumStatus' => "Souscription Premium", + 'status' => "Statut", + 'active' => "Actives", + 'inactive' => "Inactives", + 'activeCD' => "Vous devez attendre jusqu'à %s pour changer à nouveau votre nom d'utilisateur.", + 'updateMessage' => array( + 'general' => "Vos préférences ont été mises à jour.", + 'community' => "Votre description publique et votre signature de forum ont été actualisées correctement.", + 'personal' => "Un courriel de confirmation a été envoyé à %s.", + 'username' => 'Nom d\'utilisateur changé de %1$s à %2$s.', + 'avNotFound' => "Icône non trouvée.", + 'avSuccess' => "Votre avatar a été mis à jour avec succès.", + 'avNoChange' => "Aucun changement à été fait.", + 'av1stUser' => "Félicitations pour en avoir choisir un qui est unique !", + 'avNthUser' => "Au passage, votre icône est également utilisée par %d autre(s) utilisateur(s)." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Succès", + 'error' => "Oups.", + 'register' => "Enregistrement : Étape %s de 2", + 'recoverUser' => "Demande de nom d'utilisateur", + 'recoverPass' => "Changement de mot de passe : Étape %s de 2", + 'resendMail' => "Renvoyer le courriel de vérification", + 'signin' => "Connexion à votre compte" + ), + 'message' => array( + 'accActivated' => 'Votre compte a été activé.
    Vous pouvez maintenant vous connecter', + 'resendMail' => "Si vous vous êtes enregistré mais n'avez pas reçu de courriel de vérification, entrez votre adresse électronique ci-dessous et validez le formulaire. (Assurez-vous de vérifier vos dossiers de courrier indésirable et votre corbeille pour vous assurer que le courriel ne s'y soit pas perdu !)", + 'mailChangeOk' => "Votre adresse courriel a été changée avec succès.", + 'mailRevertOk' => "Votre demande de changement d'adresse courriel a été annulée/révoquée.", + 'passChangeOk' => "Votre mot de passe a été changé avec succès.", + 'deleteAccSent' => "Un courriel a été envoyé à %s avec le lien de confirmation.", + 'deleteOk' => "Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt !

    Vous pouvez maintenant fermer cette fenêtre.", + 'createAccSent' => 'Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu\'il contient pour créer votre compte.

    Si vous ne recevez pas l\'email de vérification, cliquez ici pour en envoyer un autre.', + 'recovUserSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour récupérer votre nom d'utilisateur.", + 'recovPassSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour réinitialiser votre mot de passe." + ), + 'error' => array( + 'mailTokenUsed' => "Cette clé de changement d'adresse courriel a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", + 'passTokenUsed' => "Cette clé de changement de mot de passe a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", + 'passTokenLost' => "Aucun jeton n'a été fourni. Si vous avez reçu un lien de réinitialisation du mot de passe dans un courriel, merci de copier et coller l'URL entière (y compris le jeton à la fin) dans la barre d'adresse de votre navigateur.", + 'isRecovering' => "Ce compte est déjà en train d'être récupéré. Suivez les instruction dans l'email reçu ou attendez %s pour que le token expire.", + 'loginExceeded' => "Le nombre maximum de connections depuis cette IP a été dépassé. Essayez de nouevau dans %s.", + 'signupExceeded' => "Le nombre maximum d'inscriptions depuis cette IP a été dépassé. Essayez de nouveau dans %s.", + // 'emailNotFound' => "L'address email que vous avez entrée n'est pas associée à un compte.

    Si vous avez oublié l'address email avec laquelle vous avez enregistré votre compteCFG_CONTACT_EMAIL pour obtenir de l'aide.", + 'emailNotFound' => "Cette adresse électronique n'a pas été trouvée dans notre système." + ) + ) ), 'user' => array( 'notFound' => "Utilisateur \"%s\" non trouvé!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "Plancher %d" ), 'privileges' => array( - 'main' => "Sur AoWoW, vous pouvez accumuler de la réputation. Le principal moyen d'en accumuler est d'avoir un score élevé pour vos commentaires.

    Ainsi, la réputation est une vision sommaire de vos contributions à la communauté.

    En amassant de la réputation, vous gagnez le respect de la communauté et vous obtiendrez certains privilèges. Vous pouvez en trouver la liste complète ci-dessous.", + 'main' => "Sur AoWoW, vous pouvez accumuler de la réputation. Le principal moyen d'en accumuler est d'avoir un score élevé pour vos commentaires.

    Ainsi, la réputation est une vision sommaire de vos contributions à la communauté.

    En amassant de la réputation, vous gagnez le respect de la communauté et vous obtiendrez certains privilèges. Vous pouvez en trouver la liste complète ci-dessous.", 'privilege' => "Privilège", 'privileges' => "Privilèges", 'requiredRep' => "Réputation Requise", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index f6d74b02..64a925d9 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Количество SQL запросов", 'timeSQL' => "Время выполнения SQL запросов", 'noJScript' => 'Данный сайт активно использует технологию JavaScript.
    Пожалуйста, Включите JavaScript в вашем браузере.', - 'userProfiles' => "Ваши персонажи", // translate.google :x + // 'userProfiles' => "Ваши персонажи", // translate.google :x 'pageNotFound' => "Такое %s не существует.", 'gender' => "Пол", 'sex' => [null, "Мужчина", "Женщина"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Сторона: ", 'related' => "Дополнительная информация", 'contribute' => "Добавить", - // 'replyingTo' => "Ответ на комментарий от", + // 'replyingTo' => "Ответ на комментарий от", 'submit' => "Отправить", + 'save' => 'Сохранить', 'cancel' => "Отмена", 'rewards' => "Награды", 'gains' => "Бонус", - 'login' => "[Login]", + // 'login' => "[Login]", 'forum' => "Форум", 'siteRep' => "Репутация: ", 'yourRepHistory'=> "История вашей репутации", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ": ", 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d в g:i A", + 'dateFmtUntil' => "j F Y г.", 'timeAgo' => '%s назад', 'nfSeparators' => [' ', ','], @@ -900,7 +902,6 @@ $lang = array( "Менеджер изображений", "Менеджер видео", "API партнер", "Ожидающее" ), // signIn - 'doSignIn' => "Войти в вашу учетную запись", 'signIn' => "Вход", 'user' => "Логин", 'pass' => "Пароль", @@ -909,55 +910,133 @@ $lang = array( 'forgotUser' => "Имя пользователя", 'forgotPass' => "Пароль", 'accCreate' => 'У вас еще нет учетной записи? Зарегистрируйтесь прямо сейчас!', - 'resendMail' => "Вновь выслать верификационное письмо", - 'resendHint' => "Если вы зарегистрировались, но не получили проверочного письма, пожалуйста, введите ваш email адрес ниже и подтвердите отправку формы. (Пожалуйста, удостоверьтесь, что Вы проверили папку со спамом и/или корзину Вашего почтового сервиса)", // recovery - 'recoverUser' => "Запрос имени пользователя", - 'recoverPass' => "Сброс пароля: Шаг %s из 2", - 'newPass' => "New Password", - 'tokenExpires' => "This token expires in %s.", + 'newPass' => "Новый пароль:", + 'confNewPass' => "Подтвердите новый пароль:", + 'passResetHint' => 'Если вы не знаете пароль от своей учетной записи, пожалуйста, посетите страницу сброса пароля.', + // 'tokenExpires' => "This token expires in %s.", // creation - 'register' => "Регистрация: Шаг %s из 2", - 'passConfirm' => "Повторите пароль", + 'passConfirm' => "Повторите пароль:", // dashboard - 'ipAddress' => "[IP-Adress]: ", - 'lastIP' => "[last used IP]: ", - // 'myAccount' => "[My Account]", - // 'editAccount' => "[Simply use the forms below to update your account information]", - // 'viewPubDesc' => '[View your Public Description in your Profile Page]', + 'ipAddress' => "IP-Adress: ", + 'lastIP' => "last used IP: ", + // 'myAccount' => "My Account", + // 'editAccount' => "Используйте нижеприведённую форму, чтобы обновить информацию о вашей учетной записи.", + // 'viewPubDesc' => 'View your Public Description in your Profile Page', // bans - 'accBanned' => "[This Account was closed]", - 'bannedBy' => "[Banned by]: ", - 'reason' => "[Reason]: ", - 'ends' => "[Ends on]: ", - 'permanent' => "[The ban is permanent]", - 'noReason' => "[No reason was given.]", + 'accBanned' => "This Account was closed", + 'bannedBy' => "Banned by: ", + 'reason' => "Reason: ", + 'ends' => "Ends on: ", + 'permanent' => "The ban is permanent", + 'noReason' => "No reason was given.", // form-text 'emailInvalid' => "Недопустимый адрес email.", // message_emailnotvalid - 'emailNotFound' => "The email address you entered is not associated with any account.

    If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", - 'createAccSent' => "An email was sent to %s. Simply follow the instructions to create your account.", - 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", - 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password.", - 'accActivated' => 'Your account has been activated.
    Proceed to sign in', 'userNotFound' => "The username you entered does not exists.", 'wrongPass' => "That password is not vaild.", - // 'accInactive' => "That account has not yet been confirmed active.", - 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", - 'signupExceeded'=> "The maximum number of signups from this IP has been exceeded. Please try again in %s.", + // 'accInactive' => "That account has not yet been confirmed active.", 'errNameLength' => "Имя пользователя не должно быть короче 4 символов.", // message_usernamemin 'errNameChars' => "Имя пользователя может содержать только буквы и цифры.", // message_usernamenotvalid 'errPassLength' => "Ваш пароль должен состоять минимум из 6 знаков.", // message_passwordmin 'passMismatch' => "The passwords you entered do not match.", - 'nameInUse' => "That username is already taken.", + 'nameInUse' => "That username is already in use.", 'mailInUse' => "That email is already registered to an account.", - 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Пароли не совпадают.", // message_passwordsdonotmatch - 'newPassDiff' => "Прежний и новый пароли не должны совпадать." // message_newpassdifferent + 'newPassDiff' => "Прежний и новый пароли не должны совпадать.", // message_newpassdifferent + 'newMailDiff' => "Прежний и новый e-mail адреса не должны совпадать.", // message_newemaildifferent + + // settings + 'settings' => "Параметры учетной записи", + 'settingsNote' => "Используйте нижеприведённую форму, чтобы обновить информацию о вашей учетной записи.", + 'tabGeneral' => "Общее", + 'tabPersonal' => "Персональное", + 'tabCommunity' => "Сообщество", + 'tabPremium' => "Premium", + 'preferences' => "Предпочтения", + 'modelviewer' => "3D-просмотр", + 'mvNote' => "Модель персонажа по умолчанию:", + 'lists' => "Списки", + 'listsNote' => "Показывать ID в поддерживаемых списках", + 'announcements' => "Объявления", + 'annNote' => "Удаляет данные о закрытых объявлениях, после чего вы сможете их увидеть снова.", + 'purge' => "Сбросить", + 'curPass' => "Текущий пароль:", + 'globalLogout' => "Выйти на всех устройствах и/или браузерах ", + 'curEmail' => "Текущий адрес email:", + 'newEmail' => "Новый адрес email:", + 'userPage' => "Профиль пользователя", + 'publicDesc' => "Описание", + 'publicDescNote'=> 'Расскажите нам о себе и ваших персонажах из World of Warcraft. Все, что вы напишите, будет отображаться на страница пользователя.', + 'forums' => "Форум", + 'signature' => "Подпись", + 'signatureNote' => "Этой подписью будут сопровождаться все сообщения, опубликованные вами на форумах сайта.", + 'usernameNote' => "Имя пользователя должно включать не менее 4 и не более 16 символов, и может быть изменено один раз в течение %s. Специальные символы не допускаются.", + 'curName' => "Текущее имя пользователя:", + 'newName' => "Новое имя пользователя:", + 'accDelete' => "Удалить учетную запись", + 'accDeleteNote' => 'Если вы хотите удалить свою учетную запись и все, связанные с ней персональные данные, перейдите на страницу удаления учетной записи.', + 'avatar' => "Аватар", + 'avatarNote' => "Аватар будет сопровождать все сообщения, опубликованные вами на форумах.", + 'avWowIcon' => "Значок из World of Warcraft", + 'avWowIconNote' => 'например, INV_Axe_54
    Совет: Чтобы найти название значка, дважды щелкните большом значке, когда вы смотрите страницу с описанием предмета или заклинания. Затем вставьте эту строку в документ.', + 'avIconName' => "Название иконки:", + 'none' => "Нет", + 'preview' => "Предварительный просмотр", + 'custom' => "Свой", + 'premiumStatus' => "Premium подписка", + 'status' => "Статус", + 'active' => "Активно", + 'inactive' => "Неактивно", + 'activeCD' => "Вы должны подождать до %s, чтобы снова изменить имя пользователя.", + 'updateMessage' => array( + 'general' => "Предпочтения обновлены.", + 'community' => "Описание и подпись успешно обновлены.", + 'personal' => "Письмо с подтверждением было отправлено на %s.", + 'username' => 'Имя пользователя изменено с %1$s на %2$s.', + 'avNotFound' => "Иконка не найдена.", + 'avSuccess' => "Аватар успешно обновлен. Поздравляем Вас!", + 'avNoChange' => "Не произошло никаких изменений.", + 'av1stUser' => "Аватар, выбранный Вами, уникален! /ура", + 'avNthUser' => "Примите во внимание, что такой значок уже используется %d пользователями." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Успешно", + 'error' => "Упс!", + 'register' => "Регистрация: Шаг %s из 2", + 'recoverUser' => "Запрос имени пользователя", + 'recoverPass' => "Сброс пароля: Шаг %s из 2", + 'resendMail' => "Вновь выслать верификационное письмо", + 'signin' => "Войти в вашу учетную запись" + ), + 'message' => array( + 'accActivated' => 'Ваша учетная запись была активирована.
    Перейдите к входу', + 'resendMail' => "Если вы зарегистрировались, но не получили проверочного письма, пожалуйста, введите ваш email адрес ниже и подтвердите отправку формы. (Пожалуйста, удостоверьтесь, что Вы проверили папку со спамом и/или корзину Вашего почтового сервиса)", + 'mailChangeOk' => "Ваш адрес электронной почты был успешно изменен.", + 'mailRevertOk' => "Запрос на изменение адреса электронной почты был отменен/отозван.", + 'passChangeOk' => "Ваш пароль был успешно изменен.", + 'deleteAccSent' => "Письмо с подтверждением было отправлено на %s.", + 'deleteOk' => "Ваша учетная запись была успешно удалена. Надеемся увидеть вас снова!

    Теперь вы можете закрыть это окно.", + 'createAccSent' => 'Письмо с инструкциями для активации учетной записи было отправлено на адрес %s/b>. Следуйте инструкциям, для продолжения регистрации.

    Если вы не получили письмо для подтверждения, нажмите здесь, чтобы отправить его повторно.', + 'recovUserSent' => "Письмо с инструкциями для активации учетной записи было отправлено на адрес %s/b>. Просто следуйте инструкциям для восстановления имени пользователя.", + 'recovPassSent' => "Письмо с инструкциями для активации учетной записи было отправлено на адрес %s/b>. Просто следуйте инструкциям для сброса пароля." + ), + 'error' => array( + 'mailTokenUsed' => 'Этот ключ для смены email уже был использован или недействителен. Посетите вашу страницу настроек учетной записи, чтобы попробовать снова.', + 'passTokenUsed' => 'Этот ключ для смены пароля уже был использован или недействителен. Посетите вашу страницу настроек учетной записи, чтобы попробовать снова.', + 'passTokenLost' => "Ключ не был получен. Если вы сбросили пароль по ссылке из письма, отправленного на email, пожалуйста, скопируйте URL целиком и вставьте в адресную строку (включая ключ, указанный в конце ссылки).", + 'isRecovering' => "Эта учетная запись уже восстанавливается. Следуйте инструкциям в письме или дождитесь истечения срока действия токена через %s.", + 'loginExceeded' => "Достигнуто максимальное количество попыток входа с этого IP. Пожалуйста, попробуйте снова через %s.", + 'signupExceeded' => "Достигнуто максимальное количество регистраций с этого IP. Пожалуйста, попробуйте снова через %s.", + // 'emailNotFound' => "The email address you entered is not associated with any account.

    If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", + 'emailNotFound' => "Этот адрес электронной почты не найден в нашей системе." + ) + ) ), 'user' => array( 'notFound' => "Пользователь \"%s\" не найден!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "Уровень %d" ), 'privileges' => array( - 'main' => "Здесь на AoWoW вы можете зарабатывать репутацию. Основной источник получения репутации — увеличение рейтинга ваших комментариев другими пользователями.

    Репутация примерно измеряет количество вашего вклада в сообщество.

    По мере того, как вы зарабатываете репутацию, вы получаете доверие сообщества и особые привилегии. Полный список привилегий расположен ниже.", + 'main' => "Здесь на AoWoW вы можете зарабатывать репутацию. Основной источник получения репутации — увеличение рейтинга ваших комментариев другими пользователями.

    Репутация примерно измеряет количество вашего вклада в сообщество.

    По мере того, как вы зарабатываете репутацию, вы получаете доверие сообщества и особые привилегии. Полный список привилегий расположен ниже.", 'privilege' => "Привилегия", 'privileges' => "Привилегии", 'requiredRep' => "Необходима репутация", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 0bfd5a87..4d710049 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "数据库查询次数", 'timeSQL' => "数据库查询时间", 'noJScript' => '本站点基于JavaScript。
    请在你的浏览器里启用JavaScript。', - 'userProfiles' => "我的简介", + // 'userProfiles' => "我的简介", 'pageNotFound' => "%s不存在。", 'gender' => "性别", 'sex' => [null, "男性", "女性"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "阵营:", 'related' => "相关", 'contribute' => "贡献", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "提交", + 'save' => '保存', 'cancel' => "取消", 'rewards' => "奖励", 'gains' => "获得", - 'login' => "登录", + // 'login' => "登录", 'forum' => "论坛", 'siteRep' => "站点声望:", 'yourRepHistory'=> "您的声望历史", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ':', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", + 'dateFmtUntil' => "Y年n月j日", 'timeAgo' => '%s之前', 'nfSeparators' => [',', '.'], @@ -892,7 +894,7 @@ $lang = array( ), 'account' => array( 'title' => "数据库账号", - 'email' => "电子邮箱地址", + 'email' => "邮箱地址", 'continue' => "继续", 'groups' => array( -1 => "无", "测试员", "管理员", "编辑器", "管理员", "官僚", @@ -900,7 +902,6 @@ $lang = array( "截屏管理器", "视频管理员", "API伙伴", "等待中" ), // signIn - 'doSignIn' => "登录你的数据库账号", 'signIn' => "登录", 'user' => "用户名", 'pass' => "密码", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "用户名", 'forgotPass' => "密码", 'accCreate' => '没有账号?现在创建一个!', - 'resendMail' => "重新发送验证邮件", - 'resendHint' => "[If you registered but did not receive a verification email, enter your email address below and submit the form. (Please be sure to check your spam or trash folders to make sure the email didn't accidentally get put in the wrong place!)]", // recovery - 'recoverUser' => "用户名需求", - 'recoverPass' => "密码重置:步骤 %s / 2", - 'newPass' => "新密码", - 'tokenExpires' => "此令牌将在%s过期。", + 'newPass' => "新密码:", + 'confNewPass' => "确认密码:", + 'passResetHint' => '如果您忘记了当前密码,请访问 密码重置页面 进行重置。', + // 'tokenExpires' => "此令牌将在%s过期。", // creation - 'register' => "注册 - 步骤 %s / 2", - 'passConfirm' => "确认密码", + 'passConfirm' => "确认密码:", // dashboard 'ipAddress' => "IP地址:", 'lastIP' => "上次使用IP地址:", - // 'myAccount' => "我的账号", - // 'editAccount' => "只需使用以下表格就能更新你的帐户信息", - // 'viewPubDesc' => '在你的简介页面查看你公共描述', + // 'myAccount' => "我的账号", + // 'editAccount' => "只需使用以下表格就能更新你的帐户信息", + // 'viewPubDesc' => '在你的简介页面查看你公共描述', // bans 'accBanned' => "这个账号已被关闭", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "该电子邮件地址无效。", // message_emailnotvalid - 'emailNotFound' => "你输入的电子邮件地址与任何帐户不关联。

    如果您忘记了使用哪个电子邮件注册了您的帐户,请发送电子邮件至CFG_CONTACT_EMAIL寻求帮助。", - 'createAccSent' => "电子邮件发送到%s。只需按照说明创建你的帐户。", - 'recovUserSent' => "电子邮件发送到%s。只需按照说明恢复你的用户名。", - 'recovPassSent' => "电子邮件发送到%s。只需按照说明重置你的密码。", - 'accActivated' => '你的帐户已被激活。
    继续登录', 'userNotFound' => "输入的用户名不存在。", 'wrongPass' => "密码无效。", - // 'accInactive' => "该帐户尚未确认激活。", - 'loginExceeded' => "这个IP最大登录次数已超过。请在%s后再次尝试。", - 'signupExceeded'=> "这个IP最大注册次数已超过。请在%s后再次尝试。", + // 'accInactive' => "该帐户尚未确认激活。", 'errNameLength' => "你的用户名必须至少4个字符长度。", // message_usernamemin 'errNameChars' => "你的用户名只能包含字母和数字。", // message_usernamenotvalid 'errPassLength' => "你的密码必须至少6个字符长度。", // message_passwordmin 'passMismatch' => "你输入的密码不匹配。", 'nameInUse' => "用户名已被占用。", 'mailInUse' => "该电子邮件已注册到一个帐户。", - 'isRecovering' => "此帐户已恢复。按照电子邮件中的说明或等待%s后令牌过期。", 'passCheckFail' => "密码不匹配。", // message_passwordsdonotmatch - 'newPassDiff' => "你的新密码必须与以前的密码不同。" // message_newpassdifferent + 'newPassDiff' => "你的新密码必须与以前的密码不同。", // message_newpassdifferent + 'newMailDiff' => "您的新邮箱地址必须不同于旧地址。", // message_newemaildifferent + + // settings + 'settings' => "账号设置", + 'settingsNote' => "使用下列表格就能升级您的账号信息。", + 'tabGeneral' => "常规", + 'tabPersonal' => "个人", + 'tabCommunity' => "社区", + 'tabPremium' => "高级会员", + 'preferences' => "偏好", + 'modelviewer' => "模型查看器", + 'mvNote' => "默认角色模型:", + 'lists' => "清单", + 'listsNote' => "在支持的清单中显示ID", + 'announcements' => "公告", + 'annNote' => "清空您已关闭的公告数据,以便日后再次浏览。", + 'purge' => "清除", + 'curPass' => "当前密码:", + 'globalLogout' => "从所有其他浏览器/设备中登出当前账户", + 'curEmail' => "当前邮箱地址:", + 'newEmail' => "新邮箱地址:", + 'userPage' => "用户页", + 'publicDesc' => "公开描述", + 'publicDescNote'=> '跟我们说说您自己和您的 WoW 角色吧。您输入的信息会显示在您的 用户页 上。', + 'forums' => "论坛", + 'signature' => "签名", + 'signatureNote' => "签名显示在论坛发帖的下方。", + 'usernameNote' => "用户名每%s只能更改一次,长度需为4-16个字符,不允许特殊字符。", + 'curName' => "当前用户名:", + 'newName' => "新用户名:", + 'accDelete' => "删除账户", + 'accDeleteNote' => '如果您想彻底删除您的账户以及所有个人信息,请访问我们的 账户删除页面。', + 'avatar' => "人物", + 'avatarNote' => "您的头像将显示在您所有论坛帖子的旁边。", + 'avWowIcon' => "魔兽世界图标", + 'avWowIconNote' => '如INV_Axe_54
    小建议:要找到图标的名字,只要在浏览图标spell 页面时 双击大图标,接着复制粘贴到上面。', + 'avIconName' => "图标名:", + 'none' => "无", + 'preview' => "预览", + 'custom' => "自定义", + 'premiumStatus' => "高级会员订阅", + 'status' => "状态", + 'active' => "激活", + 'inactive' => "未激活", + 'activeCD' => "您必须等到%s后才能再次更改用户名。", + 'updateMessage' => array( + 'general' => "已更新您的偏好设置。", + 'community' => "已成功更新您的公开描述与论坛签名。", + 'personal' => "确认邮件已发送到 %s。", + 'username' => '用户名已从 %1$s 更改为 %2$s。', + 'avNotFound' => "图标未找到", + 'avSuccess' => "您的头像更新成功。", + 'avNoChange' => "没有做过改变​", + 'av1stUser' => "恭喜选到了最独特的那一个! /干杯", + 'avNthUser' => "​提示,您的图标也被%d其他用户使用。" + ), + 'inputbox' => array( + 'head' => array( + 'success' => "成功", + 'error' => "哦嚯!", + 'register' => "注册 - 步骤 %s / 2", + 'recoverUser' => "用户名需求", + 'recoverPass' => "密码重置:步骤 %s / 2", + 'resendMail' => "重新发送验证邮件", + 'signin' => "登录你的数据库账号" + ), + 'message' => array( + 'accActivated' => '你的帐户已被激活。
    继续登录', + 'resendMail' => "如果您已注册但未收到验证邮件,请在下方输入您的邮箱地址并提交表单。(请务必检查您的垃圾邮件或回收站文件夹,以确保邮件没有被误放到错误的位置!)", + 'mailChangeOk' => "您的邮箱地址已成功更改。", + 'mailRevertOk' => "您的邮箱更改请求已被取消/撤销。", + 'passChangeOk' => "您的密码已成功更改。", + 'deleteAccSent' => "已向 %s 发送了一封带有确认链接的邮件。", + 'deleteOk' => "您的账户已成功删除。希望不久后能再次见到您!

    您现在可以关闭此窗口。", + 'createAccSent' => '电子邮件发送到%s。只请按照说明创建您的账户。

    如果您没有收到验证邮件,点击这里重新发送。', + 'recovUserSent' => "电子邮件发送到%s。只请按照说明恢复您的用户名。", + 'recovPassSent' => "电子邮件发送到%s。只请按照说明重置您的密码。" + ), + 'error' => array( + 'mailTokenUsed' => '该邮箱更改密钥已被使用,或不是有效密钥。请访问您的账户设置页面重新尝试。', + 'passTokenUsed' => '该密码更改密钥已被使用,或不是有效密钥。请访问您的账户设置页面重新尝试。', + 'passTokenLost' => "未提供令牌。如果您在邮件中收到重置密码链接,请将整个网址(包括最后的令牌)复制并粘贴到浏览器地址栏中。", + 'isRecovering' => "此帐户已恢复。按照电子邮件中的说明或等待%s后令牌过期。", + 'loginExceeded' => "这个IP最大登录次数已超过。请在%s后再次尝试。", + 'signupExceeded' => "这个IP最大注册次数已超过。请在%s后再次尝试。", + // 'emailNotFound' => "你输入的电子邮件地址与任何帐户不关联。

    如果您忘记了使用哪个电子邮件注册了您的帐户,请发送电子邮件至CFG_CONTACT_EMAIL寻求帮助。", + 'emailNotFound' => "未在我们的系统中找到该电子邮件地址。" + ) + ) ), 'user' => array( 'notFound' => "用户 \"%s\" 未找到", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "[Level %d]" ), 'privileges' => array( - 'main' => "在我们的网站上,你可以通过 声望. 来获取特权。获取声望的主要途径是获得评论的赞同。

    因此,声望是衡量你对社区的贡献程度的一个大致指标。

    随着声望的积累,你将获得社区的信任,并被赋予额外的特权。以下是完整的特权列表。", + 'main' => "在我们的网站上,你可以通过 声望. 来获取特权。获取声望的主要途径是获得评论的赞同。

    因此,声望是衡量你对社区的贡献程度的一个大致指标。

    随着声望的积累,你将获得社区的信任,并被赋予额外的特权。以下是完整的特权列表。", 'privilege' => "特权", 'privileges' => "特权", 'requiredRep' => "需要声望", diff --git a/pages/account.php b/pages/account.php deleted file mode 100644 index 2b39a866..00000000 --- a/pages/account.php +++ /dev/null @@ -1,465 +0,0 @@ - [false], - 'forgotpassword' => [false], - 'forgotusername' => [false] - ); - - protected $user = ''; - protected $error = ''; - protected $next = ''; - - protected $lvTabs = []; - protected $banned = []; - - protected $_get = array( - 'token' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'next' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - ); - - protected $_post = array( - 'username' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'token' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AccountPage::rememberCallback'], - 'email' => ['filter' => FILTER_SANITIZE_EMAIL] - ); - - public function __construct($pageCall, $pageParam) - { - if ($pageParam) - $this->category = [$pageParam]; - - parent::__construct($pageCall, $pageParam); - - if ($pageParam) - { - // requires auth && not authed - if ($this->validCats[$pageParam][0] && !User::isLoggedIn()) - $this->forwardToSignIn('account='.$pageParam); - // doesn't require auth && authed - else if (!$this->validCats[$pageParam][0] && User::isLoggedIn()) - header('Location: ?account', true, 302); // goto dashboard - } - } - - protected static function rememberCallback($val) - { - return $val == 'yes' ? $val : null; - } - - protected function generateContent() - { - if (!$this->category) - { - $this->createDashboard(); - return; - } - - switch ($this->category[0]) - { - case 'forgotpassword': - if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) - { - if (Cfg::get('ACC_EXT_RECOVER_URL')) - header('Location: '.Cfg::get('ACC_EXT_RECOVER_URL'), true, 302); - else - $this->error(); - } - - $this->tpl = 'acc-recover'; - $this->resetPass = false; - - if ($this->createRecoverPass($nStep)) // location-header after final step - header('Location: ?account=signin', true, 302); - - $this->head = sprintf(Lang::account('recoverPass'), $nStep); - break; - case 'forgotusername': - if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) - { - if (Cfg::get('ACC_EXT_RECOVER_URL')) - header('Location: '.Cfg::get('ACC_EXT_RECOVER_URL'), true, 302); - else - $this->error(); - } - - $this->tpl = 'acc-recover'; - $this->resetPass = false; - - if ($this->_post['email']) - { - if (!Util::isValidEmail($this->_post['email'])) - $this->error = Lang::account('emailInvalid'); - else if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE email = ?', $this->_post['email'])) - $this->error = Lang::account('emailNotFound'); - else if ($err = $this->doRecoverUser()) - $this->error = $err; - else - $this->text = sprintf(Lang::account('recovUserSent'). $this->_post['email']); - } - - $this->head = Lang::account('recoverUser'); - break; - case 'signup': - if (!Cfg::get('ACC_ALLOW_REGISTER')) - $this->error(); - - if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) - { - if (Cfg::get('ACC_EXT_CREATE_URL')) - header('Location: '.Cfg::get('ACC_EXT_CREATE_URL'), true, 302); - else - $this->error(); - } - - $this->tpl = 'acc-signUp'; - $nStep = 1; - if ($this->_post['username'] || $this->_post['password'] || $this->_post['c_password'] || $this->_post['email']) - { - if ($err = $this->doSignUp()) - $this->error = $err; - else - { - $nStep = 1.5; - $this->text = sprintf(Lang::account('createAccSent'), $this->_post['email']); - } - } - else if ($this->_get['token'] && ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE status = ?d AND token = ?', ACC_STATUS_NEW, $this->_get['token']))) - { - $nStep = 2; - DB::Aowow()->query('UPDATE ?_account SET status = ?d, statusTimer = 0, token = 0, userGroups = ?d WHERE token = ?', ACC_STATUS_OK, U_GROUP_NONE, $this->_get['token']); - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, ?d + 1, UNIX_TIMESTAMP() + ?d)', User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); - - $this->text = sprintf(Lang::account('accActivated'), $this->_get['token']); - } - else - $this->next = $this->getNext(); - - $this->head = sprintf(Lang::account('register'), $nStep); - break; - default: - header('Location: '.$this->getNext(true), true, 302); - break; - } - } - - protected function generateTitle() - { - $this->title = [Lang::account('title')]; - } - - protected function generatePath() { } - - private function createDashboard() - { - if (!User::isLoggedIn()) - $this->forwardToSignIn('account'); - - $user = DB::Aowow()->selectRow('SELECT * FROM ?_account WHERE `id` = ?d', User::$id); - $bans = DB::Aowow()->select('SELECT ab.*, a.`username`, ab.`id` AS ARRAY_KEY FROM ?_account_banned ab LEFT JOIN ?_account a ON a.`id` = ab.`staffId` WHERE ab.`userId` = ?d', User::$id); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = []; - $infobox[] = Lang::user('joinDate'). Lang::main('colon').'[tooltip name=joinDate]'. date('l, G:i:s', $user['joinDate']). '[/tooltip][span class=tip tooltip=joinDate]'. date(Lang::main('dateFmtShort'), $user['joinDate']). '[/span]'; - $infobox[] = Lang::user('lastLogin').Lang::main('colon').'[tooltip name=lastLogin]'.date('l, G:i:s', $user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.date(Lang::main('dateFmtShort'), $user['prevLogin']).'[/span]'; - $infobox[] = Lang::account('lastIP').Lang::main('colon').$user['prevIP']; - $infobox[] = Lang::account('email'). Lang::main('colon').$user['email']; - - $groups = []; - foreach (Lang::account('groups') as $idx => $key) - if ($idx >= 0 && $user['userGroups'] & (1 << $idx)) - $groups[] = (!fMod(count($groups) + 1, 3) ? '[br]' : null).Lang::account('groups', $idx); - $infobox[] = Lang::user('userGroups').Lang::main('colon').($groups ? implode(', ', $groups) : Lang::account('groups', -1)); - $infobox[] = Util::ucFirst(Lang::main('siteRep')).Lang::main('colon').User::getReputation(); - - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - - /*************/ - /* Ban Popup */ - /*************/ - - foreach ($bans as $b) - { - if (!($b['typeMask'] & (ACC_BAN_TEMP | ACC_BAN_PERM)) || ($b['end'] && $b['end'] <= time())) - continue; - - $this->banned = array( - 'by' => [$b['staffId'], $b['username']], - 'end' => $b['end'], - 'reason' => $b['reason'] - ); - - break; // one is enough - } - - /************/ - /* Listview */ - /************/ - - $this->forceTabs = true; - - // Reputation changelog (params only for comment-events) - if ($repData = DB::Aowow()->select('SELECT action, amount, date AS \'when\', IF(action IN (3, 4, 5), sourceA, 0) AS param FROM ?_account_reputation WHERE userId = ?d', User::$id)) - { - foreach ($repData as &$r) - $r['when'] = date(Util::$dateFormatInternal, $r['when']); - - $this->lvTabs[] = ['reputationhistory', ['data' => $repData]]; - } - - // comments - if ($_ = CommunityContent::getCommentPreviews(['user' => User::$id, 'comments' => true])) - { - // needs foundCount for params - // _totalCount: 377, - // note: $WH.sprintf(LANG.lvnote_usercomments, 377), - - $this->lvTabs[] = ['commentpreview', array( - 'data' => $_, - 'hiddenCols' => ['author'], - 'onBeforeCreate' => '$Listview.funcBox.beforeUserComments' - )]; - } - - // replies - if ($_ = CommunityContent::getCommentPreviews(['user' => User::$id, 'replies' => true])) - { - // needs commentid (parentComment) for data - // needs foundCount for params - // _totalCount: 377, - // note: $WH.sprintf(LANG.lvnote_usercomments, 377), - - $this->lvTabs[] = ['replypreview', array( - 'data' => $_, - 'hiddenCols' => ['author'] - )]; - } - -/* -
    - - - -
    - - -*/ - // claimed characters - // profiles - // own screenshots - // own videos - // own comments (preview) - // articles guides..? - - - // cpmsg change pass messaeg class:failure|success, msg:blabla - } - - private function createRecoverPass(&$step) - { - $step = 1; - - if ($this->_post['email']) // step 1 - { - if (!Util::isValidEmail($this->_post['email'])) - $this->error = Lang::account('emailInvalid'); - else if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE email = ?', $this->_post['email'])) - $this->error = Lang::account('emailNotFound'); - else if ($err = $this->doRecoverPass()) - $this->error = $err; - else - { - $step = 1.5; - $this->text = sprintf(Lang::account('recovPassSent'), $this->_post['email']); - } - } - else if ($this->_get['token']) // step 2 - { - $step = 2; - $this->resetPass = true; - $this->token = $this->_get['token']; - } - else if ($this->_post['token'] && $this->_post['email'] && $this->_post['password'] && $this->_post['c_password']) - { - $step = 2; - $this->resetPass = true; - $this->token = $this->_post['token']; // insecure source .. that sucks; but whats the worst that could happen .. this account cannot be recovered for some minutes - - if ($err = $this->doResetPass()) - $this->error = $err; - else - return true; - } - - return false; - } - - private function doSignUp() - { - // check username - if (!User::isValidName($this->_post['username'], $e)) - return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'); - - // check password - if (!User::isValidPass($this->_post['password'], $e)) - return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); - - if ($this->_post['password'] != $this->_post['c_password']) - return Lang::account('passMismatch'); - - // check email - if (!Util::isValidEmail($this->_post['email'])) - return Lang::account('emailInvalid'); - - // check ip - if (!User::$ip) - return Lang::main('intError'); - - // limit account creation - $ip = DB::Aowow()->selectRow('SELECT `ip`, `count`, `unbanDate` FROM ?_account_bannedips WHERE `type` = 1 AND `ip` = ?', User::$ip); - if ($ip && $ip['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ip['unbanDate'] >= time()) - { - DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip); - return sprintf(Lang::account('signupExceeded'), Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)); - } - - // username taken - if ($_ = DB::Aowow()->SelectCell('SELECT `username` FROM ?_account WHERE (`username` = ? OR `email` = ?) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $this->_post['username'], $this->_post['email'], ACC_STATUS_NEW, ACC_STATUS_NEW)) - return $_ == $this->_post['username'] ? Lang::account('nameInUse') : Lang::account('mailInUse'); - - // create.. - $token = Util::createHash(); - $ok = DB::Aowow()->query('REPLACE INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', - $this->_post['username'], - User::hashCrypt($this->_post['password']), - $this->_post['username'], - $this->_post['email'], - User::$ip, - Lang::getLocale()->value, - U_GROUP_PENDING, - ACC_STATUS_NEW, - Cfg::get('ACC_CREATE_SAVE_DECAY'), - $token - ); - if (!$ok) - return Lang::main('intError'); - - if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return Lang::main('intError2', ['send mail']); - - if ($id = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE token = ?', $token)) - Util::gainSiteReputation($id, SITEREP_ACTION_REGISTER); - - // success:: update ip-bans - if (!$ip || $ip['unbanDate'] < time()) - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, 1, UNIX_TIMESTAMP() + ?d)', User::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK')); - else - DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip); - } - - private function doRecoverPass() - { - if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token)) - return $_; - - // send recovery mail - if (!Util::sendMail($this->_post['email'], 'reset-password', [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return Lang::main('intError2', ['send mail']); - } - - private function doResetPass() - { - if ($this->_post['password'] != $this->_post['c_password']) - return Lang::account('passCheckFail'); - - if (!Util::isValidEmail($this->_post['email'])) - return Lang::account('emailInvalid'); - - $userData = DB::Aowow()->selectRow('SELECT `id, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', - $this->_post['token'], - $this->_post['email'], - ACC_STATUS_RECOVER_PASS - ); - if (!$userData) - return Lang::account('emailNotFound'); // assume they didn't meddle with the token - - if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash'])) - return Lang::account('newPassDiff'); - - if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_OK, $userData['id'])) - return Lang::main('intError'); - } - - private function doRecoverUser() - { - if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_USER, Cfg::get('ACC_RECOVERY_DECAY'), $token)) - return $_; - - if (!Util::sendMail($this->_post['email'], 'recover-user', [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return Lang::main('intError2', ['send mail']); - } - - private function initRecovery($type, $delay, &$token) - { - if (!$type) - return Lang::main('intError'); - - // check if already processing - if ($_ = DB::Aowow()->selectCell('SELECT statusTimer - UNIX_TIMESTAMP() FROM ?_account WHERE email = ? AND status <> ?d AND statusTimer > UNIX_TIMESTAMP()', $this->_post['email'], ACC_STATUS_OK)) - return sprintf(Lang::account('isRecovering'), Util::formatTime($_ * 1000)); - - // create new token and write to db - $token = Util::createHash(); - if (!DB::Aowow()->query('UPDATE ?_account SET token = ?, status = ?d, statusTimer = UNIX_TIMESTAMP() + ?d WHERE email = ?', $token, $type, $delay, $this->_post['email'])) - return Lang::main('intError'); - } - - private function getNext($forHeader = false) - { - $next = $forHeader ? '.' : ''; - if ($this->_get['next']) - $next = $this->_get['next']; - else if (isset($_SERVER['HTTP_REFERER']) && strstr($_SERVER['HTTP_REFERER'], '?')) - $next = explode('?', $_SERVER['HTTP_REFERER'])[1]; - - if ($forHeader && !$next) - $next = '.'; - - return ($forHeader && $next != '.' ? '?' : '').$next; - } -} - -?> diff --git a/setup/updates/1758578400_11.sql b/setup/updates/1758578400_11.sql new file mode 100644 index 00000000..e525afa0 --- /dev/null +++ b/setup/updates/1758578400_11.sql @@ -0,0 +1,3 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `debug` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'show ids in lists user option' AFTER `userGroups`, + MODIFY COLUMN `description` text NOT NULL DEFAULT ''; diff --git a/setup/updates/1758578400_12.sql b/setup/updates/1758578400_12.sql new file mode 100644 index 00000000..ad48339e --- /dev/null +++ b/setup/updates/1758578400_12.sql @@ -0,0 +1,11 @@ +ALTER TABLE `aowow_user_ratings` + DROP KEY `FK_acc_co_rate_user`, + DROP FOREIGN KEY `FK_userId`, + DROP PRIMARY KEY; + +ALTER TABLE `aowow_user_ratings` MODIFY `userId` int unsigned NULL; + +ALTER TABLE `aowow_user_ratings` + ADD UNIQUE KEY (`type`,`entry`,`userId`), + ADD KEY `FK_acc_co_rate_user` (`userId`), + ADD CONSTRAINT FK_userId FOREIGN KEY (`userId`) REFERENCES aowow_account(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/static/css/aowow.css b/static/css/aowow.css index e78c690e..9d9f653b 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -502,6 +502,15 @@ a.premium-user-badge { border-radius: 6px; } +/* aowow - imported for account page */ +.box { + padding: 15px; + background: #282828; + border-radius: 6px; + display: table; + margin: 10px 0; +} + .reputation-negative-amount span { /* The whole text has the class; the number is spanned */ color: red; font-weight: bold; @@ -3202,7 +3211,7 @@ td.screenshot-cell:hover img { .text h1 { color: white; - font-size: 19px; + font-size: 19px !important; font-weight: normal; border-bottom: 1px solid #505050; padding: 0 0 5px 0; diff --git a/static/js/account.js b/static/js/account.js new file mode 100644 index 00000000..49a1cfed --- /dev/null +++ b/static/js/account.js @@ -0,0 +1,462 @@ +function pm() { + var pass1 = $('#newpass').val(); + var pass2 = $('#confirmpass').val(); + var + bracket = '', + buff = ''; + + if (pass1 != '' && $WH.trim(pass1).length < 6) + buff = '' + LANG.message_passwordmin + ''; + + if (pass1 != '' && pass2 != '') { + if (buff != '') + buff += '
    '; + + if (pass1 == pass2) + buff += '' + LANG.myaccount_passmatch + ''; + else + buff += '' + LANG.myaccount_passdontmatch + ''; + } + + if (buff != '') + bracket = '}'; + + $WH.ge('pm1').innerHTML = bracket; + $WH.ge('pm2').innerHTML = buff; +} + +function spd(form) { + var desc = form.elements.desc; + if (desc.value.length == 0) + return true; + + if (desc.value.length < 10) { + alert(LANG.message_descriptiontooshort); + return false; + } + + var charLimit = Listview.funcBox.coGetCharLimit(2); + if (desc.value.length > charLimit) + if (!confirm($WH.sprintf(LANG.confirm_descriptiontoolong, charLimit, desc.value.substring(charLimit - 30, charLimit)))) + return false; + + return true; +} + +function sfs(form) { + var sig = form.elements.sig; + sig.value = $WH.trim(sig.value); + if (sig.value.length == 0) + return true; + + var charLimit = Listview.funcBox.coGetCharLimit(4); + if (sig.value.length > charLimit) + if (!confirm($WH.sprintf(LANG.confirm_signaturetoolong, charLimit, sig.value.substring(charLimit - 30, charLimit)))) + return false; + + var nLines; + if ((nLines = sig.value.indexOf("\n")) != -1 && (nLines = sig.value.indexOf("\n", nLines + 1)) != -1 && (nLines = sig.value.indexOf("\n", nLines + 1)) != -1) + if (!confirm($WH.sprintf(LANG.confirm_signaturetoomanylines, 3))) + return false; + + return true; +} + +$(document).ready(function () { + $('form#change-password').submit(function () { + var curPass = $('input[name=currentPassword]'); + var newPass = $('input[name=newPassword]'); + var checkPass = $('input[name=confirmPassword]'); + + if (!curPass.val() && !newPass.val() && !checkPass.val()) { + alert(LANG.message_enteremailorpass); + return false; + } + + if (newPass.val() || checkPass.val()) { + if (!curPass.val()) { + alert(LANG.message_enterpassword); + curPass[0].focus(); + return false; + } + + if ($WH.trim(newPass.val()).length < 6) { + alert(LANG.message_passwordmin); + newPass[0].focus(); + return false; + } + + if ($WH.trim(newPass.val()) === $WH.trim(curPass.val())) { + alert(LANG.message_newpassdifferent); + newPass[0].focus(); + return false; + } + + if (newPass.val() !== checkPass.val()) { + alert(LANG.message_passwordsdonotmatch); + newPass[0].focus(); + return false; + } + } + + return true; + }); + + $('form#change-email').submit(function () { + var curMail = $('input[name=current-email]'); + var newMail = $('input[name=newemail]'); + + if (!newMail.val()) { + alert(LANG.message_enteremailorpass); + return false; + } + + if (newMail.val()) { + if (newMail.val() == curMail.val()) { + alert(LANG.message_newemaildifferent); + newMail[0].focus(); + return false; + } + + if (!g_isEmailValid(newMail.val())) { + alert(LANG.message_emailnotvalid); + newMail[0].focus(); + return false; + } + } + + return true; + }); + + $('form#change-username').submit(function () { + var curName = $('input[name=current-username]'); + var newName = $('input[name=newUsername]'); + + if (!newName.val()) { + alert(LANG.message_enterusername); + newName[0].focus(); + return false; + } + if ($WH.trim(newName.val()).length < 4) { + alert(LANG.message_usernamemin); + newName[0].focus(); + return false; + } + if (!g_isUsernameValid(newName.val())) { + alert(LANG.message_usernamenotvalid); + newName[0].focus(); + return false; + } + if (newName.val() == curName.val()) { + alert(LANG.message_newnamedifferent); + newName[0].focus(); + return false; + } + }); +}); + +function fa_validateForm(form) { + if (form.elements.avatar[2].checked && form.elements.customicon.selectedIndex == 0) { + form.action = '?upload=image-crop'; + form.enctype = 'multipart/form-data'; + } + else { + form.action = '?account=forum-avatar'; + form.enctype = 'application/x-www-form-urlencoded'; + } + + return true; +} + +function faChange(mode) { + $WH.ge('avaSel1').style.display = (mode == 1 ? '': 'none'); + $WH.ge('avaSel2').style.display = (mode == 2 ? '': 'none'); +} + +function spawi() { + var inp = $WH.ge('wowicon'); + inp.value = $WH.trim(inp.value); + + var preview = $WH.ge('avaPre1'); + while (preview.firstChild) + $WH.de(preview.firstChild); + + $WH.ae(preview, Icon.createUser(1, inp.value, 2, null, ((g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER))); +} + +function spawj() { + var avSelect = $WH.ge('customicon'); + var preview = $WH.ge('avaPre2'); + while (preview.firstChild) + $WH.de(preview.firstChild); + + if (avSelect.selectedIndex != 0) { + $WH.ge('iconbrowse').style.display = 'none'; + iconId = avSelect.options[avSelect.selectedIndex].value; + $WH.ae(preview, Icon.createUser(2, iconId, 2, null, ((g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER))); + preview.style.display = ''; + } + else { + preview.style.display = 'none'; + $WH.ge('iconbrowse').style.display = ''; + } +} + +var imageDetailDialog = new Dialog(); +Listview.templates.avatar = { + sort: [4], + nItemsPerPage: -1, + mode: 1, + poundable: 0, + columns: [{ + id: 'name', + name: LANG.name, + type: 'text', + value: 'name', + align: 'left', + compute: function (data, td, tr) { + tr.onclick = imageDetailDialog.show.bind(null, 'imageupload', { + data: data, + onSubmit: this.template.updateImageInfo.bind(this, data) + }); + var avIcon = Icon.createUser(2, data.id, 0, null, (g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER); + avIcon.style.cssFloat = avIcon.style.styleFloat = 'left'; + td.style.position = 'relative'; + $WH.ae(td, avIcon); + $WH.ae(td, $WH.ce('span', { style: { paddingLeft: '7px', lineHeight: '1.8em' }, innerHTML: data.name })); + if (data.current) { + $WH.ae(td, $WH.ce('span', { + style: { + fontStyle: 'italic', + cssFloat: 'right', + styleFloat: 'right', + marginTop: '3px' + }, + className: 'small', + innerHTML: 'Current' + })); + } + }, + getVisibleText: function (a) { + return a.caption; + } + }, + { + id: 'size', + name: 'Size', + type: 'number', + value: 'size', + width: '125px', + compute: function (a, b) { + return Listview.funcBox.coFormatFileSize(a.size) + } + }, + { + id: 'status', + name: 'Status', + type: 'text', + value: 'status', + width: '100px', + compute: function (a, b) { + if (a.status == 2) + $WH.ae(b, $WH.ce('span', { className: 'q10', innerHTML: 'Rejected' })) + else + return 'Ready'; + } + }, + { + id: 'when', + name: 'When', + type: 'date', + value: 'when', + width: '150px', + compute: function (b, d) { + var c = $WH.ce('span'); + var a = new Date(b.when); + g_formatDate(c, (g_serverTime - a) / 1000, a); + $WH.ae(d, c) + } + }], + onBeforeCreate: function () { + for (i in this.data) + this.data[i].pos = i; + }, + createCbControls: function (e, d) { + if (!d && this.data.length < 15) + return; + + var c = $WH.ce('input'), + b = $WH.ce('input'), + a = $WH.ce('input'); + + c.type = b.type = a.type = 'button'; + + c.value = 'Delete'; + b.value = 'Set as avatar'; + a.value = 'Upload new one'; + + c.onclick = this.template.deleteFiles.bind(this); + b.onclick = this.template.useAvatar.bind(this); + a.onclick = this.template.jumpToUpload.bind(this); + + $WH.ae(e, b); + $WH.ae(e, c); + $WH.ae(e, a); + }, + updateImageInfo: function (b, a) { + if (b.name != a.name) { + $.post('?account=rename-icon', { + id: a.id, + name: a.name + }); + this.setRow(a); + } + }, + deleteFiles: function () { + var rows = this.getCheckedRows(); + if (!rows.length) + return; + + var ids = '', + first = true; + $WH.array_walk(rows, function (x) { + if (first) + first = false; + else + ids += ','; + + ids += x.id; + }); + + var _ = confirm('Are you sure you want to delete these icons?'); + if (_ == false) + return; + + $.post('?account=delete-icon', { id: ids }); + + this.deleteRows(rows); + this.resetCheckedRows(); + this.refreshRows(); + }, + useAvatar: function () { + var rows = this.getCheckedRows(); + if (!rows.length) + return; + + if (rows.length > 1) { + alert('Please select only 1 image to use as your avatar.'); + return; + } + + var row = rows[0]; + $WH.array_walk(this.data, function (x) { + x.current = 0; + x.__tr = null + }); + row.current = 1; + + new Ajax('?account=forum-avatar&avatar=2&customicon=' + row.id); + this.refreshRows() + }, + jumpToUpload: function () { + // aowow - community is not on idx:2 for extAuth cases + // _.show(2); + _.show(_.tabs.findIndex((x) => x.id == 'community')); + location.href = '?account#community'; + + var a = $WH.ac(document.fa); + window.scrollTo(0, a.y); + + document.fa.avatar[2].click(); + document.fa.customicon.selectedIndex = 0; + + spawj(); + }, + onNoData: function (lv) { + var sp = $WH.ce('span'); + var a = $WH.ce('a'); + + a.onclick = this.template.jumpToUpload.bind(this); + a.href = 'javascript:;'; + $WH.ae(a, $WH.ct('Upload')); + + $WH.ae(sp, $WH.ct("You havn't uploaded any custom avatars yet. ")); + $WH.ae(sp, a); + $WH.ae(sp, $WH.ct(' one now!')); + + $WH.ae(lv, sp); + } +}; + +Dialog.templates.imageupload = { + title: LANG.dialog_imagedetails, + // aowow - adapted to existing css - buttons: [['check', LANG.ok], ['x', LANG.cancel]], + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + fields: [ + { + id: 'id', + type: 'hidden', + label: ' ', + size: 30, + required: 0, + compute: function (field, value, form, td, tr) { + var div = $WH.ce('div'); + div.style.position = 'relative'; + + var div2 = $WH.ce('div'); + div2.style.position = 'relative'; + + var img = $WH.ce('img'); + switch (this.data.type) { + case 1: + img = Icon.createUser(2, null, 2, null, (g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER); + break; + } + + $WH.ae(div2, img); + this.icon = img; + + $WH.ae(div, field); + $WH.ae(div, div2); + + $WH.ae(td, div); + } + }, + { + id: 'name', + type: 'text', + label: LANG.dialog_imagename, + size: 20, + required: 1, + submitOnEnter: 1, + validate: function (newValue, data) { + if (newValue.match(/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/)) + return true; + else { + alert(LANG.message_invalidname); + return false; + } + } + }, + ], + onBeforeShow: function () { + switch (this.data.type) { + case 1: + this.template.width = 300; + break; + } + }, + onShow: function (form) { + switch (this.data.type) { + case 1: + var url = g_staticUrl + '/uploads/avatars/' + this.data.id + '.jpg'; + Icon.setTexture(this.icon, 2, url); + break; + } + setTimeout(function () { + var inp = form.elements.name; + inp.focus(); + inp.select(); + }, 1); + } +}; diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 32d67761..055c8497 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -2965,6 +2965,7 @@ var LANG = { message_invalidname: "Bildname ist ungültig. Muss alphanumerisch sein, maximal 20 Zeichen haben und mit einem Buchstaben anfangen.", message_newemaildifferent: "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", message_newpassdifferent: "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", + message_newnamedifferent: "Euer neuer Benutzername muss sich von eurem alten Benutzernamen unterscheiden.", message_noscreenshot: "Wählt bitte den Screenshot aus, den Ihr hochladen möchtet.", message_novideo: "Bitte gebt gültige Videoinformationen ein.", message_nothingtoviewin3d: "Es wurden keine Gegenstände ausgewählt, die in 3D angezeigt werden können.", diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 4aacd372..417ee9ab 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -3014,6 +3014,7 @@ var LANG = { message_invalidname: "Image name is invalid. Must be alphanumeric, 20 characters max, and start with a letter.", message_newemaildifferent: "Your new email address must be different than your previous one.", message_newpassdifferent: "Your new password must be different than your previous one.", + message_newnamedifferent: "Your new username must be different than your previous one.", message_noscreenshot: "Please select the screenshot to upload.", message_novideo: "Please enter valid video information.", message_nothingtoviewin3d: "No items were selected that can be viewed in 3D.", diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 6cbf3d68..ca03d740 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -2965,6 +2965,7 @@ var LANG = { message_invalidname: "El nombre de la imagen es inválido. Debe ser alfanumérico con un máx de 20 caracteres y debe empezar por una letra.", message_newemaildifferent: "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", message_newpassdifferent: "Su nueva contraseña tiene que ser diferente a Su contraseña anterior.", + message_newnamedifferent: "Su nuevo nombre de usuario tiene que ser diferente a su nombre de usuario anterior.", message_noscreenshot: "Por favor seleccione la captura de pantalla para subir.", message_novideo: "Por favor, introduce información válida del vídeo.", message_nothingtoviewin3d: "No se han seleccionado objetos que se puedan ver en 3D.", diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 747196f1..04be890c 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -2966,6 +2966,7 @@ var LANG = { message_invalidname: "Le nom de l'image est invalide. Doit être alphanumérique, 20 caractères maximum et doit commencer par une lettre.", message_newemaildifferent: "Votre nouvelle adresse courriel doit être différente de l'ancienne.", message_newpassdifferent: "Votre nouveau mot de passe doit être différent de l'ancien.", + message_newnamedifferent: "Votre nouveau nom d'utilisateur doit être différent de l'ancien.", message_noscreenshot: "Veuillez sélectionner la capture d'écran à envoyer.", message_novideo: "Veuillez entrer des informations valide pour le vidéo.", message_nothingtoviewin3d: "Aucun objets qui ont été sélectionnés ne peuvent être vus en 3D.", diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 0211ba2c..117fd9cc 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -2966,6 +2966,7 @@ var LANG = { message_invalidname: "Название изображения некорректно. Должно содержать только латинские буквы и цифры, начинаться с буквы, и быть не более 20 символов в длину.", message_newemaildifferent: "Прежний и новый e-mail адреса не должны совпадать.", message_newpassdifferent: "Прежний и новый пароли не должны совпадать.", + message_newnamedifferent: "Прежнее и новое имя пользователя не должны совпадать.", message_noscreenshot: "Выберите изображение для загрузки.", message_novideo: "Введите корректную информацию о видео.", message_nothingtoviewin3d: "Вы не выбрали предметы, которые можно просмотреть в 3D.", diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 186bb754..055d844c 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -3013,6 +3013,7 @@ var LANG = { message_invalidname: "图片名无效。必须使用字母和数字,最多20个字符,以字母开头。", message_newemaildifferent: "您的新邮箱地址必须不同于旧地址。", message_newpassdifferent: "您的新密码必须不同于旧密码。", + message_newnamedifferent: "您的新用户名必须不同于旧用户名。", message_noscreenshot: "请选择要上传的截屏。", message_novideo: "请输入有效的视频信息。", message_nothingtoviewin3d: "没有选中可以3D浏览的物品。", diff --git a/template/bricks/inputbox-form-signin.tpl.php b/template/bricks/inputbox-form-signin.tpl.php index 76876e57..a917c7a5 100644 --- a/template/bricks/inputbox-form-signin.tpl.php +++ b/template/bricks/inputbox-form-signin.tpl.php @@ -52,7 +52,7 @@
    -
    | |
    +
    | |
    diff --git a/template/pages/acc-dashboard.tpl.php b/template/pages/acc-dashboard.tpl.php deleted file mode 100644 index a90c91fc..00000000 --- a/template/pages/acc-dashboard.tpl.php +++ /dev/null @@ -1,141 +0,0 @@ - - -brick('header'); ?> - -
    -
    -
    - -brick('announcement'); - - $this->brick('pageTemplate'); - - $this->brick('infobox'); -?> - - - -
    -

    -banned): -?> -
    -

    -
      -
    • '.Lang::account('bannedBy').''.Lang::main('colon').''.$b['by'][1].''; ?>
    • -
    • '.Lang::account('ends').''.Lang::main('colon').($b['end'] ? date(Lang::main('dateFmtLong'), $b['end']) : Lang::account('permanent')); ?>
    • -
    • '.Lang::account('reason').''.Lang::main('colon').''.($b['reason'] ?: Lang::account('noReason')).''; ?>
    • -
    -
    - - - - - -

    {$lang.publicDesc}

    -
    {$lang.Your_description_has_been_updated_successfully}.
    - -
    - {$lang.viewPublicDesc|sprintf:$user.name}. -
    -
    - -
    - -
    -{* CLAIM CHARACTERS *} -

    [Select Character]

    -{strip} - -{if $user.chars} - - {foreach from=$user.chars item=c} - - - - - {/foreach} -
    - {if $c.this} - {$c.name} - {else} - {$c.name} - {/if} -   - {if $c.guild} - <{$c.guild|escape:"html"}> - {/if} -  — {$c.text} -
    -{else} - [no characters on ths account] -{/if} -
    - -{/strip} -{* CHANGE PASSWORD / EMAIL / DISPLAYNAME / AVATAR * } -

    {$lang.Change_password}

    -
    - -{if isset($cpmsg)} -
    {$cpmsg.msg}
    -{/if} - - - - - -
    {$lang.Current_password}{$lang.colon}
    {$lang.New_password}{$lang.colon}
    {$lang.Confirm_new_password}{$lang.colon}
    -
    - - -
    -
    - -
    - -*/ -?> - -brick('lvTabs'); ?> - -
    -
    -
    - -brick('footer'); ?> diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php new file mode 100644 index 00000000..ca64947c --- /dev/null +++ b/template/pages/account.tpl.php @@ -0,0 +1,291 @@ +brick('header'); +?> +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
    +

    +bans): + foreach ($this->bans as $b): + [$end, $reason, $name] = $b; +?> +
    +

    +
      +
    • '.Lang::account('bannedBy').''.($name ? ''.$name.'' : '<System>');?>
    • +
    • '.Lang::account('ends').''.($end ? date(Lang::main('dateFmtLong'), $end) : Lang::account('permanent'));?>
    • +
    • '.Lang::account('reason').''.''.($reason ?: Lang::account('noReason')).'';?>
    • +
    +
    + +
    + + + + +
    +
    + +
    + +
    +
    + + + +cfg('ACC_AUTH_MODE') == AUTH_MODE_SELF): +?> + + + + + + + +
    +
    + + + + +
    +
    + + +brick('footer'); ?> From f16479b50cc8fe453ef3dc29ae61c90937f2ecc3 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:43:07 +0200 Subject: [PATCH 690/957] Template/Update (Part 46 - II) * account management rework: Signup functionality --- endpoints/account/activate.php | 73 ++++++++ endpoints/account/signin.php | 61 ++++--- endpoints/account/signup.php | 163 ++++++++++++++++++ .../inputbox-form-signup.tpl.php} | 39 +---- 4 files changed, 279 insertions(+), 57 deletions(-) create mode 100644 endpoints/account/activate.php create mode 100644 endpoints/account/signup.php rename template/{pages/acc-signUp.tpl.php => bricks/inputbox-form-signup.tpl.php} (79%) diff --git a/endpoints/account/activate.php b/endpoints/account/activate.php new file mode 100644 index 00000000..d437d75c --- /dev/null +++ b/endpoints/account/activate.php @@ -0,0 +1,73 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + public function __construct() + { + parent::__construct(); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $msg = $this->activate(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'register', [2]), 'message' => $msg]]; + else + { + $_SESSION['error']['activate'] = $msg; + $this->forward('?account=resend'); + } + + parent::generate(); + } + + private function activate() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + if (DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` IN (?a) AND `token` = ?', [ACC_STATUS_NONE, ACC_STATUS_NEW], $this->_get['key'])) + { + // don't remove the token yet. It's needed on signin page. + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `userGroups` = ?d WHERE `token` = ?', ACC_STATUS_NONE, U_GROUP_NONE, $this->_get['key']); + + // fully apply block for further registration attempts from this ip + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d + 1, UNIX_TIMESTAMP() + ?d)', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'accActivated', [$this->_get['key']]); + } + + // grace period expired and other user claimed name + return Lang::main('intError'); + } +} + +?> diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index dce60520..c4a0329e 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -19,6 +19,7 @@ class AccountSigninResponse extends TemplateResponse use TrGetNext; protected string $template = 'text-page-generic'; + protected string $pageName = 'signin'; protected array $expectedPOST = array( 'username' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateLogin'] ], @@ -26,8 +27,8 @@ class AccountSigninResponse extends TemplateResponse 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe'] ] ); protected array $expectedGET = array( - 'token' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{32}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] ); private bool $success = false; @@ -43,46 +44,54 @@ class AccountSigninResponse extends TemplateResponse protected function generate() : void { - $username = ''; - $message = ''; + $username = + $error = ''; + $rememberMe = !!$this->_post['remember_me']; $this->title = [Lang::account('title')]; - if ($this->_get['token']) + // coming from user recovery or creation, prefill username + if ($this->_get['key']) { - // coming from username recovery, prefill username - if ($_ = DB::Aowow()->selectCell('SELECT `login` FROM ?_account WHERE `status` IN (?a) AND `token` = ? AND `statusTimer` > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token'])) - $username = $_; + if ($userData = DB::Aowow()->selectRow('SELECT a.`login` AS "0", IF(s.`expires`, 0, 1) AS "1" FROM ?_account a LEFT JOIN ?_account_sessions s ON a.`id` = s.`userId` AND a.`token` = s.`sessionId` WHERE a.`status` IN (?a) AND a.`token` = ?', + [ACC_STATUS_RECOVER_USER, ACC_STATUS_NONE], $this->_get['key'])) + [$username, $rememberMe] = $userData; } - $message = $this->doSignIn(); - if (!$this->success) - User::destroy(); - else + if ($this->doSignIn($error)) $this->forward($this->getNext(true)); + if ($error) + User::destroy(); + $this->inputbox = ['inputbox-form-signin', array( 'head' => Lang::account('inputbox', 'head', 'signin'), 'action' => '?account=signin&next='.$this->getNext(), - 'error' => $message, + 'error' => $error, 'username' => $username, - 'rememberMe' => !!$this->_post['remember_me'], + 'rememberMe' => $rememberMe, 'hasRecovery' => Cfg::get('ACC_EXT_RECOVER_URL') || Cfg::get('ACC_AUTH_MODE') == AUTH_MODE_SELF, )]; parent::generate(); } - private function doSignIn() : string + private function doSignIn(string &$error) : bool { if (is_null($this->_post['username']) && is_null($this->_post['password'])) - return ''; + return false; if (!$this->assertPOST('username')) - return Lang::account('userNotFound'); + { + $error = Lang::account('userNotFound'); + return false; + } if (!$this->assertPOST('password')) - return Lang::account('wrongPass'); + { + $error = Lang::account('wrongPass'); + return false; + } $error = match (User::authenticate($this->_post['username'], $this->_post['password'])) { @@ -95,10 +104,7 @@ class AccountSigninResponse extends TemplateResponse default => Lang::main('intError') }; - if (!$error) - $this->success = true; - - return $error; + return !$error; } private function onAuthSuccess() : string @@ -109,14 +115,11 @@ class AccountSigninResponse extends TemplateResponse return Lang::main('intError'); } - $email = filter_var($this->_post['username'], FILTER_VALIDATE_EMAIL); - // reset account status, update expiration - $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE { `email` = ? } { `login` = ? }', + $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE `id` = ?d', User::$ip, User::$ip, User::$ip, ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW, - $email ?: DBSIMPLE_SKIP, - !$email ? $this->_post['username'] : DBSIMPLE_SKIP + User::$id // available after successful User:authenticate ); if (!is_int($ok)) // num updated fields or null on fail @@ -125,6 +128,10 @@ class AccountSigninResponse extends TemplateResponse return Lang::main('intError'); } + // DELETE temp session + if ($this->_get['key']) + DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `sessionId` = ?', $this->_get['key']); + session_regenerate_id(true); // user status changed => regenerate id // create new session entry diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php new file mode 100644 index 00000000..f8a819e1 --- /dev/null +++ b/endpoints/account/signup.php @@ -0,0 +1,163 @@ + ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe']] + ); + + protected array $expectedGET = array( + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct() + { + // if the user is logged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + // redirect to external registration page, if set + if (Cfg::get('ACC_EXT_CREATE_URL')) + $this->forward(Cfg::get('ACC_EXT_CREATE_URL')); + + parent::__construct(); + + // registration not enabled on self + if (!Cfg::get('ACC_ALLOW_REGISTER')) + $this->generateError(); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + // step 1 - no params > signup form + // step 2 - any param > status box + // step 3 - on ?account=activate + + $message = $this->doSignUp(); + + if ($this->success) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]) + )]; + } + else + { + $this->inputbox = ['inputbox-form-signup', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1]), + 'error' => $message, + 'action' => '?account=signup&next='.$this->getNext(), + 'username' => $this->_post['username'] ?? '', + 'email' => $this->_post['email'] ?? '', + 'rememberMe' => !!$this->_post['remember_me'], + )]; + } + + parent::generate(); + } + + private function doSignUp() : string + { + // no input yet. show clean form + if (!$this->assertPOST('username', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + // check username + if (!Util::validateUsername($this->_post['username'], $e)) + return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'); + + // check password + if (!Util::validatePassword($this->_post['password'], $e)) + return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + + if ($this->_post['password'] !== $this->_post['c_password']) + return Lang::account('passMismatch'); + + // check ip + if (!User::$ip) + return Lang::main('intError'); + + // limit account creation + if (DB::Aowow()->selectRow('SELECT 1 FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ? AND `count` >= ?d AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'))) + { + DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = ?d', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT); + return Lang::account('inputbox', 'error', 'signupExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); + } + + // username / email taken + if ($inUseData = DB::Aowow()->SelectRow('SELECT `id`, `username`, `status` = ?d AND `statusTimer` < UNIX_TIMESTAMP() AS "expired" FROM ?_account WHERE (LOWER(`username`) = LOWER(?) OR LOWER(`email`) = LOWER(?))', ACC_STATUS_NEW, $this->_post['username'], $this->_post['email'])) + { + if ($inUseData['expired']) + DB::Aowow()->query('DELETE FROM ?_account WHERE `id` = ?d', $inUseData['id']); + else + return Util::lower($inUseData['username']) == Util::lower($this->_post['username']) ? Lang::account('nameInUse') : Lang::account('mailInUse'); + } + + // create.. + $token = Util::createHash(); + $userId = DB::Aowow()->query('INSERT INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', + $this->_post['username'], + User::hashCrypt($this->_post['password']), + $this->_post['username'], + $this->_post['email'], + User::$ip, + Lang::getLocale()->value, + U_GROUP_PENDING, + ACC_STATUS_NEW, + Cfg::get('ACC_CREATE_SAVE_DECAY'), + $token + ); + + if (!$userId) + return Lang::main('intError'); + + // create session tied to the token to store remember_me status + DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)', + $userId, $token, time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE); + + if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_CREATE_SAVE_DECAY'))) + return Lang::main('intError2', ['send mail']); + + // success: update ip-bans + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + Util::gainSiteReputation($userId, SITEREP_ACTION_REGISTER); + + $this->success = true; + return ''; + } +} + +?> diff --git a/template/pages/acc-signUp.tpl.php b/template/bricks/inputbox-form-signup.tpl.php similarity index 79% rename from template/pages/acc-signUp.tpl.php rename to template/bricks/inputbox-form-signup.tpl.php index 6b439795..20724f66 100644 --- a/template/pages/acc-signUp.tpl.php +++ b/template/bricks/inputbox-form-signup.tpl.php @@ -1,23 +1,10 @@ - - -brick('header'); ?> - -
    -
    -
    brick('announcement'); + namespace Aowow\Template; - $this->brick('pageTemplate'); + use \Aowow\Lang; ?>
    -text)): ?> -
    -

    head; ?>

    -
    -
    text; ?>
    -
    - + -
    +
    -

    head; ?>

    -
    error; ?>
    +

    +
    - + @@ -103,9 +88,9 @@ - + - + '); + + if(edit) + row.addClass('comment-reply-row').addClass('reply-edit-row'); + + row.html('' + + ''); + + /* Set up the various variables for the controls we just created */ + Body = row.find('.comment-form textarea'); + AddButton = row.find('.comment-form input[type=submit]'); + TextCounter = row.find('.comment-form span.text-counter'); + Form = row.find('.comment-form form'); + AjaxLoader = row.find('.comment-form .ajax-loader'); + FormContainer = row.find('.comment-form'); + + /* Intercept submits */ + Form.submit(function () { Submit(); return false; }); + + UpdateTextCounter(); + + /* This is kinda a mess.. Every browser seems to implement keyup, keydown and keypress differently. + * - keyup: We need to use keyup to update the text counter for the simple reason we want to update it only when the user stops typing. + * - keydown: We need to use keydown to detect the ESC key because it's the only one that works in all browsers for ESC + * - keypress: We need to use keypress to detect Enter because it's the only one that 1) Works 2) Allows us to prevent a new line from being entered in the textarea + * I find it very funny that in each scenario there is only one of the 3 that works, and that that one is always different from the others. + */ + + Body.keyup(function (e) { UpdateTextCounter(); }); + Body.keydown(function (e) { if (e.keyCode == 27) { Close(); return false; } }); // ESC + Body.keypress(function (e) { if (e.keyCode == 13) { Submit(); return false; } }); // ENTER + + if(edit) + { + post.after(row); + post.hide(); + Form.find('textarea').text(comment.replies[post.attr('data-idx')].body); + } + else + CommentsTable.append(row); + + DialogTableRowContainer = row; + Form.find('textarea').focus(); + } + + function Open() + { + if (!Initialized) + Initialize(); + + Active = true; + + if(!edit) + { + AddCommentLink.hide(); + post.find('.comment-replies').show(); + FormContainer.show(); + FormContainer.find('textarea').focus(); + } + } + + function Close() + { + Active = false; + + if(edit) + { + if(DialogTableRowContainer) + DialogTableRowContainer.remove(); + post.show(); + return; + } + + AddCommentLink.show(); + FormContainer.hide(); + + if (CommentsCount == 0) + post.find('.comment-replies').hide(); + } + + function Submit() + { + if (!Active || Submitting) + return; + + if (Body.val().length < MIN_LENGTH || Body.val().length > MAX_LENGTH) + { + /* Flash the char counter to attract the attention of the user. */ + if (!Flashing) + { + Flashing = true; + TextCounter.animate({ opacity: '0.0' }, 150); + TextCounter.animate({ opacity: '1.0' }, 150, null, function() { Flashing = false; }); + } + + return false; + } + + SetSubmitState(); + $.ajax({ + type: 'POST', + url: edit ? '?comment=edit-reply' : '?comment=add-reply', + data: { commentId: comment.id, replyId: (edit ? post.attr('data-replyid') : 0), body: Body.val() }, + success: function (newReplies) { OnSubmitSuccess(newReplies); }, + dataType: 'json', + error: function (jqXHR) { OnSubmitFailure(jqXHR.responseText); } + }); + return true; + } + + function SetSubmitState() + { + Submitting = true; + AjaxLoader.show(); + AddButton.attr('disabled', 'disabled'); + FormContainer.find('.message-box').remove(); + } + + function ClearSubmitState() + { + Submitting = false; + AjaxLoader.hide(); + AddButton.removeAttr('disabled'); + } + + function OnSubmitSuccess(newReplies) + { + comment.replies = newReplies; + Listview.templates.comment.updateReplies(comment); + } + + function OnSubmitFailure(error) + { + ClearSubmitState(); + MessageBox(FormContainer, error); + } + + function UpdateTextCounter() + { + var text = '(error)'; + var cssClass = 'q0'; + var chars = Body.val().replace(/(\s+)/g, ' ').replace(/^\s*/, '').replace(/\s*$/, '').length; + var charsLeft = MAX_LENGTH - chars; + + if (chars == 0) + text = $WH.sprintf(LANG.replylength1_format, MIN_LENGTH); + else if (chars < MIN_LENGTH) + text = $WH.sprintf(LANG.replylength2_format, MIN_LENGTH - chars); + else + { + text = $WH.sprintf(charsLeft == 1 ? LANG.replylength4_format : LANG.replylength3_format, charsLeft); + + if (charsLeft < 120) + cssClass = 'q10'; + else if (charsLeft < 240) + cssClass = 'q5'; + else if (charsLeft < 360) + cssClass = 'q11'; + } + + TextCounter.html(text).attr('class', cssClass); + } +} + +function SetupShowMoreComments(post, comment) +{ + var ShowMoreCommentsLink = post.find('.show-more-replies'); + var CommentCell = post.find('.comment-replies'); + + ShowMoreCommentsLink.click(function () { ShowMoreComments(); }); + + function ShowMoreComments() + { + /* Replace link with ajax loader */ + ShowMoreCommentsLink.hide(); + CommentCell.append(CreateAjaxLoader()); + + $.ajax({ + type: 'GET', + url: '?comment=show-replies', + data: { id: comment.id }, + success: function (replies) { comment.replies = replies; Listview.templates.comment.updateReplies(comment); }, + dataType: 'json', + error: function () { OnFetchFail(); } + }); + } + + function OnFetchFail() + { + ShowMoreCommentsLink.show(); + CommentCell.find('.ajax-loader').remove(); + + MessageBox(CommentCell, "There was an error fetching the comments. Try refreshing the page."); + } +} + +function SetupRepliesControls(post, comment) +{ + var CommentId = post.attr('data-replyid'); + var VoteUpControl = post.find('.reply-upvote'); + var VoteDownControl = post.find('.reply-downvote'); + var FlagControl = post.find('.reply-report'); + var CommentScoreText = post.find('.reply-rating'); + var CommentActions = post.find('.reply-controls'); + var DeleteButton = post.find('.reply-delete'); + var EditButton = post.find('.reply-edit'); + var Voting = false; + var Deleting = false; + // aowow - detach functionality is custom + var Detaching = false; + var DetachButton = post.find('.reply-detach'); + var Container = comment.repliesCell; + + EditButton.click(function() { + SetupAddEditComment(post, comment, true); + }); + + FlagControl.click(function () + { + if (Voting || !confirm(LANG.replyreportwarning_tip)) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=flag-reply', + data: { id: CommentId }, + success: function () { OnFlagSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + VoteUpControl.click(function () + { + if (VoteUpControl.attr('data-hasvoted') == 'true' || VoteUpControl.attr('data-canvote') != 'true' || Voting) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=upvote-reply', + data: { id: CommentId }, + success: function () { OnVoteSuccessful(1); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + VoteDownControl.click(function () + { + if (VoteDownControl.attr('data-hasvoted') == 'true' || VoteDownControl.attr('data-canvote') != 'true' || Voting) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=downvote-reply', + data: { id: CommentId }, + success: function () { OnVoteSuccessful(-1); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + DetachButton.click(function () + { + if (Detaching) { + MessageBox(CommentActions, LANG.message_cantdetachcomment); + return; + } + + if (!confirm(LANG.confirm_detachcomment)) { + return; + } + + Detaching = true; + $.ajax({ + type: 'POST', + url: '?comment=detach-reply', + data: { id: CommentId }, + success: function () { OnDetachSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + DeleteButton.click(function () + { + if (Deleting) + return; + + if (!confirm(LANG.deletereplyconfirmation_tip)) + return; + + Deleting = true; + $.ajax({ + type: 'POST', + url: '?comment=delete-reply', + data: { id: CommentId }, + success: function () { OnDeleteSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + function OnVoteSuccessful(ratingChange) + { + var rating = parseInt(CommentScoreText.text()); + + rating += ratingChange; + + CommentScoreText.text(rating); + + if(ratingChange > 0) + VoteUpControl.attr('data-hasvoted', 'true'); + else + VoteDownControl.attr('data-hasvoted', 'true'); + + VoteUpControl.attr('data-canvote', 'false'); + VoteDownControl.attr('data-canvote', 'false'); + + if(ratingChange > 0) + FlagControl.remove(); + Voting = false; + } + + function OnFlagSuccessful() + { + Voting = false; + FlagControl.remove(); + } + + function OnDetachSuccessful() + { + post.remove(); + MessageBox(Container, LANG.message_commentdetached); + Detaching = false; + } + + function OnDeleteSuccessful() + { + post.remove(); + Deleting = false; + } + + function OnError(text) + { + Voting = false; + Deleting = false; + Detaching = false; + + if (!text) + text = LANG.genericerror; + + MessageBox(CommentActions, text); + } +} + +/* +Global comment-related functions +*/ + +function co_addYourComment() +{ + tabsContribute.focus(0); + var ta = $WH.gE(document.forms['addcomment'], 'textarea')[0]; + ta.focus(); +} + +function co_validateForm(f) +{ + var ta = $WH.gE(f, 'textarea')[0]; + + // prevent locale comments on guide pages + var locale = Locale.getId(); + // aowow - disabled + // if(locale != LOCALE_ENUS && $(f).attr('action') && ($(f).attr('action').replace(/^.*type=([0-9]*).*$/i, '$1')) == 100) + if (false) + { + alert(LANG.message_cantpostlcomment_tip); + return false; + } + + if (g_user.permissions & 1) { + return true; + } + + if (Listview.funcBox.coValidate(ta)) { + return true; + } + + return false; +} + +// Display a warning if a user attempts to leave the page and he has started writing a message +$(document).ready(function() +{ + g_setupChangeWarning($("form[name=addcomment]"), [$("textarea[name=commentbody]")], LANG.message_startedpost); +}); diff --git a/setup/tools/filegen/templates/global.js/conditionList.js b/setup/tools/filegen/templates/global.js/conditionList.js new file mode 100644 index 00000000..c788c24c --- /dev/null +++ b/setup/tools/filegen/templates/global.js/conditionList.js @@ -0,0 +1,329 @@ +/* aowow - custom: TrinityCore Conditions */ +var ConditionList = new function() { + var self = this, + _conditions = null; + + self.createCell = function(conditions) + { + if (!conditions) + return null; + + _conditions = conditions; + + return _createCell(); + }; + + self.createTab = function(conditions) + { + if (!conditions) + return null; + + _conditions = conditions; + + return _createTab(); + }; + + function _makeList(mask, src, tpl) + { + var arr = Listview.funcBox.assocBinFlags(mask, src).sort(), + buff = ''; + + for (var i = 0, len = arr.length; i < len; ++i) + { + if (len > 1 && i == len - 1) + buff += LANG.or; + else if (i > 0) + buff += LANG.comma; + + buff += $WH.sprintf(tpl, arr[i], src[arr[i]]); + } + + return buff; + } + + function _parseEntry(entry, targets, target) + { + var str = '', + negate = false, + strIdx = 0, + param = []; + + [strIdx, ...param] = entry; + + negate = strIdx < 0; + strIdx = Math.abs(strIdx); + + if (!g_conditions[strIdx]) + return 'unknown condition index #' + strIdx; + + switch (strIdx) + { + case 5: + var standings = {}; + for (let i in g_reputation_standings) + standings[i * 1 + 1] = g_reputation_standings[i]; + + param[1] = _makeList(entry[2], standings, '$2'); + break; + + case 6: + if (entry[1] == 1) + param[0] = $WH.sprintf('[span class=icon-alliance]$1[/span]', g_sides[1]); + else if (entry[1] == 2) + param[0] = $WH.sprintf('[span class=icon-horde]$1[/span]', g_sides[2]); + else + param[0] = $WH.sprintf('[span class=icon-alliance]$1[/span]$2[span class=icon-horde]$3[/span]', g_sides[1], LANG.or, g_sides[2]); + break; + + case 10: + param[0] = g_drunk_states[entry[1]] ?? 'UNK DRUNK STATE'; + break; + + case 13: + param[2] = g_instance_info[entry[3]] ?? 'UNK INSTANCE INFO'; + break; + + case 15: + param[0] = _makeList(entry[1], g_chr_classes, '[class=$1]'); + break; + + case 16: + param[0] = _makeList(entry[1], g_chr_races, '[race=$1]'); + break; + + case 20: + if (entry[1] == 0) + param[0] = $WH.sprintf('[span class=icon-$1]$2[/span]', g_file_genders[0], LANG.male); + else if (entry[1] == 1) + param[0] = $WH.sprintf('[span class=icon-$1]$2[/span]', g_file_genders[1], LANG.female); + else + param[0] = g_npc_types[10]; // not specified + break; + + case 21: + var states = {}; + for (let i in g_unit_states) + states[i * 1 + 1] = g_unit_states[i]; + + param[0] = _makeList(entry[1], states, '$2'); + break; + + case 22: + if (entry[2]) + param[0] = '[zone=' + entry[2] + ']'; + else + param[0] = g_zone_categories[entry[1]] ?? 'UNK ZONE'; + break; + + case 24: + param[0] = g_npc_types[entry[1]] ?? 'UNK NPC TYPE'; + break; + + case 26: + var idx = 0, buff = []; + while (entry[1] >= (1 << idx)) { + if (!(entry[1] & (1 << idx++))) + continue; + + buff.push(idx); + } + param[0] = buff ? buff.join(LANG.comma) : ''; + break; + + case 27: + case 37: + case 38: + param[1] = g_operators[entry[2]]; + break; + + case 31: + if (entry[2] && entry[1] == 3) + param[0] = '[npc=' + entry[2] + ']'; + else if (entry[2] && entry[1] == 5) + param[0] = '[object=' + entry[2] + ']'; + else + param[0] = g_world_object_types[entry[1]] ?? 'UNK TYPEID'; + break; + + case 32: + var objectTypes = {}; + for (let i in g_world_object_types) + objectTypes[i * 1 + 1] = g_world_object_types[i]; + + param[0] = _makeList(entry[1], objectTypes, '$2'); + break; + + case 33: + param[0] = targets[entry[1]]; + param[1] = g_relation_types[entry[2]] ?? 'UNK RELATION'; + param[2] = targets[target]; + break; + + case 34: + param[0] = targets[entry[1]]; + + var standings = {}; + for (let i in g_reputation_standings) + standings[i * 1 + 1] = g_reputation_standings[i]; + param[1] = _makeList(entry[2], standings, '$2'); + break; + + case 35: + param[0] = targets[entry[1]]; + param[2] = g_operators[entry[3]]; + break; + + case 42: + if (!entry[1]) + param[0] = g_stand_states[entry[2]] ?? 'UNK STAND_STATE'; + else if (entry[1] == 1) + param[0] = g_stand_states[entry[2] ? 1 : 0]; + else + param[0] = ''; + break; + + case 47: + var quest_states = {}; + for (let i in g_quest_states) + quest_states[i * 1 + 1] = g_quest_states[i]; + + param[1] = _makeList(entry[2], quest_states, '$2'); + break; + } + + str = g_conditions[strIdx]; + + // fill in params + str = $WH.sprintfa(str, param[0], param[1], param[2]); + + // resolve NegativeCondition + str = str.replace(/\$N([^:]*):([^;]*);/g, '$' + (negate > 0 ? 2 : 1)); + + // resolve vars + return str.replace(/\$C(\d+)([^:]*):([^;]*);/g, (_, i, y, n) => (i > 0 ? y : n)); + } + + function _createTab() + { + var buff = ''; + + // tabs for conditionsTypes + for (g in _conditions) + { + if (!g_condition_sources[g]) + continue; + + let k = 0; + for (h in _conditions[g]) + { + var srcGroup, srcEntry, srcId, target, + targets, desc, + nGroups = Object.keys(_conditions[g][h]).length, + curGroup = 1; + + [srcGroup, srcEntry, srcId, target] = h.split(':').map((x) => parseInt(x)); + [targets, desc] = g_condition_sources[g]; + + // resolve targeting + let src = desc.replace(/\$T([^:]*):([^;]*);/, (_, t1, t2) => (target ? t2 : t1).replace('%', targets[target])); + let rand = $WH.rs(); + + buff += '[h3][toggler' + (k ? '=hidden' : '') + ' id=' + rand + ']' + $WH.sprintfa(src, srcGroup, srcEntry, srcId) + '[/toggler][/h3][div' + (k++ ? '=hidden' : '') + ' id=' + rand + ']'; + + if (nGroups > 1) + { + buff += LANG.note_condition_group + '[br][br]'; + buff += '[table class=grid]'; + } + + // table for elseGroups + for (i in _conditions[g][h]) + { + var group = _conditions[g][h][i], + nEntries = Object.keys(_conditions[g][h][i]).length; + + if (nGroups <= 1 && nEntries > 1) + buff += '[div style="padding-left:15px"]' + LANG.note_condition + '[/div]'; + if (nGroups > 1) + buff += '[tr][td width=70px valign=middle align=center]' + LANG.group + ' ' + (curGroup++) + LANG.colon + '[/td][td]'; + + // individual conditions + buff += '[ol]'; + for (j in group) + buff += '[li]' + _parseEntry(group[j], targets, target) + '[/li]'; + buff += '[/ol]'; + + if (nGroups > 1) + buff += '[/td][/tr]'; + } + + if (nGroups > 1) + buff += '[/tr][/table]'; + + buff += '[/div]'; + } + } + + return buff; + } + + function _createCell() + { + var rows = []; + + // tabs for conditionsTypes + for (let g in _conditions) + { + if (!g_condition_sources[g]) + continue; + + for (let h in _conditions[g]) + { + var target, targets, + + [, , , target] = h.split(':').map((x) => parseInt(x)); + [targets, ] = g_condition_sources[g]; + + let nElseGroups = Object.keys(_conditions[g][h]).length + + // table for elseGroups + for (let i in _conditions[g][h]) + { + let subGroup = [], + group = _conditions[g][h][i], + nEntries = Object.keys(_conditions[g][h][i]).length + buff = ''; + + if (nElseGroups > 1) + { + let rand = $WH.rs(); + buff += '[toggler' + (i > 0 ? '=hidden' : '') + ' id=cell-' + rand + ']' + (i > 0 ? LANG.cnd_or : LANG.cnd_either) + '[/toggler][div' + (i > 0 ? '=hidden' : '') + ' id=cell-' + rand + ']'; + } + + // individual conditions + for (let j in group) + subGroup.push(_parseEntry(group[j], targets, target)); + + for (j in subGroup) + { + if (nEntries > 1 && j > 0 && j == subGroup.length - 1) + buff += LANG.and + '[br]'; + else if (nEntries > 1 && j > 0) + buff += ',[br]'; + + buff += subGroup[j]; + } + + if (nElseGroups > 1) + buff += '[/div]'; + + rows.push(buff); + } + } + } + + return rows.length > 1 ? rows.join('[br]') : rows[0]; + } + +} +/* end custom */ diff --git a/setup/tools/filegen/templates/global.js/contacttool.js b/setup/tools/filegen/templates/global.js/contacttool.js new file mode 100644 index 00000000..516bc733 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/contacttool.js @@ -0,0 +1,550 @@ +var ContactTool = new function() +{ + this.general = 0; + this.comment = 1; + this.post = 2; + this.screenshot = 3; + this.character = 4; + this.video = 5; + this.guide = 6; + + var _dialog; + + var contexts = { + 0: [ // general + [1, true], // General feedback + [2, true], // Bug report + [8, true], // Article misinformation + [3, true], // Typo/mistranslation + [4, true], // Advertise with us + [5, true], // Partnership opportunities + [6, true], // Press inquiry + [7, true] // Other + ], + 1: [ // comment + [15, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Advertising + [16, true], // Inaccurate + [17, true], // Out of date + [18, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Spam + [19, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [20, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 2: [ // forum post + [30, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Advertising + [37, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0 && (post.roles & U_GROUP_MODERATOR) == 0 && g_users[post.user].avatar == 2); }], // Avatar + [31, true], // Inaccurate + [32, true], // Out of date + [33, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Spam + [34, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0 && post.op && !post.sticky); }], // Sticky request + [35, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [36, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0);}] // Other + ], + 3: [ // screenshot + [45, true], // Inaccurate, + [46, true], // Out of date, + [47, function(screen) { return (g_users && g_users[screen.user] && (g_users[screen.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [48, function(screen) { return (g_users && g_users[screen.user] && (g_users[screen.user].roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 4: [ // character + [60, true], // Inaccurate completion data + [61, true] // Other + ], + 5: [ // video + [45, true], // Inaccurate, + [46, true], // Out of date, + [47, function(video) { return (g_users && g_users[video.user] && (g_users[video.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [48, function(video) { return (g_users && g_users[video.user] && (g_users[video.user].roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 6: [ // Guide + [45, true], // Inaccurate, + [46, true], // Out of date, + [48, true] // Other + ] + }; + + var errors = { + 1: LANG.ct_resp_error1, + 2: LANG.ct_resp_error2, + 3: LANG.ct_resp_error3, + 7: LANG.ct_resp_error7 + }; + + var oldHash = null; + + this.displayError = function(field, message) + { + alert(message); + } + + this.onShow = function() + { + if (location.hash && location.hash != '#contact') + oldHash = location.hash; + if (this.data.mode == 0) + location.replace('#contact'); + } + + this.onHide = function() + { + if (oldHash && (oldHash.indexOf('screenshots:') == -1 || oldHash.indexOf('videos:') == -1)) + location.replace(oldHash); + else + location.replace('#.'); + } + + this.onSubmit = function(data, button, form) + { + if (data.submitting) + return false; + + for (var i = 0; i < form.elements.length; ++i) + form.elements[i].disabled = true; + + var params = [ + 'contact=1', + 'mode=' + $WH.urlencode(data.mode), + 'reason=' + $WH.urlencode(data.reason), + 'desc=' + $WH.urlencode(data.description), + 'ua=' + $WH.urlencode(navigator.userAgent), + 'appname=' + $WH.urlencode(navigator.appName), + 'page=' + $WH.urlencode(data.currenturl) + ]; + + if (data.mode == 0) // contact us + { + if (data.relatedurl) + params.push('relatedurl=' + $WH.urlencode(data.relatedurl)); + if (data.email) + params.push('email=' + $WH.urlencode(data.email)); + } + else if (data.mode == 1) // comment + params.push('id=' + $WH.urlencode(data.comment.id)); + else if (data.mode == 2) // forum post + params.push('id=' + $WH.urlencode(data.post.id)); + else if (data.mode == 3) // screenshot + params.push('id=' + $WH.urlencode(data.screenshot.id)); + else if (data.mode == 4) // character + params.push('id=' + $WH.urlencode(data.profile.source)); + else if (data.mode == 5) // video + params.push('id=' + $WH.urlencode(data.video.id)); + else if (data.mode == 6) // guide + params.push('id=' + $WH.urlencode(data.guide.id)); + + data.submitting = true; + var url = '?contactus'; + new Ajax(url, { + method: 'POST', + params: params.join('&'), + onSuccess: function(xhr, opt) { + var resp = xhr.responseText; + if (resp == 0) + { + if (g_user.name) + alert($WH.sprintf(LANG.ct_dialog_thanks_user, g_user.name)); + else + alert(LANG.ct_dialog_thanks); + + Lightbox.hide(); + } + else + { + if (errors[resp]) + alert(errors[resp]); + else + alert('Error: ' + resp); + } + }, + onFailure: function(xhr, opt) { + alert('Failure submitting contact request: ' + xhr.statusText); + }, + onComplete: function(xhr, opt) { + for (var i = 0; i < form.elements.length; ++i) + form.elements[i].disabled = false; + + data.submitting = false; + } + }); + return false; + } + + this.show = function(opt) + { + if (!opt) + opt = {}; + + var data = { mode: 0 }; + $WH.cO(data, opt); + data.reasons = contexts[data.mode]; + if (location.href.indexOf('#contact') != -1) + data.currenturl = location.href.substr(0, location.href.indexOf('#contact')); + else + data.currenturl = location.href; + + var form = 'contactus'; + if (data.mode != 0) + form = 'reportform'; + + if (!_dialog) + { + this.init(); + } + + _dialog.show(form, { + data: data, + onShow: this.onShow, + onHide: this.onHide, + onSubmit: this.onSubmit + }) + } + + this.checkPound = function() + { + if (location.hash && location.hash == '#contact') + { + ContactTool.show(); + } + } + + var dialog_contacttitle = LANG.ct_dialog_contactwowhead; + + this.init = function() + { + _dialog = new Dialog(); + + Dialog.templates.contactus = { + title: dialog_contacttitle, + width: 550, + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + + fields: [ + { + id: 'reason', + type: 'select', + label: LANG.ct_dialog_reason, + required: 1, + options: [], + compute: function(field, value, form, td) + { + $WH.ee(field); + + for (var i = 0; i < this.data.reasons.length; ++i) + { + var id = this.data.reasons[i][0]; + var check = this.data.reasons[i][1]; + var valid = false; + if (typeof check == 'function') + valid = check(this.extra); + else + valid = check; + + if (!valid) + continue; + + var o = $WH.ce('option'); + o.value = id; + if (value && value == id) + o.selected = true; + + $WH.ae(o, $WH.ct(g_contact_reasons[id])); + $WH.ae(field, o); + } + + field.onchange = function() + { + if (this.value == 1 || this.value == 2 || this.value == 3) + { + form.currenturl.parentNode.parentNode.style.display = ''; + form.relatedurl.parentNode.parentNode.style.display = ''; + } + else + { + form.currenturl.parentNode.parentNode.style.display = 'none'; + form.relatedurl.parentNode.parentNode.style.display = 'none'; + } + }.bind(field); + + td.style.width = '98%'; + }, + validate: function(newValue, data, form) + { + var error = ''; + if (!newValue || newValue.length == 0) + error = LANG.ct_dialog_error_reason; + + if (error == '') + return true; + + ContactTool.displayError(form.reason, error); + form.reason.focus(); + return false; + } + }, + { + id: 'currenturl', + type: 'text', + disabled: true, + label: LANG.ct_dialog_currenturl, + size: 40 + }, + { + id: 'relatedurl', + type: 'text', + label: LANG.ct_dialog_relatedurl, + caption: LANG.ct_dialog_optional, + size: 40, + validate: function(newValue, data, form) + { + var error = ''; + var urlRe = /^(http(s?)\:\/\/|\/)?([\w]+:\w+@)?([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))(:[\d]{1,5})?((\/?\w+\/)+|\/?)(\w+\.[\w]{3,4})?((\?\w+=\w+)?(&\w+=\w+)*)?/; + newValue = newValue.trim(); + if (newValue.length >= 250) + error = LANG.ct_dialog_error_relatedurl; + else if (newValue.length > 0 && !urlRe.test(newValue)) + error = LANG.ct_dialog_error_invalidurl; + + if (error == '') + return true; + + ContactTool.displayError(form.relatedurl, error); + form.relatedurl.focus(); + return false; + } + }, + { + id: 'email', + type: 'text', + label: LANG.ct_dialog_email, + caption: LANG.ct_dialog_email_caption, + compute: function(field, value, form, td, tr) + { + if (g_user.email) + { + this.data.email = g_user.email; + tr.style.display = 'none'; + } + else + { + var func = function() + { + $('#contact-emailwarn').css('display', g_isEmailValid($(form.email).val()) ? 'none' : ''); + Lightbox.reveal(); + }; + + $(field).keyup(func).blur(func); + } + }, + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length >= 100) + error = LANG.ct_dialog_error_emaillen; + else if (newValue.length > 0 && !g_isEmailValid(newValue)) + error = LANG.ct_dialog_error_email; + + if (error == '') + return true; + + ContactTool.displayError(form.email, error); + form.email.focus(); + return false; + } + }, + { + id: 'description', + type: 'textarea', + caption: LANG.ct_dialog_desc_caption, + width: '98%', + required: 1, + size: [10, 30], + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length == 0 || newValue.length > 10000) + error = LANG.ct_dialog_error_desc; + + if (error == '') + return true; + + ContactTool.displayError(form.description, error); + form.description.focus(); + return false; + } + }, + { + id: 'noemailwarning', + type: 'caption', + compute: function(field, value, form, td) + { + $(td).html('').css('white-space', 'normal').css('padding', '0 4px'); + } + } + ], + + onInit: function(form) + { + + }, + + onShow: function(form) + { + if (this.data.focus && form[this.data.focus]) + setTimeout(g_setCaretPosition.bind(null, form[this.data.focus], form[this.data.focus].value.length), 100); + else if (form['reason'] && !form.reason.value) + setTimeout($WH.bindfunc(form.reason.focus, form.reason), 10); + else if (form['relatedurl'] && !form.relatedurl.value) + setTimeout($WH.bindfunc(form.relatedurl.focus, form.relatedurl), 10); + else if (form['email'] && !form.email.value) + setTimeout($WH.bindfunc(form.email.focus, form.email), 10); + else if (form['description'] && !form.description.value) + setTimeout($WH.bindfunc(form.description.focus, form.description), 10); + + setTimeout(Lightbox.reveal, 250); + } + } + + Dialog.templates.reportform = { + title: LANG.ct_dialog_report, + width: 550, + // height: 360, + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + fields: [ + { + id: 'reason', + type: 'select', + label: LANG.ct_dialog_reason, + options: [], + compute: function(field, value, form, td) + { + switch (this.data.mode) + { + case 1: // comment + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportcomment, '' + this.data.comment.user + ''); + break; + case 2: // forum post + var rep = '' + this.data.post.user + ''; + if (this.data.post.op) + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reporttopic, rep); + else + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportpost, rep); + break; + case 3: // screenshot + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportscreen, '' + this.data.screenshot.user + ''); + break; + case 4: // character + $WH.ee(form.firstChild); + $WH.ae(form.firstChild, $WH.ct(LANG.ct_dialog_reportchar)); + break; + case 5: // video + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportvideo, '' + this.data.video.user + ''); + break; + case 6: // guide + form.firstChild.innerHTML = 'Report guide'; + break; + } + form.firstChild.setAttribute('style', ''); + + $WH.ee(field); + + var extra; + if (this.data.mode == 1) + extra = this.data.comment; + else if (this.data.mode == 2) + extra = this.data.post; + else if (this.data.mode == 3) + extra = this.data.screenshot; + else if (this.data.mode == 4) + extra = this.data.profile; + else if (this.data.mode == 5) + extra = this.data.video; + else if (this.data.mode == 6) + extra = this.data.guide; + + $WH.ae(field, $WH.ce('option', { selected: (!value), value: -1 })); + + for (var i = 0; i < this.data.reasons.length; ++i) + { + var id = this.data.reasons[i][0]; + var check = this.data.reasons[i][1]; + var valid = false; + if (typeof check == 'function') + valid = check(extra); + else + valid = check; + + if (!valid) + continue; + + var o = $WH.ce('option'); + o.value = id; + if (value && value == id) + o.selected = true; + + $WH.ae(o, $WH.ct(g_contact_reasons[id])); + $WH.ae(field, o); + } + + td.style.width = '98%'; + }, + validate: function(newValue, data, form) + { + var error = ''; + if (!newValue || newValue == -1 || newValue.length == 0) + error = LANG.ct_dialog_error_reason; + + if (error == '') + return true; + + ContactTool.displayError(form.reason, error); + form.reason.focus(); + return false; + } + }, + { + id: 'description', + type: 'textarea', + caption: LANG.ct_dialog_desc_caption, + width: '98%', + required: 1, + size: [10, 30], + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length == 0 || newValue.length > 10000) + error = LANG.ct_dialog_error_desc; + + if (error == '') + return true; + + ContactTool.displayError(form.description, error); + form.description.focus(); + return false; + } + } + ], + + onInit: function(form) + { + + }, + + onShow: function(form) + { + /* Work-around for IE7 */ + var reason = $(form).find("*[name=reason]")[0]; + var description = $(form).find("*[name=description]")[0]; + + if (this.data.focus && form[this.data.focus]) + setTimeout(g_setCaretPosition.bind(null, form[this.data.focus], form[this.data.focus].value.length), 100); + else if (!reason.value) + setTimeout($WH.bindfunc(reason.focus, reason), 10); + else if (!description.value) + setTimeout($WH.bindfunc(description.focus, description), 10); + } + } + } + + $(document).ready(this.checkPound); +}; diff --git a/setup/tools/filegen/templates/global.js/cookies.js b/setup/tools/filegen/templates/global.js/cookies.js new file mode 100644 index 00000000..3cce1d3e --- /dev/null +++ b/setup/tools/filegen/templates/global.js/cookies.js @@ -0,0 +1,37 @@ +// TODO: Create a "Cookies" object + +function g_cookiesEnabled() +{ + document.cookie = 'enabledTest'; + return (document.cookie.indexOf("enabledTest") != -1) ? true : false; +} + +function g_getWowheadCookie(name) +{ + if (g_user.id > 0) + { + return g_user.cookies[name]; // no point checking if it exists, as undefined tests as false anyways + } + else + { + return $WH.gc(name); // plus gc does the same thing.. + } +} + +function g_setWowheadCookie(name, data, browser) +{ + var temp = name.substr(0, 5) == 'temp_'; + if (!browser && g_user.id > 0 && !temp) { + new Ajax('?cookie=' + name + '&' + name + '=' + $WH.urlencode(data), { + method: 'get', + onSuccess: function(xhr) { + if (xhr.responseText == 0) + g_user.cookies[name] = data; + } + }); + } + else if (browser || g_user.id == 0) + { + $WH.sc(name, 14, data, null, location.hostname); + } +} diff --git a/setup/tools/filegen/templates/global.js/dialog.js b/setup/tools/filegen/templates/global.js/dialog.js new file mode 100644 index 00000000..e46fa3a4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/dialog.js @@ -0,0 +1,568 @@ +var Dialog = function() +{ +var + _self = this, + _template, + _onSubmit = null, + _templateName, + + _funcs = {}, + _data, + + _inited = false, + _form = $WH.ce('form'), + _elements = {}; + + _form.onsubmit = function() { + _processForm(); + return false + }; + + this.show = function(template, opt) + { + if (template) + { + _templateName = template; + _template = Dialog.templates[_templateName]; + + _self.template = _template; + } + else + return; + + if (_template.onInit && !_inited) + (_template.onInit.bind(_self, _form, opt))(); + + if (opt.onBeforeShow) + _funcs.onBeforeShow = opt.onBeforeShow.bind(_self, _form); + + if (_template.onBeforeShow) + _template.onBeforeShow = _template.onBeforeShow.bind(_self, _form); + + if (opt.onShow) + _funcs.onShow = opt.onShow.bind(_self, _form); + + if (_template.onShow) + _template.onShow = _template.onShow.bind(_self, _form); + + if (opt.onHide) + _funcs.onHide = opt.onHide.bind(_self, _form); + + if (_template.onHide) + _template.onHide = _template.onHide.bind(_self, _form); + + if (opt.onSubmit) + _funcs.onSubmit = opt.onSubmit; + + if (_template.onSubmit) + _onSubmit = _template.onSubmit.bind(_self, _form); + + if (opt.data) + { + _inited = false; + _data = {}; + $WH.cO(_data, opt.data); + } + + _self.data = _data; + + Lightbox.show('dialog-' + _templateName, { + onShow: _onShow, + onHide: _onHide + }); + } + + this.getValue = function(id) + { + return _getValue(id); + } + + this.setValue = function(id, value) + { + _setValue(id, value); + } + + this.getSelectedValue = function(id) + { + return _getSelectedValue(id); + } + + this.getCheckedValue = function(id) + { + return _getCheckedValue(id); + } + + function _onShow(dest, first) + { + if (first || !_inited) + _initForm(dest); + + if (_template.onBeforeShow) + _template.onBeforeShow(); + + if (_funcs.onBeforeShow) + _funcs.onBeforeShow(); + + Lightbox.setSize(_template.width, _template.height); + dest.className = 'dialog'; + + _updateForm(); + + if (_template.onShow) + _template.onShow(); + + if (_funcs.onShow) + _funcs.onShow(); + } + + function _initForm(dest) + { + $WH.ee(dest); + $WH.ee(_form); + + var container = $WH.ce('div'); + container.className = 'text'; + $WH.ae(dest, container); + + $WH.ae(container, _form); + + if (_template.title) + { + var h = $WH.ce('h1'); + $WH.ae(h, $WH.ct(_template.title)); + $WH.ae(_form, h); + } + + var t = $WH.ce('table'), + tb = $WH.ce('tbody'), + mergeCell = false; + + $WH.ae(t, tb); + $WH.ae(_form, t); + + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + element; + + if (!mergeCell) + { + tr = $WH.ce('tr'); + th = $WH.ce('th'); + td = $WH.ce('td'); + } + + field.__tr = tr; + + if (_data[field.id] == null) + _data[field.id] = (field.value ? field.value : ''); + + var options; + if (field.options) + { + options = []; + + if (field.optorder) + $WH.cO(options, field.optorder); + else + { + for (var j in field.options) + options.push(j); + } + + if (field.sort) + options.sort(function(a, b) { return field.sort * $WH.strcmp(field.options[a], field.options[b]); }); + } + + switch (field.type) + { + case 'caption': + + th.colSpan = 2; + th.style.textAlign = 'left'; + th.style.padding = 0; + + if (field.compute) + (field.compute.bind(_self, null, _data[field.id], _form, th, tr))(); + else if (field.label) + $WH.ae(th, $WH.ct(field.label)); + + $WH.ae(tr, th); + $WH.ae(tb, tr); + + continue; + break; + + case 'textarea': + + var f = element = $WH.ce('textarea'); + + f.name = field.id; + + if (field.disabled) + f.disabled = true; + + f.rows = field.size[0]; + f.cols = field.size[1]; + + td.colSpan = 2; + + if (field.label) + { + th.colSpan = 2; + th.style.textAlign = 'left'; + th.style.padding = 0; + td.style.padding = 0; + + $WH.ae(th, $WH.ct(field.label)); + $WH.ae(tr, th); + $WH.ae(tb, tr); + + tr = $WH.ce('tr'); + } + + $WH.ae(td, f); + + break; + + case 'select': + + var f = element = $WH.ce('select'); + + f.name = field.id; + + if (field.size) + f.size = field.size; + + if (field.disabled) + f.disabled = true; + + if (field.multiple) + f.multiple = true; + + for (var j = 0, len2 = options.length; j < len2; ++j) + { + var o = $WH.ce('option'); + + o.value = options[j]; + + $WH.ae(o, $WH.ct(field.options[options[j]])); + $WH.ae(f, o) + } + + $WH.ae(td, f); + + break; + + case 'dynamic': + + td.colSpan = 2; + td.style.textAlign = 'left'; + td.style.padding = 0; + + if (field.compute) + (field.compute.bind(_self, null, _data[field.id], _form, td, tr))(); + + $WH.ae(tr, td); + $WH.ae(tb, tr); + + element = td; + + break; + + case 'checkbox': + case 'radio': + + var k = 0; + element = []; + for (var j = 0, len2 = options.length; j < len2; ++j) + { + var + s = $WH.ce('span'), + f, + l, + uniqueId = 'sdfler46' + field.id + '-' + options[j]; + + if (j > 0 && !field.noInputBr) + $WH.ae(td, $WH.ce('br')); + + l = $WH.ce('label'); + l.setAttribute('for', uniqueId); + l.onmousedown = $WH.rf; + + f = $WH.ce('input', { name: field.id, value: options[j], id: uniqueId }); + f.setAttribute('type', field.type); + + if (field.disabled) + f.disabled = true; + + if (field.submitOnDblClick) + l.ondblclick = f.ondblclick = function(e) { _processForm(); }; + + if (field.compute) + (field.compute.bind(_self, f, _data[field.id], _form, td, tr))(); + + $WH.ae(l, f); + $WH.ae(l, $WH.ct(field.options[options[j]])); + $WH.ae(td, l); + + element.push(f); + } + + break; + + default: // Textbox + + var f = element = $WH.ce('input'); + + f.name = field.id; + + if (field.size) + f.size = field.size; + + if (field.disabled) + f.disabled = true; + + if (field.submitOnEnter) + { + f.onkeypress = function(e) { + e = $WH.$E(e); + if (e.keyCode == 13) + _processForm(); + }; + } + + f.setAttribute('type', field.type); + + $WH.ae(td, f); + + break; + } + + if (field.label) + { + if (field.type == 'textarea') + { + if (field.labelAlign) + td.style.textAlign = field.labelAlign; + + td.colSpan = 2; + } + else + { + if (field.labelAlign) + th.style.textAlign = field.labelAlign; + + $WH.ae(th, $WH.ct(field.label)); + $WH.ae(tr, th); + } + } + + if (field.placeholder) + f.placeholder = field.placeholder; + + if (field.type != 'checkbox' && field.type != 'radio') + { + if (field.width) + f.style.width = field.width; + + if (field.compute && field.type != 'caption' && field.type != 'dynamic') + (field.compute.bind(_self, f, _data[field.id], _form, td, tr))(); + } + + if (field.caption) + { + var s = $WH.ce('small'); + if (field.type != 'textarea') + s.style.paddingLeft = '2px'; + s.className = 'q0'; // commented in 5.0? + $WH.ae(s, $WH.ct(field.caption)); + $WH.ae(td, s); + } + + $WH.ae(tr, td); + $WH.ae(tb, tr); + + mergeCell = field.mergeCell; + + _elements[field.id] = element; + } + + for (var i = _template.buttons.length; i > 0; --i) + { + var + button = _template.buttons[i - 1], + a = $WH.ce('a'); + + a.onclick = _processForm.bind(a, button[0]); + a.className = 'dialog-' + button[0]; + a.href = 'javascript:;'; + $WH.ae(a, $WH.ct(button[1])); + $WH.ae(dest, a); + } + + var _ = $WH.ce('div'); + _.className = 'clear'; + $WH.ae(dest, _); + + _inited = true; + } + + function _updateForm() + { + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + f = _elements[field.id]; + + switch (field.type) + { + case 'caption': // Do nothing + break; + + case 'select': + for (var j = 0, len2 = f.options.length; j < len2; j++) + f.options[j].selected = (f.options[j].value == _data[field.id] || $WH.in_array(_data[field.id], f.options[j].value) != -1); + break; + + case 'checkbox': + case 'radio': + for (var j = 0, len2 = f.length; j < len2; j++) + f[j].checked = (f[j].value == _data[field.id] || $WH.in_array(_data[field.id], f[j].value) != -1); + break; + + default: + f.value = _data[field.id]; + break; + } + + if (field.update) + (field.update.bind(_self, null, _data[field.id], _form, f))(); + } + } + + function _onHide() + { + if (_template.onHide) + _template.onHide(); + + if (_funcs.onHide) + _funcs.onHide(); + } + + function _processForm(button) + { + // if (button == 'x') // aowow - button naming differs + if (button == 'cancel') // Special case + return Lightbox.hide(); + + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + newValue; + + switch (field.type) + { + case 'caption': // Do nothing + continue; + + case 'select': + newValue = _getSelectedValue(field.id); + break; + + case 'checkbox': + case 'radio': + newValue = _getCheckedValue(field.id); + break; + + case 'dynamic': + if (field.getValue) + { + newValue = field.getValue(field, _data, _form); + break; + } + default: + newValue = _getValue(field.id); + break; + } + + if (field.validate) + { + if (!field.validate(newValue, _data, _form)) + return; + } + + if (newValue && typeof newValue == 'string') + newValue = $WH.trim(newValue); + + _data[field.id] = newValue; + } + + _submitData(button); + } + + function _submitData(button) + { + var ret; + + if (_onSubmit) + ret = _onSubmit(_data, button, _form); + + if (_funcs.onSubmit) + ret = _funcs.onSubmit(_data, button, _form); + + if (ret === undefined || ret) + Lightbox.hide(); + + return false; + } + + function _getValue(id) + { + return _elements[id].value; + } + + function _setValue(id, value) + { + _elements[id].value = value; + } + + function _getSelectedValue(id) + { + var + result = [], + f = _elements[id]; + + for (var i = 0, len = f.options.length; i < len; i++) + { + if (f.options[i].selected) + result.push(parseInt(f.options[i].value) == f.options[i].value ? parseInt(f.options[i].value) : f.options[i].value); + } + + if (result.length == 1) + result = result[0]; + + return result; + } + + function _getCheckedValue(id) + { + var + result = [], + f = _elements[id]; + + for (var i = 0, len = f.length; i < len; i++) + { + if (f[i].checked) + result.push(parseInt(f[i].value) == f[i].value ? parseInt(f[i].value) : f[i].value); + } + + return result; + } +}; + +Dialog.templates = {}; +Dialog.extraFields = {}; diff --git a/setup/tools/filegen/templates/global.js/dom_manipulation.js b/setup/tools/filegen/templates/global.js/dom_manipulation.js new file mode 100644 index 00000000..787c45e9 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/dom_manipulation.js @@ -0,0 +1,252 @@ +/* +Global functions related to DOM manipulation, events & forms that jQuery doesn't already provide +*/ + +function g_addCss(css) +{ + var style = $WH.ce('style'); + style.type = 'text/css'; + + if (style.styleSheet) // ie + style.styleSheet.cssText = css; + else + $WH.ae(style, $WH.ct(css)); + + var head = $WH.gE(document, 'head')[0]; + $WH.ae(head, style); +} + +function g_setTextNodes(n, text) +{ + if (n.nodeType == 3) + n.nodeValue = text; + else + { + for (var i = 0; i < n.childNodes.length; ++i) + g_setTextNodes(n.childNodes[i], text); + } +} + +function g_setInnerHtml(n, text, nodeType) +{ + if (n.nodeName.toLowerCase() == nodeType) + n.innerHTML = text; + else + { + for (var i = 0; i < n.childNodes.length; ++i) + g_setInnerHtml(n.childNodes[i], text, nodeType); + } +} + +function g_getFirstTextContent(node) +{ + for (var i = 0; i < node.childNodes.length; ++i) + { + if (node.childNodes[i].nodeName == '#text') + return node.childNodes[i].nodeValue; + + var ret = g_getFirstTextContent(node.childNodes[i]); + if (ret) + return ret; + } + + return false; +} + +function g_getTextContent(el) +{ + var txt = ''; + for (var i = 0; i < el.childNodes.length; ++i) + { + if (el.childNodes[i].nodeValue) + txt += el.childNodes[i].nodeValue; + else if (el.childNodes[i].nodeName == 'BR') + txt += '\n'; + + txt += g_getTextContent(el.childNodes[i]); + } + + return txt; +} + +function g_toggleDisplay(el) +{ + el = $(el); + el.toggle(); + if (el.is(':visible')) + return true; + + return false; +} + +function g_enableScroll(enabled) +{ + if (!enabled) + { + $WH.aE(document, 'mousewheel', g_enableScroll.F); + $WH.aE(window, 'DOMMouseScroll', g_enableScroll.F); + } + else + { + $WH.dE(document, 'mousewheel', g_enableScroll.F); + $WH.dE(window, 'DOMMouseScroll', g_enableScroll.F); + } +} + +g_enableScroll.F = function(e) +{ + if (e.stopPropagation) + e.stopPropagation(); + if (e.preventDefault) + e.preventDefault(); + + e.returnValue = false; + e.cancelBubble = true; + + return false; +}; + +// from http://blog.josh420.com/archives/2007/10/setting-cursor-position-in-a-textbox-or-textarea-with-javascript.aspx +function g_setCaretPosition(elem, caretPos) +{ + if (!elem) + return; + + if (elem.createTextRange) + { + var range = elem.createTextRange(); + range.move('character', caretPos); + range.select(); + } + else if (elem.selectionStart != undefined) + { + elem.focus(); + elem.setSelectionRange(caretPos, caretPos); + } + else + elem.focus(); +} + +function g_insertTag(where, tagOpen, tagClose, repFunc) +{ + var n = $WH.ge(where); + + n.focus(); + if (n.selectionStart != null) + { + var s = n.selectionStart, + e = n.selectionEnd, + sL = n.scrollLeft, + sT = n.scrollTop; + + var selectedText = n.value.substring(s, e); + if (typeof repFunc == 'function') + selectedText = repFunc(selectedText); + + n.value = n.value.substr(0, s) + tagOpen + selectedText + tagClose + n.value.substr(e); + n.selectionStart = n.selectionEnd = e + tagOpen.length; + + n.scrollLeft = sL; + n.scrollTop = sT; + } + else if (document.selection && document.selection.createRange) + { + var range = document.selection.createRange(); + + if (range.parentElement() != n) + return; + + var selectedText = range.text; + if (typeof repFunc == 'function') + selectedText = repFunc(selectedText); + + range.text = tagOpen + selectedText + tagClose; +/* + range.moveEnd("character", -tagClose.length); + range.moveStart("character", range.text.length); + + range.select(); +*/ + } + + if (n.onkeyup) + n.onkeyup(); +} + +function g_onAfterTyping(input, func, delay) +{ + var timerId; + var ldsgksdgnlk623 = function() + { + if (timerId) + { + clearTimeout(timerId); + timerId = null; + } + timerId = setTimeout(func, delay); + }; + input.onkeyup = ldsgksdgnlk623; +} + +function g_onClick(el, func) +{ + var firstEvent = 0; + + function rightClk(n) + { + if (firstEvent) + { + if (firstEvent != n) + return; + } + else + firstEvent = n; + + func(true); + } + + el.onclick = function(e) + { + e = $WH.$E(e); + + if (e._button == 2) // middle click + return true; + + return false; + } + + el.oncontextmenu = function() + { + rightClk(1); + + return false; + } + + el.onmouseup = function(e) + { + e = $WH.$E(e); + + if (e._button == 3 || e.shiftKey || e.ctrlKey) // Right/Shift/Ctrl + { + rightClk(2); + } + else if (e._button == 1) // Left + { + func(false); + } + + return false; + } +} + +function g_isLeftClick(e) +{ + e = $WH.$E(e); + return (e && e._button == 1); +} + +function g_preventEmptyFormSubmission() // Used on the homepage and in the top bar +{ + if (!$.trim(this.elements[0].value)) + return false; +} diff --git a/setup/tools/filegen/templates/global.js/favorites.js b/setup/tools/filegen/templates/global.js/favorites.js new file mode 100644 index 00000000..2470302f --- /dev/null +++ b/setup/tools/filegen/templates/global.js/favorites.js @@ -0,0 +1,262 @@ +var Favorites = new function() +{ + var _type = null; + var _typeId = null; + var _favIcon = null; + + this.pageInit = function(h1, type, typeId) + { + if (typeof h1 == 'string') + { + if (!document.querySelector) + return; + + h1 = document.querySelector(h1); + } + + if (!h1 || typeof type != 'number' || typeof typeId != 'number') + return; + + _type = type; + _typeId = typeId; + + createIcon(h1); + } + + function initFavIcon() + { + var h1 = typeof g_pageInfo == 'object' && typeof g_pageInfo.type == 'number' && typeof g_pageInfo.typeId == 'number' ? document.querySelector('#main-contents h1') : null; + if (!h1) { + if (document.readyState !== 'complete') + setTimeout(initFavIcon, 9); + + return; + } + + _type = g_pageInfo.type; + _typeId = g_pageInfo.typeId; + + createIcon(h1); + } + + this.hasFavorites = function() + { + return !!g_favorites.length + } + + this.getMenu = function() + { + var favMenu = []; + var nGroups = 0; + var nEntries = 0; + + for (var i = 0, favGroup; favGroup = g_favorites[i]; i++) + { + if (!favGroup.entities.length) + continue; + + nGroups++; + var subMenu = []; + for (var j = 0, favEntry; favEntry = favGroup.entities[j]; j++) + { + subMenu.push([favEntry[0], favEntry[1], '?' + g_types[favGroup.id] + '=' + favEntry[0]]); + nEntries++ + } + + Menu.sort(subMenu); + favMenu.push([favGroup.id, LANG.types[favGroup.id][2], , subMenu]) + } + + Menu.sort(favMenu); + + // display short favorites as 1-dim list + if ((nGroups == 1 && nEntries <= 45) || (nGroups == 2 && nGroups + nEntries <= 30) || (nGroups > 2 && nGroups + nEntries <= 15)) + { + var list = []; + + for (var i = 0; subMenu = favMenu[i]; i++) + { + list.push([, subMenu[MENU_IDX_NAME]]); + + for (var j = 0, subEntry; subEntry = subMenu[MENU_IDX_SUB][j]; j++) + { + var listEntry = [subEntry[MENU_IDX_ID], subEntry[MENU_IDX_NAME], subEntry[MENU_IDX_URL]]; + + if (subEntry[MENU_IDX_OPT]) + listEntry[MENU_IDX_OPT] = subEntry[MENU_IDX_OPT]; + + list.push(listEntry); + } + } + + favMenu = list; + } + + return favMenu; + } + + this.refreshMenu = function() + { + var menuRoot = $('#toplinks-favorites'); + if (!menuRoot.length) + return; + + var favMenu = Favorites.getMenu(); + if (!favMenu.length) { + menuRoot.hide(); + return; + } + + Menu.add(menuRoot, favMenu); + menuRoot.show(); + } + + function createIcon(heading) + { + _favIcon = $('', { + 'class': 'fav-star', + mouseout: $WH.Tooltip.hide + }).appendTo(heading); + + if (g_user.id) + { + _favIcon.addClass('fav-star' + (isFaved(_type, _typeId) ? '-1' : '-0')).click((function(type, typeId, name) { + toggleEntry(type, typeId, name); + updateIcon(type, typeId); + $WH.Tooltip.hide(); + }).bind(null, _type, _typeId, heading.textContent.trim().replace(/(.+)<.*/, '$1'))); + + _favIcon.mouseover(function(event) { + var tt = this.className.match(/\bfav-star-0\b/) ? LANG.addtofavorites : LANG.removefromfavorites; + $WH.Tooltip.show(this, tt, false, false, 'q2'); + }); + + } + else + { + _favIcon.addClass('fav-star-0').click(function() { + location.href = "?account=signin"; + $WH.Tooltip.hide(); + }).mouseover(function(event) { + $WH.Tooltip.show(this, LANG.favorites_login + '
    ' + LANG.clicktologin + ''); + }); + } + } + + function updateIcon(type, typeId) + { + if (_favIcon) + { + var rmv = 'fav-star-0'; + var add = 'fav-star-1'; + if (!isFaved(type, typeId)) + { + rmv = 'fav-star-1'; + add = 'fav-star-0'; + } + + _favIcon.removeClass(rmv).addClass(add); + } + } + + function isFaved(type, typeId) + { + var idx = getIndex(type); + if (idx == -1) + return false; + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + if (j[0] == typeId) + return true; + + return false; + } + + function toggleEntry(type, typeId, name) + { + if (isFaved(type, typeId)) + removeEntry(type, typeId); + else + addEntry(type, typeId, name); + } + + function addEntry(type, typeId, name) + { + var idx = getIndex(type, true); + if (idx == -1) + { + /* $WH. */ console.error("Invalid type when adding entity to favorites! Type was:", type); + return; + } + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + { + if (j[0] == typeId) + { + alert(LANG.favorites_duplicate.replace('%s', LANG.types[type][1])); + return; + } + } + + sendUpdate('add', type, typeId); + g_favorites[idx].entities.push([typeId, name]); + Favorites.refreshMenu(); + } + + function removeEntry(type, typeId) + { + var idx = getIndex(type); + if (idx == -1) + return; + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + { + if (j[0] == typeId) + { + sendUpdate('remove', type, typeId); + g_favorites[idx].entities.splice(i, 1); + if (!g_favorites[idx].entities.length) + g_favorites.splice(idx, 1); + + Favorites.refreshMenu(); + return; + } + } + } + + function getIndex(type, createNew) + { + if (!LANG.types[type]) + return -1; + + for (var i = 0, j; j = g_favorites[i]; i++) + if (j.id == type) + return i; + + if (!createNew) + return -1; + + g_favorites.push({ id: type, entities: [] }); + + g_favorites.sort(function(a, b) { return $WH.strcmp(LANG.types[a.id], LANG.types[b.id]) }); + + for (i = 0; j = g_favorites[i]; i++) + if (j.id == type) + return i; + + return -1; + } + + function sendUpdate(method, type, typeId) + { + var data = { + id: typeId, + // sessionKey: g_user.sessionKey + }; + data[method] = type; + $.post('?account=favorites', data); + } + + if (document.querySelector && $WH.localStorage.isSupported()) + initFavIcon(); +}; diff --git a/setup/tools/filegen/templates/global.js/guide.js b/setup/tools/filegen/templates/global.js/guide.js new file mode 100644 index 00000000..254abab5 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/guide.js @@ -0,0 +1,368 @@ +var g_localTime = new Date(); + +/* This function is to get the stars for the vote control for the guides. */ + +function GetStars(stars, ratable, userRating, guideId) +{ + var STARS_MAX = 5; + var averageRating = stars; + + if (userRating) + stars = userRating; + + stars = Math.round(stars*2)/2; + var starsRounded = Math.round(stars); + var ret = $("").addClass('stars').addClass('max-' + STARS_MAX).addClass('stars-' + starsRounded); + + if (!g_user.id) + ratable = false; + + if (ratable) + ret.addClass('ratable'); + + if (userRating) + ret.addClass('rated'); + + /* This is kinda lame but oh well */ + var contents = ''; + + var wbr = '​'; + var tmp = stars; + for (var i = 1; i <= STARS_MAX; ++i) + { + if (tmp < 1 && tmp > 0) + contents += ''; + else + contents += ''; + --tmp; + + contents += '' + wbr + ''; + } + + for (var i = 1; i <= STARS_MAX; ++i) + contents += ''; + + contents += ''; + + ret.append(contents); + + if (ratable) + { + var starNumber = 0; + ret.find('i.clickable').each(function() { var starId = ++starNumber; $(this).click(function() { VoteGuide(guideId, averageRating, starId); }); }) + } + + if (userRating) + { + var clear = $("").addClass('clear').click(function() { VoteGuide(guideId, averageRating, 0); }); + ret.append(clear); + } + + if (stars >= 0) + ret.mouseover(function(event) {$WH.Tooltip.showAtCursor(event, 'Rating: ' + stars + ' / ' + STARS_MAX, 0, 0, 'q');}).mousemove(function(event) {$WH.Tooltip.cursorUpdate(event)}).mouseout(function() {$WH.Tooltip.hide()}); + + return ret; +} + +function VoteGuide(guideId, oldRating, newRating) +{ + // Update stars display + $('#guiderating').html(GetStars(oldRating, true, newRating, guideId)); + + // Vote + $.ajax({cache: false, url: '?guide=vote', type: 'POST', + error: function() { + $('#guiderating').html(GetStars(oldRating, true, 0, guideId)); + alert('Voting failed. Try again later.'); + }, + success: function(json) { + var data = eval('(' + json + ')'); + $('#guiderating-value').text(data.rating); + $('#guiderating-votes').text(GetN5(data.nvotes)); + }, + data: { id: guideId, rating: newRating } + }); +} + +/* g_enhanceTextarea and createOptionsMenuWidget are only ever used by the article/guide editor. Why are they in global.js? */ + +function g_enhanceTextarea (ta, opt) { + if (!(ta instanceof jQuery)) + ta = $(ta); + + if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") + return; + + if (typeof opt != "object") + opt = {}; + + var canResize = (function(el) { + if (!el.dynamicResizeOption) + return true; + + if ($WH.localStorage.get("dynamic-textarea-resizing") === "true") + return true; + + if ($WH.localStorage.get("dynamic-textarea-resizing") === "false") + return false; + + return !el.hasOwnProperty("dynamicSizing") || el.dynamicSizing; + }).bind(null, opt); + + var height = ta.height() || 500; + var wrapper = $("
    ", { "class": "enhanced-textarea-wrapper" }).insertBefore(ta).append(ta); + + if (!opt.hasOwnProperty("color")) + wrapper.addClass("enhanced-textarea-dark"); + else if (opt.color) + wrapper.addClass("enhanced-textarea-" + opt.color); + + if (!opt.hasOwnProperty("dynamicSizing") || opt.dynamicSizing || opt.dynamicResizeOption) { + var expander = $("
    ", { "class": "enhanced-textarea-expander" }).prependTo(wrapper); + var dynamicResize = function(textarea, exactHeight, canResizeFn) { + if (!canResizeFn()) + return; + + // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
    ") + "
    ").height() + (D ? 14 : 34) + "px"); + textarea.css("height", textarea.siblings(".enhanced-textarea-expander").html($WH.htmlentities(textarea.val()) + "
    ").height() + (exactHeight ? 14 : 34) + "px"); + }; + + ta.bind("keydown keyup change", dynamicResize.bind(this, ta, opt.exactLineHeights, canResize)); + dynamicResize(ta, opt.exactLineHeights, canResize); + + var setWidth = function(el) { el.css("width", el.parent().width() + "px"); }; + + setWidth(expander); + setTimeout(setWidth.bind(null, expander), 1); + + if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) + wrapper.addClass("enhanced-textarea-dynamic-sizing"); + } + + if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) + wrapper.addClass("enhanced-textarea-focus-changes"); + + if (opt.markup) { + var _markupMenu = $("
    ", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); + var _segments = $("
    ", { "class": "enhanced-textarea-markup" }).appendTo(_markupMenu); + var _toolbar = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _menu = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + + if (opt.markup == "inline") + ar_AddInlineToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); + else + ar_AddToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); + + if (opt.dynamicResizeOption) { + var _dynResize = $("
    ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _lblDynResize = $("
    />
    @@ -118,9 +103,3 @@ - -
    - - - -brick('footer'); ?> From 8fadce88ad0460a7df05912fdd99a552206572db Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:51:36 +0200 Subject: [PATCH 691/957] Template/Update (Part 46 - III) * account management rework: Recovery Options --- endpoints/account/forgot-password.php | 101 +++++++++++++ endpoints/account/forgot-username.php | 100 +++++++++++++ endpoints/account/resend-submit.php | 52 +++++++ endpoints/account/resend.php | 98 +++++++++++++ endpoints/account/reset-password.php | 121 ++++++++++++++++ template/bricks/inputbox-form-email.tpl.php | 46 ++++++ .../bricks/inputbox-form-password.tpl.php | 78 ++++++++++ template/bricks/inputbox-form-signin.tpl.php | 2 +- template/mails/reset-password_0.tpl | 2 +- template/mails/reset-password_2.tpl | 2 +- template/mails/reset-password_3.tpl | 2 +- template/mails/reset-password_4.tpl | 2 +- template/mails/reset-password_6.tpl | 2 +- template/mails/reset-password_8.tpl | 2 +- template/pages/acc-recover.tpl.php | 136 ------------------ 15 files changed, 603 insertions(+), 143 deletions(-) create mode 100644 endpoints/account/forgot-password.php create mode 100644 endpoints/account/forgot-username.php create mode 100644 endpoints/account/resend-submit.php create mode 100644 endpoints/account/resend.php create mode 100644 endpoints/account/reset-password.php create mode 100644 template/bricks/inputbox-form-email.tpl.php create mode 100644 template/bricks/inputbox-form-password.tpl.php delete mode 100644 template/pages/acc-recover.tpl.php diff --git a/endpoints/account/forgot-password.php b/endpoints/account/forgot-password.php new file mode 100644 index 00000000..4121f47f --- /dev/null +++ b/endpoints/account/forgot-password.php @@ -0,0 +1,101 @@ + display email form + * 2. submit email form > send mail with recovery link + * 3. click recovery link from mail > display password reset form + * 4. submit password reset form > update password + */ + +class AccountforgotpasswordResponse extends TemplateResponse +{ + use TrRecoveryHelper, TrGetNext; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'forgot-password'; + + protected array $expectedPOST = array( + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + // don't redirect logged in users + // you can be forgetful AND logged in + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + $msg = $this->processMailForm(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverPass', [1.5]), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'recoverPass', [1]), + 'error' => $msg, + 'action' => '?account=forgot-password&next='.$this->getNext(), + 'email' => $this->_post['email'] ?? '' + )]; + } + + private function processMailForm() : string + { + // no input yet. show clean email form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); + + // on cooldown pretend we dont know the email address + if ($timeout && $timeout > time()) + return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // pretend recovery started + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) + { + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]); + } + + // recovery actually started + if ($err = $this->startRecovery(ACC_STATUS_RECOVER_PASS, 'reset-password', $this->_post['email'])) + return $err; + + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/forgot-username.php b/endpoints/account/forgot-username.php new file mode 100644 index 00000000..4a6245d4 --- /dev/null +++ b/endpoints/account/forgot-username.php @@ -0,0 +1,100 @@ + display email form + * 2. submit email form > send mail with recovery link + * ( 3. click recovery link from mail to go to signin page (so not on this page) ) + */ + +class AccountforgotusernameResponse extends TemplateResponse +{ + use TrRecoveryHelper; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'forgot-username'; + + protected array $expectedPOST = array( + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + // if the user is looged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + $msg = $this->processMailForm(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverUser'), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'recoverUser'), + 'error' => $msg, + 'action' => '?account=forgot-username' + )]; + } + + private function processMailForm() : string + { + // no input yet. show empty form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); + + // on cooldown pretend we dont know the email address + if ($timeout && $timeout > time()) + return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // pretend recovery started + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) + { + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]); + } + + // recovery actually started + if ($err = $this->startRecovery(ACC_STATUS_RECOVER_USER, 'recover-user', $this->_post['email'])) + return $err; + + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/resend-submit.php b/endpoints/account/resend-submit.php new file mode 100644 index 00000000..c45c0499 --- /dev/null +++ b/endpoints/account/resend-submit.php @@ -0,0 +1,52 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + public function __construct(string $pageParam) + { + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $error = $message = ''; + + if ($this->assertPOST('email')) + $message = Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + else + $error = Lang::main('intError'); + + parent::generate(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => $message, + 'error' => $error + )]; + } +} + +?> diff --git a/endpoints/account/resend.php b/endpoints/account/resend.php new file mode 100644 index 00000000..234aea17 --- /dev/null +++ b/endpoints/account/resend.php @@ -0,0 +1,98 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + // error from account=activate + if (isset($_SESSION['error']['activate'])) + { + $msg = $_SESSION['error']['activate']; + unset($_SESSION['error']['activate']); + } + else + $msg = $this->resend(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'resendMail'), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'resendMail'), + 'message' => Lang::account('inputbox', 'message', 'resendMail'), + 'error' => $msg, + 'action' => '?account=resend', + )]; + } + + private function resend() : string + { + // no input yet. show clean form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT')); + + // on cooldown pretend we dont know the email address + if ($timeout && $timeout > time()) + return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // check email and account status + if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ?_account WHERE `email` = ? AND `status` = ?d', $this->_post['email'], ACC_STATUS_NEW)) + { + if (!Util::sendMail($this->_post['email'], 'activate-account', [$token])) + return Lang::main('intError'); + + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + } + + // pretend recovery started + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/reset-password.php b/endpoints/account/reset-password.php new file mode 100644 index 00000000..44c39b0b --- /dev/null +++ b/endpoints/account/reset-password.php @@ -0,0 +1,121 @@ + display email form + * 2. submit email form > send mail with recovery link + * 3. click recovery link from mail > display password reset form + * 4. submit password reset form > update password + */ + +class AccountresetpasswordResponse extends TemplateResponse +{ + use TrRecoveryHelper, TrGetNext; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'reset-password'; + + protected array $expectedGET = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + ); + protected array $expectedPOST = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + private bool $success = false; + + public function __construct() + { + $this->title[] = Lang::account('title'); + + parent::__construct(); + + // don't redirect logged in users + // you can be forgetful AND logged in + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + parent::generate(); + + $errMsg = ''; + if (!$this->assertGET('key') && !$this->assertPOST('key')) + $errMsg = Lang::account('inputbox', 'error', 'passTokenLost'); + else if ($this->_get['key'] && !DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `token` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS)) + $errMsg = Lang::account('inputbox', 'error', 'passTokenUsed'); + + if ($errMsg) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'error'), + 'error' => $errMsg + )]; + + return; + } + + // step "2.5" + $errMsg = $this->doResetPass(); + if ($this->success) + $this->forward('?account=signin'); + + // step 2 + $this->inputbox = ['inputbox-form-password', array( + 'head' => Lang::account('inputbox', 'head', 'recoverPass', [2]), + 'token' => $this->_post['key'] ?? $this->_get['key'], + 'action' => '?account=reset-password&next=account=signin', + 'error' => $errMsg, + )]; + } + + private function doResetPass() : string + { + // no input yet. show clean form + if (!$this->assertPOST('key', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + if ($this->_post['password'] != $this->_post['c_password']) + return Lang::account('passCheckFail'); + + $userData = DB::Aowow()->selectRow('SELECT `id`, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', + $this->_post['key'], + $this->_post['email'], + ACC_STATUS_RECOVER_PASS + ); + if (!$userData) + return Lang::account('inputbox', 'error', 'emailNotFound'); + + if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash'])) + return Lang::account('newPassDiff'); + + if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_NONE, $userData['id'])) + return Lang::main('intError'); + + $this->success = true; + return ''; + } +} + +?> diff --git a/template/bricks/inputbox-form-email.tpl.php b/template/bricks/inputbox-form-email.tpl.php new file mode 100644 index 00000000..dfc7be5d --- /dev/null +++ b/template/bricks/inputbox-form-email.tpl.php @@ -0,0 +1,46 @@ + +
    + + + +
    +
    +

    +
    + + +
    + +
    + +
    + + +
    + +
    +
    + diff --git a/template/bricks/inputbox-form-password.tpl.php b/template/bricks/inputbox-form-password.tpl.php new file mode 100644 index 00000000..5f76c375 --- /dev/null +++ b/template/bricks/inputbox-form-password.tpl.php @@ -0,0 +1,78 @@ + +
    + + + +
    +
    +

    +
    + + + + + + + + + + + + + + + + + + +
    + + +
    +
    + + diff --git a/template/bricks/inputbox-form-signin.tpl.php b/template/bricks/inputbox-form-signin.tpl.php index a917c7a5..4fa30d83 100644 --- a/template/bricks/inputbox-form-signin.tpl.php +++ b/template/bricks/inputbox-form-signin.tpl.php @@ -52,7 +52,7 @@
    -
    | |
    +
    | |
    diff --git a/template/mails/reset-password_0.tpl b/template/mails/reset-password_0.tpl index d2e8bb11..7fe9762b 100644 --- a/template/mails/reset-password_0.tpl +++ b/template/mails/reset-password_0.tpl @@ -2,6 +2,6 @@ Password Reset Follow this link to reset your password. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s If you did not request this mail simply ignore it. diff --git a/template/mails/reset-password_2.tpl b/template/mails/reset-password_2.tpl index ba7fd163..97a3cd75 100644 --- a/template/mails/reset-password_2.tpl +++ b/template/mails/reset-password_2.tpl @@ -2,6 +2,6 @@ Réinitialisation du mot de passe Suivez ce lien pour réinitialiser votre mot de passe. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Si vous n'avez pas fait de demande de réinitialisation, ignorez cet e-mail. diff --git a/template/mails/reset-password_3.tpl b/template/mails/reset-password_3.tpl index d7cd3a00..0cf87890 100644 --- a/template/mails/reset-password_3.tpl +++ b/template/mails/reset-password_3.tpl @@ -2,6 +2,6 @@ Kennwortreset Folgt diesem Link um euer Kennwort zurückzusetzen. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Falls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden. diff --git a/template/mails/reset-password_4.tpl b/template/mails/reset-password_4.tpl index 80c54065..615da1fb 100644 --- a/template/mails/reset-password_4.tpl +++ b/template/mails/reset-password_4.tpl @@ -2,6 +2,6 @@ 重置密码 点击此链接以重置您的密码。 -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s 如果您没有请求此邮件,请忽略它。 diff --git a/template/mails/reset-password_6.tpl b/template/mails/reset-password_6.tpl index 0560bfd2..83ad31fc 100644 --- a/template/mails/reset-password_6.tpl +++ b/template/mails/reset-password_6.tpl @@ -2,6 +2,6 @@ Reinicio de Contraseña Siga este enlace para reiniciar su contraseña. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Si usted no solicitó este correo, por favor ignorelo. diff --git a/template/mails/reset-password_8.tpl b/template/mails/reset-password_8.tpl index e9b88d44..ca1317d8 100644 --- a/template/mails/reset-password_8.tpl +++ b/template/mails/reset-password_8.tpl @@ -2,6 +2,6 @@ Сброс пароля Перейдите по этой ссылке, чтобы сбросить свой пароль. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Если вы не запрашивали это письмо, просто проигнорируйте его. diff --git a/template/pages/acc-recover.tpl.php b/template/pages/acc-recover.tpl.php deleted file mode 100644 index 8b0edad8..00000000 --- a/template/pages/acc-recover.tpl.php +++ /dev/null @@ -1,136 +0,0 @@ - - -brick('header'); ?> - -
    -
    -
    -brick('announcement'); - - $this->brick('pageTemplate'); -?> -
    -text)): ?> -
    -

    head; ?>

    -
    -
    text; ?>
    -
    -resetPass): ?> - - -
    -
    -

    head; ?>

    -
    error; ?>
    - - - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - - - -
    -
    -

    head; ?>

    -
    error; ?>
    - -
    - -
    - - -
    - -
    -
    - - -
    -
    -
    - -brick('footer'); ?> From 258ac19f0a7624de373d0e4ebc507fc81cd0c9d5 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:55:08 +0200 Subject: [PATCH 692/957] Template/Update (Part 46 - IV) * account management rework: Personal Settings functionality * email, password, username update * email updates now also mails the old address for confirmation --- README.md | 1 + endpoints/account/account.php | 14 ++-- endpoints/account/confirm-email-address.php | 62 +++++++++++++++ endpoints/account/confirm-password.php | 60 ++++++++++++++ endpoints/account/revert-email-address.php | 62 +++++++++++++++ endpoints/account/update-email.php | 80 +++++++++++++++++++ endpoints/account/update-password.php | 86 +++++++++++++++++++++ endpoints/account/update-username.php | 61 +++++++++++++++ includes/kernel.php | 2 +- localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- localization/locale_zhcn.php | 2 +- setup/updates/1758578400_13.sql | 2 + setup/updates/1758578400_14.sql | 2 + template/mails/change-email_0.tpl | 11 +++ template/mails/change-email_2.tpl | 11 +++ template/mails/change-email_3.tpl | 11 +++ template/mails/change-email_4.tpl | 11 +++ template/mails/change-email_6.tpl | 11 +++ template/mails/change-email_8.tpl | 12 +++ template/mails/revert-email_0.tpl | 11 +++ template/mails/revert-email_2.tpl | 11 +++ template/mails/revert-email_3.tpl | 11 +++ template/mails/revert-email_4.tpl | 11 +++ template/mails/revert-email_6.tpl | 11 +++ template/mails/revert-email_8.tpl | 11 +++ template/mails/update-password_0.tpl | 10 +++ template/mails/update-password_2.tpl | 10 +++ template/mails/update-password_3.tpl | 10 +++ template/mails/update-password_4.tpl | 10 +++ template/mails/update-password_6.tpl | 10 +++ template/mails/update-password_8.tpl | 10 +++ template/pages/account.tpl.php | 6 +- 36 files changed, 628 insertions(+), 15 deletions(-) create mode 100644 endpoints/account/confirm-email-address.php create mode 100644 endpoints/account/confirm-password.php create mode 100644 endpoints/account/revert-email-address.php create mode 100644 endpoints/account/update-email.php create mode 100644 endpoints/account/update-password.php create mode 100644 endpoints/account/update-username.php create mode 100644 setup/updates/1758578400_13.sql create mode 100644 setup/updates/1758578400_14.sql create mode 100644 template/mails/change-email_0.tpl create mode 100644 template/mails/change-email_2.tpl create mode 100644 template/mails/change-email_3.tpl create mode 100644 template/mails/change-email_4.tpl create mode 100644 template/mails/change-email_6.tpl create mode 100644 template/mails/change-email_8.tpl create mode 100644 template/mails/revert-email_0.tpl create mode 100644 template/mails/revert-email_2.tpl create mode 100644 template/mails/revert-email_3.tpl create mode 100644 template/mails/revert-email_4.tpl create mode 100644 template/mails/revert-email_6.tpl create mode 100644 template/mails/revert-email_8.tpl create mode 100644 template/mails/update-password_0.tpl create mode 100644 template/mails/update-password_2.tpl create mode 100644 template/mails/update-password_3.tpl create mode 100644 template/mails/update-password_4.tpl create mode 100644 template/mails/update-password_6.tpl create mode 100644 template/mails/update-password_8.tpl diff --git a/README.md b/README.md index 44bea61d..cc69e630 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Also, this project is not meant to be used for commercial purposes of any kind! + [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php) + [Multibyte String](https://www.php.net/manual/en/book.mbstring.php) + [File Information](https://www.php.net/manual/en/book.fileinfo.php) + + [Internationalization](https://www.php.net/manual/en/book.intl.php) + [GNU Multiple Precision](https://www.php.net/manual/en/book.gmp.php) (When using TrinityCore as auth source) + MySQL ≥ 5.7.0 OR MariaDB ≥ 10.6.4 OR similar + [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101) (no other other providers are supported at this time) diff --git a/endpoints/account/account.php b/endpoints/account/account.php index cd57c720..e560a55e 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -28,6 +28,7 @@ class AccountBaseResponse extends TemplateResponse public string $curEmail = ''; public string $curName = ''; public string $renameCD = ''; + public string $activeCD = ''; public array $description = []; public array $signature = []; public int $avMode = 0; @@ -51,7 +52,7 @@ class AccountBaseResponse extends TemplateResponse { array_unshift($this->title, Lang::account('settings')); - $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon` FROM ?_account WHERE `id` = ?d', User::$id); + $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon`, `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id); Lang::sort('game', 'ra'); @@ -108,10 +109,13 @@ class AccountBaseResponse extends TemplateResponse $this->curEmail = $user['email'] ?? ''; // Username - $this->curName = User::$username; - - // todo localize date format; store time - // $this->renameCD = date('F j, o', time() + 7 * DAY); + $this->curName = User::$username; + $this->renameCD = Util::formatTime(Cfg::get('ACC_RENAME_DECAY') * 1000); + if ($user['renameCooldown'] > time()) + { + $locCode = implode('_', str_split(Lang::getLocale()->json(), 2)); // ._. + $this->activeCD = (new \IntlDateFormatter($locCode, pattern: Lang::main('dateFmtIntl')))->format($user['renameCooldown']); + } /* COMMUNITY */ diff --git a/endpoints/account/confirm-email-address.php b/endpoints/account/confirm-email-address.php new file mode 100644 index 00000000..eb11801c --- /dev/null +++ b/endpoints/account/confirm-email-address.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->change(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + // this should probably leave change info intact for revert + // todo - move personal settings changes to separate table + private function change() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time()) + return Lang::account('inputbox', 'error', 'mailTokenUsed'); + + // 0 changes == error + if (!DB::Aowow()->query('UPDATE ?_account SET `email` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('inputbox', 'message', 'mailChangeOk'); + } +} + +?> diff --git a/endpoints/account/confirm-password.php b/endpoints/account/confirm-password.php new file mode 100644 index 00000000..4eec91f0 --- /dev/null +++ b/endpoints/account/confirm-password.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->confirm(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + private function confirm() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_PASS || $acc['statusTimer'] < time()) + return Lang::account('inputbox', 'error', 'passTokenUsed'); + + // 0 changes == error + if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('inputbox', 'message', 'passChangeOk'); + } +} + +?> diff --git a/endpoints/account/revert-email-address.php b/endpoints/account/revert-email-address.php new file mode 100644 index 00000000..37475082 --- /dev/null +++ b/endpoints/account/revert-email-address.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->revert(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + // this should probably take precedence over email-change + // todo - move personal settings changes to separate table + private function revert() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time()) + return Lang::account('inputbox', 'error', 'mailTokenUsed'); + + // 0 changes == error + if (!DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('inputbox', 'message', 'mailRevertOk'); + } +} + +?> diff --git a/endpoints/account/update-email.php b/endpoints/account/update-email.php new file mode 100644 index 00000000..104e18a5 --- /dev/null +++ b/endpoints/account/update-email.php @@ -0,0 +1,80 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updateMail()) + $_SESSION['msg'] = ['email', $this->success, $msg]; + } + + private function updateMail() : string + { + // no input yet + if (is_null($this->_post['newemail'])) + return Lang::main('intError'); + // truncated due to validation fail + if (!$this->_post['newemail']) + return Lang::account('emailInvalid'); + + if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ? AND `id` <> ?d', $this->_post['newemail'], User::$id)) + return Lang::account('mailInUse'); + + $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', User::$id); + if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL) + return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + + $oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); + if ($this->_post['newemail'] == $oldEmail) + return Lang::account('newMailDiff'); + + $token = Util::createHash(); + + // store new mail in updateValue field, exchange when confirmation mail gets confirmed + if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + $this->_post['newemail'], ACC_STATUS_CHANGE_EMAIL, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id)) + return Lang::main('intError'); + + if (!Util::sendMail($this->_post['newemail'], 'change-email', [$token, $this->_post['newemail']], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + if (!Util::sendMail($oldEmail, 'revert-email', [$token, $oldEmail], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + $this->success = true; + return Lang::account('updateMessage', 'personal', [$this->_post['newemail']]); + } +} + +?> diff --git a/endpoints/account/update-password.php b/endpoints/account/update-password.php new file mode 100644 index 00000000..cc4151d6 --- /dev/null +++ b/endpoints/account/update-password.php @@ -0,0 +1,86 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'newPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'confirmPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'globalLogout' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox']] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updatePassword()) + $_SESSION['msg'] = ['password', $this->success, $msg]; + } + + private function updatePassword() : string + { + if (!$this->assertPOST('currentPassword', 'newPassword', 'confirmPassword')) + return Lang::main('intError'); + + if (!Util::validatePassword($this->_post['newPassword'], $e)) + return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + + if ($this->_post['newPassword'] !== $this->_post['confirmPassword']) + return Lang::account('passMismatch'); + + $userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ?_account WHERE `id` = ?d', User::$id); + if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time()) + return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + + if (!User::verifyCrypt($this->_post['currentPassword'], $userData['passHash'])) + return Lang::account('wrongPass'); + + if (User::verifyCrypt($this->_post['newPassword'], $userData['passHash'])) + return Lang::account('newPassDiff'); + + $token = Util::createHash(); + + // store new hash in updateValue field, exchange when confirmation mail gets confirmed + if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + User::hashCrypt($this->_post['newPassword']), ACC_STATUS_CHANGE_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id)) + return Lang::main('intError'); + + $email = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); + if (!Util::sendMail($email, 'update-password', [$token, $email], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + // logout all other active sessions + if ($this->_post['globalLogout']) + DB::Aowow()->query('UPDATE ?_account_sessions SET `status` = ?d, `touched` = ?d WHERE `userId` = ?d AND `sessionId` <> ? AND `status` = ?d', SESSION_FORCED_LOGOUT, time(), User::$id, session_id(), SESSION_ACTIVE); + + $this->success = true; + return Lang::account('updateMessage', 'personal', [User::$email]); + } +} + +?> diff --git a/endpoints/account/update-username.php b/endpoints/account/update-username.php new file mode 100644 index 00000000..d301fb6a --- /dev/null +++ b/endpoints/account/update-username.php @@ -0,0 +1,61 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updateUsername()) + $_SESSION['msg'] = ['username', $this->success, $msg]; + } + + private function updateUsername() : string + { + if (!$this->assertPOST('newUsername')) + return Lang::main('intError'); + + if (DB::Aowow()->selectCell('SELECT `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id) > time()) + return Lang::main('intError'); // should have grabbed the error response.. + + // yes, including your current name. you don't want to change into your current name, right? + if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_post['newUsername'])) + return Lang::account('nameInUse'); + + DB::Aowow()->query('UPDATE ?_account SET `username` = ?, `renameCooldown` = ?d WHERE `id` = ?d', $this->_post['newUsername'], time() + Cfg::get('acc_rename_decay'), User::$id); + + $this->success = true; + return Lang::account('updateMessage', 'username', [User::$username, $this->_post['newUsername']]); + } +} + +?> diff --git a/includes/kernel.php b/includes/kernel.php index 4d444d5a..7145501d 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -13,7 +13,7 @@ define('CLI_HAS_E', CLI && // WIN10 and later u (!OS_WIN || (function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(STDOUT)))); -$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring', 'fileinfo'/*, 'gmp'*/]; +$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring', 'fileinfo', 'intl'/*, 'gmp'*/]; $badExt = []; $error = ''; if ($ext = array_filter($reqExt, fn($x) => !extension_loaded($x))) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 71e46780..23ff084b 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", - 'dateFmtUntil' => "j. F Y", + 'dateFmtIntl' => "d. MMMM y", 'timeAgo' => 'vor %s', 'nfSeparators' => ['.', ','], diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 812c3606..670ec5e9 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", - 'dateFmtUntil' => "F j, Y", + 'dateFmtIntl' => "MMMM d, y", 'timeAgo' => "%s ago", 'nfSeparators' => [',', '.'], diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 385861b1..9e5af257 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d/m/Y", 'dateFmtLong' => "d/m/Y \a \l\a\s g:i A", - 'dateFmtUntil' => "j \d\\e F \d\\e Y", + 'dateFmtIntl' => "d 'de' MMMM 'de' y", 'timeAgo' => 'hace %s', 'nfSeparators' => ['.', ','], diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 1974541e..cd982692 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ' : ', 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d à g:i A", - 'dateFmtUntil' => "j F Y", + 'dateFmtIntl' => "d MMMM y", 'timeAgo' => 'il y a %s', 'nfSeparators' => [' ', ','], diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 64a925d9..5f702169 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ": ", 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d в g:i A", - 'dateFmtUntil' => "j F Y г.", + 'dateFmtIntl' => "d MMMM y г.", 'timeAgo' => '%s назад', 'nfSeparators' => [' ', ','], diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 4d710049..e184e6dd 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ':', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", - 'dateFmtUntil' => "Y年n月j日", + 'dateFmtIntl' => "y年M月d日", 'timeAgo' => '%s之前', 'nfSeparators' => [',', '.'], diff --git a/setup/updates/1758578400_13.sql b/setup/updates/1758578400_13.sql new file mode 100644 index 00000000..c055f944 --- /dev/null +++ b/setup/updates/1758578400_13.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `renameCooldown` int unsigned NOT NULL DEFAULT 0 COMMENT 'timestamp when rename is available again' AFTER `updateValue`; diff --git a/setup/updates/1758578400_14.sql b/setup/updates/1758578400_14.sql new file mode 100644 index 00000000..3445704e --- /dev/null +++ b/setup/updates/1758578400_14.sql @@ -0,0 +1,2 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_rename_decay'; +INSERT INTO `aowow_config` VALUES ('acc_rename_decay', 30 * 24 * 60 * 60, '30 * 24 * 60 * 60', 3, 129, 'delay between username changes'); diff --git a/template/mails/change-email_0.tpl b/template/mails/change-email_0.tpl new file mode 100644 index 00000000..58a92f10 --- /dev/null +++ b/template/mails/change-email_0.tpl @@ -0,0 +1,11 @@ +# Created: 2025 +Email Change Confirm +Greetings, + +We received a request to change your account's email address. If you made this request, please follow the link below to confirm the change. + +HOST_URL?account=confirm-email-address&key=%1$s + +If you didn't request this change please feel free to disregard this email. If the link did not work or you have any further concerns about this, please contact CONTACT_EMAIL. The link will become invalid %10$s after this email was sent. + +The NAME_SHORT team diff --git a/template/mails/change-email_2.tpl b/template/mails/change-email_2.tpl new file mode 100644 index 00000000..c9743068 --- /dev/null +++ b/template/mails/change-email_2.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Demande de confirmation de changement d'adresse e-mail +Bonjour, + +Nous avons reçu une demande de modification de l'adresse e-mail associée à votre compte. Si vous êtes à l'origine de cette demande, veuillez suivre le lien ci-dessous pour confirmer le changement. + +HOST_URL?account=confirm-email-address&key=%1$s + +Si vous n'avez pas demandé ce changement, vous pouvez ignorer cet e-mail. Si le lien ne fonctionne pas ou si vous avez d'autres préoccupations à ce sujet, veuillez contacter CONTACT_EMAIL. Ce lien deviendra invalide %10$s après l'envoi de cet e-mail. + +L'équipe NAME_SHORT diff --git a/template/mails/change-email_3.tpl b/template/mails/change-email_3.tpl new file mode 100644 index 00000000..8abae027 --- /dev/null +++ b/template/mails/change-email_3.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Bestätigung der E-Mail-Änderung angefordert +Hallo, + +Wir haben eine Anfrage zur Änderung Ihrer E-Mail-Adresse erhalten. Wenn Sie diese Anfrage gestellt haben, folgen Sie bitte dem untenstehenden Link, um die Änderung zu bestätigen. + +HOST_URL?account=confirm-email-address&key=%1$s + +Falls Sie diese Änderung nicht angefordert haben, können Sie diese E-Mail ignorieren. Falls der Link nicht funktioniert oder Sie weitere Fragen haben, wenden Sie sich bitte an CONTACT_EMAIL. Der Link wird %10$s nach Versand dieser E-Mail ungültig. + +Das Team von NAME_SHORT diff --git a/template/mails/change-email_4.tpl b/template/mails/change-email_4.tpl new file mode 100644 index 00000000..9fbd9efa --- /dev/null +++ b/template/mails/change-email_4.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +确认更改电子邮件地址 +您好, + +我们收到了一项更改您账户电子邮件地址的请求。如果是您本人操作,请点击下方链接以确认更改。 + +HOST_URL?account=confirm-email-address&key=%1$s + +如果您未曾发起此更改,请忽略此邮件。如果链接无法使用或您对此有任何疑问,请联系 CONTACT_EMAIL。此链接将在本邮件发送后 %10$s 失效。 + +NAME_SHORT 团队敬上 diff --git a/template/mails/change-email_6.tpl b/template/mails/change-email_6.tpl new file mode 100644 index 00000000..16aabc7b --- /dev/null +++ b/template/mails/change-email_6.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Confirmación de cambio de correo electrónico +Saludos, + +Hemos recibido una solicitud para cambiar la dirección de correo electrónico de su cuenta. Si usted realizó esta solicitud, siga el enlace de abajo para confirmar el cambio. + +HOST_URL?account=confirm-email-address&key=%1$s + +Si usted no solicitó este cambio, puede ignorar este correo. Si el enlace no funciona o tiene alguna inquietud, por favor contacte a CONTACT_EMAIL. El enlace se invalidará %10$s después de que este correo haya sido enviado. + +El equipo de NAME_SHORT diff --git a/template/mails/change-email_8.tpl b/template/mails/change-email_8.tpl new file mode 100644 index 00000000..aaeeed88 --- /dev/null +++ b/template/mails/change-email_8.tpl @@ -0,0 +1,12 @@ +# GPTed from 2025 source +Подтверждение изменения адреса электронной почты +Здравствуйте, + +Мы получили запрос на изменение адреса электронной почты, связанного с вашим аккаунтом. Если вы отправили этот запрос, пожалуйста, перейдите по ссылке ниже для подтверждения изменения. + +HOST_URL?account=confirm-email-address&key=%1$s + +Если вы не запрашивали это изменение, просто проигнорируйте это письмо. Если ссылка не работает или у вас есть дополнительные вопросы, пожалуйста, свяжитесь с CONTACT_EMAIL. Ссылка станет недействительной через %10$s после отправки этого письма. + +Команда NAME_SHORT +Пожалуйста, перейдите по ссылке ниже, чтобы подтвердить ваш новый адрес электронной почты. diff --git a/template/mails/revert-email_0.tpl b/template/mails/revert-email_0.tpl new file mode 100644 index 00000000..881c02f9 --- /dev/null +++ b/template/mails/revert-email_0.tpl @@ -0,0 +1,11 @@ +# Created: 2025 +Email Change Requested +Greetings, + +We received a request to change your account's email address. If you made this request, please follow the instructions in the confirmation email sent to the address indicated. If you didn't make such a request, please click the link below to prevent the email from being changed. + +HOST_URL?account=revert-email-address&key=%1$s + +If the link did not work or you have any further concerns about this, please contact CONTACT_EMAIL. This link will automatically become invalid %10$s from now. + +The NAME_SHORT team diff --git a/template/mails/revert-email_2.tpl b/template/mails/revert-email_2.tpl new file mode 100644 index 00000000..b9b37958 --- /dev/null +++ b/template/mails/revert-email_2.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Demande de modification d'adresse e-mail +Bonjour, + +Nous avons reçu une demande de modification de l'adresse e-mail associée à votre compte. Si vous êtes à l'origine de cette demande, veuillez suivre les instructions contenues dans l'e-mail de confirmation envoyé à l'adresse indiquée. Si vous n'êtes pas à l'origine de cette demande, veuillez cliquer sur le lien ci-dessous pour empêcher la modification de l'adresse e-mail. + +HOST_URL?account=revert-email-address&key=%1$s + +Si le lien ne fonctionne pas ou si vous avez d'autres préoccupations à ce sujet, veuillez contacter CONTACT_EMAIL. Ce lien deviendra automatiquement invalide dans %10$s. + +L'équipe NAME_SHORT diff --git a/template/mails/revert-email_3.tpl b/template/mails/revert-email_3.tpl new file mode 100644 index 00000000..4ccc7af9 --- /dev/null +++ b/template/mails/revert-email_3.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +E-Mail-Änderung angefordert +Hallo, + +Wir haben eine Anfrage zur Änderung Ihrer E-Mail-Adresse erhalten. Wenn Sie diese Anfrage gestellt haben, folgen Sie bitte den Anweisungen in der Bestätigungs-E-Mail, die an die angegebene Adresse gesendet wurde. Falls Sie diese Anfrage nicht gestellt haben, klicken Sie bitte auf den untenstehenden Link, um die Änderung der E-Mail-Adresse zu verhindern. + +HOST_URL?account=revert-email-address&key=%1$s + +Falls der Link nicht funktioniert oder Sie weitere Fragen haben, wenden Sie sich bitte an CONTACT_EMAIL. Dieser Link wird automatisch nach %%10$s ungültig. + +Ihr NAME_SHORT-Team diff --git a/template/mails/revert-email_4.tpl b/template/mails/revert-email_4.tpl new file mode 100644 index 00000000..ecc86e88 --- /dev/null +++ b/template/mails/revert-email_4.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +请求更改电子邮件地址 +您好, + +我们收到了一项更改您账户电子邮件地址的请求。如果是您本人操作,请按照发送到指定地址的确认邮件中的说明进行操作。如果不是您本人操作,请点击下方链接以阻止电子邮件地址的更改。 + +HOST_URL?account=revert-email-address&key=%1$s + +如果链接无法使用或您对此有任何疑问,请联系 CONTACT_EMAIL。此链接将在 %10$s 后自动失效。 + +NAME_SHORT 团队敬上 diff --git a/template/mails/revert-email_6.tpl b/template/mails/revert-email_6.tpl new file mode 100644 index 00000000..2fbf927b --- /dev/null +++ b/template/mails/revert-email_6.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Solicitud de cambio de correo electrónico +Saludos, + +Hemos recibido una solicitud para cambiar la dirección de correo electrónico de su cuenta. Si usted realizó esta solicitud, siga las instrucciones en el correo de confirmación enviado a la dirección indicada. Si no realizó esta solicitud, haga clic en el enlace de abajo para evitar el cambio de correo electrónico. + +HOST_URL?account=revert-email-address&key=%1$s + +Si el enlace no funciona o tiene alguna inquietud, por favor contacte a CONTACT_EMAIL. Este enlace se invalidará automáticamente en %10$s. + +El equipo de NAME_SHORT diff --git a/template/mails/revert-email_8.tpl b/template/mails/revert-email_8.tpl new file mode 100644 index 00000000..b257ad2a --- /dev/null +++ b/template/mails/revert-email_8.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Запрос на изменение адреса электронной почты +Здравствуйте, + +Мы получили запрос на изменение адреса электронной почты, связанного с вашим аккаунтом. Если вы отправили этот запрос, пожалуйста, следуйте инструкциям в письме с подтверждением, отправленном на указанный адрес. Если вы не отправляли такой запрос, пожалуйста, перейдите по ссылке ниже, чтобы предотвратить изменение адреса электронной почты. + +HOST_URL?account=revert-email-address&key=%1$s + +Если ссылка не работает или у вас есть дополнительные вопросы, пожалуйста, свяжитесь с CONTACT_EMAIL. Эта ссылка автоматически станет недействительной через %10$s. + +Команда NAME_SHORT diff --git a/template/mails/update-password_0.tpl b/template/mails/update-password_0.tpl new file mode 100644 index 00000000..d55404cb --- /dev/null +++ b/template/mails/update-password_0.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Password Confirmation +Hey! + +Please click the link below to confirm your new password. +HOST_URL?account=confirm-password&key=%1$s + +Let us know if you have any problems! + +The NAME_SHORT team diff --git a/template/mails/update-password_2.tpl b/template/mails/update-password_2.tpl new file mode 100644 index 00000000..e971a03a --- /dev/null +++ b/template/mails/update-password_2.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Confirmation du mot de passe +Bonjour ! + +Veuillez cliquer sur le lien ci-dessous pour confirmer votre nouveau mot de passe. +HOST_URL?account=confirm-password&key=%1$s + +Faites-nous savoir si vous rencontrez des problèmes ! + +L'équipe NAME_SHORT diff --git a/template/mails/update-password_3.tpl b/template/mails/update-password_3.tpl new file mode 100644 index 00000000..348a7abb --- /dev/null +++ b/template/mails/update-password_3.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Passwortbestätigung +Hallo! + +Bitte klicke auf den untenstehenden Link, um dein neues Passwort zu bestätigen. +HOST_URL?account=confirm-password&key=%1$s + +Lass uns wissen, falls du Probleme hast! + +Das NAME_SHORT Team diff --git a/template/mails/update-password_4.tpl b/template/mails/update-password_4.tpl new file mode 100644 index 00000000..affa20f4 --- /dev/null +++ b/template/mails/update-password_4.tpl @@ -0,0 +1,10 @@ +# Created by ChatGPT from May 2025 base; locale 0 +密码确认 +你好! + +请点击下面的链接以确认你的新密码。 +HOST_URL?account=confirm-password&key=%1$s + +如果你有任何问题,请告诉我们! + +NAME_SHORT 团队敬上 diff --git a/template/mails/update-password_6.tpl b/template/mails/update-password_6.tpl new file mode 100644 index 00000000..b46dd849 --- /dev/null +++ b/template/mails/update-password_6.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Confirmación de contraseña +¡Hola! + +Por favor, haz clic en el siguiente enlace para confirmar tu nueva contraseña. +HOST_URL?account=confirm-password&key=%1$s + +¡Avísanos si tienes algún problema! + +El equipo de NAME_SHORT diff --git a/template/mails/update-password_8.tpl b/template/mails/update-password_8.tpl new file mode 100644 index 00000000..9266fa1d --- /dev/null +++ b/template/mails/update-password_8.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Подтверждение пароля +Здравствуйте! + +Пожалуйста, перейдите по ссылке ниже, чтобы подтвердить ваш новый пароль. +HOST_URL?account=confirm-password&key=%1$s + +Сообщите нам, если у вас возникнут какие-либо проблемы! + +Команда NAME_SHORT diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php index ca64947c..fe9b076b 100644 --- a/template/pages/account.tpl.php +++ b/template/pages/account.tpl.php @@ -110,9 +110,9 @@ if ($this->bans):
    -
    -renameCD): ?> -

    renameCD]);?>
    +
    renameCD]);?>
    +activeCD): ?> +

    activeCD]);?>
    From 1d5539b3620ba6e2e07af923afc630c15a838460 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:46:57 +0200 Subject: [PATCH 693/957] Template/Update (Part 46 - V) * account management rework: Avatar functionality * show avatar at comments (beckported, because no forums) --- endpoints/account/account.php | 25 +++-- endpoints/account/delete-icon.php | 47 +++++++++ endpoints/account/forum-avatar.php | 108 +++++++++++++++++++++ endpoints/account/premium-border.php | 41 ++++++++ endpoints/account/rename-icon.php | 36 +++++++ endpoints/upload/image-complete.php | 88 +++++++++++++++++ endpoints/upload/image-crop.php | 84 ++++++++++++++++ endpoints/user/user.php | 7 +- includes/components/avatarmgr.class.php | 122 ++++++++++++++++++++++++ includes/dbtypes/user.class.php | 27 +++++- includes/user.class.php | 34 ++++--- localization/locale_dede.php | 13 +++ localization/locale_enus.php | 13 +++ localization/locale_eses.php | 13 +++ localization/locale_frfr.php | 13 +++ localization/locale_ruru.php | 13 +++ localization/locale_zhcn.php | 13 +++ setup/tools/clisetup/filegen.us.php | 1 + setup/updates/1758578400_15.sql | 3 + setup/updates/1758578400_16.sql | 2 + static/css/aowow.css | 3 +- static/js/global.js | 92 +++++++++++++++--- template/pages/account.tpl.php | 54 ++++++++--- template/pages/image-crop.tpl.php | 45 +++++++++ 24 files changed, 839 insertions(+), 58 deletions(-) create mode 100644 endpoints/account/delete-icon.php create mode 100644 endpoints/account/forum-avatar.php create mode 100644 endpoints/account/premium-border.php create mode 100644 endpoints/account/rename-icon.php create mode 100644 endpoints/upload/image-complete.php create mode 100644 endpoints/upload/image-crop.php create mode 100644 includes/components/avatarmgr.class.php create mode 100644 setup/updates/1758578400_15.sql create mode 100644 setup/updates/1758578400_16.sql create mode 100644 template/pages/image-crop.tpl.php diff --git a/endpoints/account/account.php b/endpoints/account/account.php index e560a55e..2512d508 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -14,12 +14,13 @@ class AccountBaseResponse extends TemplateResponse protected array $scripts = [[SC_JS_FILE, 'js/account.js']]; // display status of executed step (forwarding back to this page) - public ?array $generalMessage = null; - public ?array $emailMessage = null; - public ?array $usernameMessage = null; - public ?array $passwordMessage = null; - public ?array $communityMessage = null; - public ?array $avatarMessage = null; + public ?array $generalMessage = null; + public ?array $emailMessage = null; + public ?array $usernameMessage = null; + public ?array $passwordMessage = null; + public ?array $communityMessage = null; + public ?array $avatarMessage = null; + public ?array $premiumborderMessage = null; // form fields public int $modelrace = 0; @@ -36,6 +37,7 @@ class AccountBaseResponse extends TemplateResponse public int $customicon = 0; public array $customicons = []; public bool $premium = false; + public int $reputation = 0; public ?Listview $avatarManager = null; public ?array $bans; @@ -130,7 +132,7 @@ class AccountBaseResponse extends TemplateResponse $this->avMode = $user['avatar']; // status [reviewing, ok, rejected]? (only 2: rejected processed in js) - if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d AND `status` > 0', User::$id))) + if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d', User::$id))) { array_walk($cuAvatars, function (&$x) { $x['when'] *= 1000; // uploaded timestamp expected as msec for some reason @@ -139,7 +141,7 @@ class AccountBaseResponse extends TemplateResponse }); foreach ($cuAvatars as $a) - if ($a['status'] != 2) + if ($a['status'] != AvatarMgr::STATUS_REJECTED) $this->customicons[$a['id']] = $a['name']; // TODO - replace with array_find in PHP 8.4 @@ -154,6 +156,8 @@ class AccountBaseResponse extends TemplateResponse if (!$this->premium) return; + $this->reputation = User::getReputation(); + // Avatar Manager $this->avatarManager = new Listview([ 'template' => 'avatar', @@ -161,11 +165,12 @@ class AccountBaseResponse extends TemplateResponse 'name' => '$LANG.tab_avatars', 'parent' => 'avatar-manage', 'hideNav' => 1 | 2, // top | bottom - 'data' => $cuAvatars ?? [] + 'data' => $cuAvatars ?? [], + 'note' => Lang::account('avatarSlots', [count($this->customicons), Cfg::get('acc_max_avatar_uploads')]) ]); // Premium Border Selector - // ??? + // solved by js } } diff --git a/endpoints/account/delete-icon.php b/endpoints/account/delete-icon.php new file mode 100644 index 00000000..e70ea64a --- /dev/null +++ b/endpoints/account/delete-icon.php @@ -0,0 +1,47 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id')) + return; + + // non-int > error + $selected = DB::Aowow()->selectCell('SELECT `current` FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + if ($selected === null || $selected === false) + return; + + DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + + // if deleted avatar is also currently selected, unset + if ($selected) + DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + + $path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']); + if (!unlink($path)) + trigger_error('AccountDeleteiconResponse - failed to delete file: '.$path, E_USER_ERROR); + } +} + +?> diff --git a/endpoints/account/forum-avatar.php b/endpoints/account/forum-avatar.php new file mode 100644 index 00000000..a123d572 --- /dev/null +++ b/endpoints/account/forum-avatar.php @@ -0,0 +1,108 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2 ]], + 'wowicon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]], // file name can have \W chars: inv_misc_fork&knife, achievement_dungeon_drak'tharon_heroic + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + // called via ajax + protected array $expectedGET = array( + 'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 2, 'max_range' => 2]], + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + $msg = match ($this->_post['avatar'] ?? $this->_get['avatar']) + { + 0 => $this->unset(), // none + 1 => $this->fromIcon(), // wow icon + 2 => $this->fromUpload(!$this->_get['avatar']), // custom icon (premium feature) + default => Lang::main('genericError') + }; + + if ($msg) + $_SESSION['msg'] = ['avatar', $this->success, $msg]; + } + + private function unset() : string + { + $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } + + private function fromIcon() : string + { + if (!$this->assertPOST('wowicon')) + return Lang::main('intError'); + + $icon = strtolower(trim($this->_post['wowicon'])); + + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_icons WHERE `name` = ?', $icon)) + return Lang::account('updateMessage', 'avNotFound'); + + $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1, `wowicon` = ? WHERE `id` = ?d', strtolower($icon), User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + $msg = Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account WHERE `wowicon` = ?', $icon)) > 1) + $msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]); + else + $msg .= ' '.Lang::account('updateMessage', 'av1stUser'); + + return $msg; + } + + protected function fromUpload(bool $viaPOST) : string + { + if (!User::isPremium()) + return Lang::main('genericError'); + + if (($viaPOST && !$this->assertPOST('customicon')) || (!$viaPOST && !$this->assertGET('customicon'))) + return Lang::main('intError'); + + $customIcon = $this->_post['customicon'] ?? $this->_get['customicon']; + + $x = DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = IF(`id` = ?d, 1, 0) WHERE `userId` = ?d AND `status` <> ?d', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED); + if (!is_int($x)) + return Lang::main('genericError'); + + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `avatar` = 2 WHERE `id` = ?d', User::$id))) + return Lang::main('intError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } +} + +?> diff --git a/endpoints/account/premium-border.php b/endpoints/account/premium-border.php new file mode 100644 index 00000000..e1ee43d8 --- /dev/null +++ b/endpoints/account/premium-border.php @@ -0,0 +1,41 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 4]], + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if (!$this->assertPOST('avatarborder')) + return; + + $x = DB::Aowow()->query('UPDATE ?_account SET `avatarborder` = ?d WHERE `id` = ?d', $this->_post['avatarborder'], User::$id); + if (!is_int($x)) + $_SESSION['msg'] = ['premiumborder', false, Lang::main('genericError')]; + else if (!$x) + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avNoChange')]; + else + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avSuccess')]; + } +} + +?> diff --git a/endpoints/account/rename-icon.php b/endpoints/account/rename-icon.php new file mode 100644 index 00000000..46735d01 --- /dev/null +++ b/endpoints/account/rename-icon.php @@ -0,0 +1,36 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'name' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' =>'/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/']] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id', 'name')) + return; + + // regexp same as in account.js + DB::Aowow()->query('UPDATE ?_account_avatars SET `name` = ? WHERE `id` = ?d AND `userId` = ?d', trim($this->_post['name']), $this->_post['id'], User::$id); + } +} + +?> diff --git a/endpoints/upload/image-complete.php b/endpoints/upload/image-complete.php new file mode 100644 index 00000000..10f9ea3b --- /dev/null +++ b/endpoints/upload/image-complete.php @@ -0,0 +1,88 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCoords']], + ); + + public string $imgHash; + public int $newId; + + public function __construct(string $pageParam) + { + if (User::isBanned()) + $this->generate404(); + + parent::__construct($pageParam); + + if (!preg_match('/^upload=image-complete&(\d+)\.(\w{16})$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->newId, $this->imgHash] = $m; + + if (!$this->imgHash || !$this->newId) + $this->generate404(); + } + + protected function generate() : void + { + if (!$this->handleComplete()) + $_SESSION['msg'] = ['avatar', false, AvatarMgr::$error ?: Lang::main('intError')]; + } + + private function handleComplete() : bool + { + if (!$this->assertPOST('coords')) + return false; + + if (!AvatarMgr::init()) + return false; + + if (!AvatarMgr::loadFile(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')) + return false; + + if (!AvatarMgr::cropImg(...$this->_post['coords'])) + return false; + + if (!AvatarMgr::createAtlas($this->newId)) + return false; + + $fSize = filesize(sprintf(AvatarMgr::PATH_AVATARS, $this->newId)); + if (!$fSize) + return false; + + $newId = DB::Aowow()->query('INSERT INTO ?_account_avatars (`id`, `userId`, `name`, `when`, `size`) VALUES (?d, ?d, ?, ?d, ?d)', $this->newId, User::$id, 'Avatar '.$this->newId, time(), $fSize); + if (!is_int($newId)) + { + trigger_error('UploadImagecompleteResponse - avatar query failed', E_USER_ERROR); + return false; + } + + // delete temp files + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')); + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash)); + + return true; + } + + protected static function checkCoords(string $val) : ?array + { + if (preg_match('/^[01]\.[0-9]{3}(,[01]\.[0-9]{3}){3}$/', $val)) + return explode(',', $val); + + return null; + } +} + +?> diff --git a/endpoints/upload/image-crop.php b/endpoints/upload/image-crop.php new file mode 100644 index 00000000..ba046dd6 --- /dev/null +++ b/endpoints/upload/image-crop.php @@ -0,0 +1,84 @@ +generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if ($err = $this->handleUpload()) + { + $_SESSION['msg'] = ['avatar', false, $err]; + $this->forward('?account#community'); + } + + $this->h1 = Lang::account('avatarSubmit'); + + $fileBase = User::$username.'-avatar-'.$this->nextId.'-'.$this->imgHash; + $dimensions = AvatarMgr::calcImgDimensions(); + + $this->cropper = $dimensions + array( + 'url' => Cfg::get('STATIC_URL').'/uploads/temp/'.$fileBase.'.jpg', + 'parent' => 'av-container', + 'minCrop' => ICON_SIZE_LARGE, // optional; defaults to 150 - min selection size (a square) + 'type' => Type::NPC, // NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] + 'typeId' => 15384, // = arbitrary image upload + 'constraint' => [1, 1] // [xMult, yMult] - relative size to each other (here: be square) + ); + + parent::generate(); + } + + private function handleUpload() : string + { + if (!AvatarMgr::init()) + return Lang::main('intError'); + + if (!AvatarMgr::validateUpload()) + return AvatarMgr::$error; + + if (!AvatarMgr::loadUpload()) + return Lang::main('intError'); + + $n = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account_avatars WHERE `userId` = ?d', User::$id); + if ($n && $n > Cfg::get('ACC_MAX_AVATAR_UPLOADS')) + return Lang::main('intError'); + + // why is ++(); illegal syntax? WHO KNOWS!? + $this->nextId = (DB::Aowow()->selectCell('SELECT MAX(`id`) FROM ?_account_avatars') ?: 0) + 1; + + if (!AvatarMgr::tempSaveUpload(['avatar', $this->nextId], $this->imgHash)) + return Lang::main('intError'); + + return ''; + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php index b3e8f7b8..c89a6cf2 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -38,7 +38,7 @@ class UserBaseResponse extends TemplateResponse if (!$pageParam) $this->forwardToSignIn('user'); - if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) + if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`avatarborder`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) $this->user = $user; else $this->generateNotFound(Lang::user('notFound', [$pageParam])); @@ -115,12 +115,15 @@ class UserBaseResponse extends TemplateResponse default => '' }; + if (!($this->user['userGroups'] & U_GROUP_PREMIUM)) + $this->user['avatarborder'] = 2; + $this->userIcon = array( // JS: Icon.createUser() $this->user['avatar'], // avatar: 1(iconString), 2(customId) $avatarMore, // avatarMore: iconString or customId IconElement::SIZE_MEDIUM, // size: (always medium) null, // url: (always null) - User::isInGroup(U_GROUP_PREMIUM) ? 0 : 2, // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] + $this->user['avatarborder'], // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] false, // noBorder: always false '$Icon.getPrivilegeBorder('.$this->user['sumRep'].')' // reputationLevel: calculated in js from passed rep points ); diff --git a/includes/components/avatarmgr.class.php b/includes/components/avatarmgr.class.php new file mode 100644 index 00000000..4df080c8 --- /dev/null +++ b/includes/components/avatarmgr.class.php @@ -0,0 +1,122 @@ + self::MAX_W || $is[1] > self::MAX_H) + self::$error = Lang::account('selectAvatar'); + } + else + self::$error = Lang::account('selectAvatar'); + + if (!self::$error) + return true; + + self::$fileName = ''; + return false; + } + + /* create icon texture atlas + * ****************************** + * * LARGE * MEDIUM * + * * * * + * * * * + * * ************* + * * * SMOL * * + * * * * * + * * ********* * + * ****************************** + * + * as static/uploads/avatars/.jpg + */ + + public static function createAtlas(string $fileName) : bool + { + if (!self::$img) + return false; + + $sizes = [ICON_SIZE_LARGE, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL]; + + $dest = imagecreatetruecolor(ICON_SIZE_LARGE + ICON_SIZE_MEDIUM, ICON_SIZE_LARGE); + $srcW = imagesx(self::$img); + $srcH = imagesx(self::$img); + + $destX = $destY = 0; + foreach ($sizes as $idx => $dim) + { + imagecopyresampled($dest, self::$img, $destX, $destY, 0, 0, $dim, $dim, $srcW, $srcH); + + if ($idx % 2) + $destY += $dim; + else + $destX += $dim; + } + + if (!imagejpeg($dest, sprintf(self::PATH_AVATARS, $fileName), self::JPEG_QUALITY)) + return false; + + self::$img = null; + $dest = null; + return true; + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + // unsure yet how that's supposed to work + // for now pending uploads can be used right away +} + +?> diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php index 1d356760..44f4f83b 100644 --- a/includes/dbtypes/user.class.php +++ b/includes/dbtypes/user.class.php @@ -26,7 +26,7 @@ class UserList extends DBTypeList foreach ($this->iterate() as $userId => $__) { $data[$this->curTpl['username']] = array( - 'border' => 0, // border around avatar (rarityColors) + 'border' => $this->getPremiumborder(), 'roles' => $this->curTpl['userGroups'], 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), 'posts' => 0, // forum posts @@ -47,22 +47,39 @@ class UserList extends DBTypeList $data[$this->curTpl['username']]['avatarmore'] = $this->curTpl['wowicon']; break; case 2: - if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> 2', $userId)) + if ($this->isPremium()) { - $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; - $data[$this->curTpl['username']]['avatarmore'] = $av; + if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> ?d', $userId, AvatarMgr::STATUS_REJECTED)) + { + $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; + $data[$this->curTpl['username']]['avatarmore'] = $av; + } } break; } // more optional data // sig: markdown formated string (only used in forum?) - // border: seen as null|1|3 .. changes the border around the avatar (i suspect its meaning changed and got decoupled from premium-status with the introduction of patreon-status) } return [Type::USER => $data]; } + // seen as null|1|3 .. changes the border around the avatar (chosen from account > premium tab?) + // changed at the end of MoP. No longer a jsBool but index to Icon.premiumBorderClasses + private function getPremiumBorder() : int + { + if (!$this->isPremium() || !$this->curTpl['avatar']) + return 2; // 2 is "none" + + return $this->curTpl['avatarborder']; + } + + public function isPremium() : bool + { + return $this->curTpl['userGroups'] & U_GROUP_PREMIUM || $this->curTpl['reputation'] >= Cfg::get('REP_REQ_PREMIUM'); + } + public function getListviewData() : array { return []; } public function renderTooltip() : ?string { return null; } diff --git a/includes/user.class.php b/includes/user.class.php index 5fca60fe..ebf180af 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -24,6 +24,7 @@ class User private static int $reputation = 0; private static string $dataKey = ''; private static int $excludeGroups = 1; + private static int $avatarborder = 2; // 2 is default / reputation colored private static ?LocalProfileList $profiles = null; public static function init() @@ -62,7 +63,7 @@ class User $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id()); $userData = DB::Aowow()->selectRow( - 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug` + 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`, a.`avatar`, a.`avatarborder` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` @@ -119,6 +120,7 @@ class User self::$status = $userData['status']; self::$debug = $userData['debug']; self::$email = $userData['email']; + self::$avatarborder = $userData['avatarborder']; if (Cfg::get('PROFILER_ENABLE')) { @@ -129,6 +131,18 @@ class User self::$profiles = (new LocalProfileList($conditions)); } + // reset premium options + if (!self::isPremium()) + { + if ($userData['avatar'] == 2) + { + DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1 WHERE `id` = ?d', self::$id); + DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = 0 WHERE `userId` = ?d', self::$id); + } + + // avatar borders + // do not reset, it's just not sent to the browser + } // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) // - consecutive visits @@ -482,7 +496,7 @@ class User public static function isPremium() : bool { - return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM'); + return !self::isBanned() && (self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM')); } public static function isLoggedIn() : bool @@ -568,14 +582,14 @@ class User if (self::$debug) $gUser['debug'] = true; // csv id-list output option on listviews - if (self::getPremiumBorder()) - $gUser['settings'] = ['premiumborder' => 1]; + if (self::isPremium()) + { + $gUser['premium'] = 1; + $gUser['settings'] = ['premiumborder' => self::$avatarborder]; + } else $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ? - if (self::isPremium()) - $gUser['premium'] = 1; - if ($_ = self::getProfilerExclusions()) $gUser = array_merge($gUser, $_); @@ -717,12 +731,6 @@ class User return $data; } - - // not sure what to set .. user selected? - public static function getPremiumBorder() : bool - { - return self::isInGroup(U_GROUP_PREMIUM); - } } ?> diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 23ff084b..c1e1e179 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent 'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "Neuen Avatar hochladen", + 'goToManager' => "Zur Avatarverwaltung gehen", + 'manageAvatars' => "Avatare verwalten", + 'avatarSlots' => '%1$d / %2$d Avatarplätze belegt', + 'manageBorders' => "Premium Rahmen verwalten", + 'selectAvatar' => "Wählt einen Avatar zum hochladen.", + 'errTooSmall' => "Euer Avatar muss wenigstens %dpx groß sein.", + 'cropAvatar' => "Ihr könnt Euren Avatar zuschneiden.", + 'avatarSubmit' => "Avatar-Einsendung", + 'reminder' => "Erinnerung", + 'avatarCoC' => "Dass Benutzen von Bildern, die gegen die Regeln verstoßen kann zum Verlust Eures Premium-Status führen.", + // settings 'settings' => "Kontoeinstellungen", 'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 670ec5e9..9ec1ff02 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Your new password must be different than your previous one.", // message_newpassdifferent 'newMailDiff' => "Your new email address must be different than your previous one.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "Upload new Avatar", + 'goToManager' => "Go to Avatar Manager", + 'manageAvatars' => "Manage Avatars", + 'avatarSlots' => 'Using %1$d / %2$d avatar slots', + 'manageBorders' => "Manage Premium Borders", + 'selectAvatar' => "Please select the avatar to upload.", + 'errTooSmall' => "Your avatar must be at last %dpx in size.", + 'cropAvatar' => "You may crop your avatar.", + 'avatarSubmit' => "Avatar Submission", + 'reminder' => "Reminder", + 'avatarCoC' => "Using imagery violating out terms of service may result in revocation of your premium privileges.", + // settings 'settings' => "Account Settings", 'settingsNote' => "Simply use the forms below to update your account information.", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 9e5af257..e281220a 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior.",// message_newpassdifferent 'newMailDiff' => "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Mi cuenta", 'settingsNote' => "Simplemente usa el siguiente formulario para actualizar la información de tu cuenta.", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index cd982692..2d19e8cb 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien.", // message_newpassdifferent 'newMailDiff' => "Votre nouvelle adresse courriel doit être différente de l'ancienne.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Mon compte", 'settingsNote' => "Veuillez utiliser les formulaires ci-dessous pour apporter des changements.", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 5f702169..9238b977 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Прежний и новый пароли не должны совпадать.", // message_newpassdifferent 'newMailDiff' => "Прежний и новый e-mail адреса не должны совпадать.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Параметры учетной записи", 'settingsNote' => "Используйте нижеприведённую форму, чтобы обновить информацию о вашей учетной записи.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index e184e6dd..c72151a8 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "你的新密码必须与以前的密码不同。", // message_newpassdifferent 'newMailDiff' => "您的新邮箱地址必须不同于旧地址。", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "账号设置", 'settingsNote' => "使用下列表格就能升级您的账号信息。", diff --git a/setup/tools/clisetup/filegen.us.php b/setup/tools/clisetup/filegen.us.php index 0d7ac502..70188e42 100644 --- a/setup/tools/clisetup/filegen.us.php +++ b/setup/tools/clisetup/filegen.us.php @@ -39,6 +39,7 @@ CLISetup::registerUtility(new class extends UtilityScript 'static/uploads/screenshots/thumb/', 'static/uploads/temp/', 'static/uploads/guide/images/', + 'static/uploads/avatars/' ); public function __construct() diff --git a/setup/updates/1758578400_15.sql b/setup/updates/1758578400_15.sql new file mode 100644 index 00000000..32ee0455 --- /dev/null +++ b/setup/updates/1758578400_15.sql @@ -0,0 +1,3 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_max_avatar_uploads'; +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('acc_max_avatar_uploads', 10, 10, 3, 129, 'premium users may upload this many avatars'); diff --git a/setup/updates/1758578400_16.sql b/setup/updates/1758578400_16.sql new file mode 100644 index 00000000..4b2f0d43 --- /dev/null +++ b/setup/updates/1758578400_16.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `avatarborder` tinyint unsigned NOT NULL DEFAULT 2 AFTER `avatar`; diff --git a/static/css/aowow.css b/static/css/aowow.css index 9d9f653b..7692cdd9 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -1161,7 +1161,8 @@ span.iconblizzard { .iconsmall-premiumred del { background-image:url(../images/Icon/small/border/premiumred.png); } .iconmedium-premiumred del { background-image:url(../images/Icon/medium/border/premiumred.png); } .iconlarge-premiumred del { - background-image:url(../images/logos/special/subscribe/patron-icon.png); + background-image:url(../images/Icon/large/border/premiumred.png); +/* background-image:url(../images/logos/special/subscribe/patron-icon.png); aowow - yeah, no */ height:85px; } diff --git a/static/js/global.js b/static/js/global.js index af34dd52..b389cbd3 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -1124,6 +1124,25 @@ function g_GetStaffColorFromRoles(roles) { return ''; } +// aowow - stand in for WH.User.getCommentRoleLabel +function g_GetCommentRoleLabel(roles, title) { + if (title) { + return title; + } + + if (roles & U_GROUP_ADMIN) { + return g_user_roles[2]; // LANG.administrator_abbrev + } + else if (roles & U_GROUP_MOD) { + return g_user_roles[4]; // LANG.moderator + } + else if (roles & U_GROUP_PREMIUMISH) { + return LANG.premiumuser; + } + + return null; +}; + function g_formatDate(sp, elapsed, theDate, time, alone) { var today = new Date(); var event_day = new Date(); @@ -13957,6 +13976,49 @@ Listview.templates = { $(div).show(); }, + applyAuthorTitle: function (container, title) + { + if (!title.label) + return; + + let cssClass = ['comment-reply-author-label'].concat(title.classes); + + $WH.ae(container, $WH.ct(' ')); // aowow - LANG.wordspace_punct + + if (title.url) + $WH.ae(container, $WH.ce('a', { className: cssClass.join(' '), href: title.url }, $WH.ct(`<${ title.label }>`))); + else + $WH.ae(container, $WH.ce('span', { className: cssClass.join(' ') }, $WH.ct(`<${ title.label }>`))); + }, + + getAuthorTitle: function (author) + { + let title = { + classes: [], + label: undefined, + url: undefined + }; + + if (g_pageInfo.author === author) { + title.label = LANG.guideAuthor; + return title; + } + + let user = g_users[author]; + if (user) { + // aowow - let roleColor = WH.User.getCommentTitleClass(_.roles, _.tierClass, user); + let roleColor = g_GetStaffColorFromRoles(user.roles); + if (roleColor) { + title.classes.push(roleColor); + } + // aowow - title.label = WH.User.getCommentRoleLabel(user.roles, user.title, user.tierTitle); + title.label = g_GetCommentRoleLabel(user.roles, user.title); + title.url = /* user.tierTitle && !user.title ? '/?premium' : */ ''; // aowow - tierTitle being the premium tier ("Rare|Epic|Legendary Premium User") + } + + return title; + }, + updateReplies: function(comment) { this.updateRepliesCell(comment); @@ -14063,7 +14125,13 @@ Listview.templates = { row.attr('data-replyid', reply.id); row.attr('data-idx', i); - row.find('.reply-text').addClass(g_GetStaffColorFromRoles(reply.roles)); + + // aowow - let cssClass = WH.User.getCommentRoleClass(reply.roles, reply.username); + let cssClass = g_GetStaffColorFromRoles(reply.roles); + if (!['comment-blue', 'comment-green'].includes(cssClass) && owner) { + cssClass = 'comment-green'; // comment-guide-author + } + row.find('.reply-text').addClass(cssClass); var replyWhen = $(''); replyWhen.text(g_formatDate(null, elapsed, creationDate)); @@ -14074,12 +14142,7 @@ Listview.templates = { replyByUserLink.attr('href', '?user=' + reply.username); replyByUserLink.text(reply.username); replyBy.append(replyByUserLink); - - if (owner) - { - $WH.ae(replyBy[0], $WH.ct(' ')); - $WH.ae(replyBy[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>'))); - } + this.applyAuthorTitle(replyBy[0], this.getAuthorTitle(reply.username)) replyBy.append(' ').append(replyWhen).append(' ').append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(creationDate))); @@ -14170,7 +14233,6 @@ Listview.templates = { updateCommentAuthor: function(comment, container) { var user = g_users[comment.user]; - let owner = g_pageInfo.author === comment.user; var postedOn = new Date(comment.date); var elapsed = (g_serverTime - postedOn) / 1000; @@ -14178,12 +14240,18 @@ Listview.templates = { container.append(LANG.lvcomment_by); container.append($WH.sprintf('$2', comment.user, comment.user)); - if (owner) - { - $WH.ae(container[0], $WH.ct(' ')); - $WH.ae(container[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>'))); + // aowow - avatar recovered and transplanted from commentsv1 version + if (user != null && user.avatar) { + var icon = Icon.createUser(user.avatar, user.avatarmore, 0, null, (user.roles & U_GROUP_PREMIUM) ? user.border : Icon.STANDARD_BORDER, 0, Icon.getPrivilegeBorder(user.reputation)); + icon.style.marginRight = '3px'; + icon.style.cssFloat = 'left'; + + container.css('lineHeight', '25px'); + container.append(icon); } + // aowow - end recover container.append(g_getReputationPlusAchievementText(user.gold, user.silver, user.copper, user.reputation)); + this.applyAuthorTitle(container[0], this.getAuthorTitle(comment.user)); container.append($WH.sprintf(' $3', comment.id, comment.id, g_formatDate(null, elapsed, postedOn))); container.append(' '); container.append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(postedOn))); diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php index fe9b076b..54d67966 100644 --- a/template/pages/account.tpl.php +++ b/template/pages/account.tpl.php @@ -146,9 +146,7 @@ if ($this->bans):
    - +
    -user::isInGroup(U_GROUP_PREMIUM) && 0): ?> +user::isInGroup(U_GROUP_PREMIUM)): ?> avMode == 2 ? ' checked="checked"' : '');?> />   
    @@ -226,7 +224,7 @@ if ($this->bans):
    - Go to Avatar Manager +
    @@ -255,18 +253,46 @@ if ($this->bans):
    • '.Lang::account('inactive'); ?>
    • '.Lang::account('active'); ?>
    -Manage Avatars +

    -

    Manage Premium Borders

    - Todo - - +

    + premiumborderMessage): ?> +
    + + +
    +
    +
    +
    + +
    +
    + + + @@ -280,9 +306,7 @@ if ($this->bans): _.add('', {id: 'premium'}); _.flush(); - +
    diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php new file mode 100644 index 00000000..59d130f7 --- /dev/null +++ b/template/pages/image-crop.tpl.php @@ -0,0 +1,45 @@ +brick('header'); +?> +
    +
    +
    + +brick('announcement'); + +$this->brick('pageTemplate'); +?> +
    +

    h1; ?>

    + + +
    +
    + +
    + +
    + +
    + +
    +
    + +

    +
    + + + +
    +
    +
    +
    + +brick('footer'); ?> From a48e94cd8bc8c2c9a9283253b243f64e3b2686e1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:49:03 +0200 Subject: [PATCH 694/957] Template/Update (Part 46 - VI) * account management rework: Delete account --- endpoints/account/confirm-delete.php | 128 ++++++++++++++++++ endpoints/account/delete.php | 71 ++++++++++ .../response/templateresponse.class.php | 8 +- includes/defines.php | 3 +- includes/user.class.php | 5 +- localization/locale_dede.php | 2 + localization/locale_enus.php | 2 + localization/locale_eses.php | 2 + localization/locale_frfr.php | 2 + localization/locale_ruru.php | 2 + localization/locale_zhcn.php | 2 + static/css/delete.css | 42 ++++++ .../confirm-delete-account_0.tpl.php | 26 ++++ .../confirm-delete-account_2.tpl.php | 26 ++++ .../confirm-delete-account_3.tpl.php | 26 ++++ .../confirm-delete-account_4.tpl.php | 26 ++++ .../confirm-delete-account_6.tpl.php | 26 ++++ .../confirm-delete-account_8.tpl.php | 26 ++++ template/localized/delete-account_0.tpl.php | 15 ++ template/localized/delete-account_2.tpl.php | 15 ++ template/localized/delete-account_3.tpl.php | 15 ++ template/localized/delete-account_4.tpl.php | 15 ++ template/localized/delete-account_6.tpl.php | 15 ++ template/localized/delete-account_8.tpl.php | 15 ++ template/mails/delete-account_0.tpl | 21 +++ template/mails/delete-account_2.tpl | 21 +++ template/mails/delete-account_3.tpl | 21 +++ template/mails/delete-account_4.tpl | 21 +++ template/mails/delete-account_6.tpl | 21 +++ template/mails/delete-account_8.tpl | 21 +++ template/pages/delete.tpl.php | 26 ++++ 31 files changed, 661 insertions(+), 6 deletions(-) create mode 100644 endpoints/account/confirm-delete.php create mode 100644 endpoints/account/delete.php create mode 100644 static/css/delete.css create mode 100644 template/localized/confirm-delete-account_0.tpl.php create mode 100644 template/localized/confirm-delete-account_2.tpl.php create mode 100644 template/localized/confirm-delete-account_3.tpl.php create mode 100644 template/localized/confirm-delete-account_4.tpl.php create mode 100644 template/localized/confirm-delete-account_6.tpl.php create mode 100644 template/localized/confirm-delete-account_8.tpl.php create mode 100644 template/localized/delete-account_0.tpl.php create mode 100644 template/localized/delete-account_2.tpl.php create mode 100644 template/localized/delete-account_3.tpl.php create mode 100644 template/localized/delete-account_4.tpl.php create mode 100644 template/localized/delete-account_6.tpl.php create mode 100644 template/localized/delete-account_8.tpl.php create mode 100644 template/mails/delete-account_0.tpl create mode 100644 template/mails/delete-account_2.tpl create mode 100644 template/mails/delete-account_3.tpl create mode 100644 template/mails/delete-account_4.tpl create mode 100644 template/mails/delete-account_6.tpl create mode 100644 template/mails/delete-account_8.tpl create mode 100644 template/pages/delete.tpl.php diff --git a/endpoints/account/confirm-delete.php b/endpoints/account/confirm-delete.php new file mode 100644 index 00000000..b5d0b6b9 --- /dev/null +++ b/endpoints/account/confirm-delete.php @@ -0,0 +1,128 @@ + [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + protected array $expectedPOST = array( + 'submit' => [FILTER_UNSAFE_RAW ], + 'cancel' => [FILTER_UNSAFE_RAW ], + 'confirm' => [FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], + 'key' => [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + public bool $confirm = true; // just to select the correct localized brick + public string $username = ''; + public string $deleteFormTarget = '?account=confirm-delete'; + public ?array $inputbox = null; + public string $key = ''; + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('accDelete')); + + $this->username = User::$username; + + parent::generate(); + + $msg = Lang::account('inputbox', 'error', 'purgeTokenUsed'); + + // display default confirm template + if ($this->assertGET('key') && DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_get['key'])) + { + $this->key = $this->_get['key']; + return; + } + + // perform action and display status + if ($this->assertPOST('key') && ($userId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_post['key']))) + { + if ($this->_post['cancel']) + $msg = $this->cancel($userId); + else if ($this->_post['submit'] && $this->_post['confirm']) + $msg = $this->purge($userId); + } + + // throw error and display in status + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg + )]; + } + + private function cancel(int $userId) : string + { + if (DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "" WHERE `id` = ?d', ACC_STATUS_NONE, $userId)) + { + $this->success = true; + return Lang::account('inputbox', 'message', 'deleteCancel'); + } + + return Lang::main('intError'); + } + + private function purge(int $userId) : string + { + // empty all user settings and cookies + DB::Aowow()->query('DELETE FROM ?_account_cookies WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_reputation WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `userId` = ?d', $userId); // cascades to aowow_account_weightscale_data + + // delete profiles, unlink chars + DB::Aowow()->query('DELETE pp FROM ?_profiler_profiles pp JOIN ?_account_profiles ap ON ap.`profileId` = pp.`id` WHERE ap.`accountId` = ?d', $userId); + // DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d', $userId); // already deleted via FK? + + // delete all sessions and bans + DB::Aowow()->query('DELETE FROM ?_account_banned WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `userId` = ?d', $userId); + + // delete forum posts (msg: This post was from a user who has deleted their account. (no translations at src); comments/replies are unaffected) + // ... + + // replace username with userId and empty fields + DB::Aowow()->query( + 'UPDATE ?_account SET + `login` = "", `passHash` = "", `username` = `id`, `email` = NULL, `userGroups` = 0, `userPerms` = 0, + `curIp` = "", `prevIp` = "", `curLogin` = 0, `prevLogin` = 0, + `locale` = 0, `debug` = 0, `avatar` = 0, `wowicon` = "", `title` = "", `description` = "", `excludeGroups` = 0, + `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "", `renameCooldown` = 0 + WHERE `id` = ?d', + ACC_STATUS_DELETED, $userId + ); + + $this->success = true; + return Lang::account('inputbox', 'message', 'deleteOk'); + } +} + +?> diff --git a/endpoints/account/delete.php b/endpoints/account/delete.php new file mode 100644 index 00000000..34da70e2 --- /dev/null +++ b/endpoints/account/delete.php @@ -0,0 +1,71 @@ + ['filter' => FILTER_UNSAFE_RAW] + ); + + public string $username = ''; + public string $deleteFormTarget = '?account=delete'; + public ?array $inputbox = null; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('accDelete')); + + parent::generate(); + + $this->username = User::$username; + + if ($this->_post['proceed']) + { + $error = false; + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` NOT IN (?a) AND `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', [ACC_STATUS_NEW, ACC_STATUS_NONE, ACC_STATUS_PURGING], User::$id)) + { + $token = Util::createHash(40); + + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + ACC_STATUS_PURGING, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id); + + Util::sendMail(User::$email, 'delete-account', [$token, User::$email, User::$username]); + } + else + $error = true; + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $error ? 'error' : 'success'), + 'message' => $error ? '' : Lang::account('inputbox', 'message', 'deleteAccSent', [User::$email]), + 'error' => $error ? Lang::account('inputbox', 'error', 'isRecovering') : '' + )]; + } + } +} + +?> diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index bef62121..bc2cda60 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -472,14 +472,16 @@ class TemplateResponse extends BaseResponse 'viError' => $_SESSION['error']['vi'] ?? null ); + // we cannot blanket NUMERIC_CHECK the data as usernames of deleted users are their id which does not support String.lower() + if ($this->contribute & CONTRIBUTE_CO) - $community['co'] = Util::toJSON(CommunityContent::getComments($this->type, $this->typeId)); + $community['co'] = Util::toJSON(CommunityContent::getComments($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); if ($this->contribute & CONTRIBUTE_SS) - $community['ss'] = Util::toJSON(CommunityContent::getScreenshots($this->type, $this->typeId)); + $community['ss'] = Util::toJSON(CommunityContent::getScreenshots($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); if ($this->contribute & CONTRIBUTE_VI) - $community['vi'] = Util::toJSON(CommunityContent::getVideos($this->type, $this->typeId)); + $community['vi'] = Util::toJSON(CommunityContent::getVideos($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); unset($_SESSION['error']); diff --git a/includes/defines.php b/includes/defines.php index 29900e2e..eb18180f 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -69,7 +69,8 @@ define('ACC_STATUS_RECOVER_PASS', 3); // currently recover define('ACC_STATUS_CHANGE_EMAIL', 4); // currently changing contact email define('ACC_STATUS_CHANGE_PASS', 5); // currently changing password define('ACC_STATUS_CHANGE_USERNAME', 6); // currently changing username -define('ACC_STATUS_DELETED', 7); // is deleted - only a stub remains +define('ACC_STATUS_PURGING', 7); // deletion is pending +define('ACC_STATUS_DELETED', 99); // is deleted - only a stub remains // Session Status define('SESSION_ACTIVE', 1); diff --git a/includes/user.class.php b/includes/user.class.php index ebf180af..e310239f 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -280,10 +280,11 @@ class User 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() - WHERE { a.`email` = ? } { a.`login` = ? } + WHERE { a.`email` = ? } { a.`login` = ? } AND `status` <> ?d GROUP BY a.`id`', $email ?: DBSIMPLE_SKIP, - !$email ? $nameOrEmail : DBSIMPLE_SKIP + !$email ? $nameOrEmail : DBSIMPLE_SKIP, + ACC_STATUS_DELETED ); if (!$query) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index c1e1e179..3004a8d5 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Ihr Kennwort wurde erfolgreich geändert.", 'deleteAccSent' => "Eine E-Mail mit einem Bestätigungslink wurde an %s gesendet.", 'deleteOk' => "Ihr Konto wurde erfolgreich entfernt. Wir hoffen, Sie bald wiederzusehen!

    Sie können dieses Fenster jetzt schließen.", + 'deleteCancel' => "Die Kontolöschung wurde abgebrochen.", 'createAccSent' => 'Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um Euer Konto zu erstellen.

    Falls du keine Bestätigungsnachricht erhalten hast klicke hier um eine neue zu senden.', 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euren Benutzernamen zu erhalten.", 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euer Kennwort zurückzusetzen.", @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Dieser Schlüssel zur Änderung der E-Mail-Adresse wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', 'passTokenUsed' => 'Dieser Schlüssel zur Änderung des Kennworts wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'purgeTokenUsed' => 'Dieser Schlüssel zum Löschen des Kontos wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', 'passTokenLost' => "Kein Token wurde bereitgestellt. Wenn Sie in einer E-Mail einen Link zum Zurücksetzen des Kennworts erhalten haben, kopieren Sie die gesamte URL (einschließlich des Tokens am Ende) in die Adressleiste Ihres Browsers.", 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 9ec1ff02..9c239530 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Your password has been changed successfully.", 'deleteAccSent' => "An email has been sent to %s with confirmation link attached.", 'deleteOk' => "Your account has been successfully removed. We hope to see you again soon!

    You may now close this window.", + 'deleteCancel' => "Account deletion was canceled.", 'createAccSent' => 'An email was sent to %s. Simply follow the instructions to create your account.

    If you don\'t receive the verification email, click here to send another one.', 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Either that email change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', 'passTokenUsed' => 'Either that password change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', + 'purgeTokenUsed' => 'Either that account delete key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', 'passTokenLost' => "No token was provided. If you received a reset password link in an email, please copy and paste the entire URL (including the token at the end) into your browser's location bar.", 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index e281220a..0b178b77 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Tu contraseña ha sido cambiada correctamente.", 'deleteAccSent' => "Se ha enviado un correo electrónico a %s con el enlace de confirmación adjunto.", 'deleteOk' => "Tu cuenta ha sido eliminada correctamente. ¡Esperamos verte de nuevo pronto!

    Ahora puedes cerrar esta ventana.", + 'deleteCancel' => "La eliminación de la cuenta fue cancelada.", 'createAccSent' => 'Un correo fue enviado a %s. Sigue las instrucciones para crear tu cuenta.

    Si no recibes el correo de verificación, haz clic aquí para enviar otro.', 'recovUserSent' => "Un correo fue enviado a %s. Sigue las instrucciones para recuperar tu nombre de usuario.", 'recovPassSent' => "Un correo fue enviado a %s. Sigue las instrucciones para restablecer tu contraseña." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Ese código de cambio de correo electrónico ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', 'passTokenUsed' => 'Ese código de cambio de contraseña ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', + 'purgeTokenUsed' => 'Esa clave de eliminación de cuenta ya ha sido usada, o no es válida. Visita tu página de configuración de cuenta para intentarlo de nuevo.', 'passTokenLost' => "No se recibió ningún código de petición. Si recibiste un enlace para restablecer tu contraseña por correo, por favor copia y pega la dirección completa (incluyendo el código del final) en la barra de dirección de tu navegador.", 'isRecovering' => "Esta cuenta ya se encuentra en proceso de recuperación. Sigue las instrucciones en tu correo o espera %s para que el token expire.", 'loginExceeded' => "Has excedido la cantidad de inicios de sesión con esta IP. Por favor intenta en %s.", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 2d19e8cb..ffa2d017 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Votre mot de passe a été changé avec succès.", 'deleteAccSent' => "Un courriel a été envoyé à %s avec le lien de confirmation.", 'deleteOk' => "Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt !

    Vous pouvez maintenant fermer cette fenêtre.", + 'deleteCancel' => "La suppression du compte a été annulée.", 'createAccSent' => 'Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu\'il contient pour créer votre compte.

    Si vous ne recevez pas l\'email de vérification, cliquez ici pour en envoyer un autre.', 'recovUserSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour récupérer votre nom d'utilisateur.", 'recovPassSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour réinitialiser votre mot de passe." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => "Cette clé de changement d'adresse courriel a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", 'passTokenUsed' => "Cette clé de changement de mot de passe a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", + 'purgeTokenUsed' => "Cette clé de suppression de compte a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", 'passTokenLost' => "Aucun jeton n'a été fourni. Si vous avez reçu un lien de réinitialisation du mot de passe dans un courriel, merci de copier et coller l'URL entière (y compris le jeton à la fin) dans la barre d'adresse de votre navigateur.", 'isRecovering' => "Ce compte est déjà en train d'être récupéré. Suivez les instruction dans l'email reçu ou attendez %s pour que le token expire.", 'loginExceeded' => "Le nombre maximum de connections depuis cette IP a été dépassé. Essayez de nouevau dans %s.", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 9238b977..72e8a01c 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Ваш пароль был успешно изменен.", 'deleteAccSent' => "Письмо с подтверждением было отправлено на %s.", 'deleteOk' => "Ваша учетная запись была успешно удалена. Надеемся увидеть вас снова!

    Теперь вы можете закрыть это окно.", + 'deleteCancel' => "Удаление учетной записи было отменено.", 'createAccSent' => 'Письмо с инструкциями для активации учетной записи было отправлено на адрес %s/b>. Следуйте инструкциям, для продолжения регистрации.

    Если вы не получили письмо для подтверждения, нажмите здесь, чтобы отправить его повторно.', 'recovUserSent' => "Письмо с инструкциями для активации учетной записи было отправлено на адрес %s/b>. Просто следуйте инструкциям для восстановления имени пользователя.", 'recovPassSent' => "Письмо с инструкциями для активации учетной записи было отправлено на адрес %s/b>. Просто следуйте инструкциям для сброса пароля." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Этот ключ для смены email уже был использован или недействителен. Посетите вашу страницу настроек учетной записи, чтобы попробовать снова.', 'passTokenUsed' => 'Этот ключ для смены пароля уже был использован или недействителен. Посетите вашу страницу настроек учетной записи, чтобы попробовать снова.', + 'purgeTokenUsed' => 'Этот ключ для удаления учетной записи уже был использован или недействителен. Посетите вашу страницу настроек учетной записи, чтобы попробовать снова.', 'passTokenLost' => "Ключ не был получен. Если вы сбросили пароль по ссылке из письма, отправленного на email, пожалуйста, скопируйте URL целиком и вставьте в адресную строку (включая ключ, указанный в конце ссылки).", 'isRecovering' => "Эта учетная запись уже восстанавливается. Следуйте инструкциям в письме или дождитесь истечения срока действия токена через %s.", 'loginExceeded' => "Достигнуто максимальное количество попыток входа с этого IP. Пожалуйста, попробуйте снова через %s.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index c72151a8..34b40587 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "您的密码已成功更改。", 'deleteAccSent' => "已向 %s 发送了一封带有确认链接的邮件。", 'deleteOk' => "您的账户已成功删除。希望不久后能再次见到您!

    您现在可以关闭此窗口。", + 'deleteCancel' => "账户删除已取消。", 'createAccSent' => '电子邮件发送到%s。只请按照说明创建您的账户。

    如果您没有收到验证邮件,点击这里重新发送。', 'recovUserSent' => "电子邮件发送到%s。只请按照说明恢复您的用户名。", 'recovPassSent' => "电子邮件发送到%s。只请按照说明重置您的密码。" @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => '该邮箱更改密钥已被使用,或不是有效密钥。请访问您的账户设置页面重新尝试。', 'passTokenUsed' => '该密码更改密钥已被使用,或不是有效密钥。请访问您的账户设置页面重新尝试。', + 'purgeTokenUsed' => '该账户删除密钥已被使用,或不是有效密钥。请访问您的账户设置页面重新尝试。', 'passTokenLost' => "未提供令牌。如果您在邮件中收到重置密码链接,请将整个网址(包括最后的令牌)复制并粘贴到浏览器地址栏中。", 'isRecovering' => "此帐户已恢复。按照电子邮件中的说明或等待%s后令牌过期。", 'loginExceeded' => "这个IP最大登录次数已超过。请在%s后再次尝试。", diff --git a/static/css/delete.css b/static/css/delete.css new file mode 100644 index 00000000..257d8d12 --- /dev/null +++ b/static/css/delete.css @@ -0,0 +1,42 @@ +.account-delete-box { + background: #161616; + border-radius: 3px; + box-sizing: border-box; + line-height: 1.7em; + margin: 0 auto; + max-width: 100%; + padding: 15px; + width: 34em; +} + +.account-delete-box [class^="heading-size-"] { + margin-top: 0; + text-align: center; +} + +.account-delete-box-warning, +.account-delete-box-alternative { + font-size: 175%; + line-height: 1.2; +} + +.account-delete-box-warning, +.account-delete-box-warning * { + color: #ff4040 !important +} + +.account-delete-box-warning b { + display: block; + font-size: 200%; + font-weight: 900; + text-align: center; +} + +.account-delete-box-confirm { + text-align: center; +} + +/* from global.css */ +.text p { + margin: 10px 0px; +} diff --git a/template/localized/confirm-delete-account_0.tpl.php b/template/localized/confirm-delete-account_0.tpl.php new file mode 100644 index 00000000..db78173a --- /dev/null +++ b/template/localized/confirm-delete-account_0.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_2.tpl.php b/template/localized/confirm-delete-account_2.tpl.php new file mode 100644 index 00000000..74e8b617 --- /dev/null +++ b/template/localized/confirm-delete-account_2.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_3.tpl.php b/template/localized/confirm-delete-account_3.tpl.php new file mode 100644 index 00000000..637f018f --- /dev/null +++ b/template/localized/confirm-delete-account_3.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_4.tpl.php b/template/localized/confirm-delete-account_4.tpl.php new file mode 100644 index 00000000..e4c023c3 --- /dev/null +++ b/template/localized/confirm-delete-account_4.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_6.tpl.php b/template/localized/confirm-delete-account_6.tpl.php new file mode 100644 index 00000000..18f58f57 --- /dev/null +++ b/template/localized/confirm-delete-account_6.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_8.tpl.php b/template/localized/confirm-delete-account_8.tpl.php new file mode 100644 index 00000000..af869141 --- /dev/null +++ b/template/localized/confirm-delete-account_8.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/delete-account_0.tpl.php b/template/localized/delete-account_0.tpl.php new file mode 100644 index 00000000..633484f0 --- /dev/null +++ b/template/localized/delete-account_0.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_2.tpl.php b/template/localized/delete-account_2.tpl.php new file mode 100644 index 00000000..5f34b9b5 --- /dev/null +++ b/template/localized/delete-account_2.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_3.tpl.php b/template/localized/delete-account_3.tpl.php new file mode 100644 index 00000000..15db9de0 --- /dev/null +++ b/template/localized/delete-account_3.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_4.tpl.php b/template/localized/delete-account_4.tpl.php new file mode 100644 index 00000000..987545c9 --- /dev/null +++ b/template/localized/delete-account_4.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_6.tpl.php b/template/localized/delete-account_6.tpl.php new file mode 100644 index 00000000..12544c3f --- /dev/null +++ b/template/localized/delete-account_6.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_8.tpl.php b/template/localized/delete-account_8.tpl.php new file mode 100644 index 00000000..b8b84244 --- /dev/null +++ b/template/localized/delete-account_8.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/mails/delete-account_0.tpl b/template/mails/delete-account_0.tpl new file mode 100644 index 00000000..7bbef9db --- /dev/null +++ b/template/mails/delete-account_0.tpl @@ -0,0 +1,21 @@ +# 2025 +Please verify your request to be forgotten +Greetings, + +We’ve just received a request to exercise the “right to be forgotten” from the following email address %2$s in accordance with our Privacy Policy. + +Please click on following link HOST_URL?account=confirm-delete&key=%1$s to confirm your selection. You will get one last chance to review your choices once you are back on the site. + +Should you choose to proceed with this process, we will permanently delete or anonymize any Personal Data linked to your account. + +This information will include, but is not limited to: + + * Your Identity %3$s, and the email address associated with this login. + * Your current Premium status and data, should you be a Premium member. + * Your profile information and preferences. + * In some cases, content that you've authored, including comments, guides and forum posts. + * Note that game data connected to your gaming identities will re-appear when other users request data updates, unless you delete that data at the source. + +Once we receive your final confirmation, we will be removing your Personal Data. + +If you have any questions or need further assistance, please contact CONTACT_EMAIL. diff --git a/template/mails/delete-account_2.tpl b/template/mails/delete-account_2.tpl new file mode 100644 index 00000000..7ccda5d2 --- /dev/null +++ b/template/mails/delete-account_2.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Veuillez vérifier votre demande de droit à l'oubli +Bonjour, + +Nous venons de recevoir une demande d'exercice du « droit à l'oubli » de l'adresse e-mail suivante %2$s conformément à notre politique de confidentialité. + +Veuillez cliquer sur le lien suivant HOST_URL?account=confirm-delete&key=%1$s pour confirmer votre choix. Vous aurez une dernière chance de revoir vos choix une fois de retour sur le site. + +Si vous choisissez de poursuivre ce processus, nous supprimerons ou anonymiserons définitivement toutes les données personnelles liées à votre compte. + +Ces informations incluront, sans s'y limiter : + + * Votre identité %3$s, et l'adresse e-mail associée à cette connexion. + * Votre statut Premium actuel et les données, si vous êtes membre Premium. + * Vos informations de profil et préférences. + * Dans certains cas, le contenu que vous avez créé, y compris les commentaires, guides et messages sur le forum. + * Notez que les données de jeu liées à vos identités de jeu réapparaîtront lorsque d'autres utilisateurs demanderont des mises à jour de données, sauf si vous supprimez ces données à la source. + +Une fois que nous aurons reçu votre confirmation finale, nous supprimerons vos données personnelles. + +Si vous avez des questions ou besoin d'aide supplémentaire, veuillez contacter CONTACT_EMAIL. diff --git a/template/mails/delete-account_3.tpl b/template/mails/delete-account_3.tpl new file mode 100644 index 00000000..b8ce03a4 --- /dev/null +++ b/template/mails/delete-account_3.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Bitte bestätigen Sie Ihre Anfrage auf Vergessenwerden +Hallo, + +Wir haben gerade eine Anfrage zum "Recht auf Vergessenwerden" von der folgenden E-Mail-Adresse %2$s gemäß unserer Datenschutzrichtlinie erhalten. + +Bitte klicken Sie auf den folgenden Link HOST_URL?account=confirm-delete&key=%1$s, um Ihre Auswahl zu bestätigen. Sie erhalten eine letzte Gelegenheit, Ihre Auswahl zu überprüfen, sobald Sie wieder auf der Website sind. + +Wenn Sie sich entscheiden, diesen Prozess fortzusetzen, werden wir alle mit Ihrem Konto verknüpften personenbezogenen Daten dauerhaft löschen oder anonymisieren. + +Diese Informationen umfassen unter anderem: + + * Ihre Identität %3$s und die mit diesem Login verknüpfte E-Mail-Adresse. + * Ihren aktuellen Premium-Status und Daten, falls Sie ein Premium-Mitglied sind. + * Ihre Profilinformationen und Präferenzen. + * In einigen Fällen von Ihnen erstellte Inhalte, einschließlich Kommentare, Guides und Forenbeiträge. + * Beachten Sie, dass Spieldaten, die mit Ihren Spielidentitäten verbunden sind, wieder erscheinen, wenn andere Nutzer Datenaktualisierungen anfordern, es sei denn, Sie löschen diese Daten an der Quelle. + +Sobald wir Ihre endgültige Bestätigung erhalten haben, werden wir Ihre personenbezogenen Daten entfernen. + +Wenn Sie Fragen haben oder weitere Unterstützung benötigen, kontaktieren Sie bitte CONTACT_EMAIL. diff --git a/template/mails/delete-account_4.tpl b/template/mails/delete-account_4.tpl new file mode 100644 index 00000000..a1738dfe --- /dev/null +++ b/template/mails/delete-account_4.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +请验证您的被遗忘权请求 +您好, + +我们刚刚收到来自以下电子邮件地址 %2$s 的“被遗忘权”请求,依据我们的隐私政策。 + +请点击以下链接 HOST_URL?account=confirm-delete&key=%1$s 以确认您的选择。返回网站后,您将有最后一次机会审查您的选择。 + +如果您选择继续此流程,我们将永久删除或匿名化与您的账户相关的所有个人数据。 + +这些信息包括但不限于: + + * 您的身份 %3$s,以及与此登录关联的电子邮件地址。 + * 您当前的高级会员状态和数据(如适用)。 + * 您的个人资料信息和偏好设置。 + * 在某些情况下,您创作的内容,包括评论、指南和论坛帖子。 + * 请注意,与您的游戏身份相关的游戏数据在其他用户请求数据更新时会重新出现,除非您在源头删除这些数据。 + +一旦我们收到您的最终确认,我们将删除您的个人数据。 + +如有任何疑问或需要进一步帮助,请联系 CONTACT_EMAIL。 diff --git a/template/mails/delete-account_6.tpl b/template/mails/delete-account_6.tpl new file mode 100644 index 00000000..421351f6 --- /dev/null +++ b/template/mails/delete-account_6.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Por favor, verifique su solicitud de derecho al olvido +Saludos, + +Acabamos de recibir una solicitud para ejercer el "derecho al olvido" desde la siguiente dirección de correo electrónico %2$s de acuerdo con nuestra Política de Privacidad. + +Por favor, haga clic en el siguiente enlace HOST_URL?account=confirm-delete&key=%1$s para confirmar su selección. Tendrá una última oportunidad de revisar sus opciones una vez que regrese al sitio. + +Si decide continuar con este proceso, eliminaremos o anonimizaremos permanentemente cualquier dato personal vinculado a su cuenta. + +Esta información incluirá, pero no se limitará a: + + * Su identidad %3$s y la dirección de correo electrónico asociada a este inicio de sesión. + * Su estado Premium actual y datos, si es miembro Premium. + * Su información de perfil y preferencias. + * En algunos casos, contenido que haya creado, incluyendo comentarios, guías y publicaciones en foros. + * Tenga en cuenta que los datos de juego conectados a sus identidades de juego volverán a aparecer cuando otros usuarios soliciten actualizaciones de datos, a menos que elimine esos datos en la fuente. + +Una vez que recibamos su confirmación final, eliminaremos sus datos personales. + +Si tiene alguna pregunta o necesita más ayuda, por favor contacte a CONTACT_EMAIL. diff --git a/template/mails/delete-account_8.tpl b/template/mails/delete-account_8.tpl new file mode 100644 index 00000000..f9f916c4 --- /dev/null +++ b/template/mails/delete-account_8.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Пожалуйста, подтвердите ваш запрос на удаление данных +Здравствуйте, + +Мы только что получили запрос на реализацию "права быть забытым" с адреса электронной почты %2$s в соответствии с нашей Политикой конфиденциальности. + +Пожалуйста, перейдите по следующей ссылке HOST_URL?account=confirm-delete&key=%1$s, чтобы подтвердить свой выбор. После возвращения на сайт у вас будет последний шанс пересмотреть свое решение. + +Если вы решите продолжить процесс, мы навсегда удалим или анонимизируем все персональные данные, связанные с вашей учетной записью. + +Эта информация будет включать, но не ограничиваться: + + * Вашу личность %3$s и адрес электронной почты, связанный с этим входом. + * Ваш текущий статус и данные Premium, если вы являетесь Premium-участником. + * Вашу информацию профиля и предпочтения. + * В некоторых случаях созданный вами контент, включая комментарии, руководства и сообщения на форуме. + * Обратите внимание, что игровые данные, связанные с вашими игровыми идентификаторами, появятся снова, когда другие пользователи запросят обновление данных, если только вы не удалите эти данные у источника. + +После получения вашего окончательного подтверждения мы удалим ваши персональные данные. + +Если у вас есть вопросы или вам нужна дополнительная помощь, пожалуйста, свяжитесь с CONTACT_EMAIL. diff --git a/template/pages/delete.tpl.php b/template/pages/delete.tpl.php new file mode 100644 index 00000000..77753488 --- /dev/null +++ b/template/pages/delete.tpl.php @@ -0,0 +1,26 @@ +brick('header'); +?> +
    +
    +
    +brick('announcement'); + + $this->brick('pageTemplate'); + +if ($this->inputbox): + $this->brick(...$this->inputbox); // $templateName, [$templateVars] +elseif ($this->confirm): + $this->localizedBrick('confirm-delete-account'); +else: + $this->localizedBrick('delete-account'); +endif; +?> +
    +
    + + +brick('footer'); ?> From 6557e70d5c5e55d67f91ab416ab7a735c3cfcbdf Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 11 Aug 2025 16:00:18 +0200 Subject: [PATCH 695/957] Template/Update (Part 47) * split global.js into its components, so it can be reasonably processed by setup * make reputation requirements configurable * move Markup and Locale back into global.js (removed associated build scripts) * extend Icon to display iconId in lightbox popup --- .gitignore | 3 +- endpoints/class/class.php | 2 - endpoints/compare/compare.php | 1 - endpoints/icon/get-id-from-name.php | 29 + endpoints/item/item.php | 1 - endpoints/items/items.php | 2 +- endpoints/itemset/itemset.php | 2 +- endpoints/npc/npc.php | 2 +- endpoints/object/object.php | 2 - endpoints/pet/pet.php | 2 - endpoints/petcalc/petcalc.php | 1 - endpoints/profile/profile.php | 1 - endpoints/race/race.php | 2 - endpoints/search/search.php | 1 - endpoints/spell/spell.php | 2 - includes/cfg.class.php | 28 +- .../response/templateresponse.class.php | 2 - .../{locales.ss.php => global-js.ss.php} | 10 +- setup/tools/filegen/markup.ss.php | 24 - .../filegen/templates/global.js/0_user.js | 73 + .../tools/filegen/templates/global.js/ajax.js | 51 + .../filegen/templates/global.js/animations.js | 123 + .../templates/global.js/announcement.js | 157 + .../filegen/templates/global.js/audio.js | 377 + .../templates/global.js/clicktocopy.js | 122 + .../filegen/templates/global.js/comments.js | 459 + .../templates/global.js/conditionList.js | 329 + .../templates/global.js/contacttool.js | 550 + .../filegen/templates/global.js/cookies.js | 37 + .../filegen/templates/global.js/dialog.js | 568 + .../templates/global.js/dom_manipulation.js | 252 + .../filegen/templates/global.js/favorites.js | 262 + .../filegen/templates/global.js/guide.js | 368 + .../tools/filegen/templates/global.js/icon.js | 404 + .../filegen/templates/global.js/lightbox.js | 161 + .../tools/filegen/templates/global.js/line.js | 38 + .../filegen/templates/global.js/links.js | 138 + .../filegen/templates/global.js/listview.js | 5029 ++++ .../templates/global.js/listview_templates.js | 7070 +++++ .../filegen/templates/global.js/livesearch.js | 368 + .../{locale.js.in => global.js/locale.js} | 24 +- .../filegen/templates/global.js/mapper.js | 1113 + .../filegen/templates/global.js/mapviewer.js | 293 + .../{Markup.js.in => global.js/markup.js} | 1110 +- .../tools/filegen/templates/global.js/menu.js | 987 + .../filegen/templates/global.js/messagebox.js | 16 + .../templates/global.js/modelviewer.js | 676 + .../templates/global.js/pagetemplate.js | 630 + .../templates/global.js/positioning.js | 10 + .../filegen/templates/global.js/profiler.js | 19 + .../templates/global.js/progressbar.js | 122 + .../filegen/templates/global.js/rectangle.js | 35 + .../filegen/templates/global.js/redbutton.js | 48 + .../templates/global.js/screenshots.js | 624 + .../templates/global.js/search_lvbrowse.js | 84 + .../filegen/templates/global.js/showonmap.js | 5 + .../filegen/templates/global.js/slider.js | 289 + .../filegen/templates/global.js/summary.js | 78 + .../filegen/templates/global.js}/swfobject.js | 0 .../tools/filegen/templates/global.js/tabs.js | 364 + .../filegen/templates/global.js/tracking.js | 130 + .../filegen/templates/global.js/ui_ux.js | 887 + .../filegen/templates/global.js/utilities.js | 254 + .../filegen/templates/global.js/videos.js | 539 + .../tools/filegen/templates/global.js/vote.js | 28 + .../tools/filegen/templates/global.js/wow.js | 417 + .../tools/filegen/templates/global.js/wsa.js | 4 + setup/tools/setupScript.class.php | 49 +- setup/updates/1758578400_17.sql | 1 + static/js/Draggable.js | 134 +- static/js/global.js | 24060 ---------------- static/js/locale_zhcn.js | 2 +- static/js/video.js | 1870 +- 73 files changed, 26256 insertions(+), 25699 deletions(-) create mode 100644 endpoints/icon/get-id-from-name.php rename setup/tools/filegen/{locales.ss.php => global-js.ss.php} (86%) delete mode 100644 setup/tools/filegen/markup.ss.php create mode 100644 setup/tools/filegen/templates/global.js/0_user.js create mode 100644 setup/tools/filegen/templates/global.js/ajax.js create mode 100644 setup/tools/filegen/templates/global.js/animations.js create mode 100644 setup/tools/filegen/templates/global.js/announcement.js create mode 100644 setup/tools/filegen/templates/global.js/audio.js create mode 100644 setup/tools/filegen/templates/global.js/clicktocopy.js create mode 100644 setup/tools/filegen/templates/global.js/comments.js create mode 100644 setup/tools/filegen/templates/global.js/conditionList.js create mode 100644 setup/tools/filegen/templates/global.js/contacttool.js create mode 100644 setup/tools/filegen/templates/global.js/cookies.js create mode 100644 setup/tools/filegen/templates/global.js/dialog.js create mode 100644 setup/tools/filegen/templates/global.js/dom_manipulation.js create mode 100644 setup/tools/filegen/templates/global.js/favorites.js create mode 100644 setup/tools/filegen/templates/global.js/guide.js create mode 100644 setup/tools/filegen/templates/global.js/icon.js create mode 100644 setup/tools/filegen/templates/global.js/lightbox.js create mode 100644 setup/tools/filegen/templates/global.js/line.js create mode 100644 setup/tools/filegen/templates/global.js/links.js create mode 100644 setup/tools/filegen/templates/global.js/listview.js create mode 100644 setup/tools/filegen/templates/global.js/listview_templates.js create mode 100644 setup/tools/filegen/templates/global.js/livesearch.js rename setup/tools/filegen/templates/{locale.js.in => global.js/locale.js} (74%) create mode 100644 setup/tools/filegen/templates/global.js/mapper.js create mode 100644 setup/tools/filegen/templates/global.js/mapviewer.js rename setup/tools/filegen/templates/{Markup.js.in => global.js/markup.js} (84%) create mode 100644 setup/tools/filegen/templates/global.js/menu.js create mode 100644 setup/tools/filegen/templates/global.js/messagebox.js create mode 100644 setup/tools/filegen/templates/global.js/modelviewer.js create mode 100644 setup/tools/filegen/templates/global.js/pagetemplate.js create mode 100644 setup/tools/filegen/templates/global.js/positioning.js create mode 100644 setup/tools/filegen/templates/global.js/profiler.js create mode 100644 setup/tools/filegen/templates/global.js/progressbar.js create mode 100644 setup/tools/filegen/templates/global.js/rectangle.js create mode 100644 setup/tools/filegen/templates/global.js/redbutton.js create mode 100644 setup/tools/filegen/templates/global.js/screenshots.js create mode 100644 setup/tools/filegen/templates/global.js/search_lvbrowse.js create mode 100644 setup/tools/filegen/templates/global.js/showonmap.js create mode 100644 setup/tools/filegen/templates/global.js/slider.js create mode 100644 setup/tools/filegen/templates/global.js/summary.js rename {static/js => setup/tools/filegen/templates/global.js}/swfobject.js (100%) create mode 100644 setup/tools/filegen/templates/global.js/tabs.js create mode 100644 setup/tools/filegen/templates/global.js/tracking.js create mode 100644 setup/tools/filegen/templates/global.js/ui_ux.js create mode 100644 setup/tools/filegen/templates/global.js/utilities.js create mode 100644 setup/tools/filegen/templates/global.js/videos.js create mode 100644 setup/tools/filegen/templates/global.js/vote.js create mode 100644 setup/tools/filegen/templates/global.js/wow.js create mode 100644 setup/tools/filegen/templates/global.js/wsa.js create mode 100644 setup/updates/1758578400_17.sql delete mode 100644 static/js/global.js diff --git a/.gitignore b/.gitignore index d4c2da01..c23b513e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,7 @@ # generated files /static/js/profile_all.js -/static/js/locale.js -/static/js/Markup.js +/static/js/global.js /static/widgets/power.js /static/widgets/power/demo.html /static/widgets/searchbox.js diff --git a/endpoints/class/class.php b/endpoints/class/class.php index 70922cc8..50ca4666 100644 --- a/endpoints/class/class.php +++ b/endpoints/class/class.php @@ -19,8 +19,6 @@ class ClassBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 12]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::CHR_CLASS; public int $typeId = 0; public ?string $expansion = null; diff --git a/endpoints/compare/compare.php b/endpoints/compare/compare.php index f2cffbe0..5268795d 100644 --- a/endpoints/compare/compare.php +++ b/endpoints/compare/compare.php @@ -20,7 +20,6 @@ class CompareBaseResponse extends TemplateResponse [SC_JS_FILE, 'js/Draggable.js'], [SC_JS_FILE, 'js/filters.js'], [SC_JS_FILE, 'js/Summary.js'], - [SC_JS_FILE, 'js/swfobject.js'], [SC_CSS_FILE, 'css/Summary.css'] ); protected array $expectedGET = array( diff --git a/endpoints/icon/get-id-from-name.php b/endpoints/icon/get-id-from-name.php new file mode 100644 index 00000000..178602cc --- /dev/null +++ b/endpoints/icon/get-id-from-name.php @@ -0,0 +1,29 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[\w_-]+$/']] + ); + + protected function generate() : void + { + if (!$this->assertGET('name')) + { + $this->result = 'null'; + return; + } + + $this->result = 0; + if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_icons WHERE `name` = ?', $this->_get['name'])) + $this->result = $id; + } +} + +?> diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 5d81b9b2..f476b5b9 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -18,7 +18,6 @@ class ItemBaseResponse extends TemplateResponse implements ICache protected array $breadcrumb = [0, 0]; protected array $scripts = array( - [SC_JS_FILE, 'js/swfobject.js'], [SC_JS_FILE, 'js/profile.js'], [SC_JS_FILE, 'js/filters.js'] ); diff --git a/endpoints/items/items.php b/endpoints/items/items.php index 97d4a9d1..21aff287 100644 --- a/endpoints/items/items.php +++ b/endpoints/items/items.php @@ -19,7 +19,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache protected array $breadcrumb = [0, 0]; protected array $dataLoader = ['weight-presets']; - protected array $scripts = [[SC_JS_FILE, 'js/filters.js'], [SC_JS_FILE, 'js/swfobject.js']]; + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; protected array $expectedGET = array( 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] ); diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php index 0b017380..398fe55e 100644 --- a/endpoints/itemset/itemset.php +++ b/endpoints/itemset/itemset.php @@ -17,7 +17,7 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 2]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js'], [SC_JS_FILE, 'js/Summary.js']]; + protected array $scripts = [[SC_JS_FILE, 'js/Summary.js']]; public int $type = Type::ITEMSET; public int $typeId = 0; diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index 38de19f9..3d4624e2 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -17,7 +17,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 4]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js'], [SC_CSS_FILE, 'css/Profiler.css']]; + protected array $scripts = [[SC_CSS_FILE, 'css/Profiler.css']]; public int $type = Type::NPC; public int $typeId = 0; diff --git a/endpoints/object/object.php b/endpoints/object/object.php index 203fa6a0..9c6a1ba2 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -17,8 +17,6 @@ class ObjectBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 5]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::OBJECT; public int $typeId = 0; public ?Book $book = null; diff --git a/endpoints/pet/pet.php b/endpoints/pet/pet.php index df2af810..f383a0f7 100644 --- a/endpoints/pet/pet.php +++ b/endpoints/pet/pet.php @@ -17,8 +17,6 @@ class PetBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 8]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::PET; public int $typeId = 0; public ?string $expansion = null; diff --git a/endpoints/petcalc/petcalc.php b/endpoints/petcalc/petcalc.php index 0532008d..cff9539b 100644 --- a/endpoints/petcalc/petcalc.php +++ b/endpoints/petcalc/petcalc.php @@ -19,7 +19,6 @@ class PetcalcBaseResponse extends TemplateResponse [SC_CSS_FILE, 'css/talentcalc.css'], [SC_CSS_FILE, 'css/talent.css'], [SC_JS_FILE, 'js/petcalc.js'], - [SC_JS_FILE, 'js/swfobject.js'], [SC_CSS_FILE, 'css/petcalc.css'] ); diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index ee921ccb..284ed283 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -19,7 +19,6 @@ class ProfileBaseResponse extends TemplateResponse 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'], diff --git a/endpoints/race/race.php b/endpoints/race/race.php index 2134bedc..1bb261c6 100644 --- a/endpoints/race/race.php +++ b/endpoints/race/race.php @@ -23,8 +23,6 @@ class RaceBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 13]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::CHR_RACE; public int $typeId = 0; public ?string $expansion = null; diff --git a/endpoints/search/search.php b/endpoints/search/search.php index dc2687cd..edf8cb73 100644 --- a/endpoints/search/search.php +++ b/endpoints/search/search.php @@ -23,7 +23,6 @@ class SearchBaseResponse extends TemplateResponse implements ICache protected string $pageName = 'search'; protected ?int $activeTab = parent::TAB_DATABASE; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; protected array $expectedGET = array( 'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] ); diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 05111983..8f1e6645 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -23,8 +23,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 1]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::SPELL; public int $typeId = 0; public array $reagents = [false, null]; diff --git a/includes/cfg.class.php b/includes/cfg.class.php index 138e541d..b82cb1ad 100644 --- a/includes/cfg.class.php +++ b/includes/cfg.class.php @@ -48,17 +48,17 @@ class Cfg private static $isLoaded = false; private static $rebuildScripts = array( - // 'rep_req_border_unco' => ['global'], // currently not a template or buildScript - // 'rep_req_border_rare' => ['global'], - // 'rep_req_border_epic' => ['global'], - // 'rep_req_border_lege' => ['global'], - 'profiler_enable' => ['realms', 'realmMenu'], - 'battlegroup' => ['realms', 'realmMenu'], - 'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'], - 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power'], - 'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'], - 'contact_email' => ['markup'], - 'locales' => ['locales'] + 'rep_req_border_uncommon' => ['globaljs'], + 'rep_req_border_rare' => ['globaljs'], + 'rep_req_border_epic' => ['globaljs'], + 'rep_req_border_legendary' => ['globaljs'], + 'profiler_enable' => ['realms', 'realmMenu'], + 'battlegroup' => ['realms', 'realmMenu'], + 'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'], + 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power'], + 'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'], + 'contact_email' => ['globaljs'], + 'locales' => ['globaljs'] ); public static function load() : void @@ -294,16 +294,16 @@ class Cfg yield $k => self::$store[$k]; } - public static function applyToString(string $string) : string + public static function applyToString(string $string, bool $nf = true) : string { return preg_replace_callback( ['/CFG_([A-Z_]+)/', '/((HOST|STATIC)_URL)/'], - function ($m) { + function ($m) use ($nf) { if (!isset(self::$store[strtolower($m[1])])) return $m[1]; [$val, $flags, , , ] = self::$store[strtolower($m[1])]; - return $flags & (self::FLAG_TYPE_FLOAT | self::FLAG_TYPE_INT) ? Lang::nf($val) : $val; + return ($flags & (self::FLAG_TYPE_FLOAT | self::FLAG_TYPE_INT)) && $nf ? Lang::nf($val) : $val; }, $string ); diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index bc2cda60..a8760bd4 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -111,8 +111,6 @@ class TemplateResponse extends BaseResponse [SC_JS_FILE, 'widgets/power.js', SC_FLAG_NO_TIMESTAMP | SC_FLAG_APPEND_LOCALE], [SC_JS_FILE, 'js/locale_%s.js', SC_FLAG_LOCALIZED ], [SC_JS_FILE, 'js/global.js' ], - [SC_JS_FILE, 'js/locale.js' ], - [SC_JS_FILE, 'js/Markup.js' ], [SC_CSS_FILE, 'css/basic.css' ], [SC_CSS_FILE, 'css/global.css' ], [SC_CSS_FILE, 'css/aowow.css' ], diff --git a/setup/tools/filegen/locales.ss.php b/setup/tools/filegen/global-js.ss.php similarity index 86% rename from setup/tools/filegen/locales.ss.php rename to setup/tools/filegen/global-js.ss.php index 4eef9790..28fd1d55 100644 --- a/setup/tools/filegen/locales.ss.php +++ b/setup/tools/filegen/global-js.ss.php @@ -9,8 +9,6 @@ if (!CLI) die('not in cli mode'); -// Create 'locale.js'-file in static/js - /* 0: { // English id: LOCALE_ENUS, @@ -55,11 +53,13 @@ CLISetup::registerSetup("build", new class extends SetupScript use TrTemplateFile; protected $info = array( - 'locales' => [[], CLISetup::ARGV_PARAM, 'Compiles the Locale Object (static/js/locale.js) with available languages.'] + 'globaljs' => [[], CLISetup::ARGV_PARAM, 'Compiles the global javascript file (static/js/global.js).'] ); - protected $fileTemplateDest = ['static/js/locale.js']; - protected $fileTemplateSrc = ['locale.js.in']; + protected $fileTemplateDest = ['static/js/global.js']; + protected $fileTemplateSrc = ['global.js']; + + private bool $numFmt = false; private function locales() : string { diff --git a/setup/tools/filegen/markup.ss.php b/setup/tools/filegen/markup.ss.php deleted file mode 100644 index 70ceecba..00000000 --- a/setup/tools/filegen/markup.ss.php +++ /dev/null @@ -1,24 +0,0 @@ - [[], CLISetup::ARGV_PARAM, 'Fills the markup parser (static/js/Markup.js) with site variables.'] - ); - - protected $fileTemplateSrc = ['Markup.js.in']; - protected $fileTemplateDest = ['static/js/Markup.js']; -}); - -?> diff --git a/setup/tools/filegen/templates/global.js/0_user.js b/setup/tools/filegen/templates/global.js/0_user.js new file mode 100644 index 00000000..7202a369 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/0_user.js @@ -0,0 +1,73 @@ +// Needed for IE because it's dumb + +'abbr article aside audio canvas details figcaption figure footer header hgroup mark menu meter nav output progress section summary time video'.replace(/\w+/g,function(n){document.createElement(n)}) + + +/* +User-related functions +TODO: Move global variables/functions into User class +*/ + +// IMPORTANT: If you update/change the permission groups below make sure to also update them in User.inc.php! + +/*********/ +/* ROLES */ +/*********/ + +var U_GROUP_TESTER = 0x1; +var U_GROUP_ADMIN = 0x2; +var U_GROUP_EDITOR = 0x4; +var U_GROUP_MOD = 0x8; +var U_GROUP_BUREAU = 0x10; +var U_GROUP_DEV = 0x20; +var U_GROUP_VIP = 0x40; +var U_GROUP_BLOGGER = 0x80; +var U_GROUP_PREMIUM = 0x100; +var U_GROUP_LOCALIZER = 0x200; +var U_GROUP_SALESAGENT = 0x400; +var U_GROUP_SCREENSHOT = 0x800; +var U_GROUP_VIDEO = 0x1000; +var U_GROUP_APIONLY = 0x2000; +var U_GROUP_PENDING = 0x4000; + + +/******************/ +/* ROLE SHORTCUTS */ +/******************/ + +var U_GROUP_STAFF = U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_MOD | U_GROUP_BUREAU | U_GROUP_DEV | U_GROUP_BLOGGER | U_GROUP_LOCALIZER | U_GROUP_SALESAGENT; +var U_GROUP_EMPLOYEE = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV; +var U_GROUP_GREEN_TEXT = U_GROUP_MOD | U_GROUP_BUREAU | U_GROUP_DEV; +var U_GROUP_PREMIUMISH = U_GROUP_PREMIUM | U_GROUP_EDITOR; +var U_GROUP_MODERATOR = U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU; +var U_GROUP_COMMENTS_MODERATOR = U_GROUP_BUREAU | U_GROUP_MODERATOR | U_GROUP_LOCALIZER; +var U_GROUP_PREMIUM_PERMISSIONS = U_GROUP_PREMIUM | U_GROUP_STAFF | U_GROUP_VIP; + +var g_users = {}; +var g_favorites = []; +var g_customColors = {}; + +function g_isUsernameValid(username) { + return (username.match(/[^a-z0-9]/i) == null && username.length >= 4 && username.length <= 16); +} + +var User = new function() { + var self = this; + + /**********/ + /* PUBLIC */ + /**********/ + + self.hasPermissions = function(roles) + { + if(!roles) + return true; + + return !!(g_user.roles & roles); + } + + /**********/ + /* PRIVATE */ + /**********/ + +}; diff --git a/setup/tools/filegen/templates/global.js/ajax.js b/setup/tools/filegen/templates/global.js/ajax.js new file mode 100644 index 00000000..22761baa --- /dev/null +++ b/setup/tools/filegen/templates/global.js/ajax.js @@ -0,0 +1,51 @@ +function Ajax(url, opt) +{ + if (!url) + return; + + var _; + + try { _ = new XMLHttpRequest() } catch (e) + { + try { _ = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) + { + try { _ = new ActiveXObject("Microsoft.XMLHTTP") } catch (e) + { + if (window.createRequest) + _ = window.createRequest(); + else + { + alert(LANG.message_ajaxnotsupported); + return; + } + } + } + } + + this.request = _; + + $WH.cO(this, opt); + this.method = this.method || (this.params && 'POST') || 'GET'; + + _.open(this.method, url, this.async == null ? true : this.async); + _.onreadystatechange = Ajax.onReadyStateChange.bind(this); + + if (this.method.toUpperCase() == 'POST') + _.setRequestHeader('Content-Type', (this.contentType || 'application/x-www-form-urlencoded') + '; charset=' + (this.encoding || 'UTF-8')); + + _.send(this.params); +} + +Ajax.onReadyStateChange = function() +{ + if (this.request.readyState == 4) + { + if (this.request.status == 0 || (this.request.status >= 200 && this.request.status < 300)) + this.onSuccess != null && this.onSuccess(this.request, this); + else + this.onFailure != null && this.onFailure(this.request, this); + + if (this.onComplete != null) + this.onComplete(this.request, this); + } +}; diff --git a/setup/tools/filegen/templates/global.js/animations.js b/setup/tools/filegen/templates/global.js/animations.js new file mode 100644 index 00000000..a41697ef --- /dev/null +++ b/setup/tools/filegen/templates/global.js/animations.js @@ -0,0 +1,123 @@ +/* + * jQuery Color Animations + * Copyright 2007 John Resig + * Released under the MIT and GPL licenses. + */ + +(function(jQuery){ + + // We override the animation for all of these color styles + jQuery.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor'], function(i,attr){ + jQuery.fx.step[attr] = function(fx){ + if ( fx.state == 0 ) { + fx.start = getColor( fx.elem, attr ); + fx.end = getRGB( fx.end ); + } + + fx.elem.style[attr] = "rgb(" + [ + Math.max(Math.min( parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0]), 255), 0), + Math.max(Math.min( parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1]), 255), 0), + Math.max(Math.min( parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2]), 255), 0) + ].join(",") + ")"; + } + }); + + // Color Conversion functions from highlightFade + // By Blair Mitchelmore + // http://jquery.offput.ca/highlightFade/ + + // Parse strings looking for color tuples [255,255,255] + function getRGB(color) { + var result; + + // Check if we're already dealing with an array of colors + if ( color && color.constructor == Array && color.length == 3 ) + return color; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) + return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])]; + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) + return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55]; + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) + return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)]; + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) + return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)]; + + // Otherwise, we're most likely dealing with a named color + return colors[jQuery.trim(color).toLowerCase()]; + } + + function getColor(elem, attr) { + var color; + + do { + color = jQuery.curCSS(elem, attr); + + // Keep going until we find an element that has color, or we hit the body + if ( color != '' && color != 'transparent' || jQuery.nodeName(elem, "body") ) + break; + + attr = "backgroundColor"; + } while ( elem = elem.parentNode ); + + return getRGB(color); + }; + + // Some named colors to work with + // From Interface by Stefan Petre + // http://interface.eyecon.ro/ + + var colors = { + aqua:[0,255,255], + azure:[240,255,255], + beige:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + cyan:[0,255,255], + darkblue:[0,0,139], + darkcyan:[0,139,139], + darkgrey:[169,169,169], + darkgreen:[0,100,0], + darkkhaki:[189,183,107], + darkmagenta:[139,0,139], + darkolivegreen:[85,107,47], + darkorange:[255,140,0], + darkorchid:[153,50,204], + darkred:[139,0,0], + darksalmon:[233,150,122], + darkviolet:[148,0,211], + fuchsia:[255,0,255], + gold:[255,215,0], + green:[0,128,0], + indigo:[75,0,130], + khaki:[240,230,140], + lightblue:[173,216,230], + lightcyan:[224,255,255], + lightgreen:[144,238,144], + lightgrey:[211,211,211], + lightpink:[255,182,193], + lightyellow:[255,255,224], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,165,0], + pink:[255,192,203], + purple:[128,0,128], + violet:[128,0,128], + red:[255,0,0], + silver:[192,192,192], + white:[255,255,255], + yellow:[255,255,0] + }; + +})(jQuery); diff --git a/setup/tools/filegen/templates/global.js/announcement.js b/setup/tools/filegen/templates/global.js/announcement.js new file mode 100644 index 00000000..0149b984 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/announcement.js @@ -0,0 +1,157 @@ +var Announcement = function(opt) +{ + if (!opt) + opt = {}; + + $WH.cO(this, opt); + + if (this.parent) + this.parentDiv = $WH.ge(this.parent); + else + return; + + if (g_user.id > 0 && (!g_cookiesEnabled() || g_getWowheadCookie('announcement-' + this.id) == 'closed')) + return; + + this.initialize(); +}; + +Announcement.prototype = { + initialize: function() + { + // aowow - animation fix + // this.parentDiv.style.display = 'none'; + this.parentDiv.style.opacity = '0'; + + if (this.mode === undefined || this.mode == 1) + this.parentDiv.className = 'announcement announcement-contenttop'; + else + this.parentDiv.className = 'announcement announcement-pagetop'; + + var div = this.innerDiv = $WH.ce('div'); + div.className = 'announcement-inner text'; + this.setStyle(this.style); + + var a = null; + var id = parseInt(this.id); + + if (g_user && (g_user.roles & (U_GROUP_ADMIN|U_GROUP_BUREAU)) > 0 && Math.abs(id) > 0) + { + if (id < 0) + { + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&status=2'; + a.onclick = function() { return confirm('Are you sure you want to delete ' + this.name + '?'); }; + $WH.ae(a, $WH.ct('Delete')); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.style.marginRight = '10px'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&status=' + (this.status == 1 ? 0 : 1); + a.onclick = function() { return confirm('Are you sure you want to delete ' + this.name + '?'); }; + $WH.ae(a, $WH.ct((this.status == 1 ? 'Disable' : 'Enable'))); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + } + + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.style.marginRight = '22px'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&edit'; + $WH.ae(a, $WH.ct('Edit announcement')); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + } + + var markupDiv = $WH.ce('div'); + markupDiv.id = this.parent + '-markup'; + $WH.ae(div, markupDiv); + + if (id >= 0) + { + a = $WH.ce('a'); + + a.id = 'closeannouncement'; + a.href = 'javascript:;'; + a.className = 'announcement-close'; + if (this.nocookie) + a.onclick = this.hide.bind(this); + else + a.onclick = this.markRead.bind(this); + + $WH.ae(div, a); + g_addTooltip(a, LANG.close); + } + + $WH.ae(div, $WH.ce('div', { style: { clear: 'both' } })); + + $WH.ae(this.parentDiv, div); + + this.setText(this.text); + + setTimeout(this.show.bind(this), 500); // Delay to avoid visual lag + }, + + show: function() + { + // $(this.parentDiv).animate({ + // opacity: 'show', + // height: 'show' + // },{ + // duration: 333 + // }); + + // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed + this.parentDiv.style.opacity = '100'; + this.parentDiv.style.height = (this.parentDiv.offsetHeight + 10) + 'px'; + g_trackEvent('Announcements', 'Show', '' + this.name); + }, + + hide: function() + { + // $(this.parentDiv).animate({ + // opacity: 'hide', + // height: 'hide' + // },{ + // duration: 200 + // }); + + // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed + this.parentDiv.style.opacity = '0'; + this.parentDiv.style.height = '0px'; + setTimeout(function() { + this.parentDiv.style.display = 'none'; + }.bind(this), 400); + }, + + markRead: function() + { + g_trackEvent('Announcements', 'Close', '' + this.name); + g_setWowheadCookie('announcement-' + this.id, 'closed'); + this.hide(); + }, + + setStyle: function(style) + { + this.style = style; + this.innerDiv.setAttribute('style', style); + }, + + setText: function(text) + { + this.text = text; + Markup.printHtml(this.text, this.parent + '-markup'); + g_addAnalyticsToNode($WH.ge(this.parent + '-markup'), { + 'category': 'Announcements', + 'actions': { + 'Follow link': function(node) { return true; } + } + }, this.id); + } +}; diff --git a/setup/tools/filegen/templates/global.js/audio.js b/setup/tools/filegen/templates/global.js/audio.js new file mode 100644 index 00000000..106a93c4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/audio.js @@ -0,0 +1,377 @@ +var g_audiocontrols = { + __windowloaded: false, +}; +var g_audioplaylist = {}; + +// aowow - why is window.JSON here, wedged between the audio controls. It's only used for SearchBrowseButtons (and sourced by Listview) +if (!window.JSON) { + window.JSON = { + parse: function (sJSON) { + return eval("(" + sJSON + ")"); + }, + + stringify: function (obj) { + if (obj instanceof Object) + { + var str = ''; + if (obj.constructor === Array) + { + for (var i = 0; i < obj.length; str += this.stringify(obj[i]) + ',', i++) {} + return '[' + str.substr(0, str.length - 1) + ']'; + } + if (obj.toString !== Object.prototype.toString) + return '"' + obj.toString().replace(/"/g, '\\$&') + '"'; + + for (var e in obj) + str += '"' + e.replace(/"/g, '\\$&') + '":' + this.stringify(obj[e]) + ','; + + return '{' + str.substr(0, str.length - 1) + '}'; + } + + return typeof obj === 'string' ? '"' + obj.replace(/"/g, '\\$&') + '"' : String(obj); + } + } +} + +AudioControls = function () +{ + var fileIdx = -1; + var canPlay = false; + var looping = false; + var fullPlayer = false; + var autoStart = false; + var controls = {}; + var playlist = []; + var url = ''; + + function updatePlayer(_self, itr, doPlay) + { + var elAudio = $WH.ce('audio'); + elAudio.preload = 'none'; + elAudio.controls = 'true'; + $(elAudio).click(function (s) { s.stopPropagation() }); + elAudio.style.marginTop = '5px'; + + controls.audio.parentNode.replaceChild(elAudio, controls.audio); + controls.audio = elAudio; + $WH.aE(controls.audio, 'ended', setNextTrack.bind(_self)); + + if (doPlay) + { + elAudio.preload = 'auto'; + autoStart = true; + $WH.aE(controls.audio, 'canplaythrough', autoplay.bind(this)); + } + + if (!canPlay) + controls.table.style.visibility = 'visible'; + + var file; + do + { + fileIdx += itr; + if (fileIdx > playlist.length - 1) + { + fileIdx = 0; + if (!canPlay) + { + var div = $WH.ce('div'); + // div.className = 'minibox'; Aowow custom + div.className = 'minibox minibox-left'; + $WH.st(div, $WH.sprintf(LANG.message_browsernoaudio, file.type)); + controls.table.parentNode.replaceChild(div, controls.table); + return + } + } + + if (fileIdx < 0) + fileIdx = playlist.length - 1; + + file = playlist[fileIdx]; + } + while (controls.audio.canPlayType(file.type) == ''); + + var elSource = $WH.ce('source'); + elSource.src = file.url; + elSource.type = file.type; + $WH.ae(controls.audio, elSource); + + if (controls.hasOwnProperty('title')) + { + if (url) + { + $WH.ee(controls.title); + var a = $WH.ce('a'); + a.href = url; + $WH.st(a, '"' + file.title + '"'); + $WH.ae(controls.title, a); + } + else + $WH.st(controls.title, '"' + file.title + '"'); + } + + if (controls.hasOwnProperty('trackdisplay')) + $WH.st(controls.trackdisplay, '' + (fileIdx + 1) + ' / ' + playlist.length); + + if (!canPlay) + { + canPlay = true; + for (var i = fileIdx + 1; i <= playlist.length - 1; i++) + { + if (controls.audio.canPlayType(playlist[i].type)) + { + $(controls.controlsdiv).children('a').removeClass('button-red-disabled'); + break; + } + } + } + + if (controls.hasOwnProperty('addbutton')) + { + $(controls.addbutton).removeClass('button-red-disabled'); + // $WH.st(controls.addbutton, LANG.add); Aowow: doesnt work with RedButtons + RedButton.setText(controls.addbutton, LANG.add); + } + } + + function autoplay() + { + if (!autoStart) + return; + + autoStart = false; + controls.audio.play(); + } + + this.init = function (files, parent, opt) + { + if (!$WH.is_array(files)) + return; + + if (files.length == 0) + return; + + if ((parent.id == '') || g_audiocontrols.hasOwnProperty(parent.id)) + { + var i = 0; + while (g_audiocontrols.hasOwnProperty('auto-audiocontrols-' + (++i))) {} + parent.id = 'auto-audiocontrols-' + i; + } + + g_audiocontrols[parent.id] = this; + + if (typeof opt == 'undefined') + opt = {}; + + looping = !!opt.loop; + if (opt.hasOwnProperty('url')) + url = opt.url; + + playlist = files; + controls.div = parent; + + if (!opt.listview) + { + var tbl = $WH.ce('table', { className: 'audio-controls' }); + controls.table = tbl; + controls.table.style.visibility = 'hidden'; + $WH.ae(controls.div, tbl); + + var tr = $WH.ce('tr'); + $WH.ae(tbl, tr); + + var td = $WH.ce('td'); + $WH.ae(tr, td); + + controls.audio = $WH.ce('div'); + $WH.ae(td, controls.audio); + + controls.title = $WH.ce('div', { className: 'audio-controls-title' }); + $WH.ae(td, controls.title); + + controls.controlsdiv = $WH.ce('div', { className: 'audio-controls-pagination' }); + $WH.ae(td, controls.controlsdiv); + + var prevBtn = createButton(LANG.previous, true); + $WH.ae(controls.controlsdiv, prevBtn); + $WH.aE(prevBtn, 'click', this.btnPrevTrack.bind(this)); + + controls.trackdisplay = $WH.ce('div', { className: 'audio-controls-pagination-track' }); + $WH.ae(controls.controlsdiv, controls.trackdisplay); + + var nextBtn = createButton(LANG.next, true); + $WH.ae(controls.controlsdiv, nextBtn); + $WH.aE(nextBtn, 'click', this.btnNextTrack.bind(this)) + } + else + { + fullPlayer = true; + var div = $WH.ce('div'); + controls.table = div; + $WH.ae(controls.div, div); + + controls.audio = $WH.ce('div'); + $WH.ae(div, controls.audio); + + controls.trackdisplay = opt.trackdisplay; + controls.controlsdiv = $WH.ce('span'); + $WH.ae(div, controls.controlsdiv); + } + + if (g_audioplaylist.isEnabled() && !opt.fromplaylist) + { + var addBtn = createButton(LANG.add); + $WH.ae(controls.controlsdiv, addBtn); + $WH.aE(addBtn, 'click', this.btnAddToPlaylist.bind(this, addBtn)); + controls.addbutton = addBtn; + + if (fullPlayer) + addBtn.style.verticalAlign = '50%'; + } + + if (g_audiocontrols.__windowloaded) + this.btnNextTrack(); + }; + + function setNextTrack() + { + updatePlayer(this, 1, (looping || (fileIdx < (playlist.length - 1)))); + } + + this.btnNextTrack = function () + { + updatePlayer(this, 1, (canPlay && (controls.audio.readyState > 1) && (!controls.audio.paused))); + }; + + this.btnPrevTrack = function () + { + updatePlayer(this, -1, (canPlay && (controls.audio.readyState > 1) && (!controls.audio.paused))); + }; + + this.btnAddToPlaylist = function (_self) + { + if (fullPlayer) + { + for (var i = 0; i < playlist.length; i++) + g_audioplaylist.addSound(playlist[i]); + } + else + g_audioplaylist.addSound(playlist[fileIdx]); + + _self.className += ' button-red-disabled'; + // $WH.st(_self, LANG.added); // Aowow doesn't work with RedButtons + RedButton.setText(_self, LANG.added); + }; + + this.isPlaying = function () + { + return !controls.audio.paused; + }; + + this.removeSelf = function () + { + controls.table.parentNode.removeChild(controls.table); + delete g_audiocontrols[controls.div]; + }; + + function createButton(text, disabled) + { + return $WH.g_createButton(text, null, { + disabled: disabled, + // 'float': false, Aowow - adapted style + // style: 'margin:0 12px; display:inline-block' + style: 'margin:0 12px; display:inline-block; float:inherit; ' + }); + } +}; + +$WH.aE(window, 'load', function () +{ + g_audiocontrols.__windowloaded = true; + for (var i in g_audiocontrols) + if (i.substr(0, 2) != '__') + g_audiocontrols[i].btnNextTrack(); +}); + +AudioPlaylist = function () +{ + var enabled = false; + var playlist = []; + var player, container; + + this.init = function () + { + if (!$WH.localStorage.isSupported()) + return; + + enabled = true; + + var tracks; + if (tracks = $WH.localStorage.get('AudioPlaylist')) + playlist = JSON.parse(tracks); + }; + + this.savePlaylist = function () + { + if (!enabled) + return false; + + $WH.localStorage.set('AudioPlaylist', JSON.stringify(playlist)); + }; + + this.isEnabled = function () + { + return enabled; + }; + + this.addSound = function (track) + { + if (!enabled) + return false; + + this.init(); + playlist.push(track); + this.savePlaylist(); + }; + + this.deleteSound = function (idx) + { + if (idx < 0) + playlist = []; + else + playlist.splice(idx, 1); + + this.savePlaylist(); + + if (!player.isPlaying()) + { + player.removeSelf(); + this.setAudioControls(container); + } + + if (playlist.length == 0) + $WH.Tooltip.hide(); + }; + + this.getList = function () + { + var buf = []; + for (var i = 0; i < playlist.length; i++) + buf.push(playlist[i].title); + + return buf; + }; + + this.setAudioControls = function (parent) + { + if (!enabled) + return false; + + container = parent; + player = new AudioControls(); + player.init(playlist, container, { loop: true, fromplaylist: true }); + }; +}; + +g_audioplaylist = (new AudioPlaylist); +g_audioplaylist.init(); diff --git a/setup/tools/filegen/templates/global.js/clicktocopy.js b/setup/tools/filegen/templates/global.js/clicktocopy.js new file mode 100644 index 00000000..9225b6f4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/clicktocopy.js @@ -0,0 +1,122 @@ +$WH.clickToCopy = function (el, textOrFn, opt) +{ + opt = opt || {}; + + $WH.aE(el, 'click', $WH.clickToCopy.copy.bind(null, el, textOrFn, opt)); + // $WH.preventSelectStart(el); + + el.classList.add('click-to-copy'); + + if (opt.modifyTooltip) + { + el._fixTooltip = function (e) { + return e + '
    ' + $WH.ce('span', { className: 'q2', innerHTML: $WH.clickToCopy.getTooltip(false, opt) }).outerHTML; + }; + + opt.overrideOtherTooltips = false; + } + + // aowow - fitted to old system + // $WH.Tooltips.attach( + $WH.Tooltip.simple( + el, + $WH.clickToCopy.getTooltip.bind(null, false, opt), + undefined, + // { + /* byCursor: */ !opt.attachToElement, + // stopPropagation: opt.overrideOtherTooltips + // } + ); +}; + +$WH.clickToCopy.copy = function (el, textOrFn, opt, ev) +{ + ev.preventDefault(); + ev.stopPropagation(); + + if (textOrFn === undefined) + { + if (!el.childNodes[0] || !el.childNodes[0].textContent) + { + let text = 'Could not find text to copy.'; + // $WH.error(text, el); + + if (opt.attachToElement) + $WH.Tooltip.show(el, text, 'q10'); + else + $WH.Tooltip.showAtCursor(ev, text, 'q10'); + + return; + } + + textOrFn = el.childNodes[0].textContent; + } + else if (typeof textOrFn === 'function') + textOrFn = textOrFn(); + + $WH.copyToClipboard(textOrFn); + + if (opt.attachToElement) + $WH.Tooltip.show(el, $WH.clickToCopy.getTooltip(true, opt)); + else + $WH.Tooltip.showAtCursor(ev, $WH.clickToCopy.getTooltip(true, opt)); +}; + +$WH.clickToCopy.getTooltip = function (clicked, opt) +{ + let txt = ''; + let attr = undefined; + + if (clicked) + { + txt = ' ' + LANG.copied; + attr = { className: 'q1 icon-tick' }; + } + else + txt = LANG.clickToCopy; + + let tt = $WH.ce('div', attr, $WH.ct(txt)); + + if (opt.prefix) + { + tt.style.marginTop = '10px'; + let prefix = typeof opt.prefix === 'function' ? opt.prefix() : opt.prefix; + return prefix + tt.outerHTML; + } + + return tt.outerHTML; +}; + +$WH.copyToClipboard = function (text, t) +{ + if (!$WH.copyToClipboard.hiddenInput) + { + $WH.copyToClipboard.hiddenInput = $WH.ce('textarea', { className: 'hidden-element' }); + $WH.ae(document.body, $WH.copyToClipboard.hiddenInput); + } + + $WH.copyToClipboard.hiddenInput.value = text; + + let isEmpty = $WH.copyToClipboard.hiddenInput.value === ''; + if (isEmpty) + $WH.copyToClipboard.hiddenInput.value = LANG.nothingToCopy_tip; + + $WH.copyToClipboard.hiddenInput.focus(); + $WH.copyToClipboard.hiddenInput.select(); + + if (!document.execCommand('copy')) + prompt(null, text); + + $WH.copyToClipboard.hiddenInput.blur(); + + if (t) + { + if (isEmpty) + $WH.Tooltips.showFadingTooltipAtCursor(LANG.nothingToCopy_tip, t, 'q10'); + else + { + let e = $WH.ce('span', { className: 'q1 icon-tick' }, $WH.ct(' ' + LANG.copied)); + $WH.Tooltips.showFadingTooltipAtCursor(e.outerHTML, t); + } + } +}; diff --git a/setup/tools/filegen/templates/global.js/comments.js b/setup/tools/filegen/templates/global.js/comments.js new file mode 100644 index 00000000..352a6be2 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/comments.js @@ -0,0 +1,459 @@ +/* Note: comment replies are called "comments" because part of this code was taken from another project of mine. */ + +function SetupReplies(post, comment) +{ + SetupAddEditComment(post, comment, false); + SetupShowMoreComments(post, comment); + + post.find('.comment-reply-row').each(function () { SetupRepliesControls($(this), comment); }); + post.find('.comment-reply-row').hover(function () { $(this).find('span').attr('data-hover', 'true'); }, function () { $(this).find('span').attr('data-hover', 'false'); }); +} + +function SetupAddEditComment(post, comment, edit) +{ + /* Variables that will be set by Initialize() */ + var Form = null; + var Body = null; + var AddButton = null; + var TextCounter = null; + var AjaxLoader = null; + var FormContainer = null; + var DialogTableRowContainer = null; + + /* Constants */ + var MIN_LENGTH = 15; + var MAX_LENGTH = 600; + + /* State keeping booleans */ + var Initialized = false; + var Active = false; + var Flashing = false; + var Submitting = false; + + /* Shortcuts */ + var CommentsTable = post.find('.comment-replies > table'); + var AddCommentLink = post.find('.add-reply'); + var CommentsCount = comment.replies.length; + + if(edit) + Open(); + else + AddCommentLink.click(function () { Open(); }); + + function Initialize() + { + if (Initialized) + return; + + Initialized = true; + + var row = $('
    ' + + '' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '' + + '' + + '' + + '' + + '
    ' + + 'Text counter placeholder' + + '
    ' + + '
    ' + + '
    \n"; +echo '
    '.PHP_EOL; if ($this->infobox): ?> + + attributes): ?> + + +
    @@ -18,11 +21,13 @@ echo " \n"; infobox; ?> + contributions): ?> + \n"; + echo ' '.PHP_EOL; elseif (is_object($objective)): // has icon set (spell / item / ...) or unordered linked list echo $objective?->renderContainer(20, $iconOffset, true); endif; endforeach; if ($this->end): - echo " \n"; + echo ' '.PHP_EOL; endif; if ($this->suggestedPl): - echo ' \n"; + echo ' '.PHP_EOL; endif; ?> +
    @@ -31,6 +36,7 @@ echo " \n"; contributions; ?> + \n"; if ($this->contribute & CONTRIBUTE_SS): ?> + + contribute & CONTRIBUTE_VI && ($this->user::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO) || !empty($this->community['vi']))): ?> + + contribute & CONTRIBUTE_SS): ?> + + contribute & CONTRIBUTE_VI && ($this->user::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO) || !empty($this->community['vi']))): ?> + + \n"; + echo '
    '.PHP_EOL; endif; ?> diff --git a/template/bricks/inputbox-form-email.tpl.php b/template/bricks/inputbox-form-email.tpl.php index dfc7be5d..c12bff9b 100644 --- a/template/bricks/inputbox-form-email.tpl.php +++ b/template/bricks/inputbox-form-email.tpl.php @@ -3,6 +3,7 @@ use \Aowow\Lang; ?> +
    diff --git a/template/bricks/inputbox-form-signup.tpl.php b/template/bricks/inputbox-form-signup.tpl.php index 20724f66..876ff2e5 100644 --- a/template/bricks/inputbox-form-signup.tpl.php +++ b/template/bricks/inputbox-form-signup.tpl.php @@ -3,6 +3,7 @@ use \Aowow\Lang; ?> +
    + diff --git a/template/bricks/mail.tpl.php b/template/bricks/mail.tpl.php index 27bdbe48..384955f2 100644 --- a/template/bricks/mail.tpl.php +++ b/template/bricks/mail.tpl.php @@ -3,36 +3,44 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + if (['header' => $header, 'subject' => $subject, 'text' => $text, 'attachments' => $attachments] = $this->mail): $offset ??= 0; // in case we have multiple icons on the page (prominently quest-rewards) - echo '

    '.Lang::mail('mailDelivery', $header)."

    \n"; + echo '

    '.Lang::mail('mailDelivery', $header).'

    '.PHP_EOL; if ($subject): - echo '
    '.$subject."
    \n"; + echo '
    '.$subject.'
    '.PHP_EOL; endif; if ($text): - echo '
    '.$text."
    \n"; + echo '
    '.$text.'
    '.PHP_EOL; endif; if ($attachments): ?> + + renderContainer(20, $offset, true); endforeach; ?> +
    + map): if ($foundIn): echo '
    '.$foundIn[0].' '; echo Lang::concat($mapperData, true, function ($areaData, $areaId) use ($foundIn) { return ''.$foundIn[$areaId].' ('.array_sum(array_column($areaData, 'count')).')'; }); - echo ".
    \n"; + echo '.'.PHP_EOL; else: - echo "
    \n"; + echo '
    '.PHP_EOL; endif; if (isset($mapper['zone']) && $mapper['zone'] < 0): ?> +
    + +
    + +
    + +
    + +
    + +
    + + + diff --git a/template/bricks/markup.tpl.php b/template/bricks/markup.tpl.php index 69801b13..95d7038c 100644 --- a/template/bricks/markup.tpl.php +++ b/template/bricks/markup.tpl.php @@ -5,4 +5,5 @@ //]]>
    + diff --git a/template/bricks/pageTemplate.tpl.php b/template/bricks/pageTemplate.tpl.php index 7934d751..17c37a7a 100644 --- a/template/bricks/pageTemplate.tpl.php +++ b/template/bricks/pageTemplate.tpl.php @@ -1,40 +1,44 @@ diff --git a/template/bricks/reagentList.tpl.php b/template/bricks/reagentList.tpl.php index 0bdd4a13..273f77f0 100644 --- a/template/bricks/reagentList.tpl.php +++ b/template/bricks/reagentList.tpl.php @@ -9,6 +9,7 @@ + + +

    concat('title'); ?>

    @@ -47,39 +55,49 @@ if ($this->altHomeLogo): featuredBox): ?>
    + featuredBox): ?> +
    + featuredBox['overlays']): ?> + +
    +
    diff --git a/template/pages/icon.tpl.php b/template/pages/icon.tpl.php index 5ec5f25a..59a3a906 100644 --- a/template/pages/icon.tpl.php +++ b/template/pages/icon.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -18,6 +21,7 @@ ?>
    + brick('redButtons'); ?> @@ -27,9 +31,11 @@ + brick('markup', ['markup' => $this->article]); ?> +

    diff --git a/template/pages/icons.tpl.php b/template/pages/icons.tpl.php index 2ab62c48..35f59368 100644 --- a/template/pages/icons.tpl.php +++ b/template/pages/icons.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); ?> +
    -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php index 59d130f7..6084bf49 100644 --- a/template/pages/image-crop.tpl.php +++ b/template/pages/image-crop.tpl.php @@ -3,17 +3,21 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); ?> +

    h1; ?>

    diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index 23b1c768..070fb0b1 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -18,15 +21,19 @@ ?>
    + brick('redButtons'); ?>

    h1; ?>

    + unavailable): ?> +
    + brick('markup', ['markup' => $this->article]); if ($this->map): - echo "

    ".$this->map[4]."

    \n"; + echo '

    '.$this->map[4].'

    '.PHP_EOL; $this->brick('mapper'); endif; if ($this->transfer): - echo "
    \n ".$this->transfer."\n"; + echo '
    '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; if ($this->subItems): ?> +

    + subItems['data'], ceil(count($this->subItems['data']) / 2)) as $columns): ?> +
      + ['name' => $name, 'enchantment' => $enchantment, 'chance' => $chance]): echo '
    • ...'.$name.' '.Lang::item('_chance', [$chance]).'
      '; - echo Lang::concat($enchantment, Lang::CONCAT_NONE, fn($txt, $eId) => ''.$txt.'')."
    • \n"; + echo Lang::concat($enchantment, Lang::CONCAT_NONE, fn($txt, $eId) => ''.$txt.'').'
    '.PHP_EOL; endforeach; ?> +
    + brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [0]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [0]]); ?> +
    -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    @@ -37,6 +43,7 @@ $this->brick('redButtons'); slotList): ?> +
    @@ -45,11 +52,13 @@ if ($this->slotList): makeOptionsList($this->slotList, $f['sl'], 28); ?>
    + typeList): ?> +
    @@ -62,6 +71,7 @@ if ($this->typeList): }); ?>
    +
    @@ -141,7 +151,7 @@ if ($this->typeList):
    - + makeRadiosList('gb', Lang::main('gb'), $f['gb'] ?? '', 24, fn($v, &$k) => ($k = $k ?: '') || 1); ?>
    diff --git a/template/pages/itemset.tpl.php b/template/pages/itemset.tpl.php index 5bc4b51c..42a3b36f 100644 --- a/template/pages/itemset.tpl.php +++ b/template/pages/itemset.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -18,60 +21,73 @@ ?>
    + brick('redButtons'); + $this->brick('redButtons'); if ($this->expansion): - echo '

    '.$this->h1."

    \n"; + echo '

    '.$this->h1.'

    '.PHP_EOL; else: - echo '

    '.$this->h1."

    \n"; + echo '

    '.$this->h1.'

    '.PHP_EOL; endif; if ($this->unavailable): ?> +
    + brick('markup', ['markup' => $this->article]); echo $this->description; ?> +
    + pieces as [, $icon]): echo $icon->renderContainer(20, $iconIdx, true); endforeach; ?> +

    bonusExt; ?>

    - +
      + spells as [$nItems, $spellId, $text]): - echo '
    • '.Lang::itemset('_pieces', [$nItems]).''.$text."
    • \n"; + echo '
    • '.Lang::itemset('_pieces', [$nItems]).''.$text.'
    • '.PHP_EOL; endforeach; ?> +
    + summary): ?> @@ -82,6 +98,7 @@ if ($this->summary): + diff --git a/template/pages/itemsets.tpl.php b/template/pages/itemsets.tpl.php index 23297b55..3f2d09c9 100644 --- a/template/pages/itemsets.tpl.php +++ b/template/pages/itemsets.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [2]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [2]]); ?> +
    -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    diff --git a/template/pages/list-page-generic.tpl.php b/template/pages/list-page-generic.tpl.php index 72ca1ac3..c4e1d1cb 100644 --- a/template/pages/list-page-generic.tpl.php +++ b/template/pages/list-page-generic.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -14,7 +17,9 @@ $this->brick('pageTemplate'); ?> +
    + brick('redButtons'); @@ -38,15 +43,20 @@ echo '

    '.$this->tabsTitle.'

    '; endif; ?> +
    + lvTabs): $this->brick('lvTabs'); ?> +
    + +
    diff --git a/template/pages/maintenance.tpl.php b/template/pages/maintenance.tpl.php index b3f9ecfa..b65cbd3d 100644 --- a/template/pages/maintenance.tpl.php +++ b/template/pages/maintenance.tpl.php @@ -1,4 +1,9 @@ - + + diff --git a/template/pages/maps.tpl.php b/template/pages/maps.tpl.php index 8e54af9d..daf37bbe 100644 --- a/template/pages/maps.tpl.php +++ b/template/pages/maps.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    diff --git a/template/pages/npc.tpl.php b/template/pages/npc.tpl.php index ba68c727..6f2de964 100644 --- a/template/pages/npc.tpl.php +++ b/template/pages/npc.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -18,6 +21,7 @@ ?>
    + brick('redButtons'); ?>

    h1.($this->subname ? ' <'.$this->subname.'>' : ''); ?>

    @@ -28,47 +32,55 @@ if ($this->accessory): echo '
    '.Lang::npc('accessoryFor').' '; echo Lang::concat($this->accessory, true, fn ($v) => ''.$v[1].''); - echo ".
    \n"; + echo '.
    '.PHP_EOL; endif; if ($this->placeholder): ?> +
    placeholder);?>
    + map): $this->brick('mapper'); else: - echo ' '.Lang::npc('unkPosition')."\n"; + echo ' '.Lang::npc('unkPosition').''.PHP_EOL; endif; if ([$quoteGroups, $count] = $this->quotes): ?> +

    + reputation): ?> +

    + brick('markup', ['markup' => $this->smartAI]); ?> +

    diff --git a/template/pages/npcs.tpl.php b/template/pages/npcs.tpl.php index 90100955..46a6189a 100644 --- a/template/pages/npcs.tpl.php +++ b/template/pages/npcs.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); ?> +
    -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    @@ -33,6 +39,7 @@ $this->brick('redButtons'); makeOptionsList(Lang::npc('rank'), $f['cl'], 28); ?>
    + petFamPanel): ?>
    @@ -41,7 +48,9 @@ $this->brick('redButtons'); makeOptionsList(Lang::game('fa'), $f['fa'], 28); ?>
    + + diff --git a/template/pages/object.tpl.php b/template/pages/object.tpl.php index b3e076e5..ad44552b 100644 --- a/template/pages/object.tpl.php +++ b/template/pages/object.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -18,6 +21,7 @@ ?>
    + brick('redButtons'); ?>

    h1; ?>

    @@ -26,7 +30,7 @@ $this->brick('markup', ['markup' => $this->article]); if ($this->relBoss): - echo "
    ".sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1])."
    \n"; + echo '
    '.sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1]).'
    '.PHP_EOL; echo '
    '; endif; diff --git a/template/pages/objects.tpl.php b/template/pages/objects.tpl.php index d5055534..aeacf714 100644 --- a/template/pages/objects.tpl.php +++ b/template/pages/objects.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); ?> +
    -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    ucFirst(Lang::main('name')).Lang::main('colon'); ?>
    diff --git a/template/pages/privilege.tpl.php b/template/pages/privilege.tpl.php index 7aafd824..f86c7cdb 100644 --- a/template/pages/privilege.tpl.php +++ b/template/pages/privilege.tpl.php @@ -1,8 +1,11 @@ brick('header'); ?> +
    @@ -16,9 +19,11 @@

    h1;?>

    privReqPoints;?>


    + brick('markup', ['markup' => $this->article]); ?> +
    diff --git a/template/pages/privileges.tpl.php b/template/pages/privileges.tpl.php index 6e86449b..5f4005eb 100644 --- a/template/pages/privileges.tpl.php +++ b/template/pages/privileges.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -23,11 +26,13 @@
    + privileges as $id => [$earned, $name, $value]): - echo ' \n"; + echo ' '.PHP_EOL; endforeach; ?> +
     
    '.$name.'
    '.Lang::nf($value)."
     
    '.$name.'
    '.Lang::nf($value).'
    diff --git a/template/pages/profile.tpl.php b/template/pages/profile.tpl.php index 2edd318d..fed434d7 100644 --- a/template/pages/profile.tpl.php +++ b/template/pages/profile.tpl.php @@ -1,8 +1,11 @@ brick('header'); ?> +
    diff --git a/template/pages/profiler.tpl.php b/template/pages/profiler.tpl.php index f036e1f0..8880f490 100644 --- a/template/pages/profiler.tpl.php +++ b/template/pages/profiler.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    diff --git a/template/pages/profiles.tpl.php b/template/pages/profiles.tpl.php index db39deb1..7879c37c 100644 --- a/template/pages/profiles.tpl.php +++ b/template/pages/profiles.tpl.php @@ -3,28 +3,34 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); -# pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before , it will become firstChild (a text node) + # 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'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    @@ -93,14 +99,19 @@ $this->brick('redButtons'); roster): ?> +

    roster;?>

    + +
    + +
    renderFilter(12); ?> diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index b59bb47a..104827e9 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -18,70 +21,80 @@ ?>
    + brick('redButtons'); ?>

    h1; ?>

    + unavailable): ?>
    + objectives): - echo $this->objectives."\n"; + echo $this->objectives.PHP_EOL; elseif ($this->requestItems): - echo '

    '.Lang::quest('progress')."

    \n"; - echo $this->requestItems."\n"; + echo '

    '.Lang::quest('progress').'

    '.PHP_EOL; + echo $this->requestItems.PHP_EOL; elseif ($this->offerReward): - echo '

    '.Lang::quest('completion')."

    \n"; - echo $this->offerReward."\n"; + echo '

    '.Lang::quest('completion').'

    '.PHP_EOL; + echo $this->offerReward.PHP_EOL; endif; $iconOffset = 0; if ($this->end || $this->objectiveList): ?> + + objectiveList as $objective): if (is_string($objective)): // just text line - echo ' \n"; + echo ' '.PHP_EOL; elseif (is_array($objective)): // proxy npc data ['id' => $id, 'text' => $text, 'qty' => $qty, 'proxy' => $proxies] = $objective; - echo '

     

    '.$objective."

     

    '.$objective.'

     

    '.$text.''.($qty ? ' ('.$qty.')' : '').'
    \n"; + echo '

     

    '.$text.''.($qty ? ' ('.$qty.')' : '').'
    '.PHP_EOL; endforeach; - echo "

     

    ".$this->end."

     

    '.$this->end.'

     

    '.Lang::quest('suggestedPl', [$this->suggestedPl])."

     

    '.Lang::quest('suggestedPl', [$this->suggestedPl]).'
    + providedItem): ?> +
    @@ -91,6 +104,7 @@ if ($this->end || $this->objectiveList): + brick('mapper'); if ($this->details): - echo '

    '.Lang::quest('description')."

    \n" . $this->details."\n"; + echo '

    '.Lang::quest('description').'

    '.PHP_EOL; + echo ' '.$this->details.PHP_EOL; endif; if ($this->requestItems && $this->objectives): ?> +

    + offerReward && ($this->requestItems || $this->objectives)): ?> +

    + rewards): - echo '

    '.Lang::main('rewards')."

    \n"; + echo '

    '.Lang::main('rewards').'

    '.PHP_EOL; if ($choice): $this->brick('rewards', ['rewTitle' => Lang::quest('rewardChoices'), 'rewards' => $choice, 'offset' => $iconOffset]); @@ -125,7 +144,7 @@ if ([$spells, $items, $choice, $money] = $this->rewards): if ($spells): if ($choice): - echo "
    \n"; + echo '
    '.PHP_EOL; endif; $this->brick('rewards', ['rewTitle' => $spells['title'], 'rewards' => $spells['cast'], 'offset' => $iconOffset, 'extra' => $spells['extra']]); @@ -134,7 +153,7 @@ if ([$spells, $items, $choice, $money] = $this->rewards): if ($items || $money): if ($choice || $spells): - echo "
    \n"; + echo '
    '.PHP_EOL; endif; $this->brick('rewards', array( @@ -149,26 +168,28 @@ endif; if ([$xp, $rep, $title, $tp, $honor, $arena] = $this->gains): ?> +

      +
      '.Lang::nf($xp).' '.Lang::quest('experience')."
      \n"; + echo '
    • '.Lang::nf($xp).' '.Lang::quest('experience').'
    • '.PHP_EOL; endif; if ($rep): foreach ($rep as $r): - echo '
    • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name']."
    • \n"; + echo '
    • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name'].'
    • '.PHP_EOL; endforeach; endif; if ($title): - echo '
    • '.Lang::quest('rewardTitle', $title)."
    • \n"; + echo '
    • '.Lang::quest('rewardTitle', $title).'
    • '.PHP_EOL; endif; if ($tp): - echo '
    • '.Lang::quest('bonusTalents', [$tp])."
    • \n"; + echo '
    • '.Lang::quest('bonusTalents', [$tp]).'
    • '.PHP_EOL; endif; if ($arena || $honor): @@ -181,20 +202,21 @@ if ([$xp, $rep, $title, $tp, $honor, $arena] = $this->gains): if ($arena): echo ' '.$arena.''; endif; - echo "\n"; + echo ''.PHP_EOL; endif; - echo "
    \n"; + echo ' '.PHP_EOL; endif; $this->brickIf($this->mail, 'mail', ['offset' => ++$iconOffset]); if ($this->transfer): - echo "
    "; - echo "
    \n ".$this->transfer."\n"; + echo '
    '.PHP_EOL; + echo '
    '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; - ?> +

    diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index 427e5aaa..1dbeb100 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -1,28 +1,34 @@ brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); ?> +
    -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

    h1; ?>

    diff --git a/template/pages/roster.tpl.php b/template/pages/roster.tpl.php index 88ed8975..7159bf1b 100644 --- a/template/pages/roster.tpl.php +++ b/template/pages/roster.tpl.php @@ -1,21 +1,25 @@ brick('header'); ?> +
    brick('announcement'); - -$this->brick('pageTemplate'); + $this->brick('announcement'); + $this->brick('pageTemplate'); ?> +
    + brick('redButtons'); ?>

    h1; ?>

    @@ -25,9 +29,11 @@ $this->brick('pageTemplate'); ?>
    + brick('lvTabs'); ?> +
    diff --git a/template/pages/screenshot.tpl.php b/template/pages/screenshot.tpl.php index 819607b7..60559162 100644 --- a/template/pages/screenshot.tpl.php +++ b/template/pages/screenshot.tpl.php @@ -3,20 +3,23 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); - -$this->brick('infobox'); + $this->brick('pageTemplate'); + $this->brick('infobox'); ?> +

    h1; ?>

    diff --git a/template/pages/search.tpl.php b/template/pages/search.tpl.php index a0845be4..a66adbed 100644 --- a/template/pages/search.tpl.php +++ b/template/pages/search.tpl.php @@ -3,29 +3,35 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); ?>
    + brick('redButtons'); + $this->brick('redButtons'); if (count($this->lvTabs)): echo '

    '.Lang::main('foundResult').' '.$this->search.''; if ($this->invalidTerms): echo ''.Lang::main('ignoredTerms', [$this->invalidTerms]).''; endif; - echo "

    \n"; + echo ''.PHP_EOL; ?> +
    + brick('lvTabs'); @@ -34,14 +40,16 @@ else: if ($this->invalidTerms): echo ''.Lang::main('ignoredTerms', [$this->invalidTerms]).''; endif; - echo "\n"; + echo ''.PHP_EOL; ?> +
    +
    diff --git a/template/pages/sound-playlist.tpl.php b/template/pages/sound-playlist.tpl.php index 1d561627..59ea456a 100644 --- a/template/pages/sound-playlist.tpl.php +++ b/template/pages/sound-playlist.tpl.php @@ -1,10 +1,11 @@ brick('header'); ?> +
    diff --git a/template/pages/sound.tpl.php b/template/pages/sound.tpl.php index 6dbb1a32..619a7f28 100644 --- a/template/pages/sound.tpl.php +++ b/template/pages/sound.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
    @@ -16,6 +19,7 @@ ?>
    + brick('redButtons'); ?> @@ -27,6 +31,7 @@ $this->brickIf($this->map, 'mapper'); ?> +
      + reagents[0]): - echo "
      \n"; + echo '
      '.PHP_EOL; endif; endif; ?> +
      brick('markup', ['markup' => $this->article]); + $this->brick('markup', ['markup' => $this->article]); if ($this->transfer): - echo "
      \n ".$this->transfer."\n"; + echo '
      '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; ?> @@ -133,36 +144,43 @@ endif;
      + stances): ?> + + items): ?> + + effects as $i => $e): ?> + \n"; + echo ''.PHP_EOL; if ($idx == count($e['modifies'][$type]) - 1 || !(($idx + 1) % 3)) - echo ""; + echo ''; if ($idx == 17 && count($e['modifies'][$type]) > 21): $folded = true; ?> + + +
      '.Lang::spell('_gcd');?> gcd;?>
      stances;?>
      items;?>
      +
      ".implode("
      ", $e['footer'])."\n"; + echo '
      '.implode('
      ', $e['footer']).'
      '.PHP_EOL; endif; if ($e['markup']): @@ -173,6 +191,7 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML if ($e['icon']): ?> + renderContainer(iconIdxOffset: $iconTabIdx); ?> @@ -182,12 +201,14 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML + $si, 'spellName' => $sn, 'item' => $it, 'icon' => $ic, 'chance' => $ch] = $e['perfectItem']; ?> +
      renderContainer(0, $iconTabIdx, true); ?>
      @@ -201,7 +222,9 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML if ($e['modifies']): ?> +
      + [$icon, $ranks]): if (!$idx || !($idx % 3)) - echo ""; + echo ''; $icon->renderContainer(iconIdxOffset: $iconTabIdx); // just to assign iconOffset - echo "
      typeId."\">".($type ? $icon->text : "".$icon->text."")."".($ranks ? "
      (".Lang::spell('_rankRange', $ranks).")" : '')."
      '.($type ? $icon->text : ''.$icon->text.'').''.($ranks ? '
      ('.Lang::spell('_rankRange', $ranks).')' : '').'
      @@ -234,18 +258,22 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML
      +
      @@ -272,7 +303,9 @@ if ($this->attributes): ?>

      @@ -283,6 +316,7 @@ $this->brick('lvTabs'); $this->brick('contribute'); ?> +
      diff --git a/template/pages/spells.tpl.php b/template/pages/spells.tpl.php index 0579cdad..e3452808 100644 --- a/template/pages/spells.tpl.php +++ b/template/pages/spells.tpl.php @@ -1,28 +1,34 @@ brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
      brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [1]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [1]]); ?> +
      -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

      h1; ?>

      @@ -33,6 +39,7 @@ $this->brick('redButtons'); makeOptionsList(Lang::game('sc') , $f['sc'], 28,); ?>
      + classPanel): ?>
      ucFirst(Lang::game('class')).Lang::main('colon'); ?>
      @@ -42,11 +49,13 @@ $this->brick('redButtons'); makeOptionsList(Lang::game('cl') , $f['cl'], 28, fn($v, $k, &$e) => $v && ($e = ['class' => 'c'.$k])); ?>
      + glyphPanel): ?> +
      @@ -55,7 +64,9 @@ if ($this->glyphPanel): makeOptionsList(Lang::game('gl') , $f['gl'], 28); ?>
      + + diff --git a/template/pages/talent.tpl.php b/template/pages/talent.tpl.php index dbc85fe8..2bb03f46 100644 --- a/template/pages/talent.tpl.php +++ b/template/pages/talent.tpl.php @@ -1,10 +1,11 @@ brick('header'); ?> +
      diff --git a/template/pages/text-page-generic.tpl.php b/template/pages/text-page-generic.tpl.php index cda04390..4c05960c 100644 --- a/template/pages/text-page-generic.tpl.php +++ b/template/pages/text-page-generic.tpl.php @@ -1,11 +1,15 @@ brick('header'); ?> +
      + brick('announcement'); @@ -13,11 +17,13 @@ if ([$typeStr, $id] = $this->doResync): ?> +
      + inputbox): $this->brick(...$this->inputbox); // $templateName, [$templateVars] else: ?> +
      h1 ? '

      '.$this->h1.'

      ' : '');?> @@ -35,10 +42,13 @@ else: echo $this->extraHTML ?? ''; ?> +
      + +
      diff --git a/template/pages/user.tpl.php b/template/pages/user.tpl.php index 3e4190da..81bf0372 100644 --- a/template/pages/user.tpl.php +++ b/template/pages/user.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
      @@ -14,37 +17,50 @@ $this->brick('pageTemplate'); ?> + + brick('infobox'); ?> +
      + userIcon): ?> +

      h1; ?>

      +

      h1; ?>

      + +

      description): ?> +
      +
      + lvTabs)): ?> + +
      diff --git a/template/pages/video.tpl.php b/template/pages/video.tpl.php index f3cded7d..a069c3ab 100644 --- a/template/pages/video.tpl.php +++ b/template/pages/video.tpl.php @@ -3,20 +3,23 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
      brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); - -$this->brick('infobox'); + $this->brick('pageTemplate'); + $this->brick('infobox'); ?> +

      h1; ?>

      From 1c88254a0624ff8756472a67df49f834c3ff9802 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 06:13:19 +0200 Subject: [PATCH 957/957] style: drop redundant semicolons after function declarations (filegen templates + static js) --- .../filegen/templates/global.js/animations.js | 2 +- .../filegen/templates/global.js/guide.js | 2 +- .../filegen/templates/global.js/listview.js | 6 ++--- .../templates/global.js/listview_templates.js | 26 +++++++++---------- .../filegen/templates/global.js/mapper.js | 4 +-- .../filegen/templates/global.js/ui_ux.js | 2 +- static/js/TalentCalc.js | 2 +- static/js/admin-article.js | 2 +- static/js/admin.js | 2 +- static/js/article-description.js | 2 +- static/js/basic.js | 2 +- static/js/fileuploader.js | 2 +- static/js/maps.js | 2 +- static/js/petcalc.js | 2 +- static/js/profile.js | 2 +- static/js/talent.js | 2 +- static/js/user.js | 2 +- static/js/video.js | 2 +- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/setup/tools/filegen/templates/global.js/animations.js b/setup/tools/filegen/templates/global.js/animations.js index a41697ef..da37f6fe 100644 --- a/setup/tools/filegen/templates/global.js/animations.js +++ b/setup/tools/filegen/templates/global.js/animations.js @@ -68,7 +68,7 @@ } while ( elem = elem.parentNode ); return getRGB(color); - }; + } // Some named colors to work with // From Interface by Stefan Petre diff --git a/setup/tools/filegen/templates/global.js/guide.js b/setup/tools/filegen/templates/global.js/guide.js index 254abab5..20822e62 100644 --- a/setup/tools/filegen/templates/global.js/guide.js +++ b/setup/tools/filegen/templates/global.js/guide.js @@ -228,7 +228,7 @@ function g_enhanceTextarea (ta, opt) { } ta.data("wh-enhanced", true); -}; +} $WH.createOptionsMenuWidget = function (id, txt, opt) { var chevron = $WH.createOptionsMenuWidget.chevron; diff --git a/setup/tools/filegen/templates/global.js/listview.js b/setup/tools/filegen/templates/global.js/listview.js index 476c2a04..79394951 100644 --- a/setup/tools/filegen/templates/global.js/listview.js +++ b/setup/tools/filegen/templates/global.js/listview.js @@ -2961,7 +2961,7 @@ Listview.extraCols = { var mText = ConditionList.createCell(row.condition); Markup.printHtml(mText, td); - return; + }, getVisibleText: function(row) { @@ -3875,7 +3875,7 @@ Listview.funcBox = { } ); } - return; + }, coFlagOutOfDate: function(comment) @@ -3975,7 +3975,7 @@ Listview.funcBox = { } // aowow: custom end - return; + } }, diff --git a/setup/tools/filegen/templates/global.js/listview_templates.js b/setup/tools/filegen/templates/global.js/listview_templates.js index d05913fd..8217c524 100644 --- a/setup/tools/filegen/templates/global.js/listview_templates.js +++ b/setup/tools/filegen/templates/global.js/listview_templates.js @@ -144,7 +144,7 @@ Listview.templates = { $(wrapper).append(revText); } - return; + }, getVisibleText: function(guide) { @@ -359,7 +359,7 @@ Listview.templates = { span.addClass('q2'); $(td).append(span); - return; + }, sortFunc: function(a, b) { @@ -2208,7 +2208,7 @@ Listview.templates = { a.addClass('listview-cleartext'); a.attr('href', '?user=' + user.username); $(td).append(a); - return; + }, getVisibleText: function(user) { return user.username; @@ -2226,7 +2226,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.reputation)); - return; + }, sortFunc: function(a, b) { if (b.reputation == a.reputation) @@ -2251,7 +2251,7 @@ Listview.templates = { sp.html(buf); $(td).append(sp); - return; + }, sortFunc: function(a, b) { var sumA = (a.gold * 1000 * 1000) + (a.silver * 1000) + a.copper; @@ -2267,7 +2267,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.comments)); - return; + }, sortFunc: function(a, b) { if (a.comments == b.comments) @@ -2281,7 +2281,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.posts)); - return; + }, sortFunc: function(a, b) { if (a.posts == b.posts) @@ -2295,7 +2295,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.screenshots)); - return; + }, sortFunc: function(a, b) { if (a.screenshots == b.screenshots) @@ -2309,7 +2309,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.reports)); - return; + }, sortFunc: function(a, b) { if (a.reports == b.reports) @@ -2323,7 +2323,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.votes)); - return; + }, sortFunc: function(a, b) { if (a.votes == b.votes) @@ -2337,7 +2337,7 @@ Listview.templates = { type: 'text', compute: function(user, c) { $(c).append($WH.number_format(user.uploads)); - return; + }, sortFunc: function(a, c) { if (a.uploads == c.uploads) @@ -2396,7 +2396,7 @@ Listview.templates = { else { $(td).append(l_reputation_names[rep.action]); } - return; + }, getVisibleText: function(rep) { return l_reputation_names[rep.action]; @@ -2418,7 +2418,7 @@ Listview.templates = { } $(td).append(span); - return; + }, getVisibleText: function(rep) { return rep.amount; diff --git a/setup/tools/filegen/templates/global.js/mapper.js b/setup/tools/filegen/templates/global.js/mapper.js index 243af3b5..03f067f6 100644 --- a/setup/tools/filegen/templates/global.js/mapper.js +++ b/setup/tools/filegen/templates/global.js/mapper.js @@ -152,7 +152,7 @@ function Mapper(opt, noScroll) this.setZones(opt.zoneparent, opt.zones); this.updateMap(noScroll); -}; +} Mapper.sizes = [ [ 488, 325, 'normal'], @@ -1080,7 +1080,7 @@ Mapper.prototype = { this.onPinUpdate && this.onPinUpdate(this); - return; + }, pinOver: function() diff --git a/setup/tools/filegen/templates/global.js/ui_ux.js b/setup/tools/filegen/templates/global.js/ui_ux.js index aa3a6821..b4663894 100644 --- a/setup/tools/filegen/templates/global.js/ui_ux.js +++ b/setup/tools/filegen/templates/global.js/ui_ux.js @@ -151,7 +151,7 @@ function g_GetCommentRoleLabel(roles, title) return LANG.premiumuser; return null; -}; +} function g_formatDate(sp, elapsed, theDate, time, alone) { diff --git a/static/js/TalentCalc.js b/static/js/TalentCalc.js index 50a2a5e8..66750bf7 100644 --- a/static/js/TalentCalc.js +++ b/static/js/TalentCalc.js @@ -2289,7 +2289,7 @@ function TalentCalc() { _setPoints(basePoints, -1); _setGlyphSlots(lvl); _refreshGlyphs(); - }; + } function _setLock(locked) { if (_locked != locked) { diff --git a/static/js/admin-article.js b/static/js/admin-article.js index eb0215dd..b65655c8 100644 --- a/static/js/admin-article.js +++ b/static/js/admin-article.js @@ -100,4 +100,4 @@ function updateSnippet() { $("#snippet").val(a) } }) -}; \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js index 36899e33..b119e1e3 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -16,4 +16,4 @@ function ar_ValidateUrl(a) { else { return "You used invalid characters in your URL.\n\nYou can only use the following:\n a to z\n 0 to 9\n = _ & . / -" } -}; +} diff --git a/static/js/article-description.js b/static/js/article-description.js index 9dd67556..7d07af4f 100644 --- a/static/js/article-description.js +++ b/static/js/article-description.js @@ -81,4 +81,4 @@ function updatePlaceholder() { else $("#description").text(data); }) -}; \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/basic.js b/static/js/basic.js index c0af5d79..251f1aaf 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -553,7 +553,7 @@ $WH.eO = function(z) { // Duplicate object $WH.dO = function(s) { - function f(){}; + function f(){} f.prototype = s; return new f; } diff --git a/static/js/fileuploader.js b/static/js/fileuploader.js index 025cdf33..f8eed06f 100644 --- a/static/js/fileuploader.js +++ b/static/js/fileuploader.js @@ -1367,7 +1367,7 @@ qq.extend(qq.UploadHandlerXhr.prototype, { } for (key in this._options.customHeaders){ xhr.setRequestHeader(key, this._options.customHeaders[key]); - }; + } xhr.send(file); }, _onComplete: function(id, xhr){ diff --git a/static/js/maps.js b/static/js/maps.js index 03224dd8..368ae8e7 100644 --- a/static/js/maps.js +++ b/static/js/maps.js @@ -88,4 +88,4 @@ function ma_UpdateLink(_) { } $WH.ge('link-to-this-map').href = b; -}; +} diff --git a/static/js/petcalc.js b/static/js/petcalc.js index 824e6baf..2d4c85ad 100644 --- a/static/js/petcalc.js +++ b/static/js/petcalc.js @@ -119,4 +119,4 @@ function pc_readPound() { pc_object.setWhBuild(pc_build); } } -}; +} diff --git a/static/js/profile.js b/static/js/profile.js index 36a7369f..bf750c55 100644 --- a/static/js/profile.js +++ b/static/js/profile.js @@ -588,7 +588,7 @@ function pr_addEquipButton(id, itemId) $('#' + id).append(button); - return; + } }); } diff --git a/static/js/talent.js b/static/js/talent.js index 61cf8c8b..cc1ebdf3 100644 --- a/static/js/talent.js +++ b/static/js/talent.js @@ -143,4 +143,4 @@ function tc_readPound() { } } } -}; +} diff --git a/static/js/user.js b/static/js/user.js index 40b34959..6cdf2450 100644 --- a/static/js/user.js +++ b/static/js/user.js @@ -165,7 +165,7 @@ Listview.funcBox.beforeUserComments = function() }).bind(this); $WH.ae(d, i); } - }; + } this.customFilter = function (comment, i) { diff --git a/static/js/video.js b/static/js/video.js index dc00cb8a..58059ace 100644 --- a/static/js/video.js +++ b/static/js/video.js @@ -801,7 +801,7 @@ function () { if (!resizing) { // aowow - /uploads/videos/ not seen on server // aOriginal.href = g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'; - aOriginal.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId);; + aOriginal.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId); var hasFrom = video.date && video.user; if (hasFrom) { var
      ucFirst(Lang::main('name')).Lang::main('colon'); ?>