= $limit ) { return true; } set_transient( $key, $count + 1, MINUTE_IN_SECONDS ); return false; } /** * Neutralise spreadsheet formula injection in CSV cells. * * @param mixed $value Cell value. * @return mixed */ function accessiq_csv_safe( $value ) { if ( is_string( $value ) && '' !== $value && false !== strpos( "=+-@\t\r", $value[0] ) ) { return "'" . $value; } return $value; } /** * Compliance Badge Shortcode — [accessiq_badge level="AA" style="dark"] * Outputs an inline WCAG compliance badge, optionally linked to the statement page. */ add_shortcode( 'accessiq_badge', 'accessiq_compliance_badge' ); function accessiq_compliance_badge( $atts ) { $atts = shortcode_atts( array( 'level' => 'AA', 'style' => 'dark', 'claim' => 'commitment', 'version' => '' ), $atts, 'accessiq_badge' ); $version = in_array( $atts['version'], array( '2.1', '2.2' ), true ) ? $atts['version'] : ( '2.1' === AccessIQ_Settings::instance()->get( 'scan_wcag_version', '2.2' ) ? '2.1' : '2.2' ); $level = in_array( $atts['level'], array( 'A', 'AA', 'AAA' ), true ) ? $atts['level'] : 'AA'; $dark = ( 'dark' === $atts['style'] ); $bg = $dark ? '#0f172a' : '#ffffff'; $border = '#10d98e'; $text = $dark ? '#e2e8f0' : '#0f172a'; $stmt_page_id = (int) get_option( 'accessiq_statement_page_id', 0 ); $stmt_url = $stmt_page_id ? get_permalink( $stmt_page_id ) : ''; // An automated scan cannot certify conformance, so the default wording is a commitment, not a claim. // Site owners who have verified conformance themselves can opt in with claim="compliant". if ( 'compliant' === $atts['claim'] ) { /* translators: 1: WCAG version (2.1 or 2.2), 2: WCAG level (A, AA, AAA) */ $label = sprintf( __( 'WCAG %1$s %2$s Compliant', 'accessiq' ), $version, $level ); } else { /* translators: 1: WCAG version (2.1 or 2.2), 2: WCAG level (A, AA, AAA) */ $label = sprintf( __( 'Committed to WCAG %1$s %2$s', 'accessiq' ), $version, $level ); } $badge = sprintf( '' . '' . '%4$s', esc_attr( $bg ), esc_attr( $border ), esc_attr( $text ), esc_html( $label ) ); if ( $stmt_url ) { return '' . $badge . ''; } return $badge; } /** * Render the Upgrade to Pro admin page. */ function accessiq_render_upgrade_page() { $upgrade_url = function_exists( 'ap_fs' ) ? ap_fs()->get_upgrade_url() : 'https://accessiq.thereadscope.com/#pricing'; ?>
⚡

id, 'accessiq' ) ) { return; } // Dismissal is per user (legacy global option still honoured). if ( get_option( 'accessiq_upgrade_notice_dismissed' ) || get_user_meta( get_current_user_id(), 'accessiq_upgrade_notice_dismissed', true ) ) { return; } $upgrade_url = 'https://accessiq.thereadscope.com/#pricing'; ?>

PRO AccessIQ Pro —

