| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Handles any action whose verb starts with 'bulk' (bulktrash, |
| 9 |
* bulk_trash_delete_permanently, bulk_trash_restore, bulkignore, bulklater, |
| 10 |
* bulkcaptured). Validates that $_POST['idnum'] is present (echoes an error |
| 11 |
* and returns '' if not, matching pre-refactor behavior) and that it does not |
| 12 |
* exceed MAX_BULK_ACTION_IDS (echoes an error and returns '' without running |
| 13 |
* any mutation if it does), then iterates ids and applies the per-row |
| 14 |
* status/trash/delete operation. |
| 15 |
* |
| 16 |
* Registered in the dispatcher's prefix-match map (not the exact-match map). |
| 17 |
* |
| 18 |
* The per-row dispatch logic previously lived on |
| 19 |
* PluginLogicAdminActions::doBulkAction() (M201, design-audit-2026-06-02). |
| 20 |
* That parent method is now a thin shim that calls doBulkAction() here so |
| 21 |
* existing tests/callers do not break. |
| 22 |
*/ |
| 23 |
class ABJ_404_Solution_BulkActionHandler implements ABJ_404_Solution_AdminActionHandlerInterface { |
| 24 |
|
| 25 |
/** |
| 26 |
* Maximum number of ids accepted in a single bulk action POST. The |
| 27 |
* admin redirects/captured tables only ever render bulk-select |
| 28 |
* checkboxes for rows on the CURRENT page (see |
| 29 |
* includes/html/tableRowPageRedirects.html and |
| 30 |
* tableRowCapturedURLs.html) -- there is no "select all across pages" |
| 31 |
* feature -- so the largest legitimate submission is bounded by the |
| 32 |
* table's own page-size cap (ABJ404_OPTION_MAX_PERPAGE = 500, defined |
| 33 |
* in includes/Loader.php). This constant sits well above that (2x |
| 34 |
* headroom) so a legitimate max-page-size selection always passes, |
| 35 |
* while a forged or accidental oversized POST (e.g. 100,000 ids) |
| 36 |
* cannot monopolize PHP execution time or hammer the database with |
| 37 |
* that many mutation queries in a single request. |
| 38 |
*/ |
| 39 |
const MAX_BULK_ACTION_IDS = 1000; |
| 40 |
|
| 41 |
/** @var ABJ_404_Solution_PluginLogicAdminActions */ |
| 42 |
private $parent; |
| 43 |
|
| 44 |
public function __construct(ABJ_404_Solution_PluginLogicAdminActions $parent) { |
| 45 |
$this->parent = $parent; |
| 46 |
} |
| 47 |
|
| 48 |
public function nonceAction(): string { |
| 49 |
return 'abj404_bulkProcess'; |
| 50 |
} |
| 51 |
|
| 52 |
public function nonceArg(): string { |
| 53 |
return '_wpnonce'; |
| 54 |
} |
| 55 |
|
| 56 |
public function useCheckAdminReferer(): bool { |
| 57 |
return true; |
| 58 |
} |
| 59 |
|
| 60 |
public function handle(string $action, string &$sub): string { |
| 61 |
if (!isset($_POST['idnum']) || !is_array($_POST['idnum'])) { |
| 62 |
$this->parent->getLogger()->debugMessage( |
| 63 |
"No ID(s) specified for bulk action: " . esc_html($action)); |
| 64 |
echo sprintf(__("Error: No ID(s) specified for bulk action. (%s)", '404-solution'), |
| 65 |
esc_html($action)); |
| 66 |
return ''; |
| 67 |
} |
| 68 |
if (count($_POST['idnum']) > self::MAX_BULK_ACTION_IDS) { |
| 69 |
$this->parent->getLogger()->debugMessage( |
| 70 |
"Too many ID(s) specified for bulk action (" . count($_POST['idnum']) . |
| 71 |
", max " . self::MAX_BULK_ACTION_IDS . "): " . esc_html($action)); |
| 72 |
echo sprintf( |
| 73 |
__("Error: Too many ID(s) specified for bulk action (maximum %d). (%s)", '404-solution'), |
| 74 |
self::MAX_BULK_ACTION_IDS, esc_html($action)); |
| 75 |
return ''; |
| 76 |
} |
| 77 |
$ids = array(); |
| 78 |
foreach ($_POST['idnum'] as $rawId) { |
| 79 |
if (is_scalar($rawId)) { |
| 80 |
$ids[] = absint($rawId); |
| 81 |
} |
| 82 |
} |
| 83 |
$message = $this->doBulkAction($action, $ids); |
| 84 |
return $message; |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Iterate the given ids and apply the per-row operation implied by the |
| 89 |
* bulk action verb. Returns a human-readable summary. Public so the |
| 90 |
* legacy PluginLogicAdminActions::doBulkAction() shim and existing tests |
| 91 |
* can call it directly without going through handle()'s POST gate. |
| 92 |
* |
| 93 |
* @param string $action |
| 94 |
* @param array<int, int> $ids |
| 95 |
* @return string |
| 96 |
*/ |
| 97 |
public function doBulkAction(string $action, array $ids): string { |
| 98 |
$message = ""; |
| 99 |
$logger = $this->parent->getLogger(); |
| 100 |
$redirectsRepo = $this->parent->getRedirectsRepo(); |
| 101 |
|
| 102 |
$logger->debugMessage("In doBulkAction. Action: " . |
| 103 |
esc_html($action == '' ? '(none)' : $action) . ", ids: " . wp_kses_post((string)json_encode($ids))); |
| 104 |
|
| 105 |
if ($action == "bulkignore" || $action == "bulkcaptured" || $action == "bulklater" || |
| 106 |
$action == "bulk_trash_restore") { |
| 107 |
|
| 108 |
$status = 0; |
| 109 |
if ($action == "bulkignore") { |
| 110 |
$status = ABJ404_STATUS_IGNORED; |
| 111 |
} else if ($action == "bulkcaptured") { |
| 112 |
$status = ABJ404_STATUS_CAPTURED; |
| 113 |
} else if ($action == "bulklater") { |
| 114 |
$status = ABJ404_STATUS_LATER; |
| 115 |
} |
| 116 |
|
| 117 |
$count = 0; |
| 118 |
foreach ($ids as $id) { |
| 119 |
$s = $redirectsRepo->moveRedirectsToTrash($id, 0); |
| 120 |
if ($action != "bulk_trash_restore") { |
| 121 |
$s = $redirectsRepo->updateRedirectTypeStatus($id, (string)$status); |
| 122 |
} |
| 123 |
if ($s == "") { |
| 124 |
$count++; |
| 125 |
} |
| 126 |
} |
| 127 |
if ($action == "bulkignore") { |
| 128 |
$message = $count . " " . __('URL(s) marked as Ignored.', '404-solution'); |
| 129 |
} else if ($action == "bulkcaptured") { |
| 130 |
$message = $count . " " . __('URL(s) marked as Captured.', '404-solution'); |
| 131 |
} else if ($action == "bulklater") { |
| 132 |
$message = $count . " " . __('URL(s) marked as Later.', '404-solution'); |
| 133 |
} else { |
| 134 |
$message = $count . " " . __('URL(s) restored.', '404-solution'); |
| 135 |
} |
| 136 |
|
| 137 |
} else if ($action == "bulk_trash_delete_permanently") { |
| 138 |
$count = 0; |
| 139 |
foreach ($ids as $id) { |
| 140 |
$redirectsRepo->deleteRedirect(absint($id)); |
| 141 |
$count ++; |
| 142 |
} |
| 143 |
$message = $count . " " . __('URL(s) deleted', '404-solution'); |
| 144 |
|
| 145 |
} else if ($action == "bulktrash") { |
| 146 |
$count = 0; |
| 147 |
foreach ($ids as $id) { |
| 148 |
$s = $redirectsRepo->moveRedirectsToTrash($id, 1); |
| 149 |
if ($s == "") { |
| 150 |
$count ++; |
| 151 |
} |
| 152 |
} |
| 153 |
$message = $count . " " . __('URL(s) moved to trash', '404-solution'); |
| 154 |
|
| 155 |
} else { |
| 156 |
$logger->errorMessage("Unrecognized bulk action: " . esc_html($action)); |
| 157 |
echo sprintf(__("Error: Unrecognized bulk action. (%s)", '404-solution'), esc_html($action)); |
| 158 |
} |
| 159 |
return $message; |
| 160 |
} |
| 161 |
} |
| 162 |
|