%3$s',
esc_attr( $id ),
esc_attr__( 'More info', 'acceguru' ),
esc_html( $text )
);
}
/** @var string Hook suffix for the dashboard page. */
private string $dashboard_hook = '';
/** @var string Hook suffix for the settings page. */
private string $settings_hook = '';
/** @var string Hook suffix for the bulk alt-text page. */
private string $bulk_alt_hook = '';
/** @var string Hook suffix for the accessibility statement page. */
private string $statement_hook = '';
/**
* Constructor - registers all hooks.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menus' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_init', array( $this, 'maybe_create_statement_page' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_media_alt_assets' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_chrome' ) );
add_action( 'admin_init', array( $this, 'maybe_set_install_time' ) );
add_action( 'admin_init', array( $this, 'handle_review_notice_action' ) );
add_action( 'wp_dashboard_setup', array( $this, 'register_dashboard_widget' ) );
}
/**
* Register a dashboard widget on the WP Home screen with a brief
* accessibility scan summary.
*
* @return void
*/
public function register_dashboard_widget(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
wp_add_dashboard_widget(
'acceguru_dashboard_widget',
esc_html__( 'AcceGuru - Accessibility Status', 'acceguru' ),
array( $this, 'render_dashboard_widget' )
);
}
/**
* Render the dashboard widget content.
*
* @return void
*/
public function render_dashboard_widget(): void {
$summary = get_option( 'acceguru_site_scan_summary', array() );
$pages_scanned = is_array( $summary ) ? (int) ( $summary['pages_scanned'] ?? 0 ) : 0;
$dash_url = admin_url( 'admin.php?page=acceguru' );
if ( ! empty( $summary ) && is_array( $summary ) && ! Acceguru_Scanner::summary_is_current( $summary ) ) {
echo '' . esc_html__( 'Saved scan results use older rules or settings. Run a new full-site scan to refresh them.', 'acceguru' ) . '
';
echo '' . esc_html__( 'Open AcceGuru Dashboard', 'acceguru' ) . '
';
return;
}
if ( $pages_scanned <= 0 ) {
echo '' . esc_html__( 'No site scan yet. Run your first full-site scan to see accessibility issues here.', 'acceguru' ) . '
';
echo '' . esc_html__( 'Open AcceGuru Dashboard', 'acceguru' ) . '
';
return;
}
$data = $this->build_dashboard_data();
$totals = $data['totals'] ?? array();
$bpt = isset( $totals['by_post_type'] ) && is_array( $totals['by_post_type'] ) ? $totals['by_post_type'] : array();
$total_issues = (int) ( $totals['total'] ?? 0 );
$by_impact = $totals['by_impact'] ?? array();
// Same date, and the same full/cancelled rule, as the plugin dashboard
// page. Every scan save writes 'scan_date', so the line always appears;
// the old separate key was only ever written by a summary recompute.
$last_scan_raw = isset( $summary['scan_date'] ) ? (string) $summary['scan_date'] : '';
$last_scan_ts = '' !== $last_scan_raw ? strtotime( $last_scan_raw ) : false;
$last_scan = $last_scan_ts ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_scan_ts ) : $last_scan_raw;
$scan_partial = ! empty( $summary['partial'] );
?>
·
|
|
|
|
|
|
$row ) :
$pt_obj = get_post_type_object( $pt );
$label = $pt_obj && ! empty( $pt_obj->labels->name ) ? $pt_obj->labels->name : $pt;
?>
| () |
|
|
|
|
|
time() ) {
return false;
}
}
$installed = (int) get_option( 'acceguru_install_time', 0 );
if ( ! $installed || ( time() - $installed ) < ( 5 * DAY_IN_SECONDS ) ) {
return false;
}
// Require at least one site scan so we don't pester users on day 5
// who never tried the main feature.
$summary = get_option( 'acceguru_site_scan_summary', array() );
if ( empty( $summary['pages_scanned'] ) ) {
return false;
}
return true;
}
/**
* Render the review-prompt card. Caller decides where to place it.
*
* @return void
*/
public static function render_review_notice(): void {
if ( ! self::should_show_review_notice() ) {
return;
}
$base_url = admin_url( 'admin.php?page=acceguru' );
$snooze_url = wp_nonce_url( add_query_arg( 'acceguru_review_action', 'snooze', $base_url ), 'acceguru_review_notice' );
$never_url = wp_nonce_url( add_query_arg( 'acceguru_review_action', 'dismiss', $base_url ), 'acceguru_review_notice' );
$review_url = 'https://wordpress.org/support/plugin/acceguru/reviews/';
?>
.wp-has-submenu,
#adminmenu #toplevel_page_acceguru > a.menu-top {
background-color: #2FBE8B !important;
color: #134B87 !important;
font-weight: 700;
transition: background-color .15s ease, color .15s ease;
}
#adminmenu #toplevel_page_acceguru > a.menu-top .wp-menu-name {
color: #134B87 !important;
}
#adminmenu #toplevel_page_acceguru:hover > a.menu-top,
#adminmenu #toplevel_page_acceguru.opensub > a.menu-top,
#adminmenu #toplevel_page_acceguru.wp-has-current-submenu > a.menu-top,
#adminmenu #toplevel_page_acceguru.current > a.menu-top {
background-color: #134B87 !important;
color: #2FBE8B !important;
}
#adminmenu #toplevel_page_acceguru:hover > a.menu-top .wp-menu-name,
#adminmenu #toplevel_page_acceguru.opensub > a.menu-top .wp-menu-name,
#adminmenu #toplevel_page_acceguru.wp-has-current-submenu > a.menu-top .wp-menu-name,
#adminmenu #toplevel_page_acceguru.current > a.menu-top .wp-menu-name {
color: #2FBE8B !important;
}
';
}
// -------------------------------------------------------------------------
// Menus
// -------------------------------------------------------------------------
/**
* Registers top-level menu and submenus.
*/
public function register_menus(): void {
$this->dashboard_hook = (string) add_menu_page(
esc_html__( 'AcceGuru', 'acceguru' ),
esc_html__( 'AcceGuru', 'acceguru' ),
'manage_options',
'acceguru',
array( $this, 'render_dashboard_page' ),
ACCEGURU_URL . 'assets/images/icon.png',
80
);
add_submenu_page(
'acceguru',
esc_html__( 'Dashboard', 'acceguru' ),
esc_html__( 'Dashboard', 'acceguru' ),
'manage_options',
'acceguru',
array( $this, 'render_dashboard_page' )
);
$this->settings_hook = (string) add_submenu_page(
'acceguru',
esc_html__( 'Settings', 'acceguru' ),
esc_html__( 'Settings', 'acceguru' ),
'manage_options',
'acceguru-settings',
array( $this, 'render_settings_page' )
);
// Bulk Alt Text and Statement now live as tabs of the Settings
// screen. The render_*_page methods below remain so the
// settings-page.php tab includes can call them.
}
// -------------------------------------------------------------------------
// Settings API
// -------------------------------------------------------------------------
/**
* Registers all settings sections and fields.
*/
public function register_settings(): void {
// Per-tab page slug + option group. Separate groups are critical:
// options.php zeroes any registered checkbox absent from $_POST, so a
// shared group across tabs would wipe other tabs' checkboxes when the
// user saves a single tab.
$page = 'acceguru-settings-scanner';
$group = 'acceguru_settings_scanner';
// ----- Scanner Settings -----
add_settings_section( 'acceguru_scanner', esc_html__( 'Scanner Settings', 'acceguru' ), '__return_false', $page );
register_setting( $group, 'acceguru_wcag_level', array( 'sanitize_callback' => array( $this, 'sanitize_wcag_level' ) ) );
add_settings_field(
'acceguru_wcag_level',
esc_html__( 'WCAG level to check', 'acceguru' ) . ' ' . self::help_tip(
__( 'A, AA and AAA are WCAG conformance levels. AA is a common project target. AAA includes additional criteria and may not be achievable for all content. Choose the level appropriate for your site.', 'acceguru' )
),
array( $this, 'render_select' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_wcag_level',
'choices' => array( 'A' => 'A', 'AA' => 'AA', 'AAA' => 'AAA' ),
)
);
register_setting( $group, 'acceguru_include_section_508', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
add_settings_field(
'acceguru_include_section_508',
esc_html__( 'Include Section 508 rules', 'acceguru' ),
array( $this, 'render_checkbox' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_include_section_508',
'description' => __( 'Adds axe-core rules tagged for Section 508. Enable this to include Section 508-tagged findings in scan results.', 'acceguru' ),
)
);
register_setting( $group, 'acceguru_include_en_301_549', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
add_settings_field(
'acceguru_include_en_301_549',
esc_html__( 'Include EN 301 549 rules', 'acceguru' ),
array( $this, 'render_checkbox' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_include_en_301_549',
'description' => __( 'Adds axe-core rules tagged for EN 301 549. Enable this to include EN 301 549-tagged findings in scan results.', 'acceguru' ),
)
);
register_setting( $group, 'acceguru_include_best_practice', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
register_setting( $group, 'acceguru_skip_noindex', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
register_setting( $group, 'acceguru_compat_wpml', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
add_settings_field(
'acceguru_include_best_practice',
esc_html__( 'Include recommended checks', 'acceguru' ) . ' ' . self::help_tip(
__( 'Adds recommended automated checks from axe-core and AcceGuru that are not formal WCAG failures. Recommended on.', 'acceguru' )
),
array( $this, 'render_checkbox' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_include_best_practice',
'default' => 1,
)
);
// Post types eligible for full-site scan. Multi-checkbox of every
// public post type the site has registered.
register_setting( $group, 'acceguru_scan_post_types', array(
'sanitize_callback' => array( $this, 'sanitize_post_types' ),
'default' => array( 'post', 'page' ),
) );
add_settings_field(
'acceguru_scan_post_types',
esc_html__( 'Post types to include in full-site scan', 'acceguru' ) . ' ' . self::help_tip(
__( 'Choose which content types the site-scan walks. Defaults to Posts + Pages. On large sites with thousands of WooCommerce products you may want to leave Products off.', 'acceguru' )
),
array( $this, 'render_post_types' ),
$page,
'acceguru_scanner',
array( 'option' => 'acceguru_scan_post_types' )
);
register_setting( $group, 'acceguru_show_editor_sidebar', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
add_settings_field(
'acceguru_show_editor_sidebar',
esc_html__( 'Show AcceGuru in Block Editor sidebar', 'acceguru' ),
array( $this, 'render_checkbox' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_show_editor_sidebar',
'default' => 1,
'description' => __( 'Disable to hide the editor sidebar panel. Useful when the editor is too crowded with other plugins.', 'acceguru' ),
)
);
register_setting( $group, 'acceguru_overlay_position', array( 'sanitize_callback' => array( $this, 'sanitize_overlay_position' ), 'default' => 'right' ) );
add_settings_field(
'acceguru_overlay_position',
esc_html__( 'Front-end overlay position', 'acceguru' ) . ' ' . self::help_tip(
__( 'Which side the AcceGuru panel docks to when you click the admin-bar button on the front end. Both options push the page content over to make room.', 'acceguru' )
),
array( $this, 'render_select' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_overlay_position',
'choices' => array(
'right' => __( 'Right (default)', 'acceguru' ),
'left' => __( 'Left', 'acceguru' ),
),
)
);
register_setting( $group, 'acceguru_fab_position', array( 'sanitize_callback' => array( $this, 'sanitize_fab_position' ), 'default' => 'bottom-right' ) );
register_setting( $group, 'acceguru_skip_link_targets', array( 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ) );
register_setting( $group, 'acceguru_scan_exclude_selectors', array( 'sanitize_callback' => 'sanitize_textarea_field', 'default' => '' ) );
add_settings_field(
'acceguru_skip_link_targets',
esc_html__( 'Skip-link target selectors', 'acceguru' ) . ' ' . self::help_tip(
__( 'Comma-separated CSS selectors the skip link will try first, before falling back to defaults (main, [role=main], #main, #content, #primary). Use when your theme\'s main content area has a non-standard ID/class.', 'acceguru' )
),
array( $this, 'render_text' ),
$page,
'acceguru_scanner',
array(
'option' => 'acceguru_skip_link_targets',
'placeholder' => '#site-main, .page-wrapper',
)
);
// ----- Fixer Settings (own tab, four sub-sections) -----
$page = 'acceguru-settings-fixers';
$group = 'acceguru_settings_fixers';
add_settings_section( 'acceguru_fixer_structure', esc_html__( 'Page structure', 'acceguru' ), '__return_false', $page );
add_settings_section( 'acceguru_fixer_forms', esc_html__( 'Forms & search', 'acceguru' ), '__return_false', $page );
add_settings_section( 'acceguru_fixer_links', esc_html__( 'Links & buttons', 'acceguru' ), '__return_false', $page );
add_settings_section( 'acceguru_fixer_cleanup', esc_html__( 'Cleanup', 'acceguru' ), '__return_false', $page );
$fixer_fields = array(
// Page structure.
'acceguru_fix_skip_links' => array(
'section' => 'acceguru_fixer_structure',
'label' => __( 'Enable skip links', 'acceguru' ),
'tip' => __( 'Adds a "Skip to main content" link as the first focusable element on every page. Helps keyboard users bypass the navigation. WCAG 2.4.1.', 'acceguru' ),
),
'acceguru_fix_focus_outlines' => array(
'section' => 'acceguru_fixer_structure',
'label' => __( 'Enable focus outlines', 'acceguru' ),
'tip' => __( 'Adds a visible focus indicator to every interactive element. Required so keyboard users can see what they have selected. WCAG 2.4.7.', 'acceguru' ),
),
'acceguru_fix_html_lang' => array(
'section' => 'acceguru_fixer_structure',
'label' => __( 'Add lang and dir attributes to ', 'acceguru' ),
'tip' => __( 'Adds based on the WordPress site language and text direction, so screen readers use the right pronunciation and right-to-left languages (Arabic, Hebrew) render correctly. WCAG 3.1.1.', 'acceguru' ),
),
// Forms & search.
'acceguru_fix_search_form_label' => array(
'section' => 'acceguru_fixer_forms',
'label' => __( 'Add an accessible name to the search field', 'acceguru' ),
'tip' => __( 'Adds an aria-label to the WP search field when it has no accessible name of its own, so screen readers can announce it. Skipped when the field already has a name. WCAG 1.3.1 / 4.1.2.', 'acceguru' ),
),
'acceguru_fix_label_comment_form' => array(
'section' => 'acceguru_fixer_forms',
'label' => __( 'Add labels to the comment form', 'acceguru' ),
'tip' => __( 'Adds an accessible name to the WordPress comment fields (Name, Email, Website, Comment) when the theme leaves them without a label, so screen-reader users know what each field is for. WCAG 1.3.1 / 4.1.2.', 'acceguru' ),
),
'acceguru_fix_empty_search_results' => array(
'section' => 'acceguru_fixer_forms',
'label' => __( 'Show search results page on empty search', 'acceguru' ),
'tip' => __( 'When a search is submitted with an empty query, force WordPress to render the search results template so users see a meaningful "no results" response instead of being silently sent to the home page.', 'acceguru' ),
),
// Links & buttons.
'acceguru_fix_label_new_window' => array(
'section' => 'acceguru_fixer_links',
'label' => __( 'Label links opening a new window', 'acceguru' ),
'tip' => __( 'Adds an "(opens in new tab)" screen-reader label and a small ↗ icon to every that does not already announce the new window. WCAG 3.2.5.', 'acceguru' ),
),
'acceguru_fix_label_document_links' => array(
'section' => 'acceguru_fixer_links',
'label' => __( 'Flag links to documents (PDF, Word...)', 'acceguru' ),
'tip' => __( 'Appends a short "(PDF)", "(Word)", "(Excel)" marker to links that point at non-HTML files - plus the file size for files on this site - so users know a download or different format follows. Skips links that already say so. WCAG 3.2.4 best practice.', 'acceguru' ),
),
'acceguru_fix_force_link_underline' => array(
'section' => 'acceguru_fixer_links',
'label' => __( 'Underline non-navigation links', 'acceguru' ),
'tip' => __( 'Underlines links inside content (skipping menus and button-styled links) so colour-blind and low-vision users can tell links from plain text. WCAG 1.4.1.', 'acceguru' ),
),
'acceguru_fix_autoplay_video_controls' => array(
'section' => 'acceguru_fixer_links',
'label' => __( 'Add controls to autoplay videos', 'acceguru' ),
'tip' => __( 'Forces controls="" onto every