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 / admin / RedirectRowPresenter.php

RedirectRowPresenter.php in 404 Solution trunk, at includes/admin/RedirectRowPresenter.php

460 lines 18.1 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 * Presents one Page Redirects table row using the row templates.
9 */
10 class ABJ_404_Solution_RedirectRowPresenter {
11
12 /** @var ABJ_404_Solution_Functions */
13 private $functions;
14
15 /** @var ABJ_404_Solution_RedirectDestinationWarningPolicy */
16 private $warningPolicy;
17
18 /** @var ABJ_404_Solution_RedirectDestinationLinkResolver */
19 private $linkResolver;
20
21 /** @var ABJ_404_Solution_RedirectRowActionsPresenter */
22 private $actionsPresenter;
23
24 public function __construct(ABJ_404_Solution_Functions $functions,
25 ABJ_404_Solution_RedirectDestinationWarningPolicy $warningPolicy,
26 ABJ_404_Solution_RedirectDestinationLinkResolver $linkResolver,
27 ABJ_404_Solution_RedirectRowActionsPresenter $actionsPresenter) {
28 $this->functions = $functions;
29 $this->warningPolicy = $warningPolicy;
30 $this->linkResolver = $linkResolver;
31 $this->actionsPresenter = $actionsPresenter;
32 }
33
34 /**
35 * @param array<string, mixed> $row
36 * @param string $sub
37 * @param array<string, mixed> $tableOptions
38 * @param array<mixed> $deadDestIds
39 */
40 public function render(array $row, string $sub, array $tableOptions, array $deadDestIds, int $y): string {
41 $state = $this->viewState($row, $sub, $tableOptions, $deadDestIds, $y);
42 return $this->fillRedirectRowTemplate($this->templateReplacements($row, $state));
43 }
44
45 /**
46 * @param array<string, mixed> $row
47 * @param array<string, mixed> $tableOptions
48 * @param array<mixed> $deadDestIds
49 * @return array{
50 * rowType: mixed,
51 * rowStatus: int,
52 * rowFinalDest: string,
53 * rowUrl: string,
54 * lastUsed: int,
55 * destLink: array{link: string, title: string},
56 * link: string,
57 * regexState: array{class: string, normalStyle: string, warningStyle: string},
58 * rowClass: string,
59 * actions: array{
60 * edit: string,
61 * logs: string,
62 * trash: string,
63 * delete: string,
64 * links: array{editlink: string, logslink: string, trashlink: string, ajaxTrashLink: string, trashtitle: string, deletelink: string}
65 * },
66 * warning: array{exists: string, notExists: string, text: string, destForView: string},
67 * rowEngine: string
68 * }
69 */
70 private function viewState(array $row, string $sub, array $tableOptions, array $deadDestIds, int $y): array {
71 $rowType = $this->rowType($row);
72 $rowStatus = $this->rowStatus($row);
73 $rowFinalDest = $this->rowFinalDest($row);
74 $destForView = $this->displayDestForView($row, $rowType);
75 $rowUrl = $this->rowUrl($row);
76 $lastUsed = $this->lastUsed($row);
77 $destLink = $this->linkResolver->resolve($rowType, $rowFinalDest);
78 $destinationIsMissing = $this->destinationIsMissing($rowType, $rowFinalDest);
79 $regexState = $this->regexState($rowUrl, $rowStatus);
80
81 return array(
82 'rowType' => $rowType,
83 'rowStatus' => $rowStatus,
84 'rowFinalDest' => $rowFinalDest,
85 'rowUrl' => $rowUrl,
86 'lastUsed' => $lastUsed,
87 'destLink' => $destLink,
88 'link' => $destLink['link'] !== '' ? "href='" . esc_url($destLink['link']) . "'" : '',
89 'regexState' => $regexState,
90 'rowClass' => $this->rowClass($y) . $this->destinationClass($row, $destinationIsMissing) . $regexState['class'],
91 'actions' => $this->actionsPresenter->render($row, $sub, $tableOptions),
92 'warning' => $this->warningPolicy->resolve(
93 $row,
94 $rowType,
95 $rowFinalDest,
96 $destForView,
97 $destinationIsMissing,
98 $deadDestIds
99 ),
100 'rowEngine' => $this->rowEngine($row),
101 );
102 }
103
104 /**
105 * @param array<string, mixed> $row
106 * @param array{
107 * rowType: mixed,
108 * rowStatus: int,
109 * rowFinalDest: string,
110 * rowUrl: string,
111 * lastUsed: int,
112 * destLink: array{link: string, title: string},
113 * link: string,
114 * regexState: array{class: string, normalStyle: string, warningStyle: string},
115 * rowClass: string,
116 * actions: array{
117 * edit: string,
118 * logs: string,
119 * trash: string,
120 * delete: string,
121 * links: array{editlink: string, logslink: string, trashlink: string, ajaxTrashLink: string, trashtitle: string, deletelink: string}
122 * },
123 * warning: array{exists: string, notExists: string, text: string, destForView: string},
124 * rowEngine: string
125 * } $state
126 * @return array<string, string>
127 */
128 private function templateReplacements(array $row, array $state): array {
129 $rowStatus = $state['rowStatus'];
130 $lastUsed = $state['lastUsed'];
131 $rowEngine = $state['rowEngine'];
132 $rowCode = is_scalar($row['code'] ?? '') ? (string)($row['code'] ?? '') : '';
133 $last = $lastUsed != 0
134 ? ABJ_404_Solution_SiteLocalTimestamp::format('Y/m/d h:i:s A', abs($lastUsed))
135 : __('Never Used', '404-solution');
136 $rowId = is_scalar($row['id'] ?? '') ? (string)($row['id'] ?? '') : '';
137 $regexState = $state['regexState'];
138 $actions = $state['actions'];
139 $warning = $state['warning'];
140 $destLink = $state['destLink'];
141 $links = $actions['links'];
142
143 return array(
144 '{rowid}' => $rowId,
145 '{rowClass}' => $state['rowClass'],
146 '{visitorURL}' => esc_url(home_url($state['rowUrl'])),
147 '{rowURL}' => esc_html($state['rowUrl']),
148 '{url-is-normal}' => $regexState['normalStyle'],
149 '{url-looks-like-regex}' => $regexState['warningStyle'],
150 '{editBtnHTML}' => $actions['edit'],
151 '{logsBtnHTML}' => $actions['logs'],
152 '{trashBtnHTML}' => $actions['trash'],
153 '{deleteBtnHTML}' => $actions['delete'],
154 '{statusBadgeClass}' => $this->statusBadgeClass($rowStatus),
155 '{codeBadgeClass}' => $this->codeBadgeClass($rowCode),
156 '{lastUsedClass}' => $lastUsed == 0 ? 'abj404-never-used' : '',
157 '{link}' => $this->scalarString($state['link'] ?? ''),
158 '{title}' => esc_attr($destLink['title']),
159 '{dest}' => esc_attr($warning['destForView']),
160 '{destination-exists}' => $warning['exists'],
161 '{destination-does-not-exist}' => $warning['notExists'],
162 '{destination-warning-text}' => $warning['text'],
163 '{status}' => esc_html($this->statusLabel($rowStatus)),
164 '{statusTitle}' => $this->statusTitle($rowStatus),
165 '{engineHTML}' => $this->engineLabelHtml($rowEngine),
166 '{rowScore}' => '',
167 '{scoreCell}' => $this->buildScoreCell($row['score'] ?? null, $rowEngine, $rowStatus),
168 '{type}' => esc_html($this->typeLabel($row)),
169 '{rowCode}' => esc_html($this->codeDisplay($rowCode)),
170 '{hits}' => esc_html((string)(is_scalar($row['logshits'] ?? 0) ? (int)($row['logshits'] ?? 0) : 0)),
171 '{logsLink}' => $links['logslink'],
172 '{trashLink}' => $links['trashlink'],
173 '{ajaxTrashLink}' => $links['ajaxTrashLink'],
174 '{trashtitle}' => $links['trashtitle'],
175 '{deletelink}' => $links['deletelink'],
176 '{created_date}' => esc_html(ABJ_404_Solution_SiteLocalTimestamp::format(
177 'Y/m/d h:i:s A',
178 abs(is_scalar($row['timestamp'] ?? 0) ? intval($row['timestamp'] ?? 0) : 0)
179 )),
180 '{last_used_date}' => esc_html($last),
181 );
182 }
183
184 /**
185 * @param array<string, mixed> $row
186 */
187 private function rowType(array $row): int {
188 return is_scalar($row['type'] ?? 0) ? (int)($row['type'] ?? 0) : 0;
189 }
190
191 /** @param array<string, mixed> $row */
192 private function rowStatus(array $row): int {
193 return is_scalar($row['status'] ?? 0) ? (int)($row['status'] ?? 0) : 0;
194 }
195
196 /** @param array<string, mixed> $row */
197 private function rowFinalDest(array $row): string {
198 return is_string($row['final_dest'] ?? '') ? (string)($row['final_dest'] ?? '') : '';
199 }
200
201 /** @param array<string, mixed> $row */
202 private function destForView(array $row): string {
203 return trim(is_scalar($row['dest_for_view'] ?? '') ? (string)($row['dest_for_view'] ?? '') : '');
204 }
205
206 /** @param array<string, mixed> $row */
207 private function displayDestForView(array $row, int $rowType): string {
208 $destForView = $this->destForView($row);
209 if ($rowType === ABJ404_TYPE_404_DISPLAYED && $destForView === '') {
210 return __('(404 page)', '404-solution');
211 }
212 return $destForView;
213 }
214
215 /** @param array<string, mixed> $row */
216 private function rowUrl(array $row): string {
217 return is_string($row['url'] ?? '') ? (string)($row['url'] ?? '') : '';
218 }
219
220 /** @param array<string, mixed> $row */
221 private function lastUsed(array $row): int {
222 return is_scalar($row['last_used'] ?? 0) ? (int)($row['last_used'] ?? 0) : 0;
223 }
224
225 /** @param mixed $rowType */
226 private function destinationIsMissing($rowType, string $rowFinalDest): bool {
227 return $rowType != ABJ404_TYPE_404_DISPLAYED && trim((string)$rowFinalDest) === '';
228 }
229
230 /** @param array<string, mixed> $row */
231 private function rowEngine(array $row): string {
232 return is_string($row['engine'] ?? '') ? trim((string)($row['engine'] ?? '')) : '';
233 }
234
235 /**
236 * @param array<string, string> $replacements
237 */
238 public function fillRedirectRowTemplate(array $replacements): string {
239 $html = $this->tpl('tableRowPageRedirects.html');
240 $html = $this->functions->str_replace(array_keys($replacements), array_values($replacements), $html);
241 return $this->functions->doNormalReplacements($html);
242 }
243
244 /**
245 * @param mixed $rawScore
246 * @param string $rowEngine
247 * @param int $rowStatus ABJ404_STATUS_* of the row being rendered.
248 */
249 public function buildScoreCell($rawScore, string $rowEngine, int $rowStatus): string {
250 if ($rawScore !== null && $rawScore !== '') {
251 $scoreNum = (float)(is_numeric($rawScore) ? $rawScore : 0);
252 $scorePct = number_format($scoreNum, 0);
253 return $this->functions->str_replace(
254 array('{badge_class}', '{score_pct}'),
255 array($this->scoreBadgeClass($scoreNum), esc_html($scorePct)),
256 $this->tpl('viewRedirectsTableScoreBadge.html')
257 );
258 }
259
260 return $this->functions->str_replace(
261 '{title_attr}',
262 esc_attr($this->noScoreTitle($rowEngine, $rowStatus)),
263 $this->tpl('viewRedirectsTableScoreManual.html')
264 );
265 }
266
267 /**
268 * Explain an empty Confidence cell in terms of the row it is on.
269 *
270 * A missing score used to be reported as "Manual redirect" for every row,
271 * which was written when only an admin-created redirect could lack one.
272 * Captured 404s are not manual redirects, and until near-miss scores were
273 * threaded into the capture insert every captured row was scoreless -- so
274 * the Captured tab told the admin that a whole table of URLs the plugin
275 * had recorded automatically had been typed in by hand.
276 *
277 * @param string $rowEngine
278 * @param int $rowStatus
279 * @return string
280 */
281 private function noScoreTitle(string $rowEngine, int $rowStatus): string {
282 if ($rowEngine !== '') {
283 return __('No confidence score for this engine', '404-solution');
284 }
285 if (defined('ABJ404_STATUS_CAPTURED') && $rowStatus === ABJ404_STATUS_CAPTURED) {
286 // Either automatic matching is off, or it ran and nothing scored.
287 // The row cannot tell the two apart, and both mean the same thing
288 // to the admin reading the column.
289 return __('No suggested match was scored for this URL', '404-solution');
290 }
291 if ($rowStatus === ABJ404_STATUS_AUTO) {
292 return __('No confidence score was recorded for this redirect', '404-solution');
293 }
294 return __('Manual redirect, no confidence score', '404-solution');
295 }
296
297 /**
298 * @param mixed $rowStatus
299 */
300 private function statusTitle($rowStatus): string {
301 if ($rowStatus == ABJ404_STATUS_MANUAL) {
302 return __('Manually created', '404-solution');
303 }
304 if ($rowStatus == ABJ404_STATUS_AUTO) {
305 return __('Automatically created', '404-solution');
306 }
307 if ($rowStatus == ABJ404_STATUS_REGEX) {
308 return __('Regular Expression (Manually Created)', '404-solution');
309 }
310 return __('Unknown', '404-solution');
311 }
312
313 private function statusLabel(int $rowStatus): string {
314 $labels = array(
315 ABJ404_STATUS_MANUAL => __('Manual', '404-solution'),
316 ABJ404_STATUS_AUTO => __('Auto', '404-solution'),
317 ABJ404_STATUS_REGEX => __('Regex', '404-solution'),
318 );
319 if (defined('ABJ404_STATUS_CAPTURED')) {
320 $labels[ABJ404_STATUS_CAPTURED] = __('Captured', '404-solution');
321 }
322 if (defined('ABJ404_STATUS_IGNORED')) {
323 $labels[ABJ404_STATUS_IGNORED] = __('Ignored', '404-solution');
324 }
325 if (defined('ABJ404_STATUS_LATER')) {
326 $labels[ABJ404_STATUS_LATER] = __('Later', '404-solution');
327 }
328 return isset($labels[$rowStatus]) ? $labels[$rowStatus] : __('Unknown', '404-solution');
329 }
330
331 /** @param array<string, mixed> $row */
332 private function typeLabel(array $row): string {
333 $rowType = is_scalar($row['type'] ?? 0) ? (int)($row['type'] ?? 0) : 0;
334 $labels = array(
335 ABJ404_TYPE_EXTERNAL => __('External', '404-solution'),
336 ABJ404_TYPE_CAT => __('Category', '404-solution'),
337 ABJ404_TYPE_TAG => __('Tag', '404-solution'),
338 ABJ404_TYPE_HOME => __('Home', '404-solution'),
339 ABJ404_TYPE_404_DISPLAYED => __('(404 page)', '404-solution'),
340 );
341 if ($rowType === ABJ404_TYPE_POST) {
342 return $this->postTypeLabel($row);
343 }
344 return isset($labels[$rowType]) ? $labels[$rowType] : '';
345 }
346
347 /** @param array<string, mixed> $row */
348 private function postTypeLabel(array $row): string {
349 $slug = is_scalar($row['wp_post_type'] ?? '') ? trim((string)($row['wp_post_type'] ?? '')) : '';
350 if ($slug === '') {
351 $slug = 'post';
352 }
353 if (function_exists('get_post_type_object')) {
354 $postType = get_post_type_object($slug);
355 $singular = is_object($postType) && property_exists($postType, 'labels') && is_object($postType->labels)
356 && property_exists($postType->labels, 'singular_name') && is_scalar($postType->labels->singular_name)
357 ? trim((string)$postType->labels->singular_name)
358 : '';
359 if ($singular !== '') {
360 return $singular;
361 }
362 }
363 return ucfirst(strtolower($slug));
364 }
365
366 /**
367 * @param mixed $rowStatus
368 */
369 private function statusBadgeClass($rowStatus): string {
370 if ($rowStatus == ABJ404_STATUS_AUTO) {
371 return 'abj404-badge-auto';
372 }
373 if ($rowStatus == ABJ404_STATUS_REGEX) {
374 return 'abj404-badge-regex';
375 }
376 return 'abj404-badge-manual';
377 }
378
379 private function codeBadgeClass(string $rowCode): string {
380 $codeBadgeMap = array(
381 '301' => 'abj404-badge-301',
382 '302' => 'abj404-badge-302',
383 '307' => 'abj404-badge-307',
384 '308' => 'abj404-badge-308',
385 '410' => 'abj404-badge-410',
386 '451' => 'abj404-badge-451',
387 '0' => 'abj404-badge-meta',
388 );
389 return isset($codeBadgeMap[$rowCode]) ? $codeBadgeMap[$rowCode] : 'abj404-badge-302';
390 }
391
392 private function codeDisplay(string $rowCode): string {
393 if (abj_service('settings_mode_preference')->getMode() === 'simple') {
394 return ABJ_404_Solution_View_RedirectTypeUI::getPlainLanguageCodeLabel($rowCode);
395 }
396 return $rowCode;
397 }
398
399 private function scoreBadgeClass(float $scoreNum): string {
400 if ($scoreNum >= 80) {
401 return 'abj404-score-high';
402 }
403 if ($scoreNum >= 50) {
404 return 'abj404-score-medium';
405 }
406 return 'abj404-score-low';
407 }
408
409 /** @return array{class: string, normalStyle: string, warningStyle: string} */
410 private function regexState(string $rowUrl, int $rowStatus): array {
411 /** @var ABJ_404_Solution_RegexHelper $regexHelper */
412 $regexHelper = abj_service('regex_helper');
413 $urlLooksLikeRegex = $regexHelper->urlLooksLikeRegex($rowUrl);
414 if ($urlLooksLikeRegex && $rowStatus != ABJ404_STATUS_REGEX) {
415 return array('class' => ' url-looks-like-regex', 'normalStyle' => 'display: none;', 'warningStyle' => '');
416 }
417 return array('class' => '', 'normalStyle' => '', 'warningStyle' => 'display: none;');
418 }
419
420 /**
421 * @param array<string, mixed> $row
422 */
423 private function destinationClass(array $row, bool $destinationIsMissing): string {
424 if ($destinationIsMissing) {
425 return ' destination-does-not-exist';
426 }
427 if (array_key_exists('published_status', $row) && $row['published_status'] == '0') {
428 return ' destination-does-not-exist';
429 }
430 return '';
431 }
432
433 private function rowClass(int $y): string {
434 return $y == 0 ? 'alternate' : 'normal-non-alternate';
435 }
436
437 private function engineLabelHtml(string $rowEngine): string {
438 if ($rowEngine === '') {
439 return '';
440 }
441 return $this->functions->str_replace(
442 '{engine}',
443 esc_html($rowEngine),
444 $this->tpl('viewRedirectsTableEngineLabel.html')
445 );
446 }
447
448 /**
449 * @param mixed $value
450 */
451 private function scalarString($value): string {
452 return is_string($value) ? $value : '';
453 }
454
455 private function tpl(string $name): string {
456 $raw = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false);
457 return rtrim((string)$raw, "\n");
458 }
459 }
460