config = require __DIR__ . '/config.php'; $this->boxConfig = $this->config['box']; // Load Box JWT configuration if (!file_exists($this->boxConfig['jwt_config_path'])) { throw new Exception('Box JWT configuration file not found'); } $jwtConfigJson = file_get_contents($this->boxConfig['jwt_config_path']); $this->jwtConfig = json_decode($jwtConfigJson, true); if (!$this->jwtConfig) { throw new Exception('Invalid Box JWT configuration'); } } /** * Generate JWT assertion for Box authentication */ private function generateJWT() { $authConfig = $this->jwtConfig['boxAppSettings']['appAuth']; $keyId = $authConfig['publicKeyID']; $privateKey = $authConfig['privateKey']; $passphrase = $authConfig['passphrase']; // Decrypt private key with passphrase $privateKeyResource = openssl_pkey_get_private($privateKey, $passphrase); if (!$privateKeyResource) { throw new Exception('Failed to decrypt private key'); } // Export private key in PEM format openssl_pkey_export($privateKeyResource, $pemKey); $claims = [ 'iss' => $this->jwtConfig['boxAppSettings']['clientID'], 'sub' => $this->boxConfig['enterprise_id'], 'box_sub_type' => 'enterprise', 'aud' => 'https://api.box.com/oauth2/token', 'jti' => bin2hex(random_bytes(32)), 'exp' => time() + 45 ]; $header = [ 'alg' => 'RS256', 'typ' => 'JWT', 'kid' => $keyId ]; return JWT::encode($claims, $pemKey, 'RS256', null, $header); } /** * Authenticate with Box and get access token */ public function authenticate() { // Check if we have a valid cached token if ($this->accessToken && $this->tokenExpiry && time() < $this->tokenExpiry) { return $this->accessToken; } try { $assertion = $this->generateJWT(); $postData = [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion, 'client_id' => $this->jwtConfig['boxAppSettings']['clientID'], 'client_secret' => $this->jwtConfig['boxAppSettings']['clientSecret'] ]; $ch = curl_init($this->boxConfig['oauth_url']); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($postData), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/x-www-form-urlencoded' ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { throw new Exception('Box authentication failed: ' . $response); } $data = json_decode($response, true); if (!isset($data['access_token'])) { throw new Exception('No access token in response'); } $this->accessToken = $data['access_token']; $this->tokenExpiry = time() + ($data['expires_in'] ?? 3600) - 300; // 5 min buffer return $this->accessToken; } catch (Exception $e) { throw new Exception('Box authentication error: ' . $e->getMessage()); } } /** * Make an authenticated API request to Box */ private function apiRequest($endpoint, $method = 'GET', $data = null) { $token = $this->authenticate(); $url = $this->boxConfig['api_base_url'] . $endpoint; $ch = curl_init($url); $headers = [ 'Authorization: Bearer ' . $token, 'Content-Type: application/json' ]; curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_CUSTOMREQUEST => $method ]); if ($data !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 404) { return ['error' => 'Not found', 'httpCode' => 404]; } if ($httpCode !== 200) { return ['error' => 'API request failed', 'httpCode' => $httpCode, 'response' => $response]; } return json_decode($response, true); } /** * Get folder information including path collection */ public function getFolderInfo($folderId) { return $this->apiRequest('/folders/' . $folderId); } /** * Get folder contents (files and folders) */ public function getFolderContents($folderId, $limit = 1000) { return $this->apiRequest('/folders/' . $folderId . '/items?limit=' . $limit); } /** * Get the folder that is two levels up from the given folder * Returns array with 'id' and 'name' or null if not found */ public function getGrandparentFolder($folderId) { $folderInfo = $this->getFolderInfo($folderId); if (isset($folderInfo['error'])) { return ['error' => $folderInfo['error']]; } $pathCollection = $folderInfo['path_collection']['entries'] ?? []; $totalCount = count($pathCollection); // Two levels up means we need at least 3 folders in the path // (root folder is always index 0) if ($totalCount < 3) { return ['error' => 'Folder is not deep enough (needs at least 2 parent folders)']; } // Index for two levels up // If path is [0: Root, 1: Level1, 2: Level2, 3: Level3, 4: Current] // Two levels up from Current (index 4) is Level2 (index 2) // That's totalCount - 3 $grandparentIndex = $totalCount - 3; if ($grandparentIndex < 0) { $grandparentIndex = 0; // Fallback to root } $grandparent = $pathCollection[$grandparentIndex]; return [ 'id' => $grandparent['id'], 'name' => $grandparent['name'] ]; } /** * Validate a Box ID and return comprehensive information */ public function validateBoxId($boxId) { try { // Get folder info $folderInfo = $this->getFolderInfo($boxId); if (isset($folderInfo['error'])) { return [ 'valid' => false, 'error' => $folderInfo['error'] ]; } // Get grandparent folder (two levels up) $grandparent = $this->getGrandparentFolder($boxId); // Get folder contents $contents = $this->getFolderContents($boxId); if (isset($contents['error'])) { return [ 'valid' => false, 'error' => $contents['error'] ]; } // Parse contents into files and folders $files = []; $folders = []; foreach ($contents['entries'] ?? [] as $item) { $itemData = [ 'id' => $item['id'], 'name' => $item['name'], 'type' => $item['type'] ]; if ($item['type'] === 'folder') { $folders[] = $itemData; } else { $files[] = $itemData; } } return [ 'valid' => true, 'folderInfo' => [ 'id' => $folderInfo['id'], 'name' => $folderInfo['name'] ], 'grandparent' => $grandparent, 'contents' => [ 'total' => $contents['total_count'] ?? 0, 'folders' => $folders, 'files' => $files ] ]; } catch (Exception $e) { return [ 'valid' => false, 'error' => 'Validation error: ' . $e->getMessage() ]; } } }