admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aiaas_storage_v2'), 'deleteConfirm' => 'Delete ALL AAS files for this user? This is permanent.', 'workingText' => 'Working...', 'deletingText' => 'Deleting...', )); wp_enqueue_script($handle); } } if (!function_exists('aiaas_storage_v2_enabled')) { function aiaas_storage_v2_enabled(): bool { return true; } } if (!function_exists('aiaas_storage_v2_human')) { function aiaas_storage_v2_human($bytes): string { $bytes = max(0, (int)$bytes); if ($bytes < 1024) return $bytes . ' B'; $kb = $bytes / 1024; if ($kb < 1024) return number_format($kb, 2) . ' KB'; $mb = $kb / 1024; if ($mb < 1024) return number_format($mb, 2) . ' MB'; $gb = $mb / 1024; return number_format($gb, 2) . ' GB'; } } /** * Compute storage usage for a user. * Counts: * - AAS custom posts authored by user (post_content + selected meta) * - Attachments authored by user (filesize) * * Returns array with totals and breakdown. */ if (!function_exists('aiaas_storage_v2_calc')) { function aiaas_storage_v2_calc(int $user_id): array { $uid = (int) $user_id; $cache_key = 'aiaas_storage_' . $user_id; $cached = get_transient($cache_key); if (is_array($cached) && isset($cached['total_bytes'])) { return $cached; } $post_types = apply_filters('aiaas_storage_v2_post_types', [ 'aiaas_project', 'aiaas_series', 'aiaas_chapter', 'aiaas_component', 'aiaas_book_version', 'aiaas_template', ]); $meta_keys = apply_filters('aiaas_storage_v2_meta_keys', [ 'aiaas_chapter_notes', 'aiaas_snapshot', 'aiaas_revision_text', 'aiaas_import_raw', 'aiaas_outline_json', ]); $total_text = 0; $total_meta = 0; $total_media = 0; // Posts + postmeta (use WP APIs to avoid direct SQL warnings in Plugin Check) if (!empty($post_types)) { foreach ($post_types as $pt) { $q = new WP_Query([ 'post_type' => $pt, 'post_status' => ['publish','draft','pending','private','future'], 'author' => $user_id, 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', ]); if (!empty($q->posts)) { foreach ($q->posts as $pid) { $p = get_post((int)$pid); if ($p && isset($p->post_content)) { $total_text += strlen((string)$p->post_content); } if (!empty($meta_keys)) { foreach ($meta_keys as $mk) { $mv = get_post_meta((int)$pid, (string)$mk, true); if (is_string($mv)) { $total_meta += strlen($mv); } elseif (is_array($mv)) { // Best-effort: count serialized size $total_meta += strlen(wp_json_encode($mv)); } } } } } wp_reset_postdata(); } } // Media (attachments) that belong to this user. // We cannot rely on post_author alone because some imports create attachments with author=0. // We count attachments if ANY of the following is true: // - attachment post_author is the user // - attachment post_parent is a project/chapter owned by the user // - attachment has AAS upload meta pointing to a user-owned project $owned_parent_ids = []; // Projects + chapters owned by user (preferred) with back-compat to post_author. $owned_parent_ids = array_merge( get_posts([ 'post_type' => ['aiaas_project','aiaas_chapter'], 'post_status' => 'any', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => [ [ 'key' => '_aiaas_owner_user_id', 'value' => (string) $user_id, 'compare' => '=', ], ], ]), get_posts([ 'post_type' => ['aiaas_project','aiaas_chapter'], 'post_status' => 'any', 'author' => $user_id, 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', ]) ); $owned_parent_ids = array_values(array_unique(array_map('intval', array_filter($owned_parent_ids)))); $attachments_by_author = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'author' => $user_id, 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', ]); // Attachments related to this user. // IMPORTANT: we do not rely on post_parent because many imported attachments have post_parent = 0. $owned_project_ids = []; foreach ((array) $owned_parent_ids as $pid) { if (get_post_type((int) $pid) === 'aiaas_project') { $owned_project_ids[] = (int) $pid; } } $owned_project_ids = array_values(array_unique(array_map('intval', $owned_project_ids))); $meta_or = [ // Preferred ownership marker (we backfill it below when possible). [ 'key' => '_aiaas_owner_user_id', 'value' => (string) $uid, 'compare' => '=', ], // Typical AAS marker. [ 'key' => '_aiaas_uploaded_via_aas', 'compare' => 'EXISTS', ], ]; // Attachments linked to projects owned by the user (legacy / missing owner marker). if (!empty($owned_project_ids)) { $meta_or[] = [ 'key' => '_aiaas_uploaded_project_id', 'value' => array_map('strval', $owned_project_ids), 'compare' => 'IN', ]; } $meta_query = ['relation' => 'OR']; foreach ($meta_or as $mq) { $meta_query[] = $mq; } $attachments_related_args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => $meta_query, ]; // IMPORTANT: // Do NOT add post_parent__in to this query. // Many AAS-imported attachments have post_parent = 0, and adding post_parent__in // would accidentally filter out attachments that match the meta_query. $attachments_related = get_posts($attachments_related_args); // Separate query: attachments physically attached (post_parent) to owned content. // Kept separate to avoid filtering out meta-matched attachments whose post_parent is 0. $attachments_by_parent = []; if (!empty($owned_parent_ids)) { $attachments_by_parent = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', 'post_parent__in' => $owned_parent_ids, ]); } // Fallback: Attachments referenced in the user's chapter/project content. // This is critical for DOCX imports where WP attachments may have post_author != user // and post_parent = 0, but are embedded via URLs. $attachments_from_content = []; $owned_content_ids = $owned_parent_ids; if (!empty($owned_content_ids)) { foreach ($owned_content_ids as $pid) { $content = (string) get_post_field('post_content', (int)$pid); if ($content === '') continue; // 1) Fast ID extraction from common WP markup. // Gutenberg / classic image markup often includes wp-image-{ID}. $id_matches = []; if (preg_match_all('~\\bwp-image-(\\d+)\\b~', $content, $id_matches)) { foreach ((array)$id_matches[1] as $id) { $id = (int) $id; if ($id > 0) $attachments_from_content[] = $id; } } // Classic [caption] shortcodes often include attachment_{ID}. $id_matches = []; if (preg_match_all('~\\battachment_(\\d+)\\b~', $content, $id_matches)) { foreach ((array)$id_matches[1] as $id) { $id = (int) $id; if ($id > 0) $attachments_from_content[] = $id; } } // Match absolute uploads URLs. $matches = []; if (preg_match_all('~(?:https?:)?//[^"\'\s>]+/wp-content/uploads/[^"\'\s>]+~i', $content, $matches)) { foreach ((array)$matches[0] as $u) { $u = html_entity_decode((string)$u, ENT_QUOTES); // Strip querystrings/fragments; attachment_url_to_postid is strict. $u = preg_replace('~[?#].*$~', '', $u); $aid = (int) attachment_url_to_postid($u); if ($aid) $attachments_from_content[] = $aid; } } // Match relative uploads URLs. $matches = []; if (preg_match_all('~(/wp-content/uploads/[^"\'\s>]+)~i', $content, $matches)) { foreach ((array)$matches[1] as $rel) { $rel = html_entity_decode((string)$rel, ENT_QUOTES); $rel = preg_replace('~[?#].*$~', '', $rel); $u = rtrim(site_url('/'), '/') . $rel; $aid = (int) attachment_url_to_postid($u); if ($aid) $attachments_from_content[] = $aid; } } } } $attachments = array_values(array_unique(array_map('intval', array_filter(array_merge( (array)$attachments_by_author, (array)$attachments_related, (array)$attachments_by_parent, (array)$attachments_from_content ))))); // Backfill ownership for any attachment we can confidently attribute. // If it has a project id that belongs to the user, stamp _aiaas_owner_user_id. if (!empty($owned_project_ids) && !empty($attachments)) { foreach ($attachments as $aid) { $aid = (int) $aid; if ($aid <= 0) continue; $existing_owner = (int) get_post_meta($aid, '_aiaas_owner_user_id', true); if ($existing_owner > 0) continue; $pid = (int) get_post_meta($aid, '_aiaas_uploaded_project_id', true); if ($pid > 0 && in_array($pid, $owned_project_ids, true)) { update_post_meta($aid, '_aiaas_owner_user_id', (string) $uid); } } } if (!empty($attachments)) { $missing = 0; $missing_samples = []; foreach ($attachments as $aid) { $aid = (int) $aid; if ($aid <= 0) { continue; } $path = get_attached_file($aid); if ($path && file_exists($path)) { $total_media += (int) filesize($path); } else { $missing++; if (count($missing_samples) < 10) { $missing_samples[] = [ 'id' => $aid, 'url' => wp_get_attachment_url($aid), 'path' => $path, ]; } } } } $total_bytes = (int) $total_text + (int) $total_meta + (int) $total_media; $out = [ 'user_id' => (int) $user_id, 'total_bytes' => $total_bytes, 'text_bytes' => (int) $total_text, 'meta_bytes' => (int) $total_meta, 'media_bytes' => (int) $total_media, 'updated_at' => time(), ]; // Cache briefly to avoid repeated heavy scans set_transient($cache_key, $out, 5 * MINUTE_IN_SECONDS); return $out; } } if (!function_exists('aiaas_storage_v2_save')) { function aiaas_storage_v2_save(int $user_id, array $data): void { update_user_meta($user_id, 'aiaas_storage_v2', $data); update_user_meta($user_id, 'aiaas_storage_total_bytes', (int)($data['total_bytes'] ?? 0)); update_user_meta($user_id, 'aiaas_storage_total_human', (string)($data['total_human'] ?? '0 B')); delete_transient('aiaas_storage_' . $user_id); } } if (!function_exists('aiaas_storage_v2_delete_all_user_files')) { function aiaas_storage_v2_delete_all_user_files(int $user_id): array { // Delete ALL AAS posts + media for this user. // IMPORTANT: we cannot rely on post_author alone because some imports create posts/attachments with author=0. // We treat a post/media item as "owned" by this user if: // - post_author is the user OR // - post has _aiaas_owner_user_id = user OR // - attachment is linked to a user-owned project/chapter via post_parent OR AAS meta markers OR is referenced in owned content $post_types = apply_filters('aiaas_storage_v2_post_types', [ 'aiaas_project','aiaas_series','aiaas_chapter','aiaas_component','aiaas_book_version','aiaas_template' ]); $deleted_posts = 0; $deleted_media = 0; // 1) Delete AAS posts owned by user (meta owner marker). $posts_meta = get_posts([ 'post_type' => $post_types, 'post_status' => 'any', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => [ [ 'key' => '_aiaas_owner_user_id', 'value' => (string) $user_id, 'compare' => '=', ], ], ]); // 2) Delete AAS posts authored by user (back-compat). $posts_author = get_posts([ 'post_type' => $post_types, 'post_status' => 'any', 'author' => $user_id, 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ]); $posts = array_values(array_unique(array_map('intval', array_filter(array_merge((array)$posts_meta, (array)$posts_author))))); foreach ($posts as $pid) { wp_delete_post((int)$pid, true); $deleted_posts++; } // 3) Delete attachments (media) that belong to this user. // This mirrors the media ownership logic used in storage calculations. $owned_parent_ids = array_values(array_unique(array_map('intval', array_filter(array_merge( get_posts([ 'post_type' => ['aiaas_project','aiaas_chapter'], 'post_status' => 'any', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => [ [ 'key' => '_aiaas_owner_user_id', 'value' => (string) $user_id, 'compare' => '=', ], ], ]), get_posts([ 'post_type' => ['aiaas_project','aiaas_chapter'], 'post_status' => 'any', 'author' => $user_id, 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', ]) ))))); $owned_project_ids = []; foreach ($owned_parent_ids as $pid) { if (get_post_type((int)$pid) === 'aiaas_project') { $owned_project_ids[] = (int)$pid; } } $owned_project_ids = array_values(array_unique(array_map('intval', $owned_project_ids))); $attachments_by_author = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'author' => $user_id, 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', ]); $meta_or = [ [ 'key' => '_aiaas_owner_user_id', 'value' => (string) $user_id, 'compare' => '=', ], [ 'key' => '_aiaas_uploaded_via_aas', 'compare' => 'EXISTS', ], ]; if (!empty($owned_project_ids)) { $meta_or[] = [ 'key' => '_aiaas_uploaded_project_id', 'value' => array_map('strval', $owned_project_ids), 'compare' => 'IN', ]; } $meta_query = ['relation' => 'OR']; foreach ($meta_or as $mq) { $meta_query[] = $mq; } $attachments_related = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => $meta_query, ]); $attachments_by_parent = []; if (!empty($owned_parent_ids)) { $attachments_by_parent = get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => -1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', 'post_parent__in' => $owned_parent_ids, ]); } // Attachments referenced in owned content (critical fallback for DOCX/PDF imports). $attachments_from_content = []; if (!empty($owned_parent_ids)) { foreach ($owned_parent_ids as $pid) { $content = (string) get_post_field('post_content', (int)$pid); if ($content === '') continue; $id_matches = []; if (preg_match_all('~\\bwp-image-(\\d+)\\b~', $content, $id_matches)) { foreach ((array)$id_matches[1] as $id) { $id = (int) $id; if ($id > 0) $attachments_from_content[] = $id; } } $id_matches = []; if (preg_match_all('~\\battachment_(\\d+)\\b~', $content, $id_matches)) { foreach ((array)$id_matches[1] as $id) { $id = (int) $id; if ($id > 0) $attachments_from_content[] = $id; } } $matches = []; if (preg_match_all('~(?:https?:)?//[^"\'\\s>]+/wp-content/uploads/[^"\'\\s>]+~i', $content, $matches)) { foreach ((array)$matches[0] as $u) { $u = html_entity_decode((string)$u, ENT_QUOTES); $u = preg_replace('~[?#].*$~', '', $u); $aid = (int) attachment_url_to_postid($u); if ($aid) $attachments_from_content[] = $aid; } } $matches = []; if (preg_match_all('~(/wp-content/uploads/[^"\'\\s>]+)~i', $content, $matches)) { foreach ((array)$matches[1] as $rel) { $rel = html_entity_decode((string)$rel, ENT_QUOTES); $rel = preg_replace('~[?#].*$~', '', $rel); $u = rtrim(site_url('/'), '/') . $rel; $aid = (int) attachment_url_to_postid($u); if ($aid) $attachments_from_content[] = $aid; } } } } $attachments = array_values(array_unique(array_map('intval', array_filter(array_merge( (array)$attachments_by_author, (array)$attachments_related, (array)$attachments_by_parent, (array)$attachments_from_content ))))); foreach ($attachments as $aid) { wp_delete_attachment((int)$aid, true); $deleted_media++; } // Clear stored totals delete_user_meta($user_id, 'aiaas_storage_v2'); update_user_meta($user_id, 'aiaas_storage_total_bytes', 0); update_user_meta($user_id, 'aiaas_storage_total_human', '0 B'); return [ 'user_id' => $user_id, 'deleted_posts' => $deleted_posts, 'deleted_media' => $deleted_media, 'done' => true, ]; } } /** Admin menu page */ add_action('admin_menu', function () { if (!aiaas_storage_v2_enabled()) return; add_submenu_page( 'aiaas-settings', 'Storage Tools', 'Storage Tools', 'manage_options', 'aiaas-storage-tools', 'aiaas_storage_v2_render_admin_page' ); }); if (!function_exists('aiaas_storage_v2_render_admin_page')) { function aiaas_storage_v2_render_admin_page() { if (!current_user_can('manage_options')) { wp_die('Permission denied.'); } $users = get_users(['fields' => ['ID','display_name','user_login'], 'number' => 500]); ?>

Storage & Cleanup Tools

Select a user, then calculate storage or delete all their AAS files.

Output

Permission denied.

'; return; } $users = get_users(['fields' => ['ID','display_name','user_login'], 'number' => 500]); ?>
Storage & Cleanup Tools
Select a user, then calculate storage or delete all their AAS files.