opts = get_option( 'accessiq_settings', AccessIQ::default_settings() ); add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_filter( 'manage_posts_columns', array( $this, 'add_accessibility_column' ) ); add_filter( 'manage_pages_columns', array( $this, 'add_accessibility_column' ) ); add_action( 'manage_posts_custom_column', array( $this, 'render_accessibility_column' ), 10, 2 ); add_action( 'manage_pages_custom_column', array( $this, 'render_accessibility_column' ), 10, 2 ); add_action( 'admin_init', array( $this, 'maybe_redirect_to_welcome' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'wp_ajax_accessiq_save', array( $this, 'ajax_save' ) ); } public function maybe_redirect_to_welcome() { if ( ! get_transient( 'accessiq_show_welcome' ) ) { return; } // WordPress multisite activation sets 'activate-multi' with no nonce. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['activate-multi'] ) ) { // No nonce available from WP core multisite activation flow. delete_transient( 'accessiq_show_welcome' ); return; } delete_transient( 'accessiq_show_welcome' ); wp_safe_redirect( admin_url( 'admin.php?page=accessiq-about' ) ); exit; } public function add_menu() { $icon = ACCESSIQ_URL . 'admin/images/menu-icon.png'; add_menu_page( 'AccessIQ', 'AccessIQ', 'manage_options', 'accessiq', array( $this, 'render_page' ), $icon, 30 ); add_submenu_page( 'accessiq', 'Overview', '🏠 Overview', 'manage_options', 'accessiq', array( $this, 'render_page' ) ); add_submenu_page( 'accessiq', 'All Issues', '⚠ All Issues', 'manage_options', 'accessiq-issues', array( $this, 'render_issues_page' ) ); } /** * Chart data for Overview (matches dashboard.php logic). * * @return array{score:int,gradeColor:string,devices:array} */ private function get_dashboard_chart_payload() { global $wpdb; $last = get_posts( array( 'post_type' => ACCESSIQ_SCAN_CPT, 'posts_per_page' => 1, 'orderby' => 'date', 'order' => 'DESC', ) ); $score = 0; if ( ! empty( $last ) ) { $score = (int) get_post_meta( $last[0]->ID, '_accessiq_score', true ); } if ( $score >= 70 ) { $grade_color = '#57f1db'; } elseif ( $score >= 50 ) { $grade_color = '#fbbf24'; } else { $grade_color = '#f87171'; } $sessions_tbl = $wpdb->prefix . 'accessiq_sessions'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $rows = $wpdb->get_results( 'SELECT COALESCE(device_type, \'Desktop\') AS device, COUNT(*) AS total FROM `' . esc_sql( $sessions_tbl ) . '` GROUP BY device_type ORDER BY total DESC' ); $devices = array( 'Desktop' => 0, 'Mobile' => 0, 'Tablet' => 0 ); foreach ( (array) $rows as $row ) { $key = ucfirst( strtolower( trim( $row->device ) ) ); if ( isset( $devices[ $key ] ) ) { $devices[ $key ] = (int) $row->total; } } return array( 'score' => $score, 'gradeColor' => $grade_color, 'devices' => $devices, ); } public function enqueue( $hook ) { // Small menu stylesheet + sidebar script run on every admin screen (they style the AccessIQ menu group). wp_enqueue_style( 'accessiq-admin-menu', ACCESSIQ_URL . 'admin/css/admin-menu.css', array(), ACCESSIQ_VERSION ); wp_enqueue_script( 'accessiq-sidebar', ACCESSIQ_URL . 'admin/js/sidebar.js', array( 'jquery' ), ACCESSIQ_VERSION, true ); if ( strpos( $hook, 'accessiq' ) === false ) { return; } // The full admin stylesheet only loads on AccessIQ screens (no external font requests). wp_enqueue_style( 'accessiq-admin-sidebar', ACCESSIQ_URL . 'admin/css/admin.css', array( 'accessiq-admin-menu' ), ACCESSIQ_VERSION ); $admin_deps = array( 'jquery' ); if ( 'toplevel_page_accessiq' === $hook || 'accessiq_page_accessiq-scanner' === $hook ) { wp_enqueue_script( 'accessiq-chartjs', ACCESSIQ_URL . 'admin/js/chart.min.js', array(), '4.4.8', true ); $admin_deps[] = 'accessiq-chartjs'; } wp_enqueue_script( 'accessiq-admin', ACCESSIQ_URL . 'admin/js/admin.js', $admin_deps, ACCESSIQ_VERSION, true ); $d = ( 'toplevel_page_accessiq' === $hook ) ? $this->get_dashboard_chart_payload() : array( 'score' => 0, 'gradeColor' => '#10d98e', 'devices' => array( 'Desktop' => 0, 'Mobile' => 0, 'Tablet' => 0 ), ); $chart_data = array( 'score' => (int) $d['score'], 'gradeColor' => sanitize_hex_color( $d['gradeColor'] ) ?: '#10d98e', 'desk' => (int) ( $d['devices']['Desktop'] ?? 0 ), 'mob' => (int) ( $d['devices']['Mobile'] ?? 0 ), 'tab' => (int) ( $d['devices']['Tablet'] ?? 0 ), ); wp_localize_script( 'accessiq-admin', 'accessiqAdmin', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accessiq_nonce' ), 'save_scan_nonce' => wp_create_nonce( 'accessiq_save_scan' ), 'settings_nonce' => wp_create_nonce( 'accessiq_settings_nonce' ), 'settings' => $this->opts, 'scan_url' => admin_url( 'admin.php?page=accessiq-scanner' ), 'chart_data' => $chart_data, ) ); } public function ajax_save() { check_ajax_referer( 'accessiq_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $s = $this->opts; // Merge-only: a field is changed only when it is present in the request, so a partial // or malformed POST can never reset the rest of the settings to defaults. $has = static function ( $key ) { return isset( $_POST[ $key ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked above. }; $text = static function ( $key ) { return sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized }; $bool_fields = array( 'widget_enabled', 'show_on_mobile', 'profiles_enabled', 'scanner_enabled', 'schedule_enabled', 'analytics_enable_click_tracking', 'analytics_enable_behavior_tracking', 'analytics_exclude_logged_in', 'analytics_replace_dashboard', ); foreach ( $bool_fields as $key ) { if ( $has( $key ) ) { $s[ $key ] = ( 'true' === $text( $key ) ); } } if ( $has( 'widget_position' ) && in_array( $text( 'widget_position' ), array( 'bottom-right', 'bottom-left', 'top-right', 'top-left' ), true ) ) { $s['widget_position'] = $text( 'widget_position' ); } if ( $has( 'widget_color' ) ) { $color = sanitize_hex_color( $text( 'widget_color' ) ); if ( $color ) { $s['widget_color'] = $color; } } if ( $has( 'widget_panel_width' ) && in_array( sanitize_key( $text( 'widget_panel_width' ) ), array( 'compact', 'normal', 'wide' ), true ) ) { $s['widget_panel_width'] = sanitize_key( $text( 'widget_panel_width' ) ); } if ( $has( 'widget_panel_height' ) && in_array( sanitize_key( $text( 'widget_panel_height' ) ), array( 'auto', '400', '500', 'full' ), true ) ) { $s['widget_panel_height'] = sanitize_key( $text( 'widget_panel_height' ) ); } // Checkbox groups: jQuery omits an empty array, so the form sends accessiq_lists=1 to say "these lists are authoritative". if ( $has( 'accessiq_lists' ) ) { $lists = array( 'widget_features' => array( 'font_size', 'letter_spacing', 'line_height', 'dyslexia_font', 'reading_line', 'reading_mask', 'contrast', 'monochrome', 'saturation', 'highlight_links', 'highlight_titles', 'stop_animations', 'big_cursor', 'keyboard_nav', 'screen_reader', 'wcag_checker' ), 'enabled_profiles' => array( 'vision', 'adhd', 'seizure', 'dyslexia', 'keyboard' ), 'autofix_features' => array( 'skip_link', 'focus_outline', 'html_lang', 'alt_fix' ), ); foreach ( $lists as $key => $allowed ) { $raw = isset( $_POST[ $key ] ) ? (array) wp_unslash( $_POST[ $key ] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $s[ $key ] = array_values( array_intersect( array_map( 'sanitize_key', $raw ), $allowed ) ); } } if ( $has( 'scan_level' ) && in_array( $text( 'scan_level' ), array( 'A', 'AA', 'AAA' ), true ) ) { $s['scan_level'] = $text( 'scan_level' ); } if ( $has( 'scan_wcag_version' ) && in_array( $text( 'scan_wcag_version' ), array( '2.1', '2.2' ), true ) ) { $s['scan_wcag_version'] = $text( 'scan_wcag_version' ); } if ( $has( 'notify_email' ) ) { $email = sanitize_email( wp_unslash( $_POST['notify_email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $s['notify_email'] = is_email( $email ) ? $email : get_option( 'admin_email' ); } if ( $has( 'schedule_freq' ) && in_array( sanitize_key( $text( 'schedule_freq' ) ), array( 'daily', 'weekly', 'monthly' ), true ) ) { $s['schedule_freq'] = sanitize_key( $text( 'schedule_freq' ) ); } $accessiq_retention = (int) ( $s['analytics_data_retention'] ?? 90 ); if ( $has( 'analytics_data_retention' ) && in_array( absint( $text( 'analytics_data_retention' ) ), array( 30, 60, 90, 180 ), true ) ) { $accessiq_retention = absint( $text( 'analytics_data_retention' ) ); } $s['analytics_data_retention'] = $accessiq_retention; update_option( 'accessiq_settings', $s ); $this->opts = $s; // Mirror standalone options for backward compatibility / retention cron. update_option( 'accessiq_analytics_enable_click_tracking', ! empty( $s['analytics_enable_click_tracking'] ) ); update_option( 'accessiq_analytics_enable_behavior_tracking', ! empty( $s['analytics_enable_behavior_tracking'] ) ); update_option( 'accessiq_analytics_exclude_logged_in', ! empty( $s['analytics_exclude_logged_in'] ) ); update_option( 'accessiq_analytics_replace_dashboard', ! empty( $s['analytics_replace_dashboard'] ) ); update_option( 'accessiq_analytics_data_retention', $accessiq_retention ); do_action( 'accessiq_settings_saved' ); wp_send_json_success( array( 'message' => __( 'Settings saved!', 'accessiq' ) ) ); } public function render_page() { include ACCESSIQ_DIR . 'admin/views/dashboard.php'; } public function render_issues_page() { include ACCESSIQ_DIR . 'admin/views/issues.php'; } public function render_scanner_page() { include ACCESSIQ_DIR . 'admin/views/scanner.php'; } public function render_reports_page() { include ACCESSIQ_DIR . 'admin/views/reports.php'; } public function render_settings_page() { include ACCESSIQ_DIR . 'admin/views/settings.php'; } public function get( $key, $default = null ) { return $this->opts[ $key ] ?? $default; } /** * Add Accessibility Score column to Posts / Pages list tables. * * @param array $columns Existing columns. * @return array */ public function add_accessibility_column( $columns ) { $columns['accessiq_score'] = '⚡ ' . __( 'Accessibility', 'accessiq' ); return $columns; } /** * Render Accessibility Score column cell. * * @param string $column Column key. * @param int $post_id Current post ID. */ public function render_accessibility_column( $column, $post_id ) { if ( 'accessiq_score' !== $column ) { return; } global $wpdb; $url = get_permalink( $post_id ); if ( ! $url ) { echo '' . esc_html__( 'Not scanned', 'accessiq' ) . ''; return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $scan = $wpdb->get_row( $wpdb->prepare( "SELECT p.ID, pm.meta_value AS score FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_accessiq_score' LEFT JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_accessiq_url' WHERE p.post_type = %s AND pm2.meta_value = %s ORDER BY p.post_date DESC LIMIT 1", ACCESSIQ_SCAN_CPT, $url ) ); if ( ! $scan || null === $scan->score ) { echo '' . esc_html__( 'Not scanned', 'accessiq' ) . ''; return; } $score = (int) $scan->score; $color = $score >= 80 ? '#10d98e' : ( $score >= 50 ? '#f59e0b' : '#ef4444' ); $label = $score >= 80 ? __( 'Good', 'accessiq' ) : ( $score >= 50 ? __( 'Fair', 'accessiq' ) : __( 'Poor', 'accessiq' ) ); printf( '%2$s/100 · %3$s', esc_attr( $color ), esc_html( $score ), esc_html( $label ) ); } }