PluginProbe
404 Solution / 4.3.3
404 Solution v4.3.3
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 / View_RedirectsTable.php

View_RedirectsTable.php in 404 Solution 4.3.3, at includes/view/View_RedirectsTable.php

281 lines 12.0 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 require_once __DIR__ . '/../admin/RedirectsTableColumns.php';
8 require_once __DIR__ . '/../admin/RedirectDestinationWarningPolicy.php';
9 require_once __DIR__ . '/../admin/RedirectDestinationLinkResolver.php';
10 require_once __DIR__ . '/../admin/RedirectRowActionsPresenter.php';
11 require_once __DIR__ . '/../admin/RedirectRowPresenter.php';
12 require_once __DIR__ . '/../admin/RedirectAddModalPresenter.php';
13 require_once __DIR__ . '/../admin/RedirectsTablePagePresenter.php';
14
15 /**
16 * Page Redirects admin page renderer. Owns the page wrapper, the table shell,
17 * and the modern Add Redirect modal. Row presentation, columns, destination
18 * warnings, and destination link resolution live in admin presenter/policy
19 * classes.
20 *
21 * Outside callers (via the View facade __call dispatch):
22 * - includes/ajax/ViewUpdater.php (pagination AJAX -> getAdminRedirectsPageTable)
23 * - PluginLogic admin page entry points (echoAdminRedirectsPage)
24 * - tests/BugProof_ReleaseReadinessTest greps buildRedirectsColumnDefs source
25 */
26 class ABJ_404_Solution_View_RedirectsTable extends ABJ_404_Solution_ViewComponent {
27
28 /** @var ABJ_404_Solution_RedirectsTableColumns|null */
29 private $redirectsTableColumns = null;
30
31 /** @var ABJ_404_Solution_RedirectDestinationWarningPolicy|null */
32 private $destinationWarningPolicy = null;
33
34 /** @var ABJ_404_Solution_RedirectDestinationLinkResolver|null */
35 private $destinationLinkResolver = null;
36
37 /** @var ABJ_404_Solution_RedirectRowPresenter|null */
38 private $redirectRowPresenter = null;
39
40 /** @var ABJ_404_Solution_RedirectAddModalPresenter|null */
41 private $redirectAddModalPresenter = null;
42
43 /** @var ABJ_404_Solution_RedirectsTablePagePresenter|null */
44 private $redirectsTablePagePresenter = null;
45
46 /** Load a template file from includes/html/ and trim its trailing newline. */
47 private function tpl(string $name): string {
48 $raw = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false);
49 return rtrim((string)$raw, "\n");
50 }
51
52 /**
53 * @return void
54 */
55 public function echoAdminRedirectsPage() {
56 echo $this->pagePresenter()->render();
57 }
58
59 /**
60 * Echo the modern Add Redirect modal.
61 *
62 * @param array<string, mixed> $tableOptions
63 * @return void
64 */
65 public function echoAddRedirectModal($tableOptions) {
66 echo $this->addModalPresenter()->render($tableOptions);
67 }
68
69 /**
70 * Compute (and remember) the table-data signature for the redirects or
71 * captured tab WITHOUT rendering the table HTML, status counts, or
72 * pagination.
73 *
74 * This is the renderless, READ-ONLY path the background detect-only refresh
75 * uses (report.md Finding 3, report3.md Finding 2). It is not metadata-only:
76 * it runs the SAME one-page read the full render runs -- getTableOptions ->
77 * getRedirectsForView -> rememberTableDataSignature -- so the signature it
78 * returns is byte-identical to the one a full render would stamp (same rows,
79 * same live resolution). What it skips is all the foreground work an idle
80 * poll has no use for: the HTML build, the status-count aggregates, the two
81 * pagination-link builds, AND the live denorm write-back (suppressed via the
82 * _abj404_suppress_denorm_writeback flag below) -- a change-detection probe
83 * must observe, never mutate. The full render still persists, so the stored
84 * denorm values stay fresh. Both tabs read through getRedirectsForView, so
85 * this one method serves both subpages.
86 *
87 * @param string $sub 'abj404_redirects' or 'abj404_captured'.
88 * @param array<string, mixed> $tableOptionOverrides Internal per-request read options.
89 * @return string The remembered signature for $sub.
90 */
91 public function computeTableDataSignature($sub, array $tableOptionOverrides = array()) {
92 $tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub);
93 $tableOptions = array_replace($tableOptions, $tableOptionOverrides);
94 $tableOptions['_abj404_suppress_denorm_writeback'] = true;
95 $rows = $this->viewReadService->getRedirectsForView($sub, $tableOptions);
96 /** @var array<int, array<string, mixed>> $typedRows */
97 $typedRows = array_values(array_filter($rows, 'is_array'));
98 $this->shared->rememberTableDataSignature($sub, $typedRows);
99 return $this->shared->getCurrentTableDataSignature($sub);
100 }
101
102 /**
103 * @param string $sub
104 * @param array<string, mixed> $tableOptionOverrides Internal per-request read options.
105 * @return string
106 */
107 public function getAdminRedirectsPageTable($sub, array $tableOptionOverrides = array()) {
108 $tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub);
109 $tableOptions = array_replace($tableOptions, $tableOptionOverrides);
110 $columns = $this->buildRedirectsColumnDefs($tableOptions);
111
112 $headerColumns = ABJ_404_Solution_TableRenderTranslationTracer::traceScope(
113 'redirect_column_headers',
114 'select all|sort URL|tooltip|header templates',
115 fn(): string => $this->logs->getTableColumns($sub, $columns)
116 );
117
118 $rows = $this->viewReadService->getRedirectsForView($sub, $tableOptions);
119 /** @var array<int, array<string, mixed>> $typedRedirectRows */
120 $typedRedirectRows = array_values(array_filter($rows, 'is_array'));
121 $this->shared->rememberTableDataSignature($sub, $typedRedirectRows);
122
123 // Bounded dead-destination check: pass only the ids on this page so the
124 // rollup read scales with the page size (~25), never the whole table.
125 $displayedRedirectIds = array();
126 foreach ($typedRedirectRows as $row) {
127 if (isset($row['id']) && is_scalar($row['id'])) {
128 $displayedRedirectIds[] = (int) $row['id'];
129 }
130 }
131 $deadDestIds = abj_service('redirect_dead_destination_checker')->findDeadDestinationIds($displayedRedirectIds);
132
133 $displayed = 0;
134 $y = 1;
135 $bodyRows = '';
136 $progress = ABJ_404_Solution_AjaxRowLoopProgress::begin('redirects_rows', count($typedRedirectRows));
137 foreach ($typedRedirectRows as $row) {
138 $progress->tick($row);
139 $bodyRows .= $this->buildRedirectRowHTML($row, $sub, $tableOptions, $deadDestIds, $y);
140 $y = ($y === 0) ? 1 : 0;
141 $displayed++;
142 }
143 $progress->finish();
144 if ($displayed == 0) {
145 // The single-table denorm read (denorm Step 3b) is always complete
146 // and serveable, so zero displayed rows is a genuinely empty
147 // listing. There is no "still preparing" state to distinguish.
148 $bodyRows .= ABJ_404_Solution_TableRenderTranslationTracer::traceScope(
149 'redirect_empty_state',
150 'No Redirect Records To Display|Redirects will appear here once created',
151 fn(): string => $this->f->str_replace(
152 array('{title}', '{help}'),
153 array(
154 __('No Redirect Records To Display', '404-solution'),
155 __('Redirects will appear here once created.', '404-solution'),
156 ),
157 $this->tpl('viewRedirectsTableRedirectsEmptyState.html')
158 )
159 );
160 }
161
162 return $this->f->str_replace(
163 array('{header_columns}', '{body_rows}'),
164 array($headerColumns, $bodyRows),
165 $this->tpl('viewRedirectsTableRedirectsTableShell.html')
166 );
167 }
168
169 /**
170 * @param array<string, mixed> $tableOptions
171 * @return array<string, array<string, string>>
172 */
173 public function buildRedirectsColumnDefs(array $tableOptions): array {
174 return $this->columns()->build($tableOptions);
175 }
176
177 /**
178 * @param array<string, mixed> $row
179 * @param string $sub
180 * @param array<string, mixed> $tableOptions
181 * @param array<mixed> $deadDestIds
182 * @param int $y
183 * @return string
184 */
185 public function buildRedirectRowHTML(array $row, string $sub, array $tableOptions, array $deadDestIds, int $y): string {
186 return $this->rowPresenter()->render($row, $sub, $tableOptions, $deadDestIds, $y);
187 }
188
189 /**
190 * @param ABJ_404_Solution_RedirectDestinationWarningContext $ctx
191 * @return array{exists: string, notExists: string, text: string, destForView: string}
192 */
193 public function resolveDestinationWarnings(ABJ_404_Solution_RedirectDestinationWarningContext $ctx): array {
194 return $this->warningPolicy()->resolve($ctx->row, $ctx->rowType, $ctx->rowFinalDest,
195 $ctx->destForView, $ctx->destinationIsMissing, $ctx->deadDestIds);
196 }
197
198 /**
199 * @param array<string, string> $replacements
200 * @return string
201 */
202 public function fillRedirectRowTemplate(array $replacements): string {
203 return $this->rowPresenter()->fillRedirectRowTemplate($replacements);
204 }
205
206 /**
207 * @param mixed $rawScore
208 * @param string $rowEngine
209 * @return string
210 */
211 public function buildScoreCell($rawScore, string $rowEngine): string {
212 return $this->rowPresenter()->buildScoreCell($rawScore, $rowEngine);
213 }
214
215 /**
216 * @param mixed $rowType
217 * @param string $rowFinalDest
218 * @return array{link: string, title: string}
219 */
220 public function resolveRedirectDestLink($rowType, string $rowFinalDest): array {
221 return $this->linkResolver()->resolve($rowType, $rowFinalDest);
222 }
223
224 private function columns(): ABJ_404_Solution_RedirectsTableColumns {
225 if (!($this->redirectsTableColumns instanceof ABJ_404_Solution_RedirectsTableColumns)) {
226 $this->redirectsTableColumns = new ABJ_404_Solution_RedirectsTableColumns();
227 }
228 return $this->redirectsTableColumns;
229 }
230
231 private function warningPolicy(): ABJ_404_Solution_RedirectDestinationWarningPolicy {
232 if (!($this->destinationWarningPolicy instanceof ABJ_404_Solution_RedirectDestinationWarningPolicy)) {
233 $this->destinationWarningPolicy = new ABJ_404_Solution_RedirectDestinationWarningPolicy();
234 }
235 return $this->destinationWarningPolicy;
236 }
237
238 private function linkResolver(): ABJ_404_Solution_RedirectDestinationLinkResolver {
239 if (!($this->destinationLinkResolver instanceof ABJ_404_Solution_RedirectDestinationLinkResolver)) {
240 $this->destinationLinkResolver = new ABJ_404_Solution_RedirectDestinationLinkResolver($this->logger);
241 }
242 return $this->destinationLinkResolver;
243 }
244
245 private function rowPresenter(): ABJ_404_Solution_RedirectRowPresenter {
246 if (!($this->redirectRowPresenter instanceof ABJ_404_Solution_RedirectRowPresenter)) {
247 $this->redirectRowPresenter = new ABJ_404_Solution_RedirectRowPresenter(
248 $this->f,
249 $this->warningPolicy(),
250 $this->linkResolver(),
251 new ABJ_404_Solution_RedirectRowActionsPresenter($this->f, $this->shared)
252 );
253 }
254 return $this->redirectRowPresenter;
255 }
256
257 private function addModalPresenter(): ABJ_404_Solution_RedirectAddModalPresenter {
258 if (!($this->redirectAddModalPresenter instanceof ABJ_404_Solution_RedirectAddModalPresenter)) {
259 $this->redirectAddModalPresenter = new ABJ_404_Solution_RedirectAddModalPresenter(
260 $this->f,
261 $this->optionsPresenter,
262 $this->redirectTypeUI,
263 $this->redirectConditions
264 );
265 }
266 return $this->redirectAddModalPresenter;
267 }
268
269 private function pagePresenter(): ABJ_404_Solution_RedirectsTablePagePresenter {
270 if (!($this->redirectsTablePagePresenter instanceof ABJ_404_Solution_RedirectsTablePagePresenter)) {
271 $this->redirectsTablePagePresenter = new ABJ_404_Solution_RedirectsTablePagePresenter(
272 $this->f,
273 $this->logic,
274 $this->listTableChrome,
275 $this->addModalPresenter()
276 );
277 }
278 return $this->redirectsTablePagePresenter;
279 }
280 }
281