PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / Integration / Admin_Bar.php

Admin_Bar.php in Code Snippets 4.0.0-beta.2, at php/Integration/Admin_Bar.php

587 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Integration;
4
5 use Code_Snippets\Model\Snippet;
6 use Code_Snippets\REST_API\Snippets\Snippets_REST_Controller;
7 use WP_Admin_Bar;
8 use function Code_Snippets\code_snippets;
9 use function Code_Snippets\get_snippets;
10 use function Code_Snippets\Settings\are_settings_unified;
11 use function Code_Snippets\Settings\get_setting;
12 use const Code_Snippets\PLUGIN_FILE;
13 use const Code_Snippets\PLUGIN_VERSION;
14
15 /**
16 * Register Code Snippets quick links in the WordPress Admin Bar.
17 *
18 * @package Code_Snippets
19 */
20 class Admin_Bar {
21
22 /**
23 * Root node ID.
24 *
25 * @var string
26 */
27 private const ROOT_NODE_ID = 'code-snippets';
28
29 /**
30 * Active snippets pagination query arg.
31 *
32 * @var string
33 */
34 private const ACTIVE_PAGE_QUERY_ARG = 'code_snippets_ab_active_page';
35
36 /**
37 * Inactive snippets pagination query arg.
38 *
39 * @var string
40 */
41 private const INACTIVE_PAGE_QUERY_ARG = 'code_snippets_ab_inactive_page';
42
43 /**
44 * Safe mode node ID.
45 *
46 * @var string
47 */
48 private const SAFE_MODE_NODE_ID = 'code-snippets-safe-mode';
49
50 /**
51 * Script handle.
52 *
53 * @var string
54 */
55 private const SCRIPT_HANDLE = 'code-snippets-admin-bar';
56
57 /**
58 * Stylesheet handle.
59 *
60 * @var string
61 */
62 private const STYLE_HANDLE = 'code-snippets-admin-bar';
63
64 /**
65 * Class constructor.
66 */
67 public function __construct() {
68 add_action( 'admin_bar_menu', [ $this, 'register_nodes' ], 80 );
69
70 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
71 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
72 }
73
74 /**
75 * Enqueue scripts and styles for admin bar nodes.
76 *
77 * @return void
78 */
79 public function enqueue_assets(): void {
80 if ( ! is_admin_bar_showing() ) {
81 return;
82 }
83
84 wp_enqueue_style(
85 self::STYLE_HANDLE,
86 plugins_url( 'dist/admin-bar.css', PLUGIN_FILE ),
87 [ 'admin-bar', 'dashicons' ],
88 PLUGIN_VERSION
89 );
90
91 wp_enqueue_script(
92 self::SCRIPT_HANDLE,
93 plugins_url( 'dist/admin-bar.js', PLUGIN_FILE ),
94 [ 'wp-i18n', 'wp-url' ],
95 PLUGIN_VERSION,
96 [ 'in_footer' => true ]
97 );
98
99 wp_localize_script(
100 self::SCRIPT_HANDLE,
101 'CODE_SNIPPETS_ADMIN_BAR',
102 [
103 'restUrl' => esc_url_raw( rest_url( Snippets_REST_Controller::get_base_route() ) ),
104 'nonce' => wp_create_nonce( 'wp_rest' ),
105 'perPage' => $this->get_snippet_limit(),
106 'isNetwork' => is_network_admin(),
107 'excludeTypes' => [ 'cond' ],
108 // translators: %d: snippet identifier.
109 'snippetPlaceholder' => esc_html__( 'Snippet #%d', 'code-snippets' ),
110 'editUrlBase' => code_snippets()->get_menu_url( 'edit' ),
111 'activeNodeId' => 'wp-admin-bar-' . self::ROOT_NODE_ID . '-active-snippets',
112 'inactiveNodeId' => 'wp-admin-bar-' . self::ROOT_NODE_ID . '-inactive-snippets',
113 ]
114 );
115 }
116
117 /**
118 * Register admin bar nodes.
119 *
120 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
121 *
122 * @return void
123 */
124 public function register_nodes( WP_Admin_Bar $wp_admin_bar ): void {
125 if ( ! is_admin_bar_showing() || ! code_snippets()->current_user_can() ) {
126 return;
127 }
128
129 // Always show safe mode indicator regardless of setting.
130 $this->add_safe_mode_nodes( $wp_admin_bar );
131
132 // Check if admin bar menu is enabled via settings.
133 $is_enabled = get_setting( 'general', 'enable_admin_bar' );
134
135 if ( ! $is_enabled && ! apply_filters( 'code_snippets/admin_bar/enabled', false ) ) {
136 return;
137 }
138
139 $title = sprintf(
140 '<span class="ab-icon code-snippets-admin-bar-icon" aria-hidden="true"></span><span class="ab-label">%s</span>',
141 esc_html__( 'Snippets', 'code-snippets' )
142 );
143
144 $wp_admin_bar->add_node(
145 [
146 'id' => self::ROOT_NODE_ID,
147 'title' => $title,
148 'href' => code_snippets()->get_menu_url( 'manage' ),
149 ]
150 );
151
152 $this->add_quick_links( $wp_admin_bar );
153 $this->add_snippet_listings( $wp_admin_bar );
154 $this->add_safe_mode_link( $wp_admin_bar );
155 }
156
157 /**
158 * Render a small color badge for a snippet's execution kind, matching the
159 * type-badge treatment on the snippets list.
160 *
161 * @param string $kind Execution kind: 'php', 'content', 'css', 'js'.
162 *
163 * @return string Safe HTML.
164 */
165 private function format_kind_badge( string $kind ): string {
166 $kind = 'content' === $kind ? 'html' : $kind;
167 $labels = [
168 'php' => 'PHP',
169 'html' => 'HTML',
170 'css' => 'CSS',
171 'js' => 'JS',
172 'cond' => 'COND',
173 ];
174 $colours = [
175 'php' => '#2271b1',
176 'html' => '#cd4510',
177 'css' => '#9b59b6',
178 'js' => '#f7d67a',
179 'cond' => '#22826f',
180 ];
181 $text_colours = [
182 'js' => '#1c1f20',
183 ];
184
185 $label = $labels[ $kind ] ?? strtoupper( $kind );
186 $colour = $colours[ $kind ] ?? '#5b7290';
187 $text_colour = $text_colours[ $kind ] ?? '#fff';
188
189 return sprintf(
190 '<span class="code-snippets-kind-badge" style="background:%1$s;color:%2$s">%3$s</span> ',
191 esc_attr( $colour ),
192 esc_attr( $text_colour ),
193 esc_html( $label )
194 );
195 }
196
197 /**
198 * Add menu item for safe mode status.
199 *
200 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
201 *
202 * @return void
203 */
204 private function add_safe_mode_nodes( WP_Admin_Bar $wp_admin_bar ): void {
205 if ( ! Evaluate_Functions::is_safe_mode_active() ) {
206 return;
207 }
208
209 $wp_admin_bar->add_node(
210 [
211 'id' => self::SAFE_MODE_NODE_ID,
212 'title' => esc_html__( 'Snippets Safe Mode Active', 'code-snippets' ),
213 'href' => 'https://snipco.de/safe-mode',
214 'parent' => 'top-secondary',
215 'meta' => [
216 'class' => 'code-snippets-safe-mode code-snippets-safe-mode-active',
217 'target' => '_blank',
218 'rel' => 'noopener noreferrer',
219 ],
220 ]
221 );
222 }
223
224 /**
225 * Add a safe mode documentation link under the Code Snippets menu.
226 *
227 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
228 *
229 * @return void
230 */
231 private function add_safe_mode_link( WP_Admin_Bar $wp_admin_bar ): void {
232 $title = sprintf(
233 '%s <span class="code-snippets-external-icon dashicons dashicons-external" aria-hidden="true"></span>',
234 esc_html__( 'Safe Mode', 'code-snippets' )
235 );
236
237 $wp_admin_bar->add_node(
238 [
239 'id' => self::ROOT_NODE_ID . '-safe-mode-doc',
240 'title' => $title,
241 'href' => 'https://snipco.de/safe-mode',
242 'parent' => self::ROOT_NODE_ID,
243 'meta' => [
244 'class' => 'code-snippets-safe-mode',
245 'target' => '_blank',
246 'rel' => 'noopener noreferrer',
247 ],
248 ]
249 );
250 }
251
252 /**
253 * Add quick links to common Code Snippets screens.
254 *
255 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
256 *
257 * @return void
258 */
259 private function add_quick_links( WP_Admin_Bar $wp_admin_bar ): void {
260 $plugin = code_snippets();
261 $is_licensed = $plugin->licensing->is_licensed();
262 $upgrade_url = self_admin_url( 'admin.php?page=code_snippets_upgrade' );
263
264 $wp_admin_bar->add_node(
265 [
266 'id' => self::ROOT_NODE_ID . '-manage',
267 'title' => esc_html_x( 'Manage', 'snippets', 'code-snippets' ),
268 'href' => $plugin->get_menu_url( 'manage' ),
269 'parent' => self::ROOT_NODE_ID,
270 ]
271 );
272
273 $statuses = [
274 'all' => _x( 'All Snippets', 'snippets', 'code-snippets' ),
275 'active' => _x( 'Active Snippets', 'snippets', 'code-snippets' ),
276 'inactive' => _x( 'Inactive Snippets', 'snippets', 'code-snippets' ),
277 ];
278
279 foreach ( $statuses as $status => $label ) {
280 $wp_admin_bar->add_node(
281 [
282 'id' => self::ROOT_NODE_ID . "-status-$status",
283 'title' => esc_html( $label ),
284 'href' => esc_url( add_query_arg( 'status', $status, $plugin->get_menu_url( 'manage' ) ) ),
285 'parent' => self::ROOT_NODE_ID . '-manage',
286 ]
287 );
288 }
289
290 $wp_admin_bar->add_node(
291 [
292 'id' => self::ROOT_NODE_ID . '-add',
293 'title' => esc_html_x( 'Add New', 'snippet', 'code-snippets' ),
294 'href' => $plugin->get_menu_url( 'add' ),
295 'parent' => self::ROOT_NODE_ID,
296 ]
297 );
298
299 $types = [
300 'php' => _x( 'Functions (PHP)', 'snippet type', 'code-snippets' ),
301 'html' => _x( 'Content (HTML)', 'snippet type', 'code-snippets' ),
302 'css' => _x( 'Styles (CSS)', 'snippet type', 'code-snippets' ),
303 'js' => _x( 'Scripts (JS)', 'snippet type', 'code-snippets' ),
304 'cond' => _x( 'Conditions (COND)', 'snippet type', 'code-snippets' ),
305 ];
306
307 $types = array_intersect_key( $types, array_flip( Snippet::get_types() ) );
308 $pro_types = [ 'css', 'js', 'cond' ];
309
310 foreach ( $types as $type => $label ) {
311 $is_disabled = in_array( $type, $pro_types, true ) && ! $is_licensed;
312
313 $url = $is_disabled ?
314 $upgrade_url :
315 add_query_arg( 'type', $type, $plugin->get_menu_url( 'add' ) );
316
317 $wp_admin_bar->add_node(
318 [
319 'id' => self::ROOT_NODE_ID . "-add-$type",
320 'title' => esc_html( $label ),
321 'href' => esc_url( $url ),
322 'parent' => self::ROOT_NODE_ID . '-add',
323 'meta' => $is_disabled ? [ 'class' => 'code-snippets-disabled' ] : [],
324 ]
325 );
326 }
327
328 $wp_admin_bar->add_node(
329 [
330 'id' => self::ROOT_NODE_ID . '-insights',
331 'title' => esc_html_x( 'Insights', 'snippets', 'code-snippets' ),
332 'href' => $plugin->get_menu_url( 'insights' ),
333 'parent' => self::ROOT_NODE_ID,
334 ]
335 );
336
337 $wp_admin_bar->add_node(
338 [
339 'id' => self::ROOT_NODE_ID . '-import',
340 'title' => esc_html_x( 'Import', 'snippets', 'code-snippets' ),
341 'href' => $plugin->get_menu_url( 'import' ),
342 'parent' => self::ROOT_NODE_ID,
343 ]
344 );
345
346 $wp_admin_bar->add_node(
347 [
348 'id' => self::ROOT_NODE_ID . '-settings',
349 'title' => esc_html_x( 'Settings', 'snippets', 'code-snippets' ),
350 'href' => $plugin->get_menu_url( 'settings', are_settings_unified() ? 'network' : 'admin' ),
351 'parent' => self::ROOT_NODE_ID,
352 ]
353 );
354 }
355
356 /**
357 * Add a list of snippets under the active and inactive statuses.
358 *
359 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
360 *
361 * @return void
362 */
363 private function add_snippet_listings( WP_Admin_Bar $wp_admin_bar ): void {
364 $items_per_page = $this->get_snippet_limit();
365 if ( $items_per_page < 1 ) {
366 return;
367 }
368
369 $plugin = code_snippets();
370
371 $snippets = array_filter(
372 get_snippets(),
373 static function ( Snippet $snippet ): bool {
374 return ! $snippet->trashed && ! $snippet->is_condition();
375 }
376 );
377
378 $active_snippets = array_values(
379 array_filter(
380 $snippets,
381 static function ( Snippet $snippet ): bool {
382 return $snippet->active;
383 }
384 )
385 );
386
387 $inactive_snippets = array_values(
388 array_filter(
389 $snippets,
390 static function ( Snippet $snippet ): bool {
391 return ! $snippet->active;
392 }
393 )
394 );
395
396 usort(
397 $active_snippets,
398 static function ( Snippet $a, Snippet $b ): int {
399 return strcasecmp( $a->display_name, $b->display_name );
400 }
401 );
402
403 usort(
404 $inactive_snippets,
405 static function ( Snippet $a, Snippet $b ): int {
406 return strcasecmp( $a->display_name, $b->display_name );
407 }
408 );
409
410 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
411 $active_page = isset( $_GET[ self::ACTIVE_PAGE_QUERY_ARG ] ) ? absint( $_GET[ self::ACTIVE_PAGE_QUERY_ARG ] ) : 1;
412
413 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
414 $inactive_page = isset( $_GET[ self::INACTIVE_PAGE_QUERY_ARG ] ) ? absint( $_GET[ self::INACTIVE_PAGE_QUERY_ARG ] ) : 1;
415
416 $active_total_pages = max( 1, (int) ceil( count( $active_snippets ) / $items_per_page ) );
417 $inactive_total_pages = max( 1, (int) ceil( count( $inactive_snippets ) / $items_per_page ) );
418
419 $active_page = max( 1, min( $active_page, $active_total_pages ) );
420 $inactive_page = max( 1, min( $inactive_page, $inactive_total_pages ) );
421
422 $active_offset = ( $active_page - 1 ) * $items_per_page;
423 $inactive_offset = ( $inactive_page - 1 ) * $items_per_page;
424
425 $active_page_snippets = array_slice( $active_snippets, $active_offset, $items_per_page );
426 $inactive_page_snippets = array_slice( $inactive_snippets, $inactive_offset, $items_per_page );
427
428 $wp_admin_bar->add_node(
429 [
430 'id' => self::ROOT_NODE_ID . '-active-snippets',
431 // translators: %d: number of active snippets.
432 'title' => sprintf( esc_html__( 'Active snippets (%d)', 'code-snippets' ), count( $active_snippets ) ),
433 'href' => esc_url( add_query_arg( 'status', 'active', $plugin->get_menu_url( 'manage' ) ) ),
434 'parent' => self::ROOT_NODE_ID,
435 ]
436 );
437
438 if ( $active_total_pages > 1 ) {
439 $wp_admin_bar->add_node(
440 [
441 'id' => self::ROOT_NODE_ID . '-active-pagination',
442 'title' => $this->get_pagination_controls_html( 'active', $active_page, $active_total_pages, self::ACTIVE_PAGE_QUERY_ARG ),
443 'parent' => self::ROOT_NODE_ID . '-active-snippets',
444 'meta' => [ 'class' => 'code-snippets-pagination-node' ],
445 ]
446 );
447 }
448
449 foreach ( $active_page_snippets as $snippet ) {
450 $wp_admin_bar->add_node(
451 [
452 'id' => self::ROOT_NODE_ID . '-snippet-' . $snippet->id,
453 'title' => $this->format_snippet_title( $snippet ),
454 'href' => esc_url( add_query_arg( 'id', $snippet->id, $plugin->get_menu_url( 'edit' ) ) ),
455 'parent' => self::ROOT_NODE_ID . '-active-snippets',
456 'meta' => [ 'class' => 'code-snippets-snippet-item' ],
457 ]
458 );
459 }
460
461 $wp_admin_bar->add_node(
462 [
463 'id' => self::ROOT_NODE_ID . '-inactive-snippets',
464 // translators: %d: number of inactive snippets.
465 'title' => sprintf( esc_html__( 'Inactive snippets (%d)', 'code-snippets' ), count( $inactive_snippets ) ),
466 'href' => esc_url( add_query_arg( 'status', 'inactive', $plugin->get_menu_url( 'manage' ) ) ),
467 'parent' => self::ROOT_NODE_ID,
468 ]
469 );
470
471 if ( $inactive_total_pages > 1 ) {
472 $wp_admin_bar->add_node(
473 [
474 'id' => self::ROOT_NODE_ID . '-inactive-pagination',
475 'title' => $this->get_pagination_controls_html( 'inactive', $inactive_page, $inactive_total_pages, self::INACTIVE_PAGE_QUERY_ARG ),
476 'parent' => self::ROOT_NODE_ID . '-inactive-snippets',
477 'meta' => [ 'class' => 'code-snippets-pagination-node' ],
478 ]
479 );
480 }
481
482 foreach ( $inactive_page_snippets as $snippet ) {
483 $wp_admin_bar->add_node(
484 [
485 'id' => self::ROOT_NODE_ID . '-snippet-' . $snippet->id,
486 'title' => $this->format_snippet_title( $snippet ),
487 'href' => esc_url( add_query_arg( 'id', $snippet->id, $plugin->get_menu_url( 'edit' ) ) ),
488 'parent' => self::ROOT_NODE_ID . '-inactive-snippets',
489 'meta' => [ 'class' => 'code-snippets-snippet-item' ],
490 ]
491 );
492 }
493 }
494
495 /**
496 * Build an admin bar snippet title including type prefix.
497 *
498 * @param Snippet $snippet Snippet object.
499 *
500 * @return string Safe HTML.
501 */
502 private function format_snippet_title( Snippet $snippet ): string {
503 return $this->format_kind_badge( $snippet->type ) . esc_html( $snippet->display_name );
504 }
505
506 /**
507 * Retrieve the number of snippets to show per page in the admin bar.
508 *
509 * @return int
510 */
511 private function get_snippet_limit(): int {
512 $limit = (int) get_setting( 'general', 'admin_bar_snippet_limit' );
513
514 if ( $limit < 1 ) {
515 $limit = 20;
516 }
517
518 return max( 1, (int) apply_filters( 'code_snippets/admin_bar/snippet_limit', $limit ) );
519 }
520
521 /**
522 * Build pagination controls HTML for a snippet listing submenu.
523 *
524 * @param string $status Snippet status: "active" or "inactive".
525 * @param int $page Current page.
526 * @param int $total_pages Total pages.
527 * @param string $page_query_arg Query arg used for progressive enhancement.
528 *
529 * @return string
530 */
531 private function get_pagination_controls_html( string $status, int $page, int $total_pages, string $page_query_arg ): string {
532 $first_url = remove_query_arg( $page_query_arg );
533 $prev_url = $page > 2 ? add_query_arg( $page_query_arg, $page - 1 ) : remove_query_arg( $page_query_arg );
534 $next_url = add_query_arg( $page_query_arg, $page + 1 );
535 $last_url = add_query_arg( $page_query_arg, $total_pages );
536
537 $disabled_first = $page <= 1 ? 'true' : 'false';
538 $disabled_prev = $page <= 1 ? 'true' : 'false';
539 $disabled_next = $page >= $total_pages ? 'true' : 'false';
540 $disabled_last = $page >= $total_pages ? 'true' : 'false';
541
542 $html = sprintf(
543 '<span class="code-snippets-pagination-controls" data-status="%1$s" data-page="%2$d" data-total-pages="%3$d" data-query-arg="%15$s">' .
544 '<a class="code-snippets-pagination-button" href="%4$s" data-action="first" aria-disabled="%9$s">&laquo;</a>' .
545 '<a class="code-snippets-pagination-button" href="%5$s" data-action="prev" aria-disabled="%10$s">&lsaquo; %6$s</a>' .
546 '<span class="code-snippets-pagination-button code-snippets-pagination-page">%7$s (%2$d/%3$d)</span>' .
547 '<a class="code-snippets-pagination-button" href="%8$s" data-action="next" aria-disabled="%11$s">%12$s &rsaquo;</a>' .
548 '<a class="code-snippets-pagination-button" href="%13$s" data-action="last" aria-disabled="%14$s">&raquo;</a>' .
549 '</span>',
550 esc_attr( $status ),
551 $page,
552 $total_pages,
553 esc_url( $first_url ),
554 esc_url( $prev_url ),
555 esc_html__( 'Back', 'code-snippets' ),
556 esc_html__( 'Page', 'code-snippets' ),
557 esc_url( $next_url ),
558 $disabled_first,
559 $disabled_prev,
560 $disabled_next,
561 esc_html__( 'Next', 'code-snippets' ),
562 esc_url( $last_url ),
563 $disabled_last,
564 esc_attr( $page_query_arg )
565 );
566
567 return wp_kses(
568 $html,
569 [
570 'span' => [
571 'class' => [],
572 'data-status' => [],
573 'data-page' => [],
574 'data-total-pages' => [],
575 'data-query-arg' => [],
576 ],
577 'a' => [
578 'class' => [],
579 'href' => [],
580 'data-action' => [],
581 'aria-disabled' => [],
582 ],
583 ]
584 );
585 }
586 }
587