Skip to content

[8] - Show ban and mute info to users#205

Open
TheSCREWEDSoftware wants to merge 15 commits into
azerothcore:masterfrom
TheSCREWEDSoftware:8_punish_info
Open

[8] - Show ban and mute info to users#205
TheSCREWEDSoftware wants to merge 15 commits into
azerothcore:masterfrom
TheSCREWEDSoftware:8_punish_info

Conversation

@TheSCREWEDSoftware

@TheSCREWEDSoftware TheSCREWEDSoftware commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

This PR is not a Split from the main PR: #197

But the Git history is based on the last commit of #204

Also opening this so it can be sorted for review by Rabbit, so I can also do 1 last PR which implements PDUMP (with a random GUID and other info to not btraced to the real player).

  • Profile Tab (Warning similair to 2FA)

Account Muted, Account Banned and Characters Banned (it got re-order to properly match the character order instad of name)
Character tab now displays if account is banned or if it's muted (banned > muted)
Screenshot_25

Banned characters
Screenshot_26

Muted (not serving yet) + Banned Characters
Screenshot_27

Muted (serving) + Banned Characters
Screenshot_28

Muted (Serving) + Banned Account + Banned Characters
Screenshot_29

  • Characters Tab
Screenshot_30

"Banned" = Perma Banned

Summary by CodeRabbit

  • New Features

    • Added a user Security page with password change, website/in-game 2FA management (verify, remove), and recent connections history.
    • Introduced admin dark mode with a toggle and themed styling.
    • Revamped Item Restoration and Mail Return UIs with card/grid layouts and richer item visuals.
    • Expanded server/tools and realm module management (module requirements, refresh, and Scroll of Resurrection availability).
    • Added login history tracking with GeoIP support and related UI/logging.
  • Bug Fixes

    • Added nonce protection and input validation to sensitive admin actions and item restoration.
    • Improved character status/badges, ordering/reset behavior, and mail item quality handling.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c77b4729-616e-4621-ab07-3fc70d28beee

📥 Commits

Reviewing files that changed from the base of the PR and between c1cc262 and e1f9344.

📒 Files selected for processing (1)
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php

📝 Walkthrough

Walkthrough

This PR adds security and 2FA flows, login-history tracking, module-aware admin tooling, a dark-mode toggle and shared styling updates, plus refreshed card-based UI for item restoration, mail return, unstuck, RAF, and character ordering.

Changes

Security, admin tooling, and UI refresh

Layer / File(s) Summary
Shared foundation
src/acore-wp-plugin/src/Utils/AcoreCharColors.php, src/acore-wp-plugin/src/Manager/Opts.php, src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php, src/acore-wp-plugin/src/boot.php, src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php, src/acore-wp-plugin/src/Hooks/Various/DarkMode.php
Adds shared character-color utilities, new option flags, boot-time module loading, required WP 2FA registration, and a SmartstoneService docblock edit.
Security backend and REST
src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php, src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php, src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php, src/acore-wp-plugin/src/Manager/ACoreServices.php, src/acore-wp-plugin/src/Components/Tools/ToolsApi.php, src/acore-wp-plugin/src/Hooks/User/Include.php
Adds login-history logging and lookup helpers, password-change handling with history checks, 2FA validation and unlock helpers, server-info REST routes for 2FA and login history, item-restore authorization, and the user-hook include wiring for the new modules.
Admin panels and module controls
src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php, src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php, src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php, src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php
Updates the realm settings page with module panels and SOAP checks, extends the tools page with module-aware Scroll of Resurrection, web integration, 2FA, and login-history controls, and adds nonce checks and threshold cleanup in the admin settings controller and RAF form.
User profile security and status
src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php, src/acore-wp-plugin/src/Components/UserPanel/UserController.php, src/acore-wp-plugin/src/Components/UserPanel/UserView.php, src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php, src/acore-wp-plugin/src/Hooks/User/User.php, src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php, src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php, src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
Adds the Security submenu, renders the Security page, rewrites profile expansion controls, adds profile notices for 2FA removals and bans, shows recent connections, and updates character-order status handling with mute and ban data.
Item, mail, unstuck, RAF, and scroll UI
src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php, src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnController.php, src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php, src/acore-wp-plugin/web/assets/mail-return/mail-return.js, src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php, src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php, src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollView.php
Reworks item restoration into a card grid, refreshes mail return into a sidebar and card layout, updates unstuck rendering and data delivery, adjusts RAF presentation, and gates the Resurrection Scroll menu and view on module availability.
Dark mode and shared styles
src/acore-wp-plugin/src/Hooks/Various/DarkMode.php, src/acore-wp-plugin/web/assets/css/dark-mode.css, src/acore-wp-plugin/web/assets/css/theme.css, src/acore-wp-plugin/web/assets/css/main.css
Adds a dark-mode toggle and broad stylesheet updates for admin screens, character rows, security tables, mail return, item restoration, profile sub-pages, buttons, forms, and myCRED UI.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through nonce fields and dark-mode glow,
The login trails and 2FA now flow.
With cards and colors the pages cheer,
And my little paws gave the admin a gear!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main user-facing change: displaying ban and mute information.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (20)
src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php-272-280 (1)

272-280: 🎯 Functional Correctness | 🟠 Major

Send the REST nonce with this status check.
server-info is gated by manage_options, but this $.get() is the only REST call here that skips X-WP-Nonce. WordPress cookie auth can treat it as unauthenticated and hide the SOAP/modules panels for logged-in admins.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php` around
lines 272 - 280, The checkSoap status request is missing the REST nonce, so the
`server-info` call can be treated as unauthenticated for logged-in admins.
Update the `checkSoap` function in `RealmSettings.php` so the `$.get(restBase +
'server-info', ...)` request includes `X-WP-Nonce` the same way the other REST
calls do, using the existing nonce value already available in this admin page.
Keep the existing success/fail/always behavior in `setSoapStatus` and the
`manual` button state handling unchanged.
src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php-220-243 (1)

220-243: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not render the protected WP2FA panel before server-side unlock.

The gate is client-side only because $wp2faHtml is already present in the hidden DOM. Anyone with the page can reveal or inspect the backup-code/settings UI without completing the fresh 2FA check. Render the panel only when $twofaUnlocked is true; after successful verification, reload so the server-side transient controls rendering.

Proposed direction
-                    <div id="acore-2fa-panel" style="display:none; margin-top:16px;">
-                        <?= $wp2faHtml ?>
-                    </div>
+                    <div id="acore-2fa-panel" style="display:none; margin-top:16px;"></div>
-                if (data && data.success) {
-                    revealPanel();
+                if (data && data.success) {
+                    window.location.reload();

Apply the same pattern to the email gate branch.

Also applies to: 244-267

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php` around
lines 220 - 243, The WP2FA management panel is still being rendered into the DOM
in the gated branch, so the unlock is only cosmetic and can be bypassed by
inspecting hidden HTML. Update the SecurityPage rendering logic so $wp2faHtml is
emitted only when $twofaUnlocked is true, and keep the gated branch from
outputting the protected panel until a server-side unlock has occurred. After a
successful TOTP verification in the related unlock flow, reload the page so the
transient-controlled $twofaUnlocked state can render the panel, and apply the
same server-side gating pattern to the email-gate branch.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php-200-208 (1)

