looks_like_a_bot() ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id ) { return; } $today = current_time( 'Y-m-d' ); $visitor_key = 'acsseo_seen_' . md5( $post_id . '|' . $today . '|' . $this->visitor_fingerprint() ); if ( false !== get_transient( $visitor_key ) ) { return; } set_transient( $visitor_key, 1, DAY_IN_SECONDS ); $day_key = self::DAY_PREFIX . $today; $current = (int) get_post_meta( $post_id, $day_key, true ); update_post_meta( $post_id, $day_key, $current + 1 ); $total = (int) get_post_meta( $post_id, self::TOTAL_KEY, true ); update_post_meta( $post_id, self::TOTAL_KEY, $total + 1 ); } /** * One way fingerprint of the current visitor. * * The raw address and browser string are hashed immediately and never * written to the database; only the hash reaches the transient key. * * @return string */ private function visitor_fingerprint() { $address = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; return wp_hash( $address . '|' . $agent ); } /** * Cheap user agent test so obvious crawlers are not counted. * * @return bool */ private function looks_like_a_bot() { $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( '' === $agent ) { return true; } return (bool) preg_match( '/bot|crawl|spider|slurp|scrape|curl|wget|monitor|preview|headless/i', $agent ); } /** * Read the counters for one entry. * * @param int $post_id Post ID. * @return array{today:int,yesterday:int,month:int,total:int} */ public static function summary( $post_id ) { $all = get_post_meta( $post_id ); $today = current_time( 'Y-m-d' ); $yesterday = gmdate( 'Y-m-d', strtotime( $today . ' -1 day' ) ); $month = substr( $today, 0, 7 ); $read = static function ( $key ) use ( $all ) { return isset( $all[ $key ][0] ) ? (int) $all[ $key ][0] : 0; }; $month_total = 0; foreach ( $all as $key => $value ) { if ( 0 !== strpos( $key, self::DAY_PREFIX ) ) { continue; } if ( 0 === strpos( substr( $key, strlen( self::DAY_PREFIX ) ), $month ) ) { $month_total += isset( $value[0] ) ? (int) $value[0] : 0; } } return array( 'today' => $read( self::DAY_PREFIX . $today ), 'yesterday' => $read( self::DAY_PREFIX . $yesterday ), 'month' => $month_total, 'total' => $read( self::TOTAL_KEY ), ); } /** * Add the views column to the supported list tables. * * @return void */ public function register_columns() { if ( ! ACSSEO_Settings::get( 'stats_enabled' ) || ! ACSSEO_Settings::get( 'stats_column' ) ) { return; } foreach ( ACSSEO_Meta::supported_post_types() as $post_type ) { add_filter( "manage_{$post_type}_posts_columns", array( $this, 'add_column' ) ); add_action( "manage_{$post_type}_posts_custom_column", array( $this, 'render_column' ), 10, 2 ); } } /** * Append the views column. * * @param array $columns Existing columns. * @return array */ public function add_column( $columns ) { $columns['acsseo_views'] = __( 'Views', 'acs-seo-plus' ); return $columns; } /** * Print one cell of the views column. * * @param string $column Column key. * @param int $post_id Post ID. * @return void */ public function render_column( $column, $post_id ) { if ( 'acsseo_views' !== $column ) { return; } $summary = self::summary( $post_id ); printf( '%1$s
%2$s
', esc_html( sprintf( /* translators: %s: number of views today. */ __( 'Today: %s', 'acs-seo-plus' ), number_format_i18n( $summary['today'] ) ) ), esc_html( sprintf( /* translators: %s: total number of views. */ __( 'Total: %s', 'acs-seo-plus' ), number_format_i18n( $summary['total'] ) ) ) ); } /** * Add the statistics meta box to the editor. * * @return void */ public function add_meta_box() { if ( ! ACSSEO_Settings::get( 'stats_enabled' ) ) { return; } foreach ( ACSSEO_Meta::supported_post_types() as $post_type ) { add_meta_box( 'acsseo_stats_box', __( 'Page views', 'acs-seo-plus' ), array( $this, 'render_meta_box' ), $post_type, 'side', 'default' ); } } /** * Print the statistics meta box. * * @param WP_Post $post Current post. * @return void */ public function render_meta_box( $post ) { $summary = self::summary( $post->ID ); $rows = array( __( 'Today', 'acs-seo-plus' ) => $summary['today'], __( 'Yesterday', 'acs-seo-plus' ) => $summary['yesterday'], __( 'This month', 'acs-seo-plus' ) => $summary['month'], __( 'All time', 'acs-seo-plus' ) => $summary['total'], ); ?> $value ) : ?>

get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- housekeeping task that runs once a day and has no core API equivalent. $wpdb->prepare( "SELECT post_id, meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key < %s LIMIT 5000", $wpdb->esc_like( self::DAY_PREFIX ) . '%', self::DAY_PREFIX . $cutoff ) ); foreach ( $stale as $row ) { delete_post_meta( (int) $row->post_id, $row->meta_key ); } } }