'Unauthorized.' ), 403 ); } update_user_meta( get_current_user_id(), 'accessiq_upgrade_notice_dismissed', 1 ); wp_send_json_success(); } ); /** * Main Plugin Class */ final class AccessIQ { private static $instance = null; public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action( 'plugins_loaded', array( $this, 'init' ) ); } public static function activate() { self::create_tables(); flush_rewrite_rules(); if ( ! get_option( 'accessiq_settings' ) ) { add_option( 'accessiq_settings', self::default_settings() ); } if ( ! wp_next_scheduled( 'accessiq_cleanup_old_data' ) ) { wp_schedule_event( time(), 'daily', 'accessiq_cleanup_old_data' ); } set_transient( 'accessiq_show_welcome', true, 30 ); } private static function create_tables() { global $wpdb; $charset = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $issues_table = $wpdb->prefix . 'accessiq_issues'; dbDelta( "CREATE TABLE {$issues_table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, scan_id BIGINT(20) UNSIGNED NOT NULL, wcag_id VARCHAR(20) NOT NULL, level VARCHAR(5) NOT NULL, element TEXT NOT NULL, issue_type VARCHAR(100) NOT NULL, severity VARCHAR(20) NOT NULL DEFAULT 'error', suggestion TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, dismissed_at DATETIME DEFAULT NULL, dismiss_note VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), KEY scan_id (scan_id), KEY dismissed (dismissed_at) ) {$charset};" ); $clicks_table = $wpdb->prefix . 'accessiq_clicks'; dbDelta( "CREATE TABLE {$clicks_table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, page_url VARCHAR(255) NOT NULL, x_percent DECIMAL(5,2) NOT NULL, y_percent DECIMAL(5,2) NOT NULL, element VARCHAR(255) NOT NULL, device_type VARCHAR(50) NOT NULL, session_id VARCHAR(100) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id), KEY session_id (session_id), KEY created_at (created_at), KEY page_created (page_url(191), created_at) ) {$charset};" ); $sessions_table = $wpdb->prefix . 'accessiq_sessions'; dbDelta( "CREATE TABLE {$sessions_table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, session_id VARCHAR(100) NOT NULL, page_url VARCHAR(255) NOT NULL, time_spent INT(11) UNSIGNED DEFAULT 0 NOT NULL, is_bounce TINYINT(1) DEFAULT 1 NOT NULL, referrer VARCHAR(255) DEFAULT '' NOT NULL, device_type VARCHAR(50) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id), KEY session_id (session_id), KEY created_at (created_at) ) {$charset};" ); // UNIQUE KEY session_page (session_id, page_url(191)) is created in upgrade_db(): // dbDelta cannot compare prefix-length unique keys and re-issues ADD KEY on every run. $pv_table = $wpdb->prefix . 'accessiq_page_views'; dbDelta( "CREATE TABLE {$pv_table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL DEFAULT '', page_url VARCHAR(500) NOT NULL DEFAULT '', page_title VARCHAR(300) NOT NULL DEFAULT '', time_spent INT(11) NOT NULL DEFAULT 0, scroll_depth TINYINT(3) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY session_id (session_id), KEY created_at (created_at) ) {$charset};" ); $dashboard_table = $wpdb->prefix . 'accessiq_dashboard'; dbDelta( "CREATE TABLE {$dashboard_table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT(20) UNSIGNED NOT NULL, widget_key VARCHAR(100) NOT NULL, position INT(11) NOT NULL DEFAULT 0, is_visible TINYINT(1) NOT NULL DEFAULT 1, config LONGTEXT NOT NULL, PRIMARY KEY (id), KEY user_id (user_id) ) {$charset};" ); } /** * Run versioned schema migrations that dbDelta cannot express reliably. * * Idempotent: every step inspects the live schema first, so running twice is safe. * * @param string $from_version Previously installed DB version. * @return bool True when the schema is up to date; false when a step failed (version is then not bumped, so it retries). */ private static function upgrade_db( $from_version ) { global $wpdb; if ( ! version_compare( (string) $from_version, '1.6', '<' ) ) { return true; } // Do not hammer a failing migration: retry at most once an hour. if ( get_transient( 'accessiq_db_upgrade_failed' ) ) { return false; } $table = $wpdb->prefix . 'accessiq_sessions'; $ok = true; // Sessions must allow one row per page (a UNIQUE session_id broke bounce tracking), and the // session_start handler relies on UNIQUE (session_id, page_url) for INSERT IGNORE. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $indexes = $wpdb->get_results( "SHOW INDEX FROM `{$table}` WHERE Key_name = 'session_id'" ); foreach ( (array) $indexes as $idx ) { if ( isset( $idx->Non_unique ) && 0 === (int) $idx->Non_unique ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange $ok = ( false !== $wpdb->query( "ALTER TABLE `{$table}` DROP INDEX `session_id`" ) ) && $ok; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange $ok = ( false !== $wpdb->query( "ALTER TABLE `{$table}` ADD KEY `session_id` (`session_id`)" ) ) && $ok; break; } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $page_idx = $wpdb->get_results( "SHOW INDEX FROM `{$table}` WHERE Key_name = 'session_page'" ); $page_unique = false; foreach ( (array) $page_idx as $idx ) { if ( isset( $idx->Non_unique ) && 0 === (int) $idx->Non_unique ) { $page_unique = true; break; } } if ( ! $page_unique ) { // Remove duplicate (session_id, page_url prefix) rows first, keeping the newest, in batches. for ( $i = 0; $i < 200; $i++ ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $dupes = $wpdb->get_col( "SELECT t1.id FROM `{$table}` t1 INNER JOIN `{$table}` t2 ON t1.session_id = t2.session_id AND LEFT(t1.page_url, 191) = LEFT(t2.page_url, 191) AND t1.id < t2.id LIMIT 5000" ); if ( empty( $dupes ) ) { break; } $ids = implode( ',', array_map( 'absint', $dupes ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( false === $wpdb->query( "DELETE FROM `{$table}` WHERE id IN ({$ids})" ) ) { $ok = false; break; } } if ( ! empty( $page_idx ) ) { // A non-unique key with this name exists: replace it. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->query( "ALTER TABLE `{$table}` DROP INDEX `session_page`" ); } if ( $ok ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange $ok = ( false !== $wpdb->query( "ALTER TABLE `{$table}` ADD UNIQUE KEY `session_page` (`session_id`, `page_url`(191))" ) ); } } if ( ! $ok ) { set_transient( 'accessiq_db_upgrade_failed', $wpdb->last_error ? substr( $wpdb->last_error, 0, 200 ) : '1', HOUR_IN_SECONDS ); } return $ok; } public static function default_settings() { return array( 'widget_enabled' => true, 'widget_position' => 'bottom-right', 'widget_color' => '#1d4ed8', 'show_on_mobile' => true, 'widget_features' => array( 'font_size', 'contrast', 'dyslexia_font', 'keyboard_nav' ), 'profiles_enabled' => true, 'enabled_profiles' => array( 'vision', 'adhd', 'seizure', 'dyslexia', 'keyboard' ), 'scanner_enabled' => true, 'scan_level' => 'AA', 'scan_wcag_version' => '2.2', 'notify_email' => get_option( 'admin_email' ), 'analytics_enable_click_tracking' => true, 'analytics_enable_behavior_tracking' => true, 'analytics_exclude_logged_in' => false, 'analytics_replace_dashboard' => false, 'analytics_data_retention' => 90, ); } public function register_cpt() { register_post_type( ACCESSIQ_SCAN_CPT, array( 'public' => false, 'show_ui' => false, 'show_in_rest' => false, 'supports' => array( 'title', 'custom-fields' ), 'labels' => array( 'name' => 'AccessIQ Scans' ), ) ); } public function init() { // DB upgrade check — runs on every plugins_loaded, no-ops when version matches. $installed_db = (string) get_option( 'accessiq_db_version', '1.0' ); if ( $installed_db !== ACCESSIQ_DB_VERSION && ! get_transient( 'accessiq_db_upgrade_failed' ) ) { self::create_tables(); if ( self::upgrade_db( $installed_db ) ) { delete_transient( 'accessiq_db_upgrade_failed' ); update_option( 'accessiq_db_version', ACCESSIQ_DB_VERSION ); } } add_action( 'init', array( $this, 'register_cpt' ) ); add_action( 'init', function () { // Bundled translations (e.g. bn_BD); WordPress.org language packs still take precedence. load_plugin_textdomain( 'accessiq', false, dirname( plugin_basename( ACCESSIQ_FILE ) ) . '/languages' ); } ); AccessIQ_Settings::instance(); AccessIQ_AutoFix::instance(); AccessIQ_Widget::instance(); AccessIQ_Statement::init(); AccessIQ_Scanner::instance(); AccessIQ_Scheduler::instance(); $accessiq_heatmap = new AccessIQ_Heatmap_Tracker(); $accessiq_heatmap->init(); $accessiq_behavior = new AccessIQ_Behavior_Tracker(); $accessiq_behavior->init(); $accessiq_dashboard = new AccessIQ_Dashboard_Builder(); $accessiq_dashboard->init(); if ( is_admin() ) { $accessiq_analytics_menu = new AccessIQ_Analytics_Menu(); $accessiq_analytics_menu->init(); } } } AccessIQ::instance();