Skip to content

Multiple Changes to Acore-CMS user and admin experience.#197

Closed
TheSCREWEDSoftware wants to merge 48 commits into
azerothcore:masterfrom
TheSCREWEDSoftware:user_info_master
Closed

Multiple Changes to Acore-CMS user and admin experience.#197
TheSCREWEDSoftware wants to merge 48 commits into
azerothcore:masterfrom
TheSCREWEDSoftware:user_info_master

Conversation

@TheSCREWEDSoftware

@TheSCREWEDSoftware TheSCREWEDSoftware commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

This quite of a big PR and many aspects of were pointed, cleaned up or suggested (code wise) by AI (Claude anthropic) not exclusively AI but helped by AI.

The only thing of this PR that I have not tested it's the IP logging for IPv4, mock data worked and Locally (using local IPs) worked, but did not test public IPs, code and logically wise it should work.


Dark Mode

The users now have the option to choose between dark mode and keep it light (still the default) and it will remember on the user's settings (part of the wordpress DB), this setting supports all user and adminstrator (AC) related pages.

White
image

Dark
image

Security Tab

  • Change Password
    and
  • 2FA (from wp-2fa)

Have been both moved from Profile to the new Security Tab, what does the Security Tab bring and do now?

  • Password

Will now tell you (from now and onwards) the last time you password was changed

image image

Also depending on your adminstrator settings, you may or not be able to re-use your old password.

Under the AzerothCore > Tools > Web Integration

image

Security logging, if enabled will log the IP connections on the website and the last in-game one if it exists, if disabled doesn't do anything.

And for "Allowing Old Password" just allows you re-use an old passwords (currently it only saves up to 10, this is hashed not plained text, so same behaviour and same check the website and core do via the database to see if it's valid or not password before storing).

Back to the security tab, now part of it you also ahe the dedicated 2FA Fields for Website and In-game!

  • Website
image

Supports both Authecator app or E-mail.

After having a 2FA actively on the account, it will be required for the login and any Password changes:

image image

In the removal of the 2FA for either the in-game or website you will be informated! (in security tab and top of profile page)

image image

This message will display for either user removal or staff's removal, so no "hidden" removal of security features of an user, but still gives the admisntrator a way to remove them in the case the user can prove their are the owner but they can't access it.

  • In-game
image

In-game setup is done fully in-game, but to be able to manage it on the website (to remove the 2FA in-game) it requires you to have 2FA setup on the website first as security measure.

image

(cannot change it until i get myself 2FA in the websit)

In the admin side of things you check and remove either of the 2FA

image

And the last point of the secuirty tab is full list of login IPs (from most recent to oldest) in Security tab shows up to 50 (can expand infinite every 50 entries), yellow selection = matches your current IP (in-game or website).

(top/default)
Screenshot_15

(expanded)
Screenshot_17

(profile bottom page, shows up to 10 latest, you can click see more to take to security tab)
Screenshot_14

--

Character selectors got a rework!

Character Selector Row Meaning? Idk how to phrase.
-- Icons and badges support tooltips
-- Left Tab = Class Colour
-- Border (3 sides and selection) = Faction Colour
-- Levels have a badge and colour (brown/vanilla = 0-60, green/tbc = 61-70 and blue/wrath = 71-80+)
-- Race (uses Faction colour Border) and slightly bigger
-- Class (uses class colour border) and slightly bigger
-- Heartstone just has a border (looks prettier)

  • Order Character Screen
    Also now has a reset order (sets to NULL on the DB) for the times order soft-locks a characters from being logged into)

Dark
image

White
image

Re-ordered
image

  • Unstuck