200-208: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle non-string SOAP failures before stripos.

ServerInfoApi::serverInfo() can return an Exception/non-string response, but Line 207 passes it to stripos(), which can fatal instead of returning the intended 503.

Proposed fix
            $result = ServerInfoApi::serverInfo();
+           if ($result instanceof \Throwable) {
+               error_log('[acore] server-info SOAP error: ' . $result->getMessage());
+               return new \WP_Error('soap_error', __('Server information is temporarily unavailable.', 'acore-wp-plugin'), ['status' => 503]);
+           }
+           if (!is_string($result)) {
+               error_log('[acore] server-info SOAP error: non-string response');
+               return new \WP_Error('soap_error', __('Server information is temporarily unavailable.', 'acore-wp-plugin'), ['status' => 503]);
+           }
            $errorPatterns = [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php` around lines
200 - 208, ServerInfoApi::serverInfo() may return an Exception or other
non-string value, so the SOAP error detection loop must guard before calling
stripos(). In the server info handling block, normalize or branch on the result
type first, then only run the errorPatterns matching for string responses; for
non-string failures, log them and return the intended 503 path directly. Use the
existing ServerInfoApi::serverInfo, $result, and $errorPatterns block to locate
the fix.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php-447-456 (1)

447-456: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Apply the same attempt limiter to direct in-game 2FA removal verification.

This fresh-token path bypasses the 5-attempt transient used by verify-website-2fa, so repeated wrong TOTP submissions to this endpoint are not rate-limited.

Proposed fix
                $data  = $request->get_json_params();
                $token = isset($data['token']) ? trim((string) $data['token']) : '';
+               $attemptKey = acore_2fa_attempt_key($user->ID);
+               if ((int) get_transient($attemptKey) >= 5)
+                   return new \WP_Error('rate_limited', __('Too many incorrect codes. Please wait a few minutes.', 'acore-wp-plugin'), ['status' => 429]);
                if (!preg_match('/^\d{6}$/', $token))
                    return new \WP_Error('invalid_token', __('Please enter a valid 6-digit code.'), ['status' => 400]);
-               if (!\ACore\Components\ServerInfo\acore_wp2fa_code_is_valid($user->ID, $token))
+               if (!\ACore\Components\ServerInfo\acore_wp2fa_code_is_valid($user->ID, $token)) {
+                   set_transient($attemptKey, ((int) get_transient($attemptKey)) + 1, 10 * MINUTE_IN_SECONDS);
                    return new \WP_Error('wrong_token', __('Incorrect code. Please try again.'), ['status' => 401]);
+               }
+               delete_transient($attemptKey);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php` around lines
447 - 456, The fresh-token branch in ServerInfoApi’s in-game 2FA removal flow is
missing the same 5-attempt limiter used by verify-website-2fa, so repeated wrong
TOTP submissions are not rate-limited. Update the direct verification path
around acore_wp2fa_code_is_valid to reuse the existing attempt-tracking
transient logic (increment on failure, block after 5, reset on success) with the
same user-scoped key pattern used elsewhere in ServerInfoApi. Keep the
authorization flow unchanged, but ensure both the unlock-based and fresh-token
paths enforce the same retry protection before returning wrong_token.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php-56-61 (1)

56-61: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include unbandate = bandate permanent account bans.

The character-ban query treats unbandate = bandate as permanent, but this account-ban query filters those rows out, so permanent account bans can be missed.

Proposed fix
-                   AND (`unbandate` = 0 OR `unbandate` > UNIX_TIMESTAMP())
+                   AND (`unbandate` = 0 OR `unbandate` = `bandate` OR `unbandate` > UNIX_TIMESTAMP())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php`
around lines 56 - 61, The account-ban lookup in CharactersController::...
currently excludes permanent bans because the query only accepts unbandate = 0
or future timestamps. Update the account_banned query used for $accBanRow so it
also treats unbandate = bandate as a permanent ban, matching the character-ban
logic. Keep the existing active and latest-row behavior intact while broadening
the WHERE clause to include this permanent-ban case.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php-35-39 (1)

35-39: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mirror the permanent-ban predicate in the menu badge query.

Without unbandate = bandate, permanent account bans encoded that way will not show the red Banned badge in the submenu.

Proposed fix
-                   AND (`unbandate` = 0 OR `unbandate` > UNIX_TIMESTAMP())
+                   AND (`unbandate` = 0 OR `unbandate` = `bandate` OR `unbandate` > UNIX_TIMESTAMP())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php` around
lines 35 - 39, The CharactersMenu badge query is missing the permanent-ban
condition used elsewhere, so accounts banned with matching unbandate and bandate
won’t be marked banned. Update the query in CharactersMenu::executeQuery usage
to mirror the existing permanent-ban predicate by adding the unbandate = bandate
check alongside the current active and unbandate timestamp logic, keeping the
badge state consistent for all permanent bans.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php-40-42 (1)

40-42: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat unbandate = bandate as a permanent account ban here too.

If the account-ban query includes permanent rows with equal bandate/unbandate, this view currently renders them as a temporary ban with 0 seconds remaining instead of Banned.

Proposed fix
         // Account ban
         $isAccountBanned = !empty($accBanRow);
-        $accBanPerma     = $isAccountBanned && intval($accBanRow['unbandate']) === 0;
-        $accBanRemaining = ($isAccountBanned && !$accBanPerma) ? max(0, intval($accBanRow['unbandate']) - $now) : 0;
+        $accBanBandate   = $isAccountBanned ? intval($accBanRow['bandate']) : 0;
+        $accBanUnbandate = $isAccountBanned ? intval($accBanRow['unbandate']) : 0;
+        $accBanPerma     = $isAccountBanned && ($accBanUnbandate === 0 || $accBanUnbandate === $accBanBandate);
+        $accBanRemaining = ($isAccountBanned && !$accBanPerma) ? max(0, $accBanUnbandate - $now) : 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php` around
lines 40 - 42, The account-ban rendering logic in CharactersView treats only
unbandate = 0 as permanent, so equal bandate/unbandate rows still show as a
temporary ban with 0 seconds remaining. Update the ban-state checks around
$isAccountBanned, $accBanPerma, and $accBanRemaining so that rows where
unbandate equals bandate are also classified as permanent, and ensure the UI
path that displays “Banned” is used for that case instead of the remaining-time
display.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php-31-46 (1)

31-46: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard missing account IDs before building the character query.

getAcoreAccountId() can return no row; interpolating that into c.account = $accId can produce malformed SQL for users without a linked game account. Validate once and bind it like the other queries.

Proposed fix
-        $accId = ACoreServices::I()->getAcoreAccountId();
+        $accId = ACoreServices::I()->getAcoreAccountId();
+        if (!isset($accId) || $accId === '' || !is_numeric($accId)) {
+            wp_die('<div class="notice notice-error"><p>Unable to load your game account information.</p></div>');
+        }
+        $accId = intval($accId);
         $conn  = ACoreServices::I()->getCharacterEm()->getConnection();

@@
-            WHERE c.`deleteDate` IS NULL AND c.`account` = $accId
+            WHERE c.`deleteDate` IS NULL AND c.`account` = ?
             ORDER BY COALESCE(c.`order`, c.`guid`)
         ";
-        $chars = $conn->executeQuery($query)->fetchAllAssociative();
+        $chars = $conn->executeQuery($query, [$accId])->fetchAllAssociative();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php`
around lines 31 - 46, The character lookup in CharactersController is building
SQL with a raw $accId from ACoreServices::I()->getAcoreAccountId(), which can be
missing and lead to invalid queries. In the controller flow that prepares the
characters list, guard for a missing account ID before executing the query, and
if present, bind the account value as a parameter instead of interpolating it
into the SQL string. Use the existing query/connection logic around
getAcoreAccountId(), getCharacterEm()->getConnection(), and executeQuery() to
keep the fix localized.
src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php-15-25 (1)

15-25: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Add a retention path for stored login IPs.

This stores website and in-game IP history indefinitely. IP addresses are sensitive personal data; add a retention setting and scheduled pruning before enabling this broadly.

Also applies to: 44-54, 94-104, 330-337

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php` around lines 15 -
25, The login IP storage in ConnectionLogger currently keeps sensitive address
history indefinitely, so add a configurable retention policy and scheduled
pruning before broad use. Update the table/record flow around the schema
creation and the logging logic in ConnectionLogger to respect a retention
setting, and add a cleanup routine that deletes entries older than the
configured window. Use the existing ConnectionLogger methods and schema
definition as the integration points, and make sure the pruning is wired into
the plugin’s scheduled job path rather than leaving data to accumulate forever.
src/acore-wp-plugin/src/Hooks/User/User.php-784-819 (1)

784-819: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move account lookup inside the guarded block.

Line 785 can initialize DB services before the try, so a connection/config failure can break profile.php admin notices instead of silently skipping the badge.

Proposed fix
     $now   = time();
-    $accId = \ACore\Manager\ACoreServices::I()->getAcoreAccountId();
-    if (!$accId) return;
 
     try {
-        $authConn = \ACore\Manager\ACoreServices::I()->getAccountEm()->getConnection();
-        $charConn = \ACore\Manager\ACoreServices::I()->getCharacterEm()->getConnection();
+        $services = \ACore\Manager\ACoreServices::I();
+        $accId    = $services->getAcoreAccountId();
+        if (!$accId) return;
+
+        $authConn = $services->getAccountEm()->getConnection();
+        $charConn = $services->getCharacterEm()->getConnection();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Hooks/User/User.php` around lines 784 - 819, Move the
account/service lookup into the protected badge-check path so a
connection/config failure does not escape before error handling. In the User
class method that builds the admin notice badge, keep the ACoreServices::I()
calls and getAcoreAccountId/getConnection lookups inside the try or after the
guard, and return early only after the database-backed checks have safely run.
This ensures profile.php notices fail closed instead of breaking the page when
DB services are unavailable.
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php-66-78 (1)

66-78: 🩺 Stability & Availability | 🟠 Major

Catch \Throwable around the SOAP password sync.
catch (\Exception $e) won’t intercept PHP Errors from service setup or SOAP calls, so the request can still fatal instead of taking the “website password unchanged” error path.

Proposed fix
-    } catch (\Exception $e) {
+    } catch (\Throwable $e) {
         $soapError = $e->getMessage();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php` around lines 66 - 78,
The SOAP password sync in PasswordHandler::handlePasswordChange only catches
\Exception, so PHP Errors can still fatal before the “website password
unchanged” fallback runs. Update the try/catch around
ACoreServices::I()->getAccountSoap() and setAccountPassword() to catch
\Throwable instead, and keep assigning the message into $soapError so the
existing error path is used for both Exceptions and Errors.
src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php-228-230 (1)

228-230: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Avoid plaintext GeoIP lookups Both lookup paths still send login IPs to http://ip-api.com over HTTP (/json and /batch). ip-api’s free endpoints are HTTP-only, so switch to an HTTPS-capable provider/pro plan or make GeoIP an explicit admin opt-in with clear disclosure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php` around lines 228 -
230, The GeoIP lookup in ConnectionLogger still sends login IPs to ip-api over
plain HTTP, so update the lookup path in the relevant lookup helper(s) used by
ConnectionLogger to avoid transmitting data insecurely. Replace the current
ip-api-based requests with an HTTPS-capable provider or gate GeoIP behind an
explicit admin opt-in with clear disclosure, and make sure both the single-IP
and batch lookup flows are handled consistently.
src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php-582-590 (1)

582-590: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reset leaves the old thresholds in storage.

This handler removes every threshold input before submit, but SettingsController::loadTools() only persists keys that are present in $_POST. That means acore_name_unlock_thresholds is never overwritten here, so the previous thresholds survive the “reset”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php` around lines
582 - 590, The reset handler for acore-name-unlock-reset is clearing the
threshold rows in the UI, but that does not remove the stored
acore_name_unlock_thresholds value because SettingsController::loadTools() only
saves fields present in $_POST. Update the Tools.php click callback so the form
submits an explicit empty value for the thresholds setting before calling
submit, ensuring the reset path overwrites the old stored thresholds instead of
leaving them behind.
src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php-264-270 (1)

264-270: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use real buttons for the threshold actions.

These controls are rendered as divs, so they are not keyboard-focusable and are not exposed as buttons to assistive tech. That makes threshold management unreachable for keyboard-only admins.

Proposed fix
-    <div id="acore-name-unlock-thresholds-add" class="button">
+    <button type="button" id="acore-name-unlock-thresholds-add" class="button">
         <span class="dashicons dashicons-plus" style="margin-top:5px;"></span> Add
-    </div>
-    <div id="acore-name-unlock-reset" class="button acore-btn-danger" title="Reset Name Unlock to defaults">
+    </button>
+    <button type="button" id="acore-name-unlock-reset" class="button acore-btn-danger" title="Reset Name Unlock to defaults">
         <span class="dashicons dashicons-image-rotate" style="margin-top:5px;"></span> Reset
-    </div>
+    </button>
-        const $btnDel = $(`<div class="button acore-btn-danger">`).appendTo($td);
+        const $btnDel = $('<button type="button" class="button acore-btn-danger">').appendTo($td);
         $btnDel.append(`<span class="dashicons dashicons-trash"></span>`);

Also applies to: 543-545

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php` around lines
264 - 270, The threshold action controls in the AdminPanel Tools view are
implemented as div-based pseudo-buttons, so they are not keyboard-accessible or
announced properly to assistive tech. Update the Add/Reset controls in Tools.php
(the threshold actions rendered near the acore-name-unlock-thresholds-add and
acore-name-unlock-reset IDs, including the matching threshold block later in the
file) to use real button elements while preserving the existing styling and
click behavior.
src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php-177-190 (1)

177-190: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist GeoIP as disabled when Security Logging is turned off.

If this page loads with Security Logging enabled and the user switches it off, the change handler only disables the GeoIP select. Disabled inputs are omitted from $_POST, and SettingsController::loadTools() only stores posted keys, so the previous acore_geoip_lookup value survives and can silently come back on later.

Proposed fix
 $('`#acore_security_logging`').on('change', function(){
     var on = $(this).val() === '1';
+    var $form = $(this).closest('form');
+    $form.find('input[type="hidden"][name="acore_geoip_lookup"]').remove();
+
     $('`#acore-geoip-wrap`').toggleClass('acore-days-inactive-disabled', !on)
          .attr('title', on ? '' : 'Security Logging must be enabled');
-    $('`#acore_geoip_lookup`').prop('disabled', !on);
+    $('`#acore_geoip_lookup`').prop('disabled', !on);
+
+    if (!on) {
+        $('`#acore_geoip_lookup`').val('0');
+        $('<input>', {
+            type: 'hidden',
+            name: 'acore_geoip_lookup',
+            value: '0'
+        }).appendTo($form);
+    }
 });

Also applies to: 336-342

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php` around lines
177 - 190, Persist acore_geoip_lookup as disabled whenever Security Logging is
turned off, because the GeoIP select is disabled and omitted from $_POST so
SettingsController::loadTools can keep the old value. Update the Tools page
behavior around the acore_geoip_lookup field (including the change-handler logic
that disables it) so a disabled state still submits an explicit 0, or otherwise
ensure the settings payload always includes acore_geoip_lookup when
acore_security_logging is not enabled. Keep the UI state and the stored option
in sync so the disabled setting cannot silently re-enable later.
src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php-193-199 (1)

193-199: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allow Old Passwords is currently a no-op.

This toggle never changes password behavior: acore_process_security_password() still rejects reused passwords unconditionally, so enabling the setting has no effect.

Proposed fix
-    if (acore_password_is_reused($user->ID, $newPass)) {
+    if (\ACore\Manager\Opts::I()->acore_allow_old_passwords !== '1'
+        && acore_password_is_reused($user->ID, $newPass)) {
         acore_pw_set_message($user->ID, 'error', __('This password has been used before. Please choose a different one.', 'acore-wp-plugin'));
         wp_redirect($security_url);
         exit;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php` around lines
193 - 199, The “Allow Old Passwords” setting is currently ignored by the
password-change flow, so wire the toggle through the password validation path.
Update acore_process_security_password() to read
Opts::I()->acore_allow_old_passwords and skip the reused-password rejection when
the option is enabled, while preserving the existing check when it is disabled.
Use the existing acore_allow_old_passwords option and the password validation
logic in acore_process_security_password() so the admin toggle actually changes
behavior.
src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php-215-235 (1)

215-235: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't infer success from the word "mail".

This branch only removes the card when the raw SOAP response happens to contain "mail". Any wording or localization change will turn a successful restore into an apparent failure and leave the UI out of sync. Please return a structured REST payload here (for example success/message) and branch on that instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php`
around lines 215 - 235, The restore success handling in the ItemRestorationPage
flow is currently inferring success from a raw response string containing
“mail”, which is brittle. Update the response handling in the restore promise
chain to use a structured REST payload with explicit success/message fields
instead of string matching, and branch in the existing success callback based on
that shape. Keep the card removal, renumbering, and empty-state logic in the
same success path, and use the existing error path for non-success responses.
src/acore-wp-plugin/src/Manager/ACoreServices.php-408-418 (1)

408-418: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't throw uncaught InvalidArgumentException out of the restore-list path.

ToolsApi::ItemRestoreList() calls this method directly from a REST callback, so these new exceptions will turn a bad or forged cguid into a 500 instead of a clean 4xx response. Convert this failure into a route-level WP_Error, or stop throwing here and return a sentinel the REST layer can map explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Manager/ACoreServices.php` around lines 408 - 418,
ACoreServices::restore character ownership validation currently throws
InvalidArgumentException from the restore-list path, which can bubble through
ToolsApi::ItemRestoreList into a 500. Update the flow so this failure is handled
at the REST boundary: either have ACoreServices return a clear sentinel/error
state instead of throwing, or catch the exception in ToolsApi::ItemRestoreList
and convert it into a WP_Error with an appropriate 4xx status. Keep the behavior
for valid restores unchanged and ensure the new handling covers both invalid
cguid input and missing character ownership.
src/acore-wp-plugin/web/assets/css/main.css-690-695 (1)

690-695: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the restore grid responsive to the actual content width.

With a fixed 460px sidebar, the content column is often far too narrow for four cards that each need an 80px icon plus padding, so this grid starts overflowing well before the 800px breakpoint. An adaptive definition avoids that.

Suggested CSS
 .item-restore-grid {
     display: grid;
-    grid-template-columns: repeat(4, 1fr);
+    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
     gap: 12px;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/web/assets/css/main.css` around lines 690 - 695, The
fixed four-column definition in item-restore-grid is causing overflow in the
narrow content column. Update the grid in main.css to adapt based on available
width instead of always using four equal columns, and keep the change scoped to
the .item-restore-grid rule so the cards wrap naturally before the
sidebar-constrained layout breaks.
src/acore-wp-plugin/web/assets/mail-return/mail-return.js-2-18 (1)

2-18: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Abort or ignore stale mail-list requests.

Rapidly clicking two characters can let the slower response render last, leaving one character highlighted while another character's mails and Return buttons are on screen. That makes the destructive action target the wrong mail set. Abort the previous XHR or discard responses whose charGuid is no longer active.

Suggested fix
+    var currentRequest = null;
+    var activeCharGuid = null;
+
     jQuery("`#acore-characters-mail`").on("click", ".acore-char-card", function () {
         var card = jQuery(this);
         var charGuid = card.data("char-guid");
@@
+        activeCharGuid = charGuid;
+        if (currentRequest) {
+            currentRequest.abort();
+        }
+
-        jQuery.ajax({
+        currentRequest = jQuery.ajax({
             success: function (mails) {
+                if (activeCharGuid !== charGuid) {
+                    return;
+                }
                 loading.hide();
@@
             },
             error: function (xhr, status, error) {
+                if (status === "abort") {
+                    return;
+                }
                 loading.hide();
                 emptyMsg.text("Failed to load mails.");
                 emptyWrap.show();
                 console.error("Failed to load mails:", error);
+            },
+            complete: function () {
+                currentRequest = null;
             }

Also applies to: 25-34, 65-149, 180-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/web/assets/mail-return/mail-return.js` around lines 2 -
18, The mail list loading flow in mail-return.js can render stale results when
multiple character cards are clicked quickly, so update the character-switch
request handling to ignore or cancel outdated responses. Use the click handler
on jQuery("`#acore-characters-mail`") / ".acore-char-card" and the mail-loading
path that populates "`#mail-return-items`" to track the active charGuid, abort any
previous XHR, or discard any callback whose charGuid no longer matches the
current active card before rendering mails and Return buttons. Ensure the
response-handling logic only updates the UI for the most recently selected
character.
🟡 Minor comments (8)
src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php-314-328 (1)

314-328: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the visible module list after auto refresh too.

The auto refresh persists fresh modules, but skips repainting the list/timestamp, so the panel can keep showing stale modules or “No modules loaded” until a manual refresh.

Proposed fix
-            if (!opts.auto) {
-                var $list = $('`#acore-modules-list`').empty();
-                if (newModules.length) {
-                    newModules.forEach(function(m){
-                        $list.append($('<span>').addClass('acore-module-tag').text(m));
-                    });
-                } else {
-                    $list.append($('<span>').addClass('acore-modules-empty').text('No modules found.'));
-                }
-                var d = new Date(res.refreshed * 1000);
-                $('`#acore-modules-refreshed`').html('Last refreshed:<br>' + d.toLocaleString());
+            var $list = $('`#acore-modules-list`').empty();
+            if (newModules.length) {
+                newModules.forEach(function(m){
+                    $list.append($('<span>').addClass('acore-module-tag').text(m));
+                });
+            } else {
+                $list.append($('<span>').addClass('acore-modules-empty').text('No modules found.'));
             }
+            var d = new Date(res.refreshed * 1000);
+            $('`#acore-modules-refreshed`').html('Last refreshed:<br>' + d.toLocaleString());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php` around
lines 314 - 328, The auto-refresh path in RealmSettings should update the
visible module list and refreshed timestamp the same way the manual refresh path
does. After the modules are persisted in the refresh handler, make sure the UI
repaint logic that updates `#acore-modules-list` and `#acore-modules-refreshed` runs
even when opts.auto is true, using the existing newModules and res.refreshed
values; keep renderAutoCheck(diffReferenced(...)) and knownModules assignment in
sync afterward.
src/acore-wp-plugin/src/Components/UserPanel/UserController.php-217-221 (1)

217-221: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the stale getModel() return contract.

The docblock still says UserModel, but Line 221 returns the controller itself. Either update the docblock to @return self/UserController or restore the intended model object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/UserPanel/UserController.php` around lines
217 - 221, The getModel() contract is stale because UserController::getModel()
returns the controller instance, not a UserModel. Update the docblock on
getModel() to match the actual return type (self/UserController) or change the
method to return the intended model object if that was the original design; make
sure the signature and documentation are consistent.

Source: Linters/SAST tools

src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php-28-29 (1)

28-29: 🎯 Functional Correctness | 🟡 Minor

Use wp_date() for these timestamps src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php:28-29 renders user-facing ban/mute end times, so date() can drift from the site timezone. Switch both helpers to wp_date().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php` around
lines 28 - 29, The timestamp helpers in CharactersView currently use date(),
which can ignore the site timezone for user-facing ban/mute end times. Update
the formatDate and formatTime methods to use wp_date() instead, preserving the
existing formats while relying on WordPress timezone handling.
src/acore-wp-plugin/src/Hooks/User/User.php-877-877 (1)

877-877: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t label IPv6 addresses as IPv4.

acore_resolve_client_ip() can return IPv6, so the heading should say “Your IP” instead.

Proposed fix
-    <h2 class="acore-conn-heading"><span><?php _e('Recent Connections', 'acore-wp-plugin'); ?></span><span class="acore-conn-myip"><?php _e('Your IPv4:', 'acore-wp-plugin'); ?> <?= esc_html($myIp) ?></span></h2>
+    <h2 class="acore-conn-heading"><span><?php _e('Recent Connections', 'acore-wp-plugin'); ?></span><span class="acore-conn-myip"><?php _e('Your IP:', 'acore-wp-plugin'); ?> <?= esc_html($myIp) ?></span></h2>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Hooks/User/User.php` at line 877, The heading text in
the User rendering block is incorrectly hardcoded as IPv4 even though
acore_resolve_client_ip() may return IPv6. Update the label in the User.php view
inside the Recent Connections header to say “Your IP” instead of “Your IPv4,”
keeping the existing $myIp output and related escaping unchanged.
src/acore-wp-plugin/src/Utils/AcoreCharColors.php-84-93 (1)

84-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a neutral fallback for unknown factions.

Line 87 currently renders any non-alliance race as Horde red, so unknown race IDs get Horde styling. Line 88 also assigns $fDark but never uses it.

Proposed fix
-        $fLight  = $faction === 'alliance' ? '`#3FACF4`' : '`#FF653D`';
-        $fDark   = $faction === 'alliance' ? '`#3FACF4`' : '`#FF653D`';
+        $fLight  = $faction === 'alliance'
+            ? '`#3FACF4`'
+            : ($faction === 'horde' ? '`#FF653D`' : self::FALLBACK_LIGHT);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Utils/AcoreCharColors.php` around lines 84 - 93, The
rowStyle method in AcoreCharColors is treating every non-alliance race as Horde,
so unknown race IDs get the wrong styling; update the faction-color fallback in
rowStyle to use a neutral value when RACE_FACTION returns unknown, and keep
alliance/Horde colors only for explicit matches. Also remove or stop computing
the unused $fDark variable in rowStyle so the logic stays clean and consistent.

Source: Linters/SAST tools

src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php-11-20 (1)

11-20: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the declared text-domain property here.

Opts exposes acore_page_alias, not page_alias, so these new _e() calls resolve against a null/empty domain and the new copy will not be translatable.

Proposed fix
-    <h1><?php _e('Recruit a Friend', Opts::I()->page_alias); ?></h1>
-    <p class="acore-raf-intro"><?php _e('Recruit your friends, help them level up and earn unique rewards.', Opts::I()->page_alias); ?></p>
+    <h1><?php _e('Recruit a Friend', Opts::I()->acore_page_alias); ?></h1>
+    <p class="acore-raf-intro"><?php _e('Recruit your friends, help them level up and earn unique rewards.', Opts::I()->acore_page_alias); ?></p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php`
around lines 11 - 20, The new localization calls in RafProgressPage are using
the wrong text-domain property, which breaks translation lookups. Update the
`_e()` calls in this view to use `Opts::I()->acore_page_alias` instead of
`Opts::I()->page_alias`, keeping the existing copy and structure intact so the
strings are registered under the declared domain.
src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php-82-90 (1)

82-90: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Start the mail panel hidden.

The markup says this column stays hidden until mails are loaded, but #mail-return-content renders immediately, so users land on an empty card before selecting a character.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php` around
lines 82 - 90, The mail return panel is rendering visible immediately even
though it should stay hidden until mail data is loaded. Update the markup in
MailReturnView so the `#mail-return-content` container starts hidden by default
and is only shown by the existing JS once the mails are ready; keep the change
scoped to the mail panel structure around the mail list and heading elements.
src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php-25-36 (1)

25-36: 🎯 Functional Correctness | 🟡 Minor

Define window.whTooltips before loading power.js
The current const whTooltips = ... runs after the CDN script and does not expose a global config object. Move this to an inline-before assignment (or enqueue a window.whTooltips block) ahead of power.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php` around
lines 25 - 36, Define the tooltip config as a global before the Power.js CDN
script loads, because the current MailReturnView setup creates a local const
after enqueueing power.js and it won’t be picked up. Update the logic in
MailReturnView so the whTooltips assignment is emitted as window.whTooltips (or
an inline script inserted before wp_enqueue_script for power.js) and keep the
existing options intact. Use the unique symbols wp_enqueue_script('power-js')
and the whTooltips block to place the fix in the right spot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php`:
- Around line 25-32: Remove the remote power.js enqueue from MailReturnView and
avoid exposing mailReturnData.nonce to any third-party script on this privileged
page. Update the MailReturnView render path to use a local vendored/pinned asset
or move the needed behavior server-side, and keep the wp_localize_script data
scoped only to trusted code in acore-mail-return-js.

In `@src/acore-wp-plugin/src/Components/Tools/ToolsApi.php`:
- Around line 15-20: The restore path in ToolsApi is only checking
currentAccountOwnsCharacterName($cname), which still allows a guessed recovery
item id to be used with an owned character name. Update the authorization logic
around the $item validation so the restore item is verified against the caller’s
account by joining the recovery item back to characters.account before any SOAP
restore command is executed. Keep the existing item-id validation, but add the
ownership check for the specific restore item itself in the same flow that uses
$item and $cname.

In `@src/acore-wp-plugin/src/Hooks/User/User.php`:
- Line 822: The date formatter in User::getUserData (the $fmtDate assignment)
uses an arrow function, which is incompatible with the declared PHP 7.1 runtime.
Replace the fn($ts) => ... expression with a standard anonymous function that
returns the same formatted date string, keeping the existing behavior intact.

---

Major comments:
In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php`:
- Around line 272-280: The checkSoap status request is missing the REST nonce,
so the `server-info` call can be treated as unauthenticated for logged-in
admins. Update the `checkSoap` function in `RealmSettings.php` so the
`$.get(restBase + 'server-info', ...)` request includes `X-WP-Nonce` the same
way the other REST calls do, using the existing nonce value already available in
this admin page. Keep the existing success/fail/always behavior in
`setSoapStatus` and the `manual` button state handling unchanged.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php`:
- Around line 582-590: The reset handler for acore-name-unlock-reset is clearing
the threshold rows in the UI, but that does not remove the stored
acore_name_unlock_thresholds value because SettingsController::loadTools() only
saves fields present in $_POST. Update the Tools.php click callback so the form
submits an explicit empty value for the thresholds setting before calling
submit, ensuring the reset path overwrites the old stored thresholds instead of
leaving them behind.
- Around line 264-270: The threshold action controls in the AdminPanel Tools
view are implemented as div-based pseudo-buttons, so they are not
keyboard-accessible or announced properly to assistive tech. Update the
Add/Reset controls in Tools.php (the threshold actions rendered near the
acore-name-unlock-thresholds-add and acore-name-unlock-reset IDs, including the
matching threshold block later in the file) to use real button elements while
preserving the existing styling and click behavior.
- Around line 177-190: Persist acore_geoip_lookup as disabled whenever Security
Logging is turned off, because the GeoIP select is disabled and omitted from
$_POST so SettingsController::loadTools can keep the old value. Update the Tools
page behavior around the acore_geoip_lookup field (including the change-handler
logic that disables it) so a disabled state still submits an explicit 0, or
otherwise ensure the settings payload always includes acore_geoip_lookup when
acore_security_logging is not enabled. Keep the UI state and the stored option
in sync so the disabled setting cannot silently re-enable later.
- Around line 193-199: The “Allow Old Passwords” setting is currently ignored by
the password-change flow, so wire the toggle through the password validation
path. Update acore_process_security_password() to read
Opts::I()->acore_allow_old_passwords and skip the reused-password rejection when
the option is enabled, while preserving the existing check when it is disabled.
Use the existing acore_allow_old_passwords option and the password validation
logic in acore_process_security_password() so the admin toggle actually changes
behavior.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php`:
- Around line 56-61: The account-ban lookup in CharactersController::...
currently excludes permanent bans because the query only accepts unbandate = 0
or future timestamps. Update the account_banned query used for $accBanRow so it
also treats unbandate = bandate as a permanent ban, matching the character-ban
logic. Keep the existing active and latest-row behavior intact while broadening
the WHERE clause to include this permanent-ban case.
- Around line 31-46: The character lookup in CharactersController is building
SQL with a raw $accId from ACoreServices::I()->getAcoreAccountId(), which can be
missing and lead to invalid queries. In the controller flow that prepares the
characters list, guard for a missing account ID before executing the query, and
if present, bind the account value as a parameter instead of interpolating it
into the SQL string. Use the existing query/connection logic around
getAcoreAccountId(), getCharacterEm()->getConnection(), and executeQuery() to
keep the fix localized.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php`:
- Around line 35-39: The CharactersMenu badge query is missing the permanent-ban
condition used elsewhere, so accounts banned with matching unbandate and bandate
won’t be marked banned. Update the query in CharactersMenu::executeQuery usage
to mirror the existing permanent-ban predicate by adding the unbandate = bandate
check alongside the current active and unbandate timestamp logic, keeping the
badge state consistent for all permanent bans.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php`:
- Around line 40-42: The account-ban rendering logic in CharactersView treats
only unbandate = 0 as permanent, so equal bandate/unbandate rows still show as a
temporary ban with 0 seconds remaining. Update the ban-state checks around
$isAccountBanned, $accBanPerma, and $accBanRemaining so that rows where
unbandate equals bandate are also classified as permanent, and ensure the UI
path that displays “Banned” is used for that case instead of the remaining-time
display.

In `@src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php`:
- Around line 200-208: ServerInfoApi::serverInfo() may return an Exception or
other non-string value, so the SOAP error detection loop must guard before
calling stripos(). In the server info handling block, normalize or branch on the
result type first, then only run the errorPatterns matching for string
responses; for non-string failures, log them and return the intended 503 path
directly. Use the existing ServerInfoApi::serverInfo, $result, and
$errorPatterns block to locate the fix.
- Around line 447-456: The fresh-token branch in ServerInfoApi’s in-game 2FA
removal flow is missing the same 5-attempt limiter used by verify-website-2fa,
so repeated wrong TOTP submissions are not rate-limited. Update the direct
verification path around acore_wp2fa_code_is_valid to reuse the existing
attempt-tracking transient logic (increment on failure, block after 5, reset on
success) with the same user-scoped key pattern used elsewhere in ServerInfoApi.
Keep the authorization flow unchanged, but ensure both the unlock-based and
fresh-token paths enforce the same retry protection before returning
wrong_token.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php`:
- Around line 215-235: The restore success handling in the ItemRestorationPage
flow is currently inferring success from a raw response string containing
“mail”, which is brittle. Update the response handling in the restore promise
chain to use a structured REST payload with explicit success/message fields
instead of string matching, and branch in the existing success callback based on
that shape. Keep the card removal, renumbering, and empty-state logic in the
same success path, and use the existing error path for non-success responses.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php`:
- Around line 220-243: The WP2FA management panel is still being rendered into
the DOM in the gated branch, so the unlock is only cosmetic and can be bypassed
by inspecting hidden HTML. Update the SecurityPage rendering logic so $wp2faHtml
is emitted only when $twofaUnlocked is true, and keep the gated branch from
outputting the protected panel until a server-side unlock has occurred. After a
successful TOTP verification in the related unlock flow, reload the page so the
transient-controlled $twofaUnlocked state can render the panel, and apply the
same server-side gating pattern to the email-gate branch.

In `@src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php`:
- Around line 15-25: The login IP storage in ConnectionLogger currently keeps
sensitive address history indefinitely, so add a configurable retention policy
and scheduled pruning before broad use. Update the table/record flow around the
schema creation and the logging logic in ConnectionLogger to respect a retention
setting, and add a cleanup routine that deletes entries older than the
configured window. Use the existing ConnectionLogger methods and schema
definition as the integration points, and make sure the pruning is wired into
the plugin’s scheduled job path rather than leaving data to accumulate forever.
- Around line 228-230: The GeoIP lookup in ConnectionLogger still sends login
IPs to ip-api over plain HTTP, so update the lookup path in the relevant lookup
helper(s) used by ConnectionLogger to avoid transmitting data insecurely.
Replace the current ip-api-based requests with an HTTPS-capable provider or gate
GeoIP behind an explicit admin opt-in with clear disclosure, and make sure both
the single-IP and batch lookup flows are handled consistently.

In `@src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php`:
- Around line 66-78: The SOAP password sync in
PasswordHandler::handlePasswordChange only catches \Exception, so PHP Errors can
still fatal before the “website password unchanged” fallback runs. Update the
try/catch around ACoreServices::I()->getAccountSoap() and setAccountPassword()
to catch \Throwable instead, and keep assigning the message into $soapError so
the existing error path is used for both Exceptions and Errors.

In `@src/acore-wp-plugin/src/Hooks/User/User.php`:
- Around line 784-819: Move the account/service lookup into the protected
badge-check path so a connection/config failure does not escape before error
handling. In the User class method that builds the admin notice badge, keep the
ACoreServices::I() calls and getAcoreAccountId/getConnection lookups inside the
try or after the guard, and return early only after the database-backed checks
have safely run. This ensures profile.php notices fail closed instead of
breaking the page when DB services are unavailable.

In `@src/acore-wp-plugin/src/Manager/ACoreServices.php`:
- Around line 408-418: ACoreServices::restore character ownership validation
currently throws InvalidArgumentException from the restore-list path, which can
bubble through ToolsApi::ItemRestoreList into a 500. Update the flow so this
failure is handled at the REST boundary: either have ACoreServices return a
clear sentinel/error state instead of throwing, or catch the exception in
ToolsApi::ItemRestoreList and convert it into a WP_Error with an appropriate 4xx
status. Keep the behavior for valid restores unchanged and ensure the new
handling covers both invalid cguid input and missing character ownership.

In `@src/acore-wp-plugin/web/assets/css/main.css`:
- Around line 690-695: The fixed four-column definition in item-restore-grid is
causing overflow in the narrow content column. Update the grid in main.css to
adapt based on available width instead of always using four equal columns, and
keep the change scoped to the .item-restore-grid rule so the cards wrap
naturally before the sidebar-constrained layout breaks.

In `@src/acore-wp-plugin/web/assets/mail-return/mail-return.js`:
- Around line 2-18: The mail list loading flow in mail-return.js can render
stale results when multiple character cards are clicked quickly, so update the
character-switch request handling to ignore or cancel outdated responses. Use
the click handler on jQuery("`#acore-characters-mail`") / ".acore-char-card" and
the mail-loading path that populates "`#mail-return-items`" to track the active
charGuid, abort any previous XHR, or discard any callback whose charGuid no
longer matches the current active card before rendering mails and Return
buttons. Ensure the response-handling logic only updates the UI for the most
recently selected character.

---

Minor comments:
In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php`:
- Around line 314-328: The auto-refresh path in RealmSettings should update the
visible module list and refreshed timestamp the same way the manual refresh path
does. After the modules are persisted in the refresh handler, make sure the UI
repaint logic that updates `#acore-modules-list` and `#acore-modules-refreshed` runs
even when opts.auto is true, using the existing newModules and res.refreshed
values; keep renderAutoCheck(diffReferenced(...)) and knownModules assignment in
sync afterward.

In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php`:
- Around line 28-29: The timestamp helpers in CharactersView currently use
date(), which can ignore the site timezone for user-facing ban/mute end times.
Update the formatDate and formatTime methods to use wp_date() instead,
preserving the existing formats while relying on WordPress timezone handling.

In `@src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php`:
- Around line 82-90: The mail return panel is rendering visible immediately even
though it should stay hidden until mail data is loaded. Update the markup in
MailReturnView so the `#mail-return-content` container starts hidden by default
and is only shown by the existing JS once the mails are ready; keep the change
scoped to the mail panel structure around the mail list and heading elements.
- Around line 25-36: Define the tooltip config as a global before the Power.js
CDN script loads, because the current MailReturnView setup creates a local const
after enqueueing power.js and it won’t be picked up. Update the logic in
MailReturnView so the whTooltips assignment is emitted as window.whTooltips (or
an inline script inserted before wp_enqueue_script for power.js) and keep the
existing options intact. Use the unique symbols wp_enqueue_script('power-js')
and the whTooltips block to place the fix in the right spot.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php`:
- Around line 11-20: The new localization calls in RafProgressPage are using the
wrong text-domain property, which breaks translation lookups. Update the `_e()`
calls in this view to use `Opts::I()->acore_page_alias` instead of
`Opts::I()->page_alias`, keeping the existing copy and structure intact so the
strings are registered under the declared domain.

In `@src/acore-wp-plugin/src/Components/UserPanel/UserController.php`:
- Around line 217-221: The getModel() contract is stale because
UserController::getModel() returns the controller instance, not a UserModel.
Update the docblock on getModel() to match the actual return type
(self/UserController) or change the method to return the intended model object
if that was the original design; make sure the signature and documentation are
consistent.

In `@src/acore-wp-plugin/src/Hooks/User/User.php`:
- Line 877: The heading text in the User rendering block is incorrectly
hardcoded as IPv4 even though acore_resolve_client_ip() may return IPv6. Update
the label in the User.php view inside the Recent Connections header to say “Your
IP” instead of “Your IPv4,” keeping the existing $myIp output and related
escaping unchanged.

In `@src/acore-wp-plugin/src/Utils/AcoreCharColors.php`:
- Around line 84-93: The rowStyle method in AcoreCharColors is treating every
non-alliance race as Horde, so unknown race IDs get the wrong styling; update
the faction-color fallback in rowStyle to use a neutral value when RACE_FACTION
returns unknown, and keep alliance/Horde colors only for explicit matches. Also
remove or stop computing the unused $fDark variable in rowStyle so the logic
stays clean and consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread src/acore-wp-plugin/src/Components/Tools/ToolsApi.php Outdated
Comment thread src/acore-wp-plugin/src/Hooks/User/User.php
(Resume generated by Claude)

**PR azerothcore#199 — Characters Menu**
- Fixed typo: "succesfully" → "successfully" in save confirmation
- Fixed broken sentence in the Order Character Screen description
- Extended `user-select: none` to cover all `.acore-char-row` elements, not just `button`

**PR azerothcore#200 — Mail Return**
- *(issues covered by later PRs — no unique fixes)*

**PR azerothcore#201 — Item Restoration**
- Added `alt=` attributes to race/class icons in `CharactersView.php`
- `ToolsApi`: `ItemRestoreList` now catches exceptions and returns a 403 instead of a 500
- `ToolsApi` + `ACoreServices`: **Security fix** — `ItemRestore` now verifies the recovery item belongs to the character, not just that the user owns the character name (IDOR vulnerability)

**PR azerothcore#202 — Unstuck / Scroll / RAF**
- `ItemRestorationPage`: removed brittle `data.includes('mail')` success check — any HTTP success now triggers the success path

**PR azerothcore#203 — Security Tab**
- `CharactersController`: `resetCharacterOrder()` now guards against invalid/null account ID before running UPDATE
- `Tools.php`: added `esc_attr()` on banned-names table input (XSS fix)
- `Tools.php`: added `is_array()` guard on thresholds `foreach`
- `Tools.php` + `SettingsController`: added sentinel input so deleting all thresholds actually clears them in the DB
- `Tools.php`: GeoIP hidden fallback `value="0"` is now always rendered, not conditionally

**PR azerothcore#204 — Admin Settings / CSRF**
- `AcoreCharColors`: removed unused `$fDark`, unknown race IDs now get a neutral color instead of Horde red
- `MailReturnView`: added `esc_url()` on race/class icon `src` attributes
- `DarkMode`: added `dmToggleInFlight` flag to prevent overlapping AJAX requests on rapid dark mode clicks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php`:
- Around line 48-50: The ordering in CharactersController::getCharacters
currently uses COALESCE(c.`order`, c.`guid`), which can interleave null-order
characters with saved-order rows; update the ORDER BY logic so rows with a real
`order` value are sorted together first, and only null-order rows fall back to
GUID-based ordering afterward. Use the query in CharactersController as the fix
point and adjust the ORDER BY expression to separate the two groups without
mixing the sort domains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9589379-f0ec-49ab-8c73-6cc68e41a416

📥 Commits

Reviewing files that changed from the base of the PR and between e07f7de and c1cc262.

📒 Files selected for processing (13)
  • src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php
  • src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php
  • src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/src/Hooks/Various/DarkMode.php
  • src/acore-wp-plugin/src/Manager/ACoreServices.php
  • src/acore-wp-plugin/src/Utils/AcoreCharColors.php
  • src/acore-wp-plugin/web/assets/css/main.css
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/acore-wp-plugin/src/Utils/AcoreCharColors.php
  • src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php
  • src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersMenu.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php
  • src/acore-wp-plugin/src/Hooks/Various/DarkMode.php
  • src/acore-wp-plugin/src/Hooks/User/User.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant