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 / view / RedirectEditDeadEndRenderer.php

RedirectEditDeadEndRenderer.php in 404 Solution trunk, at includes/view/RedirectEditDeadEndRenderer.php

109 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * What the Edit Redirect screen shows when there is nothing to edit.
9 *
10 * Three ways the screen can arrive with no form to render -- no usable id at
11 * all, ids whose rows no longer exist, and a selection larger than the plugin
12 * will carry -- and one contract shared by all of them:
13 *
14 * 1. a stock WordPress notice, never a bare echoed string;
15 * 2. a link back to the list the admin came from, so the dead end is
16 * recoverable in one click;
17 * 3. a log line BELOW error level.
18 *
19 * Point 3 is the one with history. DebugLogReader::getLatestErrorLine() keys on
20 * the (ERROR) token to decide whether to mail the maintainer an error report,
21 * and none of these three is a plugin failure: a well-formed id that no longer
22 * resolves is a normal request condition (another admin deletes the row, this
23 * admin trashes it in a second tab, deleteOldRedirectsCron removes it on its
24 * own schedule) while an already-rendered list page still carries the Edit
25 * link. The plugin keeps working, which by defensive-coding rule #8 makes it a
26 * non-error. Logging it at error level mailed a bug report for a stale link
27 * (production report 349, plugin 4.3.4).
28 *
29 * A genuine database failure behind the same empty result is not hidden by
30 * this: DatabaseQueryExecutor::queryAndGetResults() is the centralized error
31 * handler and has already logged it via sqlErrorReporter.
32 *
33 * Split out of ABJ_404_Solution_View_Redirects, which owns the screen's happy
34 * path (request -> context -> record content -> form). These live together
35 * instead because the contract above is what must not drift between them, and
36 * it had no single home: each dead end carried its own copy of the reasoning
37 * and a third was about to carry a third copy.
38 */
39 class ABJ_404_Solution_RedirectEditDeadEndRenderer {
40
41 /** @var ABJ_404_Solution_RedirectEditFormPresenter */
42 private $presenter;
43
44 /** @var ABJ_404_Solution_Logging */
45 private $logger;
46
47 /**
48 * @param ABJ_404_Solution_RedirectEditFormPresenter $presenter Builds the notice markup.
49 * @param ABJ_404_Solution_Logging $logger Receives the below-error-level line.
50 */
51 public function __construct($presenter, $logger) {
52 $this->presenter = $presenter;
53 $this->logger = $logger;
54 }
55
56 /**
57 * The requested ids no longer have a row -- or the request named no usable
58 * id at all, which is a different message: naming an empty id list would
59 * print "Redirects were not found."
60 *
61 * Takes the page context rather than resolving the destination again, so
62 * the notice's way back can never disagree with the way back the edit form
63 * itself would have offered.
64 *
65 * @param array{backUrl: string, backLabel: string} $context From editRedirectPageContext().
66 * @param array<int, int> $ids The redirect ids the request asked for. Empty
67 * when the request carried no usable id at all.
68 * @return null Always null, so a caller can `return $renderer->missingRedirects(...)`.
69 */
70 public function missingRedirects(array $context, array $ids) {
71 $backUrl = $context['backUrl'];
72 $backLabel = $context['backLabel'];
73
74 if (empty($ids)) {
75 echo $this->presenter->buildNoRedirectIdsNoticeHtml($backUrl, $backLabel);
76 $this->logger->debugMessage('Edit redirect page: request carried no usable redirect id.');
77 return null;
78 }
79
80 echo $this->presenter->buildMissingRedirectsNoticeHtml($ids, $backUrl, $backLabel);
81 $this->logger->debugMessage('Edit redirect page: no redirect row exists for requested id(s): ' .
82 esc_html(implode(', ', array_map('strval', $ids))));
83
84 return null;
85 }
86
87 /**
88 * The selection is larger than one edit screen may carry, so it is refused.
89 *
90 * RedirectEditRequest's cap bounds the work one request can cause, but a
91 * bound the admin cannot see is a silent partial edit: they selected N, the
92 * screen would render MAX_SELECTED_IDS of them with nothing marking the
93 * boundary, and the save that follows applies to exactly that subset.
94 *
95 * @param array{backUrl: string, backLabel: string} $context From editRedirectPageContext().
96 * @param int $requestedCount How many redirects the request actually named.
97 * @return null Always null, so a caller can `return $renderer->tooManySelected(...)`.
98 */
99 public function tooManySelected(array $context, int $requestedCount) {
100 $maximum = ABJ_404_Solution_RedirectEditRequest::MAX_SELECTED_IDS;
101 echo $this->presenter->buildTooManySelectedNoticeHtml(
102 $requestedCount, $maximum, $context['backUrl'], $context['backLabel']);
103 $this->logger->debugMessage('Edit redirect page: refused a selection of ' . $requestedCount
104 . ' redirects; the maximum is ' . $maximum . '.');
105
106 return null;
107 }
108 }
109