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 grandparent folder (two levels up) from the given folder * This gets the campaign number folder above CAMPAIGN_ASSETS * Returns array with 'id' and 'name' or error */ public function getGrandparentFolder($folderId) { $folderInfo = $this->getFolderInfo($folderId); if (isset($folderInfo['error'])) { return ['error' => $folderInfo['error']]; } // First get the parent (CAMPAIGN_ASSETS) $parent = $folderInfo['parent'] ?? null; if (!$parent || !isset($parent['id'])) { return ['error' => 'No parent folder found']; } // Now get the parent's parent (the campaign number) $parentInfo = $this->getFolderInfo($parent['id']); if (isset($parentInfo['error'])) { return ['error' => $parentInfo['error']]; } $grandparent = $parentInfo['parent'] ?? null; if (!$grandparent) { return ['error' => 'No grandparent folder found']; } return [ 'id' => $grandparent['id'], 'name' => $grandparent['name'] ]; } /** * Get folder contents recursively (nested folders) */ private function getFolderContentsRecursive($folderId, $depth = 0, $maxDepth = 3) { // Limit recursion depth to prevent infinite loops if ($depth >= $maxDepth) { return []; } $contents = $this->getFolderContents($folderId); if (isset($contents['error'])) { return []; } $result = [ 'files' => [], 'folders' => [] ]; foreach ($contents['entries'] ?? [] as $item) { if ($item['type'] === 'folder') { // Recursively get contents of this folder $subContents = $this->getFolderContentsRecursive($item['id'], $depth + 1, $maxDepth); $folderData = [ 'id' => $item['id'], 'name' => $item['name'], 'type' => 'folder', 'contents' => $subContents ]; $result['folders'][] = $folderData; } else { $result['files'][] = [ 'id' => $item['id'], 'name' => $item['name'], 'type' => $item['type'] ]; } } return $result; } /** * 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'] ]; } // Check if folder name is SUPPLIED_ASSETS if (strtoupper($folderInfo['name']) !== 'SUPPLIED_ASSETS') { return [ 'valid' => false, 'error' => 'Invalid folder. The folder must be named "SUPPLIED_ASSETS". Current folder name: "' . $folderInfo['name'] . '"' ]; } // Get grandparent folder (two levels up) - this is the campaign number $grandparent = $this->getGrandparentFolder($boxId); // Get folder contents recursively $contents = $this->getFolderContentsRecursive($boxId); // Count total items recursively $totalItems = $this->countItemsRecursive($contents); return [ 'valid' => true, 'folderInfo' => [ 'id' => $folderInfo['id'], 'name' => $folderInfo['name'] ], 'grandparent' => $grandparent, 'contents' => [ 'total' => $totalItems, 'folders' => $contents['folders'], 'files' => $contents['files'] ] ]; } catch (Exception $e) { return [ 'valid' => false, 'error' => 'Validation error: ' . $e->getMessage() ]; } } /** * Count total items recursively */ private function countItemsRecursive($contents) { $count = count($contents['files'] ?? []); foreach ($contents['folders'] ?? [] as $folder) { $count++; // Count the folder itself $count += $this->countItemsRecursive($folder['contents'] ?? []); } return $count; } }