| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
// allow-no-test-found: exercised by ViewTableRenderingTest |
| 8 |
|
| 9 |
/** |
| 10 |
* Renders the header cells for the Captured 404 URLs admin table. |
| 11 |
*/ |
| 12 |
class ABJ_404_Solution_CapturedTableHeaderRenderer { |
| 13 |
|
| 14 |
/** @var ABJ_404_Solution_Functions */ |
| 15 |
private $f; |
| 16 |
|
| 17 |
/** @var ABJ_404_Solution_View_Shared */ |
| 18 |
private $shared; |
| 19 |
|
| 20 |
/** @var ABJ_404_Solution_ViewReadServiceInterface */ |
| 21 |
private $viewReadService; |
| 22 |
|
| 23 |
/** |
| 24 |
* @param ABJ_404_Solution_Functions $functions |
| 25 |
* @param ABJ_404_Solution_View_Shared $shared |
| 26 |
* @param ABJ_404_Solution_ViewReadServiceInterface $viewReadService |
| 27 |
*/ |
| 28 |
public function __construct($functions, ABJ_404_Solution_View_Shared $shared, $viewReadService) { |
| 29 |
$this->f = $functions; |
| 30 |
$this->shared = $shared; |
| 31 |
$this->viewReadService = $viewReadService; |
| 32 |
} |
| 33 |
|
| 34 |
/** |
| 35 |
* @param array<string, mixed> $tableOptions |
| 36 |
* @return string |
| 37 |
*/ |
| 38 |
public function render(array $tableOptions): string { |
| 39 |
$columns = array( |
| 40 |
array('title' => __('URL', '404-solution'), 'orderby' => 'url'), |
| 41 |
array('title' => __('Status', '404-solution'), 'orderby' => 'status'), |
| 42 |
array('title' => __('Hits', '404-solution'), 'orderby' => 'logshits'), |
| 43 |
array('title' => __('Created', '404-solution'), 'orderby' => 'timestamp', 'class' => 'hide-on-tablet'), |
| 44 |
array('title' => __('Last Used', '404-solution'), 'orderby' => 'last_used'), |
| 45 |
); |
| 46 |
|
| 47 |
$headerCells = ''; |
| 48 |
foreach ($columns as $col) { |
| 49 |
$headerCells .= $this->capturedHeaderCell($tableOptions, $col) . "\n"; |
| 50 |
} |
| 51 |
return $headerCells; |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* @param array<string, mixed> $tableOptions |
| 56 |
* @param array<string, mixed> $col |
| 57 |
*/ |
| 58 |
private function capturedHeaderCell(array $tableOptions, array $col): string { |
| 59 |
$rawFilter = $tableOptions['filter'] ?? 0; |
| 60 |
$currentFilter = is_scalar($rawFilter) ? $rawFilter : 0; |
| 61 |
$rawOrderby = $col['orderby'] ?? ''; |
| 62 |
$orderby = is_scalar($rawOrderby) ? (string)$rawOrderby : ''; |
| 63 |
|
| 64 |
// Post-upgrade window: a URL sort whose narrow url_sort_key cannot be |
| 65 |
// served index-ordered yet would filesort the captured majority (a |
| 66 |
// max_statement_time risk on large sites). The query already serves the |
| 67 |
// safe timestamp-DESC default (ViewQueryBuilder::resolveEffectiveSort), so |
| 68 |
// render this header non-sortable with a progress tooltip rather than a |
| 69 |
// clickable sort link that would silently not take effect. Self-heals: |
| 70 |
// once the sort key is index-ready the header is sortable again with no |
| 71 |
// user action. Shares the readiness + message with the Page Redirects |
| 72 |
// renderer via View_Shared::pendingSortTooltipText. |
| 73 |
$pendingTooltipBody = $this->shared->pendingSortTooltipText($orderby, $this->viewReadService); |
| 74 |
if ($pendingTooltipBody !== '') { |
| 75 |
return $this->capturedPendingHeaderCell($col, $pendingTooltipBody); |
| 76 |
} |
| 77 |
|
| 78 |
$sortUrl = "?page=" . ABJ404_PP . "&subpage=abj404_captured&filter=" . $currentFilter; |
| 79 |
$rawFilterText = $tableOptions['filterText'] ?? ''; |
| 80 |
if (is_scalar($rawFilterText) && (string)$rawFilterText !== '') { |
| 81 |
$sortUrl .= "&filterText=" . rawurlencode((string)$rawFilterText); |
| 82 |
} |
| 83 |
$sortUrl .= "&orderby=" . $orderby; |
| 84 |
$sortState = $this->shared->getHeaderSortState($tableOptions, $orderby, false); |
| 85 |
$sortUrl .= "&order=" . $sortState['nextOrder']; |
| 86 |
|
| 87 |
$rawExtraClass = $col['class'] ?? ''; |
| 88 |
$extraClass = is_scalar($rawExtraClass) && $rawExtraClass !== '' |
| 89 |
? ' ' . esc_attr((string)$rawExtraClass) : ''; |
| 90 |
$rawThClass = $sortState['thClass'] ?? ''; |
| 91 |
$sortClass = trim((is_scalar($rawThClass) ? (string)$rawThClass : '') . $extraClass); |
| 92 |
$classAttr = $sortClass ? ' class="' . trim($sortClass) . '"' : ''; |
| 93 |
|
| 94 |
$tooltipHtml = ''; |
| 95 |
if (isset($col['title_attr_html']) && !empty($col['title_attr_html'])) { |
| 96 |
$tooltipHtml = $this->f->str_replace( |
| 97 |
array('{more_info_label}', '{tooltip_body}'), |
| 98 |
array(esc_attr__('More info', '404-solution'), $this->columnScalar($col, 'title_attr_html')), |
| 99 |
$this->tpl('viewRedirectsTableHeaderTooltip.html') |
| 100 |
); |
| 101 |
} |
| 102 |
|
| 103 |
return $this->f->str_replace( |
| 104 |
array('{class_attr}', '{sort_url}', '{title}', '{sort_indicator}', '{tooltip_html}'), |
| 105 |
array($classAttr, esc_url($sortUrl), esc_html($this->columnScalar($col, 'title')), |
| 106 |
$this->sortStateScalar($sortState, 'indicator'), $tooltipHtml), |
| 107 |
$this->tpl('viewRedirectsTableCapturedSortableHeader.html') |
| 108 |
); |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Render a non-sortable captured-tab header (plain title + progress tooltip, |
| 113 |
* no sort link) for a column whose narrow sort key is not index-ready yet. |
| 114 |
* |
| 115 |
* @param array<string, mixed> $col |
| 116 |
* @param string $tooltipBody The pending-sort progress message. |
| 117 |
*/ |
| 118 |
private function capturedPendingHeaderCell(array $col, string $tooltipBody): string { |
| 119 |
$rawClass = $col['class'] ?? ''; |
| 120 |
$classValue = is_scalar($rawClass) ? (string)$rawClass : ''; |
| 121 |
$classAttr = $classValue !== '' ? ' class="' . esc_attr($classValue) . '"' : ''; |
| 122 |
$rawTitle = $col['title'] ?? ''; |
| 123 |
$titleValue = is_scalar($rawTitle) ? (string)$rawTitle : ''; |
| 124 |
$tooltipHtml = $this->f->str_replace( |
| 125 |
array('{more_info_label}', '{tooltip_body}'), |
| 126 |
array(esc_attr__('More info', '404-solution'), esc_html($tooltipBody)), |
| 127 |
$this->tpl('viewRedirectsTableHeaderTooltip.html') |
| 128 |
); |
| 129 |
$tooltipHtml = $this->tagPendingSortTooltip($tooltipHtml, $this->columnScalar($col, 'orderby')); |
| 130 |
return $this->f->str_replace( |
| 131 |
array('{class_attr}', '{title}', '{tooltip_html}'), |
| 132 |
array($classAttr, esc_html($titleValue), $tooltipHtml), |
| 133 |
$this->tpl('viewRedirectsTableCapturedPendingHeader.html') |
| 134 |
); |
| 135 |
} |
| 136 |
|
| 137 |
private function tagPendingSortTooltip(string $tooltipHtml, string $orderby): string { |
| 138 |
return str_replace( |
| 139 |
'class="abj404-header-tooltip lefty-tooltip"', |
| 140 |
'class="abj404-header-tooltip lefty-tooltip" data-abj404-pending-sort="' . esc_attr($orderby) . '"', |
| 141 |
$tooltipHtml |
| 142 |
); |
| 143 |
} |
| 144 |
|
| 145 |
private function tpl(string $name): string { |
| 146 |
$raw = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false); |
| 147 |
return rtrim((string)$raw, "\n"); |
| 148 |
} |
| 149 |
|
| 150 |
/** @param array<string, mixed> $column */ |
| 151 |
private function columnScalar(array $column, string $key): string { |
| 152 |
$value = $column[$key] ?? ''; |
| 153 |
return is_scalar($value) ? (string)$value : ''; |
| 154 |
} |
| 155 |
|
| 156 |
/** @param array<string, mixed> $sortState */ |
| 157 |
private function sortStateScalar(array $sortState, string $key): string { |
| 158 |
$value = $sortState[$key] ?? ''; |
| 159 |
return is_scalar($value) ? (string)$value : ''; |
| 160 |
} |
| 161 |
} |
| 162 |
|