false, 'error' => [ 'type' => 'fatal', 'message' => 'Internal error' ] ], 500); } }); } // Инициализация CRM require_once 'config.inc.php'; require_once 'include/utils/utils.php'; require_once 'includes/Loader.php'; vimport('includes.runtime.Globals'); require_once 'include/database/PearDatabase.php'; require_once 'modules/Users/Users.php'; require_once 'include/Webservices/Utils.php'; require_once 'include/Webservices/Create.php'; require_once 'include/Webservices/Login.php'; require_once 'include/Webservices/AuthToken.php'; require_once 'include/Webservices/AddRelated.php'; require_once 'data/CRMEntity.php'; require_once 'modules/Vtiger/CRMEntity.php'; $adb = PearDatabase::getInstance(); // Вспомогательные функции function getUserWsPrefix() { global $adb; $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['Users']); return ($rs && $adb->num_rows($rs) > 0) ? $adb->query_result($rs, 0, 'id') : 19; } function getProjectWsIdFromDB($projectId) { global $adb; $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['Project']); return ($rs && $adb->num_rows($rs) > 0) ? $adb->query_result($rs, 0, 'id') . 'x' . (int)$projectId : null; } function getDocumentFoldersWsPrefix() { global $adb; $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['DocumentFolders']); return ($rs && $adb->num_rows($rs) > 0) ? (int)$adb->query_result($rs, 0, 'id') : 22; } function getFolderWsIdByName($folderName) { global $adb; $rs = $adb->pquery('SELECT folderid FROM vtiger_attachmentsfolder WHERE foldername = ? LIMIT 1', [$folderName]); if ($rs && $adb->num_rows($rs) > 0) { $folderId = (int)$adb->query_result($rs, 0, 'folderid'); $prefix = getDocumentFoldersWsPrefix(); return $prefix . 'x' . $folderId; } return null; } /** * Извлекает S3 метаданные из URL */ function parseS3Url($fileUrl) { // Пример URL: https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clpr_claims/d89a4342-1256-465f-b866-095d67a3f9bd/3953bd77-fa82-495b-869b-59460c7486d2__pretenzionnaya-rabota.pdf $parsed = parse_url($fileUrl); if (!$parsed || !isset($parsed['host']) || !isset($parsed['path'])) { return null; } $path = ltrim($parsed['path'], '/'); $pathParts = explode('/', $path, 2); if (count($pathParts) < 2) { return null; } return [ 'bucket' => $pathParts[0], 'key' => $pathParts[1], 'host' => $parsed['host'] ]; } /** * Получает размер файла из S3 */ function getS3FileSize($fileUrl) { try { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $fileUrl); curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { if (preg_match('/Content-Length:\s*(\d+)/i', $response, $matches)) { return (int)$matches[1]; } } return 0; } catch (Exception $e) { writeLog("Ошибка получения размера файла: " . $e->getMessage()); return 0; } } // 🚀 ИСПРАВЛЕННАЯ ФУНКЦИЯ: Создание документов с правильными S3 метаданными function createDocumentsWithSession($data) { global $adb, $current_user; $sessionName = $data['sessionName']; $projectId = (int)$data['projectid']; $contactId = (int)$data['contactid']; $userId = (int)($data['user_id'] ?? 1); $filesArray = $data['files'] ?? []; writeLog('🚀 Начинаем создание документов с правильными S3 метаданными'); writeLog("📋 Проект: $projectId, Контакт: $contactId, Пользователь: $userId"); writeLog('📋 Файлов к обработке: ' . count($filesArray)); // Упрощенный подход: используем только webservice API без обращений к БД $projectWsId = '20x' . $projectId; // Предполагаем стандартный префикс для Project $assignedUserWsId = '19x' . $userId; // Предполагаем стандартный префикс для Users $folderWsId = '22x1'; // Default папка writeLog("✅ Project WS ID: $projectWsId"); writeLog("👤 User WS ID: $assignedUserWsId"); writeLog("📁 Папка: $folderWsId"); // Инициализируем CURL $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, 'https://crm.clientright.ru/webservice.php'); curl_setopt($ch, CURLOPT_POST, 1); $results = []; foreach ($filesArray as $i => $file) { $fileName = $file['original_file_name'] ?? $file['file_name'] ?? 'Unknown'; writeLog("📄 Обрабатываем файл #{$i}: $fileName"); try { // Проверяем обязательные поля if (empty($file['file_url'])) { throw new Exception("Отсутствует file_url"); } // Парсим S3 URL для получения метаданных $s3Info = parseS3Url($file['file_url']); if (!$s3Info) { throw new Exception("Не удалось распарсить S3 URL: " . $file['file_url']); } writeLog("🔍 S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}"); // Получаем размер файла из S3 $fileSize = getS3FileSize($file['file_url']); writeLog("📏 Размер файла: " . number_format($fileSize) . " байт"); // Определяем название и описание документа $documentTitle = $file['upload_description'] ?? $file['original_file_name'] ?? $file['file_name'] ?? 'Документ'; $documentContent = sprintf( 'Загружено через n8n. ID: %s, Контакт: %d, Проект: %d, Загружено: %s', $file['id'] ?? 'N/A', $contactId, $projectId, $file['uploaded_at'] ?? date('Y-m-d H:i:s') ); // Создаём документ с правильными S3 метаданными $docElement = [ 'notes_title' => $documentTitle, 'filename' => $file['original_file_name'] ?? $file['file_name'], // Оригинальное имя файла 'assigned_user_id' => $assignedUserWsId, 'notecontent' => $documentContent, 'filetype' => 'application/pdf', 'filesize' => (string)$fileSize, 'filelocationtype' => 'E', // External URL 'fileversion' => '1.0', 'filestatus' => '1', // Active 'folderid' => $folderWsId, // S3 метаданные 's3_bucket' => $s3Info['bucket'], 's3_key' => $s3Info['key'], 's3_etag' => '', // Будет заполнено позже при необходимости ]; writeLog("📤 Создаём документ '$documentTitle' с S3 метаданными"); writeLog("📤 S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}"); // Отправляем запрос на создание документа curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'create', 'sessionName' => $sessionName, 'elementType' => 'Documents', 'element' => json_encode($docElement, JSON_UNESCAPED_UNICODE), ]); $resp = curl_exec($ch); if ($resp === false) { throw new Exception('CURL error: ' . curl_error($ch)); } $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); writeLog("📥 Ответ создания документа: " . substr($resp, 0, 300)); $doc = json_decode($resp, true); if (!$doc || !$doc['success'] || empty($doc['result']['id'])) { throw new Exception('Failed to create document: ' . substr($resp, 0, 200)); } $documentWsId = $doc['result']['id']; list(, $docNumericId) = explode('x', $documentWsId, 2); writeLog("✅ Документ создан: $documentWsId"); // Привязываем к проекту writeLog("🔗 Привязываем документ $documentWsId к проекту $projectWsId"); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'AddRelated', 'sessionName' => $sessionName, 'sourceRecordId' => $projectWsId, 'relatedRecordId' => $documentWsId, ]); $resp = curl_exec($ch); $relationOk = false; if ($resp !== false) { $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); writeLog("📥 Ответ AddRelated: " . substr($resp, 0, 200)); $rel = json_decode($resp, true); $relationOk = isset($rel['result']['message']) && $rel['result']['message'] === 'successfull'; } // Если webservice не сработал - используем прямую привязку if (!$relationOk) { writeLog("⚠️ AddRelated не сработал, используем прямую привязку"); try { // Устанавливаем current_user для CRMEntity if (!isset($current_user) || !$current_user) { $current_user = new Users(); $current_user->retrieveCurrentUserInfoFromFile($userId); } $focus = CRMEntity::getInstance('Project'); relateEntities($focus, 'Project', $projectId, 'Documents', (int)$docNumericId); writeLog("✅ Прямая привязка успешна"); } catch (Exception $directRelationError) { writeLog("❌ Прямая привязка не удалась: " . $directRelationError->getMessage()); } } else { writeLog("✅ AddRelated успешен"); } // Возвращаем результат с сохранением всех исходных данных $result = array_merge($file, [ 'status' => 'success', 'projectid' => $projectId, 'contactid' => $contactId, 'crm_result' => [ 'document_id' => $documentWsId, 'document_numeric_id' => $docNumericId, 'project_id' => $projectId, 'contact_id' => $contactId, 'folder_id' => $folderWsId, 's3_bucket' => $s3Info['bucket'], 's3_key' => $s3Info['key'], 'file_size' => $fileSize, 'message' => 'Документ создан с правильными S3 метаданными и привязан к проекту' . (!$relationOk ? ' (прямая привязка)' : '') ] ]); $results[] = $result; writeLog("✅ Файл '$fileName' успешно обработан с S3 метаданными"); } catch (Exception $e) { writeLog("❌ Ошибка для файла '$fileName': " . $e->getMessage()); $results[] = array_merge($file, [ 'status' => 'error', 'projectid' => $projectId, 'contactid' => $contactId, 'crm_result' => [ 'message' => $e->getMessage() ] ]); } } curl_close($ch); $successCount = count(array_filter($results, function($r) { return $r['status'] === 'success'; })); writeLog("🏁 Обработка завершена. Успешно: $successCount/" . count($results)); return $results; } // Обработка запроса if ($IS_POST) { writeLog('=== START POST REQUEST V2 (FIXED) ==='); writeLog('Headers: ' . json_encode(getallheaders(), JSON_UNESCAPED_UNICODE)); // Получаем и проверяем входные данные $input = file_get_contents('php://input'); writeLog('Raw input: ' . substr($input, 0, 1000) . (strlen($input) > 1000 ? '...(truncated)' : '')); $input = ltrim($input, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); $data = json_decode($input, true); if (json_last_error() !== JSON_ERROR_NONE) { writeLog('❌ JSON Error: ' . json_last_error_msg()); json_response([ 'success' => false, 'error' => ['message' => 'Invalid JSON: ' . json_last_error_msg()] ], 400); } writeLog('Parsed data keys: ' . implode(', ', array_keys($data))); // Проверяем обязательные поля if (empty($data['sessionName'])) { writeLog('❌ Error: sessionName is required'); json_response([ 'success' => false, 'error' => ['message' => 'sessionName is required in request data'] ], 400); } if (empty($data['projectid'])) { writeLog('❌ Error: projectid is required'); json_response([ 'success' => false, 'error' => ['message' => 'projectid is required in request data'] ], 400); } if (empty($data['contactid'])) { writeLog('❌ Error: contactid is required'); json_response([ 'success' => false, 'error' => ['message' => 'contactid is required in request data'] ], 400); } // Поддерживаем оба формата: files и documents $filesArray = null; if (!empty($data['files']) && is_array($data['files'])) { $filesArray = $data['files']; } elseif (!empty($data['documents']) && is_array($data['documents'])) { $filesArray = $data['documents']; } if (empty($filesArray)) { writeLog('❌ Error: files or documents array is required'); json_response([ 'success' => false, 'error' => ['message' => 'files or documents array is required in request data'] ], 400); } writeLog("🔑 Сессия: {$data['sessionName']}"); writeLog("📋 Проект: {$data['projectid']}, Контакт: {$data['contactid']}"); writeLog('📄 Файлов: ' . count($filesArray)); // Нормализуем данные: всегда используем 'files' внутри функции $normalizedData = $data; $normalizedData['files'] = $filesArray; // Создаём документы с правильными S3 метаданными $results = createDocumentsWithSession($normalizedData); // Успешный ответ writeLog('✅ Success: processed ' . count($results) . ' files with S3 metadata'); json_response([ 'success' => true, 'total_processed' => count($filesArray), 'results' => $results, 'session_used' => $sessionName ]); } else { // GET запрос - документация API header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => true, 'message' => 'Upload Documents API v2 - Fixed for S3 Storage', 'endpoint' => 'POST to this URL with complete data object', 'format' => [ 'sessionName' => 'string (required - from n8n CRM login)', 'projectid' => 'string (required - project ID)', 'contactid' => 'string (required - contact ID)', 'user_id' => 'string (optional - default "1")', 'files' => [ // или 'documents' - поддерживаются оба формата [ 'id' => 'string (file UUID)', 'file_id' => 'string (S3 path)', 'file_url' => 'string (required - full S3 URL)', 'file_name' => 'string (S3 filename)', 'field_name' => 'string (form field)', 'uploaded_at' => 'string (ISO datetime)', 'original_file_name' => 'string (user filename)', 'upload_description' => 'string (document description)', 'filename_for_upload' => 'string (display filename)' ] ] ], 'example' => [ 'sessionName' => '27c160f968cc0cea6dd38', 'projectid' => '392790', 'contactid' => '320096', 'user_id' => '1', 'documents' => [ // пример с 'documents' (как от n8n) [ 'id' => 'd0d3d844-2db0-4c68-82e4-a803881b7527', 'file_id' => 'clpr_claims/5eac8512-1bc5-49ee-952c-3ec390db6964/d0d3d844-2db0-4c68-82e4-a803881b7527__pretenzionnaya-rabota.pdf', 'file_url' => 'https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clpr_claims/5eac8512-1bc5-49ee-952c-3ec390db6964/d0d3d844-2db0-4c68-82e4-a803881b7527__pretenzionnaya-rabota.pdf', 'file_name' => 'd0d3d844-2db0-4c68-82e4-a803881b7527__pretenzionnaya-rabota.pdf', 'field_name' => 'uploads[1][0]', 'uploaded_at' => '2025-09-15T22:46:51.913+03:00', 'original_file_name' => '1757965604702.pdf', 'upload_description' => 'Претензионная работа', 'filename_for_upload' => '1757965604702.pdf' ] ] ], 'changes' => [ 'Fixed S3 metadata handling', 'Proper filelocationtype=E setting', 'Correct filename field usage', 'Added S3 bucket and key extraction', 'Added file size detection from S3' ] ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } ?>