apiClient = $apiClient; $this->targetFolderId = $targetFolderId; } /** * Upload a file to DAM with metadata * Uses PHP's native multipart/form-data with CURLFile * * @param string $filePath Local file path * @param string $folderId Target folder ID (campaign folder) * @param array $metadata Optional metadata to set * @param array $sourceMasterAsset Complete master asset object to copy metadata from * @return array Upload result */ public function uploadFile($filePath, $folderId, $metadata = [], $sourceMasterAsset = null, $testRunner = null) { if (!file_exists($filePath)) { return [ 'success' => false, 'error' => 'File not found: ' . $filePath, 'filename' => basename($filePath) ]; } $filename = basename($filePath); $mimeType = mime_content_type($filePath); // Keep MIME type as detected - Postman uses "image/jpg" not "image/jpeg" // Don't normalize // Get base URL from ApiClient using reflection $baseUrlReflection = new ReflectionProperty($this->apiClient, 'baseUrl'); $baseUrlReflection->setAccessible(true); $baseUrl = $baseUrlReflection->getValue($this->apiClient); // Build URL - Ferrero confirmed: https://ppr.dam.ferrero.com/otmmapi/v6/assets // baseUrl from config: https://ppr.dam.ferrero.com/otmmapi $url = rtrim($baseUrl, '/') . '/v6/assets'; // Verify we're using the correct URL error_log("=== UPLOAD REQUEST START ==="); error_log("CODE VERSION: 2025-10-28-v2 (AFTER STANDALONE FIX)"); error_log("Upload URL: " . $url); error_log("Expected URL: https://ppr.dam.ferrero.com/otmmapi/v6/assets"); error_log("URL Match: " . ($url === 'https://ppr.dam.ferrero.com/otmmapi/v6/assets' ? 'YES' : 'NO')); // Build upload manifest (required by V3 API) $uploadManifest = [ 'upload_manifest' => [ 'master_files' => [ [ 'file' => [ 'file_name' => $filename, 'file_type' => $mimeType ] ] ] ] ]; // Build asset_representation - ALWAYS use full Postman structure // Standalone test proved this structure works! $assetRep = $this->buildAssetRepresentationFromMasterAsset($filename, $sourceMasterAsset); try { $assetRepJson = json_encode($assetRep); $manifestJson = json_encode($uploadManifest); if ($assetRepJson === false || $manifestJson === false) { return [ 'success' => false, 'filename' => $filename, 'error' => 'JSON encoding failed', 'http_code' => 0 ]; } // Build postFields EXACTLY like standalone script $postFields = [ 'asset_representation' => $assetRepJson, 'parent_folder_id' => $folderId, 'manifest' => $manifestJson, 'files' => new \CURLFile($filePath, $mimeType, $filename) ]; } catch (Exception $e) { return [ 'success' => false, 'filename' => $filename, 'error' => 'JSON encoding error: ' . $e->getMessage(), 'http_code' => 0 ]; } // Get authorization header $authHeader = ''; $headersReflection = new ReflectionProperty($this->apiClient, 'headers'); $headersReflection->setAccessible(true); $headers = $headersReflection->getValue($this->apiClient); if (isset($headers['Authorization'])) { $authHeader = $headers['Authorization']; } // Log auth status error_log("Upload: Auth header present: " . (!empty($authHeader) ? 'YES' : 'NO')); if (!empty($authHeader)) { error_log("Upload: Auth header starts with: " . substr($authHeader, 0, 20) . "..."); } // Use curl directly for multipart upload with extended timeout $ch = curl_init(); // Log before curl setup error_log("Upload: Setting up cURL for " . $filename); error_log("Upload: URL = " . $url); error_log("Upload: Folder ID = " . $folderId); // Setup cookie jar to maintain session $cookieFile = sys_get_temp_dir() . '/ferrero_upload_cookies.txt'; // Use EXACT cURL settings from working standalone script curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postFields, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Authorization: Bearer ' . str_replace('Bearer ', '', $authHeader) // Ensure Bearer prefix only once ], CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 60, CURLOPT_CONNECTTIMEOUT => 30, CURLOPT_COOKIEJAR => $cookieFile, CURLOPT_COOKIEFILE => $cookieFile ]); error_log("Upload: Using cookie file: " . $cookieFile); error_log("Upload: Asset Rep JSON length: " . strlen($assetRepJson ?? 'N/A')); error_log("Upload: Manifest JSON length: " . strlen($manifestJson ?? 'N/A')); error_log("Upload: Filename: " . $filename); error_log("Upload: MIME Type: " . $mimeType); error_log("Upload: File exists: " . (file_exists($filePath) ? 'YES' : 'NO')); // Execute with error suppression and timeout handling try { set_time_limit(120); // 2 minutes max for PHP error_log("Upload: About to execute cURL"); $response = @curl_exec($ch); error_log("Upload: cURL executed, HTTP Code will be checked next"); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { return [ 'success' => false, 'filename' => $filename, 'error' => 'CURL error: ' . $curlError, 'http_code' => 0 ]; } if ($response === false) { return [ 'success' => false, 'filename' => $filename, 'error' => 'Upload request failed - no response received', 'http_code' => 0 ]; } if ($httpCode == 201) { $responseData = @json_decode($response, true); return [ 'success' => true, 'asset_id' => $responseData['asset_resource_list']['asset_resource'][0]['asset']['asset_id'] ?? null, 'filename' => $filename, 'http_code' => $httpCode ]; } // Extract detailed error message from API response $errorMsg = 'Upload failed'; if (!empty($response)) { // Limit response size to prevent crashes $limitedResponse = substr($response, 0, 5000); $responseData = @json_decode($limitedResponse, true); if ($responseData && isset($responseData['exception_body'])) { $errorMsg = $responseData['exception_body']['message'] ?? $responseData['exception_body']['debug_message'] ?? 'API Error'; } elseif ($responseData && isset($responseData['error'])) { $errorMsg = $responseData['error']; } } return [ 'success' => false, 'filename' => $filename, 'error' => $errorMsg, 'http_code' => $httpCode, 'response_body' => substr($response ?? '', 0, 1000) // Limit to 1KB ]; } catch (Exception $e) { return [ 'success' => false, 'filename' => $filename, 'error' => 'PHP Exception during upload: ' . $e->getMessage(), 'http_code' => 0 ]; } } /** * Build asset representation from complete master asset object * Copies metadata_model_id, security_policy_list, and all metadata * Updates only the filename */ private function buildAssetRepresentationFromMasterAsset($filename, $masterAsset) { // Simplified structure matching standalone test that worked (1173 bytes) // Only 5 essential fields, not all 17 $assetRep = [ 'asset_resource' => [ 'asset' => [ 'metadata' => [ 'metadata_element_list' => [ [ 'id' => 'FERRERO.FIELD.MKTG.ASSET TYPE', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'type' => 'string', 'value' => 'heroimage' ] ] ], [ 'id' => 'FERRERO.FIELD.FISCAL YEAR', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'type' => 'string', 'value' => '2025/2026' ] ] ], [ 'id' => 'MAIN_LANGUAGES', 'parent_table_id' => 'FERRERO.TABULAR.FIELD.MAIN LANGUAGES', 'type' => 'com.artesia.metadata.MetadataTableField', 'values' => [ [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => 'IT' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ] ], [ 'id' => 'FERRERO.FIELD.ASSETCOMPLIANCE', 'parent_table_id' => 'FERRERO.TABULAR.FIELD.ASSETCOMPLIANCE', 'type' => 'com.artesia.metadata.MetadataTableField', 'values' => [ [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => '-' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ] ], [ 'id' => 'MARKETING_TAG', 'parent_table_id' => 'FERRERO.TABULAR.FIELD.MARKETING.TAG', 'type' => 'com.artesia.metadata.MetadataTableField', 'values' => [ [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => 'Tag' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ] ], [ 'id' => 'FERRERO.MARKET.FIELD.TYPE_VID', 'parent_table_id' => 'FERRERO.TABULAR.VID_STAT_TYPE', 'type' => 'com.artesia.metadata.MetadataTableField', 'values' => [ [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => 'music' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ] ], [ 'id' => 'ARTESIA.FIELD.ASSET DESCRIPTION', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'type' => 'string', 'value' => 'Test Description' ] ] ], [ 'id' => 'FERRERO.FIELD.MARKETING.FLAVOUR', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Apricot' ] ] ], [ 'id' => 'FERRERO.FIELD.MARKETING.SIZE', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Standard' ] ] ], [ 'id' => 'FERRERO.FIELD.STATE', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'type' => 'string', 'value' => 'Local' ] ] ], [ 'id' => 'ARTESIA.FIELD.ASSET NAME', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'value' => [ 'type' => 'string', 'value' => $filename ] ] ], [ 'id' => 'FERRERO.FIELD.SUB BRAND', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Master Asset' ] ] ], [ 'id' => 'FERRERO.FIELD.ASSET VALIDITY START PERIOD', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => '09/10/2025' ] ] ], [ 'id' => 'FERRERO.FIELD.ASSET VALIDITY END PERIOD', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => '10/10/2025' ] ] ], [ 'id' => 'FERRERO.MARKETING.FIELD.AGENCY NAME', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'is_locked' => false, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => '0000000023' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ], [ 'id' => 'FERRERO.MARKET.FIELD.IPRIGHT', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'is_locked' => false, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => 'No' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ], [ 'id' => 'FERRERO.MARKET.PROD_COMPANY', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'is_locked' => false, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => '-' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ], [ 'id' => 'FERRERO.MARKET.FIELD.LICENSIN', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => true, 'is_locked' => false, 'value' => [ 'field_value' => [ 'type' => 'string', 'value' => 'No' ], 'type' => 'com.artesia.metadata.DomainValue' ] ] ], [ 'id' => 'FERRERO.MARKET.FIELD.BUYOUT', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Digital' ] ] ], [ 'id' => 'FERRERO.MARKET.FIELD.FERRERO PROPERTY', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'YES' ] ] ], [ 'id' => 'FERRERO.MARKET.VID_N_STAT', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'No' ] ] ], [ 'id' => 'FERRERO.MARKET.FIELD.LICENSE', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Disney' ] ] ], [ 'id' => 'FERRERO.MARKETING.FIELD.SPOT_VERSION', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Master' ] ] ], [ 'id' => 'FERRERO.MARKETING.FIELD.DIRECTOR_NAME', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Director Name' ] ] ], [ 'id' => 'FERRERO.MARKETING.FIELD.VIDEO_POST_PROD_COMPANY', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Video Production Company' ] ] ], [ 'id' => 'FERRERO.MARKETING.FIELD.VID_POST_PROD_CONTACT', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Contact' ] ] ], [ 'id' => 'FERRERO.MARKETING.FIELD.AUDIO_POST_PROD_COMPANY', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => 'Audio Production Company' ] ] ] ] ], 'metadata_model_id' => 'ECOMMERCE', 'security_policy_list' => [ ['id' => 1594] ] ] ] ]; return $assetRep; } /** * Build asset representation from source metadata (copying from master asset) * Updates filename but preserves all other metadata * @deprecated Use buildAssetRepresentationFromMasterAsset instead */ private function buildAssetRepresentationFromSource($filename, $sourceMetadata) { // Copy the entire metadata structure from the source $assetRep = [ 'asset_resource' => [ 'asset' => [ 'metadata' => $sourceMetadata, 'name' => $filename ] ] ]; // Update the ASSET NAME field if it exists in the metadata if (isset($sourceMetadata['metadata_element_list'])) { foreach ($sourceMetadata['metadata_element_list'] as &$category) { if (isset($category['metadata_element_list'])) { foreach ($category['metadata_element_list'] as &$field) { if ($field['id'] === 'ARTESIA.FIELD.ASSET NAME') { $field['value']['value']['value'] = $filename; } } } } } return $assetRep; } /** * Build asset representation for upload (fallback when no source metadata) */ private function buildAssetRepresentation($filename, $metadata = []) { $assetRep = [ 'asset_resource' => [ 'asset' => [ 'metadata' => [ 'metadata_element_list' => [] ] ] ] ]; // Add filename metadata $assetRep['asset_resource']['asset']['metadata']['metadata_element_list'][] = [ 'id' => 'ARTESIA.FIELD.ASSET NAME', 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => $filename ] ] ]; // Add custom metadata fields foreach ($metadata as $fieldId => $fieldValue) { $assetRep['asset_resource']['asset']['metadata']['metadata_element_list'][] = [ 'id' => $fieldId, 'type' => 'com.artesia.metadata.MetadataField', 'value' => [ 'cascading_domain_value' => false, 'domain_value' => false, 'value' => [ 'type' => 'string', 'value' => $fieldValue ] ] ]; } return $assetRep; } /** * Upload multiple files */ public function uploadMultipleFiles($files, $folderId, $metadata = []) { $results = [ 'total' => count($files), 'successful' => 0, 'failed' => 0, 'details' => [] ]; foreach ($files as $file) { $result = $this->uploadFile($file, $folderId, $metadata); if ($result['success']) { $results['successful']++; } else { $results['failed']++; } $results['details'][] = $result; } return $results; } }