Add OnlyOfficeTemplates module
This commit is contained in:
24
languages/en_us/OnlyOfficeTemplates.php
Normal file
24
languages/en_us/OnlyOfficeTemplates.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
$languageStrings = [
|
||||
'LBL_OOT_TEMPLATES' => 'Document templates',
|
||||
'LBL_OOT_SELECT_TEMPLATE' => 'Template',
|
||||
'LBL_OOT_FORMAT' => 'Format',
|
||||
'LBL_OOT_FORMAT_PDF' => 'PDF',
|
||||
'LBL_OOT_FORMAT_DOCX' => 'DOCX',
|
||||
'LBL_OOT_DOWNLOAD' => 'Download',
|
||||
'LBL_OOT_SAVE_TO_DOCUMENTS' => 'Save to Documents',
|
||||
'LBL_OOT_NO_TEMPLATES' => 'No templates for this module',
|
||||
'LBL_OOT_EMPTY_LIST' => 'No templates found',
|
||||
'LBL_OOT_NAME' => 'Name',
|
||||
'LBL_OOT_MODULE' => 'Module',
|
||||
'LBL_OOT_FILE' => 'File',
|
||||
'LBL_OOT_CREATED_AT' => 'Created',
|
||||
'LBL_OOT_ADD_TEMPLATE' => 'Add template',
|
||||
'LBL_OOT_FILE_HINT' => 'DOCX files only',
|
||||
'LBL_OOT_EDIT_TEMPLATE' => 'Edit template',
|
||||
'LBL_OOT_NEW_TEMPLATE' => 'New template',
|
||||
'LBL_OOT_EDITOR_HINT' => 'Edit the document on the right. Saving to S3 happens automatically when closing or when clicking Save in the editor.',
|
||||
'LBL_OOT_EDITOR_FALLBACK' => 'If OnlyOffice is not configured, you can upload a DOCX file instead.',
|
||||
'LBL_OOT_ADD_VIA_UPLOAD' => 'Upload DOCX file',
|
||||
'LBL_OOT_UPLOAD_FILE' => 'Upload file',
|
||||
];
|
||||
24
languages/ru_ru/OnlyOfficeTemplates.php
Normal file
24
languages/ru_ru/OnlyOfficeTemplates.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
$languageStrings = [
|
||||
'LBL_OOT_TEMPLATES' => 'Шаблоны документов',
|
||||
'LBL_OOT_SELECT_TEMPLATE' => 'Шаблон',
|
||||
'LBL_OOT_FORMAT' => 'Формат',
|
||||
'LBL_OOT_FORMAT_PDF' => 'PDF',
|
||||
'LBL_OOT_FORMAT_DOCX' => 'DOCX',
|
||||
'LBL_OOT_DOWNLOAD' => 'Скачать',
|
||||
'LBL_OOT_SAVE_TO_DOCUMENTS' => 'Сохранить в Документы',
|
||||
'LBL_OOT_NO_TEMPLATES' => 'Нет шаблонов для этого модуля',
|
||||
'LBL_OOT_EMPTY_LIST' => 'Шаблоны не найдены',
|
||||
'LBL_OOT_NAME' => 'Название',
|
||||
'LBL_OOT_MODULE' => 'Модуль',
|
||||
'LBL_OOT_FILE' => 'Файл',
|
||||
'LBL_OOT_CREATED_AT' => 'Создан',
|
||||
'LBL_OOT_ADD_TEMPLATE' => 'Добавить шаблон',
|
||||
'LBL_OOT_FILE_HINT' => 'Только файлы DOCX',
|
||||
'LBL_OOT_EDIT_TEMPLATE' => 'Редактирование шаблона',
|
||||
'LBL_OOT_NEW_TEMPLATE' => 'Новый шаблон',
|
||||
'LBL_OOT_EDITOR_HINT' => 'Редактируйте документ справа. Сохранение в S3 происходит автоматически при закрытии или по кнопке «Сохранить» в редакторе.',
|
||||
'LBL_OOT_EDITOR_FALLBACK' => 'Если OnlyOffice не настроен, можно загрузить готовый DOCX-файл.',
|
||||
'LBL_OOT_ADD_VIA_UPLOAD' => 'Загрузить DOCX-файл',
|
||||
'LBL_OOT_UPLOAD_FILE' => 'Загрузить файл',
|
||||
];
|
||||
54
layouts/v7/modules/OnlyOfficeTemplates/AddTemplate.tpl
Normal file
54
layouts/v7/modules/OnlyOfficeTemplates/AddTemplate.tpl
Normal file
@@ -0,0 +1,54 @@
|
||||
{strip}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h4 class="pull-left">{vtranslate('LBL_OOT_ADD_TEMPLATE', $MODULE_NAME)}</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
{if $ERROR_MSG}
|
||||
<div class="alert alert-danger">{$ERROR_MSG|escape}</div>
|
||||
{/if}
|
||||
<form action="index.php" method="post" enctype="multipart/form-data" class="form-horizontal">
|
||||
<input type="hidden" name="module" value="OnlyOfficeTemplates" />
|
||||
<input type="hidden" name="action" value="UploadTemplate" />
|
||||
<input type="hidden" name="redirect" value="List" />
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-3">{vtranslate('LBL_OOT_NAME', $MODULE_NAME)} <span class="redColor">*</span></label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" name="name" class="form-control" required="required" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-3">{vtranslate('LBL_OOT_MODULE', $MODULE_NAME)} <span class="redColor">*</span></label>
|
||||
<div class="col-sm-9">
|
||||
<select name="module_name" class="form-control" required="required">
|
||||
<option value="">-- {vtranslate('LBL_SELECT_OPTION','Vtiger')} --</option>
|
||||
{foreach from=$MODULES key=modName item=modLabel}
|
||||
<option value="{$modName}">{$modLabel}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-3">{vtranslate('LBL_OOT_FILE', $MODULE_NAME)} (DOCX) <span class="redColor">*</span></label>
|
||||
<div class="col-sm-9">
|
||||
<input type="file" name="file" accept=".docx" required="required" />
|
||||
<span class="help-block">{vtranslate('LBL_OOT_FILE_HINT', $MODULE_NAME)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-9">
|
||||
<button type="submit" class="btn btn-success">{vtranslate('LBL_SAVE', $MODULE_NAME)}</button>
|
||||
<a href="index.php?module=OnlyOfficeTemplates&view=List&app=TOOLS" class="btn btn-default">{vtranslate('LBL_CANCEL', $MODULE_NAME)}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/strip}
|
||||
76
layouts/v7/modules/OnlyOfficeTemplates/Edit.tpl
Normal file
76
layouts/v7/modules/OnlyOfficeTemplates/Edit.tpl
Normal file
@@ -0,0 +1,76 @@
|
||||
{strip}
|
||||
<div class="contents tabbable">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 left-block" style="border-right: 1px solid #ddd;">
|
||||
<h4>{if $TEMPLATE.id gt 0}{vtranslate('LBL_OOT_EDIT_TEMPLATE', $MODULE_NAME)}{else}{vtranslate('LBL_OOT_ADD_TEMPLATE', $MODULE_NAME)}{/if}</h4>
|
||||
{if $ERROR_MSG}<div class="alert alert-danger">{$ERROR_MSG|escape}</div>{/if}
|
||||
<form id="ootMetadataForm" action="index.php" method="post" class="form-horizontal">
|
||||
<input type="hidden" name="module" value="OnlyOfficeTemplates" />
|
||||
<input type="hidden" name="action" value="SaveMetadata" />
|
||||
<input type="hidden" name="templateid" value="{$TEMPLATE.id}" />
|
||||
<input type="hidden" name="redirect" value="Edit" />
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-4">{vtranslate('LBL_OOT_NAME', $MODULE_NAME)}</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" name="name" class="form-control" value="{$TEMPLATE.name|escape}" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-4">{vtranslate('LBL_OOT_MODULE', $MODULE_NAME)}</label>
|
||||
<div class="col-sm-8">
|
||||
<select name="module_name" class="form-control" required>
|
||||
<option value="">-- {vtranslate('LBL_SELECT_OPTION','Vtiger')} --</option>
|
||||
{foreach from=$MODULES key=modName item=modLabel}
|
||||
<option value="{$modName}" {if $TEMPLATE.module eq $modName}selected{/if}>{$modLabel}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-4 col-sm-8">
|
||||
<button type="submit" class="btn btn-success">{vtranslate('LBL_SAVE', $MODULE_NAME)}</button>
|
||||
<a href="index.php?module=OnlyOfficeTemplates&view=List&app=TOOLS" class="btn btn-default">{vtranslate('LBL_CANCEL', $MODULE_NAME)}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-xs-8" style="min-height: 600px;">
|
||||
{if $OOT_EDITOR_AVAILABLE}
|
||||
<p class="text-muted">{vtranslate('LBL_OOT_EDITOR_HINT', $MODULE_NAME)}</p>
|
||||
<div id="ootOnlyOfficeEditor" style="width:100%; height:700px;"></div>
|
||||
<script src="{$OOT_DOCUMENT_SERVER}/web-apps/apps/api/documents/api.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
var docKey = "{$OOT_DOC_KEY|escape:'javascript'}";
|
||||
var config = {
|
||||
document: {
|
||||
fileType: "docx",
|
||||
key: docKey,
|
||||
title: "{$OOT_DOC_TITLE|escape:'javascript'}",
|
||||
url: "{$OOT_DOCUMENT_URL|escape:'javascript'}"
|
||||
},
|
||||
documentType: "word",
|
||||
editorConfig: {
|
||||
callbackUrl: "{$OOT_CALLBACK_URL|escape:'javascript'}",
|
||||
mode: "edit",
|
||||
lang: "ru"
|
||||
},
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
};
|
||||
if (typeof DocsAPI !== "undefined") {
|
||||
new DocsAPI.DocEditor("ootOnlyOfficeEditor", config);
|
||||
} else {
|
||||
document.getElementById("ootOnlyOfficeEditor").innerHTML = "<div class=\"alert alert-warning\">Не удалось загрузить OnlyOffice. Проверьте ONLYOFFICE_DOCUMENT_SERVER.</div>";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{else}
|
||||
<div class="alert alert-info">{$OOT_EDITOR_MESSAGE|escape}</div>
|
||||
<p>{vtranslate('LBL_OOT_EDITOR_FALLBACK', $MODULE_NAME)}</p>
|
||||
<a href="index.php?module=OnlyOfficeTemplates&view=AddTemplate&app=TOOLS" class="btn btn-primary">{vtranslate('LBL_OOT_ADD_VIA_UPLOAD', $MODULE_NAME)}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/strip}
|
||||
@@ -0,0 +1,65 @@
|
||||
{*
|
||||
OnlyOfficeTemplates widget: template list, format (PDF/DOCX), Download / Save to Documents
|
||||
*}
|
||||
{if $CRM_TEMPLATES_EXIST eq 0}
|
||||
<li class="dropdown-header">
|
||||
<span class="fa fa-file-text-o"></span> {vtranslate('LBL_OOT_TEMPLATES','OnlyOfficeTemplates')}
|
||||
</li>
|
||||
<li class="oot-widget" data-record="{$ID}" data-module="{$MODULE}">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{vtranslate('LBL_OOT_SELECT_TEMPLATE','OnlyOfficeTemplates')}</label>
|
||||
<select class="oot-template-id form-control input-sm">
|
||||
{foreach from=$CRM_TEMPLATES item=tpl}
|
||||
<option value="{$tpl.id}">{$tpl.name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{vtranslate('LBL_OOT_FORMAT','OnlyOfficeTemplates')}</label>
|
||||
<select class="oot-format form-control input-sm">
|
||||
<option value="pdf">{vtranslate('LBL_OOT_FORMAT_PDF','OnlyOfficeTemplates')}</option>
|
||||
<option value="docx">{vtranslate('LBL_OOT_FORMAT_DOCX','OnlyOfficeTemplates')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="btn-group btn-group-justified">
|
||||
<a href="javascript:;" class="oot-download"><i class="fa fa-download"></i> {vtranslate('LBL_OOT_DOWNLOAD','OnlyOfficeTemplates')}</a>
|
||||
<a href="javascript:;" class="oot-save-to-docs"><i class="fa fa-save"></i> {vtranslate('LBL_OOT_SAVE_TO_DOCUMENTS','OnlyOfficeTemplates')}</a>
|
||||
</div>
|
||||
</li>
|
||||
{else}
|
||||
<li><span class="text-muted">{vtranslate('LBL_OOT_NO_TEMPLATES','OnlyOfficeTemplates')}</span></li>
|
||||
{/if}
|
||||
{if $CRM_TEMPLATES_EXIST eq 0}
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var container = document.querySelector('.oot-widget');
|
||||
if (!container) return;
|
||||
container.addEventListener('click', function(e) {
|
||||
var t = e.target.closest('.oot-download, .oot-save-to-docs');
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
var record = container.getAttribute('data-record'), module = container.getAttribute('data-module');
|
||||
var sel = container.querySelector('.oot-template-id'), fmt = container.querySelector('.oot-format');
|
||||
var templateId = sel ? sel.value : '', format = fmt ? fmt.value : 'pdf';
|
||||
var mode = t.classList.contains('oot-download') ? 'download' : 'save_to_documents';
|
||||
var url = 'index.php?module=OnlyOfficeTemplates&action=CreateFromTemplate&record=' + encodeURIComponent(record) + '&source_module=' + encodeURIComponent(module) + '&template_id=' + encodeURIComponent(templateId) + '&format=' + encodeURIComponent(format) + '&mode=' + encodeURIComponent(mode);
|
||||
if (mode === 'download') {
|
||||
window.location.href = url;
|
||||
} else {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url + '&ajax=1');
|
||||
xhr.onload = function() {
|
||||
var res = {};
|
||||
try { res = JSON.parse(xhr.responseText); } catch (err) {}
|
||||
if (res.success) {
|
||||
alert(res.message || 'Сохранено в Документы');
|
||||
} else {
|
||||
alert(res.error || 'Ошибка');
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{/if}
|
||||
47
layouts/v7/modules/OnlyOfficeTemplates/List.tpl
Normal file
47
layouts/v7/modules/OnlyOfficeTemplates/List.tpl
Normal file
@@ -0,0 +1,47 @@
|
||||
{strip}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h4 class="pull-left">{vtranslate('LBL_OOT_TEMPLATES', $MODULE_NAME)}</h4>
|
||||
<a href="index.php?module=OnlyOfficeTemplates&action=CreateDraft&app=TOOLS" class="btn btn-success pull-right">
|
||||
<i class="fa fa-plus"></i> {vtranslate('LBL_OOT_ADD_TEMPLATE', $MODULE_NAME)}
|
||||
</a>
|
||||
<a href="index.php?module=OnlyOfficeTemplates&view=AddTemplate&app=TOOLS" class="btn btn-default pull-right" style="margin-right:8px;">
|
||||
{vtranslate('LBL_OOT_UPLOAD_FILE', $MODULE_NAME)}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{if empty($TEMPLATES)}
|
||||
<div class="alert alert-info">{vtranslate('LBL_OOT_EMPTY_LIST', $MODULE_NAME)}</div>
|
||||
{else}
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{vtranslate('LBL_OOT_NAME', $MODULE_NAME)}</th>
|
||||
<th>{vtranslate('LBL_OOT_MODULE', $MODULE_NAME)}</th>
|
||||
<th>{vtranslate('LBL_OOT_FILE', $MODULE_NAME)}</th>
|
||||
<th>{vtranslate('LBL_OOT_CREATED_AT', $MODULE_NAME)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach from=$TEMPLATES item=TPL}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="index.php?module=OnlyOfficeTemplates&view=Edit&templateid={$TPL.id}&app=TOOLS">{$TPL.name|escape}</a>
|
||||
</td>
|
||||
<td>{$TPL.module|escape}</td>
|
||||
<td>{$TPL.file_name|escape}</td>
|
||||
<td>{$TPL.created_at|escape}</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/strip}
|
||||
158
modules/OnlyOfficeTemplates/DESCRIPTION.md
Normal file
158
modules/OnlyOfficeTemplates/DESCRIPTION.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# OnlyOfficeTemplates — подробное описание модуля
|
||||
|
||||
## Назначение
|
||||
|
||||
Модуль **OnlyOfficeTemplates** для ClientRight CRM (Vtiger-based) предназначен для:
|
||||
|
||||
- создания и хранения DOCX-шаблонов документов в S3;
|
||||
- редактирования шаблонов в веб-интерфейсе через OnlyOffice Document Editor (по аналогии с PDFMaker: слева метаданные, справа редактор);
|
||||
- генерации документов по шаблону с подстановкой полей записи и связанных модулей (плейсхолдеры `{{field}}`, `{{ModuleName__field}}`);
|
||||
- выдачи результата в формате **PDF** (через OnlyOffice Conversion API) или **DOCX**;
|
||||
- скачивания сгенерированного файла или сохранения в модуль «Документы» CRM.
|
||||
|
||||
Модуль портативный: конфигурация через переменные окружения или внешний конфиг, без жёсткой привязки к окружению.
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
### Хранение шаблонов
|
||||
|
||||
- Шаблоны хранятся в **S3-совместимом хранилище** по пути:
|
||||
`{OOT_S3_PREFIX}/templates/{template_id}/{имя_файла}.docx`
|
||||
(по умолчанию `OOT_S3_PREFIX` = `crm2/OnlyOfficeTemplates`).
|
||||
- Метаданные — в таблице БД `vtiger_oot_templates`: id, name, module, s3_key, file_name, owner, created_at.
|
||||
|
||||
### Редактирование шаблонов (как в PDFMaker)
|
||||
|
||||
- **Список шаблонов** (Инструменты → Шаблоны документов): таблица с именем (ссылка на редактирование), модулем, файлом, датой создания.
|
||||
- **Добавить шаблон:** создаётся черновик, открывается экран редактирования:
|
||||
- **Слева:** форма — название шаблона, выбор модуля CRM, кнопки «Сохранить» / «Отмена».
|
||||
- **Справа:** OnlyOffice Document Editor в iframe; документ загружается с нашего сервера (экшен GetDocument), при сохранении/закрытии OnlyOffice Document Server отправляет файл на callback, мы сохраняем его в S3 и обновляем запись в БД.
|
||||
- **Загрузить файл:** альтернативный способ — форма с полями «Название», «Модуль» и выбором DOCX-файла; отправка в экшен UploadTemplate (загрузка в S3 и запись в БД).
|
||||
- Редактирование существующего шаблона: по клику на имя в списке открывается тот же экран с подставленными метаданными и документом из S3.
|
||||
|
||||
### Генерация документов по шаблону
|
||||
|
||||
- На **карточке записи** (любой entity-модуль) в боковой панели отображается виджет OnlyOfficeTemplates:
|
||||
- выбор шаблона (по текущему модулю записи);
|
||||
- выбор формата: PDF или DOCX;
|
||||
- кнопки «Скачать» и «Сохранить в Документы».
|
||||
- Подстановка в шаблоне:
|
||||
- поля текущей записи: `{{fieldname}}`;
|
||||
- поля связанных модулей: `{{ModuleName__fieldname}}` (например `{{Account__accountname}}`).
|
||||
- Реализация: загрузка DOCX из S3, подстановка плейсхолдеров (PHPWord), при необходимости конвертация в PDF через OnlyOffice Conversion API, затем отдача файла или сохранение в Документы (в т.ч. в S3).
|
||||
|
||||
---
|
||||
|
||||
## Требования
|
||||
|
||||
- **PHP:** расширения zip, xml, curl (или allow_url_fopen для Conversion API).
|
||||
- **Composer:** пакеты `phpoffice/phpword`, `aws/aws-sdk-php` (как правило, уже в корне проекта).
|
||||
- **S3:** доступ к S3-совместимому хранилищу (ключ, секрет, endpoint, bucket).
|
||||
- **OnlyOffice (опционально):**
|
||||
- **Conversion API** — для выдачи PDF (URL в `OOT_ONLYOFFICE_CONVERT_URL` / `ONLYOFFICE_CONVERT_URL`).
|
||||
- **Document Server** — для экрана редактирования шаблона (URL в `ONLYOFFICE_DOCUMENT_SERVER` / `OOT_ONLYOFFICE_DOCUMENT_SERVER`). Document Server должен иметь доступ по HTTP(S) к CRM (загрузка документа и callback).
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
1. Скопировать в целевую CRM:
|
||||
- `modules/OnlyOfficeTemplates/` (все файлы);
|
||||
- `layouts/v7/modules/OnlyOfficeTemplates/` (все шаблоны и ресурсы).
|
||||
2. Настроить конфигурацию (см. ниже).
|
||||
3. Выполнить установку БД и виджетов:
|
||||
- **Рекомендуется:** из корня CRM выполнить
|
||||
`php modules/OnlyOfficeTemplates/install.php`
|
||||
или открыть в браузере соответствующий URL с правами администратора.
|
||||
- Альтернатива: упаковать модуль в zip с `manifest.xml` и импортировать через Module Manager.
|
||||
4. При необходимости перегенерировать кэш меню (например `parent_tabdata.php`), чтобы пункт «Шаблоны документов» отображался в разделе «Инструменты».
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Модуль читает настройки в следующем порядке.
|
||||
|
||||
### 1. Внешний конфиг
|
||||
|
||||
Если существует файл `crm_extensions/file_storage/config.php` и в нём возвращается массив с ключом `s3`, используются данные S3 оттуда. Имя бакета берётся из `s3['bucket']`, при отсутствии — из `bucket`, `s3_bucket` в корне массива или из переменной окружения `S3_BUCKET`.
|
||||
|
||||
### 2. Переменные окружения
|
||||
|
||||
Поддерживаются два способа загрузки переменных:
|
||||
|
||||
- **EnvLoader** (если есть `crm_extensions/shared/EnvLoader.php`): загружается файл `crm_extensions/.env`. Переменные попадают в `$_ENV` и в массив EnvLoader (модуль читает их через вспомогательную функцию, а не только через `getenv()`).
|
||||
- **getenv()** — если переменные заданы в окружении веб-сервера.
|
||||
|
||||
Используемые переменные:
|
||||
|
||||
| Переменная | Описание |
|
||||
|------------|----------|
|
||||
| `S3_ACCESS_KEY` | Ключ доступа S3 |
|
||||
| `S3_SECRET_KEY` | Секретный ключ S3 |
|
||||
| `S3_ENDPOINT` | URL эндпоинта S3 (напр. `https://s3.twcstorage.ru`) |
|
||||
| `S3_BUCKET` | Имя бакета S3 |
|
||||
| `S3_REGION` | Регион (по умолчанию `ru-1`) |
|
||||
| `OOT_S3_PREFIX` | Префикс папки модуля в S3 (по умолчанию `crm2/OnlyOfficeTemplates`) |
|
||||
| `OOT_ONLYOFFICE_CONVERT_URL` | URL OnlyOffice Conversion API (для PDF) |
|
||||
| `ONLYOFFICE_CONVERT_URL` | То же, альтернативное имя |
|
||||
| `OOT_ONLYOFFICE_DOCUMENT_SERVER` | URL OnlyOffice Document Server (для редактора шаблонов) |
|
||||
| `ONLYOFFICE_DOCUMENT_SERVER` | То же, альтернативное имя |
|
||||
| `OOT_DOCUMENT_SECRET` | Секрет для подписи URL документа (рекомендуется в продакшене) |
|
||||
| `OOT_DOCUMENTS_S3_PREFIX` | Префикс в S3 для файлов, сохраняемых в Документы (по умолчанию `crm2/CRM_Active_Files/Documents`) |
|
||||
|
||||
Без Conversion API генерация PDF недоступна (только DOCX). Без Document Server экран редактирования с OnlyOffice недоступен, но остаётся загрузка готового DOCX через «Загрузить файл».
|
||||
|
||||
---
|
||||
|
||||
## Структура файлов модуля
|
||||
|
||||
- **config.php** — загрузка конфигурации (внешний конфиг + .env), функция `OnlyOfficeTemplates_getConfig()` и вспомогательная `OnlyOfficeTemplates_env()` для чтения переменных из .env/EnvLoader.
|
||||
- **OnlyOfficeTemplates.php** — обработчик vtlib (установка/удаление таблиц, добавление/удаление виджета на карточках).
|
||||
- **schema.xml** — описание таблиц `vtiger_oot_templates`, `vtiger_oot_templates_seq`.
|
||||
- **models/OnlyOfficeTemplates_Model.php** — список шаблонов по модулю, получение шаблона по id, конфиг.
|
||||
- **resources/S3Helper.php** — работа с S3 (ключи шаблонов/temp, загрузка/скачивание).
|
||||
- **resources/MergeService.php** — подстановка плейсхолдеров в DOCX (PHPWord).
|
||||
- **resources/ConvertService.php** — конвертация DOCX → PDF через OnlyOffice Conversion API.
|
||||
- **actions/Install.php** — установка через браузер.
|
||||
- **actions/UploadTemplate.php** — загрузка DOCX (POST: name, module_name, file) и сохранение в S3 и БД.
|
||||
- **actions/CreateDraft.php** — создание черновика шаблона и редирект на экран редактирования.
|
||||
- **actions/SaveMetadata.php** — сохранение имени и модуля шаблона, редирект на Edit или List.
|
||||
- **actions/GetDocument.php** — отдача DOCX для OnlyOffice Document Server (из S3 или пустой документ); опциональная проверка токена (OOT_DOCUMENT_SECRET).
|
||||
- **actions/OnlyOfficeCallback.php** — приём callback от OnlyOffice Document Server при сохранении документа, скачивание файла по переданному URL и сохранение в S3, обновление записи в `vtiger_oot_templates`.
|
||||
- **actions/CreateFromTemplate.php** — генерация документа по шаблону (merge → опционально PDF → скачать или сохранить в Документы).
|
||||
- **views/List.php**, **views/Edit.php**, **views/AddTemplate.php**, **views/GetTemplateActions.php** — представления списка, редактирования (форма + OnlyOffice), загрузки файла и виджета на карточке.
|
||||
- **layouts/v7/modules/OnlyOfficeTemplates/** — шаблоны Smarty (List.tpl, Edit.tpl, AddTemplate.tpl, GetTemplateActions.tpl).
|
||||
- **languages/** — языковые строки (ru_ru, en_us).
|
||||
|
||||
---
|
||||
|
||||
## База данных
|
||||
|
||||
- **vtiger_oot_templates:** id, name, module, s3_key, file_name, owner, created_at (и при необходимости settings в формате JSON).
|
||||
- **vtiger_oot_templates_seq:** служебная таблица для генерации id при необходимости.
|
||||
|
||||
Регистрация модуля в меню: запись в `vtiger_tab` (например parent Tools), в `vtiger_parenttabrel`, в `vtiger_profile2tab` для прав доступа. Пункт меню кэшируется в `parent_tabdata.php` — при отсутствии пункта может потребоваться перегенерация кэша.
|
||||
|
||||
---
|
||||
|
||||
## Безопасность
|
||||
|
||||
- **GetDocument:** вызывается OnlyOffice Document Server без сессии пользователя. При заданном `OOT_DOCUMENT_SECRET` в URL документа добавляется подпись (HMAC), проверяемая в GetDocument; без секрета доступ по ссылке возможен для любого, кто знает template_id.
|
||||
- **OnlyOfficeCallback:** вызывается только Document Server; проверка прав пользователя не выполняется, идентификация по ключу документа (template id). В продакшене целесообразно ограничить доступ к callback по сети (например, только с хоста Document Server).
|
||||
- **UploadTemplate:** требуется право редактирования настроек (`Settings`, `Edit`) или иная выбранная при интеграции проверка.
|
||||
|
||||
---
|
||||
|
||||
## Версии и совместимость
|
||||
|
||||
- Модуль разработан для CRM на базе Vtiger (ClientRight), интерфейс v7.
|
||||
- Зависимости: PHP 7.x+, PHPWord, AWS SDK для PHP, S3-совместимое хранилище; OnlyOffice — опционально.
|
||||
|
||||
---
|
||||
|
||||
## Краткое описание для репозитория (Gitea)
|
||||
|
||||
**OnlyOfficeTemplates** — модуль ClientRight CRM для создания и хранения DOCX-шаблонов в S3, редактирования их в OnlyOffice Document Editor (интерфейс как в PDFMaker: слева метаданные, справа редактор с сохранением в S3 по callback), генерации документов по шаблону с подстановкой полей записи и связанных модулей, выдачи в PDF (через OnlyOffice Conversion API) или DOCX и сохранения в Документы. Конфигурация через .env или внешний конфиг; модуль портативный.
|
||||
38
modules/OnlyOfficeTemplates/INSTALL.md
Normal file
38
modules/OnlyOfficeTemplates/INSTALL.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Установка OnlyOfficeTemplates
|
||||
|
||||
## Шаги
|
||||
|
||||
1. **Скопировать файлы**
|
||||
- `modules/OnlyOfficeTemplates/` — целиком.
|
||||
- `layouts/v7/modules/OnlyOfficeTemplates/` — целиком.
|
||||
- Файлы языков из `languages/*/OnlyOfficeTemplates.php` (ru_ru, en_us и при необходимости другие).
|
||||
|
||||
2. **Настроить конфигурацию**
|
||||
- Либо используйте существующий `crm_extensions/file_storage/config.php` (S3 будет взят оттуда).
|
||||
- Либо задайте в .env: `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_ENDPOINT`, `S3_BUCKET`.
|
||||
- Для конвертации в PDF задайте `OOT_ONLYOFFICE_CONVERT_URL` (например `https://office.clientright.ru:9443/ConvertService.ashx`).
|
||||
|
||||
3. **Установить модуль в CRM**
|
||||
- Из корня CRM выполните:
|
||||
```bash
|
||||
php modules/OnlyOfficeTemplates/install.php
|
||||
```
|
||||
- Либо зарегистрируйте модуль вручную в vtiger_tab и выполните SQL из `schema.xml`, затем вызовите `$mod = new OnlyOfficeTemplates(); $mod->executeSql(); $mod->addLinksToEntityModules();`
|
||||
|
||||
4. **Добавить шаблоны**
|
||||
- Через экшен UploadTemplate (POST с полями name, module_name и файлом file).
|
||||
- Либо вручную: загрузить DOCX в S3 в `{OOT_S3_PREFIX}/templates/{id}/{filename}.docx` и вставить запись в `vtiger_oot_templates`.
|
||||
|
||||
## Проверка
|
||||
|
||||
- Откройте карточку любой записи модуля (например, Проект). В боковой панели должен появиться виджет «OnlyOffice Templates» со списком шаблонов (если они добавлены для этого модуля).
|
||||
- Выберите шаблон, формат (PDF или DOCX), нажмите «Скачать» или «Сохранить в Документы».
|
||||
|
||||
## Переменные окружения (кратко)
|
||||
|
||||
| Переменная | Описание |
|
||||
|------------|----------|
|
||||
| S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT, S3_BUCKET | Доступ к S3 |
|
||||
| OOT_S3_PREFIX | Префикс папки модуля в S3 (по умолчанию crm2/OnlyOfficeTemplates) |
|
||||
| OOT_ONLYOFFICE_CONVERT_URL | URL OnlyOffice Conversion API для DOCX→PDF |
|
||||
| OOT_DOCUMENTS_S3_PREFIX | Префикс пути при сохранении в Документы |
|
||||
107
modules/OnlyOfficeTemplates/OnlyOfficeTemplates.php
Normal file
107
modules/OnlyOfficeTemplates/OnlyOfficeTemplates.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* OnlyOfficeTemplates module - DOCX template merge and PDF/DOCX export.
|
||||
* Portable: config via .env or optional crm_extensions config.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates
|
||||
{
|
||||
public $db;
|
||||
public $moduleName = 'OnlyOfficeTemplates';
|
||||
public $parentName = 'Tools';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $log;
|
||||
$this->db = PearDatabase::getInstance();
|
||||
}
|
||||
|
||||
public function vtlib_handler($modulename, $event_type)
|
||||
{
|
||||
switch ($event_type) {
|
||||
case 'module.postinstall':
|
||||
$this->executeSql();
|
||||
$this->addLinksToEntityModules();
|
||||
break;
|
||||
case 'module.preupdate':
|
||||
case 'module.disabled':
|
||||
$this->removeLinksFromEntityModules();
|
||||
break;
|
||||
case 'module.enabled':
|
||||
case 'module.postupdate':
|
||||
$this->executeSql();
|
||||
$this->removeLinksFromEntityModules();
|
||||
$this->addLinksToEntityModules();
|
||||
break;
|
||||
case 'module.preuninstall':
|
||||
$this->removeLinksFromEntityModules();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables from schema.xml
|
||||
*/
|
||||
public function executeSql()
|
||||
{
|
||||
$schemaPath = dirname(__FILE__) . '/schema.xml';
|
||||
if (!is_file($schemaPath)) {
|
||||
return;
|
||||
}
|
||||
$xml = @simplexml_load_file($schemaPath);
|
||||
if (!$xml || !isset($xml->tables->table)) {
|
||||
return;
|
||||
}
|
||||
foreach ($xml->tables->table as $table) {
|
||||
$name = (string)$table->name;
|
||||
$sql = isset($table->sql) ? (string)$table->sql : '';
|
||||
if (empty($sql)) {
|
||||
continue;
|
||||
}
|
||||
$this->db->pquery($sql, []);
|
||||
}
|
||||
// seq initial value
|
||||
$this->db->pquery("INSERT IGNORE INTO vtiger_oot_templates_seq (id) VALUES (1)", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add DETAILVIEWSIDEBARWIDGET link to all entity modules (like PDFMaker).
|
||||
*/
|
||||
public function addLinksToEntityModules()
|
||||
{
|
||||
$result = $this->db->pquery(
|
||||
"SELECT name FROM vtiger_tab WHERE isentitytype = ? AND presence = ?",
|
||||
['1', '0']
|
||||
);
|
||||
while ($row = $this->db->fetchByAssoc($result)) {
|
||||
$moduleName = $row['name'];
|
||||
$module = Vtiger_Module::getInstance($moduleName);
|
||||
if (!$module) {
|
||||
continue;
|
||||
}
|
||||
$module->deleteLink('DETAILVIEWSIDEBARWIDGET', 'OnlyOfficeTemplates');
|
||||
$module->addLink(
|
||||
'DETAILVIEWSIDEBARWIDGET',
|
||||
'OnlyOfficeTemplates',
|
||||
'module=OnlyOfficeTemplates&view=GetTemplateActions&record=$RECORD$'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove widget link from all entity modules.
|
||||
*/
|
||||
public function removeLinksFromEntityModules()
|
||||
{
|
||||
$result = $this->db->pquery(
|
||||
"SELECT name FROM vtiger_tab WHERE isentitytype = ? AND presence = ?",
|
||||
['1', '0']
|
||||
);
|
||||
while ($row = $this->db->fetchByAssoc($result)) {
|
||||
$module = Vtiger_Module::getInstance($row['name']);
|
||||
if ($module) {
|
||||
$module->deleteLink('DETAILVIEWSIDEBARWIDGET', 'OnlyOfficeTemplates');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
modules/OnlyOfficeTemplates/README.md
Normal file
59
modules/OnlyOfficeTemplates/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# OnlyOfficeTemplates
|
||||
|
||||
Модуль генерации документов из DOCX-шаблонов с подстановкой полей CRM (аналог PDFMaker). Результат — **PDF** (по умолчанию) или **DOCX**. При сохранении в Документы в формате DOCX документ можно редактировать через OnlyOffice (кнопка «Nextcloud» / open_file_v2).
|
||||
|
||||
## Возможности
|
||||
|
||||
- Шаблоны DOCX хранятся в **отдельной папке S3** (`crm2/OnlyOfficeTemplates/templates/`).
|
||||
- **Редактирование по аналогии с PDFMaker:** слева — метаданные (имя, модуль), справа — OnlyOffice Document Editor; документ сохраняется в S3 через callback Document Server.
|
||||
- Плейсхолдеры в шаблоне: `{{fieldname}}` для полей записи, `{{ModuleName__fieldname}}` для связанных модулей (Account, Contact и т.д.).
|
||||
- В виджете карточки записи: выбор шаблона, формата (PDF/DOCX), действия «Скачать» и «Сохранить в Документы».
|
||||
- При выборе PDF результат конвертируется через OnlyOffice Conversion API.
|
||||
- Модуль **портативный**: можно развернуть в другом инстансе CRM без привязки к текущему `crm_extensions`.
|
||||
|
||||
## Требования
|
||||
|
||||
- PHP с расширениями: zip, xml, curl (или allow_url_fopen для Conversion API).
|
||||
- Composer-зависимости: `phpoffice/phpword`, `aws/aws-sdk-php` (уже в корне проекта).
|
||||
- Доступ к S3-совместимому хранилищу и (для PDF) к OnlyOffice Document Server (Conversion API).
|
||||
|
||||
## Установка
|
||||
|
||||
1. Скопируйте папку `modules/OnlyOfficeTemplates` и `layouts/v7/modules/OnlyOfficeTemplates` в целевой CRM.
|
||||
2. Настройте переменные окружения или конфиг (см. раздел «Конфигурация»).
|
||||
3. Выполните установку БД и виджетов одним из способов:
|
||||
- **Через скрипт (рекомендуется):**
|
||||
`php modules/OnlyOfficeTemplates/install.php`
|
||||
из корня CRM (или откройте в браузере соответствующий URL с правами администратора).
|
||||
- **Через Module Manager:** упакуйте модуль в zip с `manifest.xml` и импортируйте.
|
||||
4. Добавьте шаблоны: загрузите DOCX в S3 в папку `{OOT_S3_PREFIX}/templates/{id}/{filename}.docx` и добавьте запись в `vtiger_oot_templates` (имя, модуль, s3_key, file_name, owner), либо используйте экшен UploadTemplate (см. ниже).
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Модуль читает настройки из:
|
||||
|
||||
1. **Внешний конфиг** (если есть): `crm_extensions/file_storage/config.php` — используются S3-данные оттуда.
|
||||
2. **Переменные окружения** (.env в `crm_extensions` или в корне):
|
||||
- `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_ENDPOINT`, `S3_BUCKET` — доступ к S3.
|
||||
- `OOT_S3_PREFIX` — префикс папки модуля в S3 (по умолчанию `crm2/OnlyOfficeTemplates`).
|
||||
- `OOT_ONLYOFFICE_CONVERT_URL` — URL Conversion API (например `https://office.example.com:9443/ConvertService.ashx` или `/converter`).
|
||||
- `ONLYOFFICE_DOCUMENT_SERVER` или `OOT_ONLYOFFICE_DOCUMENT_SERVER` — URL OnlyOffice Document Server для редактора (например `https://documentserver`). Нужен для экрана редактирования шаблона (слева форма, справа OnlyOffice). Document Server должен иметь доступ по HTTP(S) к CRM (для загрузки документа и callback).
|
||||
- `OOT_DOCUMENT_SECRET` — секрет для подписи URL документа (рекомендуется в продакшене). Если задан, в ссылку на документ добавляется токен; без него GetDocument доступен без проверки.
|
||||
- `OOT_DOCUMENTS_S3_PREFIX` — префикс для файлов, сохраняемых в Документы (по умолчанию `crm2/CRM_Active_Files/Documents`).
|
||||
|
||||
Без OnlyOffice Conversion API доступна только выдача DOCX (формат PDF не будет работать). Без Document Server редактирование шаблона в OnlyOffice недоступно, но можно загружать готовые DOCX через «Загрузить файл».
|
||||
|
||||
## Редактирование и загрузка шаблонов
|
||||
|
||||
- **Через OnlyOffice (как в PDFMaker):** «Добавить шаблон» → создаётся черновик → открывается экран: слева имя и модуль, справа OnlyOffice Document Editor. Документ по сохранению/закрытию отправляется в S3 через callback. Список шаблонов: имя — ссылка на редактирование.
|
||||
- **Загрузить файл:** кнопка «Загрузить файл» открывает форму: имя, модуль, выбор DOCX; отправка в `UploadTemplate`.
|
||||
- **Вручную:** загрузите DOCX в S3 по пути `{OOT_S3_PREFIX}/templates/{template_id}/{имя_файла}.docx` и вставьте запись в `vtiger_oot_templates`.
|
||||
|
||||
## Структура БД
|
||||
|
||||
- `vtiger_oot_templates` — id, name, module, s3_key, file_name, owner, created_at, settings (JSON, опционально).
|
||||
- `vtiger_oot_templates_seq` — при необходимости для генерации id (опционально).
|
||||
|
||||
## Портативность
|
||||
|
||||
Модуль не изменяет ядро CRM и не зависит от наличия `crm_extensions`. Все пути и ключи задаются через конфиг/переменные окружения. В другом инстансе достаточно задать свои S3_*, OOT_* и (при необходимости) ONLYOFFICE_* и выполнить установку (install.php или импорт пакета).
|
||||
40
modules/OnlyOfficeTemplates/actions/CreateDraft.php
Normal file
40
modules/OnlyOfficeTemplates/actions/CreateDraft.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Create a draft template record and redirect to Edit (OnlyOffice editor).
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_CreateDraft_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$tabId = getTabId($moduleName);
|
||||
$privileges = Users_Privileges_Model::getCurrentUserPrivilegesModel();
|
||||
if (!$privileges->hasModulePermission($tabId)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$adb = PearDatabase::getInstance();
|
||||
$currentUser = Users_Record_Model::getCurrentUserModel();
|
||||
$ownerId = $currentUser->getId();
|
||||
$name = $request->get('name') ?: vtranslate('LBL_OOT_NEW_TEMPLATE', $request->getModule());
|
||||
$module = $request->get('module_name');
|
||||
if (!$module) {
|
||||
$r = $adb->pquery("SELECT name FROM vtiger_tab WHERE isentitytype = 1 AND presence = 0 ORDER BY name LIMIT 1", []);
|
||||
$module = $adb->query_result($r, 0, 'name');
|
||||
}
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_oot_templates (name, module, s3_key, file_name, owner, created_at) VALUES (?, ?, '', '', ?, NOW())",
|
||||
[$name, $module, $ownerId]
|
||||
);
|
||||
$templateId = (int)$adb->getLastInsertID();
|
||||
if ($templateId <= 0) {
|
||||
$r = $adb->pquery("SELECT MAX(id) AS m FROM vtiger_oot_templates", []);
|
||||
$templateId = (int)$adb->query_result($r, 0, 'm');
|
||||
}
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=Edit&templateid=' . $templateId . '&app=TOOLS');
|
||||
}
|
||||
}
|
||||
185
modules/OnlyOfficeTemplates/actions/CreateFromTemplate.php
Normal file
185
modules/OnlyOfficeTemplates/actions/CreateFromTemplate.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* Generate document from template: merge placeholders, optional PDF conversion, download or save to Documents.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_CreateFromTemplate_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
$record = $request->get('record');
|
||||
$module = $request->get('source_module') ?: getSalesEntityType($record);
|
||||
if (!isPermitted($module, 'DetailView', $record)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$recordId = (int)$request->get('record');
|
||||
$templateId = (int)$request->get('template_id');
|
||||
$format = strtolower($request->get('format') ?: 'pdf'); // pdf | docx
|
||||
$mode = strtolower($request->get('mode') ?: 'download'); // download | save_to_documents
|
||||
$module = $request->get('source_module') ?: getSalesEntityType($recordId);
|
||||
|
||||
if (!$recordId || !$templateId) {
|
||||
echo json_encode(['success' => false, 'error' => 'Missing record or template_id']);
|
||||
return;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/models/OnlyOfficeTemplates_Model.php';
|
||||
require_once dirname(__DIR__) . '/resources/S3Helper.php';
|
||||
require_once dirname(__DIR__) . '/resources/MergeService.php';
|
||||
require_once dirname(__DIR__) . '/resources/ConvertService.php';
|
||||
|
||||
$model = new OnlyOfficeTemplates_Model();
|
||||
$template = $model->getTemplateById($templateId);
|
||||
if (!$template) {
|
||||
echo json_encode(['success' => false, 'error' => 'Template not found or access denied']);
|
||||
return;
|
||||
}
|
||||
$config = $model->getConfig();
|
||||
$s3 = new OnlyOfficeTemplates_S3Helper($config);
|
||||
$mergeService = new OnlyOfficeTemplates_MergeService($s3, $config);
|
||||
$convertService = new OnlyOfficeTemplates_ConvertService($config);
|
||||
|
||||
$placeholders = $mergeService->buildPlaceholders($module, $recordId);
|
||||
$tempDir = null;
|
||||
$docxPath = null;
|
||||
$pdfPath = null;
|
||||
|
||||
try {
|
||||
$docxPath = $mergeService->mergeToFile($template['s3_key'], $placeholders);
|
||||
$tempDir = dirname($docxPath);
|
||||
$baseName = pathinfo($template['file_name'], PATHINFO_FILENAME);
|
||||
$outExt = ($format === 'pdf') ? 'pdf' : 'docx';
|
||||
$outFileName = $baseName . '_' . $recordId . '.' . $outExt;
|
||||
|
||||
if ($format === 'pdf') {
|
||||
$docxUrl = $this->putMergedDocxForConversion($s3, $config, $docxPath, $recordId, $templateId);
|
||||
if (!$docxUrl) {
|
||||
echo json_encode(['success' => false, 'error' => 'Could not expose DOCX URL for conversion']);
|
||||
return;
|
||||
}
|
||||
$result = $convertService->convertToPdf($docxUrl, $template['file_name']);
|
||||
if (!$result['success']) {
|
||||
echo json_encode(['success' => false, 'error' => $result['error']]);
|
||||
return;
|
||||
}
|
||||
$pdfPath = $result['pdfPath'];
|
||||
}
|
||||
|
||||
if ($mode === 'save_to_documents') {
|
||||
$fileToSave = ($format === 'pdf') ? $pdfPath : $docxPath;
|
||||
$docId = $this->saveToDocuments($request, $module, $recordId, $fileToSave, $outFileName, $config);
|
||||
$this->cleanupTemp($tempDir, $docxPath, $pdfPath);
|
||||
echo json_encode(['success' => true, 'document_id' => $docId, 'message' => 'Saved to Documents']);
|
||||
return;
|
||||
}
|
||||
|
||||
$downloadPath = ($format === 'pdf') ? $pdfPath : $docxPath;
|
||||
$mime = ($format === 'pdf') ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
header('Content-Type: ' . $mime);
|
||||
header('Content-Disposition: attachment; filename="' . basename($outFileName) . '"');
|
||||
header('Content-Length: ' . filesize($downloadPath));
|
||||
readfile($downloadPath);
|
||||
$this->cleanupTemp($tempDir, $docxPath, $pdfPath);
|
||||
} catch (Exception $e) {
|
||||
$this->cleanupTemp($tempDir, $docxPath, $pdfPath);
|
||||
if ($request->get('ajax')) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload merged DOCX to S3 temp and return public URL for OnlyOffice converter.
|
||||
*/
|
||||
private function putMergedDocxForConversion(OnlyOfficeTemplates_S3Helper $s3, array $config, $localPath, $recordId, $templateId)
|
||||
{
|
||||
$key = $s3->getTempKey($recordId, $templateId, 'docx');
|
||||
$s3->uploadFile($localPath, $key, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
$bucket = $s3->getBucket();
|
||||
$endpoint = $config['s3']['endpoint'] ?? '';
|
||||
$base = preg_replace('#^https?://#', 'https://', $endpoint);
|
||||
if (empty($base)) {
|
||||
$base = 'https://s3.twcstorage.ru';
|
||||
}
|
||||
return rtrim($base, '/') . '/' . $bucket . '/' . $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file as Document record and link to parent. Use Documents S3 structure if FilePathManager available.
|
||||
*/
|
||||
private function saveToDocuments(Vtiger_Request $request, $module, $recordId, $localPath, $fileName, array $config)
|
||||
{
|
||||
$adb = PearDatabase::getInstance();
|
||||
$currentUser = Users_Record_Model::getCurrentUserModel();
|
||||
$ownerId = $currentUser->getId();
|
||||
$docPrefix = $config['documents_s3_prefix'] ?? 'crm2/CRM_Active_Files/Documents';
|
||||
$bucket = $config['s3_bucket'] ?? $config['s3']['bucket'];
|
||||
|
||||
$notesId = $adb->getUniqueID('vtiger_crmentity');
|
||||
if (!$notesId) {
|
||||
$r = $adb->pquery("SELECT MAX(crmid) AS m FROM vtiger_crmentity", []);
|
||||
$notesId = (int)$adb->query_result($r, 0, 'm') + 1;
|
||||
}
|
||||
|
||||
$title = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$s3Key = $docPrefix . '/' . $module . '/' . $module . '_' . $recordId . '/' . $title . '_' . $notesId . '.' . pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
|
||||
$s3 = new OnlyOfficeTemplates_S3Helper($config);
|
||||
$contentType = (pathinfo($fileName, PATHINFO_EXTENSION) === 'pdf') ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
$s3->uploadFile($localPath, $s3Key, $contentType);
|
||||
|
||||
$fileUrl = 'https://' . ($config['s3']['endpoint'] ?? 's3.twcstorage.ru');
|
||||
$fileUrl = preg_replace('#^https?://#', '', $fileUrl);
|
||||
$fileUrl = 'https://' . $fileUrl . '/' . $bucket . '/' . $s3Key;
|
||||
$fileSize = filesize($localPath);
|
||||
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_crmentity (crmid, smownerid, smcreatorid, modifiedby, setype, description, createdtime, modifiedtime, presence, deleted) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
[$notesId, $ownerId, $ownerId, $ownerId, 'Documents', '', $now, $now, 1, 0]
|
||||
);
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_notes (notesid, title, filename, filesize, filetype, filelocationtype, filedownloadcount, createdtime, modifiedtime, folderid, notecontent) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[$notesId, $title, $fileUrl, $fileSize, $contentType, 'E', 0, $now, $now, 0, '']
|
||||
);
|
||||
if (method_exists($adb, 'pquery')) {
|
||||
$adb->pquery("INSERT INTO vtiger_senotesrel (crmid, notesid) VALUES (?,?)", [$recordId, $notesId]);
|
||||
}
|
||||
$adb->pquery("INSERT INTO vtiger_notescf (notesid) VALUES (?)", [$notesId]);
|
||||
|
||||
if ($this->hasS3Columns($adb)) {
|
||||
$adb->pquery("UPDATE vtiger_notes SET s3_bucket = ?, s3_key = ? WHERE notesid = ?", [$bucket, $s3Key, $notesId]);
|
||||
}
|
||||
|
||||
return $notesId;
|
||||
}
|
||||
|
||||
private function hasS3Columns($adb)
|
||||
{
|
||||
static $has = null;
|
||||
if ($has === null) {
|
||||
$r = @$adb->pquery("SHOW COLUMNS FROM vtiger_notes LIKE 's3_key'", []);
|
||||
$has = $r && $adb->num_rows($r) > 0;
|
||||
}
|
||||
return $has;
|
||||
}
|
||||
|
||||
private function cleanupTemp($tempDir, $docxPath, $pdfPath)
|
||||
{
|
||||
if ($pdfPath && is_file($pdfPath)) {
|
||||
@unlink($pdfPath);
|
||||
}
|
||||
if ($docxPath && is_file($docxPath)) {
|
||||
@unlink($docxPath);
|
||||
}
|
||||
if ($tempDir && is_dir($tempDir)) {
|
||||
@rmdir($tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
modules/OnlyOfficeTemplates/actions/GetDocument.php
Normal file
96
modules/OnlyOfficeTemplates/actions/GetDocument.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Serve DOCX for OnlyOffice Document Server: from S3 or empty document for new template.
|
||||
* Document Server requests this URL; it must be publicly reachable.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_GetDocument_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
/** Document Server requests this URL without session; we verify token if OOT_DOCUMENT_SECRET is set. */
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
$config = OnlyOfficeTemplates_getConfig();
|
||||
$secret = $config['document_secret'] ?? '';
|
||||
if ($secret !== '') {
|
||||
$templateId = (int)$request->get('template_id');
|
||||
$token = $request->get('token');
|
||||
$expected = hash_hmac('sha256', (string)$templateId, $secret);
|
||||
if ($token === '' || !hash_equals($expected, $token)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$templateId = (int)$request->get('template_id');
|
||||
if ($templateId <= 0) {
|
||||
$this->outputEmptyDocx();
|
||||
return;
|
||||
}
|
||||
|
||||
$adb = PearDatabase::getInstance();
|
||||
$res = $adb->pquery(
|
||||
"SELECT id, name, module, s3_key, file_name, owner FROM vtiger_oot_templates WHERE id = ?",
|
||||
[$templateId]
|
||||
);
|
||||
$row = $adb->fetchByAssoc($res);
|
||||
if (!$row) {
|
||||
$this->outputEmptyDocx();
|
||||
return;
|
||||
}
|
||||
if (empty($row['s3_key']) || empty($row['file_name'])) {
|
||||
$this->outputEmptyDocx();
|
||||
return;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
require_once dirname(__DIR__) . '/resources/S3Helper.php';
|
||||
$config = OnlyOfficeTemplates_getConfig();
|
||||
$s3 = new OnlyOfficeTemplates_S3Helper($config);
|
||||
$body = $s3->getObjectBody($row['s3_key']);
|
||||
$fileName = $row['file_name'];
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
header('Content-Disposition: attachment; filename="' . basename($fileName) . '"');
|
||||
header('Content-Length: ' . strlen($body));
|
||||
header('Cache-Control: no-cache');
|
||||
echo $body;
|
||||
}
|
||||
|
||||
protected function outputEmptyDocx()
|
||||
{
|
||||
$rootDir = dirname(dirname(dirname(__DIR__)));
|
||||
$emptyPath = dirname(__DIR__) . '/resources/empty.docx';
|
||||
if (file_exists($emptyPath)) {
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
header('Content-Disposition: attachment; filename="document.docx"');
|
||||
header('Content-Length: ' . filesize($emptyPath));
|
||||
header('Cache-Control: no-cache');
|
||||
readfile($emptyPath);
|
||||
return;
|
||||
}
|
||||
if (is_file($rootDir . '/vendor/autoload.php')) {
|
||||
require_once $rootDir . '/vendor/autoload.php';
|
||||
}
|
||||
if (!class_exists('PhpOffice\PhpWord\PhpWord')) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
echo 'PHPWord not found';
|
||||
return;
|
||||
}
|
||||
$phpWord = new \PhpOffice\PhpWord\PhpWord();
|
||||
$phpWord->addSection();
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'oot_empty_') . '.docx';
|
||||
try {
|
||||
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
|
||||
$writer->save($tmp);
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
header('Content-Disposition: attachment; filename="document.docx"');
|
||||
header('Content-Length: ' . filesize($tmp));
|
||||
header('Cache-Control: no-cache');
|
||||
readfile($tmp);
|
||||
} finally {
|
||||
@unlink($tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
modules/OnlyOfficeTemplates/actions/Install.php
Normal file
41
modules/OnlyOfficeTemplates/actions/Install.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Install OnlyOfficeTemplates: register in vtiger_tab, create tables, add widget links.
|
||||
* Open in browser (as admin): index.php?module=OnlyOfficeTemplates&action=Install
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_Install_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
if (!isPermitted('Settings', 'Edit', '')) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$adb = PearDatabase::getInstance();
|
||||
|
||||
$r = $adb->pquery("SELECT tabid FROM vtiger_tab WHERE name = ?", ['OnlyOfficeTemplates']);
|
||||
if ($adb->num_rows($r) > 0) {
|
||||
$msg = 'OnlyOfficeTemplates уже зарегистрирован. Таблицы и виджеты обновлены.';
|
||||
} else {
|
||||
$maxId = $adb->query_result($adb->pquery("SELECT COALESCE(MAX(tabid),0) AS m FROM vtiger_tab", []), 0, 'm');
|
||||
$tabid = $maxId + 1;
|
||||
$maxSeq = $adb->query_result($adb->pquery("SELECT COALESCE(MAX(tabsequence),0) AS m FROM vtiger_tab", []), 0, 'm');
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_tab (tabid, name, presence, tabsequence, tablabel, modifiedby, modifiedtime, customized, ownedby, isentitytype, version, parent) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[$tabid, 'OnlyOfficeTemplates', 0, $maxSeq + 1, 'OnlyOffice Templates', null, null, 0, 0, 0, '1.0', 'Tools']
|
||||
);
|
||||
$msg = 'Модуль OnlyOfficeTemplates зарегистрирован (tabid=' . $tabid . '). Таблицы и виджеты созданы.';
|
||||
}
|
||||
|
||||
$mod = new OnlyOfficeTemplates();
|
||||
$mod->executeSql();
|
||||
$mod->addLinksToEntityModules();
|
||||
|
||||
header('Location: index.php?module=Settings&parent=Settings&view=Index&install_oot=1&install_msg=' . urlencode($msg));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
78
modules/OnlyOfficeTemplates/actions/OnlyOfficeCallback.php
Normal file
78
modules/OnlyOfficeTemplates/actions/OnlyOfficeCallback.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* OnlyOffice Document Server callback: receive saved document and store in S3.
|
||||
* Expects JSON body: status, key, url (when status 2,3,6,7). Returns {"error":0}.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_OnlyOfficeCallback_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
// Callback is called by Document Server, not by user. We validate by key matching template.
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
if (!$data || !isset($data['key']) || !isset($data['status'])) {
|
||||
echo json_encode(['error' => 1, 'message' => 'Invalid callback body']);
|
||||
return;
|
||||
}
|
||||
$key = $data['key'];
|
||||
$status = (int)$data['status'];
|
||||
$templateId = (int)$key;
|
||||
if ($templateId <= 0) {
|
||||
echo json_encode(['error' => 0]);
|
||||
return;
|
||||
}
|
||||
if (!in_array($status, [2, 3, 6, 7], true)) {
|
||||
echo json_encode(['error' => 0]);
|
||||
return;
|
||||
}
|
||||
$url = isset($data['url']) ? trim($data['url']) : '';
|
||||
if ($url === '') {
|
||||
echo json_encode(['error' => 0]);
|
||||
return;
|
||||
}
|
||||
$fileType = isset($data['filetype']) ? strtolower(trim($data['filetype'])) : 'docx';
|
||||
if ($fileType !== 'docx') {
|
||||
$fileType = 'docx';
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
require_once dirname(__DIR__) . '/resources/S3Helper.php';
|
||||
$config = OnlyOfficeTemplates_getConfig();
|
||||
$adb = PearDatabase::getInstance();
|
||||
$res = $adb->pquery("SELECT id, file_name FROM vtiger_oot_templates WHERE id = ?", [$templateId]);
|
||||
$row = $adb->fetchByAssoc($res);
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 0]);
|
||||
return;
|
||||
}
|
||||
|
||||
$fileName = $row['file_name'] ?: ('template_' . $templateId . '.' . $fileType);
|
||||
if (pathinfo($fileName, PATHINFO_EXTENSION) !== $fileType) {
|
||||
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '.' . $fileType;
|
||||
}
|
||||
$s3 = new OnlyOfficeTemplates_S3Helper($config);
|
||||
$s3Key = $s3->getTemplateKey($templateId, $fileName);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'oot_callback_') . '.' . $fileType;
|
||||
try {
|
||||
$ctx = stream_context_create(['http' => ['timeout' => 30]]);
|
||||
$content = @file_get_contents($url, false, $ctx);
|
||||
if ($content === false || strlen($content) === 0) {
|
||||
echo json_encode(['error' => 1, 'message' => 'Failed to download document']);
|
||||
return;
|
||||
}
|
||||
file_put_contents($tmpFile, $content);
|
||||
$s3->uploadFile($tmpFile, $s3Key, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
$adb->pquery("UPDATE vtiger_oot_templates SET s3_key = ?, file_name = ? WHERE id = ?", [$s3Key, $fileName, $templateId]);
|
||||
} finally {
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
echo json_encode(['error' => 0]);
|
||||
}
|
||||
}
|
||||
50
modules/OnlyOfficeTemplates/actions/SaveMetadata.php
Normal file
50
modules/OnlyOfficeTemplates/actions/SaveMetadata.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Save template name and module (metadata). Redirect back to Edit.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_SaveMetadata_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$tabId = getTabId($moduleName);
|
||||
$privileges = Users_Privileges_Model::getCurrentUserPrivilegesModel();
|
||||
if (!$privileges->hasModulePermission($tabId)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$templateId = (int)$request->get('templateid');
|
||||
$name = $request->get('name');
|
||||
$moduleName = $request->get('module_name');
|
||||
if ($templateId <= 0 || $name === null || $moduleName === null) {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=List&app=TOOLS');
|
||||
return;
|
||||
}
|
||||
$adb = PearDatabase::getInstance();
|
||||
$currentUser = Users_Record_Model::getCurrentUserModel();
|
||||
$userId = $currentUser->getId();
|
||||
$res = $adb->pquery("SELECT owner FROM vtiger_oot_templates WHERE id = ?", [$templateId]);
|
||||
if ($adb->num_rows($res) === 0) {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=List&app=TOOLS');
|
||||
return;
|
||||
}
|
||||
$owner = (int)$adb->query_result($res, 0, 'owner');
|
||||
if ($owner !== $userId) {
|
||||
$gr = $adb->pquery("SELECT 1 FROM vtiger_users2group WHERE userid = ? AND groupid = ?", [$userId, $owner]);
|
||||
if ($adb->num_rows($gr) === 0) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
$adb->pquery("UPDATE vtiger_oot_templates SET name = ?, module = ? WHERE id = ?", [$name, $moduleName, $templateId]);
|
||||
$redirect = $request->get('redirect');
|
||||
if ($redirect === 'Edit') {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=Edit&templateid=' . $templateId . '&app=TOOLS');
|
||||
} else {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=List&app=TOOLS');
|
||||
}
|
||||
}
|
||||
}
|
||||
81
modules/OnlyOfficeTemplates/actions/UploadTemplate.php
Normal file
81
modules/OnlyOfficeTemplates/actions/UploadTemplate.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Upload DOCX template to S3 and save metadata in vtiger_oot_templates.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_UploadTemplate_Action extends Vtiger_Action_Controller
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
if (!isPermitted('Settings', 'Edit', '')) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$name = $request->get('name') ?: $request->get('template_name');
|
||||
$module = $request->get('module_name');
|
||||
$file = $_FILES['file'] ?? $_FILES['template_file'] ?? null;
|
||||
|
||||
$redirect = $request->get('redirect');
|
||||
$doRedirect = ($redirect === 'List');
|
||||
|
||||
if (!$name || !$module || !$file || empty($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) {
|
||||
if ($doRedirect) {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=AddTemplate&app=TOOLS&error=' . urlencode('Укажите название, модуль и выберите файл DOCX'));
|
||||
return;
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Missing name, module_name, or valid file upload']);
|
||||
return;
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
if ($ext !== 'docx') {
|
||||
if ($doRedirect) {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=AddTemplate&app=TOOLS&error=' . urlencode('Допускаются только файлы DOCX'));
|
||||
return;
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Only DOCX files are allowed']);
|
||||
return;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/models/OnlyOfficeTemplates_Model.php';
|
||||
require_once dirname(__DIR__) . '/resources/S3Helper.php';
|
||||
|
||||
$model = new OnlyOfficeTemplates_Model();
|
||||
$config = $model->getConfig();
|
||||
$s3 = new OnlyOfficeTemplates_S3Helper($config);
|
||||
|
||||
$owner = Users_Record_Model::getCurrentUserModel()->getId();
|
||||
$adb = PearDatabase::getInstance();
|
||||
$adb->pquery("INSERT INTO vtiger_oot_templates (name, module, s3_key, file_name, owner, created_at) VALUES ('_pending', ?, '', '', ?, NOW())", [$module, $owner]);
|
||||
$templateId = (int)$adb->getLastInsertID();
|
||||
if ($templateId <= 0) {
|
||||
$r = $adb->pquery("SELECT MAX(id) AS m FROM vtiger_oot_templates", []);
|
||||
$templateId = (int)$adb->query_result($r, 0, 'm');
|
||||
}
|
||||
$fileName = basename($file['name']);
|
||||
$s3Key = $s3->getTemplateKey($templateId, $fileName);
|
||||
|
||||
try {
|
||||
$s3->uploadFile($file['tmp_name'], $s3Key, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
} catch (Exception $e) {
|
||||
$adb->pquery("DELETE FROM vtiger_oot_templates WHERE id = ?", [$templateId]);
|
||||
if ($doRedirect) {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=AddTemplate&app=TOOLS&error=' . urlencode('Ошибка загрузки: ' . $e->getMessage()));
|
||||
return;
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'S3 upload failed: ' . $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$adb->pquery("UPDATE vtiger_oot_templates SET name = ?, s3_key = ?, file_name = ? WHERE id = ?", [$name, $s3Key, $fileName, $templateId]);
|
||||
$id = $templateId;
|
||||
if ($doRedirect) {
|
||||
header('Location: index.php?module=OnlyOfficeTemplates&view=List&app=TOOLS');
|
||||
return;
|
||||
}
|
||||
echo json_encode(['success' => true, 'id' => $id, 's3_key' => $s3Key]);
|
||||
}
|
||||
}
|
||||
91
modules/OnlyOfficeTemplates/config.php
Normal file
91
modules/OnlyOfficeTemplates/config.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* OnlyOfficeTemplates module configuration.
|
||||
* Reads from environment (.env) or optional external config (crm_extensions/file_storage/config.php).
|
||||
* Portable: no hardcoded paths; set S3_*, ONLYOFFICE_* in .env or your config.
|
||||
*/
|
||||
|
||||
$OnlyOfficeTemplatesConfig = null;
|
||||
|
||||
/**
|
||||
* Read env var: getenv() then $_ENV then EnvLoader::get() (EnvLoader does not use putenv).
|
||||
*/
|
||||
function OnlyOfficeTemplates_env($key, $default = '') {
|
||||
$v = getenv($key);
|
||||
if ($v !== false && $v !== '') {
|
||||
return $v;
|
||||
}
|
||||
$v = $_ENV[$key] ?? null;
|
||||
if ($v !== null && $v !== '') {
|
||||
return $v;
|
||||
}
|
||||
if (class_exists('EnvLoader')) {
|
||||
$v = EnvLoader::get($key, $default);
|
||||
return $v !== null ? $v : $default;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
function OnlyOfficeTemplates_getConfig() {
|
||||
global $OnlyOfficeTemplatesConfig;
|
||||
if ($OnlyOfficeTemplatesConfig !== null) {
|
||||
return $OnlyOfficeTemplatesConfig;
|
||||
}
|
||||
|
||||
$baseDir = dirname(__DIR__);
|
||||
$rootDir = dirname(dirname($baseDir));
|
||||
|
||||
// Load .env (EnvLoader uses $_ENV, not getenv)
|
||||
if (file_exists($rootDir . '/crm_extensions/shared/EnvLoader.php')) {
|
||||
require_once $rootDir . '/crm_extensions/shared/EnvLoader.php';
|
||||
$envPath = $rootDir . '/crm_extensions/.env';
|
||||
if (file_exists($envPath)) {
|
||||
EnvLoader::load($envPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Try external config (crm_extensions) if present
|
||||
$extConfigPath = $rootDir . '/crm_extensions/file_storage/config.php';
|
||||
if (file_exists($extConfigPath)) {
|
||||
try {
|
||||
$ext = require $extConfigPath;
|
||||
if (is_array($ext) && isset($ext['s3'])) {
|
||||
$s3 = $ext['s3'];
|
||||
$bucket = $s3['bucket'] ?? $ext['bucket'] ?? $ext['s3_bucket'] ?? OnlyOfficeTemplates_env('S3_BUCKET', '');
|
||||
$OnlyOfficeTemplatesConfig = [
|
||||
's3' => $s3,
|
||||
's3_prefix' => OnlyOfficeTemplates_env('OOT_S3_PREFIX', 'crm2/OnlyOfficeTemplates'),
|
||||
'onlyoffice_convert_url' => OnlyOfficeTemplates_env('OOT_ONLYOFFICE_CONVERT_URL') ?: OnlyOfficeTemplates_env('ONLYOFFICE_CONVERT_URL', ''),
|
||||
'onlyoffice_document_server' => OnlyOfficeTemplates_env('OOT_ONLYOFFICE_DOCUMENT_SERVER') ?: OnlyOfficeTemplates_env('ONLYOFFICE_DOCUMENT_SERVER', ''),
|
||||
'documents_s3_prefix' => OnlyOfficeTemplates_env('OOT_DOCUMENTS_S3_PREFIX', 'crm2/CRM_Active_Files/Documents'),
|
||||
's3_bucket' => $bucket,
|
||||
'document_secret' => OnlyOfficeTemplates_env('OOT_DOCUMENT_SECRET', ''),
|
||||
];
|
||||
return $OnlyOfficeTemplatesConfig;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Build from environment
|
||||
$OnlyOfficeTemplatesConfig = [
|
||||
's3' => [
|
||||
'key' => OnlyOfficeTemplates_env('S3_ACCESS_KEY', ''),
|
||||
'secret' => OnlyOfficeTemplates_env('S3_SECRET_KEY', ''),
|
||||
'endpoint' => OnlyOfficeTemplates_env('S3_ENDPOINT', ''),
|
||||
'bucket' => OnlyOfficeTemplates_env('S3_BUCKET', ''),
|
||||
'region' => OnlyOfficeTemplates_env('S3_REGION', 'ru-1'),
|
||||
'use_path_style_endpoint' => true,
|
||||
'version' => 'latest',
|
||||
],
|
||||
's3_prefix' => OnlyOfficeTemplates_env('OOT_S3_PREFIX', 'crm2/OnlyOfficeTemplates'),
|
||||
'onlyoffice_convert_url' => OnlyOfficeTemplates_env('OOT_ONLYOFFICE_CONVERT_URL') ?: OnlyOfficeTemplates_env('ONLYOFFICE_CONVERT_URL', ''),
|
||||
'onlyoffice_document_server' => OnlyOfficeTemplates_env('OOT_ONLYOFFICE_DOCUMENT_SERVER') ?: OnlyOfficeTemplates_env('ONLYOFFICE_DOCUMENT_SERVER', ''),
|
||||
'documents_s3_prefix' => OnlyOfficeTemplates_env('OOT_DOCUMENTS_S3_PREFIX', 'crm2/CRM_Active_Files/Documents'),
|
||||
's3_bucket' => OnlyOfficeTemplates_env('S3_BUCKET', ''),
|
||||
'document_secret' => OnlyOfficeTemplates_env('OOT_DOCUMENT_SECRET', ''),
|
||||
];
|
||||
|
||||
return $OnlyOfficeTemplatesConfig;
|
||||
}
|
||||
58
modules/OnlyOfficeTemplates/install.php
Normal file
58
modules/OnlyOfficeTemplates/install.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* One-time install: register OnlyOfficeTemplates in vtiger_tab and run schema + links.
|
||||
* Run from browser: index.php?module=OnlyOfficeTemplates&action=Install
|
||||
* Or from CLI: php modules/OnlyOfficeTemplates/install.php
|
||||
* Requires admin user when run from browser.
|
||||
*/
|
||||
|
||||
$rootDir = dirname(dirname(__DIR__));
|
||||
if (php_sapi_name() === 'cli') {
|
||||
require_once $rootDir . '/config.inc.php';
|
||||
require_once $rootDir . '/vtlib/Vtiger/Module.php';
|
||||
require_once $rootDir . '/includes/utils/utils.php';
|
||||
$current_user = Users::getActiveAdminUser();
|
||||
if (!$current_user) {
|
||||
die("Admin user not found.\n");
|
||||
}
|
||||
} else {
|
||||
require_once $rootDir . '/config.inc.php';
|
||||
require_once $rootDir . '/vtlib/Vtiger/Module.php';
|
||||
require_once $rootDir . '/includes/utils/utils.php';
|
||||
if (!isPermitted('Settings', 'Edit', '')) {
|
||||
die('Access denied.');
|
||||
}
|
||||
}
|
||||
require_once __DIR__ . '/OnlyOfficeTemplates.php';
|
||||
|
||||
$adb = PearDatabase::getInstance();
|
||||
|
||||
// Check if already registered
|
||||
$r = $adb->pquery("SELECT tabid FROM vtiger_tab WHERE name = ?", ['OnlyOfficeTemplates']);
|
||||
if ($adb->num_rows($r) > 0) {
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo "OnlyOfficeTemplates already registered. Running schema and links.\n";
|
||||
}
|
||||
} else {
|
||||
$maxId = $adb->query_result($adb->pquery("SELECT COALESCE(MAX(tabid),0) AS m FROM vtiger_tab", []), 0, 'm');
|
||||
$tabid = $maxId + 1;
|
||||
$maxSeq = $adb->query_result($adb->pquery("SELECT COALESCE(MAX(tabsequence),0) AS m FROM vtiger_tab", []), 0, 'm');
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_tab (tabid, name, presence, tabsequence, tablabel, modifiedby, modifiedtime, customized, ownedby, isentitytype, version, parent) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[$tabid, 'OnlyOfficeTemplates', 0, $maxSeq + 1, 'OnlyOffice Templates', null, null, 0, 0, 0, '1.0', 'Tools']
|
||||
);
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo "Registered OnlyOfficeTemplates (tabid=$tabid).\n";
|
||||
}
|
||||
}
|
||||
|
||||
$mod = new OnlyOfficeTemplates();
|
||||
$mod->executeSql();
|
||||
$mod->addLinksToEntityModules();
|
||||
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo "Done. Schema and widget links applied.\n";
|
||||
} else {
|
||||
header('Location: index.php?module=Settings&parent=Settings&view=Index');
|
||||
exit;
|
||||
}
|
||||
39
modules/OnlyOfficeTemplates/manifest.xml
Normal file
39
modules/OnlyOfficeTemplates/manifest.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<module>
|
||||
<type>extension</type>
|
||||
<name>OnlyOfficeTemplates</name>
|
||||
<label>OnlyOffice Templates</label>
|
||||
<parent>Tools</parent>
|
||||
<version>1.0</version>
|
||||
<dependencies>
|
||||
<vtiger_version>7.*</vtiger_version>
|
||||
</dependencies>
|
||||
<tables>
|
||||
<table>
|
||||
<name>vtiger_oot_templates</name>
|
||||
<sql><![CDATA[
|
||||
CREATE TABLE IF NOT EXISTS vtiger_oot_templates (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
module VARCHAR(100) NOT NULL,
|
||||
s3_key VARCHAR(512) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
owner INT(11) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
settings TEXT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_oot_module (module),
|
||||
KEY idx_oot_owner (owner)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
||||
]]></sql>
|
||||
</table>
|
||||
<table>
|
||||
<name>vtiger_oot_templates_seq</name>
|
||||
<sql><![CDATA[
|
||||
CREATE TABLE IF NOT EXISTS vtiger_oot_templates_seq (
|
||||
id INT(11) NOT NULL DEFAULT 1
|
||||
) ENGINE=InnoDB
|
||||
]]></sql>
|
||||
</table>
|
||||
</tables>
|
||||
</module>
|
||||
124
modules/OnlyOfficeTemplates/models/OnlyOfficeTemplates_Model.php
Normal file
124
modules/OnlyOfficeTemplates/models/OnlyOfficeTemplates_Model.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* OnlyOfficeTemplates model: list templates by module, get from S3, merge, convert.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_Model
|
||||
{
|
||||
protected $db;
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = PearDatabase::getInstance();
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
$this->config = OnlyOfficeTemplates_getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* List templates available for a module (and current user).
|
||||
*
|
||||
* @param string $module
|
||||
* @return array [ ['id' =>, 'name' =>, 'file_name' =>, 'module' =>], ... ]
|
||||
*/
|
||||
public function getTemplatesByModule($module)
|
||||
{
|
||||
$userId = Users_Record_Model::getCurrentUserModel()->getId();
|
||||
$sql = "SELECT id, name, module, file_name, s3_key, owner, created_at
|
||||
FROM vtiger_oot_templates
|
||||
WHERE module = ? AND (owner = ? OR owner IN (SELECT groupid FROM vtiger_users2group WHERE userid = ?))
|
||||
ORDER BY name";
|
||||
$res = $this->db->pquery($sql, [$module, $userId, $userId]);
|
||||
$list = [];
|
||||
while ($row = $this->db->fetchByAssoc($res)) {
|
||||
$list[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'name' => $row['name'],
|
||||
'module' => $row['module'],
|
||||
'file_name' => $row['file_name'],
|
||||
's3_key' => $row['s3_key'],
|
||||
'owner' => (int)$row['owner'],
|
||||
'created_at' => $row['created_at'],
|
||||
];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one template by id (with permission check).
|
||||
*
|
||||
* @param int $templateId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTemplateById($templateId)
|
||||
{
|
||||
$userId = Users_Record_Model::getCurrentUserModel()->getId();
|
||||
$res = $this->db->pquery(
|
||||
"SELECT id, name, module, s3_key, file_name, owner FROM vtiger_oot_templates WHERE id = ? AND (owner = ? OR owner IN (SELECT groupid FROM vtiger_users2group WHERE userid = ?))",
|
||||
[$templateId, $userId, $userId]
|
||||
);
|
||||
if ($this->db->num_rows($res) === 0) {
|
||||
return null;
|
||||
}
|
||||
return $this->db->fetchByAssoc($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save template metadata and S3 key (after upload).
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $module
|
||||
* @param string $s3Key
|
||||
* @param string $fileName
|
||||
* @param int $owner
|
||||
* @return int new template id
|
||||
*/
|
||||
public function saveTemplate($name, $module, $s3Key, $fileName, $owner = null)
|
||||
{
|
||||
if ($owner === null) {
|
||||
$owner = Users_Record_Model::getCurrentUserModel()->getId();
|
||||
}
|
||||
$this->db->pquery("INSERT INTO vtiger_oot_templates (name, module, s3_key, file_name, owner, created_at) VALUES (?,?,?,?,?,NOW())",
|
||||
[$name, $module, $s3Key, $fileName, $owner]);
|
||||
$id = $this->db->getLastInsertID();
|
||||
return $id ? (int)$id : (int)$this->db->query_result($this->db->pquery("SELECT MAX(id) AS n FROM vtiger_oot_templates", []), 0, 'n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete template record and optionally S3 object (caller can delete object).
|
||||
*
|
||||
* @param int $templateId
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteTemplate($templateId)
|
||||
{
|
||||
$t = $this->getTemplateById($templateId);
|
||||
if (!$t) {
|
||||
return false;
|
||||
}
|
||||
$this->db->pquery("DELETE FROM vtiger_oot_templates WHERE id = ?", [$templateId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config (S3 prefix, bucket, OnlyOffice URL).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next id for template (for S3 path).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getNextTemplateId()
|
||||
{
|
||||
$this->db->pquery("UPDATE vtiger_oot_templates_seq SET id = LAST_INSERT_ID(id + 1)", []);
|
||||
$r = $this->db->pquery("SELECT LAST_INSERT_ID() AS n", []);
|
||||
return (int)$this->db->query_result($r, 0, 'n');
|
||||
}
|
||||
}
|
||||
78
modules/OnlyOfficeTemplates/resources/ConvertService.php
Normal file
78
modules/OnlyOfficeTemplates/resources/ConvertService.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* OnlyOffice Conversion API: DOCX -> PDF.
|
||||
* Requires onlyoffice_convert_url (e.g. https://office.clientright.ru:9443/ConvertService.ashx or /converter).
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_ConvertService
|
||||
{
|
||||
protected $convertUrl;
|
||||
protected $documentServerBase;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->convertUrl = rtrim($config['onlyoffice_convert_url'] ?? '', '/');
|
||||
if (strpos($this->convertUrl, '/converter') !== false) {
|
||||
$this->documentServerBase = preg_replace('#/converter.*$#', '', $this->convertUrl);
|
||||
} else {
|
||||
$this->documentServerBase = preg_replace('#/ConvertService\.ashx.*$#', '', $this->convertUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DOCX at given URL to PDF.
|
||||
*
|
||||
* @param string $docxUrl Absolute URL to DOCX (must be accessible by Document Server)
|
||||
* @param string $title File name for display
|
||||
* @return array [ 'success' => bool, 'pdfPath' => temp file path, 'error' => message ]
|
||||
*/
|
||||
public function convertToPdf($docxUrl, $title = 'document.docx')
|
||||
{
|
||||
if (empty($this->convertUrl)) {
|
||||
return ['success' => false, 'error' => 'OnlyOffice conversion URL not configured (OOT_ONLYOFFICE_CONVERT_URL).'];
|
||||
}
|
||||
$key = md5($docxUrl . time());
|
||||
$body = [
|
||||
'async' => false,
|
||||
'filetype' => 'docx',
|
||||
'key' => $key,
|
||||
'outputtype' => 'pdf',
|
||||
'title' => $title,
|
||||
'url' => $docxUrl,
|
||||
];
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
|
||||
'content' => json_encode($body),
|
||||
'timeout' => 120,
|
||||
],
|
||||
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
|
||||
]);
|
||||
$response = @file_get_contents($this->convertUrl, false, $ctx);
|
||||
if ($response === false) {
|
||||
return ['success' => false, 'error' => 'Conversion request failed (connection or timeout).'];
|
||||
}
|
||||
$data = json_decode($response, true);
|
||||
if (!$data) {
|
||||
$data = ['error' => -1, 'endConvert' => false];
|
||||
}
|
||||
if (!empty($data['error'])) {
|
||||
return ['success' => false, 'error' => 'Conversion error code: ' . $data['error']];
|
||||
}
|
||||
if (empty($data['endConvert']) || empty($data['fileUrl'])) {
|
||||
return ['success' => false, 'error' => 'Conversion did not return file URL.'];
|
||||
}
|
||||
$fileUrl = $data['fileUrl'];
|
||||
if (strpos($fileUrl, 'http') !== 0) {
|
||||
$fileUrl = rtrim($this->documentServerBase, '/') . '/' . ltrim($fileUrl, '/');
|
||||
}
|
||||
$tempPdf = sys_get_temp_dir() . '/oot_pdf_' . uniqid() . '.pdf';
|
||||
$pdfContent = @file_get_contents($fileUrl, false, stream_context_create(['http' => ['timeout' => 60], 'ssl' => ['verify_peer' => false]]));
|
||||
if ($pdfContent === false || strlen($pdfContent) === 0) {
|
||||
return ['success' => false, 'error' => 'Failed to download converted PDF from Document Server.'];
|
||||
}
|
||||
file_put_contents($tempPdf, $pdfContent);
|
||||
return ['success' => true, 'pdfPath' => $tempPdf];
|
||||
}
|
||||
}
|
||||
172
modules/OnlyOfficeTemplates/resources/MergeService.php
Normal file
172
modules/OnlyOfficeTemplates/resources/MergeService.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* Merge DOCX template: load from S3, substitute {{field}} and {{ModuleName__field}}, output DOCX.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_MergeService
|
||||
{
|
||||
protected $s3;
|
||||
protected $config;
|
||||
|
||||
public function __construct(OnlyOfficeTemplates_S3Helper $s3, array $config)
|
||||
{
|
||||
$this->s3 = $s3;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build placeholder map for a record: current module fields + related (Account, Contact, etc.).
|
||||
*
|
||||
* @param string $module
|
||||
* @param int $recordId
|
||||
* @return array [ 'fieldname' => value, 'Account__accountname' => value, ... ]
|
||||
*/
|
||||
public function buildPlaceholders($module, $recordId)
|
||||
{
|
||||
$adb = PearDatabase::getInstance();
|
||||
$focus = CRMEntity::getInstance($module);
|
||||
$focus->id = $recordId;
|
||||
$focus->retrieve_entity_info($recordId, $module);
|
||||
$fields = $focus->column_fields;
|
||||
|
||||
$map = [];
|
||||
foreach ($fields as $k => $v) {
|
||||
if ($v === null || $v === '') {
|
||||
$v = '';
|
||||
}
|
||||
$map[$k] = is_string($v) ? $v : (string)$v;
|
||||
}
|
||||
$map = array_merge($map, $this->getRelatedModuleFields($module, $recordId, $focus));
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related entity fields (Account, Contact, etc.) for placeholder {{ModuleName__fieldname}}.
|
||||
*/
|
||||
protected function getRelatedModuleFields($module, $recordId, CRMEntity $focus)
|
||||
{
|
||||
$map = [];
|
||||
$relFields = $this->getRelationFieldNames($module);
|
||||
foreach ($relFields as $relModule => $fieldName) {
|
||||
$relId = isset($focus->column_fields[$fieldName]) ? $focus->column_fields[$fieldName] : null;
|
||||
if (empty($relId)) {
|
||||
continue;
|
||||
}
|
||||
$relFocus = CRMEntity::getInstance($relModule);
|
||||
$relFocus->retrieve_entity_info($relId, $relModule);
|
||||
foreach ($relFocus->column_fields as $k => $v) {
|
||||
if ($v === null || $v === '') {
|
||||
$v = '';
|
||||
}
|
||||
$map[$relModule . '__' . $k] = is_string($v) ? $v : (string)$v;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common relation field names per module (account_id, contact_id, related_to, parent_id, etc.).
|
||||
*/
|
||||
/** @return array [ 'RelatedModule' => 'local_field_name', ... ] */
|
||||
protected function getRelationFieldNames($module)
|
||||
{
|
||||
$known = [
|
||||
'Project' => ['Accounts' => 'account_id', 'Contacts' => 'contact_id'],
|
||||
'Contacts' => ['Accounts' => 'account_id'],
|
||||
'Leads' => ['Accounts' => 'account_id'],
|
||||
'Potentials' => ['Accounts' => 'related_to', 'Contacts' => 'contact_id'],
|
||||
'Invoice' => ['Accounts' => 'account_id', 'Contacts' => 'contact_id'],
|
||||
'Quotes' => ['Accounts' => 'account_id', 'Contacts' => 'contact_id'],
|
||||
'SalesOrder' => ['Accounts' => 'account_id', 'Contacts' => 'contact_id'],
|
||||
'PurchaseOrder' => ['Vendors' => 'vendor_id', 'Contacts' => 'contact_id'],
|
||||
'HelpDesk' => ['Accounts' => 'parent_id', 'Contacts' => 'contact_id'],
|
||||
'Accounts' => [],
|
||||
];
|
||||
if (isset($known[$module])) {
|
||||
return $known[$module];
|
||||
}
|
||||
$out = [];
|
||||
if (in_array($module, ['Contacts', 'Leads', 'Potentials', 'Invoice', 'Quotes', 'SalesOrder'])) {
|
||||
$out['Accounts'] = 'account_id';
|
||||
if ($module !== 'Accounts') {
|
||||
$out['Contacts'] = 'contact_id';
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge template: download from S3, replace placeholders, save to temp file.
|
||||
*
|
||||
* @param string $s3Key template S3 key
|
||||
* @param array $placeholders [ 'field' => 'value', 'Account__name' => 'value' ]
|
||||
* @return string path to merged DOCX file
|
||||
*/
|
||||
public function mergeToFile($s3Key, array $placeholders)
|
||||
{
|
||||
$path = dirname(dirname(dirname(__DIR__)));
|
||||
if (!class_exists('PhpOffice\PhpWord\IOFactory')) {
|
||||
if (is_file($path . '/vendor/autoload.php')) {
|
||||
require_once $path . '/vendor/autoload.php';
|
||||
}
|
||||
}
|
||||
$tempDir = sys_get_temp_dir() . '/oot_' . uniqid();
|
||||
if (!is_dir($tempDir)) {
|
||||
mkdir($tempDir, 0755, true);
|
||||
}
|
||||
$templatePath = $tempDir . '/template.docx';
|
||||
$this->s3->downloadToFile($s3Key, $templatePath);
|
||||
|
||||
$phpWord = \PhpOffice\PhpWord\IOFactory::load($templatePath);
|
||||
$this->replaceInPhpWord($phpWord, $placeholders);
|
||||
$outPath = $tempDir . '/merged.docx';
|
||||
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
|
||||
$writer->save($outPath);
|
||||
@unlink($templatePath);
|
||||
return $outPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace {{placeholder}} in all text elements.
|
||||
*/
|
||||
protected function replaceInPhpWord(\PhpOffice\PhpWord\PhpWord $phpWord, array $placeholders)
|
||||
{
|
||||
foreach ($phpWord->getSections() as $section) {
|
||||
foreach ($section->getElements() as $element) {
|
||||
$this->replaceInElement($element, $placeholders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function replaceInElement($element, array $placeholders)
|
||||
{
|
||||
if ($element instanceof \PhpOffice\PhpWord\Element\Text) {
|
||||
$text = $element->getText();
|
||||
$text = $this->replacePlaceholders($text, $placeholders);
|
||||
$element->setText($text);
|
||||
return;
|
||||
}
|
||||
if ($element instanceof \PhpOffice\PhpWord\Element\TextRun) {
|
||||
foreach ($element->getElements() as $el) {
|
||||
$this->replaceInElement($el, $placeholders);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($element instanceof \PhpOffice\PhpWord\Element\TextBreak) {
|
||||
return;
|
||||
}
|
||||
if (method_exists($element, 'getElements')) {
|
||||
foreach ($element->getElements() as $el) {
|
||||
$this->replaceInElement($el, $placeholders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function replacePlaceholders($text, array $placeholders)
|
||||
{
|
||||
foreach ($placeholders as $key => $value) {
|
||||
$text = str_replace('{{' . $key . '}}', $value, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
120
modules/OnlyOfficeTemplates/resources/S3Helper.php
Normal file
120
modules/OnlyOfficeTemplates/resources/S3Helper.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* Portable S3 helper for OnlyOfficeTemplates (get/put using module config).
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_S3Helper
|
||||
{
|
||||
protected $client;
|
||||
protected $bucket;
|
||||
protected $prefix;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->bucket = $config['s3_bucket'] ?? ($config['s3']['bucket'] ?? '');
|
||||
$this->prefix = rtrim($config['s3_prefix'] ?? 'crm2/OnlyOfficeTemplates', '/');
|
||||
if ($this->bucket === null || $this->bucket === '') {
|
||||
throw new Exception('OnlyOfficeTemplates: S3 bucket not configured. Set S3_BUCKET in .env or add bucket to crm_extensions/file_storage/config.php (s3.bucket or root bucket).');
|
||||
}
|
||||
$s3 = $config['s3'] ?? [];
|
||||
$path = dirname(dirname(dirname(__DIR__)));
|
||||
if (!class_exists('Aws\S3\S3Client')) {
|
||||
if (is_file($path . '/vendor/autoload.php')) {
|
||||
require_once $path . '/vendor/autoload.php';
|
||||
}
|
||||
}
|
||||
$this->client = new Aws\S3\S3Client([
|
||||
'version' => $s3['version'] ?? 'latest',
|
||||
'region' => $s3['region'] ?? 'ru-1',
|
||||
'endpoint' => $s3['endpoint'],
|
||||
'use_path_style_endpoint' => !empty($s3['use_path_style_endpoint']),
|
||||
'credentials' => [
|
||||
'key' => $s3['key'],
|
||||
'secret' => $s3['secret'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Template S3 key: {prefix}/templates/{templateId}/{fileName}
|
||||
*/
|
||||
public function getTemplateKey($templateId, $fileName)
|
||||
{
|
||||
return $this->prefix . '/templates/' . (int)$templateId . '/' . $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temp generated file key: {prefix}/temp/{recordId}_{templateId}_{timestamp}.ext
|
||||
*/
|
||||
public function getTempKey($recordId, $templateId, $extension)
|
||||
{
|
||||
return $this->prefix . '/temp/' . (int)$recordId . '_' . (int)$templateId . '_' . time() . '.' . $extension;
|
||||
}
|
||||
|
||||
public function getBucket()
|
||||
{
|
||||
return $this->bucket;
|
||||
}
|
||||
|
||||
public function getPrefix()
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download object to a local file; returns path.
|
||||
*/
|
||||
public function downloadToFile($s3Key, $localPath)
|
||||
{
|
||||
$this->client->getObject([
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $s3Key,
|
||||
'SaveAs' => $localPath,
|
||||
]);
|
||||
return $localPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object body as string.
|
||||
*/
|
||||
public function getObjectBody($s3Key)
|
||||
{
|
||||
$result = $this->client->getObject([
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $s3Key,
|
||||
]);
|
||||
return (string)$result['Body'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Put string or file into S3.
|
||||
*/
|
||||
public function putObject($s3Key, $body, $contentType = null)
|
||||
{
|
||||
$params = [
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $s3Key,
|
||||
'Body' => $body,
|
||||
];
|
||||
if ($contentType) {
|
||||
$params['ContentType'] = $contentType;
|
||||
}
|
||||
$this->client->putObject($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload local file to S3.
|
||||
*/
|
||||
public function uploadFile($localPath, $s3Key, $contentType = null)
|
||||
{
|
||||
$params = [
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $s3Key,
|
||||
'SourceFile' => $localPath,
|
||||
];
|
||||
if ($contentType) {
|
||||
$params['ContentType'] = $contentType;
|
||||
}
|
||||
$this->client->putObject($params);
|
||||
}
|
||||
}
|
||||
31
modules/OnlyOfficeTemplates/schema.xml
Normal file
31
modules/OnlyOfficeTemplates/schema.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0"?>
|
||||
<schema>
|
||||
<tables>
|
||||
<table>
|
||||
<name>vtiger_oot_templates</name>
|
||||
<sql><![CDATA[
|
||||
CREATE TABLE IF NOT EXISTS vtiger_oot_templates (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
module VARCHAR(100) NOT NULL,
|
||||
s3_key VARCHAR(512) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
owner INT(11) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
settings TEXT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_oot_module (module),
|
||||
KEY idx_oot_owner (owner)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
||||
]]></sql>
|
||||
</table>
|
||||
<table>
|
||||
<name>vtiger_oot_templates_seq</name>
|
||||
<sql><![CDATA[
|
||||
CREATE TABLE IF NOT EXISTS vtiger_oot_templates_seq (
|
||||
id INT(11) NOT NULL DEFAULT 1
|
||||
) ENGINE=InnoDB
|
||||
]]></sql>
|
||||
</table>
|
||||
</tables>
|
||||
</schema>
|
||||
39
modules/OnlyOfficeTemplates/views/AddTemplate.php
Normal file
39
modules/OnlyOfficeTemplates/views/AddTemplate.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* Form to add a new DOCX template (name, module, file upload).
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_AddTemplate_View extends Vtiger_Index_View
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$tabId = getTabId($moduleName);
|
||||
$privileges = Users_Privileges_Model::getCurrentUserPrivilegesModel();
|
||||
if (!$privileges->hasModulePermission($tabId)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$viewer = $this->getViewer($request);
|
||||
|
||||
$db = PearDatabase::getInstance();
|
||||
$res = $db->pquery(
|
||||
"SELECT name FROM vtiger_tab WHERE isentitytype = 1 AND presence = 0 ORDER BY name",
|
||||
[]
|
||||
);
|
||||
$modules = [];
|
||||
while ($row = $db->fetchByAssoc($res)) {
|
||||
$modules[$row['name']] = vtranslate($row['name'], $row['name']);
|
||||
}
|
||||
|
||||
$errorMsg = $request->get('error');
|
||||
$viewer->assign('MODULE_NAME', $moduleName);
|
||||
$viewer->assign('MODULES', $modules);
|
||||
$viewer->assign('ERROR_MSG', $errorMsg ?: '');
|
||||
$viewer->view('AddTemplate.tpl', $moduleName);
|
||||
}
|
||||
}
|
||||
109
modules/OnlyOfficeTemplates/views/Edit.php
Normal file
109
modules/OnlyOfficeTemplates/views/Edit.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Edit/Create template: left = metadata (name, module), right = OnlyOffice Document Editor in iframe.
|
||||
* Document is loaded from GetDocument and saved to S3 via OnlyOffice callback.
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_Edit_View extends Vtiger_Index_View
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$tabId = getTabId($moduleName);
|
||||
$privileges = Users_Privileges_Model::getCurrentUserPrivilegesModel();
|
||||
if (!$privileges->hasModulePermission($tabId)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$templateId = (int)$request->get('templateid');
|
||||
$viewer = $this->getViewer($request);
|
||||
$adb = PearDatabase::getInstance();
|
||||
$currentUser = Users_Record_Model::getCurrentUserModel();
|
||||
$userId = $currentUser->getId();
|
||||
|
||||
$template = null;
|
||||
if ($templateId > 0) {
|
||||
$res = $adb->pquery(
|
||||
"SELECT id, name, module, file_name, owner FROM vtiger_oot_templates WHERE id = ?",
|
||||
[$templateId]
|
||||
);
|
||||
$template = $adb->fetchByAssoc($res);
|
||||
if ($template) {
|
||||
$owner = (int)$template['owner'];
|
||||
if ($owner !== $userId) {
|
||||
$gr = $adb->pquery("SELECT 1 FROM vtiger_users2group WHERE userid = ? AND groupid = ?", [$userId, $owner]);
|
||||
if ($adb->num_rows($gr) === 0) {
|
||||
$template = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
$template = [
|
||||
'id' => 0,
|
||||
'name' => '',
|
||||
'module' => '',
|
||||
'file_name' => 'document.docx',
|
||||
];
|
||||
}
|
||||
|
||||
$res = $adb->pquery(
|
||||
"SELECT name FROM vtiger_tab WHERE isentitytype = 1 AND presence = 0 ORDER BY name",
|
||||
[]
|
||||
);
|
||||
$modules = [];
|
||||
while ($row = $adb->fetchByAssoc($res)) {
|
||||
$modules[$row['name']] = vtranslate($row['name'], $row['name']);
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
$config = OnlyOfficeTemplates_getConfig();
|
||||
$docServer = rtrim($config['onlyoffice_document_server'] ?? '', '/');
|
||||
if ($docServer === '') {
|
||||
$viewer->assign('OOT_EDITOR_AVAILABLE', false);
|
||||
$viewer->assign('OOT_EDITOR_MESSAGE', 'OnlyOffice Document Server не настроен (ONLYOFFICE_DOCUMENT_SERVER).');
|
||||
} else {
|
||||
$viewer->assign('OOT_EDITOR_AVAILABLE', true);
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$tid = (int)$template['id'];
|
||||
$documentUrl = $baseUrl . '/index.php?module=OnlyOfficeTemplates&action=GetDocument&template_id=' . $tid;
|
||||
$secret = $config['document_secret'] ?? '';
|
||||
if ($secret !== '' && $tid > 0) {
|
||||
$documentUrl .= '&token=' . rawurlencode(hash_hmac('sha256', (string)$tid, $secret));
|
||||
}
|
||||
$callbackUrl = $baseUrl . '/index.php?module=OnlyOfficeTemplates&action=OnlyOfficeCallback';
|
||||
$docKey = $tid > 0 ? (string)$tid : ('new_' . $userId . '_' . time());
|
||||
$viewer->assign('OOT_DOCUMENT_SERVER', $docServer);
|
||||
$viewer->assign('OOT_DOCUMENT_URL', $documentUrl);
|
||||
$viewer->assign('OOT_CALLBACK_URL', $callbackUrl);
|
||||
$viewer->assign('OOT_DOC_KEY', $docKey);
|
||||
$viewer->assign('OOT_DOC_TITLE', $template['file_name'] ?: 'document.docx');
|
||||
}
|
||||
|
||||
$viewer->assign('MODULE_NAME', $moduleName);
|
||||
$viewer->assign('TEMPLATE', $template);
|
||||
$viewer->assign('MODULES', $modules);
|
||||
$viewer->assign('ERROR_MSG', $request->get('error') ?: '');
|
||||
$viewer->view('Edit.tpl', $moduleName);
|
||||
}
|
||||
|
||||
protected function getBaseUrl()
|
||||
{
|
||||
if (function_exists('vglobal') && (vglobal('site_URL') ?? '') !== '') {
|
||||
return rtrim(vglobal('site_URL'), '/');
|
||||
}
|
||||
$proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$path = dirname($_SERVER['SCRIPT_NAME'] ?? '');
|
||||
$path = str_replace('\\', '/', $path);
|
||||
if ($path === '/' || $path === '') {
|
||||
return $proto . '://' . $host;
|
||||
}
|
||||
return $proto . '://' . $host . $path;
|
||||
}
|
||||
}
|
||||
48
modules/OnlyOfficeTemplates/views/GetTemplateActions.php
Normal file
48
modules/OnlyOfficeTemplates/views/GetTemplateActions.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Widget view: list OnlyOffice templates for the record's module and actions (Download / Save to Documents) with format (PDF/DOCX).
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_GetTemplateActions_View extends Vtiger_BasicAjax_View
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$module = false;
|
||||
if ($request->has('source_module') && !$request->isEmpty('source_module')) {
|
||||
$source_module = $request->get('source_module');
|
||||
} elseif ($request->has('record') && !$request->isEmpty('record')) {
|
||||
$source_module = $module = getSalesEntityType($request->get('record'));
|
||||
}
|
||||
$sourceModuleModel = Vtiger_Module_Model::getInstance($source_module);
|
||||
if (!$sourceModuleModel || !$sourceModuleModel->isEntityModule()) {
|
||||
return;
|
||||
}
|
||||
if (!$request->has('record') || $request->isEmpty('record')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$record = $request->get('record');
|
||||
if (!$module) {
|
||||
$module = getSalesEntityType($record);
|
||||
}
|
||||
if ($module !== $source_module) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/models/OnlyOfficeTemplates_Model.php';
|
||||
$model = new OnlyOfficeTemplates_Model();
|
||||
$templates = $model->getTemplatesByModule($module);
|
||||
|
||||
$viewer = $this->getViewer($request);
|
||||
$viewer->assign('MODULE', $module);
|
||||
$viewer->assign('ID', $record);
|
||||
$viewer->assign('CRM_TEMPLATES', $templates);
|
||||
$viewer->assign('CRM_TEMPLATES_EXIST', empty($templates) ? 1 : 0);
|
||||
$viewer->assign('OOT_MOD', return_module_language(Vtiger_Language_Handler::getLanguage(), 'OnlyOfficeTemplates'));
|
||||
$viewer->view('GetTemplateActions.tpl', 'OnlyOfficeTemplates');
|
||||
}
|
||||
}
|
||||
41
modules/OnlyOfficeTemplates/views/List.php
Normal file
41
modules/OnlyOfficeTemplates/views/List.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple list view for OnlyOfficeTemplates (non-entity module).
|
||||
*/
|
||||
|
||||
class OnlyOfficeTemplates_List_View extends Vtiger_Index_View
|
||||
{
|
||||
public function checkPermission(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$tabId = getTabId($moduleName);
|
||||
$privileges = Users_Privileges_Model::getCurrentUserPrivilegesModel();
|
||||
if (!$privileges->hasModulePermission($tabId)) {
|
||||
throw new AppException('LBL_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Vtiger_Request $request)
|
||||
{
|
||||
$moduleName = $request->getModule();
|
||||
$viewer = $this->getViewer($request);
|
||||
|
||||
$db = PearDatabase::getInstance();
|
||||
$userId = Users_Record_Model::getCurrentUserModel()->getId();
|
||||
$res = $db->pquery(
|
||||
"SELECT id, name, module, file_name, owner, created_at
|
||||
FROM vtiger_oot_templates
|
||||
WHERE owner = ? OR owner IN (SELECT groupid FROM vtiger_users2group WHERE userid = ?)
|
||||
ORDER BY created_at DESC",
|
||||
[$userId, $userId]
|
||||
);
|
||||
$templates = [];
|
||||
while ($row = $db->fetchByAssoc($res)) {
|
||||
$templates[] = $row;
|
||||
}
|
||||
|
||||
$viewer->assign('MODULE_NAME', $moduleName);
|
||||
$viewer->assign('TEMPLATES', $templates);
|
||||
$viewer->view('List.tpl', $moduleName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user