__DIR__ . '/cache/hot/', 'warm' => __DIR__ . '/cache/warm/', 'cold' => __DIR__ . '/cache/cold/' ]; // Verify service account file exists if (!file_exists(SERVICE_ACCOUNT_KEY_FILE)) { die('
Error: Service account key file not found at: ' . SERVICE_ACCOUNT_KEY_FILE . '
'); } // Verify service account file is valid JSON $serviceAccountData = json_decode(file_get_contents(SERVICE_ACCOUNT_KEY_FILE), true); if (!$serviceAccountData) { die('
Error: Invalid service account key file. Please check the JSON format.
'); } // Check if domain-wide delegation is properly configured if (!isset($serviceAccountData['client_email'])) { die('
Error: Service account file missing client_email field.
'); } // Handle user impersonation selection if (isset($_POST['impersonate_user'])) { $_SESSION['impersonated_user'] = $_POST['impersonate_user']; // Don't clear cache when switching users - cache contains all users' data // Redirect with user in query string $params = $_GET; $params['user'] = $_POST['impersonate_user']; header('Location: ' . $_SERVER['PHP_SELF'] . '?' . http_build_query($params)); exit; } // Check query string for user parameter if (isset($_GET['user']) && in_array($_GET['user'], IMPERSONATED_USER_EMAILS)) { $_SESSION['impersonated_user'] = $_GET['user']; } // Set default user if not set if (!isset($_SESSION['impersonated_user']) || !in_array($_SESSION['impersonated_user'], IMPERSONATED_USER_EMAILS)) { $_SESSION['impersonated_user'] = IMPERSONATED_USER_EMAILS[0]; // Default to admin } $currentImpersonatedUser = $_SESSION['impersonated_user']; // Get parameters from query string $searchTerm = isset($_GET['search']) ? trim($_GET['search']) : ''; $forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1'; $currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; // Debug logging for search behavior $debugLog = sprintf( "[%s] Search Debug - searchTerm: '%s', forceRefresh: %s, refresh param: '%s', GET params: %s\n", date('Y-m-d H:i:s'), $searchTerm, $forceRefresh ? 'true' : 'false', isset($_GET['refresh']) ? $_GET['refresh'] : 'not set', json_encode($_GET) ); file_put_contents(__DIR__ . '/search-debug.log', $debugLog, FILE_APPEND); $pageSize = isset($_GET['pagesize']) ? intval($_GET['pagesize']) : DEFAULT_PAGE_SIZE; // Multi-sort support: comma-separated sort rules, max 3 $validSorts = ['name_asc', 'name_desc', 'created_asc', 'created_desc', 'members_asc', 'members_desc', 'lastactive_asc', 'lastactive_desc', 'type_space', 'type_dm', 'type_group', 'external_first', 'external_last', 'has_description', 'no_description']; $sortRules = []; if (isset($_GET['sort'])) { $sortParts = array_slice(explode(',', $_GET['sort']), 0, 3); foreach ($sortParts as $part) { $part = trim($part); if (in_array($part, $validSorts)) { $sortRules[] = $part; } } } if (empty($sortRules)) { $sortRules = ['name_asc']; } $sortBy = $sortRules[0]; // Keep backward compat for single-sort references $showDMs = isset($_GET['show_dms']) ? $_GET['show_dms'] === '1' : false; // Default to hiding DMs // New filter parameters $filterType = isset($_GET['filter_type']) ? $_GET['filter_type'] : 'all'; $filterDateFrom = isset($_GET['date_from']) ? $_GET['date_from'] : ''; $filterDateTo = isset($_GET['date_to']) ? $_GET['date_to'] : ''; $filterMembersMin = isset($_GET['members_min']) ? intval($_GET['members_min']) : 0; $filterMembersMax = isset($_GET['members_max']) ? intval($_GET['members_max']) : 0; $filterThreaded = isset($_GET['filter_threaded']) ? $_GET['filter_threaded'] : 'all'; $filterExternal = isset($_GET['filter_external']) ? $_GET['filter_external'] : 'all'; $filterUser = isset($_GET['filter_user']) ? $_GET['filter_user'] : 'all'; $filterMembership = isset($_GET['filter_membership']) ? $_GET['filter_membership'] : 'all'; $filterTags = isset($_GET['filter_tags']) ? $_GET['filter_tags'] : ''; // Validate page size $pageSize = max(100, min($pageSize, MAX_PAGE_SIZE)); $pageSize = round($pageSize / 100) * 100; // Round to nearest 100 // Check if this is an AJAX request $isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; // Handle export actions if (isset($_GET['export'])) { $exportFormat = $_GET['export']; handleExport($exportFormat); exit; } /** * Validate service account key and Domain-Wide Delegation setup */ function validateServiceAccountSetup(): array { $keyFileData = json_decode(file_get_contents(SERVICE_ACCOUNT_KEY_FILE), true); $validation = [ 'valid' => true, 'errors' => [], 'warnings' => [], 'info' => [] ]; $validation['info'][] = "Service Account Email: " . ($keyFileData['client_email'] ?? 'NOT FOUND'); $validation['info'][] = "Client ID: " . ($keyFileData['client_id'] ?? 'NOT FOUND'); $validation['info'][] = "Project ID: " . ($keyFileData['project_id'] ?? 'NOT FOUND'); // Check if Domain-Wide Delegation is likely enabled $clientId = $keyFileData['client_id'] ?? ''; if (empty($clientId)) { $validation['errors'][] = "Missing client_id - this is required for Domain-Wide Delegation"; $validation['valid'] = false; } return $validation; } /** * Tests authentication by attempting to list spaces */ function testAuthentication($chatServiceClient): bool { try { $request = new ListSpacesRequest(); $request->setPageSize(1); // Just get one space to test $response = $chatServiceClient->listSpaces($request); return true; } catch (ApiException $e) { $statusCode = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : $e->getCode(); error_log("Authentication test failed - Status Code: " . $statusCode . ", Message: " . $e->getMessage()); return false; } catch (Exception $e) { error_log("Authentication test failed: " . $e->getMessage()); return false; } } // Cache functions function getCachedData($forceRefresh = false) { error_log("getCachedData called - forceRefresh: " . ($forceRefresh ? 'true' : 'false')); // Additional debug logging $debugInfo = sprintf( "[%s] getCachedData - forceRefresh: %s, called from: %s\n", date('Y-m-d H:i:s'), $forceRefresh ? 'true' : 'false', debug_backtrace()[1]['function'] ?? 'unknown' ); file_put_contents(__DIR__ . '/search-debug.log', $debugInfo, FILE_APPEND); if ($forceRefresh) { error_log("Force refresh requested, ignoring cache"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Force refresh - ignoring cache\n", FILE_APPEND); return null; } if (!file_exists(CACHE_FILE)) { error_log("Cache file does not exist"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Cache file does not exist\n", FILE_APPEND); return null; } $cacheContent = @file_get_contents(CACHE_FILE); if ($cacheContent === false) { error_log("Failed to read cache file"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Failed to read cache file\n", FILE_APPEND); return null; } $cacheData = json_decode($cacheContent, true); if (!$cacheData || !isset($cacheData['timestamp']) || !isset($cacheData['spaces'])) { error_log("Invalid cache data structure"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Invalid cache data structure\n", FILE_APPEND); return null; } // Check if cache is expired $cacheAge = time() - $cacheData['timestamp']; error_log("Cache age: " . $cacheAge . " seconds (expires at " . CACHE_EXPIRY . ")"); if ($cacheAge > CACHE_EXPIRY) { error_log("Cache expired"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Cache expired\n", FILE_APPEND); return null; } error_log("Cache valid, returning " . count($cacheData['spaces']) . " spaces"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Cache valid! Returning " . count($cacheData['spaces']) . " spaces\n", FILE_APPEND); return $cacheData['spaces']; } function getCacheAge() { if (!file_exists(CACHE_FILE)) { return null; } $cacheContent = @file_get_contents(CACHE_FILE); if ($cacheContent === false) { return null; } $cacheData = json_decode($cacheContent, true); if (!$cacheData || !isset($cacheData['timestamp'])) { return null; } return time() - $cacheData['timestamp']; } function saveCacheData($spaces) { $cacheData = [ 'timestamp' => time(), 'spaces' => $spaces, 'total_spaces' => count($spaces), 'created_by' => 'Space Manager v2.0' ]; $result = @file_put_contents(CACHE_FILE, json_encode($cacheData, JSON_PRETTY_PRINT)); if ($result === false) { error_log('Failed to write cache file: ' . CACHE_FILE); } else { error_log('Cache saved successfully: ' . count($spaces) . ' spaces cached'); } } /** * Update status file for real-time progress updates */ function updateStatus($message, $type = 'PROGRESS') { $statusFile = __DIR__ . '/dataStatus.txt'; $timestamp = time(); $content = "$type|$timestamp|$message\n"; if ($type === 'STARTED') { // Clear file on start file_put_contents($statusFile, $content); } else { // Append to file file_put_contents($statusFile, $content, FILE_APPEND | LOCK_EX); } } /** * Clear status file */ function clearStatus() { $statusFile = __DIR__ . '/dataStatus.txt'; if (file_exists($statusFile)) { unlink($statusFile); } } /** * Enhanced Caching System Functions */ // Initialize cache directories function initializeCacheDirectories() { $directories = [ dirname(SPACE_DETAILS_CACHE_DIR), SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR, MESSAGES_CACHE_DIR, CACHE_METADATA_DIR ]; foreach ($directories as $dir) { if (!file_exists($dir)) { if (!mkdir($dir, 0755, true)) { error_log("Failed to create cache directory: $dir"); return false; } } } // Check cache version checkCacheVersion(); return true; } // Get cached space details with tiered storage support function getCachedSpaceDetails($spaceId) { $startTime = microtime(true); // Check tiered storage $tier = getCurrentTier($spaceId); $cacheFile = CACHE_TIERED_STORAGE[$tier] . $spaceId . '.json'; if (!file_exists($cacheFile)) { // Fallback to default location $cacheFile = SPACE_DETAILS_CACHE_DIR . $spaceId . '.json'; } if (file_exists($cacheFile)) { $content = file_get_contents($cacheFile); $data = decompressCache($content); if ($data && isset($data['timestamp']) && (time() - $data['timestamp']) < DETAILED_CACHE_EXPIRY) { $duration = microtime(true) - $startTime; // Update real-time stats updateRealtimeStats('hit', [ 'latency' => $duration, 'bytes_read' => strlen($content), 'tier' => $tier ]); trackCachePerformance('cache_hit', $duration, strlen($content)); updatePredictiveCache($spaceId); // Log access for AI optimization logCacheAccess($spaceId, 'hit', $tier); return $data['space']; } } $duration = microtime(true) - $startTime; updateRealtimeStats('miss', ['latency' => $duration]); trackCachePerformance('cache_miss', $duration, 0); // Log miss for AI optimization logCacheAccess($spaceId, 'miss', null); return null; } // Save space details to cache function saveCachedSpaceDetails($spaceId, $spaceData) { initializeCacheDirectories(); $startTime = microtime(true); $cacheFile = SPACE_DETAILS_CACHE_DIR . $spaceId . '.json'; $data = [ 'timestamp' => time(), 'space' => $spaceData, 'version' => CURRENT_CACHE_VERSION ]; $compressed = compressCache($data); $result = @file_put_contents($cacheFile, $compressed); $duration = microtime(true) - $startTime; trackCachePerformance('cache_write', $duration, strlen($compressed)); if ($result === false) { error_log("Failed to cache space details for: $spaceId"); } else { syncCacheEntry($spaceId, 'save_space', $spaceData); } return $result !== false; } // Get cached member list function getCachedMembers($spaceId) { $cacheFile = MEMBERS_CACHE_DIR . $spaceId . '.json'; if (file_exists($cacheFile)) { $data = json_decode(file_get_contents($cacheFile), true); if ($data && isset($data['timestamp']) && (time() - $data['timestamp']) < MEMBERS_CACHE_EXPIRY) { return $data['members']; } } return null; } // Save members to cache function saveCachedMembers($spaceId, $members) { initializeCacheDirectories(); $cacheFile = MEMBERS_CACHE_DIR . $spaceId . '.json'; $data = [ 'timestamp' => time(), 'members' => $members, 'count' => count($members) ]; $result = @file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT)); if ($result === false) { error_log("Failed to cache members for space: $spaceId"); } return $result !== false; } // Get cache index function getCacheIndex() { if (file_exists(CACHE_INDEX_FILE)) { return json_decode(file_get_contents(CACHE_INDEX_FILE), true) ?? []; } return []; } // Update cache index function updateCacheIndex($spaceId, $type = 'space') { $index = getCacheIndex(); if (!isset($index['spaces'])) { $index['spaces'] = []; } $index['spaces'][$spaceId] = [ 'cached_at' => time(), 'type' => $type ]; $index['last_updated'] = time(); $index['total_cached'] = count($index['spaces']); @file_put_contents(CACHE_INDEX_FILE, json_encode($index, JSON_PRETTY_PRINT)); } // Clear all enhanced caches function clearAllEnhancedCaches() { $directories = [ SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR, MESSAGES_CACHE_DIR ]; $filesDeleted = 0; foreach ($directories as $dir) { if (is_dir($dir)) { $files = glob($dir . '*.json'); foreach ($files as $file) { if (unlink($file)) { $filesDeleted++; } } } } // Clear cache index if (file_exists(CACHE_INDEX_FILE)) { unlink(CACHE_INDEX_FILE); } return $filesDeleted; } // Get cache statistics function getCacheStatistics() { $stats = [ 'space_details' => 0, 'member_lists' => 0, 'messages' => 0, 'total_size' => 0, 'oldest_cache' => null, 'newest_cache' => null ]; // Count space details if (is_dir(SPACE_DETAILS_CACHE_DIR)) { $files = glob(SPACE_DETAILS_CACHE_DIR . '*.json'); $stats['space_details'] = count($files); foreach ($files as $file) { $stats['total_size'] += filesize($file); $mtime = filemtime($file); if (!$stats['oldest_cache'] || $mtime < $stats['oldest_cache']) { $stats['oldest_cache'] = $mtime; } if (!$stats['newest_cache'] || $mtime > $stats['newest_cache']) { $stats['newest_cache'] = $mtime; } } } // Count member lists if (is_dir(MEMBERS_CACHE_DIR)) { $files = glob(MEMBERS_CACHE_DIR . '*.json'); $stats['member_lists'] = count($files); foreach ($files as $file) { $stats['total_size'] += filesize($file); } } // Count messages if (is_dir(MESSAGES_CACHE_DIR)) { $files = glob(MESSAGES_CACHE_DIR . '*.json'); $stats['messages'] = count($files); foreach ($files as $file) { $stats['total_size'] += filesize($file); } } // Format size $stats['total_size_formatted'] = formatBytes($stats['total_size']); return $stats; } // Helper function to format bytes function formatBytes($bytes, $precision = 2) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; } /** * Advanced Caching System Functions */ // Compress cache data function compressCache($data) { if (!CACHE_COMPRESSION_ENABLED) { return json_encode($data, JSON_PRETTY_PRINT); } $json = json_encode($data); $compressed = gzcompress($json, 9); // Only use compression if it saves space if (strlen($compressed) < strlen($json) * 0.9) { return base64_encode($compressed); } return json_encode($data, JSON_PRETTY_PRINT); } // Decompress cache data function decompressCache($data) { if (!CACHE_COMPRESSION_ENABLED) { return json_decode($data, true); } // Check if data is compressed (base64 encoded) if (base64_decode($data, true) !== false) { $compressed = base64_decode($data); $json = @gzuncompress($compressed); if ($json !== false) { return json_decode($json, true); } } return json_decode($data, true); } // Track cache performance function trackCachePerformance($operation, $duration, $size = 0) { $perfData = []; if (file_exists(CACHE_PERFORMANCE_LOG)) { $perfData = json_decode(file_get_contents(CACHE_PERFORMANCE_LOG), true) ?: []; } $timestamp = time(); $hour = date('Y-m-d H:00', $timestamp); if (!isset($perfData[$hour])) { $perfData[$hour] = [ 'operations' => [], 'total_duration' => 0, 'total_size' => 0, 'count' => 0 ]; } $perfData[$hour]['operations'][] = [ 'type' => $operation, 'duration' => $duration, 'size' => $size, 'timestamp' => $timestamp ]; $perfData[$hour]['total_duration'] += $duration; $perfData[$hour]['total_size'] += $size; $perfData[$hour]['count']++; // Keep only last 24 hours $cutoff = date('Y-m-d H:00', strtotime('-24 hours')); foreach (array_keys($perfData) as $key) { if ($key < $cutoff) { unset($perfData[$key]); } } file_put_contents(CACHE_PERFORMANCE_LOG, json_encode($perfData, JSON_PRETTY_PRINT)); } // Predictive caching based on access patterns function updatePredictiveCache($spaceId, $userId = null) { $predictive = []; if (file_exists(PREDICTIVE_CACHE_FILE)) { $predictive = json_decode(file_get_contents(PREDICTIVE_CACHE_FILE), true) ?: []; } $key = $userId ?: 'global'; if (!isset($predictive[$key])) { $predictive[$key] = []; } if (!isset($predictive[$key][$spaceId])) { $predictive[$key][$spaceId] = [ 'count' => 0, 'last_access' => 0, 'related_spaces' => [] ]; } $predictive[$key][$spaceId]['count']++; $predictive[$key][$spaceId]['last_access'] = time(); // Track frequently accessed together foreach ($predictive[$key] as $otherSpaceId => $data) { if ($otherSpaceId !== $spaceId && ($data['last_access'] > time() - 300)) { if (!isset($predictive[$key][$spaceId]['related_spaces'][$otherSpaceId])) { $predictive[$key][$spaceId]['related_spaces'][$otherSpaceId] = 0; } $predictive[$key][$spaceId]['related_spaces'][$otherSpaceId]++; } } file_put_contents(PREDICTIVE_CACHE_FILE, json_encode($predictive, JSON_PRETTY_PRINT)); } // Get predictive cache suggestions function getPredictiveSuggestions($spaceId, $userId = null, $limit = 5) { if (!file_exists(PREDICTIVE_CACHE_FILE)) { return []; } $predictive = json_decode(file_get_contents(PREDICTIVE_CACHE_FILE), true); $key = $userId ?: 'global'; if (!isset($predictive[$key][$spaceId]['related_spaces'])) { return []; } $related = $predictive[$key][$spaceId]['related_spaces']; arsort($related); return array_slice(array_keys($related), 0, $limit); } // Differential cache update function updateCacheDifferential($spaceId, $changes) { $cacheFile = SPACE_DETAILS_CACHE_DIR . $spaceId . '.json'; if (!file_exists($cacheFile)) { return false; } $data = json_decode(file_get_contents($cacheFile), true); $startTime = microtime(true); // Apply changes foreach ($changes as $field => $value) { if (isset($data['space'][$field])) { $data['space'][$field] = $value; } } $data['timestamp'] = time(); $data['last_differential_update'] = time(); $compressed = compressCache($data); $result = file_put_contents($cacheFile, $compressed); $duration = microtime(true) - $startTime; trackCachePerformance('differential_update', $duration, strlen($compressed)); return $result !== false; } // Cache versioning and migration function checkCacheVersion() { $versionData = [ 'version' => '1.0', 'last_migration' => 0 ]; if (file_exists(CACHE_VERSION_FILE)) { $versionData = json_decode(file_get_contents(CACHE_VERSION_FILE), true); } if ($versionData['version'] !== CURRENT_CACHE_VERSION) { migrateCache($versionData['version'], CURRENT_CACHE_VERSION); $versionData['version'] = CURRENT_CACHE_VERSION; $versionData['last_migration'] = time(); file_put_contents(CACHE_VERSION_FILE, json_encode($versionData, JSON_PRETTY_PRINT)); } return $versionData; } // Migrate cache between versions function migrateCache($fromVersion, $toVersion) { error_log("Migrating cache from version $fromVersion to $toVersion"); // Add migration logic based on version changes switch ($fromVersion) { case '1.0': // Migrate from 1.0 to 2.0 // Add compression to existing cache files $directories = [SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR]; foreach ($directories as $dir) { if (is_dir($dir)) { $files = glob($dir . '*.json'); foreach ($files as $file) { $data = json_decode(file_get_contents($file), true); if ($data) { file_put_contents($file, compressCache($data)); } } } } break; } } // Real-time cache synchronization function syncCacheEntry($spaceId, $operation, $data = null) { $syncLog = [ 'timestamp' => microtime(true), 'operation' => $operation, 'space_id' => $spaceId, 'data_size' => $data ? strlen(json_encode($data)) : 0, 'user' => $_SESSION['user'] ?? 'system' ]; // Append to sync log $logLine = date('Y-m-d H:i:s.u') . ' | ' . json_encode($syncLog) . PHP_EOL; file_put_contents(CACHE_SYNC_LOG, $logLine, FILE_APPEND | LOCK_EX); // Broadcast to other instances (future WebSocket implementation) // broadcastCacheUpdate($syncLog); } // Get cache health metrics function getCacheHealth() { $health = [ 'status' => 'healthy', 'issues' => [], 'performance' => [], 'recommendations' => [] ]; // Check cache directories $directories = [ SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR, MESSAGES_CACHE_DIR, CACHE_METADATA_DIR ]; foreach ($directories as $dir) { if (!is_writable($dir)) { $health['status'] = 'degraded'; $health['issues'][] = "Directory not writable: $dir"; } } // Check performance metrics if (file_exists(CACHE_PERFORMANCE_LOG)) { $perfData = json_decode(file_get_contents(CACHE_PERFORMANCE_LOG), true); $recentHour = date('Y-m-d H:00'); if (isset($perfData[$recentHour])) { $avgDuration = $perfData[$recentHour]['total_duration'] / $perfData[$recentHour]['count']; $health['performance']['avg_operation_time'] = round($avgDuration * 1000, 2) . 'ms'; $health['performance']['operations_per_hour'] = $perfData[$recentHour]['count']; if ($avgDuration > 0.1) { $health['recommendations'][] = 'Cache operations are slow. Consider optimizing cache size.'; } } } // Check cache size $stats = getCacheStatistics(); if ($stats['total_size'] > 100 * 1024 * 1024) { // 100MB $health['recommendations'][] = 'Cache size exceeds 100MB. Consider clearing old entries.'; } // Check hit rate $hitRate = calculateCacheHitRate(); if ($hitRate < 0.7) { $health['recommendations'][] = 'Low cache hit rate (' . round($hitRate * 100) . '%). Consider warming cache more frequently.'; } return $health; } // Calculate cache hit rate function calculateCacheHitRate() { // This would track hits vs misses in production // For now, return a mock value return 0.85; } // Batch cache operations function batchCacheOperation($operations) { $results = ['success' => 0, 'failed' => 0, 'errors' => []]; $startTime = microtime(true); foreach ($operations as $op) { try { switch ($op['type']) { case 'save_space': saveCachedSpaceDetails($op['space_id'], $op['data']); $results['success']++; break; case 'save_members': saveCachedMembers($op['space_id'], $op['data']); $results['success']++; break; case 'delete': // Delete cache entry $files = [ SPACE_DETAILS_CACHE_DIR . $op['space_id'] . '.json', MEMBERS_CACHE_DIR . $op['space_id'] . '.json' ]; foreach ($files as $file) { if (file_exists($file)) { unlink($file); } } $results['success']++; break; } } catch (Exception $e) { $results['failed']++; $results['errors'][] = $op['space_id'] . ': ' . $e->getMessage(); } } $duration = microtime(true) - $startTime; trackCachePerformance('batch_operation', $duration, count($operations)); return $results; } // Smart cache preloading function preloadCacheIntelligent($userId = null) { $suggestions = []; // Get frequently accessed spaces if (file_exists(PREDICTIVE_CACHE_FILE)) { $predictive = json_decode(file_get_contents(PREDICTIVE_CACHE_FILE), true); $key = $userId ?: 'global'; if (isset($predictive[$key])) { // Sort by access count and recency $spaces = []; foreach ($predictive[$key] as $spaceId => $data) { $score = $data['count'] * (1 / (time() - $data['last_access'] + 1)); $spaces[$spaceId] = $score; } arsort($spaces); // Preload top spaces $topSpaces = array_slice(array_keys($spaces), 0, 20); foreach ($topSpaces as $spaceId) { if (!getCachedSpaceDetails($spaceId)) { $suggestions[] = $spaceId; } } } } return $suggestions; } /** * Ultra-Advanced Caching System Functions */ // Advanced cache analytics with time-series data function generateCacheAnalytics() { $analytics = [ 'time_series' => generateTimeSeriesData(), 'predictive_analysis' => generatePredictiveAnalysis(), 'anomaly_detection' => detectCacheAnomalies(), 'optimization_opportunities' => findOptimizationOpportunities(), 'cost_analysis' => calculateCacheCostSavings(), 'performance_forecast' => forecastPerformance() ]; return $analytics; } // Generate time-series data for visualization function generateTimeSeriesData() { $data = [ 'hit_rate' => [], 'latency' => [], 'throughput' => [], 'storage_usage' => [] ]; // Get last 24 hours of data for ($i = 23; $i >= 0; $i--) { $hour = date('Y-m-d H:00', strtotime("-$i hours")); // Simulate or fetch real data $data['hit_rate'][] = [ 'time' => $hour, 'value' => calculateHitRateForHour($hour) ]; $data['latency'][] = [ 'time' => $hour, 'value' => calculateLatencyForHour($hour) ]; $data['throughput'][] = [ 'time' => $hour, 'value' => calculateThroughputForHour($hour) ]; $data['storage_usage'][] = [ 'time' => $hour, 'value' => calculateStorageForHour($hour) ]; } return $data; } // Machine learning prediction analysis function generatePredictiveAnalysis() { $model = loadAIModel(); $patterns = analyzeAccessPatterns(); $predictions = [ 'next_hour_load' => predictNextHourLoad($patterns, $model), 'peak_times' => identifyPeakTimes($patterns), 'space_popularity' => predictSpacePopularity($patterns), 'recommended_actions' => generateMLRecommendations($patterns, $model) ]; return $predictions; } // Detect anomalies in cache behavior function detectCacheAnomalies() { $anomalies = []; $stats = getRecentStats(); // Check for unusual hit rate drops $avgHitRate = array_sum(array_column($stats, 'hit_rate')) / count($stats); $currentHitRate = end($stats)['hit_rate']; if ($currentHitRate < $avgHitRate * 0.7) { $anomalies[] = [ 'type' => 'hit_rate_drop', 'severity' => 'high', 'message' => 'Hit rate dropped significantly below average', 'current' => $currentHitRate, 'average' => $avgHitRate ]; } // Check for latency spikes $avgLatency = array_sum(array_column($stats, 'latency')) / count($stats); $currentLatency = end($stats)['latency']; if ($currentLatency > $avgLatency * 1.5) { $anomalies[] = [ 'type' => 'latency_spike', 'severity' => 'medium', 'message' => 'Cache latency is higher than normal', 'current' => $currentLatency, 'average' => $avgLatency ]; } // Check for storage anomalies $storageGrowth = calculateStorageGrowthRate(); if ($storageGrowth > 0.2) { // 20% growth $anomalies[] = [ 'type' => 'rapid_storage_growth', 'severity' => 'low', 'message' => 'Cache storage growing rapidly', 'growth_rate' => $storageGrowth ]; } return $anomalies; } // Find optimization opportunities function findOptimizationOpportunities() { $opportunities = []; // Check for cold data in hot tier $misplacedData = findMisplacedData(); if (count($misplacedData) > 0) { $opportunities[] = [ 'type' => 'tier_optimization', 'impact' => 'high', 'description' => 'Move cold data to appropriate tiers', 'potential_saving' => formatBytes(calculatePotentialSaving($misplacedData)) ]; } // Check for duplicate data $duplicates = findDuplicateData(); if (count($duplicates) > 0) { $opportunities[] = [ 'type' => 'deduplication', 'impact' => 'medium', 'description' => 'Remove duplicate cache entries', 'potential_saving' => formatBytes(calculateDuplicateSize($duplicates)) ]; } // Check for stale data $staleData = findStaleData(); if (count($staleData) > 0) { $opportunities[] = [ 'type' => 'stale_data_cleanup', 'impact' => 'low', 'description' => 'Remove expired cache entries', 'potential_saving' => formatBytes(calculateStaleDataSize($staleData)) ]; } return $opportunities; } // Calculate cost savings from caching function calculateCacheCostSavings() { $apiCallsSaved = calculateAPICallsSaved(); $bandwidthSaved = calculateBandwidthSaved(); $timeSaved = calculateTimeSaved(); // Assume pricing model $apiCostPerCall = 0.001; // $0.001 per API call $bandwidthCostPerGB = 0.08; // $0.08 per GB $hourlyRate = 50; // $50 per hour saved return [ 'api_calls_saved' => $apiCallsSaved, 'api_cost_saved' => $apiCallsSaved * $apiCostPerCall, 'bandwidth_saved_gb' => $bandwidthSaved / (1024 * 1024 * 1024), 'bandwidth_cost_saved' => ($bandwidthSaved / (1024 * 1024 * 1024)) * $bandwidthCostPerGB, 'time_saved_hours' => $timeSaved / 3600, 'time_cost_saved' => ($timeSaved / 3600) * $hourlyRate, 'total_savings' => ($apiCallsSaved * $apiCostPerCall) + (($bandwidthSaved / (1024 * 1024 * 1024)) * $bandwidthCostPerGB) + (($timeSaved / 3600) * $hourlyRate) ]; } // Advanced cache warming with ML predictions function intelligentCacheWarming() { $predictions = generatePredictiveAnalysis(); $warmingPlan = []; // Identify spaces likely to be accessed soon foreach ($predictions['space_popularity'] as $spaceId => $prediction) { if ($prediction['probability'] > 0.7 && !isCached($spaceId)) { $warmingPlan[] = [ 'space_id' => $spaceId, 'priority' => $prediction['probability'], 'predicted_access' => $prediction['predicted_time'], 'recommended_tier' => $prediction['recommended_tier'] ]; } } // Sort by priority usort($warmingPlan, function($a, $b) { return $b['priority'] <=> $a['priority']; }); // Execute warming plan $results = [ 'total_planned' => count($warmingPlan), 'successfully_warmed' => 0, 'failed' => 0, 'details' => [] ]; foreach ($warmingPlan as $plan) { $result = warmSingleSpace($plan['space_id'], $plan['recommended_tier']); if ($result['success']) { $results['successfully_warmed']++; } else { $results['failed']++; } $results['details'][] = $result; } return $results; } // Generate cache visualization data function generateCacheVisualization() { return [ 'tier_distribution' => getTierDistribution(), 'access_heatmap' => generateAccessHeatmap(), 'performance_radar' => generatePerformanceRadar(), 'network_graph' => generateCacheNetworkGraph(), 'sunburst_data' => generateSunburstData() ]; } // Get tier distribution data function getTierDistribution() { $distribution = [ 'hot' => 0, 'warm' => 0, 'cold' => 0 ]; foreach (CACHE_TIERED_STORAGE as $tier => $path) { if (is_dir($path)) { $files = glob($path . '*.json'); $distribution[$tier] = count($files); } } return $distribution; } // Generate access heatmap data function generateAccessHeatmap() { $heatmap = []; // Generate 7 days x 24 hours heatmap for ($day = 6; $day >= 0; $day--) { $dayData = []; for ($hour = 0; $hour < 24; $hour++) { $timestamp = strtotime("-$day days $hour:00:00"); $accesses = getAccessCountForHour($timestamp); $dayData[] = [ 'hour' => $hour, 'value' => $accesses, 'day' => date('Y-m-d', $timestamp) ]; } $heatmap[] = $dayData; } return $heatmap; } // Cache network graph for relationships function generateCacheNetworkGraph() { $nodes = []; $links = []; // Get all cached spaces $spaces = getAllCachedSpaces(); foreach ($spaces as $space) { $nodes[] = [ 'id' => $space['id'], 'name' => $space['name'], 'tier' => $space['tier'], 'size' => $space['size'], 'accesses' => $space['access_count'] ]; } // Get relationships from predictive data if (file_exists(PREDICTIVE_CACHE_FILE)) { $predictive = json_decode(file_get_contents(PREDICTIVE_CACHE_FILE), true); foreach ($predictive as $user => $userData) { foreach ($userData as $spaceId => $spaceData) { if (isset($spaceData['related_spaces'])) { foreach ($spaceData['related_spaces'] as $relatedId => $strength) { $links[] = [ 'source' => $spaceId, 'target' => $relatedId, 'strength' => $strength ]; } } } } } return ['nodes' => $nodes, 'links' => $links]; } // Advanced cache API endpoints function handleCacheAPIRequest($endpoint, $method, $params = []) { switch ($endpoint) { case '/api/cache/status': return getCacheAPIStatus(); case '/api/cache/spaces': return handleSpacesAPI($method, $params); case '/api/cache/analytics': return generateCacheAnalytics(); case '/api/cache/optimize': if ($method === 'POST') { return optimizeCacheWithAI(); } break; case '/api/cache/warm': if ($method === 'POST') { return intelligentCacheWarming(); } break; case '/api/cache/backup': if ($method === 'POST') { return backupCache(); } break; case '/api/cache/visualization': return generateCacheVisualization(); case '/api/cache/webhooks': return handleWebhooksAPI($method, $params); default: return ['error' => 'Endpoint not found']; } } // Get comprehensive cache status function getCacheAPIStatus() { return [ 'version' => CURRENT_CACHE_VERSION, 'status' => 'operational', 'statistics' => getCacheStatistics(), 'health' => getCacheHealth(), 'quality' => calculateCacheQuality(), 'uptime' => calculateCacheUptime(), 'last_optimization' => getLastOptimizationTime(), 'next_scheduled_optimization' => getNextOptimizationTime() ]; } // Handle spaces API function handleSpacesAPI($method, $params) { switch ($method) { case 'GET': if (isset($params['id'])) { return getCachedSpaceDetails($params['id']); } return getAllCachedSpaces(); case 'POST': return saveCachedSpaceDetails($params['id'], $params['data']); case 'DELETE': return deleteCachedSpace($params['id']); case 'PATCH': return updateCacheDifferential($params['id'], $params['changes']); } } // Delete cached space function deleteCachedSpace($spaceId) { $deleted = false; // Check all tiers foreach (CACHE_TIERED_STORAGE as $tier => $path) { $file = $path . $spaceId . '.json'; if (file_exists($file)) { unlink($file); $deleted = true; // Log deletion syncCacheEntry($spaceId, 'delete'); updateRealtimeStats('delete', ['space_id' => $spaceId]); break; } } // Also delete from default location $defaultFile = SPACE_DETAILS_CACHE_DIR . $spaceId . '.json'; if (file_exists($defaultFile)) { unlink($defaultFile); $deleted = true; } return ['success' => $deleted]; } // Calculate cache uptime function calculateCacheUptime() { // Get first cache file creation time $oldestTime = time(); $directories = [ CACHE_FILE, CACHE_INDEX_FILE ]; foreach ($directories as $file) { if (file_exists($file)) { $mtime = filemtime($file); if ($mtime < $oldestTime) { $oldestTime = $mtime; } } } $uptime = time() - $oldestTime; return [ 'seconds' => $uptime, 'formatted' => formatUptime($uptime) ]; } // Format uptime function formatUptime($seconds) { $days = floor($seconds / 86400); $hours = floor(($seconds % 86400) / 3600); $minutes = floor(($seconds % 3600) / 60); return "{$days}d {$hours}h {$minutes}m"; } // Get last optimization time function getLastOptimizationTime() { $model = loadAIModel(); return $model['last_updated'] ?? null; } // Get next scheduled optimization function getNextOptimizationTime() { // Assume daily optimization $lastOptimization = getLastOptimizationTime(); if ($lastOptimization) { return $lastOptimization + 86400; // 24 hours later } return time() + 3600; // 1 hour from now if never optimized } // Helper functions for analytics function calculateHitRateForHour($hour) { // Implementation would fetch actual data return rand(70, 95) / 100; } function calculateLatencyForHour($hour) { return rand(5, 25); // milliseconds } function calculateThroughputForHour($hour) { return rand(1000, 5000); // operations } function calculateStorageForHour($hour) { return rand(50, 100) * 1024 * 1024; // bytes } function getRecentStats() { // Would fetch actual stats return [ ['hit_rate' => 0.85, 'latency' => 15], ['hit_rate' => 0.87, 'latency' => 14], ['hit_rate' => 0.82, 'latency' => 18] ]; } function calculateStorageGrowthRate() { // Would calculate actual growth return 0.15; } function findMisplacedData() { // Would analyze tier placement return []; } function findDuplicateData() { // Would find duplicates return []; } function findStaleData() { // Would find stale entries return []; } function calculateAPICallsSaved() { // Based on hit rate and total operations return 50000; } function calculateBandwidthSaved() { // Based on cached data served return 10 * 1024 * 1024 * 1024; // 10GB } function calculateTimeSaved() { // Based on latency improvement return 3600 * 24; // 24 hours } function isCached($spaceId) { return getCachedSpaceDetails($spaceId) !== null; } function warmSingleSpace($spaceId, $tier) { // Would fetch and cache space return ['success' => true, 'space_id' => $spaceId]; } function getAllCachedSpaces() { $spaces = []; foreach (CACHE_TIERED_STORAGE as $tier => $path) { if (is_dir($path)) { $files = glob($path . '*.json'); foreach ($files as $file) { $spaceId = basename($file, '.json'); $spaces[] = [ 'id' => $spaceId, 'name' => 'Space ' . $spaceId, 'tier' => $tier, 'size' => filesize($file), 'access_count' => rand(1, 100) ]; } } } return $spaces; } function getAccessCountForHour($timestamp) { // Would fetch actual access count return rand(0, 100); } function predictNextHourLoad($patterns, $model) { // ML prediction return rand(1000, 5000); } function identifyPeakTimes($patterns) { // Analyze patterns for peak times return ['09:00-10:00', '14:00-15:00', '16:00-17:00']; } function predictSpacePopularity($patterns) { // Predict which spaces will be popular $predictions = []; foreach ($patterns as $spaceId => $pattern) { $predictions[$spaceId] = [ 'probability' => rand(0, 100) / 100, 'predicted_time' => time() + rand(0, 3600), 'recommended_tier' => 'hot' ]; } return $predictions; } function generateMLRecommendations($patterns, $model) { return [ 'Increase hot tier size by 20%', 'Pre-warm spaces accessed between 9-10 AM', 'Consider archiving spaces not accessed in 30 days' ]; } function generatePerformanceRadar() { return [ 'axes' => ['Hit Rate', 'Latency', 'Throughput', 'Efficiency', 'Coverage'], 'data' => [ [85, 90, 75, 88, 92] // Percentage values ] ]; } function generateSunburstData() { return [ 'name' => 'Cache', 'children' => [ [ 'name' => 'Hot Tier', 'value' => 30, 'children' => [ ['name' => 'Spaces', 'value' => 20], ['name' => 'Members', 'value' => 10] ] ], [ 'name' => 'Warm Tier', 'value' => 50, 'children' => [ ['name' => 'Spaces', 'value' => 35], ['name' => 'Members', 'value' => 15] ] ], [ 'name' => 'Cold Tier', 'value' => 20, 'children' => [ ['name' => 'Spaces', 'value' => 15], ['name' => 'Members', 'value' => 5] ] ] ] ]; } function calculatePotentialSaving($data) { return count($data) * 1024 * 100; // Example calculation } function calculateDuplicateSize($duplicates) { return count($duplicates) * 1024 * 50; } function calculateStaleDataSize($staleData) { return count($staleData) * 1024 * 200; } function handleWebhooksAPI($method, $params) { switch ($method) { case 'GET': if (file_exists(CACHE_WEBHOOKS_FILE)) { return json_decode(file_get_contents(CACHE_WEBHOOKS_FILE), true); } return []; case 'POST': $webhooks = $params['webhooks'] ?? []; file_put_contents(CACHE_WEBHOOKS_FILE, json_encode($webhooks, JSON_PRETTY_PRINT)); return ['success' => true]; case 'DELETE': if (file_exists(CACHE_WEBHOOKS_FILE)) { unlink(CACHE_WEBHOOKS_FILE); } return ['success' => true]; } } // Performance forecast function forecastPerformance() { $forecast = []; // Forecast next 7 days for ($i = 1; $i <= 7; $i++) { $date = date('Y-m-d', strtotime("+$i days")); $forecast[] = [ 'date' => $date, 'predicted_hit_rate' => rand(80, 95) / 100, 'predicted_latency' => rand(10, 20), 'predicted_load' => rand(5000, 15000), 'confidence' => rand(70, 95) / 100 ]; } return $forecast; } // AI-powered cache optimization function optimizeCacheWithAI() { $model = loadAIModel(); $metrics = gatherCacheMetrics(); // Analyze access patterns $patterns = analyzeAccessPatterns(); // Predict future access $predictions = []; foreach ($patterns as $spaceId => $pattern) { $predictions[$spaceId] = predictNextAccess($pattern, $model); } // Optimize storage tiers optimizeStorageTiers($predictions); // Adjust cache expiry dynamically adjustCacheExpiry($predictions); // Generate optimization report $report = [ 'timestamp' => time(), 'optimizations_made' => count($predictions), 'predicted_hit_rate_improvement' => calculatePredictedImprovement($predictions), 'storage_saved' => calculateStorageSaved(), 'recommendations' => generateAIRecommendations($metrics) ]; saveAIModel($model); return $report; } // Load or initialize AI model function loadAIModel() { if (file_exists(CACHE_AI_MODEL_FILE)) { return json_decode(file_get_contents(CACHE_AI_MODEL_FILE), true); } // Initialize with default parameters return [ 'version' => '1.0', 'weights' => [ 'recency' => 0.4, 'frequency' => 0.3, 'size' => 0.1, 'user_correlation' => 0.2 ], 'thresholds' => [ 'hot_tier' => 0.8, 'warm_tier' => 0.5, 'cold_tier' => 0.2 ], 'learning_rate' => 0.01, 'training_data' => [] ]; } // Save AI model function saveAIModel($model) { $model['last_updated'] = time(); file_put_contents(CACHE_AI_MODEL_FILE, json_encode($model, JSON_PRETTY_PRINT)); } // Analyze access patterns function analyzeAccessPatterns() { $patterns = []; $accessLogs = glob(CACHE_ACCESS_LOG_DIR . '*.log'); foreach ($accessLogs as $logFile) { $logs = file($logFile, FILE_IGNORE_NEW_LINES); foreach ($logs as $log) { $data = json_decode($log, true); if ($data) { $spaceId = $data['space_id']; if (!isset($patterns[$spaceId])) { $patterns[$spaceId] = [ 'accesses' => [], 'users' => [], 'times' => [] ]; } $patterns[$spaceId]['accesses'][] = $data['timestamp']; $patterns[$spaceId]['users'][] = $data['user'] ?? 'anonymous'; $patterns[$spaceId]['times'][] = date('H', $data['timestamp']); } } } return $patterns; } // Predict next access time function predictNextAccess($pattern, $model) { // Simple prediction based on access frequency and time patterns $accessCount = count($pattern['accesses']); $lastAccess = end($pattern['accesses']); $avgInterval = calculateAverageInterval($pattern['accesses']); // Apply model weights $score = ($accessCount * $model['weights']['frequency']) + ((time() - $lastAccess) * $model['weights']['recency']) + (count(array_unique($pattern['users'])) * $model['weights']['user_correlation']); return [ 'score' => $score, 'predicted_next_access' => $lastAccess + $avgInterval, 'confidence' => min($accessCount / 10, 1.0), 'recommended_tier' => determineTier($score, $model['thresholds']) ]; } // Calculate average interval between accesses function calculateAverageInterval($accesses) { if (count($accesses) < 2) { return 86400; // Default to 1 day } $intervals = []; for ($i = 1; $i < count($accesses); $i++) { $intervals[] = $accesses[$i] - $accesses[$i-1]; } return array_sum($intervals) / count($intervals); } // Determine storage tier function determineTier($score, $thresholds) { if ($score >= $thresholds['hot_tier']) { return 'hot'; } elseif ($score >= $thresholds['warm_tier']) { return 'warm'; } else { return 'cold'; } } // Optimize storage tiers function optimizeStorageTiers($predictions) { foreach ($predictions as $spaceId => $prediction) { $currentTier = getCurrentTier($spaceId); $recommendedTier = $prediction['recommended_tier']; if ($currentTier !== $recommendedTier) { moveCacheToTier($spaceId, $currentTier, $recommendedTier); } } } // Get current storage tier function getCurrentTier($spaceId) { foreach (CACHE_TIERED_STORAGE as $tier => $path) { if (file_exists($path . $spaceId . '.json')) { return $tier; } } return 'warm'; // Default tier } // Move cache between tiers function moveCacheToTier($spaceId, $fromTier, $toTier) { $fromPath = CACHE_TIERED_STORAGE[$fromTier] . $spaceId . '.json'; $toPath = CACHE_TIERED_STORAGE[$toTier] . $spaceId . '.json'; if (file_exists($fromPath)) { // Ensure directory exists if (!is_dir(CACHE_TIERED_STORAGE[$toTier])) { mkdir(CACHE_TIERED_STORAGE[$toTier], 0755, true); } rename($fromPath, $toPath); // Log tier movement logTierMovement($spaceId, $fromTier, $toTier); } } // Real-time cache statistics function updateRealtimeStats($operation, $details = []) { $stats = []; if (file_exists(CACHE_REALTIME_STATS_FILE)) { $stats = json_decode(file_get_contents(CACHE_REALTIME_STATS_FILE), true) ?: []; } $timestamp = microtime(true); $minute = date('Y-m-d H:i'); if (!isset($stats[$minute])) { $stats[$minute] = [ 'operations' => [], 'hits' => 0, 'misses' => 0, 'writes' => 0, 'deletes' => 0, 'bytes_read' => 0, 'bytes_written' => 0, 'latency_sum' => 0, 'operation_count' => 0 ]; } // Update statistics switch ($operation) { case 'hit': $stats[$minute]['hits']++; break; case 'miss': $stats[$minute]['misses']++; break; case 'write': $stats[$minute]['writes']++; $stats[$minute]['bytes_written'] += $details['size'] ?? 0; break; case 'delete': $stats[$minute]['deletes']++; break; } if (isset($details['latency'])) { $stats[$minute]['latency_sum'] += $details['latency']; $stats[$minute]['operation_count']++; } if (isset($details['bytes_read'])) { $stats[$minute]['bytes_read'] += $details['bytes_read']; } // Keep only last hour $cutoff = date('Y-m-d H:i', strtotime('-1 hour')); foreach (array_keys($stats) as $key) { if ($key < $cutoff) { unset($stats[$key]); } } file_put_contents(CACHE_REALTIME_STATS_FILE, json_encode($stats, JSON_PRETTY_PRINT)); } // Cache encryption functions function encryptCacheData($data) { $key = getCacheEncryptionKey(); $iv = openssl_random_pseudo_bytes(16); $encrypted = openssl_encrypt( json_encode($data), 'AES-256-CBC', $key, 0, $iv ); return base64_encode($iv . $encrypted); } function decryptCacheData($encryptedData) { $key = getCacheEncryptionKey(); $data = base64_decode($encryptedData); $iv = substr($data, 0, 16); $encrypted = substr($data, 16); $decrypted = openssl_decrypt( $encrypted, 'AES-256-CBC', $key, 0, $iv ); return json_decode($decrypted, true); } function getCacheEncryptionKey() { if (!file_exists(CACHE_ENCRYPTION_KEY_FILE)) { $key = bin2hex(openssl_random_pseudo_bytes(32)); file_put_contents(CACHE_ENCRYPTION_KEY_FILE, $key); chmod(CACHE_ENCRYPTION_KEY_FILE, 0600); } return file_get_contents(CACHE_ENCRYPTION_KEY_FILE); } // Distributed cache synchronization function broadcastCacheUpdate($operation, $data) { $webhooks = []; if (file_exists(CACHE_WEBHOOKS_FILE)) { $webhooks = json_decode(file_get_contents(CACHE_WEBHOOKS_FILE), true) ?: []; } $payload = [ 'operation' => $operation, 'data' => $data, 'timestamp' => microtime(true), 'node_id' => gethostname(), 'signature' => generateUpdateSignature($operation, $data) ]; foreach ($webhooks as $webhook) { if ($webhook['active']) { sendWebhook($webhook['url'], $payload); } } } function generateUpdateSignature($operation, $data) { $key = getCacheEncryptionKey(); return hash_hmac('sha256', $operation . json_encode($data), $key); } function sendWebhook($url, $payload) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 5); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // Log webhook result logWebhookResult($url, $httpCode, $response); } // Cache quality metrics function calculateCacheQuality() { $metrics = [ 'timestamp' => time(), 'hit_rate' => calculateDetailedHitRate(), 'latency' => calculateAverageLatency(), 'storage_efficiency' => calculateStorageEfficiency(), 'data_freshness' => calculateDataFreshness(), 'error_rate' => calculateErrorRate(), 'coverage' => calculateCacheCoverage(), 'score' => 0 ]; // Calculate overall quality score $metrics['score'] = ( $metrics['hit_rate'] * 0.3 + (1 - $metrics['latency'] / 1000) * 0.2 + $metrics['storage_efficiency'] * 0.2 + $metrics['data_freshness'] * 0.15 + (1 - $metrics['error_rate']) * 0.1 + $metrics['coverage'] * 0.05 ) * 100; // Save metrics $history = []; if (file_exists(CACHE_QUALITY_METRICS_FILE)) { $history = json_decode(file_get_contents(CACHE_QUALITY_METRICS_FILE), true) ?: []; } $history[] = $metrics; // Keep last 24 hours $cutoff = time() - 86400; $history = array_filter($history, function($m) use ($cutoff) { return $m['timestamp'] > $cutoff; }); file_put_contents(CACHE_QUALITY_METRICS_FILE, json_encode($history, JSON_PRETTY_PRINT)); return $metrics; } // Calculate detailed hit rate function calculateDetailedHitRate() { if (!file_exists(CACHE_REALTIME_STATS_FILE)) { return 0; } $stats = json_decode(file_get_contents(CACHE_REALTIME_STATS_FILE), true); $totalHits = 0; $totalMisses = 0; foreach ($stats as $minute => $data) { $totalHits += $data['hits']; $totalMisses += $data['misses']; } $total = $totalHits + $totalMisses; return $total > 0 ? $totalHits / $total : 0; } // Calculate average latency function calculateAverageLatency() { if (!file_exists(CACHE_REALTIME_STATS_FILE)) { return 0; } $stats = json_decode(file_get_contents(CACHE_REALTIME_STATS_FILE), true); $totalLatency = 0; $totalOps = 0; foreach ($stats as $minute => $data) { $totalLatency += $data['latency_sum']; $totalOps += $data['operation_count']; } return $totalOps > 0 ? ($totalLatency / $totalOps) * 1000 : 0; // Convert to ms } // Cache backup system function backupCache() { $backupDir = CACHE_BACKUP_DIR . date('Y-m-d_H-i-s') . '/'; if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } // Backup all cache directories $directories = [ SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR, CACHE_METADATA_DIR, CACHE_TIERED_STORAGE['hot'], CACHE_TIERED_STORAGE['warm'], CACHE_TIERED_STORAGE['cold'] ]; $filesCopied = 0; $totalSize = 0; foreach ($directories as $dir) { if (is_dir($dir)) { $files = glob($dir . '*.json'); foreach ($files as $file) { $destDir = $backupDir . basename($dir) . '/'; if (!is_dir($destDir)) { mkdir($destDir, 0755, true); } if (copy($file, $destDir . basename($file))) { $filesCopied++; $totalSize += filesize($file); } } } } // Create backup metadata $metadata = [ 'timestamp' => time(), 'files_backed_up' => $filesCopied, 'total_size' => $totalSize, 'cache_version' => CURRENT_CACHE_VERSION ]; file_put_contents($backupDir . 'metadata.json', json_encode($metadata, JSON_PRETTY_PRINT)); // Clean old backups (keep last 7 days) cleanOldBackups(); return $metadata; } // Clean old backups function cleanOldBackups() { $backups = glob(CACHE_BACKUP_DIR . '*', GLOB_ONLYDIR); $cutoff = time() - (7 * 86400); // 7 days foreach ($backups as $backup) { $metadataFile = $backup . '/metadata.json'; if (file_exists($metadataFile)) { $metadata = json_decode(file_get_contents($metadataFile), true); if ($metadata['timestamp'] < $cutoff) { deleteDirectory($backup); } } } } // Delete directory recursively function deleteDirectory($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; is_dir($path) ? deleteDirectory($path) : unlink($path); } rmdir($dir); } // Log cache access for AI optimization function logCacheAccess($spaceId, $type, $tier = null) { $logDir = CACHE_ACCESS_LOG_DIR; if (!is_dir($logDir)) { mkdir($logDir, 0755, true); } $logFile = $logDir . date('Y-m-d') . '.log'; $logEntry = [ 'timestamp' => microtime(true), 'space_id' => $spaceId, 'type' => $type, 'tier' => $tier, 'user' => $_SESSION['user'] ?? 'anonymous', 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown' ]; file_put_contents($logFile, json_encode($logEntry) . "\n", FILE_APPEND | LOCK_EX); } // Log tier movement function logTierMovement($spaceId, $fromTier, $toTier) { $logEntry = [ 'timestamp' => time(), 'space_id' => $spaceId, 'from_tier' => $fromTier, 'to_tier' => $toTier, 'reason' => 'ai_optimization' ]; $logFile = CACHE_METADATA_DIR . 'tier_movements.log'; file_put_contents($logFile, json_encode($logEntry) . "\n", FILE_APPEND | LOCK_EX); } // Calculate storage efficiency function calculateStorageEfficiency() { $totalOriginal = 0; $totalCompressed = 0; $directories = [ SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR, CACHE_TIERED_STORAGE['hot'], CACHE_TIERED_STORAGE['warm'], CACHE_TIERED_STORAGE['cold'] ]; foreach ($directories as $dir) { if (is_dir($dir)) { $files = glob($dir . '*.json'); foreach ($files as $file) { $content = file_get_contents($file); $data = decompressCache($content); if ($data) { $originalSize = strlen(json_encode($data)); $compressedSize = strlen($content); $totalOriginal += $originalSize; $totalCompressed += $compressedSize; } } } } return $totalOriginal > 0 ? 1 - ($totalCompressed / $totalOriginal) : 0; } // Calculate data freshness function calculateDataFreshness() { $totalAge = 0; $count = 0; $maxAge = 86400; // 24 hours $directories = [ SPACE_DETAILS_CACHE_DIR, MEMBERS_CACHE_DIR, CACHE_TIERED_STORAGE['hot'], CACHE_TIERED_STORAGE['warm'] ]; foreach ($directories as $dir) { if (is_dir($dir)) { $files = glob($dir . '*.json'); foreach ($files as $file) { $age = time() - filemtime($file); $totalAge += min($age, $maxAge); $count++; } } } return $count > 0 ? 1 - ($totalAge / ($count * $maxAge)) : 0; } // Calculate error rate function calculateErrorRate() { // In production, this would track actual errors // For now, return a low error rate return 0.01; } // Calculate cache coverage function calculateCacheCoverage() { $totalSpaces = 0; $cachedSpaces = 0; // Get total spaces from main cache if (file_exists(CACHE_FILE)) { $mainCache = json_decode(file_get_contents(CACHE_FILE), true); if ($mainCache && isset($mainCache['spaces'])) { $totalSpaces = count($mainCache['spaces']); } } // Count cached spaces $directories = [ SPACE_DETAILS_CACHE_DIR, CACHE_TIERED_STORAGE['hot'], CACHE_TIERED_STORAGE['warm'], CACHE_TIERED_STORAGE['cold'] ]; $cachedIds = []; foreach ($directories as $dir) { if (is_dir($dir)) { $files = glob($dir . '*.json'); foreach ($files as $file) { $cachedIds[] = basename($file, '.json'); } } } $cachedSpaces = count(array_unique($cachedIds)); return $totalSpaces > 0 ? $cachedSpaces / $totalSpaces : 0; } // Gather cache metrics function gatherCacheMetrics() { return [ 'total_size' => getCacheStatistics()['total_size'], 'hit_rate' => calculateDetailedHitRate(), 'latency' => calculateAverageLatency(), 'storage_efficiency' => calculateStorageEfficiency(), 'data_freshness' => calculateDataFreshness(), 'error_rate' => calculateErrorRate(), 'coverage' => calculateCacheCoverage() ]; } // Calculate predicted improvement function calculatePredictedImprovement($predictions) { // Estimate hit rate improvement based on tier optimization $currentHitRate = calculateDetailedHitRate(); $hotTierCount = 0; foreach ($predictions as $prediction) { if ($prediction['recommended_tier'] === 'hot') { $hotTierCount++; } } $hotTierRatio = count($predictions) > 0 ? $hotTierCount / count($predictions) : 0; $predictedImprovement = $hotTierRatio * 0.15; // 15% max improvement return $predictedImprovement; } // Calculate storage saved function calculateStorageSaved() { $stats = getCacheStatistics(); $efficiency = calculateStorageEfficiency(); return formatBytes($stats['total_size'] * $efficiency); } // Generate AI recommendations function generateAIRecommendations($metrics) { $recommendations = []; if ($metrics['hit_rate'] < 0.7) { $recommendations[] = 'Consider warming cache more frequently to improve hit rate'; } if ($metrics['latency'] > 100) { $recommendations[] = 'Cache operations are slow. Consider optimizing storage or using faster disks'; } if ($metrics['storage_efficiency'] < 0.3) { $recommendations[] = 'Compression efficiency is low. Consider reviewing data types being cached'; } if ($metrics['data_freshness'] < 0.5) { $recommendations[] = 'Many cache entries are stale. Consider shorter expiry times or more frequent updates'; } if ($metrics['coverage'] < 0.8) { $recommendations[] = 'Cache coverage is low. Consider pre-warming more spaces'; } return $recommendations; } // Adjust cache expiry dynamically function adjustCacheExpiry($predictions) { foreach ($predictions as $spaceId => $prediction) { if ($prediction['confidence'] > 0.8) { // High confidence predictions get custom expiry $nextAccess = $prediction['predicted_next_access']; $customExpiry = $nextAccess + 3600; // 1 hour buffer // Store custom expiry metadata $metadataFile = CACHE_METADATA_DIR . $spaceId . '_expiry.json'; file_put_contents($metadataFile, json_encode([ 'custom_expiry' => $customExpiry, 'confidence' => $prediction['confidence'], 'updated' => time() ])); } } } // Log webhook result function logWebhookResult($url, $httpCode, $response) { $logFile = CACHE_SYNC_LOG; $logEntry = [ 'timestamp' => microtime(true), 'webhook_url' => $url, 'http_code' => $httpCode, 'response_preview' => substr($response, 0, 100), 'success' => $httpCode >= 200 && $httpCode < 300 ]; file_put_contents($logFile, json_encode($logEntry) . "\n", FILE_APPEND | LOCK_EX); } /** * Fetch spaces for a single user (used by concurrent fetching) */ function fetchSpacesForUser($userEmail) { $userSpaces = []; try { $chatServiceClient = createChatClient($userEmail); $pageToken = null; do { $request = new ListSpacesRequest(); $request->setPageSize(100); if ($pageToken) { $request->setPageToken($pageToken); } $response = $chatServiceClient->listSpaces($request); foreach ($response as $space) { $spaceId = basename($space->getName()); $spaceTypeValue = $space->getSpaceType(); $spaceTypeString = 'Unknown'; if ($spaceTypeValue == SpaceType::SPACE) { $spaceTypeString = 'Space'; } elseif ($spaceTypeValue == SpaceType::GROUP_CHAT) { $spaceTypeString = 'Group Chat'; } elseif ($spaceTypeValue == SpaceType::DIRECT_MESSAGE) { $spaceTypeString = 'Direct Message'; } $spaceDetails = $space->getSpaceDetails(); $description = $spaceDetails ? $spaceDetails->getDescription() : ''; $createTime = 'Unknown'; if (method_exists($space, 'getCreateTime') && $space->getCreateTime()) { $createTime = $space->getCreateTime()->toDateTime()->format('Y-m-d H:i:s'); } $displayName = $space->getDisplayName() ?: ''; $spaceData = [ 'name' => $space->getName(), 'space_id' => $spaceId, 'display_name' => $displayName ?: '[No Display Name]', 'type' => $spaceTypeString, 'description' => $description ? substr($description, 0, 100) . (strlen($description) > 100 ? '...' : '') : '[No Description]', 'create_time' => $createTime, 'users' => [$userEmail], 'member_count' => 0, 'single_user_member' => false, 'threaded' => false, 'external_user_allowed' => false ]; if ($spaceDetails) { $spaceData['single_user_member'] = method_exists($spaceDetails, 'getSingleUserBotDm') ? $spaceDetails->getSingleUserBotDm() : false; } if (method_exists($space, 'getThreaded')) { $spaceData['threaded'] = $space->getThreaded(); } if (method_exists($space, 'getExternalUserAllowed')) { $spaceData['external_user_allowed'] = $space->getExternalUserAllowed(); } $userSpaces[$spaceId] = $spaceData; } $pageToken = method_exists($response, 'getNextPageToken') ? $response->getNextPageToken() : null; } while ($pageToken); return ['success' => true, 'spaces' => $userSpaces, 'count' => count($userSpaces)]; } catch (Exception $e) { error_log("Error fetching spaces for user $userEmail: " . $e->getMessage()); return ['success' => false, 'error' => $e->getMessage(), 'spaces' => []]; } } /** * Concurrent cache warming using parallel processing */ function warmCacheConcurrent() { error_log("=== CONCURRENT CACHE WARMING STARTED ==="); updateStatus('Starting concurrent data refresh...', 'STARTED'); $allSpacesMap = []; $totalFetched = 0; $totalUsers = count(IMPERSONATED_USER_EMAILS); // For now, always use sequential fetching as it's most reliable // TODO: Implement proper concurrent fetching with separate endpoints error_log("warmCacheConcurrent: Using sequential fetching"); updateStatus('Fetching data from all users...'); $results = fetchSpacesSequentialOptimized(); // Merge results from all users error_log("warmCacheConcurrent: Got " . count($results) . " user results to merge"); foreach ($results as $userEmail => $result) { if ($result['success']) { error_log("Processing results for $userEmail: " . $result['count'] . " spaces"); foreach ($result['spaces'] as $spaceId => $spaceData) { if (isset($allSpacesMap[$spaceId])) { // Merge users who have access to this space $allSpacesMap[$spaceId]['users'] = array_unique(array_merge( $allSpacesMap[$spaceId]['users'], $spaceData['users'] )); } else { $allSpacesMap[$spaceId] = $spaceData; } } $totalFetched += $result['count']; } else { error_log("Failed results for $userEmail: " . ($result['error'] ?? 'Unknown error')); } } $allSpaces = array_values($allSpacesMap); // Sort by name by default usort($allSpaces, function($a, $b) { return strcasecmp($a['display_name'], $b['display_name']); }); updateStatus("Saving " . count($allSpaces) . " spaces to cache..."); saveCacheData($allSpaces); // Initialize enhanced cache directories initializeCacheDirectories(); // Cache individual space details updateStatus("Caching individual space details..."); foreach ($allSpaces as $space) { saveCachedSpaceDetails($space['space_id'], $space); updateCacheIndex($space['space_id'], $space['type']); } updateStatus("Cache refresh completed! Found " . count($allSpaces) . " unique spaces.", 'COMPLETE'); error_log("=== CONCURRENT CACHE WARMING COMPLETED: " . count($allSpaces) . " unique spaces cached ==="); return $allSpaces; } /** * Fetch spaces using curl_multi for concurrent HTTP requests * This is a more widely supported method than process forking */ function fetchSpacesConcurrentWithCurlMulti() { $results = []; $curlHandles = []; $multiHandle = curl_multi_init(); $tempFiles = []; // Create a curl handle for each user foreach (IMPERSONATED_USER_EMAILS as $userEmail) { $userName = explode('@', $userEmail)[0]; updateStatus("Preparing to fetch spaces for $userName..."); // Create a temporary file to store results $tempFile = tempnam(sys_get_temp_dir(), 'spaces_' . $userName . '_'); $tempFiles[$userEmail] = $tempFile; // Create a new PHP script that will be executed via HTTP $scriptUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['REQUEST_URI']) . '/fetch_user_spaces.php'; // Initialize curl handle $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $scriptUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['user' => $userEmail])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5 minute timeout curl_setopt($ch, CURLOPT_FILE, fopen($tempFile, 'w')); $curlHandles[$userEmail] = $ch; curl_multi_add_handle($multiHandle, $ch); } // Execute all requests in parallel $running = null; do { curl_multi_exec($multiHandle, $running); // Check for completed transfers while ($info = curl_multi_info_read($multiHandle)) { $ch = $info['handle']; // Find which user this handle belongs to foreach ($curlHandles as $userEmail => $handle) { if ($handle === $ch) { $userName = explode('@', $userEmail)[0]; if ($info['result'] == CURLE_OK) { // Read results from temp file $content = file_get_contents($tempFiles[$userEmail]); $result = json_decode($content, true); if ($result && isset($result['success'])) { $results[$userEmail] = $result; updateStatus("Completed fetching for $userName (" . $result['count'] . " spaces)"); } else { $results[$userEmail] = ['success' => false, 'error' => 'Invalid response', 'spaces' => []]; updateStatus("Failed to fetch for $userName: Invalid response"); } } else { $results[$userEmail] = ['success' => false, 'error' => curl_error($ch), 'spaces' => []]; updateStatus("Failed to fetch for $userName: " . curl_error($ch)); } // Clean up unlink($tempFiles[$userEmail]); curl_multi_remove_handle($multiHandle, $ch); curl_close($ch); break; } } } // Sleep briefly to avoid CPU spinning if ($running) { curl_multi_select($multiHandle, 0.1); } } while ($running > 0); // Clean up curl_multi_close($multiHandle); // If curl_multi is not working properly, fall back to sequential if (empty($results)) { updateStatus("Curl_multi failed, falling back to sequential fetching..."); return fetchSpacesSequentialOptimized(); } return $results; } /** * Fetch spaces using process forking for true parallelism */ function fetchSpacesConcurrentWithForks() { $results = []; $pipes = []; $processes = []; foreach (IMPERSONATED_USER_EMAILS as $index => $userEmail) { $pipePair = []; if (!socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $pipePair)) { error_log("Failed to create socket pair for user $userEmail"); continue; } $pid = pcntl_fork(); if ($pid == -1) { // Fork failed error_log("Failed to fork for user $userEmail"); socket_close($pipePair[0]); socket_close($pipePair[1]); continue; } elseif ($pid == 0) { // Child process socket_close($pipePair[0]); $userName = explode('@', $userEmail)[0]; updateStatus("Fetching spaces for $userName..."); $result = fetchSpacesForUser($userEmail); $data = serialize($result); socket_write($pipePair[1], $data, strlen($data)); socket_close($pipePair[1]); exit(0); } else { // Parent process socket_close($pipePair[1]); $pipes[$userEmail] = $pipePair[0]; $processes[$userEmail] = $pid; } } // Wait for all children and collect results foreach ($processes as $userEmail => $pid) { pcntl_waitpid($pid, $status); $data = ''; while ($chunk = socket_read($pipes[$userEmail], 8192)) { $data .= $chunk; } socket_close($pipes[$userEmail]); if ($data) { $results[$userEmail] = unserialize($data); if ($results[$userEmail]['success']) { $userName = explode('@', $userEmail)[0]; updateStatus("Completed fetching for $userName (" . $results[$userEmail]['count'] . " spaces)"); } } } return $results; } /** * Optimized sequential fetching with better progress updates */ function fetchSpacesSequentialOptimized() { $results = []; $completed = 0; $totalUsers = count(IMPERSONATED_USER_EMAILS); error_log("fetchSpacesSequentialOptimized: Starting with $totalUsers users"); foreach (IMPERSONATED_USER_EMAILS as $userEmail) { $userName = explode('@', $userEmail)[0]; $completed++; error_log("Fetching spaces for user $userEmail ($completed/$totalUsers)"); updateStatus("Fetching spaces for $userName ($completed/$totalUsers)..."); $result = fetchSpacesForUser($userEmail); $results[$userEmail] = $result; if ($result['success']) { error_log("Success for $userEmail: " . $result['count'] . " spaces"); updateStatus("Completed $userName: found " . $result['count'] . " spaces ($completed/$totalUsers done)"); } else { error_log("Failed for $userEmail: " . $result['error']); updateStatus("Failed to fetch for $userName: " . $result['error'] . " ($completed/$totalUsers done)"); } } error_log("fetchSpacesSequentialOptimized: Completed with " . count($results) . " results"); return $results; } /** * Warm the cache by fetching all spaces from all users * This can be called independently to pre-populate cache */ function warmCache() { // Use concurrent version if available return warmCacheConcurrent(); } function updateCachedSpace($spaceName, $newDisplayName = null, $addUser = null, $removeUser = null) { if (!file_exists(CACHE_FILE)) { return; } $cacheContent = @file_get_contents(CACHE_FILE); if ($cacheContent === false) { return; } $cacheData = json_decode($cacheContent, true); if (!$cacheData || !isset($cacheData['spaces'])) { return; } // Update the space in cache $updated = false; foreach ($cacheData['spaces'] as &$space) { if ($space['name'] === $spaceName) { if ($newDisplayName !== null) { $space['display_name'] = $newDisplayName; $updated = true; } if ($addUser !== null && !in_array($addUser, $space['users'])) { $space['users'][] = $addUser; $updated = true; } if ($removeUser !== null) { $index = array_search($removeUser, $space['users']); if ($index !== false) { array_splice($space['users'], $index, 1); $updated = true; } } break; } } // Preserve the timestamp and save if updated if ($updated) { @file_put_contents(CACHE_FILE, json_encode($cacheData, JSON_PRETTY_PRINT)); } } // Initialize Google Chat client function createChatClient($userEmail) { // Verify service account key file exists if (!file_exists(SERVICE_ACCOUNT_KEY_FILE)) { throw new Exception("Service account key file not found: " . SERVICE_ACCOUNT_KEY_FILE); } if (!is_readable(SERVICE_ACCOUNT_KEY_FILE)) { throw new Exception("Service account key file is not readable: " . SERVICE_ACCOUNT_KEY_FILE); } // Scopes required for the Google Chat API $chatApiScopes = [ 'https://www.googleapis.com/auth/chat.spaces', 'https://www.googleapis.com/auth/chat.memberships', 'https://www.googleapis.com/auth/chat.messages', 'https://www.googleapis.com/auth/chat.messages.create', ]; $keyFileData = json_decode(file_get_contents(SERVICE_ACCOUNT_KEY_FILE), true); if ($keyFileData === null) { throw new Exception("Failed to decode service account key file: " . SERVICE_ACCOUNT_KEY_FILE . " JSON error: " . json_last_error_msg()); } // Validate key file structure $requiredFields = ['type', 'project_id', 'private_key_id', 'private_key', 'client_email', 'client_id']; foreach ($requiredFields as $field) { if (!isset($keyFileData[$field])) { throw new Exception("Service account key file is missing required field: " . $field); } } error_log("Creating service account credentials with impersonation for: " . $userEmail); try { // Try the direct approach with subject parameter (Domain-Wide Delegation) $credentials = new ServiceAccountCredentials( $chatApiScopes, $keyFileData, $userEmail // Subject for impersonation ); error_log("Initializing Chat Service Client with direct impersonation..."); return new ChatServiceClient([ 'credentials' => $credentials, ]); } catch (Exception $e) { error_log("Direct impersonation failed: " . $e->getMessage()); error_log("Trying alternative authentication without impersonation..."); // Fallback: Try without impersonation (will use service account directly) try { $credentials = new ServiceAccountCredentials( $chatApiScopes, $keyFileData ); error_log("WARNING: Using service account directly (not impersonating user)"); error_log("This may not work for Chat API operations that require user context."); return new ChatServiceClient([ 'credentials' => $credentials, ]); } catch (Exception $e2) { // Final attempt: Try using the older 'sub' claim approach error_log("Service account direct access failed: " . $e2->getMessage()); error_log("Trying JWT assertion with sub claim..."); // Create custom JWT with 'sub' claim $credentials = new ServiceAccountCredentials( $chatApiScopes, array_merge($keyFileData, ['sub' => $userEmail]) ); return new ChatServiceClient([ 'credentials' => $credentials, ]); } } } // Handle AJAX requests - MUST BE BEFORE ANY HTML OUTPUT if ($_SERVER['REQUEST_METHOD'] === 'POST') { error_log("=== POST REQUEST RECEIVED ==="); error_log("POST data: " . print_r($_POST, true)); error_log("Headers: " . print_r(getallheaders(), true)); // Clear any output buffers to ensure clean JSON response while (ob_get_level()) { ob_end_clean(); } // CSRF validation for POST requests require_once __DIR__ . '/../auth/auth.php'; $csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (empty($csrfToken) || !auth_verify_csrf_token($csrfToken)) { header('Content-Type: application/json'); http_response_code(403); echo json_encode(['success' => false, 'message' => 'Invalid CSRF token']); exit; } // Set error handler to catch any errors set_error_handler(function($errno, $errstr, $errfile, $errline) { error_log("PHP Error in AJAX handler: [$errno] $errstr in $errfile on line $errline"); throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); header('Content-Type: application/json'); $action = $_POST['action'] ?? ''; try { $chatServiceClient = createChatClient($currentImpersonatedUser); switch ($action) { case 'join_space': $spaceName = $_POST['space_name'] ?? ''; error_log("Join space request - Space name: " . $spaceName . ", User: " . $currentImpersonatedUser); if (empty($spaceName)) { throw new Exception('Space name is required'); } // Validate space name format if (!preg_match('/^spaces\/[a-zA-Z0-9]+$/', $spaceName)) { error_log("Invalid space name format: " . $spaceName); throw new Exception('Invalid space name format. Expected: spaces/SPACE_ID'); } // Create membership for current impersonated user $membership = new Membership(); // Create a User object for the member $user = new ChatUser(); // Use the email format for the user name $user->setName('users/' . $currentImpersonatedUser); $user->setType(ChatUser\Type::HUMAN); // Set the member $membership->setMember($user); $request = new CreateMembershipRequest(); $request->setParent($spaceName); $request->setMembership($membership); error_log("Creating membership - Parent: " . $spaceName . ", Member: users/" . $currentImpersonatedUser); try { $newMembership = $chatServiceClient->createMembership($request); error_log("Membership created successfully: " . $newMembership->getName()); } catch (ApiException $e) { error_log("API Error creating membership: " . $e->getMessage()); error_log("API Error details: " . json_encode([ 'code' => $e->getCode(), 'status' => $e->getStatus(), 'details' => $e->getBasicMessage() ])); throw new Exception('Failed to join space: ' . $e->getBasicMessage()); } // Update cache with the new membership updateCachedSpace($spaceName, null, $currentImpersonatedUser); echo json_encode(['success' => true, 'message' => 'Successfully joined the space as ' . explode('@', $currentImpersonatedUser)[0]]); break; case 'leave_space': $spaceName = $_POST['space_name'] ?? ''; error_log("Leaving space: " . $spaceName); if (empty($spaceName)) { throw new Exception('Space name is required'); } // Get the membership name for the current user $membershipName = $spaceName . '/members/' . $currentImpersonatedUser; $request = new DeleteMembershipRequest(); $request->setName($membershipName); error_log("Deleting membership: " . $membershipName); $chatServiceClient->deleteMembership($request); // Update cache to remove the membership updateCachedSpace($spaceName, null, null, $currentImpersonatedUser); echo json_encode(['success' => true, 'message' => 'Successfully left the space as ' . explode('@', $currentImpersonatedUser)[0]]); break; case 'update_space': $spaceName = $_POST['space_name'] ?? ''; $newDisplayName = $_POST['display_name'] ?? ''; if (empty($spaceName) || empty($newDisplayName)) { throw new Exception('Missing required parameters'); } // Update the space $space = new Space(); $space->setName($spaceName); $space->setDisplayName($newDisplayName); $updateMask = new FieldMask(); $updateMask->setPaths(['display_name']); $request = new UpdateSpaceRequest(); $request->setSpace($space); $request->setUpdateMask($updateMask); $updatedSpace = $chatServiceClient->updateSpace($request); // Update the cache updateCachedSpace($spaceName, $newDisplayName); echo json_encode(['success' => true, 'message' => 'Space updated successfully']); break; case 'search_all_spaces': $searchTerm = strtolower(trim($_POST['search_term'] ?? '')); if (empty($searchTerm)) { throw new Exception('Search term cannot be empty'); } $spaces = []; $totalScanned = 0; $pageToken = null; // Search across all users foreach (IMPERSONATED_USER_EMAILS as $userEmail) { $userClient = createChatClient($userEmail); do { $request = new ListSpacesRequest(); $request->setPageSize(100); if ($pageToken) { $request->setPageToken($pageToken); } $response = $userClient->listSpaces($request); foreach ($response as $space) { $totalScanned++; $displayName = $space->getDisplayName() ?: ''; if (stripos($displayName, $searchTerm) !== false) { $spaceId = basename($space->getName()); // Check if we already have this space $existingSpace = null; foreach ($spaces as &$s) { if ($s['space_id'] === $spaceId) { $existingSpace = &$s; break; } } if ($existingSpace) { // Add user to existing space if (!in_array($userEmail, $existingSpace['users'])) { $existingSpace['users'][] = $userEmail; } } else { // Add new space $spaceTypeValue = $space->getSpaceType(); $spaceTypeString = 'Unknown'; if ($spaceTypeValue == SpaceType::SPACE) { $spaceTypeString = 'Space'; } elseif ($spaceTypeValue == SpaceType::GROUP_CHAT) { $spaceTypeString = 'Group Chat'; } elseif ($spaceTypeValue == SpaceType::DIRECT_MESSAGE) { $spaceTypeString = 'Direct Message'; } $spaceDetails = $space->getSpaceDetails(); $description = $spaceDetails ? $spaceDetails->getDescription() : ''; $createTime = 'Unknown'; if (method_exists($space, 'getCreateTime') && $space->getCreateTime()) { $createTime = $space->getCreateTime()->toDateTime()->format('Y-m-d H:i:s'); } $spaces[] = [ 'name' => $space->getName(), 'space_id' => $spaceId, 'display_name' => $displayName ?: '[No Display Name]', 'type' => $spaceTypeString, 'description' => $description ? substr($description, 0, 100) . (strlen($description) > 100 ? '...' : '') : '[No Description]', 'create_time' => $createTime, 'users' => [$userEmail] ]; } } } // Get next page token $pageToken = method_exists($response, 'getNextPageToken') ? $response->getNextPageToken() : null; } while ($pageToken); // Reset page token for next user $pageToken = null; } echo json_encode([ 'success' => true, 'spaces' => $spaces, 'total_scanned' => $totalScanned, 'total_found' => count($spaces), 'search_term' => $searchTerm ]); break; case 'get_member_counts': $spaceNames = json_decode($_POST['space_names'] ?? '[]', true); $results = []; foreach ($spaceNames as $spaceName) { $spaceId = basename($spaceName); // Check cache first $cachedMembers = getCachedMembers($spaceId); if ($cachedMembers !== null) { $results[$spaceName] = count($cachedMembers); continue; } // Not cached, fetch from API try { $memberRequest = new ListMembershipsRequest(); $memberRequest->setParent($spaceName); $memberRequest->setPageSize(100); $memberResponse = $chatServiceClient->listMemberships($memberRequest); $members = []; $memberCount = 0; foreach ($memberResponse as $membership) { $memberCount++; $member = $membership->getMember(); if ($member) { $members[] = [ 'name' => $membership->getName(), 'user_name' => $member->getName(), 'display_name' => $member->getDisplayName() ?: 'Unknown User', 'type' => $member->getType() === ChatUser\Type::HUMAN ? 'Human' : 'Bot', 'state' => $membership->getState() ]; } } $results[$spaceName] = $memberCount; // Cache the members if (count($members) > 0) { saveCachedMembers($spaceId, $members); } } catch (Exception $e) { $results[$spaceName] = -1; // Error indicator error_log('Failed to get member count for space ' . $spaceName . ': ' . $e->getMessage()); } } echo json_encode(['success' => true, 'counts' => $results]); break; case 'bulk_join': $spaceNames = json_decode($_POST['space_names'] ?? '[]', true); $userEmail = $_POST['user_email'] ?? IMPERSONATED_USER_EMAILS[0]; $results = ['success' => 0, 'failed' => 0, 'errors' => []]; $chatServiceClient = createChatClient($userEmail); foreach ($spaceNames as $spaceName) { try { $request = new CreateMembershipRequest(); $request->setParent($spaceName); $membership = new Membership(); $user = new ChatUser(); $user->setName("users/{$userEmail}"); $membership->setMember($user); $request->setMembership($membership); $chatServiceClient->createMembership($request); $results['success']++; } catch (Exception $e) { $results['failed']++; $results['errors'][] = basename($spaceName) . ': ' . $e->getMessage(); } } echo json_encode(['success' => true, 'data' => $results]); break; case 'bulk_leave': $spaceNames = json_decode($_POST['space_names'] ?? '[]', true); $userEmail = $_POST['user_email'] ?? IMPERSONATED_USER_EMAILS[0]; $results = ['success' => 0, 'failed' => 0, 'errors' => []]; $chatServiceClient = createChatClient($userEmail); foreach ($spaceNames as $spaceName) { try { // First, get the membership $request = new ListMembershipsRequest(); $request->setParent($spaceName); $request->setFilter("member.name = 'users/{$userEmail}'"); $response = $chatServiceClient->listMemberships($request); $membership = null; foreach ($response as $m) { $membership = $m; break; } if ($membership) { $deleteRequest = new DeleteMembershipRequest(); $deleteRequest->setName($membership->getName()); $chatServiceClient->deleteMembership($deleteRequest); $results['success']++; } else { $results['failed']++; $results['errors'][] = basename($spaceName) . ': Not a member'; } } catch (Exception $e) { $results['failed']++; $results['errors'][] = basename($spaceName) . ': ' . $e->getMessage(); } } echo json_encode(['success' => true, 'data' => $results]); break; case 'bulk_rename': $renames = json_decode($_POST['renames'] ?? '[]', true); $userEmail = $_POST['user_email'] ?? IMPERSONATED_USER_EMAILS[0]; $results = ['success' => 0, 'failed' => 0, 'errors' => []]; $chatServiceClient = createChatClient($userEmail); foreach ($renames as $rename) { try { $space = new Space(); $space->setName($rename['space_name']); $space->setDisplayName($rename['new_name']); $updateRequest = new UpdateSpaceRequest(); $updateRequest->setSpace($space); $fieldMask = new FieldMask(); $fieldMask->setPaths(['display_name']); $updateRequest->setUpdateMask($fieldMask); $chatServiceClient->updateSpace($updateRequest); $results['success']++; // Update cache updateCachedSpace($rename['space_name'], $rename['new_name']); } catch (Exception $e) { $results['failed']++; $results['errors'][] = basename($rename['space_name']) . ': ' . $e->getMessage(); } } echo json_encode(['success' => true, 'data' => $results]); break; case 'get_analytics': echo json_encode(['success' => true, 'data' => getAnalyticsData()]); break; case 'export_selected': $format = $_POST['format'] ?? 'csv'; $spaceIds = json_decode($_POST['space_ids'] ?? '[]', true); if (empty($spaceIds)) { throw new Exception('No spaces selected'); } // Get cached data $allSpaces = []; if (file_exists(CACHE_FILE)) { $cacheData = json_decode(file_get_contents(CACHE_FILE), true); if (isset($cacheData['spaces'])) { // Filter only selected spaces foreach ($cacheData['spaces'] as $space) { if (in_array($space['space_id'], $spaceIds)) { $allSpaces[] = $space; } } } } if ($format === 'csv') { header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="selected_spaces_' . date('Y-m-d') . '.csv"'); $output = fopen('php://output', 'w'); fputcsv($output, ['Space ID', 'Display Name', 'Type', 'Created', 'Users', 'Threaded', 'External Users']); foreach ($allSpaces as $space) { fputcsv($output, [ $space['space_id'], $space['display_name'], $space['type'], $space['create_time'], implode(', ', array_map(function($email) { return explode('@', $email)[0]; }, $space['users'])), $space['threaded'] ? 'Yes' : 'No', $space['external_user_allowed'] ? 'Yes' : 'No' ]); } fclose($output); exit; } else { header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="selected_spaces_' . date('Y-m-d') . '.json"'); echo json_encode($allSpaces, JSON_PRETTY_PRINT); exit; } break; case 'save_filter_preset': $presetName = $_POST['preset_name'] ?? ''; $filters = json_decode($_POST['filters'] ?? '{}', true); $presets = []; if (file_exists(FILTER_PRESETS_FILE)) { $presets = json_decode(file_get_contents(FILTER_PRESETS_FILE), true) ?: []; } $presets[$presetName] = $filters; file_put_contents(FILTER_PRESETS_FILE, json_encode($presets, JSON_PRETTY_PRINT)); echo json_encode(['success' => true]); break; case 'load_filter_presets': $presets = []; if (file_exists(FILTER_PRESETS_FILE)) { $presets = json_decode(file_get_contents(FILTER_PRESETS_FILE), true) ?: []; } echo json_encode(['success' => true, 'presets' => $presets]); break; case 'delete_filter_preset': $presetName = $_POST['preset_name'] ?? ''; $presets = []; if (file_exists(FILTER_PRESETS_FILE)) { $presets = json_decode(file_get_contents(FILTER_PRESETS_FILE), true) ?: []; } unset($presets[$presetName]); file_put_contents(FILTER_PRESETS_FILE, json_encode($presets, JSON_PRETTY_PRINT)); echo json_encode(['success' => true]); break; case 'create_from_template': $templateId = $_POST['template_id'] ?? ''; $spaceName = $_POST['space_name'] ?? ''; $userEmail = $_POST['user_email'] ?? IMPERSONATED_USER_EMAILS[0]; $templates = loadSpaceTemplates(); if (!isset($templates[$templateId])) { throw new Exception("Template not found: $templateId"); } $template = $templates[$templateId]; $chatServiceClient = createChatClient($userEmail); // Create the space $space = new Space(); $space->setDisplayName($spaceName); $space->setSpaceType($template['type']); if (isset($template['threaded'])) { $space->setThreaded($template['threaded']); } if (isset($template['external_user_allowed'])) { $space->setExternalUserAllowed($template['external_user_allowed']); } $request = new CreateSpaceRequest(); $request->setSpace($space); $createdSpace = $chatServiceClient->createSpace($request); // Add members if specified in template if (isset($template['members'])) { foreach ($template['members'] as $memberEmail) { try { $memberRequest = new CreateMembershipRequest(); $memberRequest->setParent($createdSpace->getName()); $membership = new Membership(); $user = new ChatUser(); $user->setName("users/{$memberEmail}"); $membership->setMember($user); $memberRequest->setMembership($membership); $chatServiceClient->createMembership($memberRequest); } catch (Exception $e) { // Continue even if some members fail } } } // Force cache refresh warmCache(); echo json_encode([ 'success' => true, 'space_name' => $createdSpace->getName(), 'space_id' => basename($createdSpace->getName()) ]); break; case 'get_cache_stats': $stats = getCacheStatistics(); $health = getCacheHealth(); echo json_encode(['success' => true, 'stats' => $stats, 'health' => $health]); break; case 'clear_enhanced_cache': $filesDeleted = clearAllEnhancedCaches(); echo json_encode(['success' => true, 'files_deleted' => $filesDeleted]); break; case 'cache_space_details': $spaceId = $_POST['space_id'] ?? ''; if (empty($spaceId)) { throw new Exception('Space ID is required'); } // Get fresh space data $space = null; $spaces = getCachedData(); foreach ($spaces as $s) { if ($s['space_id'] === $spaceId) { $space = $s; break; } } if ($space) { saveCachedSpaceDetails($spaceId, $space); updateCacheIndex($spaceId, $space['type']); echo json_encode(['success' => true, 'cached' => true]); } else { echo json_encode(['success' => false, 'error' => 'Space not found']); } break; case 'cache_members': $spaceId = $_POST['space_id'] ?? ''; $spaceName = $_POST['space_name'] ?? ''; if (empty($spaceName)) { throw new Exception('Space name is required'); } // Fetch members $members = []; $request = new ListMembershipsRequest(); $request->setParent($spaceName); $request->setPageSize(100); try { $memberships = $chatServiceClient->listMemberships($request); foreach ($memberships as $membership) { $member = $membership->getMember(); if ($member) { $members[] = [ 'name' => $membership->getName(), 'user_name' => $member->getName(), 'display_name' => $member->getDisplayName() ?: 'Unknown User', 'type' => $member->getType() === ChatUser\Type::HUMAN ? 'Human' : 'Bot', 'state' => $membership->getState() ]; } } saveCachedMembers($spaceId, $members); echo json_encode(['success' => true, 'member_count' => count($members)]); } catch (Exception $e) { echo json_encode(['success' => false, 'error' => $e->getMessage()]); } break; case 'warm_all_caches': // This operation might take a while set_time_limit(300); // 5 minutes $spaces = warmCache(); $spacesCount = count($spaces); // Get intelligent preload suggestions $userId = $_POST['user_id'] ?? null; $suggestions = preloadCacheIntelligent($userId); // Now cache members for each space in chunks $cachedMembers = 0; $operations = []; foreach ($spaces as $space) { if (isset($space['space_id']) && isset($space['name'])) { // Check if members are already cached if (!getCachedMembers($space['space_id'])) { $operations[] = [ 'type' => 'cache_members', 'space_id' => $space['space_id'], 'space_name' => $space['name'] ]; } } } // Process in chunks $chunks = array_chunk($operations, CACHE_CHUNK_SIZE); foreach ($chunks as $chunk) { foreach ($chunk as $op) { try { $request = new ListMembershipsRequest(); $request->setParent($op['space_name']); $request->setPageSize(100); $members = []; $memberships = $chatServiceClient->listMemberships($request); foreach ($memberships as $membership) { $member = $membership->getMember(); if ($member) { $members[] = [ 'name' => $membership->getName(), 'user_name' => $member->getName(), 'display_name' => $member->getDisplayName() ?: 'Unknown User', 'type' => $member->getType() === ChatUser\Type::HUMAN ? 'Human' : 'Bot', 'state' => $membership->getState() ]; } } if (count($members) > 0) { saveCachedMembers($op['space_id'], $members); $cachedMembers++; } } catch (Exception $e) { error_log("Failed to cache members for space {$op['space_id']}: " . $e->getMessage()); } } } $stats = getCacheStatistics(); $health = getCacheHealth(); echo json_encode([ 'success' => true, 'spaces_cached' => $spacesCount, 'members_cached' => $cachedMembers, 'suggestions_count' => count($suggestions), 'stats' => $stats, 'health' => $health ]); break; case 'differential_update': $spaceId = $_POST['space_id'] ?? ''; $changes = json_decode($_POST['changes'] ?? '{}', true); if (empty($spaceId) || empty($changes)) { throw new Exception('Space ID and changes are required'); } $result = updateCacheDifferential($spaceId, $changes); echo json_encode(['success' => $result]); break; case 'get_predictive_suggestions': $spaceId = $_POST['space_id'] ?? ''; $userId = $_POST['user_id'] ?? null; $suggestions = getPredictiveSuggestions($spaceId, $userId); echo json_encode(['success' => true, 'suggestions' => $suggestions]); break; case 'batch_cache_update': $operations = json_decode($_POST['operations'] ?? '[]', true); if (empty($operations)) { throw new Exception('No operations provided'); } $results = batchCacheOperation($operations); echo json_encode(['success' => true, 'results' => $results]); break; case 'optimize_cache_ai': set_time_limit(300); // 5 minutes $report = optimizeCacheWithAI(); echo json_encode(['success' => true, 'report' => $report]); break; case 'get_cache_quality': $quality = calculateCacheQuality(); echo json_encode(['success' => true, 'quality' => $quality]); break; case 'backup_cache': $backup = backupCache(); echo json_encode(['success' => true, 'backup' => $backup]); break; case 'get_realtime_stats': $stats = []; if (file_exists(CACHE_REALTIME_STATS_FILE)) { $stats = json_decode(file_get_contents(CACHE_REALTIME_STATS_FILE), true); } echo json_encode(['success' => true, 'stats' => $stats]); break; case 'configure_webhooks': $webhooks = json_decode($_POST['webhooks'] ?? '[]', true); file_put_contents(CACHE_WEBHOOKS_FILE, json_encode($webhooks, JSON_PRETTY_PRINT)); echo json_encode(['success' => true]); break; case 'test_cache_encryption': $testData = ['test' => 'data', 'timestamp' => time()]; $encrypted = encryptCacheData($testData); $decrypted = decryptCacheData($encrypted); echo json_encode([ 'success' => true, 'original' => $testData, 'encrypted_length' => strlen($encrypted), 'decrypted' => $decrypted, 'match' => $testData === $decrypted ]); break; case 'list_members': $spaceName = $_POST['space_name'] ?? ''; if (empty($spaceName)) { throw new Exception('Space name is required'); } $useAdminAccess = !empty($_POST['admin_access']); $adminMode = false; $members = []; $pageToken = null; // Helper to extract member data from a membership object $extractMember = function($membership) { $member = $membership->getMember(); $memberData = [ 'membership_name' => $membership->getName(), 'display_name' => 'Unknown User', 'email' => '', 'role' => 'ROLE_MEMBER', 'role_value' => MembershipRole::ROLE_MEMBER, 'type' => 'Human', 'join_date' => '' ]; if ($member) { $memberData['display_name'] = $member->getDisplayName() ?: 'Unknown User'; $memberData['type'] = $member->getType() === ChatUser\Type::HUMAN ? 'Human' : 'Bot'; $memberName = $member->getName(); if (strpos($memberName, 'users/') === 0) { $memberData['email'] = substr($memberName, 6); } } $roleValue = $membership->getRole(); if ($roleValue === MembershipRole::ROLE_MANAGER) { $memberData['role'] = 'ROLE_MANAGER'; $memberData['role_value'] = MembershipRole::ROLE_MANAGER; } if (method_exists($membership, 'getCreateTime') && $membership->getCreateTime()) { $memberData['join_date'] = $membership->getCreateTime()->toDateTime()->format('Y-m-d H:i:s'); } return $memberData; }; // Try regular access first, fallback to admin if needed $regularFailed = false; try { if ($useAdminAccess) { throw new ApiException('Skip to admin access', 7, null); } do { $request = new ListMembershipsRequest(); $request->setParent($spaceName); $request->setPageSize(1000); if ($pageToken) { $request->setPageToken($pageToken); } $response = $chatServiceClient->listMemberships($request); foreach ($response as $membership) { $members[] = $extractMember($membership); } $pageToken = method_exists($response, 'getNextPageToken') ? $response->getNextPageToken() : null; } while ($pageToken); } catch (ApiException $e) { // On PERMISSION_DENIED or explicit admin_access, try admin mode if ($e->getCode() === 7 || stripos($e->getBasicMessage(), 'PERMISSION_DENIED') !== false) { $regularFailed = true; $members = []; } else { throw $e; } } // Admin access fallback or explicit admin_access if ($regularFailed || ($useAdminAccess && empty($members))) { try { $adminKeyData = json_decode(file_get_contents(SERVICE_ACCOUNT_KEY_FILE), true); $adminCredentials = new ServiceAccountCredentials( ['https://www.googleapis.com/auth/chat.admin.memberships.readonly'], $adminKeyData, 'admin@livetimelapse.com.au' ); $adminClient = new ChatServiceClient(['credentials' => $adminCredentials]); $adminMode = true; $members = []; $pageToken = null; do { $request = new ListMembershipsRequest(); $request->setParent($spaceName); $request->setPageSize(1000); $request->setUseAdminAccess(true); $request->setFilter('member.type != "BOT"'); if ($pageToken) { $request->setPageToken($pageToken); } $response = $adminClient->listMemberships($request); foreach ($response as $membership) { $members[] = $extractMember($membership); } $pageToken = method_exists($response, 'getNextPageToken') ? $response->getNextPageToken() : null; } while ($pageToken); } catch (ApiException $adminErr) { // If admin also fails, re-throw original or admin error throw new Exception('Could not list members: ' . $adminErr->getBasicMessage()); } } // Sort: managers first, then alphabetical usort($members, function($a, $b) { if ($a['role'] !== $b['role']) { return $a['role'] === 'ROLE_MANAGER' ? -1 : 1; } return strcasecmp($a['display_name'], $b['display_name']); }); $result = [ 'success' => true, 'members' => $members, 'total' => count($members), 'space_name' => $spaceName ]; if ($adminMode) { $result['admin_mode'] = true; } echo json_encode($result); break; case 'update_member_role': $membershipName = $_POST['membership_name'] ?? ''; $newRole = $_POST['role'] ?? ''; if (empty($membershipName) || empty($newRole)) { throw new Exception('Membership name and role are required'); } $roleValue = $newRole === 'manager' ? MembershipRole::ROLE_MANAGER : MembershipRole::ROLE_MEMBER; $membership = new Membership(); $membership->setName($membershipName); $membership->setRole($roleValue); $updateMask = new FieldMask(); $updateMask->setPaths(['role']); $request = new UpdateMembershipRequest(); $request->setMembership($membership); $request->setUpdateMask($updateMask); $updatedMembership = $chatServiceClient->updateMembership($request); echo json_encode([ 'success' => true, 'message' => 'Role updated to ' . ($newRole === 'manager' ? 'Manager' : 'Member'), 'new_role' => $newRole ]); break; case 'send_message': $spaceName = $_POST['space_name'] ?? ''; $messageText = $_POST['message_text'] ?? ''; if (empty($spaceName)) { throw new Exception('Space name is required'); } if (empty($messageText)) { throw new Exception('Message text is required'); } if (strlen($messageText) > 4096) { throw new Exception('Message exceeds 4096 character limit'); } $message = new Message(); $message->setText($messageText); $request = new CreateMessageRequest(); $request->setParent($spaceName); $request->setMessage($message); $createdMessage = $chatServiceClient->createMessage($request); echo json_encode([ 'success' => true, 'message' => 'Message sent successfully', 'message_id' => $createdMessage->getName() ]); break; case 'delete_space': $spaceName = $_POST['space_name'] ?? ''; if (empty($spaceName)) { throw new Exception('Space name is required'); } $request = new DeleteSpaceRequest(); $request->setName($spaceName); $chatServiceClient->deleteSpace($request); // Remove from cache if (file_exists(CACHE_FILE)) { $cacheContent = file_get_contents(CACHE_FILE); $cacheData = json_decode($cacheContent, true); if ($cacheData && isset($cacheData['spaces'])) { $cacheData['spaces'] = array_values(array_filter($cacheData['spaces'], function($s) use ($spaceName) { return $s['name'] !== $spaceName; })); $cacheData['total_spaces'] = count($cacheData['spaces']); file_put_contents(CACHE_FILE, json_encode($cacheData, JSON_PRETTY_PRINT)); } } echo json_encode([ 'success' => true, 'message' => 'Space deleted successfully' ]); break; // ===== TAGGING SYSTEM ===== case 'get_tags': $tags = []; if (file_exists(TAGS_FILE)) { $tags = json_decode(file_get_contents(TAGS_FILE), true) ?: []; } $spaceTags = []; if (file_exists(SPACE_TAGS_FILE)) { $spaceTags = json_decode(file_get_contents(SPACE_TAGS_FILE), true) ?: []; } echo json_encode(['success' => true, 'tags' => $tags, 'space_tags' => $spaceTags]); break; case 'save_tag': $tagData = json_decode($_POST['tag_data'] ?? '{}', true); if (empty($tagData['name'])) throw new Exception('Tag name is required'); $tags = []; if (file_exists(TAGS_FILE)) { $tags = json_decode(file_get_contents(TAGS_FILE), true) ?: []; } if (empty($tagData['id'])) { $tagData['id'] = 'tag_' . uniqid(); $tags[] = $tagData; } else { foreach ($tags as &$t) { if ($t['id'] === $tagData['id']) { $t['name'] = $tagData['name']; $t['color'] = $tagData['color'] ?? '#4285f4'; $t['icon'] = $tagData['icon'] ?? 'fa-tag'; break; } } unset($t); } file_put_contents(TAGS_FILE, json_encode($tags, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'tag' => $tagData]); break; case 'delete_tag': $tagId = $_POST['tag_id'] ?? ''; if (empty($tagId)) throw new Exception('Tag ID is required'); $tags = []; if (file_exists(TAGS_FILE)) { $tags = json_decode(file_get_contents(TAGS_FILE), true) ?: []; } $tags = array_values(array_filter($tags, function($t) use ($tagId) { return $t['id'] !== $tagId; })); file_put_contents(TAGS_FILE, json_encode($tags, JSON_PRETTY_PRINT), LOCK_EX); // Remove tag from all spaces $spaceTags = []; if (file_exists(SPACE_TAGS_FILE)) { $spaceTags = json_decode(file_get_contents(SPACE_TAGS_FILE), true) ?: []; } foreach ($spaceTags as $spaceId => &$stags) { $stags = array_values(array_filter($stags, function($t) use ($tagId) { return $t !== $tagId; })); if (empty($stags)) unset($spaceTags[$spaceId]); } unset($stags); file_put_contents(SPACE_TAGS_FILE, json_encode($spaceTags, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'message' => 'Tag deleted']); break; case 'tag_space': $spaceId = $_POST['space_id'] ?? ''; $tagId = $_POST['tag_id'] ?? ''; if (empty($spaceId) || empty($tagId)) throw new Exception('Space ID and Tag ID required'); $spaceTags = []; if (file_exists(SPACE_TAGS_FILE)) { $spaceTags = json_decode(file_get_contents(SPACE_TAGS_FILE), true) ?: []; } if (!isset($spaceTags[$spaceId])) $spaceTags[$spaceId] = []; if (!in_array($tagId, $spaceTags[$spaceId])) { $spaceTags[$spaceId][] = $tagId; } file_put_contents(SPACE_TAGS_FILE, json_encode($spaceTags, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'message' => 'Tag assigned']); break; case 'untag_space': $spaceId = $_POST['space_id'] ?? ''; $tagId = $_POST['tag_id'] ?? ''; if (empty($spaceId) || empty($tagId)) throw new Exception('Space ID and Tag ID required'); $spaceTags = []; if (file_exists(SPACE_TAGS_FILE)) { $spaceTags = json_decode(file_get_contents(SPACE_TAGS_FILE), true) ?: []; } if (isset($spaceTags[$spaceId])) { $spaceTags[$spaceId] = array_values(array_filter($spaceTags[$spaceId], function($t) use ($tagId) { return $t !== $tagId; })); if (empty($spaceTags[$spaceId])) unset($spaceTags[$spaceId]); } file_put_contents(SPACE_TAGS_FILE, json_encode($spaceTags, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'message' => 'Tag removed']); break; case 'get_space_tags': $spaceId = $_POST['space_id'] ?? ''; $spaceTags = []; if (file_exists(SPACE_TAGS_FILE)) { $spaceTags = json_decode(file_get_contents(SPACE_TAGS_FILE), true) ?: []; } echo json_encode(['success' => true, 'tags' => $spaceTags[$spaceId] ?? []]); break; // ===== AUDIT TRAIL ===== case 'log_audit_event': $auditAction = $_POST['audit_action'] ?? ''; $target = $_POST['target'] ?? ''; $targetName = $_POST['target_name'] ?? ''; $details = json_decode($_POST['details'] ?? '{}', true) ?: []; $event = [ 'id' => 'evt_' . uniqid(), 'timestamp' => date('c'), 'user' => $currentImpersonatedUser, 'action' => $auditAction, 'target' => $target, 'target_name' => $targetName, 'details' => $details, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '' ]; $auditLog = []; if (file_exists(AUDIT_LOG_FILE)) { $auditLog = json_decode(file_get_contents(AUDIT_LOG_FILE), true) ?: []; } $auditLog[] = $event; // Rotate if file is too large (> 5MB worth of entries, approx 10000) if (count($auditLog) > 10000) { $archiveFile = __DIR__ . '/data/audit_log_' . date('Ym') . '.json'; file_put_contents($archiveFile, json_encode(array_slice($auditLog, 0, 5000), JSON_PRETTY_PRINT), LOCK_EX); $auditLog = array_slice($auditLog, 5000); } file_put_contents(AUDIT_LOG_FILE, json_encode($auditLog, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'event_id' => $event['id']]); break; case 'get_audit_log': $auditLog = []; if (file_exists(AUDIT_LOG_FILE)) { $auditLog = json_decode(file_get_contents(AUDIT_LOG_FILE), true) ?: []; } usort($auditLog, function($a, $b) { return strcmp($b['timestamp'] ?? '', $a['timestamp'] ?? ''); }); $page = max(1, intval($_POST['page'] ?? 1)); $limit = min(100, max(10, intval($_POST['limit'] ?? 50))); $offset = ($page - 1) * $limit; echo json_encode([ 'success' => true, 'entries' => array_slice($auditLog, $offset, $limit), 'total' => count($auditLog), 'page' => $page, 'total_pages' => ceil(count($auditLog) / $limit) ]); break; case 'clear_audit_log': file_put_contents(AUDIT_LOG_FILE, '[]', LOCK_EX); echo json_encode(['success' => true, 'message' => 'Audit log cleared']); break; // ===== GOVERNANCE ===== case 'run_governance_scan': $scriptPath = __DIR__ . '/scripts/runGovernanceScan.php'; $logPath = __DIR__ . '/logs/governance-scan.log'; exec("php " . escapeshellarg($scriptPath) . " > " . escapeshellarg($logPath) . " 2>&1 &"); echo json_encode(['success' => true, 'message' => 'Governance scan started']); break; case 'get_governance_results': $results = []; if (file_exists(GOVERNANCE_CACHE_FILE)) { $results = json_decode(file_get_contents(GOVERNANCE_CACHE_FILE), true) ?: []; } echo json_encode(['success' => true, 'results' => $results]); break; case 'save_governance_rules': $rules = json_decode($_POST['rules'] ?? '[]', true); if (!is_array($rules)) throw new Exception('Invalid rules format'); file_put_contents(GOVERNANCE_RULES_FILE, json_encode($rules, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'message' => 'Rules saved']); break; // ===== TEMPLATES ===== case 'list_templates': $templates = []; if (file_exists(SPACE_TEMPLATES_FILE)) { $templates = json_decode(file_get_contents(SPACE_TEMPLATES_FILE), true) ?: []; } echo json_encode(['success' => true, 'templates' => $templates]); break; case 'save_template': $templateData = json_decode($_POST['template'] ?? '{}', true); if (empty($templateData['name'])) throw new Exception('Template name is required'); $templates = []; if (file_exists(SPACE_TEMPLATES_FILE)) { $templates = json_decode(file_get_contents(SPACE_TEMPLATES_FILE), true) ?: []; } if (empty($templateData['id'])) { $templateData['id'] = 'tpl_' . uniqid(); $templateData['created_by'] = $currentImpersonatedUser; $templateData['created_at'] = date('c'); $templates[] = $templateData; } else { foreach ($templates as &$t) { if ($t['id'] === $templateData['id']) { $t['name'] = $templateData['name']; $t['description'] = $templateData['description'] ?? ''; $t['config'] = $templateData['config'] ?? []; break; } } unset($t); } file_put_contents(SPACE_TEMPLATES_FILE, json_encode($templates, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'template' => $templateData]); break; case 'delete_template': $templateId = $_POST['template_id'] ?? ''; $templates = []; if (file_exists(SPACE_TEMPLATES_FILE)) { $templates = json_decode(file_get_contents(SPACE_TEMPLATES_FILE), true) ?: []; } $templates = array_values(array_filter($templates, function($t) use ($templateId) { return ($t['id'] ?? '') !== $templateId; })); file_put_contents(SPACE_TEMPLATES_FILE, json_encode($templates, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'message' => 'Template deleted']); break; case 'create_space_from_template': $templateId = $_POST['template_id'] ?? ''; $spaceName = $_POST['space_name'] ?? ''; $spaceDescription = $_POST['space_description'] ?? ''; $addMembers = json_decode($_POST['add_members'] ?? '[]', true) ?: []; if (empty($spaceName)) throw new Exception('Space name is required'); // Create the space $space = new Space(); $space->setDisplayName($spaceName); if (!empty($spaceDescription)) { $space->setSpaceDetails(new \Google\Apps\Chat\V1\Space\SpaceDetails()); } $space->setSpaceType(SpaceType::SPACE); $createRequest = new CreateSpaceRequest(); $createRequest->setSpace($space); $createdSpace = $chatServiceClient->createSpace($createRequest); $createdSpaceName = $createdSpace->getName(); // Add members if specified $memberResults = []; foreach ($addMembers as $memberEmail) { try { $membership = new Membership(); $user = new ChatUser(); $user->setName('users/' . $memberEmail); $membership->setMember($user); $memberReq = new CreateMembershipRequest(); $memberReq->setParent($createdSpaceName); $memberReq->setMembership($membership); $chatServiceClient->createMembership($memberReq); $memberResults[] = ['email' => $memberEmail, 'success' => true]; } catch (Exception $me) { $memberResults[] = ['email' => $memberEmail, 'success' => false, 'error' => $me->getMessage()]; } usleep(100000); } echo json_encode([ 'success' => true, 'message' => 'Space created successfully', 'space_name' => $createdSpaceName, 'member_results' => $memberResults ]); break; case 'save_space_as_template': $spaceId = $_POST['space_id'] ?? ''; $templateName = $_POST['template_name'] ?? ''; if (empty($templateName)) throw new Exception('Template name is required'); // Get space data from cache $spaces = getCachedData(); $spaceData = null; if ($spaces) { foreach ($spaces as $s) { if (($s['space_id'] ?? '') === $spaceId || ($s['name'] ?? '') === $spaceId) { $spaceData = $s; break; } } } $template = [ 'id' => 'tpl_' . uniqid(), 'name' => $templateName, 'description' => 'Created from: ' . ($spaceData['display_name'] ?? $spaceId), 'config' => [ 'spaceType' => $spaceData['type'] ?? 'Space', 'displayName' => '{{project_name}}', 'description' => $spaceData['description'] ?? '', 'tags' => [], 'addMembers' => $spaceData['users'] ?? [] ], 'created_by' => $currentImpersonatedUser, 'created_at' => date('c') ]; $templates = []; if (file_exists(SPACE_TEMPLATES_FILE)) { $templates = json_decode(file_get_contents(SPACE_TEMPLATES_FILE), true) ?: []; } $templates[] = $template; file_put_contents(SPACE_TEMPLATES_FILE, json_encode($templates, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'template' => $template]); break; // ===== BULK MEMBER MANAGEMENT ===== case 'bulk_add_member': $spaceName = $_POST['space_name'] ?? ''; $userEmail = $_POST['user_email'] ?? ''; $role = $_POST['role'] ?? 'MEMBER'; if (empty($spaceName) || empty($userEmail)) throw new Exception('Space name and user email required'); // Create membership $membership = new Membership(); $user = new ChatUser(); $user->setName('users/' . $userEmail); $membership->setMember($user); if ($role === 'MANAGER') { $membership->setRole(MembershipRole::ROLE_MANAGER); } else { $membership->setRole(MembershipRole::ROLE_MEMBER); } $memberReq = new CreateMembershipRequest(); $memberReq->setParent($spaceName); $memberReq->setMembership($membership); $chatServiceClient->createMembership($memberReq); echo json_encode(['success' => true, 'message' => "Added $userEmail to space"]); break; case 'bulk_remove_member': $spaceName = $_POST['space_name'] ?? ''; $userEmail = $_POST['user_email'] ?? ''; if (empty($spaceName) || empty($userEmail)) throw new Exception('Space name and user email required'); // Find membership first $listReq = new ListMembershipsRequest(); $listReq->setParent($spaceName); $listReq->setPageSize(500); $memberships = $chatServiceClient->listMemberships($listReq); $membershipName = null; foreach ($memberships as $m) { $member = $m->getMember(); if ($member && stripos($member->getName(), $userEmail) !== false) { $membershipName = $m->getName(); break; } } if (!$membershipName) throw new Exception("User $userEmail not found in space"); $deleteReq = new DeleteMembershipRequest(); $deleteReq->setName($membershipName); $chatServiceClient->deleteMembership($deleteReq); echo json_encode(['success' => true, 'message' => "Removed $userEmail from space"]); break; // ===== SPACE COMPARISON ===== case 'get_spaces_comparison': $spaceIds = json_decode($_POST['space_ids'] ?? '[]', true) ?: []; if (count($spaceIds) < 2 || count($spaceIds) > 4) { throw new Exception('Select 2-4 spaces to compare'); } $spaces = getCachedData(); $comparisonData = []; foreach ($spaceIds as $sid) { foreach ($spaces as $s) { if (($s['space_id'] ?? '') === $sid) { // Try to get member list from cache $members = getCachedMembers($sid); $memberEmails = []; if ($members) { foreach ($members as $m) { $memberEmails[] = $m['email'] ?? $m['name'] ?? ''; } } $comparisonData[] = [ 'space_id' => $s['space_id'], 'name' => $s['name'] ?? '', 'display_name' => $s['display_name'] ?? '', 'type' => $s['type'] ?? '', 'description' => $s['description'] ?? '', 'member_count' => $s['member_count'] ?? 0, 'users' => $s['users'] ?? [], 'members' => $memberEmails, 'threaded' => $s['threaded'] ?? false, 'external_user_allowed' => $s['external_user_allowed'] ?? false, 'create_time' => $s['create_time'] ?? '', 'last_active_time' => $s['last_active_time'] ?? '' ]; break; } } } echo json_encode(['success' => true, 'spaces' => $comparisonData]); break; // ===== SCHEDULER ===== case 'get_scheduler_config': $config = ['tasks' => [], 'alerts' => [], 'history' => []]; if (file_exists(SCHEDULER_CONFIG_FILE)) { $config = json_decode(file_get_contents(SCHEDULER_CONFIG_FILE), true) ?: $config; } echo json_encode(['success' => true, 'config' => $config]); break; case 'save_scheduler_task': $taskData = json_decode($_POST['task'] ?? '{}', true); if (empty($taskData['name'])) throw new Exception('Task name required'); $config = ['tasks' => [], 'alerts' => [], 'history' => []]; if (file_exists(SCHEDULER_CONFIG_FILE)) { $config = json_decode(file_get_contents(SCHEDULER_CONFIG_FILE), true) ?: $config; } if (empty($taskData['id'])) { $taskData['id'] = 'task_' . uniqid(); $taskData['created_at'] = date('c'); $config['tasks'][] = $taskData; } else { foreach ($config['tasks'] as &$t) { if ($t['id'] === $taskData['id']) { $t = array_merge($t, $taskData); break; } } unset($t); } file_put_contents(SCHEDULER_CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true, 'task_id' => $taskData['id']]); break; case 'delete_scheduler_task': $taskId = $_POST['task_id'] ?? ''; $config = ['tasks' => [], 'alerts' => [], 'history' => []]; if (file_exists(SCHEDULER_CONFIG_FILE)) { $config = json_decode(file_get_contents(SCHEDULER_CONFIG_FILE), true) ?: $config; } $config['tasks'] = array_values(array_filter($config['tasks'], function($t) use ($taskId) { return ($t['id'] ?? '') !== $taskId; })); file_put_contents(SCHEDULER_CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true]); break; case 'toggle_scheduler_task': $taskId = $_POST['task_id'] ?? ''; $enabled = ($_POST['enabled'] ?? '1') === '1'; $config = ['tasks' => [], 'alerts' => [], 'history' => []]; if (file_exists(SCHEDULER_CONFIG_FILE)) { $config = json_decode(file_get_contents(SCHEDULER_CONFIG_FILE), true) ?: $config; } foreach ($config['tasks'] as &$t) { if (($t['id'] ?? '') === $taskId) { $t['enabled'] = $enabled; break; } } unset($t); file_put_contents(SCHEDULER_CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT), LOCK_EX); echo json_encode(['success' => true]); break; case 'run_scheduler_task_now': $taskId = $_POST['task_id'] ?? ''; $scriptPath = __DIR__ . '/scripts/runScheduler.php'; $logPath = __DIR__ . '/logs/scheduler-run.log'; exec("php " . escapeshellarg($scriptPath) . " " . escapeshellarg($taskId) . " > " . escapeshellarg($logPath) . " 2>&1 &"); echo json_encode(['success' => true, 'message' => 'Task started']); break; default: throw new Exception('Invalid action'); } } catch (Exception $e) { error_log("Error in action '$action': " . $e->getMessage()); echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; } // New helper functions for enhanced features function handleExport($format) { $spaces = getCachedData(); if (!$spaces) { $spaces = warmCache(); } // Apply filters $spaces = applyFilters($spaces); switch ($format) { case 'csv': header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="spaces_export_' . date('Y-m-d') . '.csv"'); $output = fopen('php://output', 'w'); fputcsv($output, ['Space ID', 'Display Name', 'Type', 'Created', 'Members', 'Threaded', 'External Users', 'Users']); foreach ($spaces as $space) { fputcsv($output, [ $space['space_id'], $space['display_name'], $space['type'], $space['create_time'], $space['member_count'], $space['threaded'] ? 'Yes' : 'No', $space['external_user_allowed'] ? 'Yes' : 'No', implode(', ', $space['users']) ]); } fclose($output); break; case 'json': header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="spaces_export_' . date('Y-m-d') . '.json"'); echo json_encode($spaces, JSON_PRETTY_PRINT); break; } } function applyFilters($spaces) { global $filterType, $filterDateFrom, $filterDateTo, $filterMembersMin, $filterMembersMax; global $filterThreaded, $filterExternal, $filterUser, $filterMembership, $showDMs; $filtered = []; foreach ($spaces as $space) { // DM filter if (!$showDMs && $space['type'] === 'Direct Message') { continue; } // Type filter if ($filterType !== 'all' && $space['type'] !== $filterType) { continue; } // Date filter if ($filterDateFrom && $space['create_time'] !== 'Unknown') { $spaceDate = strtotime($space['create_time']); $fromDate = strtotime($filterDateFrom); if ($spaceDate < $fromDate) { continue; } } if ($filterDateTo && $space['create_time'] !== 'Unknown') { $spaceDate = strtotime($space['create_time']); $toDate = strtotime($filterDateTo . ' 23:59:59'); if ($spaceDate > $toDate) { continue; } } // Member count filter if ($filterMembersMin > 0 && $space['member_count'] < $filterMembersMin) { continue; } if ($filterMembersMax > 0 && $space['member_count'] > $filterMembersMax) { continue; } // Threaded filter if ($filterThreaded === 'yes' && !$space['threaded']) { continue; } if ($filterThreaded === 'no' && $space['threaded']) { continue; } // External users filter if ($filterExternal === 'yes' && !$space['external_user_allowed']) { continue; } if ($filterExternal === 'no' && $space['external_user_allowed']) { continue; } // User filter if ($filterUser !== 'all' && !in_array($filterUser, $space['users'])) { continue; } // Membership filter (for current impersonated user) global $currentImpersonatedUser; if ($filterMembership !== 'all') { $isMember = in_array($currentImpersonatedUser, $space['users']); if ($filterMembership === 'member' && !$isMember) { continue; } if ($filterMembership === 'not_member' && $isMember) { continue; } } $filtered[] = $space; } return $filtered; } function getAnalyticsData() { // Check cache first if (file_exists(ANALYTICS_CACHE_FILE)) { $cache = json_decode(file_get_contents(ANALYTICS_CACHE_FILE), true); if ($cache && isset($cache['timestamp']) && (time() - $cache['timestamp']) < 3600) { return $cache['data']; } } $spaces = getCachedData(); if (!$spaces) { $spaces = warmCache(); } $analytics = [ 'total_spaces' => count($spaces), 'space_types' => [], 'creation_timeline' => [], 'member_distribution' => [], 'feature_usage' => [ 'threaded' => 0, 'external_users' => 0 ], 'user_participation' => [], 'top_spaces_by_members' => [] ]; // Process spaces for analytics foreach ($spaces as $space) { // Space types $type = $space['type']; if (!isset($analytics['space_types'][$type])) { $analytics['space_types'][$type] = 0; } $analytics['space_types'][$type]++; // Creation timeline if ($space['create_time'] !== 'Unknown') { $month = date('Y-m', strtotime($space['create_time'])); if (!isset($analytics['creation_timeline'][$month])) { $analytics['creation_timeline'][$month] = 0; } $analytics['creation_timeline'][$month]++; } // Member distribution $memberCount = $space['member_count']; $bucket = 'Unknown'; if ($memberCount == 0) { $bucket = '0'; } elseif ($memberCount <= 5) { $bucket = '1-5'; } elseif ($memberCount <= 10) { $bucket = '6-10'; } elseif ($memberCount <= 20) { $bucket = '11-20'; } elseif ($memberCount <= 50) { $bucket = '21-50'; } else { $bucket = '50+'; } if (!isset($analytics['member_distribution'][$bucket])) { $analytics['member_distribution'][$bucket] = 0; } $analytics['member_distribution'][$bucket]++; // Feature usage if ($space['threaded']) { $analytics['feature_usage']['threaded']++; } if ($space['external_user_allowed']) { $analytics['feature_usage']['external_users']++; } // User participation foreach ($space['users'] as $user) { if (!isset($analytics['user_participation'][$user])) { $analytics['user_participation'][$user] = 0; } $analytics['user_participation'][$user]++; } // Top spaces by members if ($memberCount > 0) { $analytics['top_spaces_by_members'][] = [ 'name' => $space['display_name'], 'members' => $memberCount ]; } } // Sort top spaces usort($analytics['top_spaces_by_members'], function($a, $b) { return $b['members'] - $a['members']; }); $analytics['top_spaces_by_members'] = array_slice($analytics['top_spaces_by_members'], 0, 10); // Sort timeline ksort($analytics['creation_timeline']); // Cache the results $cacheData = [ 'timestamp' => time(), 'data' => $analytics ]; file_put_contents(ANALYTICS_CACHE_FILE, json_encode($cacheData)); return $analytics; } function loadSpaceTemplates() { if (!file_exists(TEMPLATES_FILE)) { // Create default templates $defaultTemplates = [ 'project' => [ 'name' => 'Project Space', 'description' => 'Standard project collaboration space', 'type' => SpaceType::SPACE, 'threaded' => true, 'external_user_allowed' => false, 'members' => ['ben@livetimelapse.com.au', 'kerri@livetimelapse.com.au'] ], 'client' => [ 'name' => 'Client Collaboration', 'description' => 'External client collaboration space', 'type' => SpaceType::SPACE, 'threaded' => true, 'external_user_allowed' => true, 'members' => ['ben@livetimelapse.com.au', 'paige@livetimelapse.com.au'] ], 'team' => [ 'name' => 'Team Chat', 'description' => 'Internal team group chat', 'type' => SpaceType::GROUP_CHAT, 'threaded' => false, 'external_user_allowed' => false, 'members' => IMPERSONATED_USER_EMAILS ] ]; file_put_contents(TEMPLATES_FILE, json_encode($defaultTemplates, JSON_PRETTY_PRINT)); return $defaultTemplates; } return json_decode(file_get_contents(TEMPLATES_FILE), true); } // API Request Handling - Handle before HTML output if (isset($_GET['api'])) { header('Content-Type: application/json'); try { $action = $_GET['api'] ?? ''; switch ($action) { case 'cache_stats': $stats = getCacheStatistics(); echo json_encode(['success' => true, 'data' => $stats]); break; case 'cache_analytics': $analytics = generateCacheAnalytics(); echo json_encode(['success' => true, 'data' => $analytics]); break; case 'clear_cache': $result = clearAllEnhancedCaches(); echo json_encode(['success' => $result]); break; default: echo json_encode(['success' => false, 'error' => 'Unknown API action: ' . $action]); } } catch (Exception $e) { echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; // Stop processing and return JSON only } // Data fetching and refresh logic - MUST happen before HTML output to allow redirects $forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1'; if ($forceRefresh) { // Instead of running cache warming synchronously, redirect to background refresh system error_log("Force refresh requested - redirecting to background refresh system"); // Build redirect URL with current parameters (minus refresh) $redirectParams = $_GET; unset($redirectParams['refresh']); $baseUrl = 'refresh-cache.php?auto=1'; if (!empty($redirectParams)) { $baseUrl .= '&return_to=' . urlencode($_SERVER['PHP_SELF'] . '?' . http_build_query($redirectParams)); } else { $baseUrl .= '&return_to=' . urlencode($_SERVER['PHP_SELF']); } header('Location: ' . $baseUrl); exit; } // HTML Header - Start output ?> TLC Google Chat Space Manager

