- Set up PHP application with Composer and JWT library - Implemented SSO authentication with local dev mode - Created Box API service for folder validation - Built two-column form interface (form + preview) - Added real-time Box ID validation with AJAX - Integrated webhook submission with status response - Auto-populate Master Campaign Number from Box folder hierarchy - Responsive design with Montserrat font and black/yellow theme 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
279 lines
8.5 KiB
PHP
279 lines
8.5 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 folder that is two levels up from the given folder
|
|
* Returns array with 'id' and 'name' or null if not found
|
|
*/
|
|
public function getGrandparentFolder($folderId) {
|
|
$folderInfo = $this->getFolderInfo($folderId);
|
|
|
|
if (isset($folderInfo['error'])) {
|
|
return ['error' => $folderInfo['error']];
|
|
}
|
|
|
|
$pathCollection = $folderInfo['path_collection']['entries'] ?? [];
|
|
$totalCount = count($pathCollection);
|
|
|
|
// Two levels up means we need at least 3 folders in the path
|
|
// (root folder is always index 0)
|
|
if ($totalCount < 3) {
|
|
return ['error' => 'Folder is not deep enough (needs at least 2 parent folders)'];
|
|
}
|
|
|
|
// Index for two levels up
|
|
// If path is [0: Root, 1: Level1, 2: Level2, 3: Level3, 4: Current]
|
|
// Two levels up from Current (index 4) is Level2 (index 2)
|
|
// That's totalCount - 3
|
|
$grandparentIndex = $totalCount - 3;
|
|
|
|
if ($grandparentIndex < 0) {
|
|
$grandparentIndex = 0; // Fallback to root
|
|
}
|
|
|
|
$grandparent = $pathCollection[$grandparentIndex];
|
|
|
|
return [
|
|
'id' => $grandparent['id'],
|
|
'name' => $grandparent['name']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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']
|
|
];
|
|
}
|
|
|
|
// Get grandparent folder (two levels up)
|
|
$grandparent = $this->getGrandparentFolder($boxId);
|
|
|
|
// Get folder contents
|
|
$contents = $this->getFolderContents($boxId);
|
|
|
|
if (isset($contents['error'])) {
|
|
return [
|
|
'valid' => false,
|
|
'error' => $contents['error']
|
|
];
|
|
}
|
|
|
|
// Parse contents into files and folders
|
|
$files = [];
|
|
$folders = [];
|
|
|
|
foreach ($contents['entries'] ?? [] as $item) {
|
|
$itemData = [
|
|
'id' => $item['id'],
|
|
'name' => $item['name'],
|
|
'type' => $item['type']
|
|
];
|
|
|
|
if ($item['type'] === 'folder') {
|
|
$folders[] = $itemData;
|
|
} else {
|
|
$files[] = $itemData;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'valid' => true,
|
|
'folderInfo' => [
|
|
'id' => $folderInfo['id'],
|
|
'name' => $folderInfo['name']
|
|
],
|
|
'grandparent' => $grandparent,
|
|
'contents' => [
|
|
'total' => $contents['total_count'] ?? 0,
|
|
'folders' => $folders,
|
|
'files' => $files
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'valid' => false,
|
|
'error' => 'Validation error: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
}
|