| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Renders redirect and captured URL status filter links. |
| 9 |
*/ |
| 10 |
class ABJ_404_Solution_AdminStatusFilterTabs { |
| 11 |
|
| 12 |
/** @var ABJ_404_Solution_Functions */ |
| 13 |
private $f; |
| 14 |
/** @var ABJ_404_Solution_PluginLogic */ |
| 15 |
private $logic; |
| 16 |
/** @var ABJ_404_Solution_Logging */ |
| 17 |
private $logger; |
| 18 |
/** @var ABJ_404_Solution_ViewReadServiceInterface */ |
| 19 |
private $viewReadService; |
| 20 |
/** @var ABJ_404_Solution_View_ListTableChrome */ |
| 21 |
private $listTableChrome; |
| 22 |
|
| 23 |
/** @param ABJ_404_Solution_ViewReadServiceInterface $viewReadService */ |
| 24 |
public function __construct(ABJ_404_Solution_Functions $functions, ABJ_404_Solution_PluginLogic $pluginLogic, |
| 25 |
ABJ_404_Solution_Logging $logging, $viewReadService, |
| 26 |
ABJ_404_Solution_View_ListTableChrome $listTableChrome) { |
| 27 |
$this->f = $functions; |
| 28 |
$this->logic = $pluginLogic; |
| 29 |
$this->logger = $logging; |
| 30 |
$this->viewReadService = $viewReadService; |
| 31 |
$this->listTableChrome = $listTableChrome; |
| 32 |
} |
| 33 |
|
| 34 |
/** |
| 35 |
* @param string $sub |
| 36 |
* @param array<string, mixed> $tableOptions |
| 37 |
*/ |
| 38 |
public function render(string $sub, array $tableOptions): string { |
| 39 |
if (empty($tableOptions)) { |
| 40 |
$tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub); |
| 41 |
} |
| 42 |
|
| 43 |
return $this->tpl('viewLogsTabFiltersClearBar.html') |
| 44 |
. $this->renderList($sub) |
| 45 |
. $this->tpl('viewLogsTabFiltersClearBarClose.html'); |
| 46 |
} |
| 47 |
|
| 48 |
public function renderList(string $sub): string { |
| 49 |
$tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub); |
| 50 |
$filter = isset($tableOptions['filter']) ? intval(is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0) : 0; |
| 51 |
$url = $this->baseUrl($sub, $tableOptions); |
| 52 |
$filterState = $this->filterState($sub); |
| 53 |
$types = $filterState['types']; |
| 54 |
$counts = $filterState['counts']; |
| 55 |
$countsIncomplete = !empty($counts['_incomplete']); |
| 56 |
|
| 57 |
$html = $this->f->str_replace( |
| 58 |
'{placeholder_attr}', |
| 59 |
$countsIncomplete ? ' data-tab-counts-placeholder="1"' : '', |
| 60 |
$this->tpl('viewLogsTabFiltersListOpen.html') |
| 61 |
); |
| 62 |
if ($sub != 'abj404_captured') { |
| 63 |
$html .= $this->filterItem('', $url, ($filter == 0) ? ' class="current"' : '', __('All', '404-solution'), $this->displayCount($counts['all'] ?? 0, $countsIncomplete)); |
| 64 |
} |
| 65 |
foreach ($types as $type) { |
| 66 |
$item = $this->typedFilterItem($sub, $url, $filter, (int)$type, $counts, $countsIncomplete); |
| 67 |
if ($item !== '') { |
| 68 |
$html .= $item; |
| 69 |
} |
| 70 |
} |
| 71 |
|
| 72 |
$trashurl = $url . '&filter=' . ABJ404_TRASH_FILTER; |
| 73 |
$trashClass = (($tableOptions['filter'] ?? 0) == ABJ404_TRASH_FILTER) ? ' class="current"' : ''; |
| 74 |
$html .= $this->filterItem(' | ', $trashurl, $trashClass, __('Trash', '404-solution'), $this->displayCount($counts['trash'] ?? 0, $countsIncomplete)); |
| 75 |
$html .= $this->tpl('viewLogsTabFiltersListClose.html'); |
| 76 |
$html .= $this->tpl('viewLogsTabFiltersFormGap.html'); |
| 77 |
|
| 78 |
return $html . $this->f->str_replace( |
| 79 |
'{action_url}', |
| 80 |
$this->listTableChrome->getBulkOperationsFormURL($sub, $tableOptions), |
| 81 |
$this->tpl('viewLogsTabFiltersBulkForm.html') |
| 82 |
); |
| 83 |
} |
| 84 |
|
| 85 |
/** @param array<string, mixed> $tableOptions */ |
| 86 |
private function baseUrl(string $sub, array $tableOptions): string { |
| 87 |
$orderby = isset($tableOptions['orderby']) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url'; |
| 88 |
$order = isset($tableOptions['order']) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC'; |
| 89 |
$url = '?page=' . ABJ404_PP; |
| 90 |
if ($sub == 'abj404_captured') { |
| 91 |
$url .= '&subpage=abj404_captured'; |
| 92 |
} else if ($sub == 'abj404_redirects') { |
| 93 |
$url .= '&subpage=abj404_redirects'; |
| 94 |
} else { |
| 95 |
$this->logger->errorMessage('Unexpected sub page: ' . $sub); |
| 96 |
} |
| 97 |
$url .= '&orderby=' . sanitize_text_field($orderby); |
| 98 |
$url .= '&order=' . sanitize_text_field($order); |
| 99 |
|
| 100 |
return $url; |
| 101 |
} |
| 102 |
|
| 103 |
/** |
| 104 |
* @return array{types: array<int, int>, counts: array<string, int>} |
| 105 |
*/ |
| 106 |
private function filterState(string $sub): array { |
| 107 |
global $abj404_redirect_types; |
| 108 |
global $abj404_captured_types; |
| 109 |
|
| 110 |
if ($sub == 'abj404_redirects') { |
| 111 |
$types = $this->numericTypes($abj404_redirect_types, array(ABJ404_STATUS_MANUAL, ABJ404_STATUS_AUTO, ABJ404_STATUS_REGEX)); |
| 112 |
return array('types' => $types, 'counts' => $this->viewReadService->getRedirectStatusCounts()); |
| 113 |
} |
| 114 |
if ($sub == 'abj404_captured') { |
| 115 |
$types = $this->numericTypes($abj404_captured_types, array(ABJ404_STATUS_CAPTURED, ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER)); |
| 116 |
return array('types' => $types, 'counts' => $this->viewReadService->getCapturedStatusCounts()); |
| 117 |
} |
| 118 |
|
| 119 |
$this->logger->debugMessage('Unexpected sub type for tab filter: ' . $sub); |
| 120 |
return array('types' => array(ABJ404_STATUS_CAPTURED, ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER), 'counts' => array()); |
| 121 |
} |
| 122 |
|
| 123 |
/** |
| 124 |
* @param mixed $candidate |
| 125 |
* @param array<int, int> $defaults |
| 126 |
* @return array<int, int> |
| 127 |
*/ |
| 128 |
private function numericTypes($candidate, array $defaults): array { |
| 129 |
if (!is_array($candidate)) { |
| 130 |
return $defaults; |
| 131 |
} |
| 132 |
// One pass: validate and convert together. Two passes (validate |
| 133 |
// everything, then convert everything) left the conversion loop's own |
| 134 |
// guards provably dead -- it could only ever see values the first loop |
| 135 |
// had already accepted -- so its `return $defaults` was unreachable |
| 136 |
// code standing in for a check that had already happened. Appending to |
| 137 |
// $ints also reindexes, which is what the array_values() call the |
| 138 |
// first pass needed was for. |
| 139 |
$ints = array(); |
| 140 |
foreach ($candidate as $value) { |
| 141 |
if (is_int($value)) { |
| 142 |
$ints[] = $value; |
| 143 |
continue; |
| 144 |
} |
| 145 |
if (is_string($value) && ctype_digit($value)) { |
| 146 |
$ints[] = intval($value); |
| 147 |
continue; |
| 148 |
} |
| 149 |
return $defaults; |
| 150 |
} |
| 151 |
if (empty($ints)) { |
| 152 |
return $defaults; |
| 153 |
} |
| 154 |
return $ints; |
| 155 |
} |
| 156 |
|
| 157 |
/** @param array<string, int> $counts */ |
| 158 |
private function typedFilterItem(string $sub, string $url, int $filter, int $type, array $counts, bool $countsIncomplete): string { |
| 159 |
if ($type == ABJ404_STATUS_REGEX) { |
| 160 |
return ''; |
| 161 |
} |
| 162 |
$definition = $this->filterDefinition($type, $counts); |
| 163 |
if ($definition === null) { |
| 164 |
$this->logger->errorMessage('Unrecognized redirect type in View: ' . esc_html((string)$type)); |
| 165 |
$definition = array('title' => __('Unknown', '404-solution'), 'count' => 0); |
| 166 |
} |
| 167 |
|
| 168 |
$prefix = ($sub != 'abj404_captured' || $type != ABJ404_STATUS_CAPTURED) ? ' | ' : ''; |
| 169 |
$class = ($filter == $type) ? ' class="current"' : ''; |
| 170 |
return $this->filterItem( |
| 171 |
$prefix, |
| 172 |
$url . '&filter=' . $type, |
| 173 |
$class, |
| 174 |
$definition['title'], |
| 175 |
$this->displayCount($definition['count'], $countsIncomplete) |
| 176 |
); |
| 177 |
} |
| 178 |
|
| 179 |
/** |
| 180 |
* @param array<string, int> $counts |
| 181 |
* @return array{title: string, count: int}|null |
| 182 |
*/ |
| 183 |
private function filterDefinition(int $type, array $counts): ?array { |
| 184 |
$definitions = array( |
| 185 |
ABJ404_STATUS_MANUAL => array( |
| 186 |
'title' => __('Manual Redirects', '404-solution'), |
| 187 |
'count' => intval($counts['manual'] ?? 0) + intval($counts['regex'] ?? 0), |
| 188 |
), |
| 189 |
ABJ404_STATUS_AUTO => array( |
| 190 |
'title' => __('Automatic Redirects', '404-solution'), |
| 191 |
'count' => intval($counts['auto'] ?? 0), |
| 192 |
), |
| 193 |
ABJ404_STATUS_CAPTURED => array( |
| 194 |
'title' => 'Captured URLs', |
| 195 |
'count' => intval($counts['captured'] ?? 0), |
| 196 |
), |
| 197 |
ABJ404_STATUS_IGNORED => array( |
| 198 |
'title' => 'Ignored 404s', |
| 199 |
'count' => intval($counts['ignored'] ?? 0), |
| 200 |
), |
| 201 |
ABJ404_STATUS_LATER => array( |
| 202 |
'title' => 'Organize Later', |
| 203 |
'count' => intval($counts['later'] ?? 0), |
| 204 |
), |
| 205 |
); |
| 206 |
return $definitions[$type] ?? null; |
| 207 |
} |
| 208 |
|
| 209 |
/** @param int|string $count */ |
| 210 |
private function filterItem(string $prefix, string $url, string $class, string $title, $count): string { |
| 211 |
return $this->f->str_replace( |
| 212 |
array('{prefix}', '{url}', '{class_attr}', '{title}', '{count}'), |
| 213 |
array($prefix, esc_url($url), $class, $title, esc_html((string)$count)), |
| 214 |
$this->tpl('viewLogsTabFilterItem.html') |
| 215 |
); |
| 216 |
} |
| 217 |
|
| 218 |
/** @param mixed $count */ |
| 219 |
private function displayCount($count, bool $incomplete): string { |
| 220 |
return $incomplete ? '-' : (string)intval(is_scalar($count) ? $count : 0); |
| 221 |
} |
| 222 |
|
| 223 |
private function tpl(string $name): string { |
| 224 |
$raw = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . '/../html/' . $name, false); |
| 225 |
return rtrim((string)$raw, "\n"); |
| 226 |
} |
| 227 |
} |
| 228 |
|