\d+)', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( __CLASS__, 'increment' ), 'permission_callback' => '__return_true', 'args' => array( 'id' => array( 'required' => true, 'sanitize_callback' => 'absint', 'validate_callback' => static fn( $value ): bool => absint( $value ) > 0, ), 'token' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => static fn( $value ): bool => is_string( $value ) && 64 === strlen( $value ), ), ), ) ); } public static function increment( WP_REST_Request $request ): WP_REST_Response|WP_Error { $post_id = absint( $request['id'] ); $post = get_post( $post_id ); if ( ! $post || 'sg7g_media' !== $post->post_type || 'publish' !== $post->post_status ) { return new WP_Error( 'sg7g_invalid_media', __( 'Invalid gallery content.', '7sgaleri' ), array( 'status' => 404 ) ); } if ( '' !== (string) $post->post_password && post_password_required( $post ) ) { return new WP_Error( 'sg7g_protected_media', __( 'This gallery content is password protected.', '7sgaleri' ), array( 'status' => 403 ) ); } $token = (string) $request->get_param( 'token' ); if ( ! self::verify_token( $post_id, $token ) ) { return new WP_Error( 'sg7g_invalid_view_token', __( 'Invalid view request.', '7sgaleri' ), array( 'status' => 403 ) ); } if ( self::is_prefetch_request( $request ) || self::is_probable_bot() ) { return self::response( $post_id, false ); } $cookie_name = 'sg7g_viewed_' . $post_id; if ( isset( $_COOKIE[ $cookie_name ] ) ) { return self::response( $post_id, false ); } // A short-lived server-side fingerprint limits repeated automated hits // without storing the raw IP address or user-agent value. $fingerprint = self::fingerprint( $post_id ); if ( $fingerprint && get_transient( $fingerprint ) ) { return self::response( $post_id, false ); } $count = self::increment_atomic( $post_id ); SG7G_Statistics::record( $post_id ); if ( $fingerprint ) { set_transient( $fingerprint, 1, 6 * HOUR_IN_SECONDS ); } if ( ! headers_sent() ) { $cookie_path = defined( 'COOKIEPATH' ) && COOKIEPATH ? COOKIEPATH : '/'; $cookie_domain = defined( 'COOKIE_DOMAIN' ) && COOKIE_DOMAIN ? COOKIE_DOMAIN : ''; setcookie( $cookie_name, '1', array( 'expires' => time() + DAY_IN_SECONDS, 'path' => $cookie_path, 'domain' => $cookie_domain, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); } return self::response( $post_id, true, $count ); } public static function get( int $post_id ): int { return max( 0, (int) get_post_meta( $post_id, self::META_KEY, true ) ); } public static function token( int $post_id ): string { return hash_hmac( 'sha256', 'sg7g-view|' . $post_id, wp_salt( 'auth' ) ); } public static function format( int $count ): string { if ( $count >= 1000000 ) { return self::shorten( $count / 1000000, 'M' ); } if ( $count >= 1000 ) { return self::shorten( $count / 1000, 'B' ); } return (string) $count; } private static function increment_atomic( int $post_id ): int { global $wpdb; add_post_meta( $post_id, self::META_KEY, 0, true ); // Atomic counter normalization requires a direct read of duplicate meta rows. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s ORDER BY meta_id ASC", $post_id, self::META_KEY ) ); if ( empty( $rows ) ) { return 0; } $primary_id = (int) $rows[0]->meta_id; if ( count( $rows ) > 1 ) { $total = 0; foreach ( $rows as $row ) { $total += max( 0, (int) $row->meta_value ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $wpdb->postmeta, array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Updating one row selected by meta_id. 'meta_value' => $total, ), array( 'meta_id' => $primary_id ), array( '%d' ), array( '%d' ) ); foreach ( array_slice( $rows, 1 ) as $duplicate ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->delete( $wpdb->postmeta, array( 'meta_id' => (int) $duplicate->meta_id ), array( '%d' ) ); } } // Atomic increment cannot be expressed through update_post_meta(). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = CAST(meta_value AS UNSIGNED) + 1 WHERE meta_id = %d", $primary_id ) ); wp_cache_delete( $post_id, 'post_meta' ); return self::get( $post_id ); } private static function response( int $post_id, bool $counted, ?int $count = null ): WP_REST_Response { $count = null === $count ? self::get( $post_id ) : $count; $response = rest_ensure_response( array( 'count' => $count, 'formatted' => self::format( $count ), 'label' => sprintf( /* translators: %s is the view count. */ /* translators: %s: Formatted view count. */ __( '%s views', '7sgaleri' ), number_format_i18n( $count ) ), 'counted' => $counted, ) ); $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' ); $response->header( 'CDN-Cache-Control', 'no-store' ); $response->header( 'Pragma', 'no-cache' ); $response->header( 'X-Robots-Tag', 'noindex, nofollow' ); return $response; } private static function verify_token( int $post_id, string $token ): bool { return '' !== $token && hash_equals( self::token( $post_id ), $token ); } private static function shorten( float $value, string $suffix ): string { $formatted = number_format_i18n( $value, $value >= 10 ? 0 : 1 ); $formatted = rtrim( rtrim( $formatted, '0' ), '.,' ); return $formatted . $suffix; } private static function fingerprint( int $post_id ): string { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( '' === $ip && '' === $ua ) { return ''; } return 'sg7g_view_' . md5( wp_salt( 'nonce' ) . '|' . $post_id . '|' . $ip . '|' . $ua ); } private static function is_prefetch_request( WP_REST_Request $request ): bool { $purpose = strtolower( trim( $request->get_header( 'purpose' ) . ' ' . $request->get_header( 'sec-purpose' ) ) ); return str_contains( $purpose, 'prefetch' ) || str_contains( $purpose, 'prerender' ); } private static function is_probable_bot(): bool { $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) ) : ''; if ( '' === $ua ) { return false; } return 1 === preg_match( '/(?:bot|crawler|spider|slurp|bingpreview|facebookexternalhit|whatsapp|telegrambot|discordbot|linkedinbot|pinterest|headlesschrome|lighthouse|pagespeed|uptimerobot|monitoring)/i', $ua ); } }