pdf filename

This commit is contained in:
Yuri Kuznetsov
2025-10-30 18:26:01 +02:00
parent 3e23994991
commit 4d830e76b0
8 changed files with 179 additions and 37 deletions
@@ -37,12 +37,16 @@ use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Utils\Util;
use Espo\ORM\Entity;
use Espo\Tools\Pdf\Params;
use Espo\Core\Di;
use Espo\Tools\Pdf\Result;
use Espo\Tools\Pdf\Service;
use Exception;
/**
* @noinspection PhpUnused
*/
class GenerateType extends BaseFunction implements
Di\EntityManagerAware,
Di\InjectableFactoryAware,
@@ -92,38 +96,29 @@ class GenerateType extends BaseFunction implements
}
if (!$entity) {
$this->log("Record {$entityType} {$id} does not exist.");
$this->log("Record $entityType $id does not exist.");
throw new Error();
}
/** @var ?Template $template */
$template = $em->getEntityById(Template::ENTITY_TYPE, $templateId);
$template = $em->getRDBRepositoryByClass(Template::class)->getById($templateId);
if (!$template) {
$this->log("Template {$templateId} does not exist.");
$this->log("Template $templateId does not exist.");
throw new Error();
}
if ($fileName) {
if (!str_ends_with($fileName, '.pdf')) {
$fileName .= '.pdf';
}
} else {
$fileName = Util::sanitizeFileName($entity->get(Field::NAME)) . '.pdf';
}
$params = Params::create()->withAcl(false);
try {
$service = $this->injectableFactory->create(Service::class);
$service = $this->injectableFactory->create(Service::class);
$contents = $service->generate(
$entity->getEntityType(),
$entity->getId(),
$template->getId(),
$params
try {
$result = $service->generate(
entityType: $entity->getEntityType(),
id: $entity->getId(),
templateId: $template->getId(),
params: $params,
);
} catch (Exception $e) {
$this->log("Error while generating. Message: " . $e->getMessage() . ".", 'error');
@@ -131,20 +126,41 @@ class GenerateType extends BaseFunction implements
throw new Error();
}
/** @var Attachment $attachment */
$attachment = $em->getNewEntity(Attachment::ENTITY_TYPE);
$fileName = $this->prepareFilename($fileName, $result, $entity);
$attachment = $em->getRDBRepositoryByClass(Attachment::class)->getNew();
$attachment
->setName($fileName)
->setType('application/pdf')
->setSize($contents->getStream()->getSize())
->setSize($result->getStream()->getSize())
->setRelated(LinkParent::create($entityType, $id))
->setRole(Attachment::ROLE_ATTACHMENT);
$em->saveEntity($attachment);
$this->fileStorageManager->putStream($attachment, $contents->getStream());
$this->fileStorageManager->putStream($attachment, $result->getStream());
return $attachment->getId();
}
private function composeFilename(Entity $entity): string
{
$defaultName = $entity->get(Field::NAME) ?? $entity->getId();
return Util::sanitizeFileName($defaultName) . '.pdf';
}
private function prepareFilename(mixed $fileName, Result $result, Entity $entity): string
{
if ($fileName) {
if (!str_ends_with($fileName, '.pdf')) {
$fileName .= '.pdf';
}
return $fileName;
}
return $result->getFilename() ?? $this->composeFilename($entity);
}
}
+5
View File
@@ -53,4 +53,9 @@ class Template extends Entity
{
return $this->get('status') === self::STATUS_ACTIVE;
}
public function getFilename(): ?string
{
return $this->get('filename');
}
}
+12 -6
View File
@@ -38,6 +38,7 @@ use Espo\Core\Name\Field;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Util;
use Espo\Entities\Template;
use Espo\ORM\Entity;
use Espo\Tools\Pdf\Service;
class Pdf implements EntryPoint
@@ -69,11 +70,9 @@ class Pdf implements EntryPoint
throw new NotFound("Template not found.");
}
$contents = $this->service->generate($entityType, $entityId, $templateId);
$result = $this->service->generate($entityType, $entityId, $templateId);
$fileName = Util::sanitizeFileName($entity->get(Field::NAME) ?? 'unnamed');
$fileName = $fileName . '.pdf';
$fileName = $result->getFilename() ?? $this->composeFileName($entity);
$response
->setHeader('Content-Type', 'application/pdf')
@@ -83,9 +82,16 @@ class Pdf implements EntryPoint
->setHeader('Content-Disposition', 'inline; filename="' . basename($fileName) . '"');
if (!$request->getServerParam('HTTP_ACCEPT_ENCODING')) {
$response->setHeader('Content-Length', (string) $contents->getStream()->getSize());
$response->setHeader('Content-Length', (string) $result->getStream()->getSize());
}
$response->writeBody($contents->getStream());
$response->writeBody($result->getStream());
}
private function composeFileName(Entity $entity): string
{
$defaultName = $entity->get(Field::NAME) ?? $entity->getId();
return Util::sanitizeFileName($defaultName) . '.pdf';
}
}
@@ -21,7 +21,8 @@
"fontFace": "Font",
"title": "Title",
"style": "Style",
"status": "Status"
"status": "Status",
"filename": "Filename"
},
"links": {
},
@@ -48,6 +49,7 @@
"fontFace": {}
},
"tooltips": {
"filename": "An optional filename. Entity attributes can be used as placeholders.\n\nAdditional placeholder:\n- `{{today}}`\n\nExample:\n`Document_{{today}}_{{name}}`",
"footer": "Use {pageNumber} to print page number.",
"variables": "Copy-paste needed placeholder to Header, Body or Footer."
}
@@ -35,11 +35,11 @@
},
{
"rows": [
[{"name": "title"}, false],
[{"name": "title"}, {"name": "filename"}],
[{"name":"printHeader"}, {"name":"headerPosition"}],
[{"name":"header","fullWidth":true}],
[{"name":"header"}],
[{"name":"printFooter"}, {"name":"footerPosition"}],
[{"name":"footer","fullWidth":true}],
[{"name":"footer"}],
[{"name": "style"}]
]
}
@@ -120,6 +120,11 @@
},
"title": {
"type": "varchar"
},
"filename": {
"type": "varchar",
"maxLength": 150,
"tooltip": true
}
},
"links": {
+66
View File
@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf;
use Psr\Http\Message\StreamInterface;
/**
* @since 9.3.0
*/
class Result implements Contents
{
public function __construct(
private Contents $contents,
private ?string $filename
) {}
public function getStream(): StreamInterface
{
return $this->contents->getStream();
}
public function getString(): string
{
return $this->contents->getString();
}
public function getLength(): int
{
return $this->contents->getLength();
}
/**
* If null, it means that the template does not define the file name.
*/
public function getFilename(): ?string
{
return $this->filename;
}
}
+45 -3
View File
@@ -35,8 +35,11 @@ use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Util;
use Espo\Entities\Template as TemplateEntity;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Tools\Pdf\Data\DataLoaderManager;
@@ -52,6 +55,7 @@ class Service
private Config $config,
private Builder $builder,
private Metadata $metadata,
private DateTime $dateTime,
) {}
/**
@@ -72,8 +76,8 @@ class Service
string $id,
string $templateId,
?Params $params = null,
?Data $data = null
): Contents {
?Data $data = null,
): Result {
$params = $params ?? Params::create()->withAcl(true);
@@ -130,6 +134,44 @@ class Service
->setEngine($engine)
->build();
return $printer->printEntity($entity, $params, $data);
$contents = $printer->printEntity($entity, $params, $data);
$filename = $this->prepareFilename($entity, $template, $params);
return new Result($contents, $filename);
}
private function prepareFilename(Entity $entity, TemplateEntity $template, Params $params): ?string
{
$filename = $template->getFilename() ?? null;
if (!$filename) {
return null;
}
$forbiddenAttributeList = $this->acl->getScopeForbiddenAttributeList($entity->getEntityType());
foreach ($entity->getAttributeList() as $attribute) {
$value = $entity->get($attribute);
if (!is_scalar($value)) {
continue;
}
if ($params->applyAcl() && in_array($attribute, $forbiddenAttributeList)) {
continue;
}
$filename = str_replace('{{' . $attribute . '}}', (string) $value, $filename);
}
$today = $this->dateTime->getTodayString(null, DateTime::SYSTEM_DATE_FORMAT);
$filename = str_replace('{{today}}', $today, $filename);
if (!str_ends_with(strtolower($filename), '.pdf')) {
$filename .= '.pdf';
}
return Util::sanitizeFileName($filename) ?: null;
}
}