Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions cron/fix-double-deleted-emails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

if (!array_key_exists('HTTP_X_APPENGINE_CRON', $_SERVER) && 'dropapp_dev' != $settings['db_database']) {
throw new Exception('Not called from AppEngine cron service');
}

$permittedDatabases = ['dropapp_dev', 'dropapp_demo', 'dropapp_staging', 'dropapp_production'];
if (!in_array($settings['db_database'], $permittedDatabases)) {
throw new Exception('Not permitting to run fix-double-deleted-emails for '.$settings['db_database']);
}

$bypassAuthentication = true;

require_once 'library/core.php';

// Fix double-deleted email suffixes
// When a user is deleted twice, the email becomes: email.deleted.ID.deleted.ID
// This cron job removes the duplicate suffixes so reactivation works correctly

$result = db_query("
SELECT id, email
FROM cms_users
WHERE deleted IS NOT NULL
AND email REGEXP '@.*\.deleted\.[0-9]+.*\.deleted\.[0-9]+'
");

$fixed_count = 0;
while ($row = db_fetch($result)) {
$email = $row['email'];
$id = $row['id'];

// Remove all .deleted.ID suffixes
$pattern = '/\.deleted\.\d+/';
$clean_email = preg_replace($pattern, '', $email);

// Add back a single .deleted.ID suffix
$corrected_email = $clean_email.'.deleted.'.$id;

db_query(
'UPDATE cms_users SET email = :corrected_email WHERE id = :id',
['corrected_email' => $corrected_email, 'id' => $id]
);

simpleSaveChangeHistory('cms_users', $id, 'Fixed double-deleted email suffix via cron');

++$fixed_count;
}

echo json_encode([
'success' => true,
'message' => "Fixed {$fixed_count} email(s) with double-deleted suffixes",
]);
127 changes: 127 additions & 0 deletions cypress/e2e/2_auth_tests/2_9_DoubleDeletedEmailFix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const DELETED_USER_NAME = "Deleted User";
const DELETED_USER_EMAIL = "deleted@deleted.co";
const ARE_YOU_SURE_POPUP = "Are you sure?";
const OK_BUTTON = "OK";
const ITEM_RECOVERED = "Item recovered";
const ITEM_DELETED = "Item deleted";
const USER_DEACTIVATE_REQUEST = "do=delete";
const USER_REACTIVATE_REQUEST = "do=undelete";

describe("2_9_DoubleDeletedEmailFix_Test", () => {

beforeEach(function () {
cy.setupAjaxActionHook();
cy.loginAsAdmin();
});

it("2_9_1 Verify deleted user can be reactivated normally", () => {
// This test ensures the normal reactivation flow works
// It serves as a baseline for the double-deletion scenario
cy.visit('/?action=cms_users_deactivated');

cy.checkGridCheckboxByText(DELETED_USER_NAME);
cy.clickOnElementByTypeAndTestId("button", "reactivate-cms-user");
cy.checkElementIsVisibleByText("h3", ARE_YOU_SURE_POPUP);
cy.clickOnElementBySelectorAndText("a", OK_BUTTON);
cy.waitForAjaxAction(USER_REACTIVATE_REQUEST, ITEM_RECOVERED);

cy.checkElementIsVisibleByText("span", ITEM_RECOVERED);
cy.checkElementDoesNotExistByText("p", DELETED_USER_NAME);

// Verify user appears in Active Users
cy.visit('/?action=cms_users');
cy.checkElementIsVisibleByText("a", DELETED_USER_NAME);

// Verify email is correct (without double suffix)
cy.get('body').should('contain', DELETED_USER_EMAIL);

// Delete the user again to restore test state
cy.checkGridCheckboxByText(DELETED_USER_NAME);
cy.clickOnElementByTypeAndTestId("button", "list-delete-button");
cy.clickOnElementBySelectorAndText("div.popover-content a", "Deactivate");
cy.waitForAjaxAction(USER_DEACTIVATE_REQUEST, ITEM_DELETED);
});

it("2_9_2 Verify prevention fix - delete, reactivate, delete again cycle", () => {
// This test verifies our fix by doing multiple delete/reactivate cycles
// Without the fix, repeated deletions would append multiple .deleted suffixes
// With our fix, the email should always have only ONE .deleted suffix

// Start: User is already deleted (from previous test)
cy.visit('/?action=cms_users_deactivated');
cy.checkElementIsVisibleByText("p", DELETED_USER_NAME);

// Cycle 1: Reactivate user
cy.checkGridCheckboxByText(DELETED_USER_NAME);
cy.clickOnElementByTypeAndTestId("button", "reactivate-cms-user");
cy.checkElementIsVisibleByText("h3", ARE_YOU_SURE_POPUP);
cy.clickOnElementBySelectorAndText("a", OK_BUTTON);
cy.waitForAjaxAction(USER_REACTIVATE_REQUEST, ITEM_RECOVERED);

// Cycle 1: Delete user again
cy.visit('/?action=cms_users');
cy.checkGridCheckboxByText(DELETED_USER_NAME);
cy.clickOnElementByTypeAndTestId("button", "list-delete-button");
cy.clickOnElementBySelectorAndText("div.popover-content a", "Deactivate");
cy.waitForAjaxAction(USER_DEACTIVATE_REQUEST, ITEM_DELETED);

// Cycle 2: Reactivate again (this tests that the email still works after first cycle)
cy.visit('/?action=cms_users_deactivated');
cy.checkGridCheckboxByText(DELETED_USER_NAME);
cy.clickOnElementByTypeAndTestId("button", "reactivate-cms-user");
cy.checkElementIsVisibleByText("h3", ARE_YOU_SURE_POPUP);
cy.clickOnElementBySelectorAndText("a", OK_BUTTON);
cy.waitForAjaxAction(USER_REACTIVATE_REQUEST, ITEM_RECOVERED);

// Cycle 2: Delete user again
cy.visit('/?action=cms_users');
cy.checkGridCheckboxByText(DELETED_USER_NAME);
cy.clickOnElementByTypeAndTestId("button", "list-delete-button");
cy.clickOnElementBySelectorAndText("div.popover-content a", "Deactivate");
cy.waitForAjaxAction(USER_DEACTIVATE_REQUEST, ITEM_DELETED);

// Final verification: If all cycles succeeded, our fix is working
// Without the fix, the second reactivation would fail due to double .deleted suffix
cy.visit('/?action=cms_users_deactivated');
cy.checkElementIsVisibleByText("p", DELETED_USER_NAME);
cy.log('SUCCESS: User went through multiple delete/reactivate cycles without double-deletion issue');
});
});

/*
* MANUAL TEST INSTRUCTIONS FOR DOUBLE-DELETED EMAIL SCENARIO:
*
* To test the double-deletion bug fix manually:
*
* 1. Create a double-deleted email in the database:
* ```sql
* UPDATE cms_users
* SET email = CONCAT(email, '.deleted.', id)
* WHERE id = 100000005
* AND email LIKE '%.deleted.%';
* ```
* This simulates the bug where a user was deleted twice.
*
* 2. Verify the email now has double suffix:
* ```sql
* SELECT id, email FROM cms_users WHERE id = 100000005;
* ```
* Should show: deleted@deleted.co.deleted.100000005.deleted.100000005
*
* 3. Run the fix cron job:
* Visit: http://localhost:8100/cron/fix-double-deleted-emails.php
* (Make sure db_database is 'dropapp_dev' in config)
*
* 4. Verify the email is fixed:
* ```sql
* SELECT id, email FROM cms_users WHERE id = 100000005;
* ```
* Should show: deleted@deleted.co.deleted.100000005
*
* 5. Try to reactivate the user through the UI:
* - Go to Manage Users > Deactivated tab
* - Select "Deleted User"
* - Click "Activate"
* - Should succeed without errors
* - Email should be: deleted@deleted.co
*/
1 change: 1 addition & 0 deletions gcloud-entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function () use ($parsedUrl) {
case '/cron/dailyroutine.php':
case '/cron/reseed-auth0.php':
case '/cron/reseed-roles-auth0.php':
case '/cron/fix-double-deleted-emails.php':
case '/fake-error.php':
require substr($parsedUrl, 1); // trim /

Expand Down
5 changes: 4 additions & 1 deletion include/cms_users_handle_ajax_operations.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
[$success, $message, $redirect] = listDelete($table, $ids);
if ($success) {
foreach ($ids as $id) {
db_query('UPDATE '.$table.' SET email = CONCAT(email,".deleted.",id) WHERE id = :id', ['id' => $id]);
// Only append .deleted suffix if the email doesn't already have it
// This prevents double-deletion when someone tries to delete an already-deleted user
// Pattern checks for .deleted.<digits> after the @ symbol (e.g., user@domain.com.deleted.123)
db_query('UPDATE '.$table.' SET email = CONCAT(email,".deleted.",id) WHERE id = :id AND email NOT REGEXP "@.*\.deleted\.[0-9]+$"', ['id' => $id]);
updateAuth0UserFromDb($id);
}
}
Expand Down
5 changes: 4 additions & 1 deletion library/ajax/deleteprofile.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?php

db_transaction(function () {
db_query('UPDATE cms_users SET deleted = NOW(), email = CONCAT(email,".deleted.",id) WHERE id = :id', ['id' => $_POST['cms_user_id']]);
// Only append .deleted suffix if the email doesn't already have it
// This prevents double-deletion if the query is somehow executed twice
// Pattern checks for .deleted.<digits> after the @ symbol (e.g., user@domain.com.deleted.123)
db_query('UPDATE cms_users SET deleted = NOW(), email = CONCAT(email,".deleted.",id) WHERE id = :id AND (NOT deleted OR deleted IS NULL) AND email NOT REGEXP "@.*\.deleted\.[0-9]+$"', ['id' => $_POST['cms_user_id']]);
updateAuth0UserFromDb($_POST['cms_user_id']);
});

Expand Down