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'; $adb = PearDatabase::getInstance(); // Вспомогательные функции из test_ws_documents.php 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) { $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 updateDocumentS3Metadata($documentId, $s3Info, $fileSize, $originalFileName) { global $adb; try { writeLog("Обновляем S3 метаданные для документа $documentId"); writeLog("S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}, Size: $fileSize"); $updateQuery = " UPDATE vtiger_notes SET filename = ?, filelocationtype = 'E', filesize = ?, s3_bucket = ?, s3_key = ?, s3_etag = '' WHERE notesid = ? "; // Создаем полный S3 URL для отображения в CRM $s3Url = "https://s3.twcstorage.ru/{$s3Info['bucket']}/{$s3Info['key']}"; // Очищаем URL от HTML тегов (защита от
и других тегов) $s3Url = strip_tags(trim($s3Url)); $params = [ $s3Url, // Используем полный S3 URL вместо оригинального имени файла $fileSize, $s3Info['bucket'], $s3Info['key'], $documentId ]; writeLog("SQL: $updateQuery"); writeLog("Params: " . json_encode($params)); $result = $adb->pquery($updateQuery, $params); if ($result) { writeLog("✅ S3 метаданные успешно обновлены для документа $documentId"); return true; } else { writeLog("❌ Ошибка обновления S3 метаданных: " . $adb->database->errorMsg()); return false; } } catch (Exception $e) { writeLog("❌ Исключение при обновлении S3 метаданных: " . $e->getMessage()); return false; } } // Функция нормализации входных данных function normalizeInputData($data) { writeLog('🔍 Нормализация входных данных...'); // Новый формат: {"documents": [...], "projectid": "..."} if (isset($data['documents']) && isset($data['projectid'])) { writeLog('📋 Обнаружен новый формат с массивом documents'); $filesArray = []; foreach ($data['documents'] as $doc) { // Определяем projectid - приоритет: projectid, group_session_token, 0 $projectid = null; if (!empty($data['projectid'])) { $projectid = (int)$data['projectid']; } elseif (!empty($data['group_session_token']) && is_numeric($data['group_session_token'])) { $projectid = (int)$data['group_session_token']; } elseif (!empty($doc['projectid'])) { $projectid = (int)$doc['projectid']; } elseif (!empty($doc['group_session_token']) && is_numeric($doc['group_session_token'])) { $projectid = (int)$doc['group_session_token']; } $normalizedFile = [ 'url' => $doc['file_url'] ?? $doc['url'] ?? '', 'file_name' => $doc['file_name'] ?? $doc['original_file_name'] ?? '', 'description' => $doc['upload_description'] ?? $doc['description'] ?? '', 'projectid' => $projectid, 'ticket_id' => $data['ticket_id'] ?? $doc['ticket_id'] ?? null, // ✅ Поддержка привязки к заявке 'user_id' => $data['user_id'] ?? $doc['user_id'] ?? 1, 'contactid' => $doc['contactid'] ?? $data['contactid'] ?? 0, 'pages' => $doc['pages'] ?? 0, 'folder' => $doc['folder'] ?? '', 'newfile' => $doc['newfile'] ?? '', 'field_name' => $doc['field_name'] ?? '', 'prefix' => $doc['prefix'] ?? '' ]; $filesArray[] = $normalizedFile; writeLog("✅ Нормализован файл: {$normalizedFile['file_name']} (projectid: {$projectid})"); } return $filesArray; } // Старый формат: массив файлов напрямую if (is_array($data)) { writeLog('📋 Обнаружен массив данных'); // Проверяем, что это не объект с ключами как поля if (isset($data['url']) || isset($data['projectid'])) { writeLog('⚠️ Обнаружен объект с ключами полей, преобразуем в массив'); return [$data]; } // Проверяем первый элемент массива на наличие projectid или group_session_token if (!empty($data) && is_array($data[0])) { $firstItem = $data[0]; // Если projectid равен null, но есть group_session_token - используем его if ((!isset($firstItem['projectid']) || $firstItem['projectid'] === null) && !empty($firstItem['group_session_token']) && is_numeric($firstItem['group_session_token'])) { writeLog('🔄 Обнаружен projectid=null, используем group_session_token как projectid'); // Нормализуем каждый элемент массива $normalizedArray = []; foreach ($data as $item) { $normalizedItem = $item; $normalizedItem['projectid'] = (int)$item['group_session_token']; $normalizedArray[] = $normalizedItem; writeLog("✅ Нормализован файл: {$item['file_name']} (projectid: {$normalizedItem['projectid']})"); } return $normalizedArray; } } return $data; } // Одиночный объект if (is_object($data)) { writeLog('📋 Обнаружен одиночный объект файла'); return [$data]; } writeLog('❌ Неизвестный формат данных'); return []; } // Функция для получения сессии webservice с retry function getWebserviceSession($userName, $maxRetries = 3) { global $adb; // Получаем access key пользователя $rs = $adb->pquery('SELECT id, accesskey FROM vtiger_users WHERE user_name=?', [$userName]); if (!$rs || $adb->num_rows($rs) == 0) { writeLog("❌ Пользователь $userName не найден"); return ['error' => "Пользователь $userName не найден"]; } $userId = $adb->query_result($rs, 0, 'id'); $accessKey = $adb->query_result($rs, 0, 'accesskey'); $endpointUrl = 'https://crm.clientright.ru/webservice.php'; $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { writeLog("🔐 Попытка аутентификации #{$attempt}..."); // Добавляем случайную задержку для избежания race condition if ($attempt > 1) { $delay = rand(100, 500) * 1000; // 100-500ms в микросекундах usleep($delay); writeLog("⏱️ Задержка " . ($delay / 1000) . "ms перед попыткой #{$attempt}"); } // getchallenge curl_setopt($ch, CURLOPT_URL, $endpointUrl . '?operation=getchallenge&username=' . urlencode($userName)); curl_setopt($ch, CURLOPT_POST, 0); curl_setopt($ch, CURLOPT_HTTPGET, 1); $resp = curl_exec($ch); if ($resp === false) { writeLog('❌ CURL error (challenge): ' . curl_error($ch)); continue; } $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); $challenge = json_decode($resp, true); if (!$challenge || empty($challenge['result']['token'])) { writeLog("❌ Invalid challenge response (attempt #{$attempt}): " . substr($resp, 0, 100)); continue; } $token = $challenge['result']['token']; // login $key = md5($token . $accessKey); curl_setopt($ch, CURLOPT_URL, $endpointUrl); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'login', 'username' => $userName, 'accessKey' => $key, ]); $resp = curl_exec($ch); if ($resp === false) { writeLog('❌ CURL error (login): ' . curl_error($ch)); continue; } $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); $login = json_decode($resp, true); if (!$login || empty($login['result']['sessionName'])) { writeLog("❌ Login failed (attempt #{$attempt}): " . substr($resp, 0, 100)); continue; } writeLog("✅ Аутентификация успешна с попытки #{$attempt}"); return [ 'sessionId' => $login['result']['sessionName'], 'userId' => $userId, 'curl' => $ch ]; } curl_close($ch); return ['error' => 'Authentication failed after ' . $maxRetries . ' attempts']; } // Основная функция для создания документов function createDocumentsInCRM($filesArray, $userName = 'api') { global $adb, $current_user; writeLog('🚀 Начинаем создание документов...'); // Получаем сессию с retry логикой $session = getWebserviceSession($userName); if (isset($session['error'])) { return $session; } $sessionId = $session['sessionId']; $userId = $session['userId']; $ch = $session['curl']; $endpointUrl = 'https://crm.clientright.ru/webservice.php'; // 4. Устанавливаем current_user для CRMEntity $current_user = new Users(); $current_user->retrieveCurrentUserInfoFromFile((int)$userId); // 5. Создаём документы $results = []; foreach ($filesArray as $i => $f) { writeLog("📄 Обрабатываем файл #{$i}: " . ($f['file_name'] ?? 'Unknown')); try { // Валидация обязательных полей if (empty($f['url'])) { throw new Exception("Отсутствует URL файла"); } if (empty($f['file_name'])) { throw new Exception("Отсутствует имя файла"); } if (empty($f['projectid'])) { throw new Exception("Отсутствует ID проекта"); } if (empty($f['description'])) { $f['description'] = $f['file_name']; // Используем имя файла как описание writeLog("⚠️ Отсутствует описание, используем имя файла"); } // Получаем WS ID проекта $projectWsId = getProjectWsIdFromDB((int)$f['projectid']); if (!$projectWsId) { throw new Exception("Не найден Project ID {$f['projectid']}"); } // WS ID пользователя $usersPrefix = getUserWsPrefix(); $assignedUserWsId = $usersPrefix . 'x' . (int)$f['user_id']; // Папка (сначала Суд, потом Default) $folderWsId = getFolderWsIdByName('Суд'); if (!$folderWsId) { $folderWsId = '22x1'; writeLog("⚠️ Папка 'Суд' не найдена, используем Default"); } // Парсим S3 URL для получения метаданных $s3Info = parseS3Url($f['url']); if (!$s3Info) { throw new Exception("Не удалось распарсить S3 URL: " . $f['url']); } writeLog("🔍 S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}"); // Получаем размер файла из S3 $fileSize = getS3FileSize($f['url']); writeLog("📏 Размер файла: " . number_format($fileSize) . " байт"); // Создаём документ (webservice не поддерживает S3 метаданные напрямую) $docElement = [ 'notes_title' => $f['description'] ?: $f['file_name'], 'filename' => $f['file_name'], // Оригинальное имя файла 'assigned_user_id' => $assignedUserWsId, 'notecontent' => 'Авто из S3. Контакт: ' . ($f['contactid'] ?? '') . ' | URL: ' . $f['url'], 'filetype' => 'application/pdf', 'filesize' => (string)$fileSize, 'filelocationtype' => 'E', // External URL 'fileversion' => '1.0', 'filestatus' => '1', // Active 'folderid' => $folderWsId, ]; curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'create', 'sessionName' => $sessionId, '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"); $doc = json_decode($resp, true); if (!$doc || empty($doc['result']['id'])) { throw new Exception('Failed to create document: ' . substr($resp, 0, 100)); } $documentWsId = $doc['result']['id']; list(, $docNumericId) = explode('x', $documentWsId, 2); writeLog("✅ Документ создан: $documentWsId (numeric: $docNumericId)"); // ВАЖНО: Обновляем S3 метаданные напрямую в базе данных $s3UpdateSuccess = updateDocumentS3Metadata($docNumericId, $s3Info, $fileSize, $f['file_name']); // Определяем к чему привязываем: к HelpDesk (заявке) или к Project (проекту) $ticketId = isset($f['ticket_id']) ? (int)$f['ticket_id'] : null; if ($ticketId) { // Привязываем к HelpDesk (заявке) writeLog("📎 Привязываем к HelpDesk ticket_id: $ticketId"); // Получаем WS ID для HelpDesk $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['HelpDesk']); $helpdeskPrefix = ($rs && $adb->num_rows($rs) > 0) ? $adb->query_result($rs, 0, 'id') : 17; $ticketWsId = $helpdeskPrefix . 'x' . $ticketId; curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'AddRelated', 'sessionName' => $sessionId, 'sourceRecordId' => $ticketWsId, 'relatedRecordId' => $documentWsId, ]); $resp = curl_exec($ch); // Проверяем успех AddRelated $ok = false; if ($resp !== false) { $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); $rel = json_decode($resp, true); $ok = isset($rel['result']['message']) && $rel['result']['message'] === 'successfull'; } // Если webservice не сработал - используем прямую привязку if (!$ok) { writeLog("⚠️ AddRelated к HelpDesk не сработал, используем прямую привязку"); require_once 'data/CRMEntity.php'; require_once 'modules/Vtiger/CRMEntity.php'; $focus = CRMEntity::getInstance('HelpDesk'); relateEntities($focus, 'HelpDesk', $ticketId, 'Documents', (int)$docNumericId); } writeLog("✅ Документ привязан к HelpDesk #$ticketId"); } else { // Привязываем к проекту (старая логика) writeLog("📎 Привязываем к Project project_id: {$f['projectid']}"); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'AddRelated', 'sessionName' => $sessionId, 'sourceRecordId' => $projectWsId, 'relatedRecordId' => $documentWsId, ]); $resp = curl_exec($ch); // Проверяем успех AddRelated $ok = false; if ($resp !== false) { $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); $rel = json_decode($resp, true); $ok = isset($rel['result']['message']) && $rel['result']['message'] === 'successfull'; } // Если webservice не сработал - используем прямую привязку if (!$ok) { writeLog("⚠️ AddRelated не сработал, используем прямую привязку"); require_once 'data/CRMEntity.php'; require_once 'modules/Vtiger/CRMEntity.php'; list(, $docNumericId) = explode('x', $documentWsId, 2); $focus = CRMEntity::getInstance('Project'); relateEntities($focus, 'Project', (int)$f['projectid'], 'Documents', (int)$docNumericId); } writeLog("✅ Документ привязан к Project #{$f['projectid']}"); } // Возвращаем все входные данные + результат $result = array_merge($f, [ 'status' => 'success', 'crm_result' => [ 'document_id' => $documentWsId, 'document_numeric_id' => $docNumericId, 'project_id' => $f['projectid'], 'folder_id' => $folderWsId, 's3_bucket' => $s3Info['bucket'], 's3_key' => $s3Info['key'], 'file_size' => $fileSize, 's3_metadata_updated' => $s3UpdateSuccess, 'message' => 'Документ создан с правильными S3 метаданными и привязан к проекту' . (!$ok ? ' (прямая привязка)' : '') ] ]); $results[] = $result; writeLog("✅ Документ {$f['file_name']} успешно создан и привязан"); } catch (Exception $e) { writeLog("❌ Ошибка для файла {$f['file_name']}: " . $e->getMessage()); // В случае ошибки тоже возвращаем все входные данные $results[] = array_merge($f, [ 'status' => 'error', 'crm_result' => [ 'message' => $e->getMessage() ] ]); } } curl_close($ch); return $results; } // Обработка запроса if ($IS_POST) { writeLog('=== START POST REQUEST ==='); 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: ' . json_encode($data, JSON_UNESCAPED_UNICODE)); // Нормализуем формат данных $filesArray = normalizeInputData($data); // Проверяем массив if (!is_array($filesArray)) { writeLog('❌ Error: Input is not an array'); json_response([ 'success' => false, 'error' => ['message' => 'Input must be an array of files'] ], 400); } writeLog('Processing ' . count($filesArray) . ' files...'); // Создаём документы $results = createDocumentsInCRM($filesArray); // Проверяем результат if (isset($results['error'])) { writeLog('❌ Error: ' . $results['error']); json_response([ 'success' => false, 'error' => ['message' => $results['error']] ], 500); } // Успешный ответ writeLog('✅ Success: processed ' . count($results) . ' files'); json_response([ 'success' => true, 'total_processed' => count($filesArray), 'results' => $results ]); } else { // GET запрос - тестовый режим $testData = [ [ 'session_token' => 'sess_test', 'group_session_token' => 'token_test', 'user_id' => 1, 'group' => 'group_0', 'contactid' => 120374, 'files_count' => 2, 'group_index_num' => 0, 'description' => 'Иск', 'projectid' => 354918, 'telegram_id' => 295410106, 'unified_id' => 'usr_test', 'pages' => 2, 'url' => 'https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clientright/120374/1757346451126.pdf', 'file_name' => '1757346451126.pdf', 'field_name' => '120374', 'folder' => 'clientright/120374', 'newfile' => 'clientright/120374/1757346451126.pdf' ] ]; $results = createDocumentsInCRM($testData); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => true, 'total_processed' => count($testData), 'results' => $results ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } ?>