__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();
}
// 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');
}
$members = [];
$pageToken = null;
do {
$request = new ListMembershipsRequest();
$request->setParent($spaceName);
$request->setPageSize(1000);
if ($pageToken) {
$request->setPageToken($pageToken);
}
$response = $chatServiceClient->listMemberships($request);
foreach ($response as $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';
// Extract email from member name (users/email@domain.com)
$memberName = $member->getName();
if (strpos($memberName, 'users/') === 0) {
$memberData['email'] = substr($memberName, 6);
}
}
// Get role
$roleValue = $membership->getRole();
if ($roleValue === MembershipRole::ROLE_MANAGER) {
$memberData['role'] = 'ROLE_MANAGER';
$memberData['role_value'] = MembershipRole::ROLE_MANAGER;
}
// Get join date
if (method_exists($membership, 'getCreateTime') && $membership->getCreateTime()) {
$memberData['join_date'] = $membership->getCreateTime()->toDateTime()->format('Y-m-d H:i:s');
}
$members[] = $memberData;
}
$pageToken = method_exists($response, 'getNextPageToken') ? $response->getNextPageToken() : null;
} while ($pageToken);
// 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']);
});
echo json_encode([
'success' => true,
'members' => $members,
'total' => count($members),
'space_name' => $spaceName
]);
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
?>
CACHE_EXPIRY) {
$cacheStatusText = 'Cache Expired';
$cacheStatusClass = 'warning';
} else {
$hoursUntilExpiry = round((CACHE_EXPIRY - $cacheAge) / 3600, 1);
$cacheStatusText = 'Cache expires in ' . $hoursUntilExpiry . 'h';
$cacheStatusClass = 'active';
}
?>
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 '
Space ID
Display Name
Type
Description
Tags
Members
Features
Our Users
Created
Last Active
Link
Join
Leave
Actions
';
// 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 'No spaces found' . (!empty($searchTerm) ? ' matching "' . htmlspecialchars($searchTerm) . '"' : '') . ' ';
} 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 '
' . (strlen($space['space_id']) > 15 ? substr($space['space_id'], 0, 12) . '...' : htmlspecialchars($space['space_id'])) . '
' . $displayNameHtml . '
Save
Cancel
' . 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 'Join ';
} 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 'Leave ';
} 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 '
';
}
}
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:
Enable Domain-Wide Delegation:
Authorize in Google Workspace Admin:
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:
The service account doesn\'t have the correct scopes authorized in Google Workspace Admin
The impersonated user doesn\'t have Google Chat enabled
The Google Chat API is not enabled in your Google Cloud project
';
}
echo '
Error: ' . htmlspecialchars($errorMessage) . '
' . $errorDetails . '
Technical Details
' . htmlspecialchars(print_r($e, true)) . '
';
}
?>