PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 1.6.2 All 116 releases
top-10 / includes / admin / class-admin-banner.php

class-admin-banner.php in WebberZone Top 10 — Popular Posts trunk, at includes/admin/class-admin-banner.php

464 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Banner helper.
4 *
5 * @package WebberZone\Top_Ten
6 */
7
8 namespace WebberZone\Top_Ten\Admin;
9
10 use WebberZone\Top_Ten\Util\Hook_Registry;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Reusable admin banner helper that is configured via constructor values.
18 *
19 * Copy-paste friendly: adjust namespaces and configuration per plugin.
20 *
21 * @since 4.2.0
22 */
23 class Admin_Banner {
24
25 private const DEFAULT_STYLE_VERSION = '1.0.1';
26
27 /**
28 * Configuration array for the banner output.
29 *
30 * @var array<string, mixed>
31 */
32 public array $config = array();
33
34 /**
35 * Derived class names keyed by component.
36 *
37 * @var array<string, array<int, string>>
38 */
39 public array $class_names = array();
40
41 /**
42 * Localized strings.
43 *
44 * @var array<string, string>
45 */
46 public array $strings = array();
47
48 /**
49 * Style configuration.
50 *
51 * @var array<string, mixed>
52 */
53 public array $style = array();
54
55 /**
56 * Base class prefix shared by all banners.
57 *
58 * @var string
59 */
60 public string $base_prefix = 'wz-admin-banner';
61
62 /**
63 * Unique class prefix derived from the provided prefix.
64 *
65 * @var string
66 */
67 public string $unique_prefix = 'admin-banner';
68
69 /**
70 * Constructor.
71 *
72 * @param array $config Configuration arguments for the banner.
73 */
74 public function __construct( array $config ) {
75 $defaults = array(
76 'capability' => 'manage_options',
77 'allow_network' => false,
78 'prefix' => '',
79 'screen_ids' => array(),
80 'page_slugs' => array(),
81 'sections' => array(),
82 'exclude_screen_bases' => array( 'post', 'post-new' ),
83 'strings' => array(),
84 'link_target' => '_self',
85 'style' => array(),
86 );
87
88 $this->config = wp_parse_args( $config, $defaults );
89 $this->strings = $this->prepare_strings( $this->config['strings'] ?? array() );
90
91 $this->config['sections'] = $this->sanitize_sections( $this->config['sections'] );
92
93 $this->unique_prefix = $this->resolve_wrapper_prefix( (string) $this->config['prefix'] );
94 $this->class_names = $this->derive_class_names();
95 $this->style = $this->prepare_style_config( $this->config['style'] ?? array() );
96
97 if ( empty( $this->config['screen_ids'] ) ) {
98 $this->config['screen_ids'] = $this->collect_targets_from_sections( 'screen_ids' );
99 }
100
101 if ( empty( $this->config['page_slugs'] ) ) {
102 $this->config['page_slugs'] = $this->collect_targets_from_sections( 'page_slugs' );
103 }
104
105 $this->hooks();
106 }
107
108 /**
109 * Register hooks.
110 */
111 public function hooks(): void {
112 Hook_Registry::add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_styles' ) );
113 Hook_Registry::add_action( 'in_admin_header', array( $this, 'render' ) );
114 }
115
116 /**
117 * Enqueue banner styles if required on the current screen or page slug.
118 */
119 public function maybe_enqueue_styles(): void {
120 if ( empty( $this->style['url'] ) ) {
121 return;
122 }
123
124 $screen = ! is_network_admin() ? get_current_screen() : null;
125 $page_slug = $this->get_request_page_slug();
126
127 if ( $screen instanceof \WP_Screen && $this->should_render_on_screen( $screen, $page_slug ) ) {
128 $this->enqueue_style();
129 return;
130 }
131
132 if ( '' !== $page_slug && in_array( $page_slug, $this->config['page_slugs'], true ) ) {
133 $this->enqueue_style();
134 }
135 }
136
137 /**
138 * Render the admin banner markup when conditions are met.
139 */
140 public function render(): void {
141 if ( is_network_admin() && ! $this->config['allow_network'] ) {
142 return;
143 }
144
145 $screen = get_current_screen();
146 if ( ! ( $screen instanceof \WP_Screen ) || ! current_user_can( $this->config['capability'] ) ) {
147 return;
148 }
149
150 $page_slug = $this->get_request_page_slug();
151
152 if ( ! $this->should_render_on_screen( $screen, $page_slug ) ) {
153 return;
154 }
155
156 $current_section = $this->resolve_current_section( $screen, $page_slug );
157
158 ?>
159 <div class="<?php echo esc_attr( $this->class_attr( 'wrapper' ) ); ?>" role="region" aria-label="<?php echo esc_attr( $this->strings['region_label'] ); ?>">
160 <div class="<?php echo esc_attr( $this->class_attr( 'intro' ) ); ?>">
161 <?php if ( ! empty( $this->strings['eyebrow'] ) ) : ?>
162 <span class="<?php echo esc_attr( $this->class_attr( 'eyebrow' ) ); ?>"><?php echo esc_html( $this->strings['eyebrow'] ); ?></span>
163 <?php endif; ?>
164 <?php if ( ! empty( $this->strings['title'] ) ) : ?>
165 <p class="<?php echo esc_attr( $this->class_attr( 'title' ) ); ?>"><?php echo esc_html( $this->strings['title'] ); ?></p>
166 <?php endif; ?>
167 <?php if ( ! empty( $this->strings['text'] ) ) : ?>
168 <p class="<?php echo esc_attr( $this->class_attr( 'text' ) ); ?>"><?php echo esc_html( $this->strings['text'] ); ?></p>
169 <?php endif; ?>
170 </div>
171 <nav class="<?php echo esc_attr( $this->class_attr( 'links_wrapper' ) ); ?>" aria-label="<?php echo esc_attr( $this->strings['nav_label'] ); ?>">
172 <?php foreach ( $this->config['sections'] as $section_key => $section ) : ?>
173 <?php
174 $link_text = $section['label'] ?? '';
175 $link_url = $section['url'] ?? '';
176 $link_target = $section['target'] ?? $this->config['link_target'];
177 $link_rel = $section['rel'] ?? '';
178
179 if ( empty( $link_text ) || empty( $link_url ) ) {
180 continue;
181 }
182
183 $link_classes = $this->get_section_link_classes( $section );
184 if ( $section_key === $current_section ) {
185 $link_classes = array_merge( $link_classes, $this->class_names['link_current'] ?? array() );
186 }
187 ?>
188 <a class="<?php echo esc_attr( $this->implode_classes( $link_classes ) ); ?>" href="<?php echo esc_url( $link_url ); ?>" target="<?php echo esc_attr( $link_target ); ?>"<?php echo empty( $link_rel ) ? '' : ' rel="' . esc_attr( $link_rel ) . '"'; ?>>
189 <?php echo esc_html( $link_text ); ?>
190 </a>
191 <?php endforeach; ?>
192 </nav>
193 </div>
194 <?php
195 }
196
197 /**
198 * Enqueue the banner stylesheet.
199 */
200 public function enqueue_style(): void {
201 wp_register_style(
202 $this->style['handle'],
203 $this->style['url'],
204 (array) $this->style['deps'],
205 $this->style['version']
206 );
207 wp_enqueue_style( $this->style['handle'] );
208 }
209
210 /**
211 * Determine whether the banner should display on the current screen.
212 *
213 * @param \WP_Screen $screen Current admin screen.
214 * @param string $page_slug Current request page slug.
215 */
216 public function should_render_on_screen( \WP_Screen $screen, string $page_slug ): bool {
217 $screen_base = (string) $screen->base;
218 if ( '' !== $screen_base && in_array( $screen_base, (array) $this->config['exclude_screen_bases'], true ) ) {
219 return false;
220 }
221
222 $screen_id = (string) $screen->id;
223 if ( '' !== $screen_id && in_array( $screen_id, (array) $this->config['screen_ids'], true ) ) {
224 return true;
225 }
226
227 if ( '' !== $page_slug && in_array( $page_slug, (array) $this->config['page_slugs'], true ) ) {
228 return true;
229 }
230
231 return false;
232 }
233
234 /**
235 * Resolve the banner section to highlight based on current screen or page slug.
236 *
237 * @param \WP_Screen $screen Current admin screen.
238 * @param string $page_slug Current request page slug.
239 */
240 public function resolve_current_section( \WP_Screen $screen, string $page_slug ): string {
241 $screen_id = (string) $screen->id;
242
243 foreach ( $this->config['sections'] as $section_key => $section ) {
244 if ( ! empty( $section['screen_ids'] ) && in_array( $screen_id, (array) $section['screen_ids'], true ) ) {
245 return $section_key;
246 }
247 }
248
249 foreach ( $this->config['sections'] as $section_key => $section ) {
250 if ( ! empty( $section['page_slugs'] ) && in_array( $page_slug, (array) $section['page_slugs'], true ) ) {
251 return $section_key;
252 }
253 }
254
255 return '';
256 }
257
258 /**
259 * Prepare localized strings.
260 *
261 * @param array $strings Raw strings array.
262 */
263 public function prepare_strings( array $strings ): array {
264 $defaults = array(
265 'region_label' => '',
266 'nav_label' => '',
267 'eyebrow' => '',
268 'title' => '',
269 'text' => '',
270 );
271
272 return wp_parse_args( $strings, $defaults );
273 }
274
275 /**
276 * Resolve the wrapper prefix based on base prefix provided.
277 *
278 * @param string $prefix Base prefix.
279 */
280 public function resolve_wrapper_prefix( string $prefix ): string {
281 $prefix = sanitize_key( $prefix );
282
283 if ( '' === $prefix ) {
284 return $this->base_prefix;
285 }
286
287 return false === strpos( $prefix, $this->base_prefix ) ? "{$prefix}-admin-banner" : $prefix;
288 }
289
290 /**
291 * Prepare style configuration.
292 *
293 * @param array $style Style configuration.
294 */
295 public function prepare_style_config( array $style ): array {
296 $defaults = array(
297 'handle' => $this->sanitize_handle( "{$this->unique_prefix}-styles" ),
298 'deps' => array(),
299 'version' => self::DEFAULT_STYLE_VERSION,
300 'filename' => 'admin-banner',
301 'url' => '',
302 );
303
304 $style_config = wp_parse_args( $style, $defaults );
305
306 if ( empty( $style_config['url'] ) ) {
307 $assets_base = trailingslashit( plugin_dir_url( __FILE__ ) ) . 'css/';
308 $min_suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
309 $rtl_suffix = is_rtl() ? '-rtl' : '';
310 $style_config['url'] = $assets_base . $style_config['filename'] . $rtl_suffix . $min_suffix . '.css';
311 }
312
313 return $style_config;
314 }
315
316 /**
317 * Sanitize the sections configuration.
318 *
319 * @param array $sections Sections configuration.
320 *
321 * @return array
322 */
323 public function sanitize_sections( array $sections ): array {
324 $sanitized = array();
325
326 foreach ( $sections as $key => $section ) {
327 if ( empty( $section['label'] ) || empty( $section['url'] ) ) {
328 continue;
329 }
330
331 $section_key = sanitize_key( $key );
332
333 $sanitized[ $section_key ] = array(
334 'label' => $section['label'],
335 'url' => $section['url'],
336 'type' => isset( $section['type'] ) ? sanitize_key( $section['type'] ) : 'secondary',
337 'target' => isset( $section['target'] ) ? $section['target'] : '_self',
338 'rel' => isset( $section['rel'] ) ? $section['rel'] : '',
339 'screen_ids' => isset( $section['screen_ids'] ) ? (array) $section['screen_ids'] : array(),
340 'page_slugs' => isset( $section['page_slugs'] ) ? array_map( 'sanitize_key', (array) $section['page_slugs'] ) : array(),
341 );
342 }
343
344 return $sanitized;
345 }
346
347 /**
348 * Derive class names following the provided prefix alongside the base prefix.
349 *
350 * @return array<string, array<int, string>>
351 */
352 public function derive_class_names(): array {
353 $build = function ( string $suffix = '' ): array {
354 $classes = array( $this->base_prefix . $suffix );
355
356 if ( $this->unique_prefix !== $this->base_prefix ) {
357 $classes[] = $this->unique_prefix . $suffix;
358 }
359
360 return $classes;
361 };
362
363 return array(
364 'wrapper' => $build(),
365 'intro' => $build( '__intro' ),
366 'eyebrow' => $build( '__eyebrow' ),
367 'title' => $build( '__title' ),
368 'text' => $build( '__text' ),
369 'links_wrapper' => $build( '__links' ),
370 'link' => $build( '__link' ),
371 'link_primary' => $build( '__link--primary' ),
372 'link_secondary' => $build( '__link--secondary' ),
373 'link_current' => $build( '__link--current' ),
374 'link_new' => $build( '__link--new' ),
375 );
376 }
377
378 /**
379 * Collect screen IDs or page slugs from the sections configuration.
380 *
381 * @param string $target_key screen_ids|page_slugs key.
382 *
383 * @return array
384 */
385 public function collect_targets_from_sections( string $target_key ): array {
386 $values = array();
387
388 foreach ( $this->config['sections'] as $section ) {
389 if ( empty( $section[ $target_key ] ) ) {
390 continue;
391 }
392 foreach ( (array) $section[ $target_key ] as $value ) {
393 $values[] = (string) $value;
394 }
395 }
396
397 return array_values( array_unique( array_filter( $values ) ) );
398 }
399
400 /**
401 * Retrieve the CSS classes for a section link.
402 *
403 * @param array $section Section configuration.
404 */
405 public function get_section_link_classes( array $section ): array {
406 $classes = $this->class_names['link'] ?? array();
407 $type = isset( $section['type'] ) ? sanitize_key( $section['type'] ) : 'secondary';
408 $type = '' !== $type ? $type : 'secondary';
409 $type_key = "link_{$type}";
410
411 if ( isset( $this->class_names[ $type_key ] ) ) {
412 $classes = array_merge( $classes, (array) $this->class_names[ $type_key ] );
413 } elseif ( isset( $this->class_names['link_secondary'] ) ) {
414 $classes = array_merge( $classes, (array) $this->class_names['link_secondary'] );
415 }
416
417 return array_values( array_unique( array_filter( $classes ) ) );
418 }
419
420 /**
421 * Implode a class array into a string.
422 *
423 * @param array $classes Class list.
424 * @return string Class attribute string.
425 */
426 public function implode_classes( array $classes ): string {
427 return implode( ' ', array_unique( array_filter( $classes ) ) );
428 }
429
430 /**
431 * Retrieve a flattened class attribute by key.
432 *
433 * @param string $key Classes array key.
434 * @return string Class attribute string.
435 */
436 public function class_attr( string $key ): string {
437 return $this->implode_classes( $this->class_names[ $key ] ?? array() );
438 }
439
440 /**
441 * Sanitize a style handle.
442 *
443 * @param string $handle Raw handle.
444 */
445 public function sanitize_handle( string $handle ): string {
446 return sanitize_title_with_dashes( $handle );
447 }
448
449 /**
450 * Get the current page slug from the request.
451 */
452 public function get_request_page_slug(): string {
453 if ( isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
454 $page_raw = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
455 } else {
456 return '';
457 }
458
459 $page_slug = strtolower( (string) strtok( $page_raw, '&' ) );
460
461 return sanitize_key( $page_slug );
462 }
463 }
464