Обновленные файлы: - crm_extensions/nextcloud_editor/js/nextcloud-editor.js (5 путей) - crm_extensions/file_storage/api/get_edit_urls.php (6 путей) - modules/Documents/actions/NcPrepareEdit.php (2 пути) - crm_extensions/file_storage/api/prepare_edit.php (1 путь) - crm_extensions/file_storage/NextcloudClient.php (1 путь) - data/CRMEntity.php (nc_path для новых файлов) Все пути теперь используют /crm/ вместо /crm2/ для соответствия новому External Storage на office.clientright.ru
368 lines
13 KiB
PHP
368 lines
13 KiB
PHP
<?php
|
||
/**
|
||
* Nextcloud WebDAV Client (Fixed)
|
||
* Клиент для работы с Nextcloud через WebDAV API
|
||
*/
|
||
|
||
class NextcloudClient {
|
||
private $baseUrl;
|
||
private $username;
|
||
private $password;
|
||
private $activeFolder;
|
||
|
||
public function __construct($config) {
|
||
$this->baseUrl = rtrim($config['base_url'], '/');
|
||
$this->username = $config['username'];
|
||
$this->password = $config['password'];
|
||
$this->activeFolder = trim($config['active_folder'], '/');
|
||
}
|
||
|
||
/**
|
||
* Загрузка файла в Nextcloud
|
||
*/
|
||
public function uploadFile($localPath, $remotePath) {
|
||
try {
|
||
// Убираем activeFolder из remotePath если он уже там есть
|
||
if (strpos($remotePath, $this->activeFolder) === 0) {
|
||
$fullRemotePath = $remotePath;
|
||
} else {
|
||
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
|
||
}
|
||
|
||
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
|
||
|
||
echo "Uploading to URL: $url\n";
|
||
|
||
// Создаём папку если не существует
|
||
$this->ensureFolderExists(dirname($fullRemotePath));
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||
CURLOPT_INFILE => fopen($localPath, 'r'),
|
||
CURLOPT_INFILESIZE => filesize($localPath),
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->password,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 120,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: ' . $this->getMimeType($localPath)
|
||
]
|
||
]);
|
||
|
||
$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);
|
||
}
|
||
|
||
if ($httpCode < 200 || $httpCode >= 300) {
|
||
throw new Exception('HTTP error: ' . $httpCode . ' - ' . $response);
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'remote_path' => $fullRemotePath,
|
||
'url' => $url
|
||
];
|
||
|
||
} catch (Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Создание папки (публичный метод)
|
||
*/
|
||
public function createFolder($folderPath) {
|
||
try {
|
||
$this->ensureFolderExists($folderPath);
|
||
return [
|
||
'success' => true,
|
||
'folder_path' => $folderPath
|
||
];
|
||
} catch (Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Создание папки (улучшенная версия)
|
||
*/
|
||
private function ensureFolderExists($folderPath) {
|
||
if (empty($folderPath) || $folderPath === '.') {
|
||
return;
|
||
}
|
||
|
||
// Разбиваем путь на части
|
||
$parts = explode('/', trim($folderPath, '/'));
|
||
$currentPath = '';
|
||
|
||
foreach ($parts as $part) {
|
||
if (empty($part)) continue;
|
||
|
||
$currentPath .= '/' . $part;
|
||
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . $currentPath;
|
||
|
||
echo "Creating folder: $url\n";
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_CUSTOMREQUEST => 'MKCOL',
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->password,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 30
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$error = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
echo "Folder creation result: HTTP $httpCode\n";
|
||
|
||
// 201 = created, 405 = already exists, оба OK
|
||
if ($httpCode !== 201 && $httpCode !== 405) {
|
||
echo "Response: $response\n";
|
||
if ($error) {
|
||
throw new Exception("Curl error creating folder '$currentPath': $error");
|
||
} else {
|
||
throw new Exception("HTTP error creating folder '$currentPath': $httpCode - $response");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получение ID файла из Nextcloud
|
||
*/
|
||
public function getFileId($remotePath) {
|
||
try {
|
||
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
|
||
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_CUSTOMREQUEST => 'PROPFIND',
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->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>
|
||
<oc:fileid/>
|
||
</d:prop>
|
||
</d:propfind>'
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($httpCode === 207 && $response) {
|
||
$xml = simplexml_load_string($response);
|
||
if ($xml) {
|
||
foreach ($xml->xpath('//oc:fileid') as $element) {
|
||
return (string)$element;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
|
||
} catch (Exception $e) {
|
||
error_log("Error getting file ID: " . $e->getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Создание прямой ссылки для редактирования файла
|
||
*/
|
||
public function createDirectEditLink($remotePath, $recordId, $fileName = null) {
|
||
// Создаем ссылку на файловый менеджер Nextcloud с открытием файла
|
||
if ($fileName === null) {
|
||
$fileName = basename($remotePath);
|
||
}
|
||
// Убираем подчеркивание в начале, если есть
|
||
if (substr($fileName, 0, 1) === '_') {
|
||
$fileName = substr($fileName, 1);
|
||
}
|
||
$editUrl = $this->baseUrl . '/apps/files/?dir=/crm/CRM_Active_Files/Documents/' . $recordId . '&openfile=' . urlencode($fileName);
|
||
|
||
return [
|
||
'success' => true,
|
||
'edit_url' => $editUrl,
|
||
'direct_url' => $editUrl,
|
||
'file_id' => null
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Создание ссылки для редактирования файла
|
||
*/
|
||
public function createEditLink($remotePath, $permissions = 3) {
|
||
try {
|
||
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
|
||
|
||
// Создаём публичную ссылку через OCS API
|
||
$url = $this->baseUrl . '/ocs/v2.php/apps/files_sharing/api/v1/shares';
|
||
|
||
$postData = http_build_query([
|
||
'path' => '/' . $fullRemotePath,
|
||
'shareType' => 3, // Public link
|
||
'permissions' => $permissions, // 1=read, 2=update, 3=read+update
|
||
'format' => 'json'
|
||
]);
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $postData,
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->password,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 30,
|
||
CURLOPT_HTTPHEADER => [
|
||
'OCS-APIRequest: true',
|
||
'Content-Type: application/x-www-form-urlencoded'
|
||
]
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($httpCode !== 200) {
|
||
throw new Exception('Failed to create share link: HTTP ' . $httpCode . ' - ' . $response);
|
||
}
|
||
|
||
$data = json_decode($response, true);
|
||
|
||
if (!$data || $data['ocs']['meta']['statuscode'] !== 200) {
|
||
$message = $data['ocs']['meta']['message'] ?? 'Unknown OCS error';
|
||
throw new Exception('OCS error: ' . $message);
|
||
}
|
||
|
||
$shareUrl = $data['ocs']['data']['url'];
|
||
|
||
// Для редактирования добавляем параметр
|
||
$editUrl = $shareUrl . '/edit';
|
||
|
||
return [
|
||
'success' => true,
|
||
'share_url' => $shareUrl,
|
||
'edit_url' => $editUrl,
|
||
'share_id' => $data['ocs']['data']['id']
|
||
];
|
||
|
||
} catch (Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Проверка существования файла
|
||
*/
|
||
public function fileExists($remotePath) {
|
||
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
|
||
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_CUSTOMREQUEST => 'HEAD',
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->password,
|
||
CURLOPT_NOBODY => true,
|
||
CURLOPT_TIMEOUT => 10,
|
||
CURLOPT_RETURNTRANSFER => true
|
||
]);
|
||
|
||
curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
return $httpCode === 200;
|
||
}
|
||
|
||
/**
|
||
* Скачивание файла из Nextcloud
|
||
*/
|
||
public function downloadToTemp($remotePath) {
|
||
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
|
||
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
|
||
|
||
$tempFile = sys_get_temp_dir() . '/' . uniqid('nextcloud_download_') . '_' . basename($remotePath);
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->password,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 120
|
||
]);
|
||
|
||
$result = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($httpCode !== 200) {
|
||
throw new Exception('Download failed: HTTP ' . $httpCode);
|
||
}
|
||
|
||
file_put_contents($tempFile, $result);
|
||
return $tempFile;
|
||
}
|
||
|
||
/**
|
||
* Удаление файла
|
||
*/
|
||
public function deleteFile($remotePath) {
|
||
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
|
||
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||
CURLOPT_USERPWD => $this->username . ':' . $this->password,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 30
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
return $httpCode === 204; // 204 = No Content (успешное удаление)
|
||
}
|
||
|
||
/**
|
||
* Получение MIME типа файла
|
||
*/
|
||
private function getMimeType($filePath) {
|
||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||
$mimeType = finfo_file($finfo, $filePath);
|
||
finfo_close($finfo);
|
||
return $mimeType ?: 'application/octet-stream';
|
||
}
|
||
}
|