Structure: Campaign Number → CAMPAIGN_ASSETS → SUPPLIED_ASSETS - Changed to fetch parent, then parent's parent (proper two levels up) - Previous logic was using path_collection which showed CAMPAIGN_ASSETS - Now correctly retrieves the campaign number folder - Uses two separate API calls to traverse up the hierarchy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
324 lines
9.8 KiB
PHP
324 lines
9.8 KiB
PHP
<?php
|
|
/**
|
|
* Box API Service
|
|
* Handles Box API authentication and operations using JWT
|
|
*/
|
|
|
|
use Firebase\JWT\JWT;
|
|
|
|
class BoxService {
|
|
private $config;
|
|
private $boxConfig;
|
|
private $jwtConfig;
|
|
private $accessToken;
|
|
private $tokenExpiry;
|
|
|
|
public function __construct() {
|
|
$this->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;
|
|
}
|
|
}
|