0, 'aiaas_bytes' => 0, 'media_bytes' => 0]; } $cache_key = 'aiaas_storage_' . $user_id; $cached = get_transient($cache_key); if (is_array($cached) && isset($cached['total_bytes'], $cached['media_bytes']) && (isset($cached['aiaas_bytes']) || isset($cached['aas_bytes']))) { return [ 'total_bytes' => (int)$cached['total_bytes'], 'aiaas_bytes' => (int)($cached['aiaas_bytes'] ?? $cached['aas_bytes'] ?? 0), 'media_bytes' => (int)$cached['media_bytes'], ]; } // Manual-only storage recalculation stores results in user meta. // Prefer the new Storage V2 meta (single array) when present. $v2 = get_user_meta($user_id, 'aiaas_storage_v2', true); if (is_array($v2) && isset($v2['total_bytes'])) { $result = [ 'total_bytes' => max(0, (int)($v2['total_bytes'] ?? 0)), // "aiaas_bytes" = text/history 'aiaas_bytes' => max(0, (int)($v2['text_bytes'] ?? ($v2['aiaas_bytes'] ?? ($v2['aas_bytes'] ?? 0)))), 'media_bytes' => max(0, (int)($v2['media_bytes'] ?? 0)), ]; set_transient($cache_key, $result, 15 * 60); return $result; } // Back-compat: older meta keys (pre Storage V2) $meta_total = (int) get_user_meta($user_id, 'aiaas_storage_total_bytes', true); $meta_chapters = (int) get_user_meta($user_id, 'aiaas_storage_chapter_bytes', true); $meta_snapshots = (int) get_user_meta($user_id, 'aiaas_storage_snapshot_bytes', true); $meta_media = (int) get_user_meta($user_id, 'aiaas_storage_media_bytes', true); $meta_calced = (string) get_user_meta($user_id, 'aiaas_storage_calculated_at', true); if ($meta_calced !== '') { $result = [ 'total_bytes' => max(0, $meta_total), // "aiaas_bytes" = chapters + snapshots (AIAAS-owned text/history) 'aiaas_bytes' => max(0, $meta_chapters + $meta_snapshots), 'media_bytes' => max(0, $meta_media), ]; set_transient($cache_key, $result, 15 * 60); return $result; } // SAFETY: Do not perform expensive scans during portal/front-end loads. // If no cached value exists, return zeros and cache briefly. // Admin tools can clear cache and/or force recalculation explicitly. if (!is_admin() && !defined('AIAAS_ALLOW_STORAGE_SCAN')) { $zero = ['total_bytes' => 0, 'aiaas_bytes' => 0, 'media_bytes' => 0]; set_transient($cache_key, $zero, 5 * MINUTE_IN_SECONDS); return $zero; } // Helper: safely get attachment filesize. $size_of_attachment = static function(int $att_id): int { // Do not count sample-book assets toward a user's storage quota. if (get_post_meta($att_id, '_aiaas_sample_asset', true)) { return 0; } // Do not count sample-book assets toward a user's storage quota. if (get_post_meta($att_id, '_aiaas_sample_asset', true)) { return 0; } $att_id = absint($att_id); if ($att_id < 1) return 0; $file = get_attached_file($att_id); if (!is_string($file) || $file === '' || !file_exists($file)) return 0; $sz = @filesize($file); return is_int($sz) ? max(0, $sz) : 0; }; // (1) AAS-tagged uploads (marked during AAS usage) // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Small attachment meta lookup for per-user quota. $aas_ids = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => -1, 'fields' => 'ids', 'meta_key' => '_aiaas_uploaded_via_aas', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'author' => $user_id, ]); $aiaas_bytes = 0; if (is_array($aas_ids) && !empty($aas_ids)) { foreach ($aas_ids as $aid) { $aiaas_bytes += $size_of_attachment((int)$aid); } } // (2) All media library attachments owned by user $media_ids = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => -1, 'fields' => 'ids', 'author' => $user_id, ]); $media_bytes = 0; if (is_array($media_ids) && !empty($media_ids)) { foreach ($media_ids as $mid) { $media_bytes += $size_of_attachment((int)$mid); } } // Note: media_bytes already includes aas-tagged uploads (when the uploader is the author), // so total should not double-count. $result = [ 'total_bytes' => (int)$media_bytes, 'aiaas_bytes' => (int)$aiaas_bytes, 'media_bytes' => (int)$media_bytes, ]; // Cache briefly; media scans can be heavy on large sites. set_transient($cache_key, $result, 15 * 60); return $result; } /** * Return quick stats for a project. * * @return array{chapter_count:int, archived_count:int, word_count:int} */ public static function get_project_stats(int $project_id, int $user_id): array { $project_id = absint($project_id); $user_id = absint($user_id); if ($project_id < 1 || $user_id < 1) { return ['chapter_count' => 0, 'archived_count' => 0, 'word_count' => 0]; } $cache_key = 'aiaas_stats_' . $project_id . '_' . $user_id; $cached = get_transient($cache_key); if (!is_array($cached)) { $cached = get_transient('aiaas_stats_' . $project_id . '_' . $user_id); } if (!is_array($cached)) { $cached = get_transient('aiaas_stats_' . $project_id . '_' . $user_id); } if (is_array($cached) && isset($cached['chapter_count'], $cached['archived_count'], $cached['word_count'])) { return [ 'chapter_count' => (int)$cached['chapter_count'], 'archived_count' => (int)$cached['archived_count'], 'word_count' => (int)$cached['word_count'], ]; } // Active chapters $active_ids = get_posts([ 'post_type' => 'aiaas_chapter', 'post_status' => ['publish', 'draft', 'private'], 'numberposts' => 200, 'fields' => 'ids', 'orderby' => 'menu_order', 'order' => 'ASC', 'post_parent' => $project_id, ]); $chapter_count = is_array($active_ids) ? count($active_ids) : 0; // Archived chapters (stored in Trash) // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Small per-project meta lookup for archived chapter count. $archived_ids = get_posts([ 'post_type' => 'aiaas_chapter', 'post_status' => 'trash', 'posts_per_page' => -1, 'fields' => 'ids', 'meta_key' => 'aiaas_project_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => (string)$project_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'orderby' => 'menu_order', 'order' => 'ASC', ]); $archived_count = is_array($archived_ids) ? count($archived_ids) : 0; // Word count (active chapters only) $word_count = 0; if ($chapter_count > 0) { foreach ($active_ids as $cid) { $content = (string) get_post_field('post_content', $cid); $content = wp_strip_all_tags($content); $words = preg_split('/\s+/u', trim($content)); if (is_array($words)) { $word_count += count(array_filter($words, static function($w){ return $w !== ''; })); } } } $result = [ 'chapter_count' => (int)$chapter_count, 'archived_count' => (int)$archived_count, 'word_count' => (int)$word_count, ]; // Keep it short; stats can change often while writing. set_transient($cache_key, $result, 60); return $result; } /** * Most recently modified active chapter for a project. */ public static function get_last_edited_chapter_id(int $project_id): int { $project_id = absint($project_id); if ($project_id < 1) return 0; $posts = get_posts([ 'post_type' => 'aiaas_chapter', 'post_status' => ['publish', 'draft', 'private'], 'numberposts' => 1, 'fields' => 'ids', 'orderby' => 'modified', 'order' => 'DESC', 'post_parent' => $project_id, ]); if (is_array($posts) && !empty($posts[0])) { return absint($posts[0]); } return 0; } /** * First active chapter by menu order. */ public static function get_first_chapter_id(int $project_id): int { $project_id = absint($project_id); if ($project_id < 1) return 0; $posts = get_posts([ 'post_type' => 'aiaas_chapter', 'post_status' => ['publish', 'draft', 'private'], 'numberposts' => 1, 'fields' => 'ids', 'orderby' => 'menu_order', 'order' => 'ASC', 'post_parent' => $project_id, ]); if (is_array($posts) && !empty($posts[0])) { return absint($posts[0]); } return 0; } } }