| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Renders the "Linked from" expandable internal-source-evidence panel for a |
| 9 |
* captured 404 URL row: looks up which published pages/posts link to a |
| 10 |
* visitor-visible 404 URL, and builds the trigger button + expandable panel |
| 11 |
* HTML for each row. Extracted from View_CapturedURLsTable because internal |
| 12 |
* source-evidence lookup and rendering is a distinct responsibility from the |
| 13 |
* rest of the row's presentation formatting. |
| 14 |
*/ |
| 15 |
class ABJ_404_Solution_CapturedSourceEvidenceRenderer { |
| 16 |
|
| 17 |
/** @var ABJ_404_Solution_Functions */ |
| 18 |
private $f; |
| 19 |
|
| 20 |
public function __construct(ABJ_404_Solution_Functions $functions) { |
| 21 |
$this->f = $functions; |
| 22 |
} |
| 23 |
|
| 24 |
private function tpl(string $name): string { |
| 25 |
$raw = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false); |
| 26 |
return rtrim((string)$raw, "\n"); |
| 27 |
} |
| 28 |
|
| 29 |
/** @param array<string,string> $vars */ |
| 30 |
private function fillTpl(string $name, array $vars): string { |
| 31 |
return (string)$this->f->str_replace(array_keys($vars), array_values($vars), $this->tpl($name)); |
| 32 |
} |
| 33 |
|
| 34 |
/** |
| 35 |
* @param array<int, array<string, mixed>> $rows |
| 36 |
* @return array<string, array<string, mixed>> |
| 37 |
*/ |
| 38 |
public function evidenceByVisibleUrl(array $rows): array { |
| 39 |
$urls = array(); |
| 40 |
foreach ($rows as $row) { |
| 41 |
if (isset($row['url']) && is_string($row['url']) && $row['url'] !== '') { |
| 42 |
$urls[] = $row['url']; |
| 43 |
} |
| 44 |
} |
| 45 |
if (empty($urls)) { |
| 46 |
return array(); |
| 47 |
} |
| 48 |
|
| 49 |
$repo = $this->internalSourceEvidenceRepository(); |
| 50 |
if (!is_object($repo) || !method_exists($repo, 'getEvidenceForCapturedUrls')) { |
| 51 |
return array(); |
| 52 |
} |
| 53 |
$evidence = $repo->getEvidenceForCapturedUrls($urls, 5); |
| 54 |
if (!is_array($evidence)) { |
| 55 |
return array(); |
| 56 |
} |
| 57 |
|
| 58 |
$typedEvidence = array(); |
| 59 |
foreach ($evidence as $url => $rowEvidence) { |
| 60 |
if (is_string($url) && is_array($rowEvidence)) { |
| 61 |
$typedEvidence[$url] = $rowEvidence; |
| 62 |
} |
| 63 |
} |
| 64 |
return $typedEvidence; |
| 65 |
} |
| 66 |
|
| 67 |
/** @return mixed */ |
| 68 |
private function internalSourceEvidenceRepository() { |
| 69 |
return abj_service('internal_source_evidence_repository'); |
| 70 |
} |
| 71 |
|
| 72 |
/** |
| 73 |
* @param array<string, mixed> $evidence |
| 74 |
* @return array{trigger:string,panel:string} |
| 75 |
*/ |
| 76 |
public function htmlFor(string $rowId, array $evidence): array { |
| 77 |
$sourceCount = isset($evidence['source_count']) && is_numeric($evidence['source_count']) |
| 78 |
? (int)$evidence['source_count'] : 0; |
| 79 |
$sources = isset($evidence['sources']) && is_array($evidence['sources']) ? $evidence['sources'] : array(); |
| 80 |
if ($sourceCount <= 0 || empty($sources)) { |
| 81 |
return array('trigger' => '', 'panel' => ''); |
| 82 |
} |
| 83 |
|
| 84 |
$panelId = 'abj404-source-panel-' . sanitize_key($rowId); |
| 85 |
$trigger = $this->fillTpl('capturedSourceTrigger.html', array( |
| 86 |
'{panel_id}' => esc_attr($panelId), |
| 87 |
'{row_id}' => esc_attr($rowId), |
| 88 |
'{trigger_label}' => esc_html(sprintf(__('Sources (%d)', '404-solution'), $sourceCount)), |
| 89 |
)); |
| 90 |
|
| 91 |
$sourceRows = ''; |
| 92 |
foreach ($sources as $source) { |
| 93 |
if (!is_array($source)) { |
| 94 |
continue; |
| 95 |
} |
| 96 |
$sourceRows .= $this->sourceRowHtml($source); |
| 97 |
} |
| 98 |
if ($sourceRows === '') { |
| 99 |
return array('trigger' => '', 'panel' => ''); |
| 100 |
} |
| 101 |
|
| 102 |
$displayedCount = isset($evidence['displayed_source_count']) && is_numeric($evidence['displayed_source_count']) |
| 103 |
? (int)$evidence['displayed_source_count'] : count($sources); |
| 104 |
$truncation = ''; |
| 105 |
if ($sourceCount > $displayedCount) { |
| 106 |
$truncation = $this->fillTpl('capturedSourceTruncation.html', array( |
| 107 |
'{truncation_text}' => esc_html(sprintf(__('Showing %1$d of %2$d sources.', '404-solution'), $displayedCount, $sourceCount)), |
| 108 |
)); |
| 109 |
} |
| 110 |
|
| 111 |
$panel = $this->fillTpl('capturedSourcesPanel.html', array( |
| 112 |
'{panel_id}' => esc_attr($panelId), |
| 113 |
'{heading}' => esc_html__('Linked from', '404-solution'), |
| 114 |
'{source_rows}' => $sourceRows, |
| 115 |
'{truncation}' => $truncation, |
| 116 |
)); |
| 117 |
|
| 118 |
return array('trigger' => $trigger, 'panel' => $panel); |
| 119 |
} |
| 120 |
|
| 121 |
/** @param array<array-key, mixed> $source */ |
| 122 |
private function sourceRowHtml(array $source): string { |
| 123 |
$title = isset($source['post_title']) && is_scalar($source['post_title']) ? trim((string)$source['post_title']) : ''; |
| 124 |
$referrerUrl = isset($source['referrer_url']) && is_scalar($source['referrer_url']) ? (string)$source['referrer_url'] : ''; |
| 125 |
$label = $title !== '' ? $title : $referrerUrl; |
| 126 |
$postId = isset($source['post_id']) && is_numeric($source['post_id']) ? (int)$source['post_id'] : 0; |
| 127 |
|
| 128 |
return $this->fillTpl('capturedSourcesRow.html', array( |
| 129 |
'{source_label}' => esc_html($label), |
| 130 |
'{hit_count}' => esc_html((string)(isset($source['hit_count']) && is_numeric($source['hit_count']) |
| 131 |
? (int)$source['hit_count'] : 0)), |
| 132 |
'{edit_link}' => $this->editLinkHtml($postId), |
| 133 |
)); |
| 134 |
} |
| 135 |
|
| 136 |
/** |
| 137 |
* Builds the "Edit" admin link for a resolved source post, gated on the |
| 138 |
* current user's edit capability for that specific post. |
| 139 |
* |
| 140 |
* Authorization (current_user_can()) and edit-link construction |
| 141 |
* (get_edit_post_link()) live here -- the caller that renders |
| 142 |
* source-evidence rows -- rather than in |
| 143 |
* ABJ_404_Solution_InternalSourceEvidenceRepository, which is a |
| 144 |
* data-access repository and must return only raw post_id/post_title |
| 145 |
* data (CLAUDE.md "Strict layer separation"; c308). |
| 146 |
*/ |
| 147 |
private function editLinkHtml(int $postId): string { |
| 148 |
if ($postId <= 0) { |
| 149 |
return ''; |
| 150 |
} |
| 151 |
if (!function_exists('current_user_can') || !current_user_can('edit_post', $postId)) { |
| 152 |
return ''; |
| 153 |
} |
| 154 |
if (!function_exists('get_edit_post_link')) { |
| 155 |
return ''; |
| 156 |
} |
| 157 |
$editUrl = (string)get_edit_post_link($postId); |
| 158 |
if ($editUrl === '') { |
| 159 |
return ''; |
| 160 |
} |
| 161 |
|
| 162 |
return $this->fillTpl('capturedSourceEditLink.html', array( |
| 163 |
'{edit_url}' => esc_url($editUrl), |
| 164 |
'{edit_label}' => esc_html__('Edit', '404-solution'), |
| 165 |
)); |
| 166 |
} |
| 167 |
} |
| 168 |
|