diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/ElunaSettings.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/ElunaSettings.php index 6ba0396cd..1cfaa810f 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/ElunaSettings.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/ElunaSettings.php @@ -6,6 +6,7 @@

page_alias) ?>

Configure database connection for Eluna script that need use of the CMS.

+
diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/PVPRewards.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/PVPRewards.php index 101ce4a6b..ef1beea80 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/PVPRewards.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/PVPRewards.php @@ -14,6 +14,7 @@
Give rewards

+ diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php index d99ff9f7e..5ac88f32a 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php @@ -1,254 +1,392 @@

page_alias) ?>

Configure realm name and database connection.

- -
-
-
- General Settings -
-
-
- - - - - - - - -

- First time using SOAP? Click me! + +

+ + +
+ + +
+
+
General Settings
+
+ + + + + + + + +

First time using SOAP? Click me!

+
+
+ SOAP Settings + acore_soap_host && Opts::I()->acore_soap_user): ?> + Checking… + +
+ + + + + + + + + + + + + + + + + + + + +
+
Database: Auth
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
Database: Characters
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
Database: World
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

+ +

-
- -
- SOAP Settings -
- - - - - - - - - - - - - - - - - - - - - -
- - -
- Database: Auth -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- Database: Characters -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- Database: World -
- - - - - - - - - - - - - - - - - - - - - - - - -
+
You will need to "Save Changes" above before checking your SOAP Configuration!
+
-
+ + - -
+ + + +
+
diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index 86058a205..c1ca7b2a0 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -1,16 +1,83 @@ acore_resurrection_scroll == '1'; ?> @@ -21,20 +88,19 @@

Tools


+
-
+ + +
-
- Worldserver integration -
+
World Server Integration

- + - acore_resurrection_scroll != '1') echo 'style="display:none;"'?>> - + + @@ -67,91 +157,513 @@ -
+ +
+
+
+
Web Integration
+
+
+ + + + + + + + + + + + + + + + + + acore_pdump_enabled != '1') echo 'style="opacity:0.45;pointer-events:none;"'; ?>> + + + + + + + + + + +
+ + +

Remove 2FA

+

+ Remove Website or In-game 2FA for any account. A warning is shown to the user until they re-enable it. +

+ + +

Website

+
+ + + +
+

+ + +
+

Backup Codes

+ Check a Website account above to view backup codes. + +
+ + +

In-Game

+
+ + + +
+

+ +
+
+
+ + +
-
- Name Unlock Settings -
+
Name Unlock Settings

Allowed banned names table (characters database): -
-
+ value="acore_name_unlock_allowed_banned_names_table) ?>"> +

Inactivity Thresholds per Level: + - + -
Add
+
+
+ Add +
+
+ Reset +
+
+
+ +
+ + +
+
+
User Login History
+
+

+ Look up the recorded login IP history for any account (the same list the user sees on their Security page). +

+
+ + +
+

+ + + + + + +

+ +

-
- -
+

+ +

+ + + diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php b/src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php index b14b73694..dde69a8b6 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php @@ -44,6 +44,8 @@ public function loadSettings() { // If they did, this hidden field will be set to 'Y' if ($_SERVER['REQUEST_METHOD'] == 'POST') { + check_admin_referer('acore_realm_settings_save', 'acore_realm_settings_nonce'); + foreach (Opts::I()->getConfs() as $key => $value) { if (isset($_POST[$key])) { $this->storeConf($key, $_POST[$key]); @@ -84,6 +86,8 @@ public function loadElunaSettings() { // If they did, this hidden field will be set to 'Y' if ($_SERVER['REQUEST_METHOD'] == 'POST') { + check_admin_referer('acore_eluna_settings_save', 'acore_eluna_settings_nonce'); + foreach (Opts::I()->getConfs() as $key => $value) { if (isset($_POST[$key])) { $this->storeConf($key, $_POST[$key]); @@ -131,6 +135,7 @@ public function loadPvpRewards() { //! DEV NOTE: Put the rest of stuff within try { ... } check to handle every exception properly try { if ($_SERVER['REQUEST_METHOD'] == 'POST') { + check_admin_referer('acore_pvp_rewards_save', 'acore_pvp_rewards_nonce'); global $wpdb; $tableResult = $wpdb->query("CREATE TEMPORARY TABLE temp_pvp_rewards ( `account` VARCHAR(255) COLLATE utf8_unicode_ci, @@ -385,6 +390,13 @@ public function loadTools() { //! DEV NOTE: Put the rest of stuff within try { ... } check to handle every exception properly try { if ($_SERVER['REQUEST_METHOD'] == 'POST') { + check_admin_referer('acore_tools_save', 'acore_tools_nonce'); + + // If the thresholds sentinel is present but no rows were submitted, clear them + if (isset($_POST['acore_name_unlock_thresholds_present']) && !isset($_POST['acore_name_unlock_thresholds'])) { + $this->storeConf('acore_name_unlock_thresholds', []); + } + foreach (Opts::I()->getConfs() as $key => $value) { if (isset($_POST[$key])) { if ($key == 'acore_name_unlock_allowed_banned_names_table') { diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharacterDumpApi.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharacterDumpApi.php new file mode 100644 index 000000000..a88a2642f --- /dev/null +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharacterDumpApi.php @@ -0,0 +1,187 @@ +\d+)', [ + 'methods' => 'GET', + 'callback' => __NAMESPACE__ . '\handlePdump', + 'permission_callback' => '__return_true', + ]); + register_rest_route(ACORE_SLUG . '/v1', 'pdump/all', [ + 'methods' => 'POST', + 'callback' => __NAMESPACE__ . '\handlePdumpAll', + 'permission_callback' => '__return_true', + ]); +}); + +function handlePdump(\WP_REST_Request $request): void +{ + $guid = (int) $request->get_param('guid'); + $accId = ACoreServices::I()->getAcoreAccountId(); + + if (Opts::I()->acore_pdump_enabled != '1') { + wp_send_json_error(['message' => 'PDUMP export is not enabled on this server.'], 403); + exit; + } + + if (!$accId || $guid < 1) { + wp_send_json_error(['message' => 'Forbidden.'], 403); + exit; + } + + $conn = ACoreServices::I()->getCharacterEm()->getConnection(); + $row = $conn->executeQuery( + "SELECT `name` FROM `characters` + WHERE `guid` = ? AND `account` = ? AND `deleteDate` IS NULL + LIMIT 1", + [$guid, $accId] + )->fetchAssociative(); + + if (!$row) { + wp_send_json_error(['message' => 'Character not found.'], 403); + exit; + } + + $charName = $row['name']; + + $phpWarnings = []; + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$phpWarnings): bool { + $phpWarnings[] = "[{$errno}] {$errstr} in {$errfile}:{$errline}"; + return true; + }); + + ob_start(); + $dump = null; + try { + $writer = new CharacterDumpWriter($conn); + $dump = $writer->getDump($guid); + } catch (\Throwable $e) { + $phpWarnings[] = get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(); + } + ob_end_clean(); + restore_error_handler(); + + if (!empty($phpWarnings)) { + wp_send_json_error([ + 'message' => 'An internal error occurred while generating the dump.', + 'detail' => implode("\n", $phpWarnings), + ], 500); + exit; + } + + if ($dump === null) { + wp_send_json_error(['message' => 'Character is deleted and cannot be exported.'], 400); + exit; + } + + $filename = strtoupper($charName) . '_' . date('Ymd_His') . '.dump'; + + if (ob_get_level()) { + ob_end_clean(); + } + + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . strlen($dump)); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Pragma: no-cache'); + echo $dump; + exit; +} + +function handlePdumpAll(\WP_REST_Request $request): void +{ + $accId = ACoreServices::I()->getAcoreAccountId(); + + if (Opts::I()->acore_pdump_enabled != '1') { + wp_send_json_error(['message' => 'PDUMP export is not enabled on this server.'], 403); + exit; + } + + if (!$accId) { + wp_send_json_error(['message' => 'Forbidden.'], 403); + exit; + } + + $body = $request->get_json_params(); + $characters = $body['characters'] ?? []; + if (empty($characters) || !is_array($characters)) { + wp_send_json_error(['message' => 'No characters provided.'], 400); + exit; + } + + $conn = ACoreServices::I()->getCharacterEm()->getConnection(); + $now = date('d_m_Y_H_i_s'); + + $phpWarnings = []; + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$phpWarnings): bool { + $phpWarnings[] = "[{$errno}] {$errstr} in {$errfile}:{$errline}"; + return true; + }); + + ob_start(); + + $files = []; + foreach ($characters as $char) { + $guid = isset($char['guid']) ? (int) $char['guid'] : 0; + if ($guid < 1) continue; + + $row = $conn->executeQuery( + "SELECT `name` FROM `characters` + WHERE `guid` = ? AND `account` = ? AND `deleteDate` IS NULL LIMIT 1", + [$guid, $accId] + )->fetchAssociative(); + + if (!$row) continue; + + $writer = new CharacterDumpWriter($conn); + $dump = $writer->getDump($guid); + if ($dump === null) continue; + + $order = preg_replace('/[^0-9]/', '', (string)($char['order'] ?? '0')); + $level = preg_replace('/[^0-9]/', '', (string)($char['level'] ?? '0')); + $race = preg_replace('/[^a-zA-Z]/', '', (string)($char['race'] ?? 'Unknown')); + $class = preg_replace('/[^a-zA-Z]/', '', (string)($char['class'] ?? 'Unknown')); + + $files["{$order}_{$level}_{$race}_{$class}_{$now}.dump"] = $dump; + } + + ob_end_clean(); + restore_error_handler(); + + if (!empty($phpWarnings)) { + wp_send_json_error([ + 'message' => 'An internal error occurred while generating the dump.', + 'detail' => implode("\n", $phpWarnings), + ], 500); + exit; + } + + if (empty($files)) { + wp_send_json_error(['message' => 'No valid characters could be exported.'], 400); + exit; + } + + $tmp = tempnam(sys_get_temp_dir(), 'pdump_'); + $zip = new \ZipArchive(); + $zip->open($tmp, \ZipArchive::OVERWRITE); + foreach ($files as $filename => $content) { + $zip->addFromString($filename, $content); + } + $zip->close(); + + if (ob_get_level()) ob_end_clean(); + + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="dump_all_' . $now . '.zip"'); + header('Content-Length: ' . filesize($tmp)); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Pragma: no-cache'); + readfile($tmp); + unlink($tmp); + exit; +} diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharacterDumpWriter.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharacterDumpWriter.php new file mode 100644 index 000000000..6e551855e --- /dev/null +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharacterDumpWriter.php @@ -0,0 +1,477 @@ +getDump($charGuid); // null = character is deleted + */ +class CharacterDumpWriter +{ + /** @var \Doctrine\DBAL\Connection */ + private $conn; + + private array $items = []; + private array $mails = []; + private array $pets = []; + + private int $race = 0; + + private int $fakeGuid = 0; + private int $fakeAccount = 0; + + /** + * Per-table mapping of which column holds the character GUID. + * pet_aura / pet_spell / pet_spell_cooldown use `guid` for the PET id — excluded. + */ + private const CHAR_GUID_COLUMN = [ + 'characters' => 'guid', + 'character_account_data' => 'guid', + 'character_achievement' => 'guid', + 'character_achievement_progress' => 'guid', + 'character_action' => 'guid', + 'character_aura' => 'guid', + 'character_equipmentsets' => 'guid', + 'character_glyphs' => 'guid', + 'character_homebind' => 'guid', + 'character_inventory' => 'guid', + 'character_pet' => 'owner', + 'character_queststatus' => 'guid', + 'character_queststatus_daily' => 'guid', + 'character_queststatus_weekly' => 'guid', + 'character_queststatus_monthly' => 'guid', + 'character_queststatus_seasonal' => 'guid', + 'character_queststatus_rewarded' => 'guid', + 'character_reputation' => 'guid', + 'character_skills' => 'guid', + 'character_spell' => 'guid', + 'character_spell_cooldown' => 'guid', + 'character_talent' => 'guid', + 'mail' => 'receiver', + 'mail_items' => 'receiver', + 'item_instance' => 'owner_guid', + 'character_gifts' => 'guid', + ]; + + // Alliance races in WotLK: Human(1), Dwarf(3), NightElf(4), Gnome(7), Draenei(11) + private const ALLIANCE_RACES = [1, 3, 4, 7, 11]; + + // Stormwind — Trade District + private const POS_STORMWIND = [ + 'map' => 0, + 'zone' => 1519, + 'position_x' => -8865.09, + 'position_y' => 671.793, + 'position_z' => 97.9034, + 'orientation' => 2.16098, + ]; + + // Neutral fallback — Booty Bay + private const POS_BOOTY_BAY = [ + 'map' => 0, + 'zone' => 33, + 'position_x' => -14468.104, + 'position_y' => 483.00665, + 'position_z' => 26.776731, + 'orientation' => 0.0, + ]; + + // Orgrimmar — Valley of Strength + private const POS_ORGRIMMAR = [ + 'map' => 1, + 'zone' => 1637, + 'position_x' => 1633.8, + 'position_y' => -4440.47, + 'position_z' => 15.634, + 'orientation' => 3.08343, + ]; + + private const SKIP_TABLES = [ + 'character_declinedname', + 'character_pet_declinedname', + ]; + + /** + * Table definitions — must stay in this exact order (matches DumpTables[]). + * + * 'where' : column used in the WHERE clause + * 'set' : which pre-collected ID set to use (null = use char $guid directly) + */ + private const DUMP_TABLES = [ + ['table' => 'characters', 'where' => 'guid', 'set' => null ], + ['table' => 'character_account_data', 'where' => 'guid', 'set' => null ], + ['table' => 'character_achievement', 'where' => 'guid', 'set' => null ], + ['table' => 'character_achievement_progress', 'where' => 'guid', 'set' => null ], + ['table' => 'character_action', 'where' => 'guid', 'set' => null ], + ['table' => 'character_aura', 'where' => 'guid', 'set' => null ], + ['table' => 'character_declinedname', 'where' => 'guid', 'set' => null ], + ['table' => 'character_equipmentsets', 'where' => 'guid', 'set' => null ], + ['table' => 'character_glyphs', 'where' => 'guid', 'set' => null ], + ['table' => 'character_homebind', 'where' => 'guid', 'set' => null ], + ['table' => 'character_inventory', 'where' => 'guid', 'set' => null ], + ['table' => 'character_pet', 'where' => 'owner', 'set' => null ], + ['table' => 'character_pet_declinedname', 'where' => 'owner', 'set' => null ], + ['table' => 'character_queststatus', 'where' => 'guid', 'set' => null ], + ['table' => 'character_queststatus_daily', 'where' => 'guid', 'set' => null ], + ['table' => 'character_queststatus_weekly', 'where' => 'guid', 'set' => null ], + ['table' => 'character_queststatus_monthly', 'where' => 'guid', 'set' => null ], + ['table' => 'character_queststatus_seasonal', 'where' => 'guid', 'set' => null ], + ['table' => 'character_queststatus_rewarded', 'where' => 'guid', 'set' => null ], + ['table' => 'character_reputation', 'where' => 'guid', 'set' => null ], + ['table' => 'character_skills', 'where' => 'guid', 'set' => null ], + ['table' => 'character_spell', 'where' => 'guid', 'set' => null ], + ['table' => 'character_spell_cooldown', 'where' => 'guid', 'set' => null ], + ['table' => 'character_talent', 'where' => 'guid', 'set' => null ], + ['table' => 'mail', 'where' => 'receiver', 'set' => null ], + ['table' => 'mail_items', 'where' => 'mail_id', 'set' => 'mails' ], + ['table' => 'pet_aura', 'where' => 'guid', 'set' => 'pets' ], + ['table' => 'pet_spell', 'where' => 'guid', 'set' => 'pets' ], + ['table' => 'pet_spell_cooldown', 'where' => 'guid', 'set' => 'pets' ], + ['table' => 'item_instance', 'where' => 'guid', 'set' => 'items' ], + ['table' => 'character_gifts', 'where' => 'item_guid', 'set' => 'items' ], + ]; + + public function __construct($conn) + { + $this->conn = $conn; + } + + /** + * Generate a PII-stripped pdump string for the given character GUID. + * + * Returns null if the character is flagged as deleted + * (deleteInfos_Account is non-null) — mirroring the C++ DUMP_CHARACTER_DELETED check. + */ + public function getDump(int $guid): ?string + { + $this->items = []; + $this->mails = []; + $this->pets = []; + $this->race = 0; + // Use the upper quarter of INT UNSIGNED (max 4,294,967,295) — + // real servers are extremely unlikely to have GUIDs or account IDs this high. + $this->fakeGuid = random_int(3_000_000_000, 4_294_967_290); + $this->fakeAccount = random_int(3_000_000_000, 4_294_967_290); + + $charRow = $this->conn + ->executeQuery( + "SELECT `deleteInfos_Account`, `race` FROM `characters` WHERE `guid` = ?", + [$guid] + ) + ->fetchAssociative(); + + if (!$charRow || $charRow['deleteInfos_Account'] !== null) { + return null; + } + + $this->race = (int) $charRow['race']; + + $this->populateGuids($guid); + + $lines = []; + foreach (self::DUMP_TABLES as $def) { + if (in_array($def['table'], self::SKIP_TABLES, true)) { + continue; + } + + $tableLines = $this->dumpTable($guid, $def); + if ($tableLines === null) { + return null; + } + foreach ($tableLines as $line) { + $lines[] = $line; + } + } + + $eol = "\r\n"; + $header = "IMPORTANT NOTE: THIS DUMPFILE IS MADE FOR USE WITH THE 'PDUMP' COMMAND ONLY - EITHER THROUGH INGAME CHAT OR ON CONSOLE!" . $eol; + $header .= "IMPORTANT NOTE: DO NOT apply it directly - it will irreversibly DAMAGE and CORRUPT your database! You have been warned!" . $eol; + $header .= $eol; + + return $header . implode($eol, $lines) . (count($lines) ? $eol : ''); + } + + private function isAlliance(): bool + { + return in_array($this->race, self::ALLIANCE_RACES, true); + } + + private function capitalPos(): array + { + if ($this->race === 0) { + return self::POS_BOOTY_BAY; + } + return $this->isAlliance() ? self::POS_STORMWIND : self::POS_ORGRIMMAR; + } + + /** + * Collect secondary IDs (pets, mails, items) — mirrors PopulateGuids(). + */ + private function populateGuids(int $guid): void + { + $rs = $this->conn->executeQuery( + "SELECT `id` FROM `character_pet` WHERE `owner` = ?", [$guid] + ); + while ($id = $rs->fetchOne()) { + $this->pets[] = (int) $id; + } + + $rs = $this->conn->executeQuery( + "SELECT `id` FROM `mail` WHERE `receiver` = ?", [$guid] + ); + while ($id = $rs->fetchOne()) { + $this->mails[] = (int) $id; + } + + $rs = $this->conn->executeQuery( + "SELECT `guid` FROM `item_instance` WHERE `owner_guid` = ?", [$guid] + ); + while ($id = $rs->fetchOne()) { + $this->items[] = (int) $id; + } + } + + /** + * Dump one table, returning an array of INSERT lines (or [] if no rows). + * + * @return string[]|null + */ + private function dumpTable(int $guid, array $def): ?array + { + $table = $def['table']; + $whereField = $def['where']; + $setName = $def['set']; + + if ($setName !== null) { + $ids = $this->$setName; + if (empty($ids)) { + return []; + } + $placeholders = implode(', ', array_fill(0, count($ids), '?')); + $sql = "SELECT * FROM `{$table}` WHERE `{$whereField}` IN ({$placeholders})"; + $params = $ids; + } else { + $sql = "SELECT * FROM `{$table}` WHERE `{$whereField}` = ?"; + $params = [$guid]; + } + + $rs = $this->conn->executeQuery($sql, $params); + + $lines = []; + $columnNames = []; + while ($row = $rs->fetchAssociative()) { + if (empty($columnNames)) { + $columnNames = array_keys($row); + } + $row = $this->stripPii($table, $row); + $lines[] = $this->buildInsert($table, $columnNames, $row); + } + + return $lines; + } + + /** + * Strip / sanitise PII fields from a row before it is serialised. + * Returns the modified row. + */ + private function stripPii(string $table, array $row): array + { + $cap = $this->capitalPos(); + + switch ($table) { + case 'characters': + $row['name'] = 'Unknown'; + $row['money'] = '0'; + $row['logout_time'] = '0'; + $row['totaltime'] = '0'; + $row['leveltime'] = '0'; + $row['latency'] = '0'; + $row['online'] = '0'; + $row['creation_date'] = '2000-01-01 00:00:00'; + $row['death_expire_time'] = '0'; + $row['rest_bonus'] = '0'; + $row['is_logout_resting'] = '0'; + $row['todayHonorPoints'] = '0'; + $row['yesterdayHonorPoints'] = '0'; + $row['todayKills'] = '0'; + $row['yesterdayKills'] = '0'; + $row['taxi_path'] = null; + $row['innTriggerId'] = '0'; + $row['trans_x'] = '0'; + $row['trans_y'] = '0'; + $row['trans_z'] = '0'; + $row['trans_o'] = '0'; + $row['transguid'] = '0'; + $row['map'] = (string) $cap['map']; + $row['zone'] = (string) $cap['zone']; + $row['position_x'] = (string) $cap['position_x']; + $row['position_y'] = (string) $cap['position_y']; + $row['position_z'] = (string) $cap['position_z']; + $row['orientation'] = (string) $cap['orientation']; + $row['instance_id'] = '0'; + $row['instance_mode_mask'] = '0'; + break; + + case 'character_homebind': + $row['mapId'] = (string) $cap['map']; + $row['zoneId'] = (string) $cap['zone']; + $row['posX'] = (string) $cap['position_x']; + $row['posY'] = (string) $cap['position_y']; + $row['posZ'] = (string) $cap['position_z']; + break; + + case 'character_account_data': + $row['time'] = '0'; + // Strip custom channel names from the type-7 chat layout blob. + // Keep only Blizzard's built-in channels; player-named channels + // (e.g. guild recruitment, custom group channels) can be PII. + if ((int)($row['type'] ?? -1) === 7 && !empty($row['data'])) { + $row['data'] = self::stripCustomChannels($row['data']); + } + break; + + case 'character_aura': + if (isset($row['caster_guid'])) $row['caster_guid'] = '0'; + break; + + case 'character_equipmentsets': + $row['name'] = ''; + break; + + case 'character_achievement': + case 'character_achievement_progress': + $row['date'] = '0'; + break; + + case 'item_instance': + $row['playedTime'] = '0'; + $row['text'] = null; + $row['creatorGuid'] = '0'; + $row['giftCreatorGuid'] = '0'; + break; + + case 'mail': + $row['subject'] = 'Redacted'; + $row['body'] = 'Redacted'; + $row['sender'] = '0'; + $row['expire_time'] = '0'; + $row['deliver_time'] = '0'; + break; + + case 'character_pet': + $row['name'] = null; + break; + } + + // Replace real char GUID with anonymised value, using the correct column per table + if (isset(self::CHAR_GUID_COLUMN[$table])) { + $col = self::CHAR_GUID_COLUMN[$table]; + $row[$col] = (string) $this->fakeGuid; + } + + // Replace real account ID (only present in the characters row) + if ($table === 'characters') { + $row['account'] = (string) $this->fakeAccount; + } + + return $row; + } + + /** + * Blizzard built-in channel names that are safe to keep. + * Anything not in this list inside a CHANNELS...END block is player-defined. + */ + private const DEFAULT_CHANNELS = [ + 'General', 'Trade', 'LocalDefense', 'LookingForGroup', + 'GuildRecruitment', 'WorldDefense', + ]; + + /** + * Remove player-defined channel names from the type-7 chat config blob. + * + * The blob uses literal \n characters between lines (it's stored escaped + * in MySQL). We decode, strip, then re-encode back. + * + * CHANNELS blocks look like: + * CHANNELS\n + * SomeChannel\n + * AnotherChannel\n + * END\n + */ + private static function stripCustomChannels(string $data): string + { + $decoded = preg_replace_callback( + '/^CHANNELS\n(.*?\n)END\n/ms', + function (array $m): string { + $kept = ''; + foreach (explode("\n", rtrim($m[1], "\n")) as $line) { + $name = trim($line); + if ($name === '' || in_array($name, self::DEFAULT_CHANNELS, true)) { + $kept .= $name !== '' ? $name . "\n" : ''; + } + } + return "CHANNELS\n{$kept}END\n"; + }, + $data + ); + + return $decoded ?? $data; + } + + /** + * Build a single INSERT line exactly as AppendTableDump() does in C++. + */ + private function buildInsert(string $table, array $columns, array $row): string + { + $colList = implode('`, `', $columns); + + $values = []; + foreach ($columns as $col) { + $val = $row[$col] ?? null; + $values[] = self::escapeValue($val); + } + + return "INSERT INTO `{$table}` (`{$colList}`) VALUES (" . implode(', ', $values) . ");"; + } + + /** + * Escape a field value the same way AzerothCore's EscapeString() does. + */ + private static function escapeValue($val): string + { + if ($val === null || $val === '') { + return "'NULL'"; + } + + $s = (string) $val; + + $s = str_replace('\\', '\\\\', $s); + $s = str_replace("\0", '\\0', $s); + $s = str_replace("\n", '\\n', $s); + $s = str_replace("\r", '\\r', $s); + $s = str_replace("'", "\\'", $s); + $s = str_replace('"', '\\"', $s); + $s = str_replace("\x1a", '\\Z', $s); + + return "'" . $s . "'"; + } +} diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php index 98dee522d..031b82ad1 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php @@ -3,6 +3,7 @@ namespace ACore\Components\CharactersMenu; use ACore\Manager\ACoreServices; +use ACore\Manager\Opts; use ACore\Components\CharactersMenu\CharactersView; class CharactersController { @@ -14,30 +15,113 @@ public function __construct() { public function loadHome() { if ($_SERVER["REQUEST_METHOD"] == "POST") { - $this->saveCharacterOrder(); - ?> -

Character settings succesfully saved.

- resetCharacterOrder(); + ?> +

Character order reset successfully.

+ saveCharacterOrder(); + ?> +

Character settings successfully saved.

+ getAcoreAccountId(); + if (!$accId) { + echo $this->getView()->getHomeRender([], 0, null); + return; + } + + $conn = ACoreServices::I()->getCharacterEm()->getConnection(); + $query = "SELECT - `guid`, `name`, `order`, `race`, `class`, `level`, `gender` - FROM `characters` - WHERE `characters`.`deleteDate` IS NULL AND `account` = $accId - ORDER BY COALESCE(`order`, `guid`) + c.`guid`, c.`name`, c.`order`, c.`race`, c.`class`, c.`level`, c.`gender`, + cb.`bandate` AS `ban_bandate`, + cb.`unbandate` AS `ban_unbandate` + FROM `characters` c + LEFT JOIN `character_banned` cb + ON cb.`guid` = c.`guid` + AND cb.`active` = 1 + AND (cb.`unbandate` = 0 OR cb.`unbandate` = cb.`bandate` OR cb.`unbandate` > UNIX_TIMESTAMP()) + WHERE c.`deleteDate` IS NULL AND c.`account` = ? + ORDER BY (c.`order` IS NULL), c.`order`, c.`guid` "; - $conn = ACoreServices::I()->getCharacterEm()->getConnection(); - $queryResult = $conn->executeQuery($query); - $chars = $queryResult->fetchAllAssociative(); + $chars = $conn->executeQuery($query, [$accId])->fetchAllAssociative(); + + $authConn = ACoreServices::I()->getAccountEm()->getConnection(); + + $muteRow = $authConn + ->executeQuery("SELECT `mutetime` FROM `account` WHERE `id` = ?", [$accId]) + ->fetchAssociative(); + $mutetime = $muteRow ? intval($muteRow['mutetime']) : 0; + // Negative = pending mute (seconds magnitude, applied on next login); positive = Unix timestamp expiry + + $accBanRow = $authConn + ->executeQuery( + "SELECT `bandate`, `unbandate` FROM `account_banned` + WHERE `id` = ? AND `active` = 1 + AND (`unbandate` = 0 OR `unbandate` = `bandate` OR `unbandate` > UNIX_TIMESTAMP()) + ORDER BY `bandate` DESC LIMIT 1", + [$accId] + ) + ->fetchAssociative(); - echo $this->getView()->getHomeRender($chars); + $serverRevision = ''; + $serverRevisionUrl = ''; + try { + $worldConn = ACoreServices::I()->getWorldEm()->getConnection(); + $versionRow = $worldConn + ->executeQuery("SELECT `core_version`, `core_revision` FROM `version` LIMIT 1") + ->fetchAssociative(); + if ($versionRow) { + $full = $versionRow['core_version'] ?: ''; + if (preg_match('/^(AzerothCore rev\.\s+\S+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/', $full, $m)) { + $serverRevision = $m[1]; + } else { + $serverRevision = $full; + } + $hash = $versionRow['core_revision'] ?: ''; + if ($hash) { + $serverRevisionUrl = 'https://github.com/azerothcore/azerothcore-wotlk/commit/' . $hash; + } + } + } catch (\Throwable $e) { + // non-fatal + } + + $punishmentEnabled = Opts::I()->acore_punishment_info_enabled == '1'; + echo $this->getView()->getHomeRender( + $chars, $mutetime, $accBanRow, + $serverRevision, $serverRevisionUrl, + Opts::I()->acore_bug_report_url ?: '', + Opts::I()->acore_pdump_enabled == '1', + $punishmentEnabled && Opts::I()->acore_punishment_info_account_ban == '1', + $punishmentEnabled && Opts::I()->acore_punishment_info_account_mute == '1', + $punishmentEnabled && Opts::I()->acore_punishment_info_character_ban == '1' + ); } public function getView() { return $this->view; } + private function resetCharacterOrder() { + $accId = ACoreServices::I()->getAcoreAccountId(); + $accId = is_numeric($accId) ? (int) $accId : 0; + if ($accId <= 0) { + return; + } + $conn = ACoreServices::I()->getCharacterEm()->getConnection(); + $stmt = $conn->prepare( + "UPDATE `characters` SET `order` = NULL WHERE `account` = ? AND `deleteDate` IS NULL" + ); + $stmt->bindValue(1, $accId); + $stmt->executeQuery(); + } + private function saveCharacterOrder() { if (!isset($_POST) || !isset($_POST["characterorder"])) { return; diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php index c3a6da3af..00603a022 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php @@ -3,6 +3,8 @@ namespace ACore\Components\CharactersMenu; use ACore\Components\CharactersMenu\CharactersController; +use ACore\Manager\ACoreServices; +use ACore\Manager\Opts; add_action('init', __NAMESPACE__ . '\\characters_menu_init'); @@ -25,7 +27,44 @@ public static function I() function acore_characters_menu() { - add_submenu_page('profile.php', 'Characters', 'Characters', 'read', ACORE_SLUG . '-characters-menu', array($this, 'acore_characters_menu_page')); + $menuTitle = 'Characters'; + $punishEnabled = Opts::I()->acore_punishment_info_enabled == '1'; + $showAccBan = $punishEnabled && Opts::I()->acore_punishment_info_account_ban == '1'; + $showAccMute = $punishEnabled && Opts::I()->acore_punishment_info_account_mute == '1'; + try { + if ($showAccBan || $showAccMute) { + $accId = ACoreServices::I()->getAcoreAccountId(); + if (!$accId) throw new \Exception('no account'); + $authConn = ACoreServices::I()->getAccountEm()->getConnection(); + $now = time(); + + if ($showAccBan) { + $isBanned = (bool) $authConn->executeQuery( + "SELECT 1 FROM `account_banned` + WHERE `id` = ? AND `active` = 1 + AND (`unbandate` = 0 OR `unbandate` = `bandate` OR `unbandate` > UNIX_TIMESTAMP()) + LIMIT 1", [$accId] + )->fetchOne(); + + if ($isBanned) { + $menuTitle .= ' Banned'; + } + } + + if ($showAccMute) { + $muteRow = $authConn->executeQuery("SELECT `mutetime` FROM `account` WHERE `id` = ?", [$accId])->fetchAssociative(); + $mutetime = $muteRow ? intval($muteRow['mutetime']) : 0; + $isMuted = $mutetime < 0 || $mutetime > $now; + if ($isMuted) { + $menuTitle .= ' Muted'; + } + } + } + } catch (\Throwable $e) { + // silently skip badge on DB error + } + + add_submenu_page('profile.php', 'Characters', $menuTitle, 'read', ACORE_SLUG . '-characters-menu', array($this, 'acore_characters_menu_page')); } function acore_characters_menu_page() @@ -40,4 +79,7 @@ function characters_menu_init() $charactersMenu = CharactersMenu::I(); add_action('admin_menu', array($charactersMenu, 'acore_characters_menu')); + add_action('admin_head', function () { + echo ''; + }); } diff --git a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php index 12d98ae34..8105728bd 100644 --- a/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php +++ b/src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php @@ -2,6 +2,8 @@ namespace ACore\Components\CharactersMenu; +use ACore\Utils\AcoreCharColors; + class CharactersView { private $controller; @@ -13,47 +15,133 @@ public function __construct($controller) { $this->controller = $controller; } - public function getHomeRender($characters) { + private function formatDuration($seconds) { + $seconds = abs(intval($seconds)); + if ($seconds >= 31536000) { $n = intdiv($seconds, 31536000); return $n . ' ' . ($n === 1 ? 'year' : 'years'); } + if ($seconds >= 2592000) { $n = intdiv($seconds, 2592000); return $n . ' ' . ($n === 1 ? 'month' : 'months'); } + if ($seconds >= 86400) { $n = intdiv($seconds, 86400); return $n . ' ' . ($n === 1 ? 'day' : 'days'); } + if ($seconds >= 3600) { $n = intdiv($seconds, 3600); return $n . ' ' . ($n === 1 ? 'hour' : 'hours'); } + if ($seconds >= 60) { $n = intdiv($seconds, 60); return $n . ' ' . ($n === 1 ? 'minute' : 'minutes'); } + return $seconds . ' ' . ($seconds === 1 ? 'second' : 'seconds'); + } + + private function formatDate($ts) { return date('d-m-Y', intval($ts)); } + private function formatTime($ts) { return date('H:i', intval($ts)); } + + public function getHomeRender($characters, $mutetime = 0, $accBanRow = null, $serverRevision = '', $serverRevisionUrl = '', $bugReportUrl = '', $pdumpEnabled = false, $showAccountBan = false, $showAccountMute = false, $showCharBan = false) { + $now = time(); + + // Account mute + $isMuted = $mutetime < 0 || $mutetime > $now; + $mutePending = $mutetime < 0; + $muteRemaining = $mutePending ? abs($mutetime) : max(0, $mutetime - $now); + + // Account ban + $isAccountBanned = !empty($accBanRow); + $accBanPerma = $isAccountBanned && (intval($accBanRow['unbandate']) === 0 || $accBanRow['unbandate'] === $accBanRow['bandate']); + $accBanRemaining = ($isAccountBanned && !$accBanPerma) ? max(0, intval($accBanRow['unbandate']) - $now) : 0; + ob_start(); wp_enqueue_style('bootstrap-css', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css', array(), '5.1.3'); - wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.1'); + wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.5'); wp_enqueue_script('bootstrap-js', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js', array(), '5.1.3'); wp_enqueue_script('jquery-ui-sortable'); ?>
-

Characters Settings

-

Check some details and configure of your characters.

-
-
+
-

Order

-

Change the order in which the characters appear in your in-game character selection screen.

+

Order Character Screen

+

Change the order in which your characters appear in the in-game selection screen by dragging them into your preferred position.

+ + +
+ -
    - -
  • - @@ -62,12 +150,412 @@ public function getHomeRender($characters) {
- + $("#acore-characters-order").sortable({ + update: function(event, ui) { + var order = []; + $("#acore-characters-order li").each(function(index) { + var input = $(this).find("input[name='characterorder[]']"); + order.push(input.val()); + $(this).find(".acore-char-pos").text(index + 1); + }); + } + }); + $("#acore-characters-order").disableSelection(); + + var acorePdumpRevision = ; + var acorePdumpRevisionUrl = ; + var acorePdumpBugReportUrl = ; + var acorePdumpRestBase = ; + var acorePdumpAllUrl = ; + var acorePdumpNonce = ; + var acorePdumpModal = document.getElementById('acore-pdump-modal'); + var acorePdumpBody = document.getElementById('acore-pdump-modal-body'); + var acorePdumpConfirm = document.getElementById('acore-pdump-confirm'); + var acorePdumpOnConfirm = null; + + function acorePdumpFilename(order, level, race, cls) { + var now = new Date(); + var pad = function(n) { return String(n).padStart(2, '0'); }; + return order + '_' + level + '_' + race + '_' + cls + '_' + + pad(now.getDate()) + '_' + pad(now.getMonth() + 1) + '_' + now.getFullYear() + '_' + + pad(now.getHours()) + '_' + pad(now.getMinutes()) + '_' + pad(now.getSeconds()) + '.dump'; + } + + function acorePdumpZipFilename() { + var now = new Date(); + var pad = function(n) { return String(n).padStart(2, '0'); }; + return 'dump_all_' + pad(now.getDate()) + '_' + pad(now.getMonth() + 1) + '_' + now.getFullYear() + '_' + + pad(now.getHours()) + '_' + pad(now.getMinutes()) + '_' + pad(now.getSeconds()) + '.zip'; + } + + function acorePdumpRevisionLink() { + if (!acorePdumpRevision) return '(unknown revision)'; + if (acorePdumpRevisionUrl) { + return '' + acorePdumpRevision + ''; + } + return acorePdumpRevision; + } + + var acorePdumpError = document.getElementById('acore-pdump-error'); + var acorePdumpErrorIntro = document.getElementById('acore-pdump-error-intro'); + var acorePdumpErrorDetails = document.getElementById('acore-pdump-error-details'); + var acorePdumpErrorMsg = document.getElementById('acore-pdump-error-msg'); + + function acoreShowPdumpModal(message, onConfirm) { + acorePdumpBody.innerHTML = message; + acorePdumpError.style.display = 'none'; + acorePdumpErrorIntro.innerHTML = ''; + acorePdumpErrorMsg.textContent = ''; + acorePdumpErrorDetails.style.display = 'none'; + acorePdumpErrorDetails.removeAttribute('open'); + acorePdumpConfirm.style.display = ''; + acorePdumpOnConfirm = onConfirm; + acorePdumpModal.style.display = 'flex'; + } + + function acoreShowPdumpError(msg, detail) { + acorePdumpBody.innerHTML = ''; + // Show the human-readable message + technical detail (if any) in the
+                acorePdumpErrorMsg.textContent = detail ? (msg + '\n\n' + detail) : msg;
+
+                if (acorePdumpBugReportUrl) {
+                    acorePdumpErrorIntro.innerHTML =
+                        'There was an error, the PDUMP was not successful, it seems to be a bug, please report it on '
+                        + 'GitHub.';
+                    acorePdumpErrorDetails.style.display = 'block';
+                } else {
+                    acorePdumpErrorIntro.textContent =
+                        'There was an error, and it seems there is no URL for you to report to. '
+                        + 'Talk to the administrator to fix this, in AzerothCore → Tools → PDUMP Bug Report URL.';
+                    acorePdumpErrorDetails.style.display = 'none';
+                }
+
+                acorePdumpError.style.display = 'block';
+                acorePdumpConfirm.style.display = 'none';
+                acorePdumpOnConfirm = null;
+                acorePdumpModal.style.display = 'flex';
+            }
+
+            function acoreClosePdumpModal() {
+                acorePdumpModal.style.display = 'none';
+                acorePdumpOnConfirm = null;
+            }
+
+            function acoreDownloadDump(guid, order, level, race, cls) {
+                fetch(acorePdumpRestBase + guid, {
+                    headers: { 'X-WP-Nonce': acorePdumpNonce }
+                }).then(function(resp) {
+                    if (!resp.ok) {
+                        return resp.json().then(function(body) {
+                            var data   = (body && body.data) ? body.data : body;
+                            var msg    = (data && data.message) ? data.message : 'Export failed (HTTP ' + resp.status + ').';
+                            var detail = (data && data.detail)  ? data.detail  : null;
+                            var err    = new Error(msg);
+                            err.detail = detail;
+                            throw err;
+                        });
+                    }
+                    return resp.blob();
+                }).then(function(blob) {
+                    var filename = acorePdumpFilename(order, level, race, cls);
+                    var url  = URL.createObjectURL(blob);
+                    var link = document.createElement('a');
+                    link.href     = url;
+                    link.download = filename;
+                    document.body.appendChild(link);
+                    link.click();
+                    document.body.removeChild(link);
+                    setTimeout(function() { URL.revokeObjectURL(url); }, 10000);
+                }).catch(function(err) {
+                    acoreShowPdumpError(
+                        err && err.message ? err.message : String(err),
+                        err && err.detail  ? err.detail  : null
+                    );
+                });
+            }
+
+            function acoreDownloadAll(chars) {
+                fetch(acorePdumpAllUrl, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'X-WP-Nonce': acorePdumpNonce
+                    },
+                    body: JSON.stringify({ characters: chars })
+                }).then(function(resp) {
+                    if (!resp.ok) {
+                        return resp.json().then(function(body) {
+                            var data   = (body && body.data) ? body.data : body;
+                            var msg    = (data && data.message) ? data.message : 'Export failed (HTTP ' + resp.status + ').';
+                            var detail = (data && data.detail)  ? data.detail  : null;
+                            var err    = new Error(msg);
+                            err.detail = detail;
+                            throw err;
+                        });
+                    }
+                    return resp.blob();
+                }).then(function(blob) {
+                    var filename = acorePdumpZipFilename();
+                    var url  = URL.createObjectURL(blob);
+                    var link = document.createElement('a');
+                    link.href     = url;
+                    link.download = filename;
+                    document.body.appendChild(link);
+                    link.click();
+                    document.body.removeChild(link);
+                    setTimeout(function() { URL.revokeObjectURL(url); }, 10000);
+                }).catch(function(err) {
+                    acoreShowPdumpError(
+                        err && err.message ? err.message : String(err),
+                        err && err.detail  ? err.detail  : null
+                    );
+                });
+            }
+
+            document.getElementById('acore-pdump-cancel').addEventListener('click', acoreClosePdumpModal);
+            acorePdumpConfirm.addEventListener('click', function() {
+                if (typeof acorePdumpOnConfirm === 'function') acorePdumpOnConfirm();
+                acoreClosePdumpModal();
+            });
+            acorePdumpModal.addEventListener('click', function(e) {
+                if (e.target === acorePdumpModal) acoreClosePdumpModal();
+            });
+
+            $(document).on('click', '.acore-export-btn', function() {
+                var guid  = $(this).data('char-guid');
+                var name  = $(this).data('char-name');
+                var order = $(this).data('char-order');
+                var level = $(this).data('char-level');
+                var race  = $(this).data('char-race');
+                var cls   = $(this).data('char-class');
+                var upper = name.toUpperCase();
+                var msg  = 'You\'re about to PDUMP a.k.a make a copy of your character ' + upper + ' that can be used in AzerothCore. '
+                         + 'This will NOT COPY any custom contents, for example modules like Transmog or Custom Items like Physical Costumes. '
+                         + '

This Character Dump was extracted from this version:
' + acorePdumpRevisionLink() + + '

The following information will be anonymised or omitted from the dump: ' + + 'CHARACTER NAMEPOSITIONHEARTHSTONE LOCATION • ' + + 'GOLDTIMESTAMPSONLINE STATUS • ' + + 'ACHIEVEMENT DATESMAIL CONTENTS & SENDER • ' + + 'ITEM CREATOR/GIFTERAURA CASTER • ' + + 'EQUIPMENT SET NAMESCUSTOM CHAT CHANNELS • ' + + 'CHARACTER & ACCOUNT IDs (replaced with random values).'; + acoreShowPdumpModal(msg, function() { + acoreDownloadDump(guid, order, level, race, cls); + }); + }); + + $(document).on('click', '.acore-export-all-btn', function() { + var chars = []; + $('.acore-export-btn').each(function() { + chars.push({ + guid: $(this).data('char-guid'), + name: $(this).data('char-name'), + order: $(this).data('char-order'), + level: $(this).data('char-level'), + race: $(this).data('char-race'), + 'class': $(this).data('char-class') + }); + }); + var nameList = chars.map(function(c) { return '' + c.name.toUpperCase() + ''; }).join(', '); + var msg = 'You\'re about to PDUMP a.k.a make a copy of ALL your characters: ' + nameList + '. ' + + 'This will NOT COPY any custom contents, for example modules like Transmog or Custom Items like Physical Costumes. ' + + '

This Character Dump was extracted from this version:
' + acorePdumpRevisionLink() + + '

The following information will be anonymised or omitted from the dump: ' + + 'CHARACTER NAMEPOSITIONHEARTHSTONE LOCATION • ' + + 'GOLDTIMESTAMPSONLINE STATUS • ' + + 'ACHIEVEMENT DATESMAIL CONTENTS & SENDER • ' + + 'ITEM CREATOR/GIFTERAURA CASTER • ' + + 'EQUIPMENT SET NAMESCUSTOM CHAT CHANNELS • ' + + 'CHARACTER & ACCOUNT IDs (replaced with random values).'; + acoreShowPdumpModal(msg, function() { + acoreDownloadAll(chars); + }); + }); + }); + acore_db_world_name; $itemQuery = "SELECT mi.`mail_id`, ii.`itemEntry`, ii.`count`, - it.`name` AS item_name + it.`name` AS item_name, it.`Quality` AS item_quality FROM `mail_items` mi JOIN `item_instance` ii ON mi.`item_guid` = ii.`guid` JOIN `$worldDb`.`item_template` it ON ii.`itemEntry` = it.`entry` diff --git a/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php b/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php index 5a882e5c2..c75b11169 100644 --- a/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php +++ b/src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php @@ -2,6 +2,8 @@ namespace ACore\Components\MailReturnMenu; +use ACore\Utils\AcoreCharColors; + class MailReturnView { @@ -17,58 +19,80 @@ public function getMailReturnRender($chars) ob_start(); wp_enqueue_style('bootstrap-css', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css', array(), '5.1.3'); - wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.1'); + wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.5'); wp_enqueue_script('bootstrap-js', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js', array(), '5.1.3'); wp_enqueue_script('jquery'); - wp_enqueue_script('acore-mail-return-js', ACORE_URL_PLG . 'web/assets/mail-return/mail-return.js', array('jquery'), null, true); + wp_enqueue_script('power-js', 'https://wow.zamimg.com/widgets/power.js', array(), null, false); + wp_enqueue_script('acore-mail-return-js', ACORE_URL_PLG . 'web/assets/mail-return/mail-return.js', array('jquery'), '2.3', true); + wp_localize_script('acore-mail-return-js', 'mailReturnData', [ + 'mailsUrl' => rest_url(ACORE_SLUG . '/v1/mail-return/list'), + 'returnUrl' => rest_url(ACORE_SLUG . '/v1/mail-return'), + 'assetsUrl' => ACORE_URL_PLG . 'web/assets/', + 'nonce' => wp_create_nonce('wp_rest'), + ]); ?> -
-
-
-
-

Mail Return

-

You can return sent mails that have not yet been read by the recipient in this page. Select the character, the sent mail and hit return.

-
- - - - -