PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-per-page-helper.php

class-metasync-per-page-helper.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.25, at includes/class-metasync-per-page-helper.php

301 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Per-page selector helper for Metasync admin list tables.
4 *
5 * Centralises the "results per page" control shared by the Redirections,
6 * 404 Monitor, Changes Log and Media Library (Media Optimization) lists.
7 *
8 * Responsibilities:
9 * - Define the allow-list of page sizes (10 / 20 / 50 / 100). No
10 * arbitrary user-supplied value ever reaches a query — anything outside
11 * this list is rejected and the per-page default for the page is used.
12 * - Resolve the active page size for a given admin list, preferring an
13 * explicit submission on that list's own request key, then the user's
14 * persisted preference (stored in user_meta), and finally the
15 * page-specific default. A submitted valid value is persisted back to
16 * user_meta so it survives across page loads and sessions.
17 * - Render an inline `<select>` with the allowed options, wired to refresh
18 * the current page with the new page size.
19 *
20 * Meta key scheme: `metasync_per_page_<page_key>` (one preference per page per
21 * user), e.g. `metasync_per_page_redirections`, `metasync_per_page_404_monitor`,
22 * `metasync_per_page_sync_log`, `metasync_per_page_media_library`.
23 *
24 * Request key scheme: `per_page_<page_key>`, one per list. A single shared
25 * `per_page` key cannot work here: the Redirections screen renders BOTH the
26 * Redirections and the 404 Monitor table in the same request (see
27 * Metasync_Redirections_Admin::render_tab_content()), so a shared key made each
28 * tab's selector overwrite the other tab's stored preference.
29 *
30 * @package Metasync
31 * @subpackage Metasync/includes
32 */
33
34 // If this file is called directly, abort.
35 if (!defined('ABSPATH')) {
36 exit;
37 }
38
39 class Metasync_Per_Page_Helper
40 {
41 /**
42 * Allowed results-per-page values. Anything else is rejected.
43 */
44 public const ALLOWED_VALUES = [10, 20, 50, 100];
45
46 /**
47 * Get the allow-list of page sizes.
48 *
49 * @return int[]
50 */
51 public static function allowed_values()
52 {
53 return self::ALLOWED_VALUES;
54 }
55
56 /**
57 * The request key carrying a submitted page size for a given list.
58 *
59 * One key per list, so two tables rendered onto the same screen (the
60 * Redirections page renders Redirections AND 404 Monitor in one request)
61 * cannot clobber each other's persisted preference.
62 *
63 * @param string $page_key Unique list identifier (e.g. 'redirections').
64 * @return string
65 */
66 public static function request_key($page_key)
67 {
68 return 'per_page_' . $page_key;
69 }
70
71 /**
72 * The pagination parameter belonging to a given list.
73 *
74 * Each list paginates on its own parameter, because the Redirections screen
75 * shows two tables at once and a shared `paged` would move both. Changing
76 * one list's page size must reset THAT list to page 1 and leave the other
77 * table's page alone.
78 *
79 * @param string $page_key Unique list identifier.
80 * @return string
81 */
82 public static function paged_param($page_key)
83 {
84 $map = [
85 'redirections' => 'paged_redir',
86 '404_monitor' => 'paged_404',
87 'media_library' => 'paged_media',
88 'sync_log' => 'paged',
89 ];
90
91 return isset($map[$page_key]) ? $map[$page_key] : 'paged';
92 }
93
94 /**
95 * Query parameters that describe the visible state of each list.
96 *
97 * Keeping this allow-list beside the per-page contract ensures browser
98 * navigation never copies bulk actions, selected rows or nonces into URLs.
99 *
100 * @param string $page_key Unique list identifier.
101 * @return string[]
102 */
103 public static function state_params($page_key)
104 {
105 $map = [
106 'redirections' => ['status_filter', 'pattern_filter', 'http_code_filter', 's_redir', 'orderby_redir', 'order_redir'],
107 '404_monitor' => ['date_from', 'date_to', 'min_hits', 's_404', 'orderby_404', 'order_404'],
108 'media_library' => ['status_filter', 's', 'orderby_media', 'order_media'],
109 'sync_log' => ['date_range', 'status'],
110 ];
111
112 return isset($map[$page_key]) ? $map[$page_key] : [];
113 }
114
115 /**
116 * Return sanitized, allow-listed request state for links and redirects.
117 *
118 * @param string $page_key Unique list identifier.
119 * @param array|null $request Request source; defaults to $_REQUEST.
120 * @return array<string,string>
121 */
122 public static function request_state($page_key, $request = null)
123 {
124 $request = is_array($request) ? $request : $_REQUEST;
125 $keys = array_merge(self::state_params($page_key), [self::request_key($page_key)]);
126 $state = [];
127
128 foreach ($keys as $key) {
129 if (!isset($request[$key]) || !is_scalar($request[$key])) {
130 continue;
131 }
132
133 $value = (string) $request[$key];
134 if ($value === '') {
135 continue;
136 }
137
138 if ($key === self::request_key($page_key) && !in_array((int) $value, self::ALLOWED_VALUES, true)) {
139 continue;
140 }
141
142 if (function_exists('wp_unslash')) {
143 $value = wp_unslash($value);
144 }
145 $state[$key] = sanitize_text_field($value);
146 }
147
148 // Older links used WordPress's shared `s` parameter. Canonicalize it
149 // into the list-specific key without allowing the two tables rendered
150 // on the combined screen to overwrite one another on new requests.
151 $search_keys = [
152 'redirections' => 's_redir',
153 '404_monitor' => 's_404',
154 ];
155 if (isset($search_keys[$page_key])) {
156 $search_key = $search_keys[$page_key];
157 if (!isset($state[$search_key]) && isset($request['s']) && is_scalar($request['s'])) {
158 $value = (string) $request['s'];
159 if ($value !== '') {
160 if (function_exists('wp_unslash')) {
161 $value = wp_unslash($value);
162 }
163 $state[$search_key] = sanitize_text_field($value);
164 }
165 }
166 }
167
168 return $state;
169 }
170
171 /**
172 * Resolve the active per-page value for a given admin list.
173 *
174 * Resolution order:
175 * 1. `$_REQUEST[$request_key]` when present AND a valid allow-list
176 * integer — also persisted to user_meta so the choice survives
177 * future sessions.
178 * 2. The user's persisted preference (`metasync_per_page_<page_key>`),
179 * when it is a valid allow-list integer.
180 * 3. The supplied `$default`.
181 *
182 * Server-side validation guarantees no arbitrary user-supplied value can
183 * reach a query's LIMIT.
184 *
185 * @param string $page_key Unique list identifier (e.g. 'redirections').
186 * @param int $default Fallback page size for this list.
187 * @param string|null $request_key Query parameter carrying the submitted
188 * value. Defaults to this list's own key.
189 * @return int Validated page size from the allow-list.
190 */
191 public static function resolve($page_key, $default, $request_key = null)
192 {
193 $default = self::clamp_default($default);
194 $request_key = $request_key === null ? self::request_key($page_key) : $request_key;
195 $user_id = function_exists('get_current_user_id') ? get_current_user_id() : 0;
196 $meta_key = 'metasync_per_page_' . $page_key;
197
198 // 1) A freshly submitted value wins and is persisted.
199 if (isset($_REQUEST[$request_key])) {
200 $submitted = (int) $_REQUEST[$request_key];
201 if (in_array($submitted, self::ALLOWED_VALUES, true)) {
202 if ($user_id > 0 && function_exists('update_user_meta')) {
203 update_user_meta($user_id, $meta_key, $submitted);
204 }
205 return $submitted;
206 }
207 }
208
209 // 2) Fall back to the user's saved preference.
210 if ($user_id > 0 && function_exists('get_user_meta')) {
211 $saved = get_user_meta($user_id, $meta_key, true);
212 if ($saved !== '' && $saved !== null && $saved !== false) {
213 $saved = (int) $saved;
214 if (in_array($saved, self::ALLOWED_VALUES, true)) {
215 return $saved;
216 }
217 }
218 }
219
220 // 3) Finally, the page default.
221 return $default;
222 }
223
224 /**
225 * Render an inline rows-per-page `<select>` for the given list.
226 *
227 * The external admin change handler reloads the current URL with the new
228 * page size, so this works without a surrounding <form> and preserves the
229 * rest of the query string. It resets only THIS list's
230 * pagination parameter: page numbers for this list stop meaning anything
231 * once its page size changes, but the other table on a combined screen has
232 * not resized and must keep its page.
233 *
234 * WP_List_Table renders the table nav twice (top and bottom), so callers
235 * must pass `$instance` (the `$which` they were given) to keep the `id`
236 * unique — a repeated `id` is invalid HTML and makes the `<label for>`
237 * ambiguous.
238 *
239 * @param string $page_key Unique list identifier.
240 * @param int $current Currently active page size (selected option).
241 * @param string $instance Position/instance suffix ('top', 'bottom', ...).
242 * Omit for a list that renders one selector.
243 * @param string|null $request_key Query parameter to set on change.
244 * Defaults to this list's own key.
245 * @return string HTML markup for the selector (not echoed).
246 */
247 public static function render_selector($page_key, $current, $instance = '', $request_key = null)
248 {
249 $current = (int) $current;
250 $request_key = $request_key === null ? self::request_key($page_key) : $request_key;
251 $paged_param = self::paged_param($page_key);
252
253 $id = 'metasync-per-page-' . $page_key;
254 if ($instance !== '') {
255 $id .= '-' . $instance;
256 }
257
258 $options = '';
259 foreach (self::ALLOWED_VALUES as $value) {
260 $options .= sprintf(
261 '<option value="%d"%s>%d</option>',
262 $value,
263 selected($value, $current, false),
264 $value
265 );
266 }
267
268 return sprintf(
269 '<span class="metasync-per-page" style="margin-left:8px;">'
270 . '<label for="%1$s" class="screen-reader-text">%2$s</label>'
271 . '<select id="%1$s" name="%3$s" class="metasync-per-page-select" '
272 . 'data-per-page-key="%3$s" data-paged-param="%4$s" '
273 . 'data-page-key="%5$s" data-state-params="%6$s">'
274 . '%7$s'
275 . '</select>'
276 . '</span>',
277 esc_attr($id),
278 esc_html__('Rows per page', 'metasync'),
279 esc_attr($request_key),
280 esc_attr($paged_param),
281 esc_attr($page_key),
282 esc_attr(wp_json_encode(self::state_params($page_key))),
283 $options
284 );
285 }
286
287 /**
288 * Ensure the supplied default is itself a valid allow-list value,
289 * otherwise coerce to the first allow-list entry. Keeps callers safe
290 * even if a custom default slips through.
291 *
292 * @param int $default
293 * @return int
294 */
295 private static function clamp_default($default)
296 {
297 $default = (int) $default;
298 return in_array($default, self::ALLOWED_VALUES, true) ? $default : self::ALLOWED_VALUES[0];
299 }
300 }
301