PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 trunk, at includes/core/PluginLogicAdminActions.php

484 lines 16.3 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 // wp_unslash first, matching verifyHandlerNonce() above: this message is
254 // a plugin-authored status string round-tripped through a hidden field,
255 // and several translations contain an apostrophe. WordPress escapes it
256 // on the way back in, so without unslashing the admin sees "Couldn\'t".
257 $message = array_key_exists('display-this-message', $_POST) ?
258 sanitize_text_field(wp_unslash($_POST['display-this-message'])) : '';
259
260 $handler = $this->adminActionRegistry()->resolve((string)$action);
261 if ($handler === null) {
262 return $message;
263 }
264
265 if (!$this->verifyHandlerNonce($handler) || !is_admin()) {
266 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
267 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
268 return $message;
269 }
270
271 return $handler->handle((string)$action, $sub);
272 }
273
274 /**
275 * Run the handler's declared nonce check. Most handlers use
276 * check_admin_referer($action, $arg); the legacy 'updateOptions' branch
277 * uses wp_verify_nonce($_POST[$arg], $action) directly. Behavior is
278 * preserved verbatim per-handler so nonce semantics do not change.
279 *
280 * @param ABJ_404_Solution_AdminActionHandlerInterface $handler
281 * @return bool
282 */
283 private function verifyHandlerNonce(ABJ_404_Solution_AdminActionHandlerInterface $handler): bool {
284 $action = $handler->nonceAction();
285 $arg = $handler->nonceArg();
286 if ($handler->useCheckAdminReferer()) {
287 return (bool)check_admin_referer($action, $arg);
288 }
289 if (!isset($_POST[$arg]) || !is_scalar($_POST[$arg])) {
290 return false;
291 }
292 return (bool)wp_verify_nonce((string)$_POST[$arg], $action);
293 }
294
295 /**
296 * Backward-compatible facade for the trash link action. The real
297 * implementation lives in ABJ_404_Solution_TrashLinkActionHandler.
298 *
299 * The legacy misspelling (hanldeTrashAction) is preserved as an alias
300 * so existing call sites (View.php) and test stubs do not break; new
301 * code should call handleTrashAction().
302 *
303 * @return string
304 */
305 function handleTrashAction() {
306 if ($this->trashLinkHandler === null) {
307 $this->trashLinkHandler = new ABJ_404_Solution_TrashLinkActionHandler($this);
308 }
309 return $this->trashLinkHandler->handle();
310 }
311
312 /** @return string Legacy misspelled alias. Use handleTrashAction(). */
313 function hanldeTrashAction() {
314 return $this->handleTrashAction();
315 }
316
317 /** @return void */
318 function handleActionChangeItemsPerRow(): void {
319 $this->legacyImportActionHandler()->handleActionChangeItemsPerRow();
320 }
321
322 /** @return void */
323 function handleActionExport(): void {
324 $this->legacyImportActionHandler()->handleActionExport();
325 }
326
327 /** @return string|null */
328 function handleActionImportFile() {
329 return $this->legacyImportActionHandler()->handleActionImportFile();
330 }
331
332 /** @return void */
333 function updatePerPageOption(int $rows): void {
334 if ($this->perPageOptionUpdater === null) {
335 $this->perPageOptionUpdater = new ABJ_404_Solution_PerPageOptionUpdater();
336 }
337 $this->perPageOptionUpdater->update($rows);
338 }
339
340 /**
341 * @return string
342 */
343 function handleActionImportRedirects() {
344 return $this->legacyImportActionHandler()->handleActionImportRedirects();
345 }
346
347 /** Delete redirects.
348 * @return string
349 */
350 function handleDeleteAction() {
351 if ($this->deleteRedirectHandler === null) {
352 $this->deleteRedirectHandler = new ABJ_404_Solution_DeleteRedirectActionHandler($this);
353 }
354 return $this->deleteRedirectHandler->handle();
355 }
356
357 /** @return string */
358 function handleIgnoreAction() {
359 return $this->statusLinkActionHandler()->handleIgnoreAction();
360 }
361
362 /** @return string */
363 function handleLaterAction() {
364 return $this->statusLinkActionHandler()->handleLaterAction();
365 }
366
367 /** Edit redirect data.
368 * Facade: real implementation lives in ABJ_404_Solution_EditRedirectHandler.
369 *
370 * @param string $sub
371 * @param string $action
372 * @return string
373 */
374 function handleActionEdit(&$sub, &$action) {
375 if ($this->editRedirectHandler === null) {
376 $this->editRedirectHandler = new ABJ_404_Solution_EditRedirectHandler(
377 $this, $this->redirectFormResolver()
378 );
379 }
380 return $this->editRedirectHandler->handle($sub, $action);
381 }
382
383 /**
384 * Facade: real implementation lives in ABJ_404_Solution_BulkActionHandler.
385 *
386 * @param string $action
387 * @param array<int, int> $ids
388 * @return string
389 */
390 function doBulkAction(string $action, array $ids): string {
391 if ($this->bulkActionHandler === null) {
392 $this->bulkActionHandler = new ABJ_404_Solution_BulkActionHandler($this);
393 }
394 return $this->bulkActionHandler->doBulkAction($action, $ids);
395 }
396
397 /**
398 * @param string $sub
399 * @return void
400 */
401 function doEmptyTrash(string $sub): void {
402 if ($this->trashEmptier === null) {
403 $this->trashEmptier = new ABJ_404_Solution_TrashEmptier($this->dbQuery, $this->viewRead, $this->logger);
404 }
405 $this->trashEmptier->emptyTrash($sub);
406 }
407
408 /**
409 * Facade: real implementation lives in ABJ_404_Solution_EditRedirectHandler.
410 *
411 * @return string
412 */
413 function updateRedirectData() {
414 if ($this->editRedirectHandler === null) {
415 $this->editRedirectHandler = new ABJ_404_Solution_EditRedirectHandler(
416 $this, $this->redirectFormResolver()
417 );
418 }
419 return $this->editRedirectHandler->updateRedirectData();
420 }
421
422 /**
423 * Facade: real implementation lives in ABJ_404_Solution_RedirectFormResolver.
424 *
425 * @return array<string, mixed>
426 */
427 function getRedirectTypeAndDest(): array {
428 return $this->redirectFormResolver()->getRedirectTypeAndDest();
429 }
430
431 /**
432 * Facade: real implementation lives in ABJ_404_Solution_AddRedirectHandler.
433 *
434 * @return string
435 */
436 function addAdminRedirect() {
437 if ($this->addRedirectHandler === null) {
438 $this->addRedirectHandler = new ABJ_404_Solution_AddRedirectHandler(
439 $this, $this->redirectFormResolver()
440 );
441 }
442 return $this->addRedirectHandler->addAdminRedirect();
443 }
444
445 /**
446 * @return string Human-readable result message.
447 */
448 function handleActionUndoRegexAutoPromote() {
449 return $this->legacyImportActionHandler()->handleActionUndoRegexAutoPromote();
450 }
451
452 /**
453 * @return string Human-readable result message.
454 */
455 public function handleActionImportFromPlugin(): string {
456 return $this->legacyImportActionHandler()->handleActionImportFromPlugin();
457 }
458
459 /** @return ABJ_404_Solution_StatusLinkActionHandler */
460 private function statusLinkActionHandler(): ABJ_404_Solution_StatusLinkActionHandler {
461 if ($this->statusLinkHandler === null) {
462 $this->statusLinkHandler = new ABJ_404_Solution_StatusLinkActionHandler($this);
463 }
464 return $this->statusLinkHandler;
465 }
466
467 /** @return ABJ_404_Solution_LegacyImportActionHandler */
468 private function legacyImportActionHandler(): ABJ_404_Solution_LegacyImportActionHandler {
469 if ($this->legacyImportActionHandler === null) {
470 $this->legacyImportActionHandler = new ABJ_404_Solution_LegacyImportActionHandler($this);
471 }
472 return $this->legacyImportActionHandler;
473 }
474
475 /** @return ABJ_404_Solution_AdminActionRegistry */
476 private function adminActionRegistry(): ABJ_404_Solution_AdminActionRegistry {
477 if ($this->actionRegistry === null) {
478 $this->actionRegistry = new ABJ_404_Solution_AdminActionRegistry($this);
479 }
480 return $this->actionRegistry;
481 }
482
483 }
484