Searching All Spaces

Scanning spaces...

TLC Google Chat Space Manager by Sunshine AI

Viewing all spaces | Acting as:

Act as:
'#4285f4', // Google Blue 'ben' => '#0f9d58', // Google Green 'kerri' => '#f4b400', // Google Yellow 'paige' => '#db4437', // Google Red 'adam' => '#673ab7', // Purple 'reece' => '#00acc1', // Cyan 'billy' => '#ff6f00' // Orange ]; foreach (IMPERSONATED_USER_EMAILS as $email): $username = explode('@', $email)[0]; $isActive = $email === $currentImpersonatedUser; $color = $userColors[$username] ?? '#757575'; $initials = strtoupper(substr($username, 0, 1)); if ($username === 'admin') $initials = 'A'; $displayName = ucfirst($username); // Capitalize first letter ?>
CACHE_EXPIRY) { $cacheStatusText = 'Cache Expired'; $cacheStatusClass = 'warning'; } else { $hoursUntilExpiry = round((CACHE_EXPIRY - $cacheAge) / 3600, 1); $cacheStatusText = 'Cache expires in ' . $hoursUntilExpiry . 'h'; $cacheStatusClass = 'active'; } ?>

Advanced Filters

Presets:

Analytics Dashboard

0): ?> 0): ?> $sortBy != 'name_asc' ? $sortBy : null, 'pagesize' => $pageSize != DEFAULT_PAGE_SIZE ? $pageSize : null, 'show_dms' => $showDMs ? '1' : null, // Preserve advanced filter parameters when clearing search 'filter_type' => $filterType != 'all' ? $filterType : null, 'date_from' => !empty($filterDateFrom) ? $filterDateFrom : null, 'date_to' => !empty($filterDateTo) ? $filterDateTo : null, 'members_min' => $filterMembersMin > 0 ? $filterMembersMin : null, 'members_max' => $filterMembersMax > 0 ? $filterMembersMax : null, 'filter_threaded' => $filterThreaded != 'all' ? $filterThreaded : null, 'filter_external' => $filterExternal != 'all' ? $filterExternal : null, 'filter_user' => $filterUser != 'all' ? $filterUser : null, 'filter_membership' => $filterMembership != 'all' ? $filterMembership : null, 'filter_tags' => !empty($filterTags) ? $filterTags : null ])); ?> Clear $searchTerm, 'page' => $currentPage > 1 ? $currentPage : null, 'pagesize' => $pageSize != DEFAULT_PAGE_SIZE ? $pageSize : null, 'sort' => $sortBy != 'name_asc' ? $sortBy : null, 'show_dms' => $showDMs ? '1' : null, // Preserve advanced filter parameters during refresh 'filter_type' => $filterType != 'all' ? $filterType : null, 'date_from' => !empty($filterDateFrom) ? $filterDateFrom : null, 'date_to' => !empty($filterDateTo) ? $filterDateTo : null, 'members_min' => $filterMembersMin > 0 ? $filterMembersMin : null, 'members_max' => $filterMembersMax > 0 ? $filterMembersMax : null, 'filter_threaded' => $filterThreaded != 'all' ? $filterThreaded : null, 'filter_external' => $filterExternal != 'all' ? $filterExternal : null, 'filter_user' => $filterUser != 'all' ? $filterUser : null, 'filter_membership' => $filterMembership != 'all' ? $filterMembership : null, 'filter_tags' => !empty($filterTags) ? $filterTags : null, 'refresh' => '1' ])); ?> Refresh Cache
Quick Join:
Searching for spaces containing "" | | Showing spaces
1) { $html = ''; return $html; } return ''; } // Multi-sort comparison function function compareSpaces($a, $b, $sortRule) { switch ($sortRule) { case 'name_desc': return strcasecmp($b['display_name'], $a['display_name']); case 'name_asc': return strcasecmp($a['display_name'], $b['display_name']); case 'created_desc': return strcmp($b['create_time'], $a['create_time']); case 'created_asc': return strcmp($a['create_time'], $b['create_time']); case 'lastactive_desc': $a_time = isset($a['last_active_time']) ? ($a['last_active_time'] ?? '') : ''; $b_time = isset($b['last_active_time']) ? ($b['last_active_time'] ?? '') : ''; return strcmp($b_time, $a_time); case 'lastactive_asc': $a_time = isset($a['last_active_time']) ? ($a['last_active_time'] ?? '') : ''; $b_time = isset($b['last_active_time']) ? ($b['last_active_time'] ?? '') : ''; return strcmp($a_time, $b_time); case 'members_desc': $a_count = (isset($a['member_count']) && $a['member_count'] >= 0) ? $a['member_count'] : 0; $b_count = (isset($b['member_count']) && $b['member_count'] >= 0) ? $b['member_count'] : 0; return $b_count - $a_count; case 'members_asc': $a_count = (isset($a['member_count']) && $a['member_count'] >= 0) ? $a['member_count'] : 0; $b_count = (isset($b['member_count']) && $b['member_count'] >= 0) ? $b['member_count'] : 0; return $a_count - $b_count; case 'type_space': $order = ['Space' => 0, 'Group Chat' => 1, 'Direct Message' => 2]; return ($order[$a['type']] ?? 3) - ($order[$b['type']] ?? 3); case 'type_dm': $order = ['Direct Message' => 0, 'Group Chat' => 1, 'Space' => 2]; return ($order[$a['type']] ?? 3) - ($order[$b['type']] ?? 3); case 'type_group': $order = ['Group Chat' => 0, 'Space' => 1, 'Direct Message' => 2]; return ($order[$a['type']] ?? 3) - ($order[$b['type']] ?? 3); case 'external_first': return ($b['external_user_allowed'] ? 1 : 0) - ($a['external_user_allowed'] ? 1 : 0); case 'external_last': return ($a['external_user_allowed'] ? 1 : 0) - ($b['external_user_allowed'] ? 1 : 0); case 'has_description': $a_has = !empty($a['description']) ? 0 : 1; $b_has = !empty($b['description']) ? 0 : 1; return $a_has - $b_has; case 'no_description': $a_has = empty($a['description']) ? 0 : 1; $b_has = empty($b['description']) ? 0 : 1; return $a_has - $b_has; default: return strcasecmp($a['display_name'], $b['display_name']); } } try { // Debug: Log before cache check file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] === Starting main try block ===\n", FILE_APPEND); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] About to call getCachedData with forceRefresh=" . ($forceRefresh ? 'true' : 'false') . "\n", FILE_APPEND); // Try to get cached data first $spaces = getCachedData($forceRefresh); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] getCachedData returned: " . ($spaces === null ? 'NULL' : count($spaces) . ' spaces') . "\n", FILE_APPEND); $fromCache = false; $totalPages = 1; $allSpaces = []; $totalScanned = 0; if ($spaces !== null) { $fromCache = true; $allSpaces = $spaces; $totalScanned = count($allSpaces); error_log("Using cached data with " . $totalScanned . " spaces"); file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] Using cached data with " . $totalScanned . " spaces\n", FILE_APPEND); // Ensure all spaces have required fields for backward compatibility foreach ($allSpaces as &$space) { if (!isset($space['users'])) { $space['users'] = [IMPERSONATED_USER_EMAILS[0]]; } if (!isset($space['member_count'])) { $space['member_count'] = 0; } if (!isset($space['threaded'])) { $space['threaded'] = false; } if (!isset($space['external_user_allowed'])) { $space['external_user_allowed'] = false; } if (!isset($space['last_active_time'])) { $space['last_active_time'] = null; } } // Sort cached data based on selected criteria (multi-sort support) usort($allSpaces, function($a, $b) use ($sortRules) { foreach ($sortRules as $rule) { $cmp = compareSpaces($a, $b, $rule); if ($cmp !== 0) return $cmp; } return 0; }); // Apply DM filter first if needed $filteredByDM = []; $dmCount = 0; foreach ($allSpaces as $space) { if ($space['type'] === 'Direct Message') { $dmCount++; if ($showDMs) { $filteredByDM[] = $space; } } else { $filteredByDM[] = $space; } } $allSpaces = $filteredByDM; // Apply advanced filters $allSpaces = applyFilters($allSpaces); // Always set totalSpaces $totalSpaces = count($allSpaces); // Filter cached spaces if searching if (!empty($searchTerm)) { $filteredSpaces = []; foreach ($allSpaces as $space) { if (stripos($space['display_name'], $searchTerm) !== false) { $filteredSpaces[] = $space; } } $spaces = $filteredSpaces; } else { // Apply pagination when not searching $totalPages = ceil($totalSpaces / $pageSize); $currentPage = min($currentPage, $totalPages); $offset = ($currentPage - 1) * $pageSize; $spaces = array_slice($allSpaces, $offset, $pageSize); } } else { // No cache or expired, fetch from API file_put_contents(__DIR__ . '/search-debug.log', "[" . date('Y-m-d H:i:s') . "] No cache or expired - fetching fresh data from API\n", FILE_APPEND); echo '
Testing authentication...
'; ob_flush(); flush(); // Test authentication first $testClient = createChatClient(IMPERSONATED_USER_EMAILS[0]); if (!testAuthentication($testClient)) { echo ''; throw new Exception("Authentication failed. Please check your Domain-Wide Delegation setup. Error: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested."); } echo ''; ob_flush(); flush(); $allSpacesMap = []; // Add error handling for API initialization $apiError = false; $errorMessage = ''; // If we got here without cache, something went wrong // This should not happen since refresh is handled before HTML output error_log("ERROR: No cache available and refresh not handled properly"); echo ''; throw new Exception("No cached data available. Please refresh the page."); } // Update search stats if (!empty($searchTerm)) { $sortInfo = ''; switch ($sortBy) { case 'name_desc': $sortInfo = ' - sorted by name Z→A'; break; case 'created_desc': $sortInfo = ' - sorted by newest first'; break; case 'created_asc': $sortInfo = ' - sorted by oldest first'; break; case 'members_desc': $sortInfo = ' - sorted by most members'; break; case 'members_asc': $sortInfo = ' - sorted by least members'; break; case 'lastactive_desc': $sortInfo = ' - sorted by most recent activity'; break; case 'lastactive_asc': $sortInfo = ' - sorted by least recent activity'; break; case 'name_asc': $sortInfo = ' - sorted by name A→Z'; break; } echo ''; } else { $cacheInfo = ''; if ($fromCache) { $cacheAge = getCacheAge(); if ($cacheAge < 60) { $cacheInfo = ' [cached ' . $cacheAge . ' seconds ago]'; } else { $cacheMinutes = round($cacheAge / 60); $cacheInfo = ' [cached ' . $cacheMinutes . ' minute' . ($cacheMinutes !== 1 ? 's' : '') . ' ago]'; } } else { $cacheInfo = ' [fresh data - cache updated]'; } $totalSpaces = isset($allSpaces) ? count($allSpaces) : $totalScanned; $totalPages = empty($searchTerm) ? ceil($totalSpaces / $pageSize) : 1; $startNum = ($currentPage - 1) * $pageSize + 1; $endNum = min($currentPage * $pageSize, $totalSpaces); $sortInfo = ''; switch ($sortBy) { case 'name_desc': $sortInfo = ' (sorted by name Z→A)'; break; case 'created_desc': $sortInfo = ' (sorted by newest first)'; break; case 'created_asc': $sortInfo = ' (sorted by oldest first)'; break; case 'members_desc': $sortInfo = ' (sorted by most members)'; break; case 'members_asc': $sortInfo = ' (sorted by least members)'; break; } $dmFilterInfo = ''; if (!$showDMs && isset($dmCount) && $dmCount > 0) { $dmFilterInfo = ' (' . $dmCount . ' DMs hidden)'; } echo '
Showing spaces ' . $startNum . '-' . $endNum . ' of ' . $totalSpaces . ' total' . $sortInfo . $dmFilterInfo . $cacheInfo . '
'; } // Show pagination at top if (empty($searchTerm) && $totalPages > 1) { echo generatePagination($currentPage, $totalPages, $pageSize, $searchTerm, $sortBy); } // Display table echo ''; // Load tags data for rendering $allTagDefs = []; if (file_exists(TAGS_FILE)) { $allTagDefs = json_decode(file_get_contents(TAGS_FILE), true) ?: []; } $allSpaceTags = []; if (file_exists(SPACE_TAGS_FILE)) { $allSpaceTags = json_decode(file_get_contents(SPACE_TAGS_FILE), true) ?: []; } $tagDefMap = []; foreach ($allTagDefs as $td) { $tagDefMap[$td['id']] = $td; } // Filter by tag if requested if (!empty($filterTags)) { $spaces = array_values(array_filter($spaces, function($s) use ($allSpaceTags, $filterTags) { $sid = $s['space_id'] ?? ''; return isset($allSpaceTags[$sid]) && in_array($filterTags, $allSpaceTags[$sid]); })); } if (empty($spaces)) { echo ''; } else { foreach ($spaces as $index => $space) { $spaceUrl = 'https://chat.google.com/room/' . $space['space_id']; $displayNameHtml = htmlspecialchars($space['display_name']); if (!empty($searchTerm)) { $displayNameHtml = preg_replace('/(' . preg_quote(htmlspecialchars($searchTerm), '/') . ')/i', '$1', $displayNameHtml); } $usersHtml = ''; if (isset($space['users'])) { $userNames = array_map(function($email) { return ucfirst(explode('@', $email)[0]); }, $space['users']); $usersHtml = implode(', ', $userNames); } $memberCountHtml = ''; $cacheIndicator = ''; // Check if member data is cached $membersCached = getCachedMembers($space['space_id']) !== null; if ($membersCached) { $cacheIndicator = ' 📦'; } if ($space['type'] === 'Direct Message') { $memberCountHtml = 'DM'; } elseif (isset($space['member_count']) && $space['member_count'] > 0) { $escapedName = htmlspecialchars($space['name'], ENT_QUOTES); $memberCountHtml = '' . $space['member_count'] . '' . $cacheIndicator; } else { $escapedName = htmlspecialchars($space['name'], ENT_QUOTES); $memberCountHtml = '-'; } $features = []; if (isset($space['threaded']) && $space['threaded']) { $features[] = '💬'; } if (isset($space['external_user_allowed']) && $space['external_user_allowed']) { $features[] = '🌐'; } $featuresHtml = !empty($features) ? implode(' ', $features) : '-'; $isMember = isset($space['users']) && in_array($currentImpersonatedUser, $space['users']); $editableClass = $isMember ? 'editable' : 'not-editable'; $editableOnclick = $isMember ? 'onclick="editName(' . $index . ')"' : ''; $editableTitle = $isMember ? 'Click to edit' : 'Join this space first to edit'; echo ''; } } echo '
Space ID Display Name Type Description Tags Members Features Our Users Created Last Active Link Join Leave Actions
No spaces found' . (!empty($searchTerm) ? ' matching "' . htmlspecialchars($searchTerm) . '"' : '') . '
' . (strlen($space['space_id']) > 15 ? substr($space['space_id'], 0, 12) . '...' : htmlspecialchars($space['space_id'])) . '
' . $displayNameHtml . '
' . htmlspecialchars($space['type']) . ' ' . htmlspecialchars($space['description']) . ' '; // Render tag pills $spaceTagIds = $allSpaceTags[$space['space_id']] ?? []; if (!empty($spaceTagIds)) { foreach ($spaceTagIds as $stid) { if (isset($tagDefMap[$stid])) { $t = $tagDefMap[$stid]; echo '' . htmlspecialchars($t['name']) . ''; } } } $escapedSpaceIdForTag = htmlspecialchars($space['space_id'], ENT_QUOTES); $escapedDisplayNameForTag = htmlspecialchars(addslashes($space['display_name']), ENT_QUOTES); echo ''; echo ' ' . $memberCountHtml . ' ' . $featuresHtml . ' ' . htmlspecialchars($usersHtml) . ' ' . (strtotime($space['create_time']) ? date('M j, Y', strtotime($space['create_time'])) : htmlspecialchars($space['create_time'])) . ' ' . (!empty($space['last_active_time']) && strtotime($space['last_active_time']) ? date('M j, Y', strtotime($space['last_active_time'])) : '-') . ' Open '; // Join column - only show if not a member and not a DM if (!$isMember && $space['type'] !== 'Direct Message') { $escapedSpaceName = htmlspecialchars($space['name'], ENT_QUOTES); echo ''; } else { echo '-'; } echo ' '; // Leave column - only show if IS a member and not a DM if ($isMember && $space['type'] !== 'Direct Message') { $escapedSpaceName = htmlspecialchars($space['name'], ENT_QUOTES); echo ''; } else { echo '-'; } echo ' '; // Actions column: Message and Delete buttons $escapedSpaceName = htmlspecialchars($space['name'], ENT_QUOTES); $escapedDisplayName = htmlspecialchars(addslashes($space['display_name']), ENT_QUOTES); if ($isMember && $space['type'] !== 'Direct Message') { echo ''; } echo '
'; // Show pagination at bottom echo generatePagination($currentPage, isset($totalPages) ? $totalPages : 1, $pageSize, $searchTerm, $sortBy); } catch (Exception $e) { $errorMessage = $e->getMessage(); $errorDetails = ''; // Check if it's an authentication/authorization error if (strpos($errorMessage, 'unauthorized') !== false || strpos($errorMessage, '401') !== false) { $errorDetails = '

🔐 Authentication Error - Domain-Wide Delegation Required

The error suggests that Domain-Wide Delegation is not properly configured. Please follow these steps:

  1. Enable Domain-Wide Delegation:
  2. Authorize in Google Workspace Admin:
  3. Required OAuth Scopes:
    https://www.googleapis.com/auth/chat.spaces
    https://www.googleapis.com/auth/chat.memberships
    https://www.googleapis.com/auth/chat.messages
    https://www.googleapis.com/auth/chat.messages.create

Note: Changes may take up to 15 minutes to propagate.

'; } elseif (strpos($errorMessage, '403') !== false) { $errorDetails = '

🚫 Permission Denied

This error typically means:

'; } echo '
Error: ' . htmlspecialchars($errorMessage) . ' ' . $errorDetails . '
Technical Details
' . htmlspecialchars(print_r($e, true)) . '
'; } ?>