PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / core / PluginLogicAdminActions.php

PluginLogicAdminActions.php in 404 Solution 4.3.0, at includes/core/PluginLogicAdminActions.php

480 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Admin action dispatcher + thin facade.
10 *
11 * Two roles:
12 *
13 * 1. Action-name -> handler-class registry. handlePluginAction() routes
14 * POST verbs (addRedirect, updateOptions, emptyRedirectTrash, bulk*, ...)
15 * to the matching class in includes/admin/actions/, centralizing the
16 * nonce + is_admin() guard so it cannot be forgotten when a new action
17 * is added.
18 *
19 * 2. Backward-compatible facade for non-dispatcher entry points
20 * (link-style $_GET actions called from View.php, plus methods kept for
21 * tests that already pin the existing API). Each facade method here is
22 * a one-line delegator to the real implementation in
23 * includes/admin/actions/*Handler.php; this class does not hold the
24 * action logic itself.
25 *
26 * Per-action implementations live in:
27 * - TrashLinkActionHandler (link-style $_GET['trash'])
28 * - EditRedirectHandler (POST editRedirect, plus updateRedirectData)
29 * - AddRedirectHandler (POST addRedirect)
30 * - BulkActionHandler (POST bulk*)
31 * - RedirectFormResolver (shared form parsing + regex auto-promote)
32 * - UpdateOptionsHandler, EmptyRedirectTrashHandler, EmptyCapturedTrashHandler,
33 * PurgeRedirectsHandler, RunMaintenanceHandler, RebuildNgramCacheHandler,
34 * ClearSpellingCacheHandler, SaveGscSettingsHandler, ImportFromPluginHandler,
35 * UndoRegexAutoPromoteHandler
36 *
37 * Standalone class extracted from PluginLogicTrait_AdminActions.
38 */
39 class ABJ_404_Solution_PluginLogicAdminActions {
40
41 /** @var ABJ_404_Solution_Functions */
42 private $f;
43
44 /** @var ABJ_404_Solution_Logging */
45 private $logger;
46
47 /** @var ABJ_404_Solution_RedirectsRepositoryInterface */
48 private $redirectsRepo;
49
50 /** @var ABJ_404_Solution_ViewReadServiceInterface */
51 private $viewRead;
52
53 /** @var ABJ_404_Solution_ContentRepositoryInterface */
54 private $contentRepo;
55
56 /** @var ABJ_404_Solution_DatabaseCoreInterface */
57 private $dbCore;
58
59 /** @var ABJ_404_Solution_DatabaseQueryInterface */
60 private $dbQuery;
61
62 /** @var ABJ_404_Solution_DataAccess */
63 private $dao;
64
65 /** @var ABJ_404_Solution_PluginLogicUrlNormalization */
66 private $urlNormalization;
67
68 /** @var ABJ_404_Solution_PluginLogic */
69 private $pluginLogic;
70
71 /** @var ABJ_404_Solution_AdminActionsDependencies */
72 private $deps;
73
74 /** @var ABJ_404_Solution_RedirectFormResolver|null lazy. */
75 private $formResolver;
76
77 /** @var ABJ_404_Solution_TrashLinkActionHandler|null lazy. */
78 private $trashLinkHandler;
79
80 /** @var ABJ_404_Solution_EditRedirectHandler|null lazy. */
81 private $editRedirectHandler;
82
83 /** @var ABJ_404_Solution_AddRedirectHandler|null lazy. */
84 private $addRedirectHandler;
85
86 /** @var ABJ_404_Solution_BulkActionHandler|null lazy. */
87 private $bulkActionHandler;
88
89 /** @var ABJ_404_Solution_DeleteRedirectActionHandler|null lazy. */
90 private $deleteRedirectHandler;
91
92 /** @var ABJ_404_Solution_StatusLinkActionHandler|null lazy. */
93 private $statusLinkHandler;
94
95 /** @var ABJ_404_Solution_TrashEmptier|null lazy. */
96 private $trashEmptier;
97
98 /** @var ABJ_404_Solution_LegacyImportActionHandler|null lazy. */
99 private $legacyImportActionHandler;
100
101 /** @var ABJ_404_Solution_PerPageOptionUpdater|null lazy. */
102 private $perPageOptionUpdater;
103
104 /** @var ABJ_404_Solution_AdminActionRegistry|null lazy. */
105 private $actionRegistry;
106
107 /**
108 * Construct with a single typed dependency bundle. Replaces a
109 * 10-positional-parameter signature (audit source design-audit-2026-05-29.md,
110 * criterion 220 Interface Size). Internal field layout is unchanged; only
111 * the constructor interface narrows.
112 *
113 * @param ABJ_404_Solution_AdminActionsDependencies $deps
114 */
115 function __construct(ABJ_404_Solution_AdminActionsDependencies $deps) {
116 $this->f = $deps->getFunctions();
117 $this->logger = $deps->getLogger();
118 $this->redirectsRepo = $deps->getRedirectsRepo();
119 $this->viewRead = $deps->getViewRead();
120 $this->contentRepo = $deps->getContentRepo();
121 $this->dbCore = $deps->getDbCore();
122 $this->dbQuery = $deps->getDbQuery();
123 $this->dao = $deps->getDao();
124 $this->urlNormalization = $deps->getUrlNormalization();
125 $this->pluginLogic = $deps->getPluginLogic();
126 $this->deps = $deps;
127 }
128
129 /**
130 * Accessors used by ABJ_404_Solution_AdminActionHandlerInterface implementations
131 * under includes/admin/actions/. The dispatcher passes $this to each handler
132 * (constructor injection) so handlers can reach shared collaborators without
133 * each handler getting its own 10-argument constructor.
134 *
135 * @return ABJ_404_Solution_Functions
136 */
137 public function getFunctions() {
138 return $this->f;
139 }
140
141 /** @return ABJ_404_Solution_Logging */
142 public function getLogger() {
143 return $this->logger;
144 }
145
146 /** @return ABJ_404_Solution_RedirectsRepositoryInterface */
147 public function getRedirectsRepo() {
148 return $this->redirectsRepo;
149 }
150
151 /** @return ABJ_404_Solution_ContentRepositoryInterface */
152 public function getContentRepo() {
153 return $this->contentRepo;
154 }
155
156 /** @return ABJ_404_Solution_ViewReadServiceInterface */
157 public function getViewRead() {
158 return $this->viewRead;
159 }
160
161 /** @return ABJ_404_Solution_DatabaseCoreInterface */
162 public function getDbCore() {
163 return $this->dbCore;
164 }
165
166 /** @return ABJ_404_Solution_DatabaseQueryInterface */
167 public function getDbQuery() {
168 return $this->dbQuery;
169 }
170
171 /** @return ABJ_404_Solution_DataAccess */
172 public function getDao() {
173 return $this->dao;
174 }
175
176 /** @return ABJ_404_Solution_PluginLogicUrlNormalization */
177 public function getUrlNormalization() {
178 return $this->urlNormalization;
179 }
180
181 /** @return ABJ_404_Solution_PluginLogic */
182 public function getPluginLogic() {
183 return $this->pluginLogic;
184 }
185
186 /** @return ABJ_404_Solution_AdminActionsDependencies */
187 public function getDependencies() {
188 return $this->deps;
189 }
190
191 /**
192 * Shared by Add and Edit redirect handlers. Lazy so construction stays
193 * cheap when admin actions never fire on this request.
194 *
195 * @return ABJ_404_Solution_RedirectFormResolver
196 */
197 public function redirectFormResolver(): ABJ_404_Solution_RedirectFormResolver {
198 if ($this->formResolver === null) {
199 $this->formResolver = new ABJ_404_Solution_RedirectFormResolver(
200 $this->f, $this->logger, $this->urlNormalization
201 );
202 }
203 return $this->formResolver;
204 }
205
206 /**
207 * Verify a nonce for admin-link actions, without depending on the browser's Referer header.
208 * Public so handlers under includes/admin/actions/ that respond to GET-link
209 * actions (TrashLinkActionHandler, EditRedirectHandler) can reuse the
210 * same nonce primitive used by the legacy methods on this class.
211 *
212 * @param string $action Nonce action string used in wp_nonce_url()
213 * @param string $queryArg Nonce query arg name (default '_wpnonce')
214 * @return bool
215 */
216 public function verifyLinkNonce($action, $queryArg = '_wpnonce') {
217 if (function_exists('check_admin_referer')) {
218 $ok = check_admin_referer($action, $queryArg);
219 if ($ok) {
220 return true;
221 }
222 }
223
224 if (!function_exists('wp_verify_nonce')) {
225 return false;
226 }
227
228 if (!isset($_REQUEST[$queryArg])) {
229 return false;
230 }
231
232 $nonce = sanitize_text_field(wp_unslash($_REQUEST[$queryArg]));
233 if ($nonce === '') {
234 return false;
235 }
236
237 return wp_verify_nonce($nonce, $action) !== false;
238 }
239
240 /** Do the passed in action and return the associated message.
241 *
242 * Dispatches to a handler in includes/admin/actions/ via the registry above.
243 * Nonce verification + is_admin() guard are centralized here so they cannot
244 * be forgotten when a new action is added. Unknown actions are a no-op that
245 * returns the pre-populated display-this-message (preserving pre-refactor
246 * behavior of the original 12-branch if/else chain).
247 *
248 * @param string $action
249 * @param string $sub
250 * @return string
251 */
252 function handlePluginAction($action, &$sub) {
253 $message = array_key_exists('display-this-message', $_POST) ?
254 sanitize_text_field($_POST['display-this-message']) : '';
255
256 $handler = $this->adminActionRegistry()->resolve((string)$action);
257 if ($handler === null) {
258 return $message;
259 }
260
261 if (!$this->verifyHandlerNonce($handler) || !is_admin()) {
262 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
263 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
264 return $message;
265 }
266
267 return $handler->handle((string)$action, $sub);
268 }
269
270 /**
271 * Run the handler's declared nonce check. Most handlers use
272 * check_admin_referer($action, $arg); the legacy 'updateOptions' branch
273 * uses wp_verify_nonce($_POST[$arg], $action) directly. Behavior is
274 * preserved verbatim per-handler so nonce semantics do not change.
275 *
276 * @param ABJ_404_Solution_AdminActionHandlerInterface $handler
277 * @return bool
278 */
279 private function verifyHandlerNonce(ABJ_404_Solution_AdminActionHandlerInterface $handler): bool {
280 $action = $handler->nonceAction();
281 $arg = $handler->nonceArg();
282 if ($handler->useCheckAdminReferer()) {
283 return (bool)check_admin_referer($action, $arg);
284 }
285 if (!isset($_POST[$arg]) || !is_scalar($_POST[$arg])) {
286 return false;
287 }
288 return (bool)wp_verify_nonce((string)$_POST[$arg], $action);
289 }
290
291 /**
292 * Backward-compatible facade for the trash link action. The real
293 * implementation lives in ABJ_404_Solution_TrashLinkActionHandler.
294 *
295 * The legacy misspelling (hanldeTrashAction) is preserved as an alias
296 * so existing call sites (View.php) and test stubs do not break; new
297 * code should call handleTrashAction().
298 *
299 * @return string
300 */
301 function handleTrashAction() {
302 if ($this->trashLinkHandler === null) {
303 $this->trashLinkHandler = new ABJ_404_Solution_TrashLinkActionHandler($this);
304 }
305 return $this->trashLinkHandler->handle();
306 }
307
308 /** @return string Legacy misspelled alias. Use handleTrashAction(). */
309 function hanldeTrashAction() {
310 return $this->handleTrashAction();
311 }
312
313 /** @return void */
314 function handleActionChangeItemsPerRow(): void {
315 $this->legacyImportActionHandler()->handleActionChangeItemsPerRow();
316 }
317
318 /** @return void */
319 function handleActionExport(): void {
320 $this->legacyImportActionHandler()->handleActionExport();
321 }
322
323 /** @return string|null */
324 function handleActionImportFile() {
325 return $this->legacyImportActionHandler()->handleActionImportFile();
326 }
327
328 /** @return void */
329 function updatePerPageOption(int $rows): void {
330 if ($this->perPageOptionUpdater === null) {
331 $this->perPageOptionUpdater = new ABJ_404_Solution_PerPageOptionUpdater();
332 }
333 $this->perPageOptionUpdater->update($rows);
334 }
335
336 /**
337 * @return string
338 */
339 function handleActionImportRedirects() {
340 return $this->legacyImportActionHandler()->handleActionImportRedirects();
341 }
342
343 /** Delete redirects.
344 * @return string
345 */
346 function handleDeleteAction() {
347 if ($this->deleteRedirectHandler === null) {
348 $this->deleteRedirectHandler = new ABJ_404_Solution_DeleteRedirectActionHandler($this);
349 }
350 return $this->deleteRedirectHandler->handle();
351 }
352
353 /** @return string */
354 function handleIgnoreAction() {
355 return $this->statusLinkActionHandler()->handleIgnoreAction();
356 }
357
358 /** @return string */
359 function handleLaterAction() {
360 return $this->statusLinkActionHandler()->handleLaterAction();
361 }
362
363 /** Edit redirect data.
364 * Facade: real implementation lives in ABJ_404_Solution_EditRedirectHandler.
365 *
366 * @param string $sub
367 * @param string $action
368 * @return string
369 */
370 function handleActionEdit(&$sub, &$action) {
371 if ($this->editRedirectHandler === null) {
372 $this->editRedirectHandler = new ABJ_404_Solution_EditRedirectHandler(
373 $this, $this->redirectFormResolver()
374 );
375 }
376 return $this->editRedirectHandler->handle($sub, $action);
377 }
378
379 /**
380 * Facade: real implementation lives in ABJ_404_Solution_BulkActionHandler.
381 *
382 * @param string $action
383 * @param array<int, int> $ids
384 * @return string
385 */
386 function doBulkAction(string $action, array $ids): string {
387 if ($this->bulkActionHandler === null) {
388 $this->bulkActionHandler = new ABJ_404_Solution_BulkActionHandler($this);
389 }
390 return $this->bulkActionHandler->doBulkAction($action, $ids);
391 }
392
393 /**
394 * @param string $sub
395 * @return void
396 */
397 function doEmptyTrash(string $sub): void {
398 if ($this->trashEmptier === null) {
399 $this->trashEmptier = new ABJ_404_Solution_TrashEmptier($this->dbQuery, $this->viewRead, $this->logger);
400 }
401 $this->trashEmptier->emptyTrash($sub);
402 }
403
404 /**
405 * Facade: real implementation lives in ABJ_404_Solution_EditRedirectHandler.
406 *
407 * @return string
408 */
409 function updateRedirectData() {
410 if ($this->editRedirectHandler === null) {
411 $this->editRedirectHandler = new ABJ_404_Solution_EditRedirectHandler(
412 $this, $this->redirectFormResolver()
413 );
414 }
415 return $this->editRedirectHandler->updateRedirectData();
416 }
417
418 /**
419 * Facade: real implementation lives in ABJ_404_Solution_RedirectFormResolver.
420 *
421 * @return array<string, mixed>
422 */
423 function getRedirectTypeAndDest(): array {
424 return $this->redirectFormResolver()->getRedirectTypeAndDest();
425 }
426
427 /**
428 * Facade: real implementation lives in ABJ_404_Solution_AddRedirectHandler.
429 *
430 * @return string
431 */
432 function addAdminRedirect() {
433 if ($this->addRedirectHandler === null) {
434 $this->addRedirectHandler = new ABJ_404_Solution_AddRedirectHandler(
435 $this, $this->redirectFormResolver()
436 );
437 }
438 return $this->addRedirectHandler->addAdminRedirect();
439 }
440
441 /**
442 * @return string Human-readable result message.
443 */
444 function handleActionUndoRegexAutoPromote() {
445 return $this->legacyImportActionHandler()->handleActionUndoRegexAutoPromote();
446 }
447
448 /**
449 * @return string Human-readable result message.
450 */
451 public function handleActionImportFromPlugin(): string {
452 return $this->legacyImportActionHandler()->handleActionImportFromPlugin();
453 }
454
455 /** @return ABJ_404_Solution_StatusLinkActionHandler */
456 private function statusLinkActionHandler(): ABJ_404_Solution_StatusLinkActionHandler {
457 if ($this->statusLinkHandler === null) {
458 $this->statusLinkHandler = new ABJ_404_Solution_StatusLinkActionHandler($this);
459 }
460 return $this->statusLinkHandler;
461 }
462
463 /** @return ABJ_404_Solution_LegacyImportActionHandler */
464 private function legacyImportActionHandler(): ABJ_404_Solution_LegacyImportActionHandler {
465 if ($this->legacyImportActionHandler === null) {
466 $this->legacyImportActionHandler = new ABJ_404_Solution_LegacyImportActionHandler($this);
467 }
468 return $this->legacyImportActionHandler;
469 }
470
471 /** @return ABJ_404_Solution_AdminActionRegistry */
472 private function adminActionRegistry(): ABJ_404_Solution_AdminActionRegistry {
473 if ($this->actionRegistry === null) {
474 $this->actionRegistry = new ABJ_404_Solution_AdminActionRegistry($this);
475 }
476 return $this->actionRegistry;
477 }
478
479 }
480