cache_time = $cache_time; $this->cache_name_prefix = 'edac_scans_stats_' . EDAC_VERSION . '_' . $this->record_limit; $this->rule_count = count( edac_register_rules() ); } /** * Load all stats into the cache. Should be called by a background scheduled. * * @return void */ public function load_cache() { // Cache the summary. $this->summary(); // Cache the post_types. $scannable_post_types = Settings::get_scannable_post_types(); $post_types = get_post_types( [ 'public' => true, ] ); unset( $post_types['attachment'] ); foreach ( $post_types as $post_type ) { if ( in_array( $post_type, $scannable_post_types, true ) ) { $this->issues_summary_by_post_type( $post_type ); } } } /** * Clear the summary and post type scans stats that have been cached * * @return void */ public function clear_cache() { // Delete the cached summary stats. $transient_name = $this->cache_name_prefix . '_summary'; delete_transient( $transient_name ); // Delete the cached post_type stats. $post_types = get_post_types( [ 'public' => true, ] ); unset( $post_types['attachment'] ); foreach ( $post_types as $post_type ) { $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type; delete_transient( $transient_name ); } } /** * Gets summary information about all scans * * @param boolean $skip_cache whether to skip the cache. * @return array */ public function summary( $skip_cache = false ) { global $wpdb; $transient_name = $this->cache_name_prefix . '_summary'; $cache = get_transient( $transient_name ); if ( ! $skip_cache && ( $this->cache_time && $cache ) ) { if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time() ) { $full_scan_completed_at = (int) get_option( 'edacp_fullscan_completed_at' ); if ( $full_scan_completed_at <= $cache['cached_at'] ) { // There hasn't been a full scan completed since we've cached, so return these results. $cache['cache_hit'] = true; return $cache; } } } $data = []; $scannable_posts_count = Settings::get_scannable_posts_count(); $tests_count = $scannable_posts_count * $this->rule_count; $siteid = get_current_blog_id(); $data['scannable_posts_count'] = (int) $scannable_posts_count; $data['rule_count'] = (int) $this->rule_count; $data['tests_count'] = (int) $tests_count; $data['scannable_post_types_count'] = (int) count( Settings::get_scannable_post_types( true ) ); $post_types = get_post_types( [ 'public' => true, ] ); unset( $post_types['attachment'] ); $data['public_post_types_count'] = (int) count( $post_types ); $issues_query = new Issues_Query( [], $this->record_limit, Issues_Query::FLAG_INCLUDE_ALL_POST_TYPES ); // Count total unique meta values from postmeta table where the meta_key is _edac_issue_density. // This will give us the total number of posts that have been scanned. $data['posts_scanned'] = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation. $wpdb->prepare( 'SELECT COUNT(DISTINCT %i) FROM %i WHERE meta_key = %s', 'post_id', $wpdb->postmeta, '_edac_issue_density' ) ); $data['is_truncated'] = $issues_query->has_truncated_results(); $data['posts_with_issues'] = (int) $issues_query->distinct_posts_count(); $data['rules_failed'] = 0; // Get a count of rules that are not in the issues table. $rule_slugs = array_map( function ( $item ) { return $item['slug']; }, edac_register_rules() ); foreach ( $rule_slugs as $rule_slug ) { $rule_query = new Issues_Query( [ 'rule_slugs' => [ $rule_slug ], 'post_types' => Settings::get_scannable_post_types(), ], 1 ); if ( $rule_query->count() ) { ++$data['rules_failed']; } } $data['rules_passed'] = $this->rule_count - $data['rules_failed']; $data['passed_percentage'] = 'N/A'; $scannable_post_types = Settings::get_scannable_post_types(); if ( ! empty( $scannable_post_types ) && $data['posts_scanned'] > 0 && $tests_count > 0 ) { $data['passed_percentage'] = round( ( $data['rules_passed'] / $this->rule_count ) * 100, 2 ); } $warning_issues_query = new Issues_Query( [ 'post_types' => Settings::get_scannable_post_types(), 'rule_types' => [ Issues_Query::RULETYPE_WARNING ], ], $this->record_limit ); $data['warnings'] = (int) $warning_issues_query->count(); $data['distinct_warnings'] = (int) $warning_issues_query->distinct_count(); $contrast_issues_query = new Issues_Query( [ 'post_types' => Settings::get_scannable_post_types(), 'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ], ], $this->record_limit ); $data['contrast_errors'] = (int) $contrast_issues_query->count(); $data['distinct_contrast_errors'] = (int) $contrast_issues_query->distinct_count(); $error_issues_query = new Issues_Query( [ 'post_types' => Settings::get_scannable_post_types(), 'rule_types' => [ Issues_Query::RULETYPE_ERROR ], ], $this->record_limit ); $data['errors'] = (int) $error_issues_query->count(); $data['distinct_errors'] = (int) $error_issues_query->distinct_count(); $data['errors_without_contrast'] = $data['errors'] - $data['contrast_errors']; $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors']; $ignored_issues_query = new Issues_Query( [ 'post_types' => Settings::get_scannable_post_types(), ], $this->record_limit, Issues_Query::FLAG_ONLY_IGNORED ); $data['ignored'] = (int) $ignored_issues_query->count(); $data['distinct_ignored'] = (int) $ignored_issues_query->distinct_count(); $data['posts_without_issues'] = 0; $data['avg_issues_per_post'] = 0; if ( $data['posts_scanned'] > 0 && ! empty( Settings::get_scannable_post_types() ) && ! empty( Settings::get_scannable_post_statuses() ) ) { $ac_table_name = $wpdb->prefix . 'accessibility_checker'; $posts_without_issues = " SELECT COUNT({$wpdb->posts}.ID) FROM {$wpdb->posts} LEFT JOIN " . $wpdb->prefix . "accessibility_checker ON {$wpdb->posts}.ID = " . $wpdb->prefix . 'accessibility_checker.postid WHERE ' . $wpdb->prefix . 'accessibility_checker.postid IS NULL AND post_type IN(' . Helpers::array_to_sql_safe_list( Settings::get_scannable_post_types() ) . ') AND post_status IN(' . Helpers::array_to_sql_safe_list( Settings::get_scannable_post_statuses() ) . ')'; // give me sql that will find all post ids in the accessibility_checker table // where ALL issues with that ID are eiter ignored or globally ignored. $posts_with_just_ignored_issues = $wpdb->prepare( 'SELECT COUNT( DISTINCT postid ) FROM %i WHERE siteid = %d AND postid NOT IN ( SELECT postid FROM %i WHERE ignre=0 AND ignre_global=0 )', [ $ac_table_name, $siteid, $ac_table_name ] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation. $data['posts_without_issues'] = $wpdb->get_var( $posts_without_issues ) + $wpdb->get_var( $posts_with_just_ignored_issues ); $data['avg_issues_per_post'] = round( ( $data['warnings'] + $data['errors'] ) / $data['posts_scanned'], 2 ); // Show "< 1" only when there are issues but the average is less than 1 (excluding exactly 0). if ( $data['avg_issues_per_post'] < 1 && $data['avg_issues_per_post'] > 0 ) { $data['avg_issues_per_post'] = '< 1'; } } // Average issue density percentage is the sum of all post densities divided by the number of posts scanned. $data['avg_issue_density_percentage'] = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation. $wpdb->prepare( 'SELECT AVG(meta_value) FROM ' . $wpdb->postmeta . ' JOIN ( SELECT DISTINCT postid FROM ' . $wpdb->prefix . 'accessibility_checker WHERE ignre=%d AND ignre_global=%d ) AS distinct_posts ON ' . $wpdb->postmeta . '.post_id = distinct_posts.postid WHERE meta_key = %s', [ 0, 0, '_edac_issue_density' ] ) ); if ( null === $data['avg_issue_density_percentage'] ) { $data['avg_issue_density_percentage'] = 'N/A'; } else { $data['avg_issue_density_percentage'] = round( $data['avg_issue_density_percentage'], 2 ); } $data['fullscan_running'] = false; $data['fullscan_state'] = ''; $data['fullscan_completed_at'] = 0; // For back compat reasons the old class_exists is kept and moved to an else block. // After a few releases the else should be removed. if ( class_exists( '\EqualizeDigital\AccessibilityCheckerPro\Admin\Scans' ) ) { $scans = new Scans(); $scan_state = $scans->scan_state(); $data['fullscan_state'] = $scan_state; if ( Scans::SCAN_STATE_PHP_SCAN_RUNNING === $scan_state || Scans::SCAN_STATE_JS_SCAN_RUNNING === $scan_state ) { $data['fullscan_running'] = true; } $data['fullscan_completed_at'] = $scans->scan_date( 'php' ); } elseif ( class_exists( '\EDACP\Scans' ) ) { $scans = new \EDACP\Scans(); $scan_state = $scans->scan_state(); $data['fullscan_state'] = $scan_state; if ( \EDACP\Scans::SCAN_STATE_PHP_SCAN_RUNNING === $scan_state || \EDACP\Scans::SCAN_STATE_JS_SCAN_RUNNING === $scan_state ) { $data['fullscan_running'] = true; } $data['fullscan_completed_at'] = $scans->scan_date( 'php' ); } $data['cache_id'] = $transient_name; $data['cached_at'] = time(); $data['expires_at'] = time() + $this->cache_time; $data['cache_hit'] = false; // Handle formatting. Assumes all are numbers except for those listed in exceptions. $formatting = []; $formatting_exceptions = [ 'is_truncated', 'passed_percentage', 'avg_issue_density_percentage', 'fullscan_running', 'fullscan_state', 'fullscan_completed_at', 'cache_id', 'cached_at', 'expires_at', 'cache_hit', ]; foreach ( $data as $key => $value ) { if ( ! in_array( $key, $formatting_exceptions, true ) ) { $formatting[ $key . '_formatted' ] = Helpers::format_number( $value ); } } // Handle exceptions. $formatting['fullscan_completed_at_formatted'] = 'N/A'; if ( $data['fullscan_completed_at'] > 0 ) { $formatting['fullscan_completed_at_formatted'] = Helpers::format_date( $data['fullscan_completed_at'], true ); } $formatting['passed_percentage_formatted'] = Helpers::format_percentage( $data['passed_percentage'] ); // The density should already a percentage value, just add the sign. If not an integer, just return the value which will be 'N/A'. $formatting['avg_issue_density_percentage_formatted'] = is_int( $data['avg_issue_density_percentage'] ) ? $data['avg_issue_density_percentage'] . '%' : $data['avg_issue_density_percentage']; $formatting['cached_at_formatted'] = Helpers::format_date( $data['cached_at'], true ); $data = array_merge( $data, $formatting ); if ( $data['posts_scanned'] > 0 ) { set_transient( $transient_name, $data, $this->cache_time ); } else { // no posts have been scanned, so clear any previously cache results. $this->clear_cache(); } return $data; } /** * Gets issues summary information about a post type * * @param string $post_type post type. * @return array . */ public function issues_summary_by_post_type( $post_type ) { $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type; $cache = get_transient( $transient_name ); if ( $this->cache_time && $cache ) { if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time() ) { $full_scan_completed_at = (int) get_option( 'edacp_fullscan_completed_at' ); if ( $full_scan_completed_at <= $cache['cached_at'] ) { // There hasn't been a full scan completed since we've cached, so return these results. $cache['cache_hit'] = true; return $cache; } } } $data = []; $error_issues_query = new Issues_Query( [ 'rule_types' => [ Issues_Query::RULETYPE_ERROR ], 'post_types' => [ $post_type ], ], $this->record_limit ); $data['errors'] = $error_issues_query->count(); $data['distinct_errors'] = $error_issues_query->distinct_count(); $warning_issues_query = new Issues_Query( [ 'rule_types' => [ Issues_Query::RULETYPE_WARNING ], 'post_types' => [ $post_type ], ], $this->record_limit ); $data['warnings'] = $warning_issues_query->count(); $data['distinct_warnings'] = $warning_issues_query->distinct_count(); $color_contrast_issues_query = new Issues_Query( [ 'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ], 'post_types' => [ $post_type ], ], $this->record_limit ); $data['contrast_errors'] = $color_contrast_issues_query->count(); $data['distinct_contrast_errors'] = $color_contrast_issues_query->distinct_count(); $data['errors_without_contrast'] = $data['errors'] - $data['contrast_errors']; $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors']; foreach ( $data as $key => $val ) { $data[ $key . '_formatted' ] = Helpers::format_number( $val ); } $data['cache_id'] = $transient_name; $data['cached_at'] = time(); $data['expires_at'] = time() + $this->cache_time; $data['cache_hit'] = false; set_transient( $transient_name, $data, $this->cache_time ); return $data; } }