PluginProbe
Broken Link Notifier / trunk
Broken Link Notifier vtrunk
1.3.8 1.3.7.6 trunk 1.3.0 1.3.1.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.7.1 1.3.7.2 1.3.7.3 1.3.7.4 1.3.7.5
broken-link-notifier / includes / menu.php

menu.php in Broken Link Notifier trunk, at includes/menu.php

1,253 lines 48.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin options page
4 */
5
6 // Exit if accessed directly.
7 if ( !defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11
12 /**
13 * Initiate the class
14 */
15 if ( !is_network_admin() ) {
16 add_action( 'init', function() {
17 (new BLNOTIFIER_MENU)->init();
18 } );
19 }
20
21
22 /**
23 * Main plugin class.
24 */
25 class BLNOTIFIER_MENU {
26
27 /**
28 * The menu slug
29 *
30 * @var string
31 */
32 public $page_slug = 'blnotifier-settings';
33
34
35 /**
36 * The menu items
37 *
38 * @var array
39 */
40 public $menu_items;
41
42
43 /**
44 * The capability required to access the menu
45 *
46 * @var string
47 */
48 public $capability = 'manage_broken_links';
49
50
51 /**
52 * Constructor
53 */
54 public function __construct() {
55
56 // The menu items
57 $this->menu_items = [
58 'results' => [ __( 'Results', 'broken-link-notifier' ) ],
59 'omit-links' => [ __( 'Omitted Links', 'broken-link-notifier' ), 'edit-tags.php?taxonomy=omit-links' ],
60 'omit-pages' => [ __( 'Omitted Pages', 'broken-link-notifier' ), 'edit-tags.php?taxonomy=omit-pages' ],
61 'scan-single' => [ __( 'Page Scan', 'broken-link-notifier' ) ],
62 'scan-multi' => [ __( 'Multi-Scan', 'broken-link-notifier' ) ],
63 'link-search' => [ __( 'Link Search', 'broken-link-notifier' ) ],
64 'export' => [ __( 'Export', 'broken-link-notifier' ) ],
65 'settings' => [ __( 'Settings', 'broken-link-notifier' ) ],
66 'help' => [ __( 'Help', 'broken-link-notifier' ) ],
67 ];
68
69 } // End __construct()
70
71
72 /**
73 * Load on init
74 */
75 public function init() {
76
77 // Add the menu
78 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
79
80 // Update role after sett
81 add_action( 'admin_init', [ $this, 'update_roles' ] );
82
83 // Fix the Manage link to show active
84 add_filter( 'parent_file', [ $this, 'submenus' ] );
85
86 // Settings page fields
87 add_action( 'admin_init', [ $this, 'settings_fields' ] );
88
89 // Enqueue script
90 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
91
92 // AJAX test notification
93 add_action( 'wp_ajax_blnotifier_test_notification', [ $this, 'ajax_test_notification' ] );
94
95 } // End init()
96
97
98 /**
99 * Add page to Tools menu
100 *
101 * @return void
102 */
103 public function admin_menu() {
104 // Get the current user's roles
105 $current_user = wp_get_current_user();
106 $user_roles = (array) $current_user->roles;
107
108 $capability = 'do_not_allow';
109 $show_menu = false;
110
111 if ( in_array( 'administrator', $user_roles ) ) {
112 $capability = 'manage_options';
113 $show_menu = true;
114
115 } else {
116 $allowed_roles = get_option( 'blnotifier_editable_roles', [] );
117 if ( is_array( $allowed_roles ) && !empty( $allowed_roles ) ) {
118 foreach ( $allowed_roles as $role_slug => $value ) {
119 $role_slug = sanitize_key( $role_slug );
120
121 if ( in_array( $role_slug, $user_roles ) ) {
122 $capability = $this->capability;
123 $show_menu = true;
124 break;
125 }
126 }
127 }
128 }
129
130 if ( !$show_menu ) {
131 return;
132 }
133
134 // Count broken links
135 $count = (new BLNOTIFIER_HELPERS)->count_broken_links();
136 $notif = $count > 0 ? ' <span class="awaiting-mod">'.(new BLNOTIFIER_HELPERS)->count_broken_links().'</span>' : '';
137
138 // Add the menu
139 add_menu_page(
140 BLNOTIFIER_NAME,
141 BLNOTIFIER_NAME. $notif,
142 $capability,
143 BLNOTIFIER_TEXTDOMAIN,
144 [ $this, 'settings_page' ],
145 'dashicons-editor-unlink'
146 );
147
148 // Add the submenus
149 global $submenu;
150 foreach ( $this->menu_items as $key => $menu_item ) {
151 $link = isset( $menu_item[1] ) ? $menu_item[1] : 'admin.php?page='.BLNOTIFIER_TEXTDOMAIN.'&tab='.$key;
152 $submenu[ BLNOTIFIER_TEXTDOMAIN ][] = [ $menu_item[0], $capability, $link ];
153 }
154 } // End admin_menu()
155
156
157 /**
158 * Update user roles
159 *
160 * @return void
161 */
162 public function update_roles() {
163 if ( isset( $_GET[ 'settings-updated' ] ) && $_GET[ 'settings-updated' ] == 'true' ) {
164
165 $allowed_roles = get_option( 'blnotifier_editable_roles', [] );
166 if ( is_array( $allowed_roles ) && !empty( $allowed_roles ) ) {
167 $saniotized_allowed_roles = [];
168 foreach ( $allowed_roles as $role_slug => $value ) {
169 $saniotized_allowed_roles[] = sanitize_key( $role_slug );
170 }
171 $allowed_roles = $saniotized_allowed_roles;
172 }
173
174 $editable_roles = $this->get_editable_roles_choices();
175 if ( !empty( $editable_roles ) && is_array( $editable_roles ) ) {
176
177 foreach ( $editable_roles as $editable_role_slug => $label ) {
178 $role = get_role( $editable_role_slug );
179 if ( !$role ) {
180 continue;
181 }
182
183 if ( in_array( $editable_role_slug, $allowed_roles ) && ! $role->has_cap( $this->capability ) ) {
184 $role->add_cap( $this->capability );
185 } elseif ( $role->has_cap( $this->capability ) ) {
186 $role->remove_cap( $this->capability );
187 }
188 }
189 }
190 }
191 } // End update_roles()
192
193
194 /**
195 * Fix the Manage link to show active
196 *
197 * @param string $parent_file
198 * @return string
199 */
200 public function submenus( $parent_file ) {
201 global $submenu_file, $current_screen;
202 $options_page = 'toplevel_page_'.BLNOTIFIER_TEXTDOMAIN;
203
204 // Top level page
205 if ( $current_screen->id == $options_page ) {
206 $tab = (new BLNOTIFIER_HELPERS)->get_tab() ?? '';
207 $submenu_file = 'admin.php?page='.BLNOTIFIER_TEXTDOMAIN.'&tab='.$tab;
208
209 // Taxonomies first
210 } elseif ( $current_screen->id == 'edit-omit-links' ) {
211 $submenu_file = 'edit-tags.php?taxonomy=omit-links';
212 $parent_file = $this->get_plugin_page_short_path( null );
213 } elseif ( $current_screen->id == 'edit-omit-pages' ) {
214 $submenu_file = 'edit-tags.php?taxonomy=omit-pages';
215 $parent_file = $this->get_plugin_page_short_path( null );
216 }
217
218 // Return
219 return $parent_file;
220 } // End submenus()
221
222
223 /**
224 * Settings page
225 *
226 * @return void
227 */
228 public function settings_page() {
229 include BLNOTIFIER_PLUGIN_INCLUDES_PATH.'page.php';
230 } // End settings_page()
231
232
233 /**
234 * Settings fields
235 *
236 * @return void
237 */
238 public function settings_fields() {
239 // Add section
240 add_settings_section(
241 'general',
242 'Settings',
243 '',
244 $this->page_slug
245 );
246
247 // Has updated settings
248 $has_updated_settings = 'blnotifier_has_updated_settings';
249 register_setting( $this->page_slug, $has_updated_settings, [ $this, 'sanitize_boolean' ] );
250
251 // Pause front-end scanning
252 $pause_frontend_scanning_option_name = 'blnotifier_pause_frontend_scanning';
253 register_setting( $this->page_slug, $pause_frontend_scanning_option_name, [ $this, 'sanitize_checkbox' ] );
254 add_settings_field(
255 $pause_frontend_scanning_option_name,
256 'Pause Front-End Scanning',
257 [ $this, 'field_checkbox' ],
258 $this->page_slug,
259 'general',
260 [
261 'class' => $pause_frontend_scanning_option_name,
262 'name' => $pause_frontend_scanning_option_name,
263 'default' => false,
264 'comments' => 'You can pause front-end scanning if you just want to scan manually; disabling this means you will NOT get notified when someone visits a page with broken links'
265 ]
266 );
267
268 // Pause results verification
269 $pause_results_verification_option_name = 'blnotifier_pause_results_verification';
270 register_setting( $this->page_slug, $pause_results_verification_option_name, [ $this, 'sanitize_checkbox' ] );
271 add_settings_field(
272 $pause_results_verification_option_name,
273 'Pause Results Auto-Verification',
274 [ $this, 'field_checkbox' ],
275 $this->page_slug,
276 'general',
277 [
278 'class' => $pause_results_verification_option_name,
279 'name' => $pause_results_verification_option_name,
280 'default' => false,
281 'comments' => 'You can pause the automatic verification and removal on the results page'
282 ]
283 );
284
285 // Enable emailing
286 $enable_emailing_option_name = 'blnotifier_enable_emailing';
287 register_setting( $this->page_slug, $enable_emailing_option_name, [ $this, 'sanitize_checkbox' ] );
288 add_settings_field(
289 $enable_emailing_option_name,
290 'Enable Emailing',
291 [ $this, 'field_checkbox' ],
292 $this->page_slug,
293 'general',
294 [
295 'class' => $enable_emailing_option_name,
296 'name' => $enable_emailing_option_name,
297 'default' => true,
298 'comments' => 'You can turn off email notifications and still get website notifications'
299 ]
300 );
301
302 // Emails
303 $emails_option_name = 'blnotifier_emails';
304 register_setting( $this->page_slug, $emails_option_name, 'sanitize_text_field' );
305 add_settings_field(
306 $emails_option_name,
307 'Emails to Send Notifications',
308 [ $this, 'field_emails_with_test' ],
309 $this->page_slug,
310 'general',
311 [
312 'class' => $emails_option_name,
313 'name' => $emails_option_name,
314 'default' => get_bloginfo( 'admin_email' ),
315 'comments' => 'Separated by commas'
316 ]
317 );
318
319 // Webhook fields
320 $webhook_fields = [
321 [
322 'name' => 'discord',
323 'label' => 'Discord',
324 'comments' => 'URL should look like this: https://discord.com/api/webhooks/xxx/xxx...'
325 ],
326 [
327 'name' => 'slack',
328 'label' => 'Slack',
329 'comments' => 'URL should look like this: https://hooks.slack.com/services/xxx/xxx/xxx'
330 ],
331 [
332 'name' => 'msteams',
333 'label' => 'Microsoft Teams',
334 'comments' => 'URL should look like this: https://yourdomain.webhook.office.com/xxx/xxx...'
335 ]
336 ];
337 foreach ( $webhook_fields as $webhook_field ) {
338
339 // Enable checkbox
340 $enable_option_name = 'blnotifier_enable_'.$webhook_field[ 'name' ];
341 register_setting( $this->page_slug, $enable_option_name, [ $this, 'sanitize_checkbox' ] );
342 add_settings_field(
343 $enable_option_name,
344 'Enable '.$webhook_field[ 'label' ].' Notifications',
345 [ $this, 'field_checkbox' ],
346 $this->page_slug,
347 'general',
348 [
349 'class' => $enable_option_name,
350 'name' => $enable_option_name,
351 'default' => false,
352 'comments' => 'You can also send notifications to a '.$webhook_field[ 'label' ].' channel'
353 ]
354 );
355
356 // The url
357 $url_field_option_name = 'blnotifier_'.$webhook_field[ 'name' ];
358 register_setting( $this->page_slug, $url_field_option_name, [ $this, 'sanitize_url' ] );
359 add_settings_field(
360 $url_field_option_name,
361 $webhook_field[ 'label' ].' Webhook URL',
362 [ $this, 'field_url_with_test' ],
363 $this->page_slug,
364 'general',
365 [
366 'class' => $url_field_option_name,
367 'name' => $url_field_option_name,
368 'default' => '',
369 'comments' => $webhook_field[ 'comments' ],
370 'test_type' => $webhook_field[ 'name' ],
371 ]
372 );
373 }
374
375 // Text
376 $user_agent_option_name = 'blnotifier_user_agent';
377 register_setting( $this->page_slug, $user_agent_option_name, 'sanitize_text_field' );
378 add_settings_field(
379 $user_agent_option_name,
380 'User Agent',
381 [ $this, 'field_text' ],
382 $this->page_slug,
383 'general',
384 [
385 'class' => $user_agent_option_name,
386 'name' => $user_agent_option_name,
387 'default' => 'WordPress/{blog_version}; {blog_url}',
388 'comments' => 'Only change this if you know what you are doing. Default is "WordPress/{blog_version}; {blog_url}" (WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . ')'
389 ]
390 );
391
392 // Define an array of number fields
393 $number_fields = [
394 [
395 'name' => 'timeout',
396 'label' => 'Timeout (seconds)',
397 'default' => 5,
398 'min' => 5,
399 'comments' => 'How long to try to connect to a link\'s server before quitting'
400 ],
401 [
402 'name' => 'max_redirects',
403 'label' => 'Max Redirects',
404 'default' => 5,
405 'min' => 0,
406 'comments' => 'Maximum number of redirects before giving up on a link (will only be used if you allow redirects below)'
407 ],
408 [
409 'name' => 'max_links_per_page',
410 'label' => 'Max Links Per Page',
411 'default' => 200,
412 'min' => 0,
413 'comments' => 'Maximum number of links to check per page (0 for unlimited) - this is to prevent attacks and timeouts on pages with a large number of links'
414 ],
415 ];
416
417 // Loop through the array to add number fields
418 foreach ( $number_fields as $field ) {
419 $field_option_name = 'blnotifier_' . $field[ 'name' ];
420
421 // Register the field with a sanitization callback
422 register_setting( $this->page_slug, $field_option_name, 'absint' );
423
424 // Add the number field to the settings page
425 add_settings_field(
426 $field_option_name,
427 $field[ 'label' ],
428 [ $this, 'field_number' ],
429 $this->page_slug,
430 'general',
431 [
432 'class' => $field_option_name,
433 'name' => $field_option_name,
434 'default' => $field[ 'default' ],
435 'min' => $field[ 'min' ],
436 'comments' => $field[ 'comments' ]
437 ]
438 );
439 }
440
441 // Other checkboxes
442 $checkboxes = [
443 [
444 'name' => 'allow_redirects',
445 'label' => 'Allow Redirects',
446 'default' => true,
447 'comments' => 'Changes the method of checking for broken links from <code>HEAD</code> to <code>GET</code>. May cause issues linking to larger documents.'
448 ],
449 [
450 'name' => 'documents_use_head',
451 'label' => 'Force Documents to Use <code>HEAD</code> Requests',
452 'default' => false,
453 'comments' => 'If you have enabled allowing redirects (above), by default images, videos, and audio files force the use of <code>HEAD</code> requests rather than <code>GET</code>. Some servers automatically block <code>HEAD</code> requests for documents, so we don\'t force them by default. If you are having issues with large documents not completing a scan, then you can try enabling this option to see if it helps. If they are blocked, at least you will know why.'
454 ],
455 [
456 'name' => 'enable_warnings',
457 'label' => 'Enable Warnings',
458 'default' => true,
459 'comments' => 'Includes warnings in all scans'
460 ],
461 [
462 'name' => 'enable_good_links',
463 'label' => 'Show Good Links in Results',
464 'default' => false,
465 'comments' => 'Includes good links on results page for verification purposes only (more performance heavy and will be omitted immediately unless you pause results auto-verification)'
466 ],
467 [
468 'name' => 'enable_delete_source',
469 'label' => 'Enable Delete Source Action Link',
470 'default' => false,
471 'comments' => 'An action link will appear on the Results tab under the source where you can trash the page entirely'
472 ],
473 [
474 'name' => 'include_images',
475 'label' => 'Check for Broken Images',
476 'default' => true,
477 'comments' => 'Includes image src links in all scans'
478 ],
479 [
480 'name' => 'ssl_verify',
481 'label' => 'Warn if SSL is Not Verified',
482 'default' => true,
483 'comments' => 'If you are not concerned about insecure links, you can disable this'
484 ],
485 [
486 'name' => 'scan_header',
487 'label' => 'Scan <code>&#x3c;header&#x3e;</code> Elements',
488 'default' => false,
489 'comments' => 'Only applies to page load scans - the header elements usually include the navigation menu(s) at the top of the page'
490 ],
491 [
492 'name' => 'scan_footer',
493 'label' => 'Scan <code>&#x3c;footer&#x3e;</code> Elements',
494 'default' => false,
495 'comments' => 'Only applies to page load scans - the footer elements include any links at the bottom of every page'
496 ],
497 [
498 'name' => 'show_in_console',
499 'label' => 'Show Results in Dev Console',
500 'default' => false,
501 'comments' => 'Only applies to page load scans'
502 ]
503 ];
504 foreach ( $checkboxes as $checkbox ) {
505 $checkbox_option_name = 'blnotifier_'.$checkbox[ 'name' ];
506 register_setting( $this->page_slug, $checkbox_option_name, [ $this, 'sanitize_checkbox' ] );
507 add_settings_field(
508 $checkbox_option_name,
509 $checkbox[ 'label' ],
510 [ $this, 'field_checkbox' ],
511 $this->page_slug,
512 'general',
513 [
514 'class' => $checkbox_option_name,
515 'name' => $checkbox_option_name,
516 'default' => $checkbox[ 'default' ],
517 'comments' => $checkbox[ 'comments' ]
518 ]
519 );
520 }
521
522 // REST API
523 $enable_rest_api_option_name = 'blnotifier_enable_rest_api';
524 register_setting( $this->page_slug, $enable_rest_api_option_name, [ $this, 'sanitize_checkbox' ] );
525 add_settings_field(
526 $enable_rest_api_option_name,
527 'Enable REST API',
528 [ $this, 'field_checkbox' ],
529 $this->page_slug,
530 'general',
531 [
532 'class' => $enable_rest_api_option_name,
533 'name' => $enable_rest_api_option_name,
534 'default' => false,
535 'comments' => 'Exposes a read/delete REST API endpoint for use with AI agents and external tools.'
536 ]
537 );
538
539 $api_key_option_name = 'blnotifier_api_key';
540 register_setting( $this->page_slug, $api_key_option_name, 'sanitize_text_field' );
541 add_settings_field(
542 $api_key_option_name,
543 'API Key',
544 [ $this, 'field_api_key' ],
545 $this->page_slug,
546 'general',
547 [
548 'class' => $api_key_option_name,
549 'name' => $api_key_option_name,
550 ]
551 );
552 // Caching
553 $cache_option_name = 'blnotifier_cache';
554 register_setting( $this->page_slug, $cache_option_name, 'sanitize_text_field' );
555 add_settings_field(
556 $cache_option_name,
557 'Length of Time to Cache Good Links (in Seconds)',
558 [ $this, 'field_number' ],
559 $this->page_slug,
560 'general',
561 [
562 'class' => $cache_option_name,
563 'name' => $cache_option_name,
564 'default' => 0,
565 'min' => 0,
566 'comments' => 'Use 0 to disable caching. If you are experienced performance issues, you can set the value to 28800 (8 hours), 43200 (12 hours), 86400 (24 hours) or whatever you feel is best. Broken and warning links will never be cached. Deactivating or uninstalling the plugin will clear the cache completely.'
567 ]
568 );
569
570 // User Roles for Editing Links
571 $roles_option_name = 'blnotifier_editable_roles';
572 register_setting( $this->page_slug, $roles_option_name, [ $this, 'sanitize_checkboxes' ] );
573 add_settings_field(
574 $roles_option_name,
575 'Allow These Additional Roles to Manage Broken Links',
576 [ $this, 'field_checkboxes' ],
577 $this->page_slug,
578 'general',
579 [
580 'class' => $roles_option_name,
581 'name' => $roles_option_name,
582 'options' => $this->get_editable_roles_choices(),
583 ]
584 );
585
586 // Post types
587 $post_types_option_name = 'blnotifier_post_types';
588 register_setting( $this->page_slug, $post_types_option_name, [ $this, 'sanitize_checkboxes' ] );
589 add_settings_field(
590 $post_types_option_name,
591 'Enable Multi-Scan for These Post Types',
592 [ $this, 'field_checkboxes' ],
593 $this->page_slug,
594 'general',
595 [
596 'class' => $post_types_option_name,
597 'name' => $post_types_option_name,
598 'options' => $this->get_post_type_choices(),
599 'default' => [ 'post', 'page' ]
600 ]
601 );
602
603 // Status codes
604 $status_codes_option_name = 'blnotifier_status_codes';
605 register_setting( $this->page_slug, $status_codes_option_name, [] );
606 add_settings_field(
607 $status_codes_option_name,
608 'Status Codes',
609 [ $this, 'field_status_codes' ],
610 $this->page_slug,
611 'general',
612 [
613 'class' => $status_codes_option_name,
614 'name' => $status_codes_option_name,
615 'options' => (new BLNOTIFIER_HELPERS)->get_status_codes(),
616 ]
617 );
618
619 // Uninstall database cleanup
620 $uninstall_cleanup_option_name = 'blnotifier_uninstall_cleanup';
621 register_setting( $this->page_slug, $uninstall_cleanup_option_name, [ $this, 'sanitize_checkbox' ] );
622 add_settings_field(
623 $uninstall_cleanup_option_name,
624 'Remove Data on Uninstall',
625 [ $this, 'field_checkbox' ],
626 $this->page_slug,
627 'general',
628 [
629 'class' => $uninstall_cleanup_option_name,
630 'name' => $uninstall_cleanup_option_name,
631 'default' => false,
632 'comments' => 'Enable this option to automatically remove the database table that the links are stored in and all options when the plugin is uninstalled.'
633 ]
634 );
635 } // End settings_fields()
636
637
638 /**
639 * Custom callback function to print text field
640 *
641 * @param array $args
642 * @return void
643 */
644 public function field_text( $args ) {
645 printf(
646 '<input type="text" id="%s" name="%s" value="%s"/><br><p class="description">%s</p>',
647 esc_html( $args[ 'name' ] ),
648 esc_html( $args[ 'name' ] ),
649 esc_html( get_option( $args[ 'name' ], isset( $args[ 'default' ] ) ? $args[ 'default' ] : '' ) ),
650 esc_html( $args[ 'comments' ] )
651 );
652 } // End field_text()
653
654
655 /**
656 * Custom callback function to print url field
657 *
658 * @param array $args
659 * @return void
660 */
661 public function field_url( $args ) {
662 printf(
663 '<input type="url" id="%s" name="%s" value="%s"/><br><p class="description">%s</p>',
664 esc_html( $args[ 'name' ] ),
665 esc_html( $args[ 'name' ] ),
666 esc_url( get_option( $args[ 'name' ], isset( $args[ 'default' ] ) ? $args[ 'default' ] : '' ) ),
667 esc_html( $args[ 'comments' ] )
668 );
669 } // End field_url()
670
671
672 /**
673 * Render a URL field with an optional test notification button
674 *
675 * @param array $args
676 * @return void
677 */
678 public function field_url_with_test( $args ) {
679 $value = esc_url( get_option( $args[ 'name' ], '' ) );
680 $has_value = !empty( $value );
681 ?>
682 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
683 <input type="url" id="<?php echo esc_attr( $args[ 'name' ] ); ?>" name="<?php echo esc_attr( $args[ 'name' ] ); ?>" value="<?php echo esc_attr( $value ); ?>"/>
684 <button type="button" class="button button-secondary blnotifier-test-btn" data-type="<?php echo esc_attr( $args[ 'test_type' ] ); ?>" data-field="<?php echo esc_attr( $args[ 'name' ] ); ?>" <?php echo !$has_value ? 'disabled' : ''; ?>>
685 <?php esc_html_e( 'Send Test', 'broken-link-notifier' ); ?>
686 </button>
687 </div>
688 <p class="description"><?php echo esc_html( $args[ 'comments' ] ); ?></p>
689 <?php
690 } // End field_url_with_test()
691
692
693 /**
694 * Render the emails field with a test notification button
695 *
696 * @param array $args
697 * @return void
698 */
699 public function field_emails_with_test( $args ) {
700 $value = esc_attr( get_option( $args[ 'name' ], isset( $args[ 'default' ] ) ? $args[ 'default' ] : '' ) );
701 $has_value = !empty( $value );
702 ?>
703 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
704 <input type="text" id="<?php echo esc_attr( $args[ 'name' ] ); ?>" name="<?php echo esc_attr( $args[ 'name' ] ); ?>" value="<?php echo esc_attr( $value ); ?>" pattern="([a-zA-Z0-9+_.\-]+@[a-zA-Z0-9.\-]+.[a-zA-Z0-9]+)(\s*,\s*([a-zA-Z0-9+_.\-]+@[a-zA-Z0-9.\-]+.[a-zA-Z0-9]+))*"/>
705 <button type="button" class="button button-secondary blnotifier-test-btn" data-type="email" data-field="<?php echo esc_attr( $args[ 'name' ] ); ?>" <?php echo !$has_value ? 'disabled' : ''; ?>>
706 <?php esc_html_e( 'Send Test', 'broken-link-notifier' ); ?>
707 </button>
708 </div>
709 <p class="description"><?php echo esc_html( $args[ 'comments' ] ); ?></p>
710 <?php
711 } // End field_emails_with_test()
712
713
714 /**
715 * Sanitize url
716 *
717 * @param string $value
718 * @return string
719 */
720 public function sanitize_url( $value ) {
721 return filter_var( $value, FILTER_SANITIZE_URL );
722 } // End sanitize_url()
723
724
725 /**
726 * Custom callback function to print checkbox field
727 *
728 * @param array $args
729 * @return void
730 */
731 public function field_checkbox( $args ) {
732 $has_updated_settings = get_option( 'blnotifier_has_updated_settings' );
733 if ( !$has_updated_settings ) {
734 $value = isset( $args[ 'default' ] ) ? $args[ 'default' ] : false;
735 } else {
736 $value = $this->sanitize_checkbox( get_option( $args[ 'name' ] ) );
737 }
738 printf(
739 '<input type="checkbox" id="%s" name="%s" value="yes" %s/> <p class="description">%s</p>',
740 esc_html( $args[ 'name' ] ),
741 esc_html( $args[ 'name' ] ),
742 esc_html( checked( 1, $value, false ) ),
743 wp_kses( $args[ 'comments' ], [ 'code' => [] ] )
744 );
745 } // End field_checkbox()
746
747
748 /**
749 * Sanitize checkbox
750 *
751 * @param int $value
752 * @return boolean
753 */
754 public function sanitize_checkbox( $value ) {
755 return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
756 } // End sanitize_checkbox()
757
758
759 /**
760 * Custom callback function to print checkboxes field
761 *
762 * @param array $args
763 * @return void
764 */
765 public function field_checkboxes( $args ) {
766 $value = get_option( $args[ 'name' ] );
767
768 if ( get_option( 'blnotifier_has_updated_settings' ) ) {
769 $value = ! empty( $value ) && is_array( $value ) ? array_keys( $value ) : [];
770 } else {
771 $value = isset( $args[ 'default' ] ) && is_array( $args[ 'default' ] ) ? $args[ 'default' ] : [];
772 }
773
774 if ( isset( $args[ 'options' ] ) && is_array( $args[ 'options' ] ) ) {
775 foreach ( $args[ 'options' ] as $key => $label ) {
776 $checked = in_array( $key, $value ) ? 'checked' : '';
777 printf(
778 '<input type="checkbox" id="%s" name="%s[%s]" value="1" %s/> <label for="%s">%s</label><br>',
779 esc_html( $args[ 'name' ] . '_' . $key ),
780 esc_html( $args[ 'name' ] ),
781 esc_attr( $key ),
782 esc_html( $checked ),
783 esc_html( $args[ 'name' ] . '_' . $key ),
784 esc_html( $label )
785 );
786 }
787 }
788 } // field_checkboxes()
789
790
791 /**
792 * Custom callback function to print status codes field
793 *
794 * @param array $args
795 * @return void
796 */
797 public function field_status_codes( $args ) {
798 $value = filter_var_array( get_option( $args[ 'name' ], [] ), FILTER_SANITIZE_SPECIAL_CHARS );
799
800 $HELPERS = new BLNOTIFIER_HELPERS;
801 $broken = $HELPERS->get_bad_status_codes();
802 $warning = $HELPERS->get_warning_status_codes( true );
803
804 if ( empty( $value ) ) {
805 $mark_zero_as_broken = filter_var( get_option( 'blnotifier_mark_code_zero_broken' ), FILTER_VALIDATE_BOOLEAN );
806 if ( $mark_zero_as_broken ) {
807 $value[ 0 ] = 'broken';
808 } else {
809 $value[ 0 ] = 'warning';
810 }
811
812 foreach ( $broken as $code ) {
813 $value[ $code ] = 'broken';
814 }
815
816 foreach ( $warning as $code ) {
817 if ( !in_array( $code, array_keys( $value ) ) ) {
818 $value[ $code ] = 'warning';
819 }
820 }
821 }
822
823 if ( isset( $args[ 'options' ] ) ) {
824 echo '<style>
825 .status-row {
826 display: flex;
827 justify-content: space-between;
828 align-items: center;
829 gap: 2rem;
830 border-radius: 5px;
831 border: 1px solid #ccc;
832 padding: 10px;
833 font-weight: bold;
834 }
835 .status-row.warning .type {
836 background: yellow;
837 color: black;
838 }
839 .status-row.broken .type {
840 background: red;
841 color: white;
842 }
843 .status-row.good .type {
844 background: #008000;
845 color: white;
846 }
847 .status-row .type {
848 float: right;
849 padding: 5px 10px;
850 font-weight: bold;
851 margin: 10px;
852 width: 100px;
853 text-align: center;
854 text-transform: uppercase;
855 box-shadow: 0 2px 4px 0 rgba(7, 36, 86, 0.075);
856 border: 1px solid rgba(7, 36, 86, 0.075);
857 border-radius: 10px;
858 }
859 .status-row .code-msg,
860 .status-row .description {
861 margin-bottom: 1rem;
862 margin-left: 0 !important;
863 }
864 .status-row label {
865 margin-right: 10px;
866 }
867 .status-container {
868 margin-top: 1rem;
869 display: none; /* Initially hidden */
870 }
871 .toggle-link {
872 cursor: pointer;
873 color: #0073aa;
874 text-decoration: underline;
875 margin-bottom: 10px;
876 display: inline-block;
877 }
878 </style>';
879
880 echo '<strong>Broken Status Codes:</strong> ' . esc_html( !empty( $broken ) ? implode( ', ', $broken ) : 'None' ).'<br>';
881 echo '<strong>Warning Status Codes:</strong> ' . esc_html( !empty( $warning ) ? implode( ', ', $warning ) : 'None' ).'<br><br><br>';
882
883 echo '<a class="toggle-link" data-target="status-container">View/Change Status Types</a>';
884 echo '<div class="status-container">';
885
886 foreach ( $args[ 'options' ] as $code => $c ) {
887 $type = isset( $value[ $code ] ) ? $value[ $code ] : 'good';
888 $checked_good = $type === 'good' ? 'checked' : '';
889 $checked_warning = $type === 'warning' ? 'checked' : '';
890 $checked_broken = $type === 'broken' ? 'checked' : '';
891 $display_code = isset( $c[ 'official' ] ) && !$c[ 'official' ] ? $code : '<a href="https://http.dev/' . $code . '" target="_blank">' . $code . '</a>';
892
893 printf(
894 '<div class="status-row ' . esc_attr( $type ) . '">
895 <div class="info-input">
896 <div class="code-msg">
897 <span class="code">%s</span> <span class="message">(%s)</span>
898 </div>
899 <div class="description">%s</div>
900 <div class="selections">
901 <input type="radio" id="%s_good" name="%s[%s]" value="good" %s/>
902 <label for="%s_good">Good</label>
903 <input type="radio" id="%s_warning" name="%s[%s]" value="warning" %s/>
904 <label for="%s_warning">Warning</label>
905 <input type="radio" id="%s_broken" name="%s[%s]" value="broken" %s/>
906 <label for="%s_broken">Broken</label>
907 </div>
908 </div>
909 <div class="indicator">
910 <div class="type">%s</div>
911 </div>
912 </div><br><br>',
913 wp_kses( $display_code, [ 'a' => [ 'href' => [], 'target' => [] ] ] ),
914 esc_html( $c[ 'msg' ] ),
915 wp_kses( $c[ 'desc' ], [ 'code' ] ),
916
917 esc_html( $args[ 'name' ].'_'.$code ),
918 esc_html( $args[ 'name' ] ),
919 esc_attr( $code ),
920 esc_html( $checked_good ),
921 esc_html( $args[ 'name' ].'_'.$code ),
922
923 esc_html( $args[ 'name' ].'_'.$code ),
924 esc_html( $args[ 'name' ] ),
925 esc_attr( $code ),
926 esc_html( $checked_warning ),
927 esc_html( $args[ 'name' ].'_'.$code ),
928
929 esc_html( $args[ 'name' ].'_'.$code ),
930 esc_html( $args[ 'name' ] ),
931 esc_attr( $code ),
932 esc_html( $checked_broken ),
933 esc_html( $args[ 'name' ].'_'.$code ),
934
935 esc_attr( strtoupper( $type ) )
936 );
937 }
938
939 echo '</div>';
940 }
941 } // End field_status_codes()
942
943
944 /**
945 * Sanitize checkboxes
946 *
947 * @param array $value
948 * @return boolean
949 */
950 public function sanitize_checkboxes( $value ) {
951 if ( !is_null( $value ) ) {
952 return filter_var_array( $value, FILTER_VALIDATE_BOOLEAN );
953 } else {
954 return [];
955 }
956 } // End sanitize_checkboxes()
957
958
959 /**
960 * Sanitize boolean
961 *
962 * @param mixed $input
963 * @return boolean
964 */
965 public function sanitize_boolean( $input ) {
966 return (bool) $input;
967 } // End sanitize_boolean()
968
969
970 /**
971 * Get post type choices
972 *
973 * @return array
974 */
975 public function get_post_type_choices() {
976 $HELPERS = new BLNOTIFIER_HELPERS;
977 $results = [];
978
979 $post_types = $HELPERS->get_post_types();
980
981 if ( ! is_array( $post_types ) || empty( $post_types ) ) {
982 return $results; // return empty array if no post types found
983 }
984
985 foreach ( $post_types as $post_type ) {
986 $post_type_name = $HELPERS->get_post_type_name( $post_type );
987 if ( $post_type_name === null ) {
988 $post_type_name = $post_type; // fallback to post type slug
989 }
990 $results[ $post_type ] = $post_type_name;
991 }
992
993 return $results;
994 } // End get_post_type_choices()
995
996
997 /**
998 * Get editable roles choices
999 *
1000 * @return array
1001 */
1002 public function get_editable_roles_choices() {
1003 global $wp_roles;
1004 $results = [];
1005 $roles = $wp_roles->get_names();
1006 foreach ( $roles as $role_slug => $role_name ) {
1007 if ( $role_slug == 'administrator' ) {
1008 continue;
1009 }
1010 $results[ $role_slug ] = $role_name;
1011 }
1012 return $results;
1013 } // End get_editable_roles_choices()
1014
1015
1016 /**
1017 * Custom callback function to print multiple emails field
1018 *
1019 * @param array $args
1020 * @return void
1021 */
1022 public function field_emails( $args ) {
1023 printf(
1024 '<input type="text" id="%s" name="%s" value="%s" pattern="%s"/><br><p class="description">%s</p>',
1025 esc_html( $args[ 'name' ] ),
1026 esc_html( $args[ 'name' ] ),
1027 esc_html( get_option( $args[ 'name' ], isset( $args[ 'default' ] ) ? $args[ 'default' ] : '' ) ),
1028 '([a-zA-Z0-9+_.\-]+@[a-zA-Z0-9.\-]+.[a-zA-Z0-9]+)(\s*,\s*([a-zA-Z0-9+_.\-]+@[a-zA-Z0-9.\-]+.[a-zA-Z0-9]+))*',
1029 esc_html( $args[ 'comments' ] )
1030 );
1031 } // field_text()
1032
1033
1034 /**
1035 * Custom callback function to print number field
1036 *
1037 * @param array $args
1038 * @return void
1039 */
1040 public function field_number( $args ) {
1041 printf(
1042 '<input type="number" id="%s" name="%s" value="%d" min="%d" required/><br><p class="description">%s</p>',
1043 esc_html( $args[ 'name' ] ),
1044 esc_html( $args[ 'name' ] ),
1045 esc_attr( get_option( $args[ 'name' ], isset( $args[ 'default' ] ) ? $args[ 'default' ] : '' ) ),
1046 esc_attr( $args[ 'min' ] ),
1047 esc_html( $args[ 'comments' ] )
1048 );
1049 } // End field_number()
1050
1051
1052 /**
1053 * Custom callback function to print textarea field
1054 *
1055 * @param array $args
1056 * @return void
1057 */
1058 public function field_textarea( $args ) {
1059 printf(
1060 '<textarea class="textarea" id="%s" name="%s"/>%s</textarea><br><p class="description">%s</p>',
1061 esc_html( $args[ 'name' ] ),
1062 esc_html( $args[ 'name' ] ),
1063 esc_html( get_option( $args[ 'name' ], '' ) ),
1064 esc_html( $args[ 'comments' ] )
1065 );
1066 } // field_text()
1067
1068
1069 /**
1070 * API key field
1071 *
1072 * @param array $args
1073 * @return void
1074 */
1075 public function field_api_key( $args ) {
1076 $key = get_option( $args[ 'name' ], '' );
1077 $display = $key ? 'inline-block' : 'none';
1078 ?>
1079 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
1080 <code id="blnotifier-api-key-display" style="display:<?php echo esc_attr( $display ); ?>;padding:8px 12px;background:#f0f0f1;border:1px solid #c3c4c7;border-radius:3px;font-size:13px;letter-spacing:0.5px;word-break:break-all;"><?php echo esc_html( $key ); ?></code>
1081 <input type="hidden" id="<?php echo esc_attr( $args[ 'name' ] ); ?>" name="<?php echo esc_attr( $args[ 'name' ] ); ?>" value="<?php echo esc_attr( $key ); ?>">
1082 <button type="button" id="blnotifier-generate-key" class="button button-secondary">Generate New API Key</button>
1083 <button type="button" id="blnotifier-copy-key" class="button button-secondary" style="display:<?php echo esc_attr( $display ); ?>;">Copy</button>
1084 </div>
1085 <p class="description">Send as a header <code>X-API-Key: your_key</code> or as a query param <code>?api_key=your_key</code>. Save Settings to persist a newly generated key.</p>
1086 <?php
1087 } // End field_api_key()
1088
1089
1090 /**
1091 * Get the full plugin page path
1092 *
1093 * @param string $tab
1094 * @return string
1095 */
1096 public function get_plugin_page( $tab = 'settings' ) {
1097 if ( $tab == 'omit-links' || $tab == 'omit-pages' ) {
1098 return admin_url( 'edit-tags.php?taxonomy='.$tab );
1099 } else {
1100 return admin_url( 'admin.php?page='.BLNOTIFIER_TEXTDOMAIN ).'&tab='.sanitize_key( $tab );
1101 }
1102 } // End get_plugin_page()
1103
1104
1105 /**
1106 * Get the full plugin short path
1107 *
1108 * @param string $tab
1109 * @return string
1110 */
1111 public function get_plugin_page_short_path( $tab = 'settings' ) {
1112 if ( !is_null( $tab ) ) {
1113 $add_tab = '&tab='.sanitize_key( $tab );
1114 } else {
1115 $add_tab = '';
1116 }
1117 return BLNOTIFIER_TEXTDOMAIN.$add_tab;
1118 } // End get_plugin_page_short_path()
1119
1120
1121 /**
1122 * Enqueue script
1123 *
1124 * @param string $screen
1125 * @return void
1126 */
1127 public function enqueue_scripts( $screen ) {
1128 $options_page = 'toplevel_page_'.BLNOTIFIER_TEXTDOMAIN;
1129 $tab = (new BLNOTIFIER_HELPERS)->get_tab();
1130 if ( ( $screen == $options_page && $tab == 'settings' ) ) {
1131 $handle = 'blnotifier_settings_script';
1132 wp_register_script( $handle, BLNOTIFIER_PLUGIN_JS_PATH.'settings.js', [ 'jquery' ], BLNOTIFIER_SCRIPT_VERSION, true );
1133 wp_localize_script( $handle, 'blnotifier_settings', [
1134 'api_key' => get_option( 'blnotifier_api_key', '' ),
1135 'nonce' => wp_create_nonce( 'blnotifier_test_notification' ),
1136 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1137 ] );
1138 wp_enqueue_script( $handle );
1139 wp_enqueue_script( 'jquery' );
1140 }
1141 } // End enqueue_scripts()
1142
1143
1144 /**
1145 * AJAX handler for test notifications
1146 *
1147 * @return void
1148 */
1149 public function ajax_test_notification() {
1150 if ( !isset( $_REQUEST[ 'nonce' ] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ 'nonce' ] ) ), 'blnotifier_test_notification' ) ) {
1151 wp_send_json_error( [ 'msg' => __( 'Invalid nonce.', 'broken-link-notifier' ) ] );
1152 }
1153
1154 if ( !current_user_can( 'manage_options' ) ) {
1155 wp_send_json_error( [ 'msg' => __( 'Unauthorized.', 'broken-link-notifier' ) ] );
1156 }
1157
1158 $type = isset( $_REQUEST[ 'type' ] ) ? sanitize_key( $_REQUEST[ 'type' ] ) : '';
1159 $value = isset( $_REQUEST[ 'value' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'value' ] ) ) : '';
1160
1161 if ( !$type || !$value ) {
1162 wp_send_json_error( [ 'msg' => __( 'Missing data.', 'broken-link-notifier' ) ] );
1163 }
1164
1165 $fake_link = 'https://example.com/broken-link';
1166 $fake_code = 404;
1167 $fake_text = 'Not Found';
1168 $fake_source = home_url( '/sample-page/' );
1169
1170 if ( $type === 'email' ) {
1171 $emails = array_map( 'trim', explode( ',', $value ) );
1172 $headers = [
1173 'From: ' . BLNOTIFIER_NAME . ' <' . get_bloginfo( 'admin_email' ) . '>',
1174 'Content-Type: text/html; charset=UTF-8',
1175 ];
1176 $subject = 'Test: Broken Links Found';
1177 $message = 'This is a test notification from ' . BLNOTIFIER_NAME . '.<br><br>';
1178 $message .= 'CONTENT:<br><br>';
1179 $message .= 'URL: ' . $fake_link . '<br>Status Code: ' . $fake_code . ' - ' . $fake_text;
1180 $message .= '<br><br><hr><br>' . get_bloginfo( 'name' ) . '<br><em>' . BLNOTIFIER_NAME . ' Plugin</em>';
1181
1182 if ( wp_mail( $emails, $subject, $message, $headers ) ) {
1183 wp_send_json_success();
1184 } else {
1185 wp_send_json_error( [ 'msg' => __( 'Email could not be sent.', 'broken-link-notifier' ) ] );
1186 }
1187
1188 } elseif ( $type === 'discord' ) {
1189 $DISCORD = new BLNOTIFIER_DISCORD;
1190 $args = [
1191 'msg' => '',
1192 'embed' => true,
1193 'author_name' => 'Source: ' . $fake_source,
1194 'author_url' => $fake_source,
1195 'title' => get_bloginfo( 'name' ),
1196 'title_url' => home_url(),
1197 'desc' => '-------------------',
1198 'img_url' => '',
1199 'thumbnail_url' => '',
1200 'disable_footer' => false,
1201 'bot_avatar_url' => BLNOTIFIER_PLUGIN_IMG_PATH . 'logo-teal.png',
1202 'bot_name' => BLNOTIFIER_NAME,
1203 'fields' => [
1204 [
1205 'name' => 'Broken Link:',
1206 'value' => $fake_link . "\nStatus Code: " . $fake_code . ' - ' . $fake_text,
1207 'inline' => false,
1208 ],
1209 ],
1210 ];
1211 $result = $DISCORD->send( $value, $args );
1212 $result ? wp_send_json_success() : wp_send_json_error( [ 'msg' => __( 'Could not send to Discord.', 'broken-link-notifier' ) ] );
1213
1214 } elseif ( $type === 'msteams' ) {
1215 $MSTEAMS = new BLNOTIFIER_MSTEAMS;
1216 $args = [
1217 'site_name' => get_bloginfo( 'name' ),
1218 'title' => 'Broken Links Found',
1219 'msg' => 'The following broken links were found:',
1220 'img_url' => '',
1221 'source_url' => $fake_source,
1222 'facts' => [
1223 [
1224 'name' => 'Broken Link:',
1225 'value' => '[' . $fake_link . '](' . $fake_link . ') _Status Code: **' . $fake_code . '** - ' . $fake_text . '_',
1226 ],
1227 ],
1228 ];
1229 $result = $MSTEAMS->send( $value, $args );
1230 $result ? wp_send_json_success() : wp_send_json_error( [ 'msg' => __( 'Could not send to Microsoft Teams.', 'broken-link-notifier' ) ] );
1231
1232 } elseif ( $type === 'slack' ) {
1233 $SLACK = new BLNOTIFIER_SLACK;
1234 $args = [
1235 'title' => 'Broken Links Found',
1236 'source' => $fake_source,
1237 'fields' => [
1238 [
1239 'link' => $fake_link,
1240 'code' => $fake_code,
1241 'text' => $fake_text,
1242 ],
1243 ],
1244 ];
1245 $result = $SLACK->send( $value, $args );
1246 $result ? wp_send_json_success() : wp_send_json_error( [ 'msg' => __( 'Could not send to Slack.', 'broken-link-notifier' ) ] );
1247
1248 } else {
1249 wp_send_json_error( [ 'msg' => __( 'Unknown notification type.', 'broken-link-notifier' ) ] );
1250 }
1251 } // End ajax_test_notification()
1252
1253 }