query( "DELETE FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- テーブル名は自プラグインの定数由来。 return; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- テーブル名は自プラグインの定数由来。 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, crawler_id, ip, ip_hash, queued_at FROM {$table} ORDER BY id LIMIT %d", self::VERIFY_BATCH ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); if ( ! is_array( $rows ) || array() === $rows ) { return; } try { $repository = DefinitionRepository::fromFile( AILENS_PLUGIN_DIR . 'vendor-core/data/crawlers.json' ); } catch ( \Throwable $e ) { return; } // IPレンジスナップショット(同梱)。読めない場合はrDNSのみで続行する。 try { $ranges = \AilysLens\Core\IpRangeRepository::fromFile( AILENS_PLUGIN_DIR . 'vendor-core/data/crawler-ip-ranges.json' ); } catch ( \Throwable $e ) { $ranges = null; } $rdns_verifier = new RdnsVerifier(); $range_verifier = new \AilysLens\Core\Verification\IpRangeVerifier(); $processed_ids = array(); foreach ( $rows as $row ) { $processed_ids[] = (int) $row->id; // 成否に関わらず処理済み=生IP破棄。 $queued_at = strtotime( (string) $row->queued_at . ' ' . wp_timezone_string() ); if ( false !== $queued_at && ( time() - $queued_at ) > self::QUEUE_TTL ) { continue; } $crawler = $repository->findById( (string) $row->crawler_id ); if ( null === $crawler ) { continue; } // 仕様書§3.3: レンジ照合を優先(DNS往復なし・決定的)。 // 不一致・レンジ未収載の場合はrDNSドメイン定義があればrDNSへフォールバック。 $verified = false; if ( null !== $ranges && $ranges->hasRanges( $crawler->id ) ) { $verified = $range_verifier->matches( (string) $row->ip, $ranges->rangesFor( $crawler->id ) ); } if ( ! $verified && array() !== $crawler->verifyDomains() ) { $verified = $rdns_verifier->verify( (string) $row->ip, $crawler->verifyDomains() ); } if ( $verified ) { self::mark_verified( (string) $row->crawler_id, (string) $row->ip_hash ); } } $id_list = implode( ',', array_map( 'intval', $processed_ids ) ); $wpdb->query( "DELETE FROM {$table} WHERE id IN ({$id_list})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- intval済みID列とプラグイン定数由来のテーブル名のみ。 } /** * 同一クローラー・同一IPハッシュ(=同一日・同一IP)の未検証レコードを昇格する。 * * @param string $crawler_id クローラーID。 * @param string $ip_hash IPハッシュ。 */ private static function mark_verified( string $crawler_id, string $ip_hash ): void { global $wpdb; $table = Ailens_Schema::hits_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET verified = 1 WHERE crawler_id = %s AND ip_hash = %s AND verified = 0", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $crawler_id, $ip_hash ) ); } /** * 日次処理: 保持期間パージ+日次サマリの再集計+放置インポートのTTL掃除。 */ public static function run_daily(): void { self::purge_expired(); self::rebuild_daily_summary( 3 ); // 中断放置されたインポート作業ファイル(生IPを含む)の破棄。 require_once AILENS_PLUGIN_DIR . 'includes/admin/class-ailens-import.php'; Ailens_Import::cleanup_stale(); } /** * 保持期間(30/90/365日)を過ぎたレコードを削除する。 */ private static function purge_expired(): void { global $wpdb; $days = (int) get_option( 'ailens_retention_days', 90 ); if ( ! in_array( $days, array( 30, 90, 365 ), true ) ) { $days = 90; } $cutoff = ( new \DateTimeImmutable( 'now', wp_timezone() ) ) ->modify( '-' . $days . ' days' ) ->format( 'Y-m-d H:i:s' ); $hits = Ailens_Schema::hits_table(); $daily = Ailens_Schema::daily_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM {$hits} WHERE hit_at < %s", $cutoff ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM {$daily} WHERE summary_date < %s", substr( $cutoff, 0, 10 ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * 直近N日分の日次サマリを hits から再集計する(ダッシュボードは原則サマリから描画)。 * * 冪等: 対象日の行を消してから INSERT ... SELECT で入れ直す。 * verified昇格が遅れて反映されるケースも直近日の再集計で吸収する。 * * @param int $days_back 今日を含めて何日分を再集計するか。 */ public static function rebuild_daily_summary( int $days_back = 3 ): void { $from = ( new \DateTimeImmutable( 'now', wp_timezone() ) ) ->modify( '-' . max( 0, $days_back - 1 ) . ' days' ) ->format( 'Y-m-d' ); self::rebuild_summary_from( $from ); } /** * 指定日以降の日次サマリを再集計する(ログインポート完了時に過去日を反映するため)。 * * @param string $from Y-m-d(この日を含む)。 */ public static function rebuild_summary_from( string $from ): void { global $wpdb; $hits = Ailens_Schema::hits_table(); $daily = Ailens_Schema::daily_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM {$daily} WHERE summary_date >= %s", $from ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- テーブル名は自プラグインの定数由来。 $wpdb->query( $wpdb->prepare( "INSERT INTO {$daily} (summary_date, crawler_id, category, hits, verified_hits) SELECT DATE(hit_at), crawler_id, category, COUNT(*), SUM(verified) FROM {$hits} WHERE hit_at >= %s GROUP BY DATE(hit_at), crawler_id, category", $from . ' 00:00:00' ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } }