rootFolderId = $rootFolderId; // If clientSecret is null, treat first param as a developer token if ($clientSecret === null) { $this->accessToken = $clientIdOrToken; error_log("Box: Using provided developer token"); } else { // Use OAuth client credentials $this->clientId = $clientIdOrToken; $this->clientSecret = $clientSecret; $this->accessToken = $this->getOAuthToken(); } } /** * Get OAuth 2.0 access token using JWT authentication * Box uses JWT with RSA private key signing */ private function getOAuthToken() { // Load Box config for JWT details $configPath = __DIR__ . '/../Box-config.json'; if (!file_exists($configPath)) { error_log("Box JWT: Config file not found at " . $configPath); return null; } $config = json_decode(file_get_contents($configPath), true); $privateKey = $config['boxAppSettings']['appAuth']['privateKey'] ?? null; $passphrase = $config['boxAppSettings']['appAuth']['passphrase'] ?? null; $publicKeyID = $config['boxAppSettings']['appAuth']['publicKeyID'] ?? null; $enterpriseID = $config['enterpriseID'] ?? null; if (!$privateKey || !$publicKeyID || !$enterpriseID) { error_log("Box JWT: Missing required JWT configuration"); return null; } // Create JWT assertion $header = [ 'alg' => 'RS256', 'typ' => 'JWT', 'kid' => $publicKeyID ]; $claims = [ 'iss' => $this->clientId, 'sub' => $enterpriseID, 'box_sub_type' => 'enterprise', 'aud' => 'https://api.box.com/oauth2/token', 'jti' => bin2hex(random_bytes(16)), 'exp' => time() + 60 // Valid for 60 seconds ]; // Encode JWT $headerEncoded = $this->base64UrlEncode(json_encode($header)); $claimsEncoded = $this->base64UrlEncode(json_encode($claims)); $signatureInput = $headerEncoded . '.' . $claimsEncoded; // Sign with private key $privateKeyResource = openssl_pkey_get_private($privateKey, $passphrase); if (!$privateKeyResource) { error_log("Box JWT: Failed to load private key"); return null; } openssl_sign($signatureInput, $signature, $privateKeyResource, OPENSSL_ALGO_SHA256); $signatureEncoded = $this->base64UrlEncode($signature); $jwt = $signatureInput . '.' . $signatureEncoded; // Request token with JWT assertion $tokenUrl = 'https://api.box.com/oauth2/token'; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $tokenUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt, 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret ]), CURLOPT_SSL_VERIFYPEER => false ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); error_log("Box JWT Token Request: HTTP " . $httpCode); if ($httpCode == 200) { $data = json_decode($response, true); $token = $data['access_token'] ?? null; if ($token) { error_log("Box JWT: Token received successfully"); return $token; } } error_log("Box JWT Failed: " . $response); return null; } /** * Base64 URL encode (for JWT) */ private function base64UrlEncode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } /** * Create a subfolder for a campaign */ public function createCampaignFolder($campaignId, $campaignName) { $folderName = $campaignId . '_' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $campaignName); $data = [ 'name' => $folderName, 'parent' => ['id' => $this->rootFolderId] ]; $response = $this->apiRequest('POST', '/folders', $data); if ($response['success']) { return [ 'success' => true, 'folder_id' => $response['data']['id'] ?? null, 'folder_name' => $folderName ]; } // Check if folder already exists if (isset($response['data']['status']) && $response['data']['status'] == 409) { // Folder exists - find it $existingFolder = $this->findFolderByName($folderName); if ($existingFolder) { return [ 'success' => true, 'folder_id' => $existingFolder['id'], 'folder_name' => $folderName, 'existed' => true ]; } } return [ 'success' => false, 'error' => $response['error'] ?? 'Failed to create folder' ]; } /** * Upload file to Box with unique ID and metadata */ public function uploadFile($localPath, $originalFilename, $uniqueId, $targetFolderId, $metadata = []) { if (!file_exists($localPath)) { return [ 'success' => false, 'error' => 'File not found: ' . $localPath ]; } // Build new filename with unique ID $pathInfo = pathinfo($originalFilename); $newFilename = $pathInfo['filename'] . '_' . $uniqueId . '.' . $pathInfo['extension']; // Upload file $uploadUrl = 'https://upload.box.com/api/2.0/files/content'; $ch = curl_init(); $postFields = [ 'attributes' => json_encode([ 'name' => $newFilename, 'parent' => ['id' => $targetFolderId] ]), 'file' => new \CURLFile($localPath, mime_content_type($localPath), $newFilename) ]; curl_setopt_array($ch, [ CURLOPT_URL => $uploadUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postFields, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $this->accessToken ], CURLOPT_SSL_VERIFYPEER => false ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode == 201) { $data = json_decode($response, true); $fileId = $data['entries'][0]['id'] ?? null; // If metadata provided, set custom metadata if ($fileId && !empty($metadata)) { $this->setCustomMetadata($fileId, $metadata); } return [ 'success' => true, 'file_id' => $fileId, 'filename' => $newFilename, 'unique_id' => $uniqueId, 'box_url' => 'https://app.box.com/file/' . $fileId ]; } return [ 'success' => false, 'error' => 'Upload failed', 'http_code' => $httpCode, 'response' => $response ]; } /** * Set custom metadata on Box file using metadata template * Stores full DAM metadata JSON in "Ferrero-DAM-Metadata" field */ public function setCustomMetadata($fileId, $metadata) { // TEST: Send minimal string value to verify template works $testValue = "Test metadata - Asset ID: " . ($metadata['asset_id'] ?? 'Unknown'); // Apply metadata template to file // Template: https://oliver-na.app.box.com/master/metadata/templates/ferrerodammetadata $templateScope = 'enterprise'; $templateKey = 'ferrerodammetadata'; // Try with simple string value first $metadataValues = [ 'damMetadata' => $testValue // Start with camelCase (Box standard) ]; $endpoint = "/files/{$fileId}/metadata/{$templateScope}/{$templateKey}"; error_log("Box Metadata: Attempting POST to " . $endpoint); error_log("Box Metadata: Values: " . json_encode($metadataValues)); $response = $this->apiRequest('POST', $endpoint, $metadataValues); if ($response['success']) { error_log("Box Metadata: SUCCESS! Template applied"); return true; } error_log("Box Metadata: POST failed with: " . ($response['raw'] ?? 'Unknown')); // Try different field name variations if first attempt failed $fieldVariations = ['DAMMetadata', 'DAM-Metadata', 'dam_metadata', 'metadata']; foreach ($fieldVariations as $fieldName) { $metadataValues = [$fieldName => $testValue]; $response = $this->apiRequest('POST', $endpoint, $metadataValues); if ($response['success']) { error_log("Box Metadata: Success with field name: " . $fieldName); return true; } } // All attempts failed error_log("Box Metadata: All template attempts failed"); // POST to create metadata instance on file $endpoint = "/files/{$fileId}/metadata/{$templateScope}/{$templateKey}"; $response = $this->apiRequest('POST', $endpoint, $metadataValues); // If template doesn't exist or metadata already applied, try PUT to update if (!$response['success'] && $response['http_code'] == 409) { error_log("Box Metadata: Template instance exists, updating instead"); $response = $this->apiRequest('PUT', $endpoint, [ [ 'op' => 'replace', 'path' => '/Ferrero-DAM-Metadata', 'value' => $metadataJson ] ]); } if (!$response['success']) { error_log("Box Metadata Error: " . ($response['raw'] ?? 'Unknown')); error_log("Box Metadata: Falling back to file description"); // Fallback: Use file description $data = [ 'description' => 'DAM Asset - ID: ' . ($metadata['asset_id'] ?? 'Unknown') ]; $this->apiRequest('PUT', "/files/{$fileId}", $data); } return $response['success']; } /** * Find folder by name in root */ private function findFolderByName($folderName) { $response = $this->apiRequest('GET', "/folders/{$this->rootFolderId}/items"); if ($response['success'] && isset($response['data']['entries'])) { foreach ($response['data']['entries'] as $item) { if ($item['type'] === 'folder' && $item['name'] === $folderName) { return $item; } } } return null; } /** * Generic Box API request */ private function apiRequest($method, $endpoint, $data = null) { $url = $this->baseUrl . $endpoint; $ch = curl_init(); $options = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $this->accessToken, 'Content-Type: application/json' ], CURLOPT_SSL_VERIFYPEER => false ]; if ($data !== null && in_array($method, ['POST', 'PUT', 'PATCH'])) { $options[CURLOPT_POSTFIELDS] = json_encode($data); } curl_setopt_array($ch, $options); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return [ 'success' => $httpCode >= 200 && $httpCode < 300, 'http_code' => $httpCode, 'data' => json_decode($response, true), 'raw' => $response ]; } /** * Test Box connection */ public function testConnection() { $response = $this->apiRequest('GET', '/folders/' . $this->rootFolderId); // Log detailed response for debugging error_log("Box API Response: HTTP " . $response['http_code']); if (!$response['success']) { error_log("Box API Error: " . ($response['raw'] ?? 'No response')); } return [ 'success' => $response['success'], 'folder_name' => $response['data']['name'] ?? 'Unknown', 'http_code' => $response['http_code'], 'error' => $response['raw'] ?? null ]; } }