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>
368 lines
12 KiB
PHP
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
|
|
];
|
|
}
|
|
}
|