Skip to content

Commit 3203e18

Browse files
NyeriahclaudeHelias
authored
feat(mail-return): add mail return tool page (#180)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Stefano Borzì <stefanoborzi32@gmail.com>
1 parent 6fd89f0 commit 3203e18

7 files changed

Lines changed: 589 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace ACore\Components\MailReturnMenu;
4+
5+
use ACore\Components\MailReturnMenu\MailReturnController;
6+
7+
add_action('rest_api_init', function () {
8+
// Get sent unread mails for a character
9+
register_rest_route(ACORE_SLUG . '/v1', 'mail-return/list/(?P<charGuid>\d+)', array(
10+
'methods' => 'GET',
11+
'callback' => function ($request) {
12+
try {
13+
$charGuid = $request->get_param('charGuid');
14+
$mails = MailReturnController::getSentUnreadMails($charGuid);
15+
return new \WP_REST_Response($mails, 200);
16+
17+
} catch (\InvalidArgumentException $e) {
18+
return new \WP_Error('invalid_character', $e->getMessage(), array('status' => 400));
19+
20+
} catch (\Exception $e) {
21+
return new \WP_Error('server_error', 'An unexpected error occurred', array('status' => 500));
22+
}
23+
},
24+
'permission_callback' => function () {
25+
return is_user_logged_in();
26+
},
27+
));
28+
29+
// Return a mail
30+
register_rest_route(ACORE_SLUG . '/v1', 'mail-return', array(
31+
'methods' => 'POST',
32+
'callback' => function ($request) {
33+
try {
34+
$charGuid = $request->get_param('charGuid');
35+
$mailId = $request->get_param('mailId');
36+
$message = MailReturnController::returnMail($charGuid, $mailId);
37+
return new \WP_REST_Response(['message' => $message], 200);
38+
39+
} catch (\InvalidArgumentException $e) {
40+
return new \WP_Error('invalid_request', $e->getMessage(), array('status' => 400));
41+
42+
} catch (\Exception $e) {
43+
return new \WP_Error('server_error', 'An unexpected error occurred', array('status' => 500));
44+
}
45+
},
46+
'permission_callback' => function () {
47+
return is_user_logged_in();
48+
},
49+
));
50+
});
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php
2+
3+
namespace ACore\Components\MailReturnMenu;
4+
5+
use ACore\Manager\ACoreServices;
6+
use ACore\Manager\Opts;
7+
use ACore\Components\MailReturnMenu\MailReturnView;
8+
use InvalidArgumentException;
9+
10+
class MailReturnController
11+
{
12+
private $view;
13+
14+
public function __construct()
15+
{
16+
$this->view = new MailReturnView($this);
17+
}
18+
19+
public function renderCharacters()
20+
{
21+
echo $this->view->getMailReturnRender(self::getCharactersByAcId());
22+
}
23+
24+
private static function validateAccountId()
25+
{
26+
$accId = ACoreServices::I()->getAcoreAccountId();
27+
28+
if (!isset($accId) || $accId === null || $accId === '' || trim($accId) === '' || !is_numeric($accId)) {
29+
throw new InvalidArgumentException("Invalid user account ID provided.");
30+
}
31+
32+
return intval($accId);
33+
}
34+
35+
private static function validateCharacterOwnership($conn, $charGuid, $accId)
36+
{
37+
$stmt = $conn->prepare(
38+
"SELECT `guid`, `name` FROM `characters`
39+
WHERE `guid` = ? AND `account` = ? AND `deleteDate` IS NULL"
40+
);
41+
$stmt->bindValue(1, $charGuid);
42+
$stmt->bindValue(2, $accId);
43+
$result = $stmt->executeQuery();
44+
$character = $result->fetchAssociative();
45+
46+
if (!$character) {
47+
throw new InvalidArgumentException("Character not found");
48+
}
49+
50+
return $character;
51+
}
52+
53+
public static function getSentUnreadMails($charGuid)
54+
{
55+
if (!is_numeric($charGuid)) {
56+
throw new InvalidArgumentException("Invalid parameters");
57+
}
58+
$charGuid = intval($charGuid);
59+
60+
$accId = self::validateAccountId();
61+
$conn = ACoreServices::I()->getCharacterEm()->getConnection();
62+
63+
self::validateCharacterOwnership($conn, $charGuid, $accId);
64+
65+
// Get mails sent by this character that are unread by the receiver
66+
// messageType = 0 means player mail
67+
$query = "SELECT m.`id`, m.`subject`, m.`has_items`, m.`money`,
68+
m.`expire_time`, m.`deliver_time`,
69+
rc.`name` AS receiver_name, rc.`race` AS receiver_race,
70+
rc.`class` AS receiver_class, rc.`gender` AS receiver_gender,
71+
rc.`level` AS receiver_level
72+
FROM `mail` m
73+
JOIN `characters` rc ON m.`receiver` = rc.`guid`
74+
WHERE m.`sender` = ?
75+
AND m.`messageType` = 0
76+
AND (m.`checked` & 1) = 0
77+
ORDER BY m.`deliver_time` DESC";
78+
79+
$stmt = $conn->prepare($query);
80+
$stmt->bindValue(1, $charGuid);
81+
$result = $stmt->executeQuery();
82+
$mails = $result->fetchAllAssociative();
83+
84+
// Fetch items for mails that have items
85+
$mailIds = [];
86+
foreach ($mails as $mail) {
87+
if ($mail['has_items'] == 1) {
88+
$mailIds[] = $mail['id'];
89+
}
90+
}
91+
92+
$itemsByMail = [];
93+
if (!empty($mailIds)) {
94+
$placeholders = implode(',', array_fill(0, count($mailIds), '?'));
95+
$worldDb = Opts::I()->acore_db_world_name;
96+
$itemQuery = "SELECT mi.`mail_id`, ii.`itemEntry`, ii.`count`,
97+
it.`name` AS item_name
98+
FROM `mail_items` mi
99+
JOIN `item_instance` ii ON mi.`item_guid` = ii.`guid`
100+
JOIN `$worldDb`.`item_template` it ON ii.`itemEntry` = it.`entry`
101+
WHERE mi.`mail_id` IN ($placeholders)";
102+
$stmt = $conn->prepare($itemQuery);
103+
$i = 1;
104+
foreach ($mailIds as $id) {
105+
$stmt->bindValue($i++, $id);
106+
}
107+
$itemResult = $stmt->executeQuery();
108+
foreach ($itemResult->fetchAllAssociative() as $item) {
109+
$itemsByMail[$item['mail_id']][] = $item;
110+
}
111+
}
112+
113+
// Attach items to their mails
114+
foreach ($mails as &$mail) {
115+
$mail['items'] = $itemsByMail[$mail['id']] ?? [];
116+
}
117+
118+
return $mails;
119+
}
120+
121+
public static function returnMail($charGuid, $mailId)
122+
{
123+
$accId = self::validateAccountId();
124+
$conn = ACoreServices::I()->getCharacterEm()->getConnection();
125+
126+
// Validate charGuid and mailId are integers to prevent injection
127+
if (!is_numeric($charGuid) || !is_numeric($mailId)) {
128+
throw new InvalidArgumentException("Invalid parameters");
129+
}
130+
$charGuid = intval($charGuid);
131+
$mailId = intval($mailId);
132+
133+
// Verify the sender character belongs to the current user
134+
$sender = self::validateCharacterOwnership($conn, $charGuid, $accId);
135+
136+
// Verify the mail was sent by this character and is still unread
137+
$mailQuery = "SELECT m.`id`, m.`receiver`
138+
FROM `mail` m
139+
WHERE m.`id` = ? AND m.`sender` = ? AND m.`messageType` = 0 AND (m.`checked` & 1) = 0";
140+
$stmt = $conn->prepare($mailQuery);
141+
$stmt->bindValue(1, $mailId);
142+
$stmt->bindValue(2, $charGuid);
143+
$mailResult = $stmt->executeQuery();
144+
$mail = $mailResult->fetchAssociative();
145+
146+
if (!$mail) {
147+
throw new InvalidArgumentException("Mail not found or already read");
148+
}
149+
150+
// Verify the receiver character actually exists and has this mail
151+
$receiverQuery = "SELECT `guid`, `name` FROM `characters` WHERE `guid` = ? AND `deleteDate` IS NULL";
152+
$stmt = $conn->prepare($receiverQuery);
153+
$stmt->bindValue(1, $mail['receiver']);
154+
$receiverResult = $stmt->executeQuery();
155+
$receiver = $receiverResult->fetchAssociative();
156+
157+
if (!$receiver) {
158+
throw new InvalidArgumentException("Receiver character not found");
159+
}
160+
161+
// Use the receiver name from the database, not from user input
162+
$receiverName = $receiver['name'];
163+
164+
// Execute SOAP command to return the mail
165+
$soap = ACoreServices::I()->getGameMailSoap();
166+
$result = $soap->executeCommand(".mail return $receiverName $mailId");
167+
168+
return "Mail #$mailId returned successfully";
169+
}
170+
171+
public static function getCharactersByAcId()
172+
{
173+
$accId = self::validateAccountId();
174+
175+
$query = "SELECT
176+
c.`guid`, c.`name`, c.`order`, c.`race`, c.`class`, c.`level`, c.`gender`
177+
FROM `characters` c
178+
WHERE c.`deleteDate` IS NULL
179+
AND c.`account` = ?
180+
ORDER BY COALESCE(c.`order`, c.`guid`)
181+
";
182+
$conn = ACoreServices::I()->getCharacterEm()->getConnection();
183+
$stmt = $conn->prepare($query);
184+
$stmt->bindValue(1, $accId);
185+
$result = $stmt->executeQuery();
186+
return $result->fetchAllAssociative();
187+
}
188+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace ACore\Components\MailReturnMenu;
4+
5+
use ACore\Components\MailReturnMenu\MailReturnController;
6+
7+
require_once 'MailReturnApi.php';
8+
9+
add_action('init', __NAMESPACE__ . '\\mail_return_menu_init');
10+
11+
class MailReturnMenu
12+
{
13+
private static $instance = null;
14+
15+
/**
16+
* Singleton
17+
* @return MailReturnMenu
18+
*/
19+
public static function I()
20+
{
21+
if (!self::$instance) {
22+
self::$instance = new self();
23+
}
24+
25+
return self::$instance;
26+
}
27+
28+
function acore_mail_return_menu()
29+
{
30+
add_submenu_page('profile.php', 'Mail Return', 'Mail Return', 'read', ACORE_SLUG . '-mail-return-menu', array($this, 'acore_mail_return_menu_page'));
31+
}
32+
33+
function acore_mail_return_menu_page()
34+
{
35+
$controller = new MailReturnController();
36+
$controller->renderCharacters();
37+
}
38+
}
39+
40+
function mail_return_menu_init()
41+
{
42+
$mailReturnMenu = MailReturnMenu::I();
43+
44+
add_action('admin_menu', array($mailReturnMenu, 'acore_mail_return_menu'));
45+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace ACore\Components\MailReturnMenu;
4+
5+
6+
class MailReturnView
7+
{
8+
private $controller;
9+
10+
public function __construct($controller)
11+
{
12+
$this->controller = $controller;
13+
}
14+
15+
public function getMailReturnRender($chars)
16+
{
17+
ob_start();
18+
19+
wp_enqueue_style('bootstrap-css', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css', array(), '5.1.3');
20+
wp_enqueue_style('acore-css', ACORE_URL_PLG . 'web/assets/css/main.css', array(), '0.1');
21+
wp_enqueue_script('bootstrap-js', '//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js', array(), '5.1.3');
22+
wp_enqueue_script('jquery');
23+
wp_enqueue_script('acore-mail-return-js', ACORE_URL_PLG . 'web/assets/mail-return/mail-return.js', array('jquery'), null, true);
24+
25+
?>
26+
27+
<div class="wrap">
28+
<div class="col-sm-6">
29+
<div class="card">
30+
<div class="card-body">
31+
<h3>Mail Return</h3>
32+
<p>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.</p>
33+
<hr>
34+
35+
<label for="mail-return-char-select"><strong>Select Character:</strong></label>
36+
<select id="mail-return-char-select" class="form-select mb-3">
37+
<option value="">-- Select a character --</option>
38+
<?php foreach ($chars as $char) { ?>
39+
<option value="<?= intval($char["guid"]) ?>" data-name="<?= esc_attr($char["name"]) ?>">
40+
<?= esc_html($char["name"]) ?> (Level <?= intval($char["level"]) ?>)
41+
</option>
42+
<?php } ?>
43+
</select>
44+
45+
<div id="mail-return-loading" style="display:none;" class="text-center my-3">
46+
<div class="spinner-border text-primary" role="status">
47+
<span class="visually-hidden">Loading...</span>
48+
</div>
49+
</div>
50+
51+
<div id="mail-return-list" style="display:none;">
52+
<h5>Unread Sent Mails</h5>
53+
<ul id="mail-return-items" class="list-unstyled"></ul>
54+
<p id="mail-return-empty" style="display:none;" class="text-muted">No unread sent mails found for this character.</p>
55+
</div>
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
61+
<script>
62+
var mailReturnData = {
63+
nonce: "<?php echo esc_js(wp_create_nonce('wp_rest')); ?>",
64+
mailsUrl: "<?php echo esc_url(get_rest_url(null, ACORE_SLUG . '/v1/mail-return/list')); ?>",
65+
returnUrl: "<?php echo esc_url(get_rest_url(null, ACORE_SLUG . '/v1/mail-return')); ?>",
66+
assetsUrl: "<?php echo esc_url(ACORE_URL_PLG . 'web/assets/'); ?>"
67+
};
68+
</script>
69+
<script>var whTooltips = {colorLinks: true, iconizeLinks: true, renameLinks: true};</script>
70+
<script src="https://wow.zamimg.com/js/tooltips.js"></script>
71+
<?php
72+
return ob_get_clean();
73+
}
74+
}

src/acore-wp-plugin/src/boot.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require_once ACORE_PATH_PLG . 'src/Components/AdminPanel/AdminPanel.php';
1010
require_once ACORE_PATH_PLG . 'src/Components/CharactersMenu/CharactersMenu.php';
1111
require_once ACORE_PATH_PLG . 'src/Components/UnstuckMenu/UnstuckMenu.php';
12+
require_once ACORE_PATH_PLG . 'src/Components/MailReturnMenu/MailReturnMenu.php';
1213
require_once ACORE_PATH_PLG . 'src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php';
1314
require_once ACORE_PATH_PLG . 'src/Components/ServerInfo/ServerInfo.php';
1415
require_once ACORE_PATH_PLG . 'src/Components/Tools/ToolsInfo.php';

0 commit comments

Comments
 (0)