ferrero-opentext/src/BoxClient.php
DJP 2b42b4e42e Store DAM metadata JSON in database description field + test Box template
Database:
- Store full DAM metadata JSON in description field (5KB limit)
- Includes Box links and upload folder ID
- Full asset metadata preserved

Box Metadata Template Testing:
- Simplified to send just test string first
- Log endpoint and values being sent
- Try 5 field name variations
- Will identify correct field name from logs

Next test will show which field name works for Box template.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 17:09:58 -04:00

399 lines
13 KiB
PHP

<?php
/**
* BoxClient - Handles Box.com API integration for Ferrero workflow
* Uploads assets with metadata and unique ID tracking
*/
class BoxClient
{
private $accessToken;
private $rootFolderId;
private $baseUrl = 'https://api.box.com/2.0';
private $clientId;
private $clientSecret;
public function __construct($clientIdOrToken, $clientSecret = null, $rootFolderId = '348304357505')
{
$this->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
];
}
}