PluginProbe
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking / 1.3.1
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking v1.3.1
1.5.0 1.4.0 1.3.0 1.3.1 trunk 0.0.0-alpha.1 0.0.0-alpha.2 0.0.0-alpha.3 0.0.1-beta.1 0.0.1-beta.2 0.0.1-beta.3 0.0.1-beta.4 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4
surecookie / admin / menu.php

menu.php in SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking 1.3.1, at admin/menu.php

488 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Menu handler for SureCookie plugin.
4 *
5 * @package SureCookie
6 * @subpackage SureCookie\Admin
7 * @since 0.0.1
8 */
9
10 namespace SureCookie\Admin;
11
12 use SureCookie\Inc\Database\ConsentLog;
13 use SureCookie\Inc\Functions\Get;
14 use SureCookie\Inc\Functions\Helper;
15 use SureCookie\Inc\Functions\Sanitize;
16 use SureCookie\Inc\Functions\Settings;
17 use SureCookie\Inc\Traits\GetInstance;
18 use SureCookie\Inc\Utils\LocalizeData;
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Menu handler for SureCookie plugin.
24 *
25 * This class handles the registration and rendering of the admin menu.
26 *
27 * @since 0.0.1
28 */
29 class Menu {
30 use GetInstance;
31
32 /**
33 * Constructor.
34 *
35 * @since 0.0.1
36 */
37 public function __construct() {
38 add_action( 'admin_menu', [ $this, 'register_admin_menu' ] );
39 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
40 add_filter( 'plugin_action_links_' . SURECOOKIE_BASE, [ $this, 'plugin_action_links' ] );
41
42 if ( ! Helper::is_pro_active() && Settings::get( 'top_level_menu_enabled' ) ) {
43 add_action( 'admin_footer', [ $this, 'print_upgrade_submenu_assets' ] );
44 }
45 }
46
47 /**
48 * Register the admin menu.
49 *
50 * @since 0.0.1
51 * @return void
52 */
53 public function register_admin_menu(): void {
54 $show_top_level = Settings::get( 'top_level_menu_enabled' );
55
56 if ( $show_top_level ) {
57 // Resolve the unread-logs count once so the top-level attention dot
58 // and the Logs submenu badge stay in sync from a single lookup.
59 $unread_logs = $this->get_unread_logs_count();
60
61 add_menu_page(
62 'SureCookie',
63 $this->get_top_level_menu_title( $unread_logs ),
64 SURECOOKIE_CAPABILITY,
65 SURECOOKIE_PREFIX,
66 [ $this, 'render_admin_page' ],
67 'none',
68 58
69 );
70
71 $submenus = [
72 [
73 'id' => SURECOOKIE_PREFIX,
74 'page_title' => __( 'Dashboard', 'surecookie' ),
75 ],
76 [
77 'id' => SURECOOKIE_PREFIX . '#/cookie-manager/scan',
78 'page_title' => __( 'Tracking Manager', 'surecookie' ),
79 ],
80 [
81 'id' => SURECOOKIE_PREFIX . '#/analytics',
82 'page_title' => __( 'Logs', 'surecookie' ),
83 'menu_title' => $this->get_logs_submenu_title( $unread_logs ),
84 ],
85 [
86 'id' => SURECOOKIE_PREFIX . '#/banner/content',
87 'page_title' => __( 'Settings', 'surecookie' ),
88 ],
89 [
90 'id' => SURECOOKIE_PREFIX . '#/learn',
91 'page_title' => __( 'Learn', 'surecookie' ),
92 ],
93 ];
94
95 foreach ( $submenus as $submenu ) {
96 add_submenu_page(
97 SURECOOKIE_PREFIX,
98 $submenu['page_title'],
99 $submenu['menu_title'] ?? $submenu['page_title'],
100 SURECOOKIE_CAPABILITY,
101 $submenu['id'],
102 [ $this, 'render_admin_page' ]
103 );
104 }
105
106 if ( ! Helper::is_pro_active() ) {
107 $this->register_upgrade_submenu();
108 }
109
110 return;
111 }
112
113 add_options_page(
114 'SureCookie',
115 'SureCookie',
116 SURECOOKIE_CAPABILITY,
117 SURECOOKIE_PREFIX,
118 [ $this, 'render_admin_page' ]
119 );
120 }
121
122 /**
123 * Render the admin page.
124 *
125 * @since 0.0.1
126 * @return void
127 */
128 public function render_admin_page(): void {
129 if ( ! current_user_can( SURECOOKIE_CAPABILITY ) ) {
130 wp_die( esc_html__( 'You don\'t have access to this page.', 'surecookie' ) );
131 }
132
133 echo '<div class="wrap">';
134 echo '<div id="surecookie-admin-root"></div>';
135 echo '</div>';
136 }
137
138 /**
139 * Enqueue admin assets.
140 *
141 * @since 0.0.1
142 * @param string $hook The current admin page.
143 * @return void
144 */
145 public function enqueue_admin_assets( $hook ): void {
146 $this->enqueue_admin_menu_icon_style();
147
148 $allowed_hooks = [
149 'toplevel_page_surecookie',
150 'settings_page_surecookie',
151 'surecookie_page_surecookie-banner',
152 'surecookie_page_surecookie-settings',
153 'surecookie_page_surecookie-advanced',
154 ];
155
156 if ( ! in_array( $hook, $allowed_hooks, true ) ) {
157 return;
158 }
159
160 $asset_file = SURECOOKIE_DIR . 'build/admin.asset.php';
161
162 if ( ! file_exists( $asset_file ) ) {
163 return;
164 }
165
166 $assets = require $asset_file;
167 $extra_dependencies = [ 'updates' ];
168
169 // Enqueue WordPress media uploader.
170 wp_enqueue_media();
171
172 // Enqueue CodeMirror for CSS editor.
173 wp_enqueue_code_editor( [ 'type' => 'text/css' ] );
174
175 wp_enqueue_script(
176 'surecookie-admin',
177 SURECOOKIE_URL . 'build/admin.js',
178 array_merge( $assets['dependencies'] ?? [], $extra_dependencies ),
179 $assets['version'] ?? SURECOOKIE_VERSION,
180 true
181 );
182 wp_set_script_translations( 'surecookie-admin', 'surecookie', SURECOOKIE_DIR . 'languages' );
183
184 wp_enqueue_style(
185 'surecookie-admin',
186 SURECOOKIE_URL . 'build/' . Get::admin_style_css(),
187 [],
188 SURECOOKIE_VERSION
189 );
190
191 wp_enqueue_style(
192 'surecookie-jodit-library',
193 SURECOOKIE_URL . 'assets/css/' . Get::jodit_style_css(),
194 [],
195 SURECOOKIE_VERSION . '-jodit-4.12.18'
196 );
197
198 wp_enqueue_style(
199 'surecookie-admin-styles',
200 SURECOOKIE_URL . 'assets/css/' . Get::shared_style_css(),
201 [ 'surecookie-admin' ],
202 SURECOOKIE_VERSION
203 );
204
205 // Add palette CSS variables from stored option.
206 $palette_css = Sanitize::stylesheet( Get::palette_root_css() );
207 if ( ! empty( $palette_css ) ) {
208 wp_add_inline_style( 'surecookie-admin', wp_strip_all_tags( $palette_css ) );
209 }
210
211 do_action( 'surecookie_admin_enqueue_scripts_after', $hook );
212
213 // Localize admin data using utility.
214 LocalizeData::localize_admin_script( 'surecookie-admin' );
215 }
216
217 /**
218 * Print styles and a click handler that open the Upgrade submenu in a new tab.
219 *
220 * Runs on every admin page because the submenu appears in the sidebar site-wide.
221 * The CSS/JS selectors are scoped to `#toplevel_page_surecookie` so there is no
222 * side effect on other admin screens.
223 *
224 * @since 1.0.0
225 * @return void
226 */
227 public function print_upgrade_submenu_assets(): void {
228 ?>
229 <style>
230 #adminmenu #toplevel_page_surecookie .wp-submenu a[href*="surecookie.com/pricing"] {
231 background-color: #377A00 !important;
232 color: #ffffff !important;
233 font-weight: 500;
234 font-size: 13px;
235 line-height: 20px;
236 padding-right: 12px !important;
237 padding-left: 12px !important;
238 }
239 #adminmenu #toplevel_page_surecookie .wp-submenu a[href*="surecookie.com/pricing"]:hover,
240 #adminmenu #toplevel_page_surecookie .wp-submenu a[href*="surecookie.com/pricing"]:focus {
241 background-color: #2F6A00 !important;
242 color: #ffffff !important;
243 }
244 </style>
245 <script type="text/javascript">
246 document.addEventListener( 'DOMContentLoaded', function () {
247 var link = document.querySelector( '#adminmenu #toplevel_page_surecookie .wp-submenu a[href*="surecookie.com/pricing"]' );
248 if ( ! link ) {
249 return;
250 }
251 link.addEventListener( 'click', function ( e ) {
252 e.preventDefault();
253 window.open( link.href, '_blank', 'noopener,noreferrer' );
254 } );
255 } );
256 </script>
257 <?php
258 }
259
260 /**
261 * Add action links to the plugin list page.
262 *
263 * @since 0.0.1
264 * @param array<string, string> $links Existing action links.
265 * @return array<string, string> Modified action links.
266 */
267 public function plugin_action_links( $links ): array {
268 $admin_url = admin_url( 'admin.php' );
269
270 $new_links = [
271 'settings' => sprintf(
272 '<a href="%s">%s</a>',
273 esc_url( add_query_arg( 'page', SURECOOKIE_PREFIX, $admin_url ) . '#/banner/content' ),
274 esc_html__( 'Settings', 'surecookie' )
275 ),
276 ];
277
278 return array_merge( $new_links, $links );
279 }
280
281 /**
282 * Register the "Upgrade" submenu under SureCookie.
283 *
284 * Uses a stable internal slug so that WordPress-generated hook names
285 * (`load-{$slug}`, `admin_page_{$slug}`, etc.) stay clean. The external
286 * pricing URL is then swapped into the `$submenu` global so WordPress
287 * renders the item as an external link. The JS click handler printed in
288 * `print_upgrade_submenu_assets()` intercepts the click and opens it in a
289 * new tab, so the page is never actually navigated to - `'__return_null'`
290 * is therefore a safe no-op callback.
291 *
292 * @since 1.0.0
293 * @return void
294 */
295 private function register_upgrade_submenu(): void {
296 $upgrade_slug = SURECOOKIE_PREFIX . '_upgrade';
297
298 add_submenu_page(
299 SURECOOKIE_PREFIX,
300 __( 'Upgrade', 'surecookie' ),
301 __( 'Upgrade', 'surecookie' ),
302 SURECOOKIE_CAPABILITY,
303 $upgrade_slug,
304 '__return_null'
305 );
306
307 $upgrade_url = Helper::get_marketing_link(
308 'pricing/',
309 'pricing_submenu_link_upgrade'
310 );
311
312 // Swap the internal slug with the external pricing URL. Writing to the
313 // $submenu global is the canonical WP pattern for linking a submenu item
314 // to an external URL without polluting hook names with query strings.
315 global $submenu;
316
317 if ( ! isset( $submenu[ SURECOOKIE_PREFIX ] ) || ! is_array( $submenu[ SURECOOKIE_PREFIX ] ) ) {
318 return;
319 }
320
321 foreach ( $submenu[ SURECOOKIE_PREFIX ] as $key => $item ) {
322 if ( isset( $item[2] ) && $upgrade_slug === $item[2] ) {
323 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional: swap internal slug for external upgrade URL.
324 $submenu[ SURECOOKIE_PREFIX ][ $key ][2] = $upgrade_url;
325 break;
326 }
327 }
328 }
329
330 /**
331 * Enqueue the inline style used for the top-level admin menu icon.
332 *
333 * @since 0.0.1
334 * @return void
335 */
336 private function enqueue_admin_menu_icon_style(): void {
337 if ( ! is_admin() || ! current_user_can( SURECOOKIE_CAPABILITY ) ) {
338 return;
339 }
340
341 if ( ! Settings::get( 'top_level_menu_enabled' ) ) {
342 return;
343 }
344
345 wp_register_style( 'surecookie-admin-menu-icon', false, [], SURECOOKIE_VERSION );
346 wp_enqueue_style( 'surecookie-admin-menu-icon' );
347 wp_add_inline_style( 'surecookie-admin-menu-icon', $this->get_admin_menu_icon_css() );
348 wp_add_inline_style( 'surecookie-admin-menu-icon', $this->get_admin_menu_separator_css() );
349 wp_add_inline_style( 'surecookie-admin-menu-icon', $this->get_admin_menu_dot_css() );
350 }
351
352 /**
353 * Count the current user's unread consent logs.
354 *
355 * Centralizes the capability check and count lookup so the top-level menu
356 * attention dot and the Logs submenu badge are driven by a single query.
357 *
358 * @since 1.2.3
359 * @return int
360 */
361 private function get_unread_logs_count(): int {
362 if ( ! current_user_can( SURECOOKIE_CAPABILITY ) ) {
363 return 0;
364 }
365
366 return ConsentLog::count_unread_logs_for_user( get_current_user_id() );
367 }
368
369 /**
370 * Build the top-level menu title, appending an attention dot for unread logs.
371 *
372 * The dot mirrors the pattern used across BSF plugins (e.g. SureForms) to
373 * flag unseen activity on the parent menu item. It is removed client-side
374 * once the Logs page is viewed (see AdminApp.jsx) and stops rendering on the
375 * next load once the logs are marked as viewed server-side.
376 *
377 * @since 1.2.3
378 * @param int $unread_count Number of unread consent logs.
379 * @return string
380 */
381 private function get_top_level_menu_title( int $unread_count ): string {
382 $label = 'SureCookie';
383
384 if ( $unread_count <= 0 ) {
385 return $label;
386 }
387
388 return $label . ' <span class="surecookie-update-dot" data-surecookie-logs-dot="true" aria-hidden="true"></span>';
389 }
390
391 /**
392 * Build the Logs submenu title with an unread-count badge when needed.
393 *
394 * @since 1.2.0
395 * @since 1.2.3 Accepts the pre-computed unread count instead of querying it.
396 * @param int $unread_count Number of unread consent logs.
397 * @return string
398 */
399 private function get_logs_submenu_title( int $unread_count ): string {
400 $label = __( 'Logs', 'surecookie' );
401
402 if ( $unread_count <= 0 ) {
403 return $label;
404 }
405
406 return sprintf(
407 '%1$s <span class="update-plugins count-%2$d" data-surecookie-logs-badge="true"><span class="plugin-count">%3$s</span></span>',
408 $label,
409 $unread_count,
410 esc_html( number_format_i18n( $unread_count ) )
411 );
412 }
413
414 /**
415 * Build the submenu group-separator stylesheet.
416 *
417 * @since 1.1.0
418 * @return string
419 */
420 private function get_admin_menu_separator_css(): string {
421 return '#toplevel_page_surecookie li {
422 clear: both;
423 }
424 #toplevel_page_surecookie li:not(:last-child) a[href^="admin.php?page=surecookie"]:after {
425 border-bottom: 1px solid hsla(0,0%,100%,.2);
426 display: block;
427 float: left;
428 margin: 13px -15px 8px;
429 content: "";
430 width: calc(100% + 26px);
431 }
432 #toplevel_page_surecookie li:not(:last-child) a[href^="admin.php?page=surecookie#/cookie-manager/scan"]:after,
433 #toplevel_page_surecookie li:not(:last-child) a[href^="admin.php?page=surecookie#/banner/content"]:after {
434 content: none;
435 }';
436 }
437
438 /**
439 * Build the unread-logs attention dot stylesheet for the top-level menu.
440 *
441 * @since 1.2.3
442 * @return string
443 */
444 private function get_admin_menu_dot_css(): string {
445 return '#toplevel_page_surecookie .wp-menu-name .surecookie-update-dot {
446 display: inline-block;
447 width: 8px;
448 height: 8px;
449 margin-left: 6px;
450 border-radius: 50%;
451 background: #d63638;
452 vertical-align: middle;
453 }';
454 }
455
456 /**
457 * Build the admin menu icon stylesheet.
458 *
459 * @since 0.0.1
460 * @return string
461 */
462 private function get_admin_menu_icon_css(): string {
463 $icon_svg = 'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none"><path d="M97.0156 9.74499C120.404 0.0570803 146.141 -2.47884 170.971 2.45983C195.8 7.39873 218.609 19.59 236.51 37.4911L200.307 73.6942C189.566 62.9535 175.88 55.64 160.982 52.6766C146.085 49.7134 130.643 51.233 116.609 57.0458C102.576 62.8586 90.5814 72.7032 82.1426 85.3329C73.7038 97.9625 69.1992 112.811 69.1992 128.001C69.1992 140.594 72.2983 152.952 78.166 164.007C78.2596 164.006 78.3534 164.001 78.4473 164.001C89.4146 164.001 98.5033 172.026 100.17 182.524C105.185 179.335 111.141 177.497 117.525 177.518C133.572 177.572 146.835 189.36 149.221 204.729C153.155 204.564 157.087 204.1 160.982 203.325C175.88 200.362 189.566 193.046 200.307 182.306L236.51 218.511C218.609 236.412 195.8 248.603 170.971 253.542C146.141 258.48 120.404 255.945 97.0156 246.257C73.6273 236.569 53.6369 220.163 39.5723 199.114C25.5075 178.065 18 153.317 18 128.001C18 102.685 25.5074 77.9371 39.5723 56.8876C53.6369 35.8385 73.6272 19.433 97.0156 9.74499Z" fill="#A0A5AA"/><path d="M164 140.001C170.627 140.001 176 145.373 176 152.001C176 158.628 170.627 164.001 164 164.001C157.373 164.001 152 158.628 152 152.001C152 145.373 157.373 140.001 164 140.001Z" fill="#A0A5AA"/><path d="M108 108.001C114.627 108.001 120 113.373 120 120.001C120 126.628 114.627 132.001 108 132.001C101.373 132.001 96.0004 126.628 96 120.001C96 113.373 101.373 108.001 108 108.001Z" fill="#A0A5AA"/><path d="M152 76.0008C158.627 76.0008 164 81.3734 164 88.0008C164 94.6279 158.627 100.001 152 100.001C145.373 100.001 140 94.6279 140 88.0008C140 81.3734 145.373 76.0008 152 76.0008Z" fill="#A0A5AA"/></svg>' );
464 $active_icon_svg = 'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none"><path d="M97.0156 9.74499C120.404 0.0570803 146.141 -2.47884 170.971 2.45983C195.8 7.39873 218.609 19.59 236.51 37.4911L200.307 73.6942C189.566 62.9535 175.88 55.64 160.982 52.6766C146.085 49.7134 130.643 51.233 116.609 57.0458C102.576 62.8586 90.5814 72.7032 82.1426 85.3329C73.7038 97.9625 69.1992 112.811 69.1992 128.001C69.1992 140.594 72.2983 152.952 78.166 164.007C78.2596 164.006 78.3534 164.001 78.4473 164.001C89.4146 164.001 98.5033 172.026 100.17 182.524C105.185 179.335 111.141 177.497 117.525 177.518C133.572 177.572 146.835 189.36 149.221 204.729C153.155 204.564 157.087 204.1 160.982 203.325C175.88 200.362 189.566 193.046 200.307 182.306L236.51 218.511C218.609 236.412 195.8 248.603 170.971 253.542C146.141 258.48 120.404 255.945 97.0156 246.257C73.6273 236.569 53.6369 220.163 39.5723 199.114C25.5075 178.065 18 153.317 18 128.001C18 102.685 25.5074 77.9371 39.5723 56.8876C53.6369 35.8385 73.6272 19.433 97.0156 9.74499Z" fill="#FFFFFF"/><path d="M164 140.001C170.627 140.001 176 145.373 176 152.001C176 158.628 170.627 164.001 164 164.001C157.373 164.001 152 158.628 152 152.001C152 145.373 157.373 140.001 164 140.001Z" fill="#FFFFFF"/><path d="M108 108.001C114.627 108.001 120 113.373 120 120.001C120 126.628 114.627 132.001 108 132.001C101.373 132.001 96.0004 126.628 96 120.001C96 113.373 101.373 108.001 108 108.001Z" fill="#FFFFFF"/><path d="M152 76.0008C158.627 76.0008 164 81.3734 164 88.0008C164 94.6279 158.627 100.001 152 100.001C145.373 100.001 140 94.6279 140 88.0008C140 81.3734 145.373 76.0008 152 76.0008Z" fill="#FFFFFF"/></svg>' );
465
466 return implode(
467 "\n",
468 [
469 '#toplevel_page_surecookie .wp-menu-image:before {',
470 "\tcontent: '';",
471 "\tmask-image: url(" . wp_json_encode( $icon_svg ) . ');',
472 "\tmask-size: contain;",
473 "\tmask-repeat: no-repeat;",
474 "\tmask-position: center;",
475 "\tbackground-color: currentColor;",
476 '}',
477 '#toplevel_page_surecookie .wp-menu-image {',
478 "\talign-content: center;",
479 '}',
480 '#toplevel_page_surecookie.current .wp-menu-image:before,',
481 '#toplevel_page_surecookie.wp-has-current-submenu .wp-menu-image:before {',
482 "\tmask-image: url(" . wp_json_encode( $active_icon_svg ) . ');',
483 '}',
484 ]
485 );
486 }
487 }
488