| @@ -78,15 +78,45 @@ | ||
| 78 | 78 | * Constructor |
| 79 | 79 | */ |
| 80 | 80 | public function __construct() { |
| 81 | 81 | $this->board_id = WPF()->board->get_current( 'boardid' ); |
| 82 | - $this->register_cron_hooks(); | |
| 83 | 82 | |
| 83 | + // Register the callback for the cron hook so any already-scheduled | |
| 84 | + // cron event from a prior connected state can still execute (and so | |
| 85 | + // schedule_cron_jobs() can rely on the action being wired). Scheduling | |
| 86 | + // of the event itself is gated by AI connection — see schedule_cron_jobs(). | |
| 87 | + add_action( 'wpforo_ai_cleanup_expired_cache', [ $this, 'cleanup_expired_cache' ] ); | |
| 88 | + | |
| 84 | 89 | // Update board_id when WPF()->change_board() is called |
| 85 | 90 | add_action( 'wpforo_after_change_board', [ $this, 'on_board_change' ] ); |
| 86 | 91 | } |
| 87 | 92 | |
| 88 | 93 | /** |
| 94 | + * Schedule recurring local-storage maintenance crons. | |
| 95 | + * | |
| 96 | + * Called from AIClient::register_ai_crons() on tenant connect. | |
| 97 | + * Safe to call repeatedly — it skips if already scheduled. | |
| 98 | + */ | |
| 99 | + public function schedule_cron_jobs() { | |
| 100 | + if ( ! wp_next_scheduled( 'wpforo_ai_cleanup_expired_cache' ) ) { | |
| 101 | + wp_schedule_event( time(), 'hourly', 'wpforo_ai_cleanup_expired_cache' ); | |
| 102 | + } | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 106 | + * Unschedule recurring local-storage maintenance crons. | |
| 107 | + * | |
| 108 | + * Called from AIClient::unregister_ai_crons() on tenant disconnect. | |
| 109 | + */ | |
| 110 | + public function unschedule_cron_jobs() { | |
| 111 | + $ts = wp_next_scheduled( 'wpforo_ai_cleanup_expired_cache' ); | |
| 112 | + if ( $ts ) { | |
| 113 | + wp_unschedule_event( $ts, 'wpforo_ai_cleanup_expired_cache' ); | |
| 114 | + } | |
| 115 | + wp_clear_scheduled_hook( 'wpforo_ai_cleanup_expired_cache' ); | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 89 | 119 | * Handle board change event |
| 90 | 120 | * |
| 91 | 121 | * Updates internal board_id to match the new board context. |
| 92 | 122 | * Called by wpforo_after_change_board hook. |
| @@ -98,21 +128,8 @@ | ||
| 98 | 128 | $this->storage_mode = null; // Reset cached storage mode |
| 99 | 129 | } |
| 100 | 130 | |
| 101 | 131 | /** |
| 102 | - * Register cron hooks for local storage maintenance | |
| 103 | - */ | |
| 104 | - private function register_cron_hooks() { | |
| 105 | - // Register cleanup action - must be done here so callback exists when cron fires | |
| 106 | - add_action( 'wpforo_ai_cleanup_expired_cache', [ $this, 'cleanup_expired_cache' ] ); | |
| 107 | - | |
| 108 | - // Schedule if not already scheduled | |
| 109 | - if ( ! wp_next_scheduled( 'wpforo_ai_cleanup_expired_cache' ) ) { | |
| 110 | - wp_schedule_event( time(), 'hourly', 'wpforo_ai_cleanup_expired_cache' ); | |
| 111 | - } | |
| 112 | - } | |
| 113 | - | |
| 114 | - /** | |
| 115 | 132 | * Cleanup expired cache entries (cron callback) |
| 116 | 133 | */ |
| 117 | 134 | public function cleanup_expired_cache() { |
| 118 | 135 | if ( $this->is_local_mode() ) { |
| @@ -546,13 +563,15 @@ | ||
| 546 | 563 | if ( isset( $topic['status'] ) && (int) $topic['status'] !== 0 ) { |
| 547 | 564 | return new \WP_Error( 'unapproved_topic', wpforo_phrase( 'Unapproved topics cannot be indexed', false ) ); |
| 548 | 565 | } |
| 549 | 566 | |
| 550 | - // Get all posts for this topic ordered by creation date | |
| 567 | + // Get all approved posts for this topic ordered by creation date | |
| 568 | + // Only index approved posts (status=0) to prevent unapproved content from appearing in search | |
| 551 | 569 | $posts = WPF()->post->get_posts( [ |
| 552 | 570 | 'topicid' => $topicid, |
| 553 | 571 | 'orderby' => 'created', |
| 554 | 572 | 'order' => 'ASC', |
| 573 | + 'status' => 0, | |
| 555 | 574 | ] ); |
| 556 | 575 | if ( empty( $posts ) ) { |
| 557 | 576 | return new \WP_Error( 'no_posts', wpforo_phrase( 'No posts found for topic', false ) ); |
| 558 | 577 | } |
| @@ -809,12 +828,14 @@ | ||
| 809 | 828 | } |
| 810 | 829 | |
| 811 | 830 | // Bypass user permission check - this is admin-initiated backend indexing. |
| 812 | 831 | // Private/unapproved topics are already filtered above. |
| 832 | + // Only index approved posts (status=0) to prevent unapproved content from appearing in search. | |
| 813 | 833 | $posts = WPF()->post->get_posts( [ |
| 814 | 834 | 'topicid' => $topicid, |
| 815 | 835 | 'orderby' => 'created', |
| 816 | 836 | 'order' => 'ASC', |
| 837 | + 'status' => 0, | |
| 817 | 838 | 'check_private' => false, |
| 818 | 839 | ] ); |
| 819 | 840 | |
| 820 | 841 | if ( empty( $posts ) ) { |
| @@ -3233,8 +3254,15 @@ | ||
| 3233 | 3254 | |
| 3234 | 3255 | $column = $this->is_local_mode() ? 'local' : 'cloud'; |
| 3235 | 3256 | $topics_table = WPF()->tables->topics; |
| 3236 | 3257 | |
| 3258 | + // Cache for 1 day - these counts don't change frequently | |
| 3259 | + $cache_key = 'wpforo_ai_status_breakdown_' . $this->board_id . '_' . $this->get_storage_mode(); | |
| 3260 | + $cached = get_transient( $cache_key ); | |
| 3261 | + if ( false !== $cached && is_array( $cached ) ) { | |
| 3262 | + return $cached; | |
| 3263 | + } | |
| 3264 | + | |
| 3237 | 3265 | // Get counts for each category in a single query |
| 3238 | 3266 | $results = $wpdb->get_row( |
| 3239 | 3267 | "SELECT |
| 3240 | 3268 | COUNT(*) as total, |
| @@ -3256,9 +3284,9 @@ | ||
| 3256 | 3284 | 'storage_mode' => $this->get_storage_mode(), |
| 3257 | 3285 | ]; |
| 3258 | 3286 | } |
| 3259 | 3287 | |
| 3260 | - return [ | |
| 3288 | + $breakdown = [ | |
| 3261 | 3289 | 'total' => (int) $results['total'], |
| 3262 | 3290 | 'indexed' => (int) $results['indexed'], |
| 3263 | 3291 | 'pending' => (int) $results['pending'], |
| 3264 | 3292 | 'private' => (int) $results['private_topics'], |
| @@ -3264,8 +3292,24 @@ | ||
| 3264 | 3292 | 'private' => (int) $results['private_topics'], |
| 3265 | 3293 | 'unapproved' => (int) $results['unapproved'], |
| 3266 | 3294 | 'storage_mode' => $this->get_storage_mode(), |
| 3267 | 3295 | ]; |
| 3296 | + | |
| 3297 | + set_transient( $cache_key, $breakdown, DAY_IN_SECONDS ); | |
| 3298 | + | |
| 3299 | + return $breakdown; | |
| 3300 | + } | |
| 3301 | + | |
| 3302 | + /** | |
| 3303 | + * Clear the indexing status breakdown cache | |
| 3304 | + * | |
| 3305 | + * Should be called when topics are approved/unapproved or privacy changes. | |
| 3306 | + */ | |
| 3307 | + public function clear_indexing_status_breakdown_cache() { | |
| 3308 | + $cache_key_local = 'wpforo_ai_status_breakdown_' . $this->board_id . '_local'; | |
| 3309 | + $cache_key_cloud = 'wpforo_ai_status_breakdown_' . $this->board_id . '_cloud'; | |
| 3310 | + delete_transient( $cache_key_local ); | |
| 3311 | + delete_transient( $cache_key_cloud ); | |
| 3268 | 3312 | } |
| 3269 | 3313 | |
| 3270 | 3314 | /** |
| 3271 | 3315 | * Get sample topics that are pending indexing |