ferrero-opentext/src/BoxClient.php
DJP d8e542a569 Add PostgreSQL database integration and Box metadata template
Database Integration:
- IDGenerator now connects to PostgreSQL (localhost:5433)
- Generates tracking IDs with uniqueness check against master_assets table
- Fallback to random if database unavailable
- Direct PDO connection to ferrero_tracking database

DatabaseClient:
- Stores master assets in PostgreSQL
- Records: tracking_id, opentext_id, Box links, full metadata JSON
- Updates on conflict (upsert pattern)
- Stores box_file_id and box_url for reference

Box Metadata Enhancement:
- Uses Box metadata template API (enterprise/ferreroDAMMetadata)
- Stores full DAM metadata JSON in 'Ferrero-DAM-Metadata' field
- Fallback to file description if template not configured
- Handles template conflicts (updates existing)

Box Upload Results Now Show:
- Unique tracking ID (from database)
- Box file links (clickable)
- Database storage status
- ID source (database_direct, random, etc.)

Complete workflow: DAM → Download → Generate ID → Upload to Box → Store in DB

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 15:39:32 -04:00

368 lines
12 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)
{
$metadataJson = json_encode($metadata);
// Apply metadata template to file
// Template scope: "enterprise" (for enterprise templates) or "global" (for Box templates)
// Template key: the name of your template
$templateScope = 'enterprise'; // Assuming enterprise template
$templateKey = 'ferreroDAMMetadata'; // Template name (may need adjustment)
$metadataValues = [
'Ferrero-DAM-Metadata' => $metadataJson // Store entire JSON in this field
];
// 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
];
}
}