'password123', 'PS02' => 'password456', 'admin' => 'admin123' ]; $users = []; $i = 1; while (true) { // Check both $_ENV and $_SERVER for Hugging Face Space compatibility $username = $_ENV["USER_{$i}_NAME"] ?? $_SERVER["USER_{$i}_NAME"] ?? ''; $password = $_ENV["USER_{$i}_PASSWORD"] ?? $_SERVER["USER_{$i}_PASSWORD"] ?? ''; if (empty($username) || empty($password)) { break; } $users[$username] = $password; $i++; } // If no users found in env, use defaults return empty($users) ? $defaultUsers : $users; } public static function getStorageType() { return $_ENV['STORAGE_TYPE'] ?? $_SERVER['STORAGE_TYPE'] ?? 'github'; } public static function getGitHubConfig() { // Read from environment variables (Hugging Face Space or system environment) $config = [ 'token' => $_ENV['GITHUB_TOKEN'] ?? $_SERVER['GITHUB_TOKEN'] ?? '', 'owner' => $_ENV['GITHUB_OWNER'] ?? $_SERVER['GITHUB_OWNER'] ?? '', 'repo' => $_ENV['GITHUB_REPO'] ?? $_SERVER['GITHUB_REPO'] ?? '', 'branch' => $_ENV['GITHUB_BRANCH'] ?? $_SERVER['GITHUB_BRANCH'] ?? 'main', 'path' => $_ENV['GITHUB_PATH'] ?? $_SERVER['GITHUB_PATH'] ?? 'pages/' ]; // Debug log for Hugging Face Space error_log("GitHub Config Debug - Token present: " . (!empty($config['token']) ? 'YES' : 'NO')); error_log("GitHub Config Debug - Owner: " . $config['owner']); error_log("GitHub Config Debug - Repo: " . $config['repo']); error_log("GitHub Config Debug - Branch: " . $config['branch']); error_log("GitHub Config Debug - Path: " . $config['path']); return $config; } public static function getKVConfig() { return [ 'api_key' => $_ENV['EDGEONE_KV_API_KEY'] ?? $_SERVER['EDGEONE_KV_API_KEY'] ?? '', 'secret_key' => $_ENV['EDGEONE_KV_SECRET_KEY'] ?? $_SERVER['EDGEONE_KV_SECRET_KEY'] ?? '', 'zone_id' => $_ENV['EDGEONE_KV_ZONE_ID'] ?? $_SERVER['EDGEONE_KV_ZONE_ID'] ?? '', 'namespace' => $_ENV['EDGEONE_KV_NAMESPACE'] ?? $_SERVER['EDGEONE_KV_NAMESPACE'] ?? 'vvvebjs', 'endpoint' => $_ENV['EDGEONE_KV_ENDPOINT'] ?? $_SERVER['EDGEONE_KV_ENDPOINT'] ?? 'https://edgeone.tencentcloudapi.com' ]; } public static function getCurrentUser() { // Check session first if (session_status() === PHP_SESSION_NONE) { session_start(); } if (isset($_SESSION['username'])) { return $_SESSION['username']; } // Fallback to Basic Auth return $_SERVER['PHP_AUTH_USER'] ?? 'anonymous'; } public static function getUserPath($username = null) { if ($username === null) { $username = self::getCurrentUser(); } // Sanitize username for safe path usage $safeUsername = preg_replace('/[^a-zA-Z0-9_-]/', '_', $username); return "users/{$safeUsername}/"; } // Add method to check if running on Hugging Face Space public static function isHuggingFaceSpace() { return isset($_SERVER['SPACE_ID']) || isset($_ENV['SPACE_ID']) || strpos($_SERVER['HTTP_HOST'] ?? '', '.hf.space') !== false; } // Debug method to show all environment variables (for debugging) public static function debugEnvironment() { $debug = [ 'is_hf_space' => self::isHuggingFaceSpace(), 'space_id' => $_SERVER['SPACE_ID'] ?? $_ENV['SPACE_ID'] ?? 'not_set', 'host' => $_SERVER['HTTP_HOST'] ?? 'unknown', 'github_vars' => [ 'token_set' => !empty($_ENV['GITHUB_TOKEN'] ?? $_SERVER['GITHUB_TOKEN']), 'owner' => $_ENV['GITHUB_OWNER'] ?? $_SERVER['GITHUB_OWNER'] ?? 'not_set', 'repo' => $_ENV['GITHUB_REPO'] ?? $_SERVER['GITHUB_REPO'] ?? 'not_set', 'branch' => $_ENV['GITHUB_BRANCH'] ?? $_SERVER['GITHUB_BRANCH'] ?? 'not_set', 'path' => $_ENV['GITHUB_PATH'] ?? $_SERVER['GITHUB_PATH'] ?? 'not_set' ] ]; error_log("Environment Debug: " . json_encode($debug)); return $debug; } } // External storage handlers class KVStorage { private $config; private $userPath; public function __construct($config) { $this->config = $config; $this->userPath = StorageConfig::getUserPath(); } public function save($key, $content) { if (empty($this->config['api_key'])) { error_log("KV Save Error: API key is missing"); return false; } if (empty($this->config['zone_id']) || empty($this->config['namespace'])) { error_log("KV Save Error: Zone ID or namespace is missing"); return false; } // Add user path to key $userKey = $this->userPath . $key; // Use correct EdgeOne KV API endpoints $url = $this->config['endpoint']; $params = [ 'Action' => 'CreatePurgeTask', // 这需要根据实际的EdgeOne KV API调整 'Version' => '2022-09-01', 'Region' => 'ap-beijing', 'ZoneId' => $this->config['zone_id'], 'Namespace' => $this->config['namespace'], 'Key' => $userKey, 'Value' => base64_encode($content), 'TTL' => 86400 // 24 hours TTL ]; error_log("KV Save Debug: Attempting to save key '$userKey' to namespace '{$this->config['namespace']}'"); $result = $this->makeRequest($url, $params); if ($result) { error_log("KV Save Success: Key saved successfully"); return true; } else { error_log("KV Save Error: API request failed"); return false; } } public function get($key) { if (empty($this->config['api_key'])) { error_log("KV Get Error: API key is missing"); return false; } // Add user path to key $userKey = $this->userPath . $key; $url = $this->config['endpoint']; $params = [ 'Action' => 'DescribeOriginGroup', // 这需要根据实际的EdgeOne KV API调整 'Version' => '2022-09-01', 'Region' => 'ap-beijing', 'ZoneId' => $this->config['zone_id'], 'Namespace' => $this->config['namespace'], 'Key' => $userKey ]; error_log("KV Get Debug: Attempting to get key '$userKey' from namespace '{$this->config['namespace']}'"); $result = $this->makeRequest($url, $params); if ($result && isset($result['Value'])) { return base64_decode($result['Value']); } error_log("KV Get Error: Key not found or API request failed"); return false; } public function listUserFiles() { if (empty($this->config['api_key'])) return []; $url = $this->config['endpoint']; $params = [ 'Action' => 'DescribeOriginGroups', // 这需要根据实际的EdgeOne KV API调整 'Version' => '2022-09-01', 'Region' => 'ap-beijing', 'ZoneId' => $this->config['zone_id'], 'Namespace' => $this->config['namespace'], 'Prefix' => $this->userPath ]; $result = $this->makeRequest($url, $params); if ($result && isset($result['Keys']) && is_array($result['Keys'])) { $files = []; foreach ($result['Keys'] as $item) { if (isset($item['Key'])) { $relativePath = str_replace($this->userPath, '', $item['Key']); $files[] = [ 'name' => basename($relativePath), 'path' => $relativePath, 'size' => $item['Size'] ?? 0, 'url' => null ]; } } return $files; } return []; } private function makeRequest($url, $params) { // 添加通用的腾讯云API参数 $commonParams = [ 'Timestamp' => time(), 'Nonce' => rand(10000, 99999), 'SecretId' => $this->config['api_key'], 'SignatureMethod' => 'HmacSHA256' ]; $params = array_merge($params, $commonParams); // 生成签名 (这是简化版本,实际需要按照腾讯云API签名规范) ksort($params); $queryString = http_build_query($params); $signature = hash_hmac('sha256', $queryString, $this->config['secret_key'], true); $params['Signature'] = base64_encode($signature); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/x-www-form-urlencoded', 'User-Agent: VvvebJs-WebBuilder/1.0' ]); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { error_log("KV API cURL Error: $error"); return false; } if ($httpCode >= 200 && $httpCode < 300) { $decoded = json_decode($result, true); if (json_last_error() === JSON_ERROR_NONE) { return $decoded; } error_log("KV API JSON Error: " . json_last_error_msg()); } else { error_log("KV API HTTP Error: $httpCode - Response: $result"); } return false; } } class GitHubStorage { private $config; private $actualBranch = null; private $userPath; public function __construct($config) { $this->config = $config; $this->userPath = StorageConfig::getUserPath(); // Auto-detect the actual default branch $this->detectDefaultBranch(); } private function detectDefaultBranch() { if (empty($this->config['token']) || empty($this->config['owner']) || empty($this->config['repo'])) { return; } // Get repository info to find the default branch $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}"; $repoInfo = $this->makeRequest($url, 'GET'); if ($repoInfo && isset($repoInfo['default_branch'])) { $this->actualBranch = $repoInfo['default_branch']; error_log("GitHub: Detected default branch as '{$this->actualBranch}'"); } else { // Fallback: try common branch names $commonBranches = ['main', 'master', 'develop']; foreach ($commonBranches as $branch) { if ($this->checkBranchExists($branch)) { $this->actualBranch = $branch; error_log("GitHub: Found existing branch '{$branch}'"); break; } } } // Update config with detected branch if ($this->actualBranch) { $this->config['branch'] = $this->actualBranch; } } private function checkBranchExists($branchName) { $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/branches/{$branchName}"; $result = $this->makeRequest($url, 'GET'); return $result !== false; } public function save($filename, $content) { error_log("GitHubStorage: Starting save process for $filename"); // Get user path $userPath = $this->getUserPath(); if (!$userPath) { error_log("GitHubStorage: Failed to get user path"); return false; } $filePath = $userPath . '/' . $filename; error_log("GitHubStorage: Full file path: $filePath"); // Ensure user directory exists BEFORE attempting to save if (!$this->ensureUserDirectoryExists($userPath)) { error_log("GitHubStorage: Failed to ensure user directory exists"); return false; } // Wait a moment for GitHub API to propagate the directory creation usleep(500000); // 0.5 seconds // Now attempt to save the file $url = $this->apiUrl . "/contents/" . $filePath; // Check if file already exists to get its SHA $existingFile = $this->makeRequest($url); $sha = null; if ($existingFile !== false && isset($existingFile['sha'])) { $sha = $existingFile['sha']; error_log("GitHubStorage: Found existing file with SHA: $sha"); } else { error_log("GitHubStorage: Creating new file"); } $data = [ 'message' => $sha ? "Update $filename" : "Create $filename", 'content' => base64_encode($content), 'branch' => $this->branch ]; if ($sha) { $data['sha'] = $sha; } error_log("GitHubStorage: Sending save request to GitHub API"); $result = $this->makeRequest($url, 'PUT', $data); if ($result !== false) { error_log("GitHubStorage: File saved successfully"); return true; } else { error_log("GitHubStorage: Failed to save file"); return false; } } private function ensureUserDirectoryExists($userPath) { error_log("Ensuring user directory exists: $userPath"); // Split the path into components $pathParts = explode('/', trim($userPath, '/')); $currentPath = ''; // Create each directory level incrementally foreach ($pathParts as $part) { if (empty($part)) continue; $currentPath .= ($currentPath ? '/' : '') . $part; // Check if this directory level exists $url = $this->apiUrl . "/contents/" . $currentPath; $response = $this->makeRequest($url); if ($response === false) { // Directory doesn't exist, create it with a placeholder file error_log("Creating directory level: $currentPath"); $placeholderFile = $currentPath . '/.gitkeep'; $createUrl = $this->apiUrl . "/contents/" . $placeholderFile; $createData = [ 'message' => "Create directory: $currentPath", 'content' => base64_encode(''), 'branch' => $this->branch ]; $createResponse = $this->makeRequest($createUrl, 'PUT', $createData); if ($createResponse === false) { error_log("Failed to create directory level: $currentPath"); return false; } else { error_log("Successfully created directory level: $currentPath"); } } else { error_log("Directory level already exists: $currentPath"); } } error_log("User directory structure ensured: $userPath"); return true; } public function get($filename) { if (empty($this->config['token'])) return false; // Add user path to filename $userFilename = $this->userPath . $filename; $path = $this->config['path'] . $userFilename; $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}"; $result = $this->makeRequest($url, 'GET'); if ($result && isset($result['content'])) { return base64_decode($result['content']); } return false; } public function listUserFiles() { if (empty($this->config['token'])) return []; $userDir = $this->config['path'] . $this->userPath; $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$userDir}"; error_log("GitHub listUserFiles: Requesting $url"); $result = $this->makeRequest($url, 'GET'); if ($result && is_array($result)) { $files = []; foreach ($result as $item) { if ($item['type'] === 'file') { // Remove user path prefix from filename $relativePath = str_replace($this->userPath, '', $item['path']); $relativePath = str_replace($this->config['path'], '', $relativePath); $files[] = [ 'name' => $item['name'], 'path' => $relativePath, 'size' => $item['size'], 'url' => $item['download_url'] ]; } } error_log("GitHub listUserFiles: Found " . count($files) . " files"); return $files; } else { error_log("GitHub listUserFiles: No files found or directory doesn't exist"); // If directory doesn't exist, return empty array instead of error return []; } } public function delete($filename) { if (empty($this->config['token'])) return false; // Add user path to filename $userFilename = $this->userPath . $filename; $path = $this->config['path'] . $userFilename; $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}"; // Get current file SHA $sha = $this->getFileSHA($path); if (!$sha) return false; $data = [ 'message' => "Delete {$userFilename} via VvvebJs", 'sha' => $sha, 'branch' => $this->config['branch'] ]; $result = $this->makeRequest($url, 'DELETE', $data); return $result !== false; } private function getFileSHA($path) { $url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}"; $result = $this->makeRequest($url, 'GET'); return $result && isset($result['sha']) ? $result['sha'] : null; } private function makeRequest($url, $method = 'GET', $data = null) { $headers = [ 'Authorization: token ' . $this->config['token'], 'User-Agent: VvvebJs-WebBuilder', 'Accept: application/vnd.github.v3+json', 'Content-Type: application/json' ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if ($method === 'PUT') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); if ($data) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } } // Enhanced logging for debugging $debugUrl = preg_replace('/\/contents\/.*/', '/contents/[PATH_HIDDEN]', $url); error_log("GitHub API Request: $method $debugUrl"); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); error_log("GitHub API Response: HTTP $httpCode"); if ($curlError) { error_log("GitHub API cURL Error: $curlError"); return false; } if ($httpCode >= 200 && $httpCode < 300) { $decoded = json_decode($response, true); if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { error_log("GitHub API JSON Decode Error: " . json_last_error_msg()); error_log("GitHub API Raw Response: " . substr($response, 0, 500)); return false; } return $decoded; } else { // Enhanced error logging for debugging $errorInfo = json_decode($response, true); if ($errorInfo && isset($errorInfo['message'])) { error_log("GitHub API Error ($httpCode): " . $errorInfo['message']); if (isset($errorInfo['documentation_url'])) { error_log("GitHub API Documentation: " . $errorInfo['documentation_url']); } } else { error_log("GitHub API Error ($httpCode): " . substr($response, 0, 500)); } // Special handling for 404 errors - they might be expected when checking if files exist if ($httpCode === 404 && $method === 'GET') { error_log("GitHub API: Resource not found (this may be expected when checking file existence)"); } return false; } } } // Storage manager with user isolation class StorageManager { private $kvStorage; private $githubStorage; private $storageType; private $currentUser; public function __construct() { $this->storageType = StorageConfig::getStorageType(); $this->currentUser = StorageConfig::getCurrentUser(); if (in_array($this->storageType, ['kv', 'both'])) { $this->kvStorage = new KVStorage(StorageConfig::getKVConfig()); } if (in_array($this->storageType, ['github', 'both'])) { $this->githubStorage = new GitHubStorage(StorageConfig::getGitHubConfig()); } } public function saveFile($filename, $content) { // Ensure filename is safe and doesn't contain path traversal $filename = $this->sanitizeFilename($filename); error_log("StorageManager saveFile: Starting save for file '$filename'"); error_log("StorageManager saveFile: Storage type is '{$this->storageType}'"); error_log("StorageManager saveFile: Current user is '{$this->currentUser}'"); $success = false; $errors = []; if ($this->githubStorage) { error_log("StorageManager saveFile: Attempting GitHub save..."); $githubSuccess = $this->githubStorage->save($filename, $content); if ($githubSuccess) { error_log("StorageManager saveFile: GitHub save SUCCESS"); $success = true; } else { error_log("StorageManager saveFile: GitHub save FAILED"); $errors[] = "GitHub save failed"; } } else { error_log("StorageManager saveFile: GitHub storage not configured"); } if ($this->kvStorage) { error_log("StorageManager saveFile: Attempting KV save..."); $kvSuccess = $this->kvStorage->save($filename, $content); if ($kvSuccess) { error_log("StorageManager saveFile: KV save SUCCESS"); $success = true; } else { error_log("StorageManager saveFile: KV save FAILED"); $errors[] = "KV save failed"; } } else { error_log("StorageManager saveFile: KV storage not configured"); } if (!$success && !empty($errors)) { error_log("StorageManager saveFile: All saves failed: " . implode(", ", $errors)); } return $success; } public function getFile($filename) { // Ensure filename is safe and doesn't contain path traversal $filename = $this->sanitizeFilename($filename); error_log("StorageManager getFile: Starting load for file '$filename'"); error_log("StorageManager getFile: Storage type is '{$this->storageType}'"); error_log("StorageManager getFile: Current user is '{$this->currentUser}'"); if ($this->githubStorage) { error_log("StorageManager getFile: Attempting GitHub load..."); $content = $this->githubStorage->get($filename); if ($content !== false) { error_log("StorageManager getFile: GitHub load SUCCESS"); return $content; } else { error_log("StorageManager getFile: GitHub load FAILED"); } } else { error_log("StorageManager getFile: GitHub storage not configured"); } if ($this->kvStorage) { error_log("StorageManager getFile: Attempting KV load..."); $content = $this->kvStorage->get($filename); if ($content !== false) { error_log("StorageManager getFile: KV load SUCCESS"); return $content; } else { error_log("StorageManager getFile: KV load FAILED"); } } else { error_log("StorageManager getFile: KV storage not configured"); } error_log("StorageManager getFile: All load attempts failed"); return false; } public function listFiles() { $files = []; if ($this->githubStorage) { $githubFiles = $this->githubStorage->listUserFiles(); $files = array_merge($files, $githubFiles); } if ($this->kvStorage) { $kvFiles = $this->kvStorage->listUserFiles(); $files = array_merge($files, $kvFiles); } return $files; } public function deleteFile($filename) { // Ensure filename is safe and doesn't contain path traversal $filename = $this->sanitizeFilename($filename); $success = false; if ($this->githubStorage) { $success = $this->githubStorage->delete($filename) || $success; } // KV storage delete would need to be implemented based on provider return $success; } public function getCurrentUser() { return $this->currentUser; } public function getUserPath() { return StorageConfig::getUserPath(); } private function sanitizeFilename($filename) { // Remove any path traversal attempts $filename = str_replace(['../', '..\\', '../', '..\\'], '', $filename); // Remove leading slashes $filename = ltrim($filename, '/\\'); // Ensure safe characters only $filename = preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '_', $filename); return $filename; } }