Files
crm.clientright.ru/modules/Documents/actions/NcPrepareEdit.php
Fedor 1f96ab6e10 feat: Полная интеграция CRM → Nextcloud редактор
 Что реализовано:
- SSL/HTTPS для Nextcloud (Let's Encrypt R13)
- Redis кэширование для производительности
- Collabora Online редактор документов
- WOPI allow list настроен (0.0.0.0/0)
- Динамическое получение fileId через WebDAV
- Поддержка файлов из S3 и локальных файлов
- Автоматическое извлечение имени файла из URL
- Промежуточная страница для обхода CSRF

🚀 Как работает:
1. JavaScript передает recordId и fileName
2. PHP получает fileId через WebDAV PROPFIND
3. PHP делает редирект на рабочий URL Nextcloud
4. Файл открывается в редакторе Collabora

📁 Файлы:
- layouts/v7/lib/nextcloud-editor.js - JavaScript интеграция
- crm_extensions/file_storage/api/open_file.php - PHP редирект
- modules/Documents/actions/NcPrepareEdit.php - API подготовка
- crm_extensions/docs/ - документация

🎯 Результат: Каждый документ в CRM открывает СВОЙ файл в Nextcloud редакторе!
2025-10-21 22:10:47 +03:00

221 lines
8.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Action для подготовки файла к редактированию в Nextcloud
*/
class Documents_NcPrepareEdit_Action extends Vtiger_Action_Controller {
public function requiresPermission(\Vtiger_Request $request) {
$permissions = parent::requiresPermission($request);
$permissions[] = array('module_parameter' => 'module', 'action' => 'DetailView', 'record_parameter' => 'record');
return $permissions;
}
public function checkPermission(\Vtiger_Request $request) {
return parent::checkPermission($request);
}
public function process(\Vtiger_Request $request) {
// Устанавливаем заголовки
header('Content-Type: application/json; charset=utf-8');
try {
$recordId = trim($request->get('record'));
$fileName = trim($request->get('fileName'));
if (empty($recordId) || empty($fileName)) {
throw new Exception('record или fileName отсутствуют');
}
// Проверяем поддерживаемые расширения
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($ext, ['docx', 'xlsx', 'pptx'], true)) {
throw new Exception('Неподдерживаемое расширение: ' . $ext);
}
// Получаем fileId из Nextcloud
$fileId = $this->resolveNcFileId((int)$recordId, $fileName);
$baseUrl = $this->getNcBaseUrl();
$dirPath = $this->getDirPath($recordId);
// Токен для RichDocuments (из настроек Nextcloud)
$richDocumentsToken = '1sanuq71b3n4fm1ldkbb';
$urls = [
// РАБОЧИЙ URL! Формат от пользователя - ПРИОРИТЕТ!
'files_editing_auto' => $fileId ? $baseUrl . '/apps/files/files/' . $fileId . '?dir=/&editing=true&openfile=true' : null,
// Вариант без автоматического редактирования
'files_editing' => $fileId ? $baseUrl . '/apps/files/files/' . $fileId . '?dir=/&editing=false&openfile=true' : null,
// Collabora Editor
'collabora_editor' => $fileId ? $baseUrl . '/index.php/apps/richdocuments/index?fileId=' . $fileId : null,
// OnlyOffice Editor
'onlyoffice_editor' => $fileId ? $baseUrl . '/apps/onlyoffice?fileId=' . $fileId : null,
// Прямое открытие файла
'files_direct' => $fileId ? $baseUrl . '/apps/files/files/' . $fileId : null,
// Файловый менеджер
'files_manager' => $baseUrl . '/apps/files/?dir=/&openfile=' . rawurlencode($fileName)
];
// Убираем null значения
$urls = array_filter($urls, function($url) {
return $url !== null;
});
// Определяем основной URL для редактирования
$editUrl = isset($urls['files_editing_auto']) ? $urls['files_editing_auto'] :
(isset($urls['files_editing']) ? $urls['files_editing'] :
(isset($urls['files_direct']) ? $urls['files_direct'] : null));
echo json_encode([
'success' => true,
'data' => [
'record_id' => $recordId,
'file_name' => $fileName,
'file_id' => $fileId,
'edit_url' => $editUrl, // Основной URL
'urls' => $urls,
'message' => 'Файл подготовлен к редактированию'
]
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_UNESCAPED_UNICODE);
}
}
/**
* Получение базового URL Nextcloud
*/
private function getNcBaseUrl(): string {
return 'https://office.clientright.ru:8443';
}
/**
* Получение пути к директории
*/
private function getDirPath($recordId): string {
return '/crm/crm2/CRM_Active_Files/Documents/' . $recordId;
}
/**
* Получение fileId из Nextcloud через WebDAV
*/
private function resolveNcFileId(int $recordId, string $fileName): ?int {
try {
$config = $this->getNcConfig();
$ncPath = $this->getNcPath($recordId, $fileName);
// 1) PROPFIND для получения метаданных
$meta = $this->webdavPropfind($config, $ncPath);
if ($meta['status'] === 404) {
// Файл не существует - нужно создать
error_log("File not found in Nextcloud: {$ncPath}");
return null;
}
if ($meta['status'] === 200 && isset($meta['fileid'])) {
error_log("Found fileId for {$fileName}: " . $meta['fileid']);
return (int)$meta['fileid'];
}
error_log("Could not get fileId from PROPFIND response");
return null;
} catch (Exception $e) {
error_log('Error resolving fileId: ' . $e->getMessage());
return null;
}
}
/**
* Получение конфигурации Nextcloud
*/
private function getNcConfig(): array {
$configPath = __DIR__ . '/../../../../crm_extensions/file_storage/config.php';
if (!file_exists($configPath)) {
throw new Exception('Nextcloud config not found');
}
$config = require_once $configPath;
return $config['nextcloud'];
}
/**
* Получение пути к файлу в Nextcloud
*/
private function getNcPath(int $recordId, string $fileName): string {
return "/crm/crm2/CRM_Active_Files/Documents/{$recordId}/" . rawurlencode($fileName);
}
/**
* WebDAV PROPFIND запрос
*/
private function webdavPropfind(array $config, string $remotePath): array {
$url = $config['base_url'] . '/remote.php/dav/files/' . $config['username'] . $remotePath;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'PROPFIND',
CURLOPT_USERPWD => $config['username'] . ':' . $config['password'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/xml',
'Depth: 0'
],
CURLOPT_POSTFIELDS => '<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<d:getetag/>
<d:getlastmodified/>
<d:getcontentlength/>
<oc:fileid/>
</d:prop>
</d:propfind>'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception("cURL error: {$error}");
}
$result = ['status' => $httpCode];
if ($httpCode === 207 && $response) {
// Извлекаем данные с помощью регулярных выражений
// Ищем fileid
if (preg_match('/<oc:fileid>(\d+)<\/oc:fileid>/', $response, $matches)) {
$result['fileid'] = $matches[1];
}
// Ищем etag
if (preg_match('/<d:getetag>&quot;([^&]+)&quot;<\/d:getetag>/', $response, $matches)) {
$result['etag'] = $matches[1];
}
// Ищем размер
if (preg_match('/<d:getcontentlength>(\d+)<\/d:getcontentlength>/', $response, $matches)) {
$result['size'] = (int)$matches[1];
}
// Ищем дату модификации
if (preg_match('/<d:getlastmodified>([^<]+)<\/d:getlastmodified>/', $response, $matches)) {
$result['mtime'] = $matches[1];
}
}
return $result;
}
}
?>