Files
crm.clientright.ru/layouts/v7/lib/nextcloud-editor.js
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

442 lines
18 KiB
JavaScript
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.

/**
* Nextcloud Editor Integration JavaScript
* JavaScript для интеграции редактора документов Nextcloud
*/
/**
* Открытие редактора Nextcloud для документа
*/
function openNextcloudEditor(recordId, fileName) {
console.log('🚀 NEXTCLOUD EDITOR: Function called!', recordId, fileName);
// ПРОСТОЕ РЕШЕНИЕ - используем промежуточную страницу для редиректа!
const redirectUrl = `/crm_extensions/file_storage/api/open_file.php?recordId=${recordId}&fileName=${encodeURIComponent(fileName)}`;
console.log('🎯 Opening editor via redirect:', redirectUrl);
// Открываем редактор в новом окне через промежуточную страницу
const win = window.open(redirectUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
if (win) {
console.log('✅ Editor opened successfully');
alert('✅ Редактор открыт! Файл: ' + fileName);
} else {
console.log('❌ Failed to open editor window - popup blocked');
alert('❌ Не удалось открыть редактор. Проверьте блокировку всплывающих окон.');
}
}
function testSimpleAPI(recordId, fileName) {
console.log('🧪 Testing simple API...', recordId, fileName);
// Пропускаем простой API и сразу идем к основному
console.log('🎯 Skipping simple API, going to main API...');
callMainAPI(recordId, fileName);
}
function createEditUrls(baseEditUrl, recordId, fileName, fileId = 662) {
console.log('🔗 Creating edit URLs in JavaScript...', { fileId, fileName });
// Извлекаем базовый URL из базовой ссылки
const baseUrl = 'https://office.clientright.ru:8443';
const encodedFileName = encodeURIComponent(fileName);
const filePath = `/crm/crm2/CRM_Active_Files/Documents/${recordId}/${encodedFileName}`;
// Токен для RichDocuments (из настроек Nextcloud)
const richDocumentsToken = '1sanuq71b3n4fm1ldkbb';
const urls = {
// ЛУЧШИЙ СПОСОБ! - редирект через нашу промежуточную страницу (БЕЗ CSRF!)
'redirect_to_nextcloud': fileId ? `/crm_extensions/file_storage/api/open_file.php?fileId=${fileId}&fileName=${encodedFileName}&recordId=${recordId}` : null,
// РАБОЧИЙ СПОСОБ! - прямая ссылка на Nextcloud
'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}` : `${baseUrl}/apps/files/?dir=/&openfile=${encodedFileName}`,
// Файловый менеджер
'files_manager': `${baseUrl}/apps/files/?dir=/&openfile=${encodedFileName}`
};
// Убираем null значения
Object.keys(urls).forEach(key => {
if (urls[key] === null) {
delete urls[key];
}
});
return {
all: urls,
// РЕДИРЕКТ через нашу страницу - лучший способ (обходит CSRF)
recommended: urls.redirect_to_nextcloud || urls.files_editing_auto || urls.files_editing || urls.collabora_editor
};
}
function getEditUrls(recordId, fileName, simpleData) {
console.log('🔗 Getting edit URLs...');
$.ajax({
url: '/crm_extensions/file_storage/api/get_edit_urls.php',
method: 'GET',
data: {
record: recordId,
fileName: fileName
},
dataType: 'json',
success: function(response) {
console.log('✅ Edit URLs received:', response);
if (response.success) {
// Пробуем открыть рекомендуемый URL
const recommendedUrl = response.data.urls[response.data.recommended];
console.log('🎯 Trying recommended URL:', recommendedUrl);
openEditor(recommendedUrl, {
...simpleData,
urls: response.data.urls,
recommended: response.data.recommended
});
} else {
// Если не получилось, используем простой API
openEditor(simpleData.edit_url, simpleData);
}
},
error: function(xhr, status, error) {
console.error('❌ Failed to get edit URLs:', error);
// Если не получилось, используем простой API
openEditor(simpleData.edit_url, simpleData);
}
});
}
function callMainAPI(recordId, fileName) {
console.log('🎯 Calling main API...', recordId, fileName);
// Показываем прогресс
if (typeof app !== 'undefined' && app.helper && app.helper.showProgress) {
app.helper.showProgress({
message: 'Подготовка файла к редактированию...'
});
}
// Вызываем ПРАВИЛЬНЫЙ API для подготовки файла
$.ajax({
url: '/index.php?module=Documents&action=NcPrepareEdit',
method: 'GET',
data: {
record: recordId,
fileName: fileName
},
dataType: 'json',
success: function(response) {
console.log('📡 API Response:', response);
// Скрываем прогресс
if (typeof app !== 'undefined' && app.helper && app.helper.hideProgress) {
app.helper.hideProgress();
}
if (response.success) {
console.log('✅ File prepared successfully');
// Открываем редактор
openEditor(response.data.edit_url, response.data);
} else {
console.error('❌ API Error:', response.error);
showError('Ошибка подготовки файла: ' + response.error);
}
},
error: function(xhr, status, error) {
console.error('❌ AJAX Error:', error);
console.error('❌ Status:', status);
console.error('❌ Response:', xhr.responseText);
console.error('❌ Status Code:', xhr.status);
// Скрываем прогресс
if (typeof app !== 'undefined' && app.helper && app.helper.hideProgress) {
app.helper.hideProgress();
}
// Показываем подробную ошибку
let errorMessage = 'Ошибка подключения к серверу: ' + error;
if (xhr.responseText) {
errorMessage += '\n\nОтвет сервера:\n' + xhr.responseText;
}
showError(errorMessage);
}
});
}
// Функция для открытия редактора
function openEditor(editUrl, fileData) {
console.log('🎯 Opening editor with URL:', editUrl);
console.log('📋 All available URLs:', fileData.urls);
// Открываем редактор в новом окне
var win = window.open(editUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
if (win) {
console.log('✅ Editor opened successfully');
// Показываем уведомление об успехе
if (typeof app !== 'undefined' && app.helper && app.helper.showSuccessNotification) {
app.helper.showSuccessNotification({
message: 'Редактор открыт! Файл: ' + fileData.file_name
});
} else {
alert('✅ Редактор открыт! Файл: ' + fileData.file_name);
}
// Показываем информацию о файле
showFileInfo(fileData);
} else {
console.log('❌ Failed to open editor window - popup blocked or error');
console.log('📋 Showing modal with alternative URLs');
// Показываем модальное окно с альтернативными вариантами
showModalWithUrls(editUrl, fileData);
}
}
// Функция для отображения информации о файле
function showFileInfo(fileData) {
// Проверяем, есть ли нужные поля в fileData
var location = 'Неизвестно';
var nextcloudPath = 'Не указан';
var status = 'Не указан';
if (fileData.file_location && fileData.file_location.type) {
location = fileData.file_location.type === 's3' ? 'S3 хранилище' : 'Локальное хранилище';
}
if (fileData.nextcloud_path) {
nextcloudPath = fileData.nextcloud_path;
}
if (fileData.message) {
status = fileData.message;
}
var infoHtml = `
<div class="alert alert-info" style="margin: 10px 0;">
<h5><i class="fa fa-info-circle"></i> Информация о файле</h5>
<p><strong>Файл:</strong> ${fileData.file_name}</p>
<p><strong>Расположение:</strong> ${location}</p>
<p><strong>Путь в Nextcloud:</strong> ${nextcloudPath}</p>
<p><strong>Статус:</strong> ${status}</p>
</div>
`;
// Если есть альтернативные URL, добавляем их
if (fileData.urls) {
infoHtml += `
<div class="alert alert-warning" style="margin: 10px 0;">
<h6><i class="fa fa-link"></i> Альтернативные способы открытия:</h6>
<div class="btn-group-vertical" style="width: 100%;">
`;
Object.keys(fileData.urls).forEach(function(key) {
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const isRecommended = key === fileData.recommended;
const btnClass = isRecommended ? 'btn-success' : 'btn-default';
const icon = isRecommended ? 'fa-star' : 'fa-external-link';
infoHtml += `
<a href="${fileData.urls[key]}" target="_blank" class="btn ${btnClass} btn-sm" style="margin: 2px; text-align: left;">
<i class="fa ${icon}"></i> ${label}${isRecommended ? ' (рекомендуется)' : ''}
</a>
`;
});
infoHtml += `
</div>
</div>
`;
}
// Добавляем информацию в модальное окно или показываем отдельно
if ($('#nextcloudEditModal').length) {
$('#nextcloudEditModal .modal-body').prepend(infoHtml);
} else {
// Создаём временное уведомление
var notification = $('<div class="alert alert-info" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">' + infoHtml + '</div>');
$('body').append(notification);
// Автоматически скрываем через 5 секунд
setTimeout(function() {
notification.fadeOut(function() {
notification.remove();
});
}, 5000);
}
}
// Функция для отображения ошибок
function showError(message) {
console.error('🚨 Error:', message);
if (typeof app !== 'undefined' && app.helper && app.helper.showErrorNotification) {
app.helper.showErrorNotification({
message: message
});
} else {
alert('❌ Ошибка: ' + message);
}
}
// Функция для показа модального окна с URL
function showModalWithUrls(editUrl, fileData) {
console.log('📋 Showing modal with URLs for file:', fileData.file_name);
if (fileData.urls) {
// Используем существующую функцию showEditOptions
showEditOptions(fileData.urls, fileData.file_name, fileData.record_id);
} else {
// Если нет альтернативных URL, показываем ошибку
showError('Не удалось открыть редактор. Попробуйте открыть файл вручную в Nextcloud.');
}
}
/**
* Показ вариантов открытия файла
*/
function showEditOptions(urls, fileName, recordId) {
console.log('🎯 Showing edit options for:', fileName);
console.log('📋 Available URLs:', urls);
var buttonsHtml = '';
// Если urls - это объект (новый формат)
if (typeof urls === 'object' && !Array.isArray(urls)) {
Object.keys(urls).forEach(function(key, index) {
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const url = urls[key];
const isRecommended = key === 'correct_path';
const btnClass = isRecommended ? 'btn-success' : 'btn-primary';
const icon = isRecommended ? 'fa-star' : 'fa-external-link';
buttonsHtml += `
<a href="${url}" target="_blank" class="btn ${btnClass} btn-sm" style="margin: 3px; display: block;">
<i class="fa ${icon}"></i> ${label}${isRecommended ? ' (рекомендуется)' : ''}
</a>
`;
});
} else {
// Старый формат - массив URL
var labels = [
'С параметром openfile',
'С action=edit',
'С edit=true',
'Через RichDocuments',
'Через OnlyOffice'
];
var icons = ['fa-file', 'fa-edit', 'fa-pencil', 'fa-file-text', 'fa-file-word-o'];
var colors = ['btn-primary', 'btn-success', 'btn-info', 'btn-warning', 'btn-danger'];
urls.forEach(function(url, index) {
buttonsHtml += `
<a href="${url}" target="_blank" class="btn ${colors[index]} btn-sm" style="margin: 3px; display: block;">
<i class="fa ${icons[index]}"></i> ${labels[index]}
</a>
`;
});
}
var modalHtml = `
<div class="modal fade" id="nextcloudEditModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fa fa-edit"></i> Варианты открытия файла
</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p><strong>Файл:</strong> ${fileName}</p>
<p>Попробуйте эти варианты для прямого открытия в редакторе:</p>
<div class="btn-group-vertical" style="width: 100%;">
${buttonsHtml}
</div>
<div class="alert alert-info" style="margin-top: 15px;">
<small><strong>💡 Совет:</strong> Попробуйте варианты сверху вниз. Один из них должен открыть файл сразу в редакторе!</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
`;
// Удаляем старое модальное окно
$('#nextcloudEditModal').remove();
// Добавляем новое
$('body').append(modalHtml);
$('#nextcloudEditModal').modal('show');
}
/**
* Синхронизация изменений файла (заглушка)
*/
function syncFileChanges(recordId, fileName) {
console.log('Syncing file changes for:', recordId, fileName);
if (typeof app !== 'undefined' && app.helper && app.helper.showSuccessNotification) {
app.helper.showSuccessNotification({
message: 'Изменения синхронизированы!'
});
} else {
alert('✅ Изменения синхронизированы!');
}
}
/**
* Открытие редактора в новом окне
*/
function openInNewWindow(editUrl) {
window.open(editUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes,toolbar=no,location=no');
console.log('Opened Nextcloud editor in new window');
}
// Алиас функции для обратной совместимости
function editInNextcloud(recordId, fileName) {
console.log('📝 editInNextcloud called (alias)');
return openNextcloudEditor(recordId, fileName);
}
// Автоматическое подключение при загрузке страницы
$(document).ready(function() {
console.log('Nextcloud Editor integration loaded');
// Добавляем CSS стили для модального окна
if (!$('#nextcloud-editor-styles').length) {
$('<style id="nextcloud-editor-styles">')
.html(`
.nextcloud-edit-btn {
background: #0082c9;
border-color: #0082c9;
}
.nextcloud-edit-btn:hover {
background: #006ba6;
border-color: #006ba6;
}
`)
.appendTo('head');
}
});