array( 'verified' => 0, 'unverified' => 0, 'suspicious' => 0, ), 'by_bot' => array(), // label => [verified, unverified, suspicious]. 'by_day' => array(), // 'Y-m-d' => int (all statuses). 'by_page' => array(), // url => int (all statuses). 'first_seen' => '', 'last_seen' => '', ); } /** * Get the aggregate (backfilling missing top-level keys). * * @return array */ public static function get() { $stats = get_option( ADG_AIM_OPT_STATS, array() ); if ( ! is_array( $stats ) ) { $stats = array(); } return wp_parse_args( $stats, self::defaults() ); } /** * Record one visit. Called from ADG_AIM_Bot_Detector::log_visit(). * Runs independently of the visit log, but controlled by its own setting. * * @param string $bot_label Human-readable bot name. * @param string $status verified|unverified|suspicious. * @param string $url Request URL (already sanitized). */ public static function record( $bot_label, $status, $url ) { if ( ! (bool) ADG_AIM_Plugin::get_setting( 'stats_enabled', true ) ) { return; } $status = in_array( $status, self::STATUSES, true ) ? $status : 'unverified'; $label = ( '' !== (string) $bot_label ) ? (string) $bot_label : 'unknown'; $stats = self::get(); $now = current_time( 'mysql' ); // Totals by status. $stats['totals'][ $status ] = self::inc( $stats['totals'], $status ); // By bot (with a status breakdown). if ( ! isset( $stats['by_bot'][ $label ] ) || ! is_array( $stats['by_bot'][ $label ] ) ) { $stats['by_bot'][ $label ] = array( 'verified' => 0, 'unverified' => 0, 'suspicious' => 0, ); } $stats['by_bot'][ $label ][ $status ] = self::inc( $stats['by_bot'][ $label ], $status ); // By day (Y-m-d sorts lexicographically = chronologically). $day = current_time( 'Y-m-d' ); $stats['by_day'][ $day ] = self::inc( $stats['by_day'], $day ); $stats['by_day'] = self::trim_days( $stats['by_day'] ); // Top pages. if ( '' !== (string) $url ) { $stats['by_page'][ $url ] = self::inc( $stats['by_page'], $url ); $stats['by_page'] = self::trim_pages( $stats['by_page'] ); } if ( '' === $stats['first_seen'] ) { $stats['first_seen'] = $now; } $stats['last_seen'] = $now; update_option( ADG_AIM_OPT_STATS, $stats, false ); } /** * Reset all statistics (button in the admin). */ public static function reset() { delete_option( ADG_AIM_OPT_STATS ); } /** * Total visits across all statuses. * * @param array $stats Aggregate. * @return int */ public static function total( array $stats ) { $t = isset( $stats['totals'] ) && is_array( $stats['totals'] ) ? $stats['totals'] : array(); return (int) array_sum( array_map( 'intval', $t ) ); } /** * "Last N days" series as an ordered array 'Y-m-d' => count (with zeros). * * @param array $stats Aggregate. * @param int $days How many days back to include. * @return array */ public static function daily_series( array $stats, $days = 14 ) { $days = max( 1, (int) $days ); $by_day = isset( $stats['by_day'] ) && is_array( $stats['by_day'] ) ? $stats['by_day'] : array(); // Anchor to the same "today" as record() (current_time('Y-m-d')), and // subtract days in a UTC context — so the series does not depend on the server TZ. $tz = new DateTimeZone( 'UTC' ); $today = DateTimeImmutable::createFromFormat( 'Y-m-d', current_time( 'Y-m-d' ), $tz ); if ( false === $today ) { $today = new DateTimeImmutable( 'now', $tz ); } $series = array(); for ( $i = $days - 1; $i >= 0; $i-- ) { $key = $today->modify( "-{$i} days" )->format( 'Y-m-d' ); $series[ $key ] = isset( $by_day[ $key ] ) ? (int) $by_day[ $key ] : 0; } return $series; } /** * Top pages by visits. * * @param array $stats Aggregate. * @param int $n How many to return. * @return array url => count. */ public static function top_pages( array $stats, $n = 10 ) { $pages = isset( $stats['by_page'] ) && is_array( $stats['by_page'] ) ? $stats['by_page'] : array(); arsort( $pages ); return array_slice( $pages, 0, max( 1, (int) $n ), true ); } /* ------------------------------------------------------------------ */ /** * Increment a counter-array key's value (cast to int). * * @param array $arr Array. * @param string|int $key Key. * @return int */ private static function inc( $arr, $key ) { return ( isset( $arr[ $key ] ) ? (int) $arr[ $key ] : 0 ) + 1; } /** * Trim the by-day breakdown to the last DAYS_KEEP. * * @param array $days 'Y-m-d' => int. * @return array */ private static function trim_days( array $days ) { if ( count( $days ) <= self::DAYS_KEEP ) { return $days; } ksort( $days ); // Y-m-d: lexicographically = chronologically. return array_slice( $days, - self::DAYS_KEEP, null, true ); } /** * Shrink top pages on overflow (keep the most-visited). * * @param array $pages url => int. * @return array */ private static function trim_pages( array $pages ) { if ( count( $pages ) <= self::PAGES_CAP ) { return $pages; } arsort( $pages ); return array_slice( $pages, 0, self::PAGES_TARGET, true ); } }