image
  • Mail Return
    Complety overhaul! More intuative to users i owuld say.
  • before return gives you a prompt for validation
  • Uses grids to match the max amount of mail contents you can send (12 items, so it'a 6 columns and 2 rows)
  • Shows the faction and class you sent to.

If no mails found
image

If mails found (dark theme)
image

Re uses wowhead tooltips and higher quality icons

image

(Mails found but white theme)
image

  • Scroll of Rsurrection
    Just got visuals updated to easier to tell the user if it's enrolled or not by having green, yellow or red colours (green active, red not active, yellow informative)
image image
  • Item Restoration (not a used feature by CC, but still worked on it)

If no items found
image

If items found (white theme)

image

(dark theme)
image

To finish the profile tab!

Warning if 2FA was removed on the top of the page

image

IP logs on the bottom (latest 10 connections)

image

and a better UI for users to select their Expansion and a note.

image image

Admin Stuff

  • Added a required plugin: 2FA (from wp-2fa) (cant still be disabled)

  • Realm Settings
    Had a few changes to it, still has and uses the manual "Check for SOAP" button, but upon saving, if connection exists, it will staight up tell you.

  • SOAP Settings Badge

image

The server needs to be fully running to accept SOAP check otherwise is considered not reacahble/offline/wrong info

(world server running and soap connection working)
image

(world server not running or not working)
image

  • Modules (list)
    You're now able to get current server module lists (for their module string be more pricesely)
image image

Which is used to validate or not dependcies need (sadly the "Module Requirements" has to be define in php, probably a better altnerative could be in the future) to define a page "depended" of a module.

image image image

And in this case the module is missing (in my case not active)

So it also doesnt appear on th euser side

image

Scroll of Resurrection section associated with the module mod-resurrection-scroll

  • Tools
  • Made sure that Scroll of Ress contents always shows (otherwise moves the page format) and make sure if it not acitve it's greyed out.
  • Make a new category Web Integration for page related stuff.
  • Name unlock has their Delete Buttons inside of the box and a reset button to turn the values to default
image image
  • Web Integration
    Security Logging, track or not track IPs for the users Login History
    Allow Old password, allow or not the old passwords
    Manual check 2FA and if the user has Generated Backup codes, and remove the 2FA if needed.
image

You also can search for an account's IP history

(top, default)
Screenshot_19

(bottom)
Screenshot_20

(bottom +50)
Screenshot_21

Summary by CodeRabbit

  • New Features
    • Added a dedicated Security page (password change, website/email + in-game 2FA unlock/removal, backup-code handling, recent connections).
    • Introduced admin dark mode with an in-bar toggle.
    • Added login connection tracking with “current IP” highlighting.
  • Improvements
    • Refreshed Realm Settings with AJAX saving, module refresh, and module requirements diff/editing.
    • Updated Tools with module-aware “Scroll of Resurrection” controls and improved 2FA/web workflows.
    • Redesigns and enhancements to Mail Return and Item Restoration, plus improved character order editing with a reset option.

TheSCREWEDSoftware and others added 30 commits June 6, 2026 14:44
Moved from Profile to Secruity
- Password Change
- 2FA

Profile page also show the latest connections at the bottom
The most recent 20 connections

Security tab will show up to 20 and you can have view to sell all history
In the scenarios where the user tried to change their current password to older one (for the first time) it would log them out, never was an issue before as we didn't prevent it, the previous implementation has been reworked to make the entire check happens before the page load

@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: 5

Note

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

🟠 Major comments (23)
src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php-18-19 (1)

18-19: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add CSRF protection to the settings save flow.

Line 18 defines a state-changing admin form without a nonce, and Line 322 posts it directly. A forged request can change SOAP/DB credentials.

Suggested fix
-            <form name="form-acore-settings" method="post" action="">
+            <form name="form-acore-settings" method="post" action="">
+                <?php wp_nonce_field('acore_realm_settings_save', 'acore_realm_settings_nonce'); ?>
 $('form[name="form-acore-settings"]').on('submit', function(e){

(Then verify server-side with check_admin_referer('acore_realm_settings_save', 'acore_realm_settings_nonce') before persisting.)

Also applies to: 318-323

🤖 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 18 - 19, The form with name "form-acore-settings" lacks CSRF protection,
allowing forged requests to modify sensitive SOAP/DB credentials. Add a
WordPress nonce field inside the form using wp_nonce_field with the action
'acore_realm_settings_save' and field name 'acore_realm_settings_nonce'. Then
locate the form processing code that handles the form submission (around lines
318-323) and add server-side verification at the beginning using
check_admin_referer('acore_realm_settings_save', 'acore_realm_settings_nonce')
before any credential data is persisted to ensure the request is legitimate.
src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php-27-133 (1)

27-133: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape option values before injecting into input attributes.

Lines like Line 27/43/55/77/103/129 print raw option values into value="". Quotes/special chars can break the form and create stored XSS exposure in admin.

Suggested fix pattern
-<td><input type="text" name="acore_realm_alias" value="<?= Opts::I()->acore_realm_alias; ?>" size="20" placeholder="AzerothCore"></td>
+<td><input type="text" name="acore_realm_alias" value="<?= esc_attr(Opts::I()->acore_realm_alias); ?>" size="20" placeholder="AzerothCore"></td>

-<td><input type="password" name="acore_soap_pass" value="<?= Opts::I()->acore_soap_pass; ?>" size="20"></td>
+<td><input type="password" name="acore_soap_pass" value="<?= esc_attr(Opts::I()->acore_soap_pass); ?>" size="20"></td>

Apply the same esc_attr(...) pattern to all input value attributes in this form.

🤖 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 27 - 133, All input value attributes in the RealmSettings.php form are
directly injecting raw option values from Opts::I() calls without escaping,
creating a stored XSS vulnerability. Wrap each value attribute with the
esc_attr() function to properly escape the output. This applies to all input
fields including acore_realm_alias, acore_soap_host, acore_soap_port,
acore_soap_user, acore_soap_pass, acore_db_auth_host, acore_db_auth_port,
acore_db_auth_user, acore_db_auth_pass, acore_db_auth_name, acore_db_char_host,
acore_db_char_port, acore_db_char_user, acore_db_char_pass, acore_db_char_name,
acore_db_world_host, acore_db_world_port, acore_db_world_user,
acore_db_world_pass, and acore_db_world_name. Change each instance from
value="<?= Opts::I()->optionName; ?>" to value="<?=
esc_attr(Opts::I()->optionName); ?>".
src/acore-wp-plugin/web/assets/css/main.css-114-116 (1)

114-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t use display: none for interactive radios in the expansion selector.

Line 115 removes the radio controls from keyboard and assistive-tech navigation. This breaks non-mouse interaction for selecting expansions.

Suggested fix
 .acore-expansion-option input[type="radio"] {
-    display: none;
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    margin: -1px;
+    padding: 0;
+    border: 0;
+    overflow: hidden;
+    clip: rect(0 0 0 0);
+    clip-path: inset(50%);
+    white-space: nowrap;
 }
🤖 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 114 - 116, The
expansion selector radio controls are currently hidden using display: none,
which removes them from keyboard navigation and assistive technology trees,
breaking accessibility for non-mouse users. Replace the display: none property
on the radio input elements in the expansion selector with an accessible hiding
technique such as using a sr-only class pattern, position absolute with negative
left offset, or clip method that visually hides the element while keeping it in
the accessibility tree. This ensures keyboard users and screen reader users can
still interact with and navigate the expansion selection controls.
src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php-26-37 (1)

26-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make character cards keyboard-activatable.

role="button" alone does not make the row focusable or handle Enter/Space, so keyboard users cannot select a character. Use a real button so the existing click listener still works.

♿ Proposed fix
-                                    <div class="acore-char-row acore-char-card"
+                                    <button type="button"
+                                         class="acore-char-row acore-char-card"
                                          data-char-guid="<?= intval($char['guid']) ?>"
                                          data-char-name="<?= esc_attr($char['name']) ?>"
-                                         style="<?= esc_attr($clsStyle) ?>"
-                                         role="button">
+                                         style="<?= esc_attr($clsStyle) ?>">
                                         <span class="acore-char-name"><?= esc_html($char['name']) ?></span>
                                         <span class="acore-char-meta">
                                             <span class="acore-level" data-exp="<?= AcoreCharColors::expansionSlug(intval($char['level'])) ?>" title="<?= esc_attr(AcoreCharColors::expansionLabel(intval($char['level']))) ?>">Level <?= intval($char['level']) ?></span>
                                             <img class="race-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getRaceName(intval($char['race']))) ?>" src="<?= ACORE_URL_PLG . 'web/assets/race/' . intval($char['race']) . (intval($char['gender']) == 0 ? 'm' : 'f') . '.webp' ?>">
                                             <img class="class-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getClassName(intval($char['class']))) ?>" src="<?= ACORE_URL_PLG . 'web/assets/class/' . intval($char['class']) . '.webp' ?>">
                                         </span>
-                                    </div>
+                                    </button>

Also applies to: 94-101

🤖 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 26 - 37, The character card container uses a div element with
role="button" which is not keyboard-accessible. Replace the div element that
contains the character card (with class "acore-char-row acore-char-card" and
data attributes for char-guid and char-name) with a proper button element
instead. Keep all existing data attributes, styling, and child content intact,
and apply the same change to the other occurrence mentioned at lines 94-101.
This will automatically provide keyboard focus and Enter/Space key handling
while maintaining compatibility with the existing click listeners.
src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php-53-61 (1)

53-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a real button for selectable character cards.

The mail list can only be loaded by clicking this div; keyboard users cannot focus or activate it. A button preserves the existing delegated click handler while adding keyboard activation.

♿ Proposed fix
-                                        <div class="acore-char-row acore-char-card" data-char-guid="<?= intval($char['guid']) ?>" style="<?= esc_attr($clsStyle) ?>">
+                                        <button type="button" class="acore-char-row acore-char-card" data-char-guid="<?= intval($char['guid']) ?>" style="<?= esc_attr($clsStyle) ?>">
                                             <span class="acore-char-name"><?= esc_html($char['name']) ?></span>
                                             <span class="acore-char-meta">
                                                 <span class="acore-level" data-exp="<?= AcoreCharColors::expansionSlug(intval($char['level'])) ?>" title="<?= esc_attr(AcoreCharColors::expansionLabel(intval($char['level']))) ?>">Level <?= intval($char['level']) ?></span>
                                                 <img class="race-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getRaceName(intval($char['race']))) ?>" src="<?= ACORE_URL_PLG . 'web/assets/race/' . intval($char['race']) . (intval($char['gender']) == 0 ? 'm' : 'f') . '.webp' ?>">
                                                 <img class="class-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getClassName(intval($char['class']))) ?>" src="<?= ACORE_URL_PLG . 'web/assets/class/' . intval($char['class']) . '.webp' ?>">
                                             </span>
-                                        </div>
+                                        </button>
🤖 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 53 - 61, The character card element with class acore-char-card is
currently a div, which cannot be focused or activated by keyboard users. Replace
this div with a button element to preserve the existing delegated click handler
while adding keyboard accessibility. The button should maintain all the same
attributes (data-char-guid, style, and inner content with spans like
acore-char-name and acore-char-meta) as the current div implementation.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php-16-27 (1)

16-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protect character-order POST actions with a nonce.

Both save and reset are accepted solely from POST fields, so a third-party page can submit a logged-in user’s browser and reset/reorder characters. Verify a nonce before branching, and add the matching field in CharactersView.

🛡️ Proposed fix
     public function loadHome() {
-        if ($_SERVER["REQUEST_METHOD"] == "POST") {
+        if ($_SERVER["REQUEST_METHOD"] == "POST") {
+            check_admin_referer('acore_character_order', 'acore_character_order_nonce');
+
             if (isset($_POST["acore_reset_order"])) {
                 $this->resetCharacterOrder();

And add the matching field inside the form in CharactersView:

 <form action="" method="POST" novalidate="novalidate">
+    <?php wp_nonce_field('acore_character_order', 'acore_character_order_nonce'); ?>
🤖 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 16 - 27, The POST request handling logic in the
CharactersController.php file (specifically the REQUEST_METHOD check and the
subsequent branches for acore_reset_order with resetCharacterOrder() and
saveCharacterOrder() calls) lacks nonce verification, making it vulnerable to
CSRF attacks. Add nonce verification immediately after checking that the
REQUEST_METHOD is POST and before branching to either resetCharacterOrder() or
saveCharacterOrder(). Verify the nonce value from the POST request and use
wp_verify_nonce() with the appropriate action name. Additionally, add a
corresponding nonce field to the form in CharactersView that matches the nonce
action and name being verified in the controller.
src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php-119-134 (1)

119-134: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid sending login IPs to a hard-coded plaintext GeoIP service.

Line 131 sends each public login IP to ip-api.com over HTTP from the login path. That leaks personal data to a third party and can be observed in transit; use a local GeoIP database, a configurable HTTPS provider with caching/privacy controls, or store Unknown when no approved provider is configured.

🤖 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 119 -
134, The acore_lookup_country function is sending login IPs to a hardcoded HTTP
endpoint (ip-api.com), which exposes user data to a third party over an
unencrypted connection. Replace this external service call with one of the
following: implement a local GeoIP database lookup, use a configurable
HTTPS-based provider with built-in caching and privacy controls, or return
'Unknown' as a safe default when no approved provider is configured. Remove the
hardcoded wp_remote_get call to ip-api.com and implement proper privacy
controls.
src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php-107-116 (1)

107-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not trust client-supplied forwarding headers for security logs.

HTTP_CLIENT_IP and the first X-Forwarded-For value can be browser-supplied unless a trusted proxy strips them, so users can poison their login audit trail and current-IP highlighting. Default to REMOTE_ADDR, validate with FILTER_VALIDATE_IP, and only honor forwarded headers behind a configured trusted proxy.

🛡️ Proposed hardening
 function acore_resolve_client_ip() {
-    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
-        $ip = $_SERVER['HTTP_CLIENT_IP'];
-    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
-        $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
-        $ip = trim($parts[0]);
-    } else {
-        $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
-    }
-    return sanitize_text_field($ip);
+    $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
+
+    // If this site is deployed behind a trusted proxy, add an explicit
+    // trusted-proxy check before accepting X-Forwarded-For / Client-IP.
+    $ip = trim((string) $ip);
+
+    return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '127.0.0.1';
 }
🤖 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 107 -
116, The acore_resolve_client_ip() function prioritizes client-supplied headers
HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR which can be spoofed by users to poison
security logs. Reorder the logic to default to REMOTE_ADDR first (which is
server-supplied), only use forwarded headers if behind a configured trusted
proxy, and validate the resulting IP using FILTER_VALIDATE_IP before returning
to ensure the function returns only legitimate IP addresses.
src/acore-wp-plugin/src/Hooks/User/User.php-491-494 (1)

491-494: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not reset expansion when the field is absent or malformed.

A profile save that does not include acore-user-game-expansion falls back to WOTLK and can overwrite the existing game account. Also, intval('bad') becomes 0, which passes as Classic.

🐛 Proposed fix
-    $expansion = isset($_POST['acore-user-game-expansion']) ? intval($_POST['acore-user-game-expansion']) : null;
-
-    if ($expansion === null || !in_array($expansion, Common::EXPANSIONS)) {
+    if (!isset($_POST['acore-user-game-expansion'])) {
+        return;
+    }
+
+    $rawExpansion = wp_unslash($_POST['acore-user-game-expansion']);
+    $expansion = is_numeric($rawExpansion) ? (int) $rawExpansion : null;
+
+    if ($expansion === null || !in_array($expansion, Common::EXPANSIONS, true)) {
         $expansion = Common::EXPANSION_WOTLK;
     }
🤖 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 491 - 494, The
current logic resets the expansion to WOTLK when the field is absent from POST
data or when intval converts invalid input to 0 (which may be a valid
expansion). Instead of defaulting to WOTLK when the field is missing, only
update the expansion variable if the acore-user-game-expansion field is actually
present in POST data and passes validation. Check if the field exists in the
POST array first before attempting to validate its value, and skip setting the
expansion entirely if the field is not provided, allowing the existing expansion
value to be preserved rather than overwritten with a default.
src/acore-wp-plugin/src/Hooks/User/User.php-632-647 (1)

632-647: ⚠️ Potential issue | 🟠 Major

Refactor to use shared 2FA state helpers—deduplicate website 2FA check and align in-game logic.

The website 2FA check at lines 632–637 and 738–743 is identical to acore_website_2fa_enabled() (ServerInfoApi.php:124–130) and should call that helper instead. The in-game check duplicates the same query pattern; extract it into a username-based variant of acore_ingame_2fa_enabled() or refactor both locations to share the implementation. This avoids divergence between warning logic and the Security/REST API behavior.

🤖 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 632 - 647, The
website 2FA check logic at lines 632–637 (checking totpKey and enabledMethods)
is duplicated identically elsewhere in the file and matches the existing
acore_website_2fa_enabled() helper function in ServerInfoApi.php. Replace the
duplicate website 2FA check logic with a call to the existing
acore_website_2fa_enabled() helper, passing the user object or ID as needed.
Similarly, extract the in-game 2FA check logic (the query on the account table
checking totp_secret) into a username-based variant of
acore_ingame_2fa_enabled() or create a shared helper function to avoid
duplication. Update all locations in User.php where these checks appear to use
the centralized helpers instead of repeating the implementation.
src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php-328-407 (1)

328-407: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bind destructive 2FA actions to the checked account.

The Remove buttons are enabled after checking one username, but the click handlers read the current input later. Editing the field after a successful check can remove website/in-game 2FA or backup codes for a different account than the one that was verified.

🛡️ Proposed direction
     function wire2fa(type, $userInput, $check, $remove, $msg, onCheck) {
+        var checkedUsername = '';
+
+        $userInput.on('input', function(){
+            checkedUsername = '';
+            $remove.prop('disabled', true);
+        });
+
         $check.on('click', function(){
             var username = $userInput.val().trim();
             if (!username) { $msg.css('color','`#d63638`').text('Enter an account name first.'); return; }
@@
                     } else {
+                        checkedUsername = data.username || username;
                         $msg.css('color','`#238636`').text('2FA is active for ' + data.username + '.');
                         $remove.prop('disabled', false);
                     }
@@
         $remove.on('click', function(){
-            var username = $userInput.val().trim();
-            if (!confirm('Remove ' + type + ' 2FA for ' + username + '? This cannot be undone.')) return;
-            $remove.prop('disabled', true).text('Removing…');
-            $check.prop('disabled', true);
-            ajaxPost('admin/2fa-remove', { type: type, username: username })
+            var username = checkedUsername;
+            if (!username) return;
+            acoreConfirm('Remove ' + type + ' 2FA for ' + username + '? This cannot be undone.', function(){
+                $remove.prop('disabled', true).text('Removing…');
+                $check.prop('disabled', true);
+                ajaxPost('admin/2fa-remove', { type: type, username: username })
                 .done(function(data){
                     $msg.css('color','`#238636`')
                         .text('Removed on ' + data.date + ' by ' + data.staff + '. User will see a warning until they re-enable 2FA.');
@@
                 .always(function(){
                     $check.prop('disabled', false).text('Check');
                     if ($remove.text() === 'Removing…') $remove.text('Remove');
                 });
+            });
         });
     }
@@
-        var username = $('`#acore-2fa-web-user`').val().trim();
+        var username = $('`#acore-backup-remove`').data('checkedUsername');
         if (!username) { return; }

Also set data('checkedUsername', data.username || username) only after the Website check response confirms the account whose backup-code count is being displayed.

🤖 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
328 - 407, The Remove buttons become enabled after verifying a username through
the check handler, but the click handlers read the current input field value at
click time instead of the verified username, allowing the field to be changed
between verification and removal to target a different account. In the wire2fa
function, after each successful check in the .done callback when 2FA is
confirmed active, store the verified username on the $remove button element
using jQuery's .data() method with key 'checkedUsername' set to data.username or
username. Then modify both $remove click handlers to retrieve the username from
this stored data instead of reading directly from $userInput.val().trim().
Additionally, ensure the website 2FA onCheck callback stores the username the
same way before displaying backup code information.
src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php-98-98 (1)

98-98: ⚠️ Potential issue | 🟠 Major

Scope the 2FA unlock transient to the current session.

The transient key only includes the user ID, so verifying 2FA in one browser unlocks the management panel for every active session on that account until expiry. Update both the setter in the REST endpoint and the getter on the Security page to include the WordPress session token in the key.

🛡️ Keying pattern to apply consistently
-    $twofaUnlocked      = $websiteTotpEnabled && (bool) get_transient('acore_2fa_panel_unlock_' . $user->ID);
+    $unlockKey          = 'acore_2fa_panel_unlock_' . $user->ID . '_' . hash('sha256', wp_get_session_token());
+    $twofaUnlocked      = $websiteTotpEnabled && (bool) get_transient($unlockKey);

Also update the setter in ServerInfoApi.php:470:

-           set_transient('acore_2fa_panel_unlock_' . $user->ID, time(), 30 * MINUTE_IN_SECONDS);
+           $unlockKey = 'acore_2fa_panel_unlock_' . $user->ID . '_' . hash('sha256', wp_get_session_token());
+           set_transient($unlockKey, time(), 30 * MINUTE_IN_SECONDS);
🤖 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` at line
98, The 2FA unlock transient key in SecurityPage.php only includes the user ID,
allowing any session of that user to access the management panel once 2FA is
verified in one session. Update the transient key construction in the
$twofaUnlocked assignment in SecurityPage.php to include the WordPress session
token (retrieved via wp_get_session_token()) along with the user ID.
Additionally, update the corresponding transient setter in ServerInfoApi.php
(around line 470) to use the same key format with the session token included,
ensuring both the getter and setter use identical key construction for
consistency.
src/acore-wp-plugin/src/Hooks/User/User.php-689-713 (1)

689-713: ⚠️ Potential issue | 🟠 Major

Scope WP2FA hook removal to the profile.php page only.

The hook removal currently executes on every admin_init action except the Security subpage, but show_user_profile and edit_user_profile hooks are only fired on profile.php and user-edit.php. This causes unnecessary global hook mutations on all other admin pages. Per the comment's stated intent to remove WP2FA blocks "from the standard WordPress profile page only," guard removal with $pagenow to scope it correctly.

Proposed fix
 add_action('admin_init', function () {
+    global $pagenow;
     $page = isset($_GET['page']) ? $_GET['page'] : '';
-    if ($page === ACORE_SLUG . '-security') return; // Security sub-page: leave hooks intact
+    if ($pagenow !== 'profile.php' || $page !== '') return; // Only plain Profile > Profile
 
     global $wp_filter;
🤖 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 689 - 713, The
anonymous function hooked to admin_init is performing unnecessary global hook
mutations on every admin page instead of only on the pages where those hooks
actually fire. Add a guard at the beginning of the admin_init callback to check
the global $pagenow variable and return early if the current page is not
profile.php or user-edit.php, so the removal of show_user_profile and
edit_user_profile hook callbacks only occurs on the pages where those hooks are
actually invoked.
src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php-430-463 (1)

430-463: ⚠️ Potential issue | 🟠 Major

Escape translated strings in JavaScript context using esc_js(__()) instead of _e().

Lines 434, 438, 452, 459, and 460 use _e() directly in single-quoted JavaScript strings. Translations containing apostrophes break the script, and malicious translations can inject arbitrary JavaScript. Use esc_js(__(...)) consistently—this pattern is already applied elsewhere in the same file.

Proposed fix
-                msg.textContent = '<?php _e('Please enter a valid 6-digit code.', 'acore-wp-plugin'); ?>';
+                msg.textContent = '<?php echo esc_js(__('Please enter a valid 6-digit code.', 'acore-wp-plugin')); ?>';
@@
-            removeBtn.textContent = '<?php _e('Removing…', 'acore-wp-plugin'); ?>';
+            removeBtn.textContent = '<?php echo esc_js(__('Removing…', 'acore-wp-plugin')); ?>';
@@
-                    msg.textContent   = '<?php _e('In-game 2FA removed successfully. You can set it up again inside the game.', 'acore-wp-plugin'); ?>';
+                    msg.textContent   = '<?php echo esc_js(__('In-game 2FA removed successfully. You can set it up again inside the game.', 'acore-wp-plugin')); ?>';
@@
-                msg.textContent   = (err && (err.message || (err.data && err.data.message))) || '<?php _e('An error occurred. Please try again.', 'acore-wp-plugin'); ?>';
+                msg.textContent   = (err && (err.message || (err.data && err.data.message))) || '<?php echo esc_js(__('An error occurred. Please try again.', 'acore-wp-plugin')); ?>';
@@
-                removeBtn.textContent = '<?php _e('Remove In-game 2FA', 'acore-wp-plugin'); ?>';
+                removeBtn.textContent = '<?php echo esc_js(__('Remove In-game 2FA', 'acore-wp-plugin')); ?>';
🤖 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 430 - 463, Replace all instances of _e() with esc_js(__()) in the
JavaScript string contexts within the fetch handlers for 2FA removal.
Specifically, update the msg.textContent and removeBtn.textContent assignments
that contain _e() calls—convert _e('text', 'acore-wp-plugin') to
esc_js(__('text', 'acore-wp-plugin'))—on all five occurrences: the validation
error message, the "Removing..." state, the success message, the error fallback
message, and the button reset text. This ensures proper escaping for JavaScript
context and prevents translation-based security issues.
src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php-97-97 (1)

97-97: ⚠️ Potential issue | 🟠 Major

Separate TOTP-enabled check from website-2FA-enabled check.

$websiteTotpEnabled checks only plugin_active && totp_enabled, but is used as a blanket gate for website 2FA status. Users with email-based 2FA alone will have $websiteTotpEnabled = false, causing the in-game 2FA block to be greyed out (line 265) with the message "Website 2FA must be set up"—even though website 2FA is actually configured via email. Instead, use a generic website-2FA-enabled flag that accounts for both email and TOTP, reserving TOTP-specific checks for verification gates that require TOTP codes (lines 216 and 306).

🤖 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` at line
97, The variable $websiteTotpEnabled incorrectly gates the website 2FA status by
checking only TOTP enablement, which causes email-based 2FA users to see the
in-game 2FA block incorrectly greyed out. Create a separate generic website 2FA
enabled flag that checks whether either email-based 2FA or TOTP is enabled (by
checking if either totp_enabled or email_2fa_enabled is true in $twoFaData), and
use this new flag at the availability gate for the in-game 2FA block. Preserve
the TOTP-specific $websiteTotpEnabled check for actual TOTP code verification
operations at lines 216 and 306 where TOTP codes are required.
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php-28-91 (1)

28-91: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Require a fresh website-2FA verification before changing passwords.

This handler only checks the old password and nonce before updating SOAP and WordPress passwords. That does not meet the PR’s stated requirement that website 2FA protects password changes.

🤖 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 28 - 91,
The PasswordHandler class is missing a website 2FA verification check before
processing password changes. Add a 2FA verification requirement at the beginning
of the password change validation flow, before the wp_check_password call for
the old password. This verification should confirm the user has completed fresh
2FA authentication, and if not, set an error message via acore_pw_set_message,
redirect to the security URL, and exit early to prevent the password change from
proceeding without 2FA confirmation.
src/acore-wp-plugin/src/Components/UserPanel/UserController.php-38-39 (1)

38-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the recruiter account ID before building the SOAP command.

$_POST["recruited"] can contain non-numeric text and is later interpolated into bindraf. Cast and reject invalid IDs before lookup/command construction.

🛡️ Proposed fix
-                    $recruiterCode = $_POST["recruited"];
+                    $recruiterCode = absint($_POST["recruited"]);
+                    if ($recruiterCode <= 0) {
+                        $errorMessages[] = "Invalid recruiter value sent.";
+                    }
                     $existingRecruiterId = $acServices->getUserNameByUserId($recruiterCode);

Also applies to: 77-77

🤖 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
38 - 39, The recruiter account ID from $_POST["recruited"] is not validated
before being used in the getUserNameByUserId() call and later interpolated into
the SOAP command bindraf. Add validation to cast the recruiter code to an
integer and reject non-numeric values before passing it to
getUserNameByUserId(). This validation should occur immediately after retrieving
$_POST["recruited"] and before any further operations that use this value. Apply
the same validation to the other occurrence mentioned at line 77.
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php-53-62 (1)

53-62: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Centralize password-history enforcement across all password reset paths.

This check only protects the Security page handler. The supplied profile-update and password-reset paths update acore_password_changed_at but do not update or enforce acore_password_history, so the “no old passwords” policy can be bypassed outside this page.

🤖 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 53 - 62,
The password history enforcement logic that checks acore_allow_old_passwords and
validates against acore_password_history is only applied in this Security page
handler, but other password reset paths bypass it. Extract the entire password
history validation block (the if statement checking get_option for
acore_allow_old_passwords and the foreach loop that calls wp_check_password
against the password history) into a shared/reusable function or validation
method, then ensure this validation is called from all password update paths
(Security page, profile updates, and password resets) before allowing the
password change to proceed. This ensures the no-old-passwords policy cannot be
bypassed regardless of which path is used to change the password.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php-124-130 (1)

124-130: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat email-based website 2FA as enabled.

The PR supports authenticator-app or email website 2FA, but this helper returns false for email-only users. That also affects removal detection and 2fa-status.

🐛 Proposed fix
 function acore_website_2fa_enabled(int $userId): bool {
     $primary = get_user_meta($userId, 'wp_2fa_enabled_methods', true);
     $totpKey = get_user_meta($userId, 'wp_2fa_totp_key', true);
-    return !empty($totpKey) && (
-        (is_array($primary) && in_array('totp', $primary, true)) ||
-        $primary === 'totp'
-    );
+    $methods = is_array($primary) ? $primary : ($primary !== '' ? [$primary] : []);
+    return (in_array('totp', $methods, true) && !empty($totpKey))
+        || in_array('email', $methods, true);
 }
🤖 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
124 - 130, The acore_website_2fa_enabled function currently only checks for TOTP
authenticator-app based 2FA and returns false for email-only 2FA users. Modify
the function to also retrieve email-based 2FA metadata using get_user_meta
(similar to how totpKey is retrieved), then update the return statement to check
if either TOTP or email 2FA is enabled by including a condition that returns
true when email 2FA is not empty alongside the existing TOTP checks, ensuring
that users with email-based 2FA enabled are correctly identified as having 2FA
enabled.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php-458-466 (1)

458-466: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject stale TOTP keys when verifying website 2FA.

This endpoint only checks that wp_2fa_totp_key exists. Other code notes WP2FA may leave that key after removal, so a disabled TOTP method can still unlock the Security panel.

🛡️ Proposed fix
            $user   = wp_get_current_user();
            $rawKey = (string) get_user_meta($user->ID, 'wp_2fa_totp_key', true);
-           if (empty($rawKey))
+           $enabledMethods = get_user_meta($user->ID, 'wp_2fa_enabled_methods', true);
+           $totpActive = !empty($rawKey) && (
+               (is_array($enabledMethods) && in_array('totp', $enabledMethods, true)) ||
+               $enabledMethods === 'totp'
+           );
+           if (!$totpActive)
                return new \WP_Error('no_website_2fa', __('Website 2FA is not enabled on your account.'), ['status' => 400]);
 
            $data  = $request->get_json_params();
🤖 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
458 - 466, The current implementation only checks if the wp_2fa_totp_key
metadata exists, but WP2FA may leave this key even after the TOTP method is
disabled, allowing stale keys to unlock the Security panel. Add an additional
validation check alongside the existing empty($rawKey) check to verify that the
TOTP method is actually enabled or active (not just that the key exists), using
the appropriate WP2FA metadata or status field that indicates whether the 2FA
method is currently active for the user. This ensures that only users with
actively enabled 2FA can proceed past the initial check.
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php-91-95 (1)

91-95: ⚠️ Potential issue | 🟠 Major

Do not force a persistent auth cookie after password change.

Passing true to wp_set_auth_cookie() creates a 14-day persistent session even if the user did not explicitly request "remember me" functionality. When a user changes their password on a security page, they should not be automatically granted a persistent session without consent.

🛈 Proposed fix
     wp_set_password($newPass, $user->ID);
     update_user_meta($user->ID, 'acore_password_changed_at', current_time('mysql'));
 
     wp_set_current_user($user->ID);
-    wp_set_auth_cookie($user->ID, true);
+    wp_set_auth_cookie($user->ID, false);
🤖 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 91 - 95,
The wp_set_auth_cookie() function call is passing true as the second parameter,
which creates an unwanted 14-day persistent session without explicit user
consent for "remember me" functionality after a password change. Modify the
wp_set_auth_cookie($user->ID, true) call to pass false instead of true, or omit
the second parameter entirely to default to a non-persistent session that
respects the user's actual preferences during the password change flow.
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php-24-26 (1)

24-26: ⚠️ Potential issue | 🟠 Major

Unslash password fields before checking or storing them.

WordPress automatically slashes request data, so passwords containing quotes or backslashes will fail the current-password verification (line 28) and be stored with incorrect characters (line 91). The password history check (line 56) will also fail for affected passwords.

🐛 Proposed fix
-    $oldPass     = $_POST['acore_old_pass']     ?? '';
-    $newPass     = $_POST['acore_new_pass']     ?? '';
-    $confirmPass = $_POST['acore_confirm_pass'] ?? '';
+    $oldPass     = isset($_POST['acore_old_pass']) ? (string) wp_unslash($_POST['acore_old_pass']) : '';
+    $newPass     = isset($_POST['acore_new_pass']) ? (string) wp_unslash($_POST['acore_new_pass']) : '';
+    $confirmPass = isset($_POST['acore_confirm_pass']) ? (string) wp_unslash($_POST['acore_confirm_pass']) : '';
🤖 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 24 - 26,
The password fields retrieved from $_POST (acore_old_pass, acore_new_pass,
acore_confirm_pass) need to be unslashed before use because WordPress
automatically adds slashes to request data. Apply WordPress's wp_unslash()
function to each of the three password variable assignments for $oldPass,
$newPass, and $confirmPass to ensure passwords containing quotes or backslashes
are handled correctly during verification at line 28, password history checks at
line 56, and storage at line 91.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php-229-239 (1)

229-239: ⚠️ Potential issue | 🟠 Major

Add error handling for SOAP exceptions in the modules REST endpoint.

The executeCommand() method can throw an Exception if the SOAP service is not properly configured. Without a try-catch, this will cause the REST endpoint to return a fatal error instead of a proper error response. While normal SOAP failures return error strings (which explode() handles safely), unhandled exceptions should be caught and wrapped in a WP_Error.

Proposed fix
        'methods'             => 'POST',
        'permission_callback' => function() { return current_user_can('manage_options'); },
        'callback'            => function( $request ) {
-           $raw      = ACoreServices::I()->getServerSoap()->executeCommand('.server debug');
+           try {
+               $raw = ACoreServices::I()->getServerSoap()->executeCommand('.server debug');
+           } catch (\Throwable $e) {
+               return new \WP_Error('soap_error', $e->getMessage(), ['status' => 503]);
+           }
+           if (!is_string($raw)) {
+               return new \WP_Error('soap_error', __('Unexpected module response.', 'acore-wp-plugin'), ['status' => 503]);
+           }
            $modules  = [];
            $capturing = false;
🤖 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
229 - 239, The executeCommand() method call in the module parsing logic can
throw an Exception if the SOAP service is not properly configured, which will
cause a fatal error instead of a proper REST endpoint error response. Wrap the
entire block starting from the executeCommand() call through the foreach loop
that processes the $raw output in a try-catch block, and return a WP_Error with
an appropriate error message if an Exception is caught, ensuring the REST
endpoint handles SOAP configuration failures gracefully.
🟡 Minor comments (8)
src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php-246-250 (1)

246-250: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hide module panels when SOAP is offline.

Line 246 shows the modules/validator panels on success, but there’s no else branch to hide them on failure. After a later failed check, stale panels remain visible.

Suggested fix
     if (ok) {
         $modPanel.show();
         $valPanel.show();
+    } else {
+        $modPanel.hide();
+        $valPanel.hide();
     }
🤖 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 246 - 250, The if statement checking the ok variable only shows the
$modPanel and $valPanel panels when successful, but lacks an else branch to hide
them when the SOAP check fails. Add an else block after the closing brace of the
if (ok) statement that calls $modPanel.hide() and $valPanel.hide() to ensure the
panels are properly hidden when the SOAP connectivity check is unsuccessful.
src/acore-wp-plugin/web/assets/css/dark-mode.css-10-12 (1)

10-12: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the impossible nested <body> selector.

Line 10 uses body.acore-dark-mode body.profile-php ...; a body element cannot be a descendant of another body, so this rule never matches.

Suggested fix
-body.acore-dark-mode body.profile-php `#wpbody-content` h2,
-body.acore-dark-mode body.profile-php `#wpbody-content` h3 {
+body.acore-dark-mode.profile-php `#wpbody-content` h2,
+body.acore-dark-mode.profile-php `#wpbody-content` h3 {
     border-bottom-color: `#30363d`;
 }
🤖 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/dark-mode.css` around lines 10 - 12, The
CSS selector at line 10 contains an invalid nested body selector that cannot
match any elements since a body element cannot be a descendant of another body
element. Remove the nested body.profile-php part and instead combine both
classes on a single body selector. Change the selector to target the body
element with both the acore-dark-mode and profile-php classes applied
simultaneously, then continue with the descendant selectors `#wpbody-content` h2
and h3 from there.
src/acore-wp-plugin/web/assets/css/main.css-157-162 (1)

157-162: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

background-size is currently canceled by the later shorthand.

Line 157 sets background-size: cover, but Line 161 sets background: none, which resets background-size to auto in the same rule.

Suggested fix
 .unstuck-button {
-    background-size: cover;
     border: none;
     color: transparent;
     cursor: pointer;
-    background: none;
+    background-color: transparent;
+    background-repeat: no-repeat;
+    background-position: center;
+    background-size: cover;
     padding: 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/web/assets/css/main.css` around lines 157 - 162, The
background-size property set to cover is being overridden by the background
shorthand property set to none later in the same CSS rule block. Remove the
background: none declaration or reorder the CSS properties so that
background-size: cover appears after any background shorthand properties,
ensuring the background-size value is not reset. Alternatively, use a more
specific property like background-color: transparent instead of the background
shorthand if you only need to clear the background color while preserving the
background-size setting.

Source: Linters/SAST tools

src/acore-wp-plugin/web/assets/mail-return/mail-return.js-14-17 (1)

14-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset the empty-state text before each load.

After an AJAX error sets "Failed to load mails.", the next successful empty result shows that stale error because the empty path only calls emptyWrap.show().

🐛 Proposed fix
         loading.show();
         mailItems.empty();
+        emptyMsg.text("No unread sent mails found for this character.");
         emptyWrap.hide();
         content.hide();

Also applies to: 27-30, 178-181

🤖 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 14 -
17, The empty-state text is not being reset before displaying the empty wrap,
causing stale error messages like "Failed to load mails." to persist when a
subsequent AJAX request successfully returns an empty result. In the sections
where emptyWrap.show() is called (at lines 27-30 and 178-181 in addition to the
context around line 14-17), reset the text content of emptyWrap to a default
message before calling show() on it. This ensures that any previously set error
messages are cleared and the correct message displays to the user.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php-51-51 (1)

51-51: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the ordering value input hidden.

Line 51 renders as a visible text box by default, exposing GUIDs in the character list and allowing accidental edits.

🐛 Proposed fix
-                                            <input name="characterorder[]" value="<?= $char["guid"] ?>">
+                                            <input type="hidden" name="characterorder[]" value="<?= esc_attr($char["guid"]) ?>">
🤖 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` at line
51, The input element with name="characterorder[]" and value="<?= $char["guid"]
?>" on line 51 in CharactersView.php is currently rendered as a visible text
input box, which exposes GUIDs and allows accidental edits. Add the
type="hidden" attribute to this input element to convert it to a hidden field
while preserving its functionality for form submission.
src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php-156-159 (1)

156-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle numeric-string DeleteDate values.

DB-backed Unix timestamps commonly arrive in JSON as strings, so this branch skips * 1000 and can render raw timestamps instead of formatted dates.

🐛 Proposed fix
                     var d = item['DeleteDate'];
                     if (d) {
-                        var dt = new Date(typeof d === 'number' ? d * 1000 : d);
-                        if (!isNaN(dt)) {
+                        var dStr = String(d);
+                        var dt = /^\d+$/.test(dStr)
+                            ? new Date(Number(dStr) * 1000)
+                            : new Date(dStr.replace(' ', 'T'));
+                        if (!isNaN(dt.getTime())) {
🤖 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 156 - 159, The condition checking the type of DeleteDate in the
ternary operator on line 158 only handles numeric values with typeof d ===
'number', but it does not account for numeric strings which are common when Unix
timestamps arrive from JSON APIs. Modify the condition to also detect and
properly handle numeric-string values by checking if d is a string that
represents a valid number, and multiply those by 1000 just like numeric values.
You can use Number(d) or parseFloat(d) to validate numeric strings, and apply
the same 1000 multiplier for both actual numbers and numeric strings.
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php-78-82 (1)

78-82: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid showing raw SOAP errors to end users.

$soapError can contain transport or host details. Show a generic message and log the raw detail server-side.

🤖 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 78 - 82,
The raw SOAP error details in $soapError are being exposed to end users through
the acore_pw_set_message call in the PasswordHandler.php file, which could
reveal sensitive information. Replace the sprintf message to show users a
generic error message without including the $soapError variable, and add
server-side logging to capture the actual $soapError details using the
appropriate logging mechanism for debugging purposes.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php-343-344 (1)

343-344: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Record the admin IP in 2FA audit entries.

The response shape and PR screenshots expect removal IP details, but admin removal and backup-code removal logs never store an ip, so later alerts can only show null.

🛡️ Proposed fix
-           $log[] = ['type' => $type, 'by' => 'admin', 'timestamp' => $now, 'staff' => $staff->user_login, 'staff_id' => $staff->ID];
+           $log[] = ['type' => $type, 'by' => 'admin', 'timestamp' => $now, 'staff' => $staff->user_login, 'staff_id' => $staff->ID, 'ip' => \ACore\Hooks\User\acore_resolve_client_ip()];
            update_user_meta($user->ID, 'acore_2fa_admin_log', $log);
-           $log[] = ['type' => 'backup', 'by' => 'admin', 'timestamp' => $now, 'staff' => $staff->user_login, 'staff_id' => $staff->ID];
+           $log[] = ['type' => 'backup', 'by' => 'admin', 'timestamp' => $now, 'staff' => $staff->user_login, 'staff_id' => $staff->ID, 'ip' => \ACore\Hooks\User\acore_resolve_client_ip()];
            update_user_meta($user->ID, 'acore_2fa_admin_log', $log);

Also applies to: 368-369

🤖 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
343 - 344, The 2FA audit log entries created in the update_user_meta calls are
missing the admin's IP address field. Add the 'ip' field to the log array (which
currently contains 'type', 'by', 'timestamp', 'staff', and 'staff_id') by
capturing the admin's IP address and including it as a new key-value pair in the
associative array before the update_user_meta call. This fix needs to be applied
in multiple locations (around lines 343-344 and 368-369) where audit logs are
being recorded for admin actions and backup code removals.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e9ea5fa3-703d-4989-b29b-5093cfafb523

📥 Commits

Reviewing files that changed from the base of the PR and between 94af292 and 7a67baa.

📒 Files selected for processing (31)
  • 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/CharactersMenu/CharactersController.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnController.php
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php
  • src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php
  • src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollView.php
  • src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php
  • src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserView.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php
  • src/acore-wp-plugin/src/Hooks/User/Include.php
  • src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/src/Hooks/Various/DarkMode.php
  • src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php
  • src/acore-wp-plugin/src/Manager/ACoreServices.php
  • src/acore-wp-plugin/src/Manager/Opts.php
  • src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php
  • src/acore-wp-plugin/src/Utils/AcoreCharColors.php
  • src/acore-wp-plugin/src/boot.php
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/web/assets/css/theme.css
  • src/acore-wp-plugin/web/assets/mail-return/mail-return.js

Comment thread src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
Comment thread src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
Comment thread src/acore-wp-plugin/src/Components/UserPanel/UserController.php Outdated
Comment thread src/acore-wp-plugin/src/Manager/ACoreServices.php
Comment thread src/acore-wp-plugin/web/assets/mail-return/mail-return.js

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php (2)

185-188: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid innerHTML for REST/error payload rendering.

Line 187, Line 205, Line 218, and Line 224 write server/error content via innerHTML; this is a DOM XSS sink if any response string is attacker-influenced.

Proposed fix
-                errorBox.innerHTML = msg;
+                errorBox.textContent = msg && msg.message ? msg.message : String(msg);
@@
-                    successBox.innerHTML = data;
+                    successBox.textContent = data;
@@
-                    errorBox.innerHTML = data;
+                    errorBox.textContent = typeof data === 'string' ? data : JSON.stringify(data);
@@
-                errorBox.innerHTML = err && err.message ? err.message : 'An error occurred.';
+                errorBox.textContent = err && err.message ? err.message : 'An error occurred.';

Also applies to: 203-206, 218-225

🤖 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 185 - 188, Replace all instances of innerHTML with textContent when
setting error messages or server response content in the catch handlers. This
prevents DOM XSS vulnerabilities by treating the content as plain text instead
of HTML. Specifically, change errorBox.innerHTML = msg to errorBox.textContent =
msg in the catch function block, and apply the same change to all other
locations mentioned (lines around 205, 218, and 224) where server or error
payloads are being rendered to the DOM.

83-84: ⚠️ Potential issue | 🟡 Minor

Add X-WP-Nonce header to item-restore REST calls for CSRF protection.

The routes at lines 24 and 32 (ToolsApi.php) only check is_user_logged_in() and do not verify nonce. The fetch calls at lines 109 and 196 do not send X-WP-Nonce header. While WordPress won't reject requests without nonce in this configuration, all other authenticated REST calls in the codebase consistently include nonce for CSRF protection. ItemRestoration should follow the same pattern.

Proposed fix
-    var listUrl    = '<?= get_rest_url(null, 'acore/v1/item-restore/list/') ?>';
-    var restoreUrl = '<?= get_rest_url(null, 'acore/v1/item-restore') ?>';
+    var listUrl    = '<?= get_rest_url(null, 'acore/v1/item-restore/list/') ?>';
+    var restoreUrl = '<?= get_rest_url(null, 'acore/v1/item-restore') ?>';
+    var restNonce  = '<?= esc_js(wp_create_nonce('wp_rest')) ?>';
@@
-        fetch(listUrl + guid)
+        fetch(listUrl + guid, {
+            headers: { 'Accept': 'application/json', 'X-WP-Nonce': restNonce }
+        })
@@
-            headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
+            headers: {
+                'Accept': 'application/json',
+                'Content-Type': 'application/json',
+                'X-WP-Nonce': restNonce
+            },

Also applies to: 109-110, 196-199

🤖 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 83 - 84, The ItemRestorationPage.php file is missing CSRF
protection headers in its fetch calls. Generate a WordPress nonce using
wp_create_nonce() in the PHP section and output it to JavaScript as a data
attribute or global variable. Then update the fetch calls at lines 109 and 196
(the ones using listUrl and restoreUrl variables) to include the X-WP-Nonce
header in their request headers object, passing the nonce value you created.
This ensures consistency with other authenticated REST calls in the codebase
that already implement this CSRF protection pattern.
src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php (1)

166-187: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Track self-removal for all Website 2FA methods.

acore_2fa_sync_self_removals() only treats TOTP as enabled, so email-only Website 2FA is initialized as “not seen” and later removal is never audited. Use acore_website_2fa_enabled() here to match the feature contract.

🛡️ Proposed fix
 function acore_2fa_sync_self_removals(int $userId): void {
-    $enabled = acore_website_totp_enabled($userId);
+    $enabled = acore_website_2fa_enabled($userId);
     $seen    = get_user_meta($userId, 'acore_2fa_ws_seen_enabled', true);
🤖 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
166 - 187, The function acore_2fa_sync_self_removals() currently uses
acore_website_totp_enabled() to check if Website 2FA is enabled, which only
tracks TOTP removals and ignores email-only Website 2FA methods. Replace the
call to acore_website_totp_enabled($userId) with
acore_website_2fa_enabled($userId) to properly track self-removals for all
Website 2FA methods as intended by the feature contract.
src/acore-wp-plugin/src/Components/UserPanel/UserController.php (1)

29-41: ⚠️ Potential issue | 🟠 Major

Add nonce verification to RAF binding POST handler.

The RAF form in RafProgressPage.php (lines 34–39) posts to UserController.php::showRafProgress() without a CSRF token. The handler at lines 29–41 reads $_POST["recruited"] and executes bindraf (line 80) without verifying a nonce. Add wp_nonce_field() to the form and wp_verify_nonce() or check_admin_referer() in the controller, following the pattern used in SettingsController.php and SecurityPage.php.

Also applies to: 73–80

🤖 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
29 - 41, Add CSRF nonce verification to the RAF form POST handler in the
showRafProgress() method to prevent unauthorized form submissions. Add a
wp_nonce_field() call to the RAF form in RafProgressPage.php, then in
UserController.php at the start of the POST request handler (before processing
$_POST["recruited"] around line 29), add a wp_verify_nonce() or
check_admin_referer() call to validate the nonce matches the one from the form.
This verification should happen before any data processing and before the
bindraf execution at line 80, following the same pattern already implemented in
SettingsController.php and SecurityPage.php.
src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php (1)

126-143: ⚠️ Potential issue | 🟠 Major

Filter out all non-public IP addresses before sending to GeoIP provider.

The current prefix-based validation is incomplete and sends non-public IPs to the external API. While using filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) significantly improves coverage (handling 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8, and IPv6 ULA/link-local ranges), it still misses the Shared Address Space range (100.64.0.0/10, per RFC 6598), which will still be leaked to ip-api.com. Implement complete public IP validation before querying the external service.

🤖 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 126 -
143, The acore_lookup_country function uses incomplete prefix-based validation
that fails to filter out all non-public IP addresses before sending to the
external GeoIP API. Replace the current foreach loop checking against the
$private_ranges array with comprehensive IP validation using filter_var with
FILTER_VALIDATE_IP combined with FILTER_FLAG_NO_PRIV_RANGE and
FILTER_FLAG_NO_RES_RANGE flags. Additionally, add a manual check to filter out
the Shared Address Space range (100.64.0.0/10 per RFC 6598) that is not covered
by the filter flags, returning "Local" if the IP falls within any of these
ranges before making the remote API call.
🤖 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 58-59: The img elements for race and class icons in
MailReturnView.php are missing `alt` attributes, which prevents assistive
technologies from understanding the image content. Add `alt` attributes to both
the race-icon and class-icon img elements, using the same values already being
used for the `title` attributes (the results from AcoreCharColors::getRaceName()
and AcoreCharColors::getClassName() respectively) to ensure accessibility
compliance while maintaining consistency with the existing tooltip
functionality.

In `@src/acore-wp-plugin/src/Components/Tools/ToolsApi.php`:
- Around line 13-20: The ItemRestore method accepts the $item parameter directly
from the request and interpolates it into a SOAP command without validation,
creating a potential injection vulnerability. Before the ownership check for
$cname, add validation to ensure $item is a valid integer and reject the request
with an appropriate error if it is not. Use intval() or is_numeric() to verify
that $item contains only a valid numeric value before proceeding with the
ACoreServices SOAP command execution.
- Around line 24-35: The ItemRestorationPage.php file has two fetch calls that
are missing the required X-WP-Nonce header for REST authentication. Create a
nonce at the top of the ItemRestorationPage.php script using
wp_create_nonce('wp_rest'), then add the X-WP-Nonce header with this nonce value
to both fetch calls: the one at line 110 for the list items request and the one
at line 195 for the restore item request. Follow the implementation pattern
already used in UnstuckView.php, MailReturnView.php, and SecurityPage.php to
ensure consistency with existing code.

In `@src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php`:
- Around line 110-116: The IP extraction logic in the forwarded headers
validation section (around lines 110-116 in ConnectionLogger.php) trusts
HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP headers without validating that the
request originates from a trusted proxy. Add validation to check that the
REMOTE_ADDR server variable matches an entry in a configured trusted proxy or
CIDR list before using the forwarded headers to extract the client IP. This
prevents header spoofing attacks where an attacker can manipulate these headers
to forge arbitrary client IPs.

In `@src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php`:
- Around line 80-82: In the PasswordHandler class, the error_log call at line 81
logs sensitive information including the username (user_login) which is PII and
raw SOAP error details that may expose backend information. Replace the
user_login with the user ID (user->ID) as a non-PII identifier, and replace the
raw $soapError with a generic message that does not expose backend details. Keep
the transient message to the user generic as well without exposing raw error
information.
- Around line 24-29: The password-change 2FA verification gate in
PasswordHandler.php only checks for TOTP-enabled users via
acore_website_totp_enabled(), allowing email-based 2FA users to bypass
verification. Replace the acore_website_totp_enabled() check with the broader
acore_website_2fa_enabled() function that covers both TOTP and email-based 2FA.
Then extend the existing transient unlock mechanism (currently checked via
acore_2fa_unlock_key()) to support both TOTP and email verification paths,
ensuring all 2FA methods are validated before allowing password changes.

In `@src/acore-wp-plugin/web/assets/css/dark-mode.css`:
- Line 122: Remove the deprecated `clip` property from the hidden-radio rule in
the dark-mode.css file. The line contains `clip: rect(0 0 0 0);` which uses the
deprecated CSS property. Since the modern `clip-path` alternative is already
present on the next line, delete the `clip` property line entirely and keep only
the `clip-path` declaration.

---

Outside diff comments:
In `@src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php`:
- Around line 166-187: The function acore_2fa_sync_self_removals() currently
uses acore_website_totp_enabled() to check if Website 2FA is enabled, which only
tracks TOTP removals and ignores email-only Website 2FA methods. Replace the
call to acore_website_totp_enabled($userId) with
acore_website_2fa_enabled($userId) to properly track self-removals for all
Website 2FA methods as intended by the feature contract.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php`:
- Around line 185-188: Replace all instances of innerHTML with textContent when
setting error messages or server response content in the catch handlers. This
prevents DOM XSS vulnerabilities by treating the content as plain text instead
of HTML. Specifically, change errorBox.innerHTML = msg to errorBox.textContent =
msg in the catch function block, and apply the same change to all other
locations mentioned (lines around 205, 218, and 224) where server or error
payloads are being rendered to the DOM.
- Around line 83-84: The ItemRestorationPage.php file is missing CSRF protection
headers in its fetch calls. Generate a WordPress nonce using wp_create_nonce()
in the PHP section and output it to JavaScript as a data attribute or global
variable. Then update the fetch calls at lines 109 and 196 (the ones using
listUrl and restoreUrl variables) to include the X-WP-Nonce header in their
request headers object, passing the nonce value you created. This ensures
consistency with other authenticated REST calls in the codebase that already
implement this CSRF protection pattern.

In `@src/acore-wp-plugin/src/Components/UserPanel/UserController.php`:
- Around line 29-41: Add CSRF nonce verification to the RAF form POST handler in
the showRafProgress() method to prevent unauthorized form submissions. Add a
wp_nonce_field() call to the RAF form in RafProgressPage.php, then in
UserController.php at the start of the POST request handler (before processing
$_POST["recruited"] around line 29), add a wp_verify_nonce() or
check_admin_referer() call to validate the nonce matches the one from the form.
This verification should happen before any data processing and before the
bindraf execution at line 80, following the same pattern already implemented in
SettingsController.php and SecurityPage.php.

In `@src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php`:
- Around line 126-143: The acore_lookup_country function uses incomplete
prefix-based validation that fails to filter out all non-public IP addresses
before sending to the external GeoIP API. Replace the current foreach loop
checking against the $private_ranges array with comprehensive IP validation
using filter_var with FILTER_VALIDATE_IP combined with FILTER_FLAG_NO_PRIV_RANGE
and FILTER_FLAG_NO_RES_RANGE flags. Additionally, add a manual check to filter
out the Shared Address Space range (100.64.0.0/10 per RFC 6598) that is not
covered by the filter flags, returning "Local" if the IP falls within any of
these ranges before making the remote API call.
🪄 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: f4556f70-0dc3-4d8f-b35c-7af666632b66

📥 Commits

Reviewing files that changed from the base of the PR and between 7a67baa and 6be0629.

📒 Files selected for processing (19)
  • 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/CharactersMenu/CharactersController.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/ServerInfo/ServerInfoApi.php
  • src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
  • src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php
  • src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/src/Manager/ACoreServices.php
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/web/assets/mail-return/mail-return.js
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersController.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
  • src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/web/assets/mail-return/mail-return.js
  • src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php

Comment thread src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php Outdated
Comment thread src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
Comment thread src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
Comment thread src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php Outdated
Comment thread src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php Outdated
Comment thread src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php
Comment thread src/acore-wp-plugin/web/assets/css/dark-mode.css Outdated

@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/ServerInfo/ServerInfoApi.php`:
- Around line 493-517: The request-email-2fa endpoint callback function lacks
rate limiting, allowing an authenticated user to repeatedly trigger email sends.
Before calling wp_mail to send the verification email, implement a
transient-based rate limit check that uses the current user's ID to track and
limit requests. Check if a rate limit transient exists for the user, and if it
does, return an error response indicating the user must wait before requesting
another code. Set a rate limit transient (e.g., with a duration like 1 or 2
minutes) after a successful email send to prevent abuse while allowing
legitimate retry attempts after a reasonable interval.
🪄 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: 043367cb-8dac-474d-9637-07a5a6b65762

📥 Commits

Reviewing files that changed from the base of the PR and between 6be0629 and b308e07.

📒 Files selected for processing (10)
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php
  • src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.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/Components/UserPanel/Pages/RafProgressPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php
  • src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
💤 Files with no reviewable changes (1)
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.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/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php

Comment thread src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php
@TheSCREWEDSoftware

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/acore-wp-plugin/src/Hooks/User/User.php (1)

105-111: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Route all password-change paths through the same 2FA and reuse policy.

These profile/reset paths now update the Security-page timestamp, but they bypass the new PasswordHandler checks for Website 2FA unlock and password reuse history. Apply the same policy before these paths can commit a password change, or centralize password changes in one shared service.

Also applies to: 126-136

🤖 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 105 - 111, The
password change operation in the isset($_POST['pass1']) block at lines 105-111
is calling $soap->setAccountPassword() directly, bypassing the new
PasswordHandler checks for Website 2FA unlock and password reuse history.
Instead of directly invoking $soap->setAccountPassword(), refactor this code
path to route the password change through the PasswordHandler service class
which centralizes these policy checks. This same refactoring should be applied
to the other password-change path mentioned at lines 126-136 to ensure
consistent security policy enforcement across all password-change operations.
♻️ Duplicate comments (2)
src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php (1)

24-29: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Block password changes when Website 2FA is not set up.

This gate only verifies 2FA when it is already enabled; users with no Website 2FA configured can still change passwords. That contradicts the PR requirement that Website 2FA is required for password changes.

🔒 Proposed policy enforcement
-    if (\ACore\Components\ServerInfo\acore_website_2fa_enabled($user->ID)
-        && !get_transient(\ACore\Components\ServerInfo\acore_2fa_unlock_key($user->ID))) {
+    if (!\ACore\Components\ServerInfo\acore_website_2fa_enabled($user->ID)) {
+        acore_pw_set_message($user->ID, 'error', __('Please set up Website 2FA before changing your password.', 'acore-wp-plugin'));
+        wp_redirect($security_url);
+        exit;
+    }
+
+    if (!get_transient(\ACore\Components\ServerInfo\acore_2fa_unlock_key($user->ID))) {
         acore_pw_set_message($user->ID, 'error', __('Please verify your 2FA code before changing your password.', '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/Hooks/User/PasswordHandler.php` around lines 24 - 29,
The conditional logic in the PasswordHandler.php file's password change gate
currently only blocks users when 2FA is enabled but not verified. However, the
requirement is that Website 2FA must be configured for any password change.
Modify the if statement condition to block password changes both when 2FA is not
enabled at all (add a check for when acore_website_2fa_enabled returns false)
and when 2FA is enabled but not yet verified (the existing check). Combine these
checks with an OR operator so that password changes are blocked in either
scenario.
src/acore-wp-plugin/src/Components/Tools/ToolsApi.php (1)

16-19: 🗄️ Data Integrity & Integration | 🟡 Minor

Reject only canonical integer item IDs.

is_numeric() accepts floating-point and scientific notation strings (e.g., "12.34", "1.2e3"), and intval() silently coerces them by truncating at the first non-integer character. This means a malformed REST payload like {"item": "12.34"} gets validated as true but coerced to ID 12, potentially targeting a different item than intended. Use FILTER_VALIDATE_INT with min_range to accept only canonical integers.

🛠️ Proposed fix
-        $item = $data['item'];
+        $item = filter_var($data['item'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
         $cname = $data['cname'];
-        if (!is_numeric($item) || intval($item) <= 0) {
+        if ($item === false) {
             return new \WP_Error('invalid_item', 'Invalid item id.', ['status' => 400]);
         }
-        $item = intval($item);
🤖 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/Tools/ToolsApi.php` around lines 16 - 19,
Replace the numeric validation and type coercion in the item ID check with a
stricter validation approach. Instead of using is_numeric() followed by
intval(), use the FILTER_VALIDATE_INT filter with the min_range option set to 1
to strictly validate that the item parameter is a canonical integer with a
minimum value greater than zero. This prevents floating-point strings like
"12.34" or scientific notation from being accepted and silently coerced to
incorrect integer values, ensuring only valid positive integers pass validation
in the conditional block.
🧹 Nitpick comments (3)
src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php (1)

84-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use ACORE_SLUG in frontend REST URL construction.

Line 84 and Line 85 hardcode acore/v1 while route registration elsewhere uses ACORE_SLUG . '/v1'. Reusing the constant avoids contract drift.

♻️ Proposed change
-    var listUrl    = '<?= get_rest_url(null, 'acore/v1/item-restore/list/') ?>';
-    var restoreUrl = '<?= get_rest_url(null, 'acore/v1/item-restore') ?>';
+    var listUrl    = '<?= esc_js(get_rest_url(null, ACORE_SLUG . '/v1/item-restore/list/')) ?>';
+    var restoreUrl = '<?= esc_js(get_rest_url(null, ACORE_SLUG . '/v1/item-restore')) ?>';
🤖 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 84 - 85, The REST URL construction in the listUrl and restoreUrl
variables on lines 84-85 hardcodes the string 'acore/v1' instead of using the
ACORE_SLUG constant. Replace the hardcoded 'acore/v1' strings with ACORE_SLUG .
'/v1' in both get_rest_url() function calls to ensure consistency with route
registration elsewhere in the codebase and prevent contract drift.
src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php (1)

48-49: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Harden race/class icon URL rendering.

Line 48 and Line 49 compose src from DB values without strict casting + URL escaping. Cast to int and escape the final URL before output.

🔧 Proposed hardening patch
-<img class="race-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getRaceName(intval($char["race"]))) ?>" src="<?= ACORE_URL_PLG . "web/assets/race/" . $char["race"] . ($char["gender"] == 0 ? "m" : "f") . ".webp"; ?>">
-<img class="class-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getClassName(intval($char["class"]))) ?>" src="<?= ACORE_URL_PLG . "web/assets/class/" . $char["class"] . ".webp"; ?>">
+<img class="race-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getRaceName(intval($char["race"]))) ?>" src="<?= esc_url(ACORE_URL_PLG . 'web/assets/race/' . intval($char['race']) . (intval($char['gender']) === 0 ? 'm' : 'f') . '.webp') ?>">
+<img class="class-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getClassName(intval($char["class"]))) ?>" src="<?= esc_url(ACORE_URL_PLG . 'web/assets/class/' . intval($char['class']) . '.webp') ?>">
🤖 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 48 - 49, The race-icon and class-icon src attributes on lines 48 and 49
are constructing URLs from database values without proper type casting and
output escaping. Cast the $char["race"], $char["gender"], and $char["class"]
values to integers using intval() or explicit casting, then wrap the entire src
URL expression with esc_url() before output to ensure the URLs are properly
escaped and sanitized for HTML attribute rendering.
src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php (1)

55-56: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cast and escape icon path components before output.

Line 55 and Line 56 build image src with raw DB values. Please cast to int and escape the final URL to avoid malformed attribute output.

🔧 Proposed hardening patch
-<img class="race-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getRaceName(intval($char["race"]))) ?>" src="<?= ACORE_URL_PLG . "web/assets/race/" . $char["race"] . ($char["gender"] == 0 ? "m" : "f") . ".webp"; ?>">
-<img class="class-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getClassName(intval($char["class"]))) ?>" src="<?= ACORE_URL_PLG . "web/assets/class/" . $char["class"] . ".webp"; ?>">
+<img class="race-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getRaceName(intval($char["race"]))) ?>" src="<?= esc_url(ACORE_URL_PLG . 'web/assets/race/' . intval($char['race']) . (intval($char['gender']) === 0 ? 'm' : 'f') . '.webp') ?>">
+<img class="class-icon" height="32" width="32" title="<?= esc_attr(AcoreCharColors::getClassName(intval($char["class"]))) ?>" src="<?= esc_url(ACORE_URL_PLG . 'web/assets/class/' . intval($char['class']) . '.webp') ?>">
🤖 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/UnstuckMenu/UnstuckView.php` around lines
55 - 56, The image src attributes for both the race-icon and class-icon img
elements are constructed using raw database values from the $char array without
proper type casting or escaping. Cast the $char["race"], $char["gender"], and
$char["class"] values to integers using intval() before concatenating them into
the src path string, and wrap the entire final src URL with esc_url() to
properly escape the attribute output and prevent malformed HTML attributes.
🤖 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/AdminPanel/Pages/RealmSettings.php`:
- Around line 354-370: The issue is that after successfully saving module
requirements via AJAX, the referencedSlugs variable is not being updated with
the newly saved requirements. Inside the .done() handler of the AJAX call, after
the success message is displayed, extract the product slugs from the reqs array
that was just submitted and update the referencedSlugs variable to reflect the
current saved state. This ensures that subsequent validation checks use the most
recent requirements rather than the original PHP-initialized list.
- Around line 305-328: The module list rendering and "Last refreshed" timestamp
updates are currently wrapped in an if (!opts.auto) conditional in the
refreshModules function, which means automatic refreshes skip updating the
visible UI elements and leave stale module information displayed. Move the code
that updates the $list with module tags and the `#acore-modules-refreshed`
timestamp outside the if (!opts.auto) conditional block so that these UI
elements are refreshed during both automatic and manual refresh operations,
ensuring the panel always displays current module information.

In `@src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php`:
- Line 90: The form in Tools.php lacks nonce validation, creating a CSRF
vulnerability where logged-in admins could be tricked into changing security
settings via cross-site requests. Add a WordPress nonce field to the form
element starting at line 90 using wp_nonce_field(), then validate that nonce in
the SettingsController::loadTools() method before the loop that processes and
persists the posted options. Ensure the nonce validation includes both
verification of the nonce existence and its validity using wp_verify_nonce(),
returning early if validation fails.
- Around line 526-536: The addThreshold function is interpolating the level and
days parameter values directly into HTML template strings which creates an XSS
vulnerability if tampered values are passed. Instead of interpolating these
values directly in the HTML strings for the input elements, create the input
elements without values and then use the jQuery .val() method to set the level
and days values after creation. Additionally, ensure that any server-side values
passed to addThreshold are JSON-encoded on the backend before being sent to
JavaScript to prevent injection attacks.
- Around line 122-148: When dependent form controls are disabled (such as
acore_resurrection_scroll and acore_resurrection_scroll_days_inactive), they are
excluded from the POST body submission. This causes loadTools() to not update
these values, leaving stale data persisted. Add hidden input fields that submit
explicit default values whenever the dependent controls are disabled. For the
acore_resurrection_scroll select, add a hidden input with value 0 when
!$hasResurrectionMod is true. For the acore_resurrection_scroll_days_inactive
input, add a hidden input with an appropriate default value when !$scrollEnabled
is true. Apply the same pattern to the other affected settings mentioned at
lines 178-184 and 331-337.

In `@src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php`:
- Around line 470-487: The verify-website-2fa REST endpoint callback function
currently lacks rate limiting on failed verification attempts, allowing
unlimited 6-digit code guesses. Add a transient-based failed attempt counter
using set_transient and get_transient with a user-specific key before processing
the token validation. Check the current failed attempt count at the start of the
callback and return an error if a threshold is exceeded. Increment the failed
attempt counter each time an invalid token error is returned (for the
invalid_token, wrong_token, and no_website_2fa conditions). Reset the failed
attempt counter to zero when the token validation succeeds and the unlock
transient is set. Apply this same throttling pattern to the other verification
endpoint mentioned at lines 524-541.
- Around line 289-300: The $active status check in the ServerInfoApi.php file
only verifies if TOTP 2FA is enabled but fails to account for email-based
Website 2FA protection. Modify the condition that sets the $active variable to
also check if 'email' is present in the enabledMethods metadata, using the same
logic pattern used for checking 'totp' - check if it exists in the array or if
enabledMethods equals 'email' directly. This ensures users protected by
email-based 2FA will correctly report as active in the admin status endpoint.
- Around line 190-202: The REST API route registration for 'server-info' is
missing a `permission_callback` parameter, making it publicly accessible, and
the error response exposes sensitive SOAP backend details by returning the raw
error result. Add a `permission_callback` parameter to the register_rest_route
call that checks current_user_can('manage_options') to restrict access to
administrators only, matching the pattern used in other routes in this file.
Additionally, modify the error handling in the callback function to return a
generic error message to the client instead of returning the raw $result
variable directly, and instead log the actual error details server-side for
debugging purposes.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php`:
- Around line 111-113: In all the fetch calls using the .then(function (r) {
return r.json(); }) pattern (lines 111-113, 116-118, 206-207, and 223-224), add
a check for response.ok before parsing JSON. If the response is not successful
(not a 2xx status), throw an error to prevent API error responses from being
incorrectly processed as valid data. Modify the first .then() handler to
validate r.ok is true before calling r.json(), and throw a normalized error if
the response is not successful.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php`:
- Around line 293-355: The SecurityPage.php currently restricts in-game 2FA
management to only users with TOTP-based Website 2FA enabled by checking
$websiteTotpEnabled, but the page supports email-based Website 2FA as well.
Replace both instances of the $websiteTotpEnabled condition checks (at the start
and end of the in-game 2FA section) with a more inclusive variable like
$websiteAnyEnabled or $twofaUnlocked that represents any form of Website 2FA
being active. Additionally, locate and update the remove-ingame-2fa REST
endpoint handler to accept the email unlock path alongside the existing TOTP
unlock path.

In `@src/acore-wp-plugin/src/Components/UserPanel/UserController.php`:
- Around line 220-221: The getModel() method in the UserController class returns
$this->model, but this property is never defined in the class, resulting in an
undefined property access. Define the $model property as a class member variable
(either in the constructor or as a class property declaration) and ensure it is
properly initialized with a valid model instance before the getModel() method
attempts to return it.
- Around line 16-17: The UserController constructor passes $this as an argument
to UserView on line 17, but UserView does not have a constructor defined to
accept this parameter, which will cause a runtime error. Remove the $this
argument from the UserView instantiation. Additionally, the getModel() method
references $this->model property, but this property is never declared or
initialized in the UserController class. Add a $this->model property declaration
to the UserController class (either as a class property or initialize it in the
__construct method) to ensure the property exists when getModel() is called.

In `@src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php`:
- Around line 144-162: The acore_ip_in_cidr() function casts the CIDR prefix
bits to an integer but never validates that it falls within a valid range before
using it for string indexing and bit operations. A malformed CIDR value like
/999 can cause out-of-bounds access and runtime warnings. Add a validation check
after confirming that both $ipBin and $suBin are valid and have matching lengths
to ensure that $bits does not exceed the maximum valid bit count for the IP
version (8 times the length of the binary string), and return false if the
prefix length is out of valid range.
- Around line 222-224: The wp_remote_get call in the ConnectionLogger is using
an insecure HTTP endpoint to ip-api.com instead of HTTPS, which exposes user IP
addresses and country codes in transit. Replace the
"http://ip-api.com/json/{$ip}?fields=status,countryCode" endpoint with an HTTPS
alternative (either by upgrading to the paid pro tier of ip-api.com that
supports HTTPS, switching to a different GeoIP provider that supports HTTPS, or
removing the external lookup entirely if unencrypted transport is unacceptable
for your security logging). Note that the same issue also appears in another
location around lines 296-300, so ensure you fix all instances of this HTTP
endpoint call.

In `@src/acore-wp-plugin/src/Hooks/User/User.php`:
- Around line 632-668: The code does not properly handle email-based 2FA users
and self-removal log entries in the warning blocks. First, modify the logic
determining website 2FA state to distinguish between email-based 2FA and TOTP,
so email-only users are not incorrectly treated as unprotected. Second, before
accessing the staff field in the warning display for lastWebRemoval and
lastGameRemoval entries, check if the removal was self-initiated by examining
the entry's type or structure, since self-removal entries store 'by => self' and
'ip' instead of 'staff'. Adjust the warning message display logic to handle both
staff-removal and self-removal cases appropriately, and apply the same fixes to
the similar code block referenced at lines 735-762.

In `@src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php`:
- Around line 42-46: The WP 2FA plugin configuration currently only includes
`required => true`, which marks the plugin as required for installation but
still allows administrators to manually deactivate it from the plugins UI. To
prevent deactivation and enforce the "cannot be disabled" requirement, add the
parameter `force_activation => true` to the WP 2FA configuration array (the one
with slug 'wp-2fa'). This will remove the deactivate button from the plugins
interface and automatically re-activate the plugin if someone attempts to
deactivate it.

In `@src/acore-wp-plugin/web/assets/css/dark-mode.css`:
- Around line 21-24: The CSS selector for `#acore-item-restoration-page .table
thead` is applying dark mode styling globally instead of only when dark mode is
active, causing the dark header background and text color to appear in light
mode as well. Prefix this selector with `body.acore-dark-mode` to scope the dark
header styling exclusively to when dark mode is enabled on the body element.

In `@src/acore-wp-plugin/web/assets/css/main.css`:
- Line 120: The keyword-case stylelint rule requires CSS color keywords to be
lowercase. In the border property at line 120, change `currentColor` to
`currentcolor` to satisfy the configured `value-keyword-case` rule and resolve
the CI lint violation.
- Around line 475-477: The mail item grid uses a fixed grid-template-columns
property with repeat(6, 56px) that does not adapt to narrow screens and causes
overflow on smaller phones. Add a media query targeting small screen sizes
(e.g., max-width breakpoints) that overrides the grid-template-columns rule with
fewer columns (such as repeat(3, 56px) or repeat(4, 56px)) to ensure the slots
wrap appropriately and prevent horizontal overflow on mobile devices.

In `@src/acore-wp-plugin/web/assets/css/theme.css`:
- Around line 230-240: The Stylelint rule `declaration-empty-line-before` is
violated in the body.wp2fa-setup selector. Add an empty line before the
background property declaration to separate the custom property declarations
(--acore-bg, --acore-surface, etc.) from the regular property declarations
(background, color). This maintains proper CSS formatting by grouping related
declarations with appropriate spacing.

---

Outside diff comments:
In `@src/acore-wp-plugin/src/Hooks/User/User.php`:
- Around line 105-111: The password change operation in the
isset($_POST['pass1']) block at lines 105-111 is calling
$soap->setAccountPassword() directly, bypassing the new PasswordHandler checks
for Website 2FA unlock and password reuse history. Instead of directly invoking
$soap->setAccountPassword(), refactor this code path to route the password
change through the PasswordHandler service class which centralizes these policy
checks. This same refactoring should be applied to the other password-change
path mentioned at lines 126-136 to ensure consistent security policy enforcement
across all password-change operations.

---

Duplicate comments:
In `@src/acore-wp-plugin/src/Components/Tools/ToolsApi.php`:
- Around line 16-19: Replace the numeric validation and type coercion in the
item ID check with a stricter validation approach. Instead of using is_numeric()
followed by intval(), use the FILTER_VALIDATE_INT filter with the min_range
option set to 1 to strictly validate that the item parameter is a canonical
integer with a minimum value greater than zero. This prevents floating-point
strings like "12.34" or scientific notation from being accepted and silently
coerced to incorrect integer values, ensuring only valid positive integers pass
validation in the conditional block.

In `@src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php`:
- Around line 24-29: The conditional logic in the PasswordHandler.php file's
password change gate currently only blocks users when 2FA is enabled but not
verified. However, the requirement is that Website 2FA must be configured for
any password change. Modify the if statement condition to block password changes
both when 2FA is not enabled at all (add a check for when
acore_website_2fa_enabled returns false) and when 2FA is enabled but not yet
verified (the existing check). Combine these checks with an OR operator so that
password changes are blocked in either scenario.

---

Nitpick comments:
In `@src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php`:
- Around line 48-49: The race-icon and class-icon src attributes on lines 48 and
49 are constructing URLs from database values without proper type casting and
output escaping. Cast the $char["race"], $char["gender"], and $char["class"]
values to integers using intval() or explicit casting, then wrap the entire src
URL expression with esc_url() before output to ensure the URLs are properly
escaped and sanitized for HTML attribute rendering.

In `@src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php`:
- Around line 55-56: The image src attributes for both the race-icon and
class-icon img elements are constructed using raw database values from the $char
array without proper type casting or escaping. Cast the $char["race"],
$char["gender"], and $char["class"] values to integers using intval() before
concatenating them into the src path string, and wrap the entire final src URL
with esc_url() to properly escape the attribute output and prevent malformed
HTML attributes.

In `@src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php`:
- Around line 84-85: The REST URL construction in the listUrl and restoreUrl
variables on lines 84-85 hardcodes the string 'acore/v1' instead of using the
ACORE_SLUG constant. Replace the hardcoded 'acore/v1' strings with ACORE_SLUG .
'/v1' in both get_rest_url() function calls to ensure consistency with route
registration elsewhere in the codebase and prevent contract drift.
🪄 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: cb98e086-41a2-4cbe-9133-2b043bd4b930

📥 Commits

Reviewing files that changed from the base of the PR and between 94af292 and b462d5d.

📒 Files selected for processing (33)
  • 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/CharactersMenu/CharactersController.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnController.php
  • src/acore-wp-plugin/src/Components/MailReturnMenu/MailReturnView.php
  • src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollMenu.php
  • src/acore-wp-plugin/src/Components/ResurrectionScrollMenu/ResurrectionScrollView.php
  • src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php
  • src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
  • src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/RafProgressPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserMenu.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserView.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php
  • src/acore-wp-plugin/src/Hooks/User/Include.php
  • src/acore-wp-plugin/src/Hooks/User/PasswordHandler.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/src/Hooks/Various/DarkMode.php
  • src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php
  • src/acore-wp-plugin/src/Manager/ACoreServices.php
  • src/acore-wp-plugin/src/Manager/Opts.php
  • src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php
  • src/acore-wp-plugin/src/Utils/AcoreCharColors.php
  • src/acore-wp-plugin/src/boot.php
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/web/assets/css/theme.css
  • src/acore-wp-plugin/web/assets/mail-return/mail-return.js

Comment on lines +305 to +328
function refreshModules(opts) {
opts = opts || {};
var $btn = opts.auto ? null : $('#acore-modules-refresh').prop('disabled', true).text('Refreshing…');
return $.ajax({
url: restBase + 'server-modules', method: 'POST',
beforeSend: function(xhr){ xhr.setRequestHeader('X-WP-Nonce', nonce); }
}).done(function(res){
var newModules = (res.modules || []);

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());
}

renderAutoCheck(diffReferenced(knownModules, newModules));
knownModules = newModules;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render the refreshed module list during automatic refreshes too.

On initial connected load, refreshModules({ auto: true }) updates knownModules and the diff summary but skips the visible module tags and “Last refreshed” text, so the panel can show stale modules.

🛠️ Proposed fix
+    function renderModuleList(newModules, refreshed) {
+        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.'));
+        }
+        if (refreshed) {
+            var d = new Date(refreshed * 1000);
+            $('`#acore-modules-refreshed`').html('Last refreshed:<br>' + d.toLocaleString());
+        }
+    }
+
     function refreshModules(opts) {
@@
-            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());
-            }
+            renderModuleList(newModules, res.refreshed);
🤖 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 305 - 328, The module list rendering and "Last refreshed" timestamp
updates are currently wrapped in an if (!opts.auto) conditional in the
refreshModules function, which means automatic refreshes skip updating the
visible UI elements and leave stale module information displayed. Move the code
that updates the $list with module tags and the `#acore-modules-refreshed`
timestamp outside the if (!opts.auto) conditional block so that these UI
elements are refreshed during both automatic and manual refresh operations,
ensuring the panel always displays current module information.

@@ -22,19 +89,17 @@
<hr>
<form method="post">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Add nonce validation before saving the new security settings.

The new Web Integration toggles and reset action are saved through the Tools POST path, but this form does not send a nonce and loadTools() persists posted options without one. A cross-site request from a logged-in admin could change password-reuse/security-logging settings or reset name-unlock settings.

🛡️ Proposed fix
-            <form method="post">
+            <form method="post">
+                <?php wp_nonce_field('acore_tools_save', 'acore_tools_nonce'); ?>

Also validate it in SettingsController::loadTools() before iterating options:

             if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+                check_admin_referer('acore_tools_save', 'acore_tools_nonce');
+
                 foreach (Opts::I()->getConfs() as $key => $value) {

Also applies to: 158-194, 577-585

🤖 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` at line 90,
The form in Tools.php lacks nonce validation, creating a CSRF vulnerability
where logged-in admins could be tricked into changing security settings via
cross-site requests. Add a WordPress nonce field to the form element starting at
line 90 using wp_nonce_field(), then validate that nonce in the
SettingsController::loadTools() method before the loop that processes and
persists the posted options. Ensure the nonce validation includes both
verification of the nonce existence and its validity using wp_verify_nonce(),
returning early if validation fails.

Comment thread src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php
Comment thread src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php Outdated
Comment thread src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php
Comment thread src/acore-wp-plugin/web/assets/css/dark-mode.css Outdated
Comment thread src/acore-wp-plugin/web/assets/css/main.css Outdated
Comment thread src/acore-wp-plugin/web/assets/css/main.css
Comment thread src/acore-wp-plugin/web/assets/css/theme.css
Do not enforce 2FA for password change, it's an optional use not forced.

Still use the old/current system for password changing flow

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php (1)

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

Apply CSRF protection to all POST handlers.

The loadElunaSettings() and loadPvpRewards() methods process POST requests without nonce verification, creating CSRF vulnerabilities. An attacker could trick an administrator into unknowingly submitting malicious configuration changes or triggering reward distributions.

🔒 Proposed fix to add nonce checks

For loadElunaSettings():

     if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        check_admin_referer('acore_eluna_settings_save', 'acore_eluna_settings_nonce');
+
         foreach (Opts::I()->getConfs() as $key => $value) {

For loadPvpRewards() (first POST check at line 135):

     if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        check_admin_referer('acore_pvp_rewards_save', 'acore_pvp_rewards_nonce');
+
         global $wpdb;

For loadPvpRewards() (second POST check at line 162):

     if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        // Nonce already checked at line 135
         global $wpdb;

Don't forget to add the corresponding wp_nonce_field() calls in the Eluna settings and PvP rewards view templates.

Also applies to: 135-144, 162-162

🤖 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/SettingsController.php` at line
88, Add CSRF protection by implementing nonce verification to all POST request
handlers in the SettingsController.php file. In the loadElunaSettings() method
(POST check at line 88), add a check for wp_verify_nonce() with an appropriate
nonce action before processing the POST data. Similarly, in the loadPvpRewards()
method, add nonce verification checks at both POST handler locations (lines
135-144 and 162). Additionally, add corresponding wp_nonce_field() calls in the
view templates for Eluna settings and PvP rewards forms to generate the nonce
fields that will be sent with the POST requests.
🤖 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.

Outside diff comments:
In `@src/acore-wp-plugin/src/Components/AdminPanel/SettingsController.php`:
- Line 88: Add CSRF protection by implementing nonce verification to all POST
request handlers in the SettingsController.php file. In the loadElunaSettings()
method (POST check at line 88), add a check for wp_verify_nonce() with an
appropriate nonce action before processing the POST data. Similarly, in the
loadPvpRewards() method, add nonce verification checks at both POST handler
locations (lines 135-144 and 162). Additionally, add corresponding
wp_nonce_field() calls in the view templates for Eluna settings and PvP rewards
forms to generate the nonce fields that will be sent with the POST requests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aab296c-7232-4b05-b5f6-2bb69fbb7e37

📥 Commits

Reviewing files that changed from the base of the PR and between b462d5d and fcf9745.

📒 Files selected for processing (16)
  • 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/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php
  • src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
  • src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/web/assets/css/theme.css
🚧 Files skipped from review as they are similar to previous changes (15)
  • src/acore-wp-plugin/src/Hooks/Various/tgmplugin_activator.php
  • src/acore-wp-plugin/src/Components/Tools/ToolsApi.php
  • src/acore-wp-plugin/src/Components/CharactersMenu/CharactersView.php
  • src/acore-wp-plugin/src/Components/AdminPanel/Pages/RealmSettings.php
  • src/acore-wp-plugin/src/Components/UnstuckMenu/UnstuckView.php
  • src/acore-wp-plugin/src/Components/UserPanel/UserController.php
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/ItemRestorationPage.php
  • src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php
  • src/acore-wp-plugin/web/assets/css/dark-mode.css
  • src/acore-wp-plugin/src/Components/UserPanel/Pages/SecurityPage.php
  • src/acore-wp-plugin/web/assets/css/main.css
  • src/acore-wp-plugin/src/Components/ServerInfo/ServerInfoApi.php
  • src/acore-wp-plugin/src/Hooks/User/ConnectionLogger.php
  • src/acore-wp-plugin/src/Hooks/User/User.php
  • src/acore-wp-plugin/web/assets/css/theme.css

added missing nonce/csrf
added missing nonce/csrf and save states.
@TheSCREWEDSoftware

Copy link
Copy Markdown
Contributor Author

Closing this PR as it has been splited already (check the mentiones ab ovE)

Nyeriah pushed a commit that referenced this pull request Jul 5, 2026
Split from the main PR:
#197

Split by Claude

Unstuck restyled to the new rows; Scroll of Resurrection gets
green/red/yellow status colours; RAF (recruit-a-friend) gets a CSRF
nonce + validation. Minor shared-service tidy.

<img width="1920" height="1030" alt="image"
src="https://github.com/user-attachments/assets/97b69047-870e-41bd-878d-5c52a665384a"
/>

<img width="1920" height="1030" alt="image"
src="https://github.com/user-attachments/assets/dca4c6e3-b29d-4175-a227-84e7d52a730a"
/>

Not testde the RAF page, as the changes were made by Rabbit's review.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Dark mode toggle in WordPress admin bar
  * Character order reset functionality in character management
  * Item quality indicators in mail and restoration displays

* **Improvements**
* Redesigned character management UI with card-based layout, icons, and
metadata
  * Revamped mail return interface with character sidebar navigation
* Reimagined item restoration page with card grid and improved
organization
* Enhanced security with nonce validation and permission checks across
endpoints
  * Improved visual styling with expanded theme and dark mode CSS

* **Bug Fixes**
  * Module availability validation for Resurrection Scroll feature

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Yehonal pushed a commit to azerothcore/acore-cms-wp-plugin that referenced this pull request Jul 5, 2026
Split from the main PR:
azerothcore/acore-cms#197

Split by Claude

Unstuck restyled to the new rows; Scroll of Resurrection gets
green/red/yellow status colours; RAF (recruit-a-friend) gets a CSRF
nonce + validation. Minor shared-service tidy.

<img width="1920" height="1030" alt="image"
src="https://github.com/user-attachments/assets/97b69047-870e-41bd-878d-5c52a665384a"
/>

<img width="1920" height="1030" alt="image"
src="https://github.com/user-attachments/assets/dca4c6e3-b29d-4175-a227-84e7d52a730a"
/>

Not testde the RAF page, as the changes were made by Rabbit's review.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Dark mode toggle in WordPress admin bar
  * Character order reset functionality in character management
  * Item quality indicators in mail and restoration displays

* **Improvements**
* Redesigned character management UI with card-based layout, icons, and
metadata
  * Revamped mail return interface with character sidebar navigation
* Reimagined item restoration page with card grid and improved
organization
* Enhanced security with nonce validation and permission checks across
endpoints
  * Improved visual styling with expanded theme and dark mode CSS

* **Bug Fixes**
  * Module availability validation for Resurrection Scroll feature

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Nyeriah pushed a commit that referenced this pull request Jul 5, 2026
Split from the main PR:
#197

Split by Claude

Full overhaul: confirm prompt before returning, a 6×2 grid matching the
12-item mail cap, shows the recipient's faction/class, Wowhead tooltips
+ higher-res icons, and clean empty/loaded states.

<img width="1920" height="1030" alt="image"
src="https://github.com/user-attachments/assets/e3ed47b2-582e-40ec-a218-6ddc998be498"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added dark mode toggle for WordPress admin dashboard

* **Improvements**
* Redesigned character ordering screen with visual enhancements and
reset order option
  * Redesigned mail return interface with sidebar character navigation
* Enhanced mail display with item quality information and improved
metadata
  * Expanded admin styling and dark mode support across all admin pages

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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