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 / AdminPaginationLinks.php

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

232 lines 12.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 * Builds WordPress-style pagination controls for admin list pages.
9 */
10 class ABJ_404_Solution_AdminPaginationLinks {
11
12 /** @var ABJ_404_Solution_Functions */
13 private $f;
14 /** @var ABJ_404_Solution_PluginLogic */
15 private $logic;
16 /** @var ABJ_404_Solution_View_Shared */
17 private $shared;
18 /** @var ABJ_404_Solution_ViewReadServiceInterface */
19 private $viewReadService;
20
21 /** @param ABJ_404_Solution_ViewReadServiceInterface $viewReadService */
22 public function __construct(ABJ_404_Solution_Functions $functions,
23 ABJ_404_Solution_PluginLogic $pluginLogic, ABJ_404_Solution_View_Shared $shared,
24 $viewReadService) {
25 $this->f = $functions;
26 $this->logic = $pluginLogic;
27 $this->shared = $shared;
28 $this->viewReadService = $viewReadService;
29 }
30
31 /** @param array<string, mixed> $tableOptionOverrides Internal per-request read options. */
32 public function render(string $sub, array $tableOptionOverrides = array()): string {
33 return ABJ_404_Solution_TableRenderTranslationTracer::traceScope(
34 'pagination',
35 'state|sanitize|Loading|item(s)|page count|navigation|URL|nonce|refresh available',
36 function () use ($sub, $tableOptionOverrides): string {
37 $state = $this->paginationState($sub, $tableOptionOverrides);
38 return $this->renderTemplate($sub, $state);
39 }
40 );
41 }
42
43 /**
44 * @param array<string, mixed> $tableOptionOverrides Internal per-request read options.
45 * @return array{tableOptions: array<string, mixed>, logsid: string, orderby: string, order: string,
46 * filter: string, paged: int, perpage: int, totalPages: int, numRecords: int, filterText: string,
47 * urls: array{first: string, previous: string, next: string, last: string}}
48 */
49 private function paginationState(string $sub, array $tableOptionOverrides = array()): array {
50 $tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub);
51 $tableOptions = array_replace($tableOptions, $tableOptionOverrides);
52 $logsid = array_key_exists('logsid', $tableOptions) && is_scalar($tableOptions['logsid']) ? $tableOptions['logsid'] : 0;
53 $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
54 $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
55 $filter = array_key_exists('filter', $tableOptions) && is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0;
56
57 $logsidForUrl = $this->scalarToString($logsid);
58 $filterForUrl = (int)$filter;
59 $url = $this->baseUrl($sub, $logsidForUrl, $orderby, $order, $filterForUrl);
60 $numRecords = (int)(($sub == 'abj404_logs')
61 ? $this->viewReadService->getLogsCount((int)$logsid)
62 : $this->viewReadService->getRedirectsForViewCount($sub, $tableOptions));
63
64 $perpage = absint(array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : ABJ404_OPTION_MIN_PERPAGE);
65 if ($perpage == 0) {
66 $perpage = ABJ404_OPTION_MIN_PERPAGE;
67 }
68 $paged = absint(array_key_exists('paged', $tableOptions) && is_scalar($tableOptions['paged']) ? $tableOptions['paged'] : 1);
69 if ($paged == 0) {
70 $paged = 1;
71 }
72
73 $totalPages = max(1, (int)ceil(max(0, $numRecords) / $perpage));
74
75 $urls = $this->navigationUrls($url, $paged, $totalPages);
76 $filterText = array_key_exists('filterText', $tableOptions) && is_string($tableOptions['filterText']) ? $tableOptions['filterText'] : '';
77 if ($filterText != '') {
78 $urls = $this->appendFilterText($urls, $filterText);
79 }
80
81 return array(
82 'tableOptions' => $tableOptions,
83 'logsid' => $logsidForUrl,
84 'orderby' => $orderby,
85 'order' => $order,
86 'filter' => (string)$filterForUrl,
87 'paged' => $paged,
88 'perpage' => $perpage,
89 'totalPages' => $totalPages,
90 'numRecords' => $numRecords,
91 'filterText' => $filterText,
92 'urls' => $urls,
93 );
94 }
95
96 private function baseUrl(string $sub, string $logsid, string $orderby, string $order, int $filter): string {
97 $url = '?page=' . ABJ404_PP;
98 if ($sub !== '') {
99 $url .= '&subpage=' . rawurlencode((string)$sub);
100 }
101 if ($sub == 'abj404_logs') {
102 $url .= '&id=' . $logsid;
103 }
104 $url .= '&orderby=' . sanitize_text_field($orderby);
105 $url .= '&order=' . sanitize_text_field($order);
106 // Emit the signed filter value: the trash (-1) and handled (-2) views
107 // are negative sentinels. absint() would fold -1 -> 1 (Manual) and
108 // -2 -> 2 (Auto), so paginating within Trash/Handled would silently jump
109 // to a different tab. $filter is already an int from paginationState().
110 $url .= '&filter=' . (int)$filter;
111 return $url;
112 }
113
114 /**
115 * @return array{first: string, previous: string, next: string, last: string}
116 */
117 private function navigationUrls(string $url, int $paged, int $totalPages): array {
118 $prevurl = ($paged == 1) ? $url : $url . '&paged=' . ($paged - 1);
119 if ($paged + 1 > $totalPages) {
120 $nexturl = ($paged == 1) ? $url : $url . '&paged=' . $paged;
121 $lasturl = ($paged == 1) ? $url : $url . '&paged=' . $paged;
122 } else {
123 $nexturl = $url . '&paged=' . ($paged + 1);
124 $lasturl = $url . '&paged=' . $totalPages;
125 }
126
127 return array(
128 'first' => $url,
129 'previous' => $prevurl,
130 'next' => $nexturl,
131 'last' => $lasturl,
132 );
133 }
134
135 /**
136 * @param array{first: string, previous: string, next: string, last: string} $urls
137 * @return array{first: string, previous: string, next: string, last: string}
138 */
139 private function appendFilterText(array $urls, string $filterText): array {
140 $encoded = rawurlencode($filterText);
141 foreach ($urls as $key => $url) {
142 $urls[$key] = $url . '&filterText=' . $encoded;
143 }
144 return $urls;
145 }
146
147 /**
148 * @param array{tableOptions: array<string, mixed>, logsid: string, orderby: string, order: string,
149 * filter: string, paged: int, perpage: int, totalPages: int, numRecords: int, filterText: string,
150 * urls: array{first: string, previous: string, next: string, last: string}} $state
151 */
152 private function renderTemplate(string $sub, array $state): string {
153 $tableOptions = $state['tableOptions'];
154 $logsid = $state['logsid'];
155 $orderby = $state['orderby'];
156 $order = $state['order'];
157 $filter = $state['filter'];
158 $paged = $state['paged'];
159 $totalPages = $state['totalPages'];
160 $numRecords = $state['numRecords'];
161 $filterText = $state['filterText'];
162 $urls = $state['urls'];
163 // Match the WordPress core WP_List_Table::pagination() text shape:
164 // "{N} items" outer count and "{X} of {Y}" inline indicator. The
165 // earlier "1 - 25 of 487 redirects" / "Page 25 of 487" wording made
166 // the strip too wide to fit on the same tablenav row as bulk actions.
167 $currentlyShowingText = $numRecords < 0
168 ? '<span aria-hidden="true">-</span><span class="screen-reader-text">'
169 . esc_html__('Loading...', '404-solution') . '</span>'
170 : sprintf(
171 /* translators: %s is the total record count, already number-formatted for the current locale */
172 _n('%s item', '%s items', $numRecords, '404-solution'),
173 number_format_i18n($numRecords)
174 );
175 $currentPageText = $numRecords < 0
176 ? '<span aria-hidden="true">-</span><span class="screen-reader-text">'
177 . esc_html__('Loading...', '404-solution') . '</span>'
178 : sprintf(
179 /* translators: %1$d is current page number, %2$d is total page count */
180 esc_html__('%1$d of %2$d', '404-solution'),
181 $paged,
182 $totalPages
183 );
184
185 $html = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . '/../html/paginationLinks.html');
186 $html = $this->f->str_replace('{TEXT_BEFORE_LINKS}', $currentlyShowingText, $html);
187 $html = $this->f->str_replace('{BTN_FIRST_PAGE}', $this->pageButton($paged <= 1, $urls['first'], esc_attr__('Go to first page', '404-solution'), '&laquo;'), $html);
188 $html = $this->f->str_replace('{BTN_PREV_PAGE}', $this->pageButton($paged <= 1, $urls['previous'], esc_attr__('Go to previous page', '404-solution'), '&lsaquo;'), $html);
189 $html = $this->f->str_replace('{TEXT_CURRENT_PAGE}', $currentPageText, $html);
190 $html = $this->f->str_replace('{BTN_NEXT_PAGE}', $this->pageButton($paged >= $totalPages, $urls['next'], esc_attr__('Go to next page', '404-solution'), '&rsaquo;'), $html);
191 $html = $this->f->str_replace('{BTN_LAST_PAGE}', $this->pageButton($paged >= $totalPages, $urls['last'], esc_attr__('Go to last page', '404-solution'), '&raquo;'), $html);
192 $html = $this->f->str_replace('{filterText}', esc_attr($filterText), $html);
193 $html = $this->f->str_replace('{data-pagination-ajax-url}', esc_attr(admin_url('admin-ajax.php')), $html);
194 $html = $this->f->str_replace('{data-pagination-ajax-action}', esc_attr('ajaxUpdatePaginationLinks'), $html);
195 $html = $this->f->str_replace('{data-pagination-ajax-subpage}', esc_attr($sub), $html);
196 $html = $this->f->str_replace('{data-pagination-ajax-nonce}', esc_attr(wp_create_nonce('abj404_updatePaginationLink')), $html);
197 $html = $this->f->str_replace('{data-lazy-backfill-ajax-url}', esc_attr(admin_url('admin-ajax.php')), $html);
198 $html = $this->f->str_replace('{data-lazy-backfill-nonce}', esc_attr(wp_create_nonce('abj404_runLazyBackfill')), $html);
199 $html = $this->f->str_replace('{data-pagination-current-signature}', esc_attr($this->shared->getCurrentTableDataSignature($sub)), $html);
200 $html = $this->f->str_replace('{data-pagination-current-orderby}', esc_attr($orderby), $html);
201 $html = $this->f->str_replace('{data-pagination-current-order}', esc_attr($order), $html);
202 $html = $this->f->str_replace('{data-pagination-current-filter}', esc_attr($filter), $html);
203 $html = $this->f->str_replace('{data-pagination-current-paged}', esc_attr((string)$paged), $html);
204 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
205 $scoreRangeForAttr = is_string($rawScoreRange) ? $rawScoreRange : 'all';
206 $html = $this->f->str_replace('{data-pagination-current-score-range}', esc_attr($scoreRangeForAttr), $html);
207 $html = $this->f->str_replace('{data-pagination-current-logsid}', esc_attr($logsid), $html);
208 $autoRefresh = (($sub === 'abj404_redirects' || $sub === 'abj404_captured' || $sub === 'abj404_logs') ? '1' : '0');
209 $html = $this->f->str_replace('{data-pagination-auto-refresh}', esc_attr($autoRefresh), $html);
210 $html = $this->f->str_replace('{data-pagination-refresh-available-text}', esc_attr(__('Refresh available', '404-solution')), $html);
211
212 return $this->f->doNormalReplacements($html);
213 }
214
215 private function pageButton(bool $disabled, string $href, string $label, string $glyph): string {
216 if ($disabled) {
217 return $this->f->str_replace(array('{label}', '{glyph}'), array($label, $glyph), $this->tpl('viewLogsPaginationBtnDisabled.html'));
218 }
219 return $this->f->str_replace(array('{href}', '{label}', '{glyph}'), array(esc_url($href), $label, $glyph), $this->tpl('viewLogsPaginationBtnLink.html'));
220 }
221
222 /** @param bool|float|int|string $value */
223 private function scalarToString($value): string {
224 return (string)$value;
225 }
226
227 private function tpl(string $name): string {
228 $raw = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . '/../html/' . $name, false);
229 return rtrim((string)$raw, "\n");
230 }
231 }
232