lam/lam/lib/pdfstruct.inc
2025-09-03 07:52:42 +02:00

1712 lines
46 KiB
PHP

<?php
namespace LAM\PDF;
use LAM\PERSISTENCE\ConfigurationDatabase;
use LAM\TYPES\TypeManager;
use LAMCfgMain;
use LAMConfig;
use LAMException;
use LAM\ImageUtils\ImageManipulationFactory;
use PDO;
use ServerProfilePersistenceManager;
use XMLReader;
use XMLWriter;
use function LAM\PERSISTENCE\dbTableExists;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2003 - 2006 Michael Duergner
2011 - 2025 Roland Gruber
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Functions to manage the PDF structures.
*
* @author Michael Duergner
* @package PDF
*/
/** LAM configuration */
include_once(__DIR__ . "/config.inc");
/**
* Use as server profile name to manage global templates.
*/
const GLOBAL_PROFILE = '__GLOBAL__';
/** LDAP object */
include_once(__DIR__ . "/ldap.inc");
/**
* Manages the persistence of PDF structures.
*
* @package LAM\PDF
*/
class PdfStructurePersistenceManager {
/**
* @var PdfStructurePersistenceStrategy
*/
private $strategy;
/**
* Constructor
*/
public function __construct() {
$configDb = new ConfigurationDatabase(new LAMCfgMain());
if ($configDb->useRemoteDb()) {
$this->strategy = new PdfStructurePersistenceStrategyPdo($configDb->getPdo());
}
else {
$this->strategy = new PdfStructurePersistenceStrategyFiles();
}
}
/**
* Returns the names of existing PDF structure templates.
*
* @return array scope => names (e.g. array('user' => array('default')))
* @throws LAMException error reading templates
*/
public function getPdfStructureTemplateNames(): array {
return $this->strategy->getPdfStructureTemplateNames();
}
/**
* Deletes an PDF structure template.
*
* @param string $scope user/group/host
* @param string $name PDF structure name
* @throws LAMException error deleting template
*/
public function deletePdfStructureTemplate(string $scope, string $name): void {
if (!$this->isValidPdfStructureName($name) || !TypeManager::isValidTypeId($scope) || ($name === 'default')) {
logNewMessage(LOG_NOTICE, "Invalid account profile name: $name:$scope");
throw new LAMException(_("Unable to delete profile!"));
}
$this->strategy->deletePdfStructureTemplate($scope, $name);
}
/**
* Reads a PDF structure template.
*
* @param string $scope user/group/host
* @param string $name structure name
* @return PDFStructure PDF structure
* @throws LAMException error deleting structure
*/
public function readPdfStructureTemplate(string $scope, string $name): PDFStructure {
if (!TypeManager::isValidTypeId($scope) || !$this->isValidPDFStructureName($name)) {
throw new LAMException(_('Unable to read PDF structure.'));
}
return $this->strategy->readPdfStructureTemplate($scope, $name);
}
/**
* Saves the PDF structure.
*
* @param string $scope user/group/host
* @param string $name structure name
* @param PDFStructure $structure structure
* @throws LAMException error saving structure
*/
public function savePdfStructureTemplate(string $scope, string $name, PDFStructure $structure): void {
if (!TypeManager::isValidTypeId($scope) || !$this->isValidPDFStructureName($name)) {
throw new LAMException(_('PDF structure name not valid'));
}
$this->strategy->savePdfStructureTemplate($scope, $name, $structure);
}
/**
* Returns a list of template logo file names.
*
* @return string[] logo file names
*/
public function getPdfTemplateLogoNames(): array {
return $this->strategy->getPdfTemplateLogoNames();
}
/**
* Returns the binary data for the given template logo.
*
* @param string $name file name
* @return string binary
* @throws LAMException error reading file
*/
public function getPdfTemplateLogoBinary(string $name): string {
if (!$this->isValidLogoFileName($name)) {
throw new LAMException(_('Unable to read logo file.'));
}
return $this->strategy->getPdfTemplateLogoBinary($name);
}
/**
* Deletes a logo in global templates.
*
* @param string $name logo name
* @throws LAMException error during deletion
*/
public function deletePdfTemplateLogo(string $name): void {
if (!$this->isValidLogoFileName($name)) {
throw new LAMException(_('Unable to delete logo file.'));
}
$this->strategy->deletePdfTemplateLogo($name);
}
/**
* Saves the template logo.
*
* @param string $name file name
* @param string $data binary data
* @throws LAMException error during save
*/
public function savePdfTemplateLogo(string $name, string $data): void {
if (!$this->isValidLogoFileName($name)) {
throw new LAMException(_('Unable to upload logo file.'));
}
$this->strategy->savePdfTemplateLogo($name, $data);
}
/**
* Returns the list of available PDF logos.
*
* @param string $confName server profile name
* @param bool $readDimensions reads the image dimensions
* @return PdfLogo[] logos
* @throws LAMException error reading logos
*/
public function getPdfLogos(string $confName, bool $readDimensions = false): array {
if (!LAMConfig::isValidName($confName)) {
throw new LAMException(_('Unable to read logos.'));
}
$logoNames = $this->strategy->getPdfLogoNames($confName);
sort($logoNames);
$result = [];
if ($readDimensions) {
include_once __DIR__ . '/imageutils.inc';
}
foreach ($logoNames as $logoName) {
if ($readDimensions) {
$binary = $this->getPdfLogoBinary($confName, $logoName);
$imageManipulator = ImageManipulationFactory::getImageManipulator($binary);
$result[] = new PdfLogo($logoName, $imageManipulator->getHeight(), $imageManipulator->getWidth());
$imageManipulator = null;
}
else {
$result[] = new PdfLogo($logoName);
}
}
return $result;
}
/**
* Reads a PDF logo.
*
* @param string $confName server profile name
* @param string $name file name
* @return string binary data
* @throws LAMException error reading logo
*/
public function getPdfLogoBinary(string $confName, string $name): string {
if (!LAMConfig::isValidName($confName) || !$this->isValidLogoFileName($name)) {
logNewMessage(LOG_ERR, "Unable to read PDF logo " . $name);
throw new LAMException(_('Unable to read logo file.'));
}
return $this->strategy->getPdfLogoBinary($confName, $name);
}
/**
* Deletes a PDF logo.
*
* @param string $confName server profile name
* @param string $name logo name
* @throws LAMException error deleting logo
*/
public function deletePdfLogo(string $confName, string $name): void {
if (!LAMConfig::isValidName($confName) || !$this->isValidLogoFileName($name)) {
throw new LAMException(_('Unable to delete logo file.'));
}
// check if existing
$found = false;
$logos = $this->getPdfLogos($confName);
foreach ($logos as $logo) {
if ($logo->getName() === $name) {
$found = true;
break;
}
}
if (!$found) {
throw new LAMException(_('File does not exist.'), htmlspecialchars($name));
}
// check if still in use
$typeManager = new TypeManager();
$activeTypes = $typeManager->getConfiguredTypes();
foreach ($activeTypes as $type) {
$structures = $this->getPDFStructures($confName, $type->getId());
foreach ($structures as $structure) {
$data = $this->readPdfStructure($confName, $type->getId(), $structure);
if ($data->getLogo() == $name) {
throw new LAMException(_('Unable to delete logo file.'),
sprintf(_('Logo is still in use by PDF structure "%s" in account type "%s".'), $structure, $type->getAlias()));
}
}
}
$this->strategy->deletePdfLogo($confName, $name);
}
/**
* Saves a PDF logo.
*
* @param string $confName server profile name
* @param string $name logo name
* @param string $data binary
* @throws LAMException error saving logo
*/
public function savePdfLogo(string $confName, string $name, string $data): void {
if (!LAMConfig::isValidName($confName)) {
throw new LAMException(_('Unable to upload logo file.'));
}
if (!$this->isValidLogoFileName($name)) {
throw new LAMException(_('Unable to upload logo file.'), _('The file name must end with ".png" or ".jpg".'));
}
$this->strategy->savePdfLogo($confName, $name, $data);
}
/**
* Returns all available PDF structure definitions for the submitted account type.
*
* @param string $confName server profile name
* @param string $typeId the account type
*
* @return string[] structure names
* @throws LAMException error reading structures
*/
public function getPDFStructures(string $confName, string $typeId): array {
if (!TypeManager::isValidTypeId($typeId)) {
throw new LAMException(_('Unable to read PDF structures.'));
}
$return = $this->strategy->getPDFStructures($confName, $typeId);
sort($return);
return $return;
}
/**
* Deletes a PDF structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @throws LAMException error deleting structure
*/
public function deletePdfStructure(string $confName, string $typeId, string $name): void {
if (!LAMConfig::isValidName($confName) || !TypeManager::isValidTypeId($typeId) || !$this->isValidPDFStructureName($name)) {
logNewMessage(LOG_ERR, 'Invalid data: ' . $confName . ' ' . $typeId . ' ' . $name);
throw new LAMException(_('Unable to delete PDF structure!'));
}
$this->strategy->deletePdfStructure($confName, $typeId, $name);
}
/**
* Reads a PDF structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @return PDFStructure PDF structure
* @throws LAMException error deleting structure
*/
public function readPdfStructure(string $confName, string $typeId, string $name): PDFStructure {
if (!LAMConfig::isValidName($confName) || !TypeManager::isValidTypeId($typeId) || !$this->isValidPDFStructureName($name)) {
throw new LAMException(_('Unable to read PDF structure.'));
}
return $this->strategy->readPdfStructure($confName, $typeId, $name);
}
/**
* Saves the PDF structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @param PDFStructure $structure structure
* @throws LAMException error saving structure
*/
public function savePdfStructure(string $confName, string $typeId, string $name, PDFStructure $structure): void {
if (!LAMConfig::isValidName($confName) || !TypeManager::isValidTypeId($typeId) || !$this->isValidPDFStructureName($name)) {
throw new LAMException(_('PDF structure name not valid'));
}
$this->strategy->savePdfStructure($confName, $typeId, $name, $structure);
}
/**
* Returns if the give structure name is valid.
*
* @param string $name structure name
* @return boolean is valid
*/
private function isValidPDFStructureName(string $name): bool {
return preg_match('/^[a-z0-9_-]+$/i', $name) === 1;
}
/**
* Returns if the given logo file name is valid.
*
* @param string $fileName file name
* @return bool valid
*/
private function isValidLogoFileName(string $fileName): bool {
return preg_match('/^[a-z0-9_-]+\\.(png|jpg)$/im', $fileName) === 1;
}
/**
* Installs template structures to the given server profile.
*
* @param string $confName server profile name
* @throws LAMException error during installation
*/
public function installPDFTemplates(string $confName) {
if (!LAMConfig::isValidName($confName)) {
throw new LAMException(_("Profile name is invalid!"));
}
$serverProfilesPersistenceManager = new ServerProfilePersistenceManager();
$config = $serverProfilesPersistenceManager->loadProfile($confName);
$typeManager = new TypeManager($config);
$allTemplates = $this->getPdfStructureTemplateNames();
foreach ($typeManager->getConfiguredTypes() as $type) {
if (empty($allTemplates[$type->getScope()])) {
continue;
}
$existingStructures = $this->getPDFStructures($confName, $type->getId());
foreach ($allTemplates[$type->getScope()] as $templateName) {
if (!in_array($templateName, $existingStructures)) {
$structure = $this->readPdfStructureTemplate($type->getScope(), $templateName);
$this->savePdfStructure($confName, $type->getId(), $templateName, $structure);
}
}
}
$logos = $this->getPdfTemplateLogoNames();
$existingLogos = $this->getPdfLogos($confName);
$existingLogoNames = [];
foreach ($existingLogos as $existingLogo) {
$existingLogoNames[] = $existingLogo->getName();
}
foreach ($logos as $logo) {
if (!in_array($logo, $existingLogoNames)) {
$binary = $this->getPdfTemplateLogoBinary($logo);
$this->savePdfLogo($confName, $logo, $binary);
}
}
}
}
/**
* Logo for PDF structures.
*
* @package LAM\PDF
*/
class PdfLogo {
private $name;
private $height;
private $width;
/**
* Constructor
*
* @param string $name file name
* @param int $height height
* @param int $width width
*/
public function __construct(string $name, int $height = 0, int $width = 0) {
$this->name = $name;
$this->height = $height;
$this->width = $width;
}
/**
* Returns the file name.
*
* @return string file name
*/
public function getName(): string {
return $this->name;
}
/**
* Returns the height.
*
* @return int height
*/
public function getHeight(): int {
return $this->height;
}
/**
* Returns the width.
*
* @return int width
*/
public function getWidth(): int {
return $this->width;
}
}
/**
* Interface for PDF structure persistence.
*
* @package LAM\PDF
*/
interface PdfStructurePersistenceStrategy {
/**
* Returns the names of existing PDF structure templates.
*
* @return array scope => names (e.g. array('user' => array('default')))
* @throws LAMException error reading templates
*/
public function getPdfStructureTemplateNames(): array;
/**
* Deletes a PDF structure template,
*
* @param string $scope user/group/host
* @param string $name template name
* @throws LAMException error deleting template
*/
public function deletePdfStructureTemplate(string $scope, string $name): void;
/**
* Reads a PDF structure template.
*
* @param string $scope user/group/host
* @param string $name structure name
* @return PDFStructure PDF structure
* @throws LAMException error deleting structure
*/
public function readPdfStructureTemplate(string $scope, string $name): PDFStructure;
/**
* Saves the PDF structure.
*
* @param string $scope user/group/host
* @param string $name structure name
* @param PDFStructure $structure structure
* @throws LAMException error saving structure
*/
public function savePdfStructureTemplate(string $scope, string $name, PDFStructure $structure): void;
/**
* Returns a list of template logo file names.
*
* @return string[] logo file names
*/
public function getPdfTemplateLogoNames(): array;
/**
* Returns the binary data for the given template logo.
*
* @param string $name file name
* @return string binary
* @throws LAMException error reading file
*/
public function getPdfTemplateLogoBinary(string $name): string;
/**
* Deletes a logo in global templates.
*
* @param string $name logo name
* @throws LAMException error during deletion
*/
public function deletePdfTemplateLogo(string $name): void;
/**
* Saves the template logo.
*
* @param string $name file name
* @param string $data binary data
* @throws LAMException error during save
*/
public function savePdfTemplateLogo(string $name, string $data): void;
/**
* Returns the list of available PDF logos.
*
* @param string $confName server profile name
* @return string[] logos
* @throws LAMException error reading logos
*/
public function getPdfLogoNames(string $confName): array;
/**
* Reads a PDF logo.
*
* @param string $confName server profile name
* @param string $name file name
* @return string binary data
* @throws LAMException error reading logo
*/
public function getPdfLogoBinary(string $confName, string $name): string;
/**
* Deletes a PDF logo.
*
* @param string $confName server profile name
* @param string $name logo name
* @throws LAMException error deleting logo
*/
public function deletePdfLogo(string $confName, string $name): void;
/**
* Saves a PDF logo.
*
* @param string $confName server profile name
* @param string $name logo name
* @param string $data binary
* @throws LAMException error saving logo
*/
public function savePdfLogo(string $confName, string $name, string $data): void;
/**
* Returns all available PDF structure definitions for the submitted account type.
*
* @param string $confName server profile name
* @param string $typeId the account type
*
* @return string[] structure names
* @throws LAMException error reading structures
*/
public function getPDFStructures(string $confName, string $typeId): array;
/**
* Deletes a PDF structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @throws LAMException error deleting structure
*/
public function deletePdfStructure(string $confName, string $typeId, string $name): void;
/**
* Reads a PDF structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @return PDFStructure PDF structure
* @throws LAMException error deleting structure
*/
public function readPdfStructure(string $confName, string $typeId, string $name): PDFStructure;
/**
* Saves the PDF structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @param PDFStructure $structure structure
* @throws LAMException error saving structure
*/
public function savePdfStructure(string $confName, string $typeId, string $name, PDFStructure $structure): void;
}
/**
* Manages PDF structures on file system.
*
* @package LAM\PDF
*/
class PdfStructurePersistenceStrategyFiles implements PdfStructurePersistenceStrategy {
/**
* @inheritDoc
*/
public function getPdfStructureTemplateNames(): array {
$templatePath = __DIR__ . '/../config/templates/pdf';
$templateDir = @dir($templatePath);
$allTemplates = [];
if ($templateDir) {
$entry = $templateDir->read();
while ($entry) {
$parts = explode('.', $entry);
if ((strlen($entry) > 3) && (count($parts) == 3)) {
$name = $parts[0];
$scope = $parts[1];
$allTemplates[$scope][] = $name;
}
$entry = $templateDir->read();
}
}
return $allTemplates;
}
/**
* @inheritDoc
*/
public function deletePdfStructureTemplate(string $scope, string $name): void {
$fileName = $this->getPdfStructureTemplateFileName($scope, $name);
$deleted = @unlink($fileName);
if (!$deleted) {
throw new LAMException(_("Unable to delete PDF structure!"));
}
}
/**
* @inheritDoc
*/
public function readPdfStructureTemplate(string $scope, string $name): PDFStructure {
$fileName = $this->getPdfStructureTemplateFileName($scope, $name);
if (!is_file($fileName) || !is_readable($fileName)) {
throw new LAMException(_('Unable to read PDF structure.'));
}
$handle = fopen($fileName, 'r');
$xmlData = fread($handle, 100000000);
fclose($handle);
$reader = new PDFStructureReader();
return $reader->read($xmlData);
}
/**
* @inheritDoc
*/
public function savePdfStructureTemplate(string $scope, string $name, PDFStructure $structure): void {
$fileName = $this->getPdfStructureTemplateFileName($scope, $name);
$writer = new PDFStructureWriter();
$xml = $writer->getXML($structure);
$file = @fopen($fileName, "w");
if (!$file) {
logNewMessage(LOG_ERR, 'Unable to write ' . $fileName);
throw new LAMException(_('Unable to save PDF structure.'));
}
fwrite($file, $xml);
fclose($file);
}
/**
* Returns the file name for a PDF structure.
*
* @param string $scope user/group/host
* @param string $name structure name
* @return string file name
*/
private function getPdfStructureTemplateFileName(string $scope, string $name): string {
return __DIR__ . '/../config/templates/pdf/' . $name . '.' . $scope . '.xml';
}
/**
* @inheritDoc
*/
public function getPdfTemplateLogoNames(): array {
$templatePath = __DIR__ . '/../config/templates/pdf/logos';
$templateDir = @dir($templatePath);
$logos = [];
if ($templateDir) {
$entry = $templateDir->read();
while ($entry) {
if ((!str_starts_with($entry, '.')) && is_file($templatePath . '/' . $entry)) {
$logos[] = $entry;
}
$entry = $templateDir->read();
}
}
return $logos;
}
/**
* @inheritDoc
*/
public function getPdfTemplateLogoBinary(string $name): string {
$fileName = $this->getPdfTemplateLogoFileName($name);
$handle = fopen($fileName, 'r');
$logoBinary = fread($handle, 100000000);
fclose($handle);
return $logoBinary;
}
/**
* Returns the file name of a given logo.
*
* @param string $name logo name
* @return string file name
*/
private function getPdfTemplateLogoFileName(string $name): string {
return __DIR__ . '/../config/templates/pdf/logos/' . $name;
}
/**
* @inheritDoc
*/
public function deletePdfTemplateLogo(string $name): void {
$fileName = $this->getPdfTemplateLogoFileName($name);
$deleted = @unlink($fileName);
if (!$deleted) {
throw new LAMException(_('Unable to delete logo file.'));
}
}
/**
* @inheritDoc
*/
public function savePdfTemplateLogo(string $name, string $data): void {
$fileName = $this->getPdfTemplateLogoFileName($name);
$file = @fopen($fileName, "w");
if (!$file) {
logNewMessage(LOG_ERR, 'Unable to write ' . $fileName);
throw new LAMException(_('Unable to upload logo file.'));
}
fwrite($file, $data);
fclose($file);
}
/**
* Returns the file name of a given logo.
*
* @param string $confName server profile name
* @param string $name logo name
* @return string file name
*/
private function getPdfLogoFileName(string $confName, string $name): string {
return __DIR__ . '/../config/pdf/' . $confName . '/logos/' . $name;
}
/**
* @inheritDoc
*/
public function getPdfLogoNames(string $confName): array {
$return = [];
$dirPath = __DIR__ . '/../config/pdf/' . $confName . '/logos/';
if (!is_dir($dirPath)) {
mkdir($dirPath, 0700, true);
}
$dirHandle = opendir($dirPath);
if ($dirHandle === false) {
throw new LAMException(_('Unable to read logos.'));
}
while ($file = readdir($dirHandle)) {
if (!is_dir($file) && $file !== '.' && $file !== '..' && preg_match('/\\.(jpg|png)$/i', $file)) {
$return[] = $file;
}
}
return $return;
}
/**
* @inheritDoc
*/
public function getPdfLogoBinary(string $confName, string $name): string {
$fileName = $this->getPdfLogoFileName($confName, $name);
$handle = fopen($fileName, 'r');
$logoBinary = fread($handle, 100000000);
fclose($handle);
return $logoBinary;
}
/**
* @inheritDoc
*/
public function deletePdfLogo(string $confName, string $name): void {
// delete file
$success = @unlink($this->getPdfLogoFileName($confName, $name));
if (!$success) {
throw new LAMException(_('Unable to delete logo file.'), $name);
}
}
/**
* @inheritDoc
*/
public function savePdfLogo(string $confName, string $name, string $data): void {
$fileName = $this->getPdfLogoFileName($confName, $name);
$basePath = dirname($fileName);
if (!file_exists($basePath)) {
mkdir($basePath, 0700, true);
}
$file = @fopen($fileName, "w");
if (!$file) {
logNewMessage(LOG_ERR, 'Unable to write ' . $fileName);
throw new LAMException(_('Unable to upload logo file.'));
}
fwrite($file, $data);
fclose($file);
}
/**
* @inheritDoc
*/
public function getPDFStructures(string $confName, string $typeId): array {
$return = [];
$path = __DIR__ . '/../config/pdf/' . $confName;
if (is_dir($path) && is_readable($path)) {
$dirHandle = opendir($path);
while ($file = readdir($dirHandle)) {
$struct_file = explode('.', $file);
if (!is_dir($path . $file)
&& ($file !== '.')
&& ($file !== '..')
&& (count($struct_file) === 3)
&& ($struct_file[1] === $typeId)
&& ($struct_file[2] === 'xml')) {
$return[] = $struct_file[0];
}
}
}
return $return;
}
/**
* @inheritDoc
*/
public function deletePdfStructure(string $confName, string $typeId, string $name): void {
$fileName = $this->getPdfStructureFileName($confName, $typeId, $name);
if (!is_file($fileName) || !is_writable($fileName)) {
logNewMessage(LOG_ERR, 'PDF structure does not exist or is not writable: ' . $fileName);
throw new LAMException(_('Unable to delete PDF structure!'));
}
$deleteOk = @unlink($fileName);
if (!$deleteOk) {
logNewMessage(LOG_ERR, 'PDF structure delete failed: ' . $fileName);
throw new LAMException(_('Unable to delete PDF structure!'));
}
}
/**
* @inheritDoc
*/
public function readPdfStructure(string $confName, string $typeId, string $name): PDFStructure {
$fileName = $this->getPdfStructureFileName($confName, $typeId, $name);
if (!is_file($fileName) || !is_readable($fileName)) {
throw new LAMException(_('Unable to read PDF structure.'));
}
$handle = fopen($fileName, 'r');
$xmlData = fread($handle, 100000000);
fclose($handle);
$reader = new PDFStructureReader();
return $reader->read($xmlData);
}
/**
* @inheritDoc
*/
public function savePdfStructure(string $confName, string $typeId, string $name, PDFStructure $structure): void {
$fileName = $this->getPdfStructureFileName($confName, $typeId, $name);
$basePath = dirname($fileName);
if (!file_exists($basePath)) {
mkdir($basePath, 0700, true);
}
$writer = new PDFStructureWriter();
$xml = $writer->getXML($structure);
$file = @fopen($fileName, "w");
if (!$file) {
logNewMessage(LOG_ERR, 'Unable to write ' . $fileName);
throw new LAMException(_('Unable to save PDF structure.'));
}
fwrite($file, $xml);
fclose($file);
}
/**
* Returns the file name of the structure.
*
* @param string $confName server profile name
* @param string $typeId user/group/host
* @param string $name structure name
* @return string file name
*/
private function getPdfStructureFileName(string $confName, string $typeId, string $name): string {
return __DIR__ . '/../config/pdf/' . $confName . '/' . $name . '.' . $typeId . '.xml';
}
}
/**
* Manages PDF structures on file system.
*
* @package LAM\PDF
*/
class PdfStructurePersistenceStrategyPdo implements PdfStructurePersistenceStrategy {
private const TABLE_NAME = 'pdf_structures';
private const TABLE_NAME_LOGOS = 'pdf_logos';
private const TABLE_NAME_TEMPLATES = 'pdf_structures_templates';
private const TABLE_NAME_TEMPLATES_LOGOS = 'pdf_logos_templates';
/**
* @var PDO
*/
private $pdo;
/**
* Constructor
*
* @param PDO $pdo PDO
*/
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->checkSchema();
}
/**
* Checks if the schema has latest version.
*/
private function checkSchema(): void {
if (!dbTableExists($this->pdo, self::TABLE_NAME)) {
$this->createInitialSchema();
}
}
/**
* Creates the initial schema.
*/
public function createInitialSchema(): void {
logNewMessage(LOG_DEBUG, 'Creating database table ' . self::TABLE_NAME);
$sql = 'create table ' . self::TABLE_NAME . '('
. 'position int NOT NULL,'
. 'confname VARCHAR(300) NOT NULL,'
. 'typeid VARCHAR(300) NOT NULL,'
. 'name VARCHAR(300) NOT NULL,'
. 'data TEXT NOT NULL,'
. 'PRIMARY KEY(position)'
. ');';
$this->pdo->exec($sql);
logNewMessage(LOG_DEBUG, 'Creating database table ' . self::TABLE_NAME_TEMPLATES);
$sql = 'create table ' . self::TABLE_NAME_TEMPLATES . '('
. 'scope VARCHAR(100) NOT NULL,'
. 'name VARCHAR(300) NOT NULL,'
. 'data TEXT NOT NULL,'
. 'PRIMARY KEY(scope,name)'
. ');';
$this->pdo->exec($sql);
logNewMessage(LOG_DEBUG, 'Creating database table ' . self::TABLE_NAME_LOGOS);
$sql = 'create table ' . self::TABLE_NAME_LOGOS . '('
. 'confname VARCHAR(300) NOT NULL,'
. 'name VARCHAR(300) NOT NULL,'
. 'data LONGBLOB NOT NULL,'
. 'PRIMARY KEY(confname,name)'
. ');';
$this->pdo->exec($sql);
logNewMessage(LOG_DEBUG, 'Creating database table ' . self::TABLE_NAME_TEMPLATES_LOGOS);
$sql = 'create table ' . self::TABLE_NAME_TEMPLATES_LOGOS . '('
. 'name VARCHAR(300) NOT NULL,'
. 'data LONGBLOB NOT NULL,'
. 'PRIMARY KEY(name)'
. ');';
$this->pdo->exec($sql);
$sql = 'insert into ' . ConfigurationDatabase::TABLE_SCHEMA_VERSIONS . ' (name, version) VALUES ("pdf_structures", 1);';
$this->pdo->exec($sql);
}
/**
* @inheritDoc
*/
public function getPdfStructureTemplateNames(): array {
$statement = $this->pdo->prepare("SELECT scope, name FROM " . self::TABLE_NAME_TEMPLATES);
$statement->execute();
$results = $statement->fetchAll();
$profiles = [];
foreach ($results as $result) {
$profiles[$result['scope']][] = $result['name'];
}
return $profiles;
}
/**
* @inheritDoc
*/
public function deletePdfStructureTemplate(string $scope, string $name): void {
$statement = $this->pdo->prepare("DELETE FROM " . self::TABLE_NAME_TEMPLATES . " WHERE scope = ? AND name = ?");
$statement->execute([$scope, $name]);
}
/**
* @inheritDoc
*/
public function readPdfStructureTemplate(string $scope, string $name): PDFStructure {
$statement = $this->pdo->prepare("SELECT data FROM " . self::TABLE_NAME_TEMPLATES . ' WHERE scope = ? AND name = ?');
$statement->execute([$scope, $name]);
$results = $statement->fetchAll();
if (empty($results)) {
throw new LAMException(_('Unable to read PDF structure.'));
}
$structure = new PDFStructure();
$structure->import(json_decode($results[0]['data'], true));
return $structure;
}
/**
* @inheritDoc
*/
public function savePdfStructureTemplate(string $scope, string $name, PDFStructure $structure): void {
$json = json_encode($structure->export());
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME_TEMPLATES . " WHERE scope = ? AND name = ?");
$statement->execute([$scope, $name]);
$results = $statement->fetchAll();
$isExisting = !empty($results);
if ($isExisting) {
$statement = $this->pdo->prepare("UPDATE " . self::TABLE_NAME_TEMPLATES . " SET data = ? WHERE scope = ? AND name = ?");
$statement->execute([$json, $scope, $name]);
}
else {
$statement = $this->pdo->prepare("INSERT INTO " . self::TABLE_NAME_TEMPLATES . " (scope, name, data) VALUES (?, ?, ?)");
$statement->execute([$scope, $name, $json]);
}
}
/**
* @inheritDoc
*/
public function getPdfTemplateLogoNames(): array {
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME_TEMPLATES_LOGOS);
$statement->execute();
$results = $statement->fetchAll();
$logos = [];
foreach ($results as $result) {
$logos[] = $result['name'];
}
return $logos;
}
/**
* @inheritDoc
*/
public function getPdfTemplateLogoBinary(string $name): string {
$statement = $this->pdo->prepare("SELECT data FROM " . self::TABLE_NAME_TEMPLATES_LOGOS . ' WHERE name = ?');
$statement->execute([$name]);
$results = $statement->fetchAll();
if (empty($results)) {
throw new LAMException(_('Unable to read logo file.'));
}
return $results[0]['data'];
}
/**
* @inheritDoc
*/
public function deletePdfTemplateLogo(string $name): void {
$statement = $this->pdo->prepare("DELETE FROM " . self::TABLE_NAME_TEMPLATES_LOGOS . " WHERE name = ?");
$statement->execute([$name]);
}
/**
* @inheritDoc
*/
public function savePdfTemplateLogo(string $name, string $data): void {
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME_TEMPLATES_LOGOS . " WHERE name = ?");
$statement->execute([$name]);
$results = $statement->fetchAll();
$isExisting = !empty($results);
if ($isExisting) {
$statement = $this->pdo->prepare("UPDATE " . self::TABLE_NAME_TEMPLATES_LOGOS . " SET data = ? WHERE name = ?");
$statement->execute([$data, $name]);
}
else {
$statement = $this->pdo->prepare("INSERT INTO " . self::TABLE_NAME_TEMPLATES_LOGOS . " (name, data) VALUES (?, ?)");
$statement->execute([$name, $data]);
}
}
/**
* @inheritDoc
*/
public function getPdfLogoNames(string $confName): array {
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME_LOGOS . ' WHERE confname = ?');
$statement->execute([$confName]);
$results = $statement->fetchAll();
$logos = [];
foreach ($results as $result) {
$logos[] = $result['name'];
}
return $logos;
}
/**
* @inheritDoc
*/
public function getPdfLogoBinary(string $confName, string $name): string {
$statement = $this->pdo->prepare("SELECT data FROM " . self::TABLE_NAME_LOGOS . ' WHERE confname = ? AND name = ?');
$statement->execute([$confName, $name]);
$results = $statement->fetchAll();
if (empty($results)) {
throw new LAMException(_('Unable to read logo file.'));
}
return $results[0]['data'];
}
/**
* @inheritDoc
*/
public function deletePdfLogo(string $confName, string $name): void {
$statement = $this->pdo->prepare("DELETE FROM " . self::TABLE_NAME_LOGOS . " WHERE confname = ? AND name = ?");
$statement->execute([$confName, $name]);
}
/**
* @inheritDoc
*/
public function savePdfLogo(string $confName, string $name, string $data): void {
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME_LOGOS . " WHERE confname = ? AND name = ?");
$statement->execute([$confName, $name]);
$results = $statement->fetchAll();
$isExisting = !empty($results);
if ($isExisting) {
$statement = $this->pdo->prepare("UPDATE " . self::TABLE_NAME_LOGOS . " SET data = ? WHERE confname = ? AND name = ?");
$statement->execute([$data, $confName, $name]);
}
else {
$statement = $this->pdo->prepare("INSERT INTO " . self::TABLE_NAME_LOGOS . " (confname, name, data) VALUES (?, ?, ?)");
$statement->execute([$confName, $name, $data]);
}
}
/**
* @inheritDoc
*/
public function getPDFStructures(string $confName, string $typeId): array {
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME . ' WHERE confname = ? AND typeid = ?');
$statement->execute([$confName, $typeId]);
$results = $statement->fetchAll();
$names = [];
foreach ($results as $result) {
$names[] = $result['name'];
}
return $names;
}
/**
* @inheritDoc
*/
public function deletePdfStructure(string $confName, string $typeId, string $name): void {
$statement = $this->pdo->prepare("DELETE FROM " . self::TABLE_NAME . " WHERE confname = ? AND typeid = ? AND name = ?");
$statement->execute([$confName, $typeId, $name]);
}
/**
* @inheritDoc
*/
public function readPdfStructure(string $confName, string $typeId, string $name): PDFStructure {
$statement = $this->pdo->prepare("SELECT data FROM " . self::TABLE_NAME . ' WHERE confname = ? AND typeid = ? AND name = ?');
$statement->execute([$confName, $typeId, $name]);
$results = $statement->fetchAll();
if (empty($results)) {
throw new LAMException(_('Unable to read PDF structure.'));
}
$structure = new PDFStructure();
$structure->import(json_decode($results[0]['data'], true));
return $structure;
}
/**
* @inheritDoc
*/
public function savePdfStructure(string $confName, string $typeId, string $name, PDFStructure $structure): void {
$json = json_encode($structure->export());
$statement = $this->pdo->prepare("SELECT name FROM " . self::TABLE_NAME . " WHERE confname = ? AND typeid = ? AND name = ?");
$statement->execute([$confName, $typeId, $name]);
$results = $statement->fetchAll();
$isExisting = !empty($results);
if ($isExisting) {
$statement = $this->pdo->prepare("UPDATE " . self::TABLE_NAME . " SET data = ? WHERE confname = ? AND typeid = ? AND name = ?");
$statement->execute([$json, $confName, $typeId, $name]);
}
else {
$positionStatement = $this->pdo->prepare("SELECT MAX(position) AS position FROM " . self::TABLE_NAME);
$positionStatement->execute();
$positionResult = $positionStatement->fetchAll();
$position = $positionResult[0]['position'] + 1;
$statement = $this->pdo->prepare("INSERT INTO " . self::TABLE_NAME . " (position, confname, typeid, name, data) VALUES (?, ?, ?, ?, ?)");
$statement->execute([$position, $confName, $typeId, $name, $json]);
}
}
}
/**
* Reads a PDF structure.
*
* @author Roland Gruber
*/
class PDFStructureReader {
/**
* Reads a PDF structure file.
*
* @param string $data XML data
* @return PDFStructure structure
* @throws LAMException error reading structure
*/
public function read(string $data): PDFStructure {
$xml = new XMLReader();
$xml->XML($data);
$structure = new PDFStructure();
// open <pdf>
@$xml->read();
if (!$xml->name == 'pdf') {
logNewMessage(LOG_ERR, 'Unknown tag name: ' . $xml->name);
throw new LAMException(_('Unable to read PDF structure.'));
}
$structure->setLogo($xml->getAttribute('filename'));
$structure->setTitle($xml->getAttribute('headline'));
$structure->setFoldingMarks($xml->getAttribute('foldingmarks'));
$sections = [];
while ($xml->read()) {
if (($xml->nodeType === XMLReader::SIGNIFICANT_WHITESPACE)
|| (($xml->name === 'pdf') && ($xml->nodeType == XMLReader::END_ELEMENT))) {
continue;
}
elseif ($xml->name === 'text') {
$xml->read();
$sections[] = new PDFTextSection($xml->value);
$xml->read();
if ($xml->name !== 'text') {
logNewMessage(LOG_ERR, 'Unexpected tag name: ' . $xml->name);
throw new LAMException(_('Unable to read PDF structure.'));
}
}
elseif ($xml->name === 'section') {
$sections[] = $this->readSection($xml);
}
else {
logNewMessage(LOG_ERR, 'Unexpected tag name: ' . $xml->name);
throw new LAMException(_('Unable to read PDF structure.'));
}
}
$xml->close();
$structure->setSections($sections);
return $structure;
}
/**
* Reads a single section from XML.
*
* @param XMLReader $xml reader
* @return PDFEntrySection section
* @throws LAMException unable to parse section
*/
private function readSection(XMLReader $xml): PDFEntrySection {
$section = new PDFEntrySection($xml->getAttribute('name'));
$entries = [];
while ($xml->read()) {
if (($xml->name === 'section') && ($xml->nodeType == XMLReader::END_ELEMENT)) {
break;
}
elseif (($xml->nodeType === XMLReader::END_ELEMENT)
|| ($xml->nodeType === XMLReader::SIGNIFICANT_WHITESPACE)) {
continue;
}
elseif ($xml->name === 'entry') {
$entries[] = new PDFSectionEntry($xml->getAttribute('name'));
}
else {
logNewMessage(LOG_ERR, 'Unexpected tag name: ' . $xml->name);
throw new LAMException(_('Unable to read PDF structure.'));
}
}
$section->setEntries($entries);
return $section;
}
}
/**
* Writes PDF structures to files.
*
* @author Roland Gruber
*/
class PDFStructureWriter {
/**
* Returns the generated XML.
*
* @param PDFStructure $structure structure
* @return string XML
*/
public function getXML(PDFStructure $structure): string {
$writer = new XMLWriter();
$writer->openMemory();
$writer->setIndent(true);
$writer->setIndentString("\t");
$writer->startElement('pdf');
if ($structure->getLogo() !== null) {
$writer->writeAttribute('filename', $structure->getLogo());
}
$writer->writeAttribute('headline', $structure->getTitle());
if ($structure->getFoldingMarks() !== null) {
$writer->writeAttribute('foldingmarks', $structure->getFoldingMarks());
}
foreach ($structure->getSections() as $section) {
if ($section instanceof PDFTextSection) {
$writer->startElement('text');
$writer->text($section->getText());
}
else {
$writer->startElement('section');
if ($section->isAttributeTitle()) {
$writer->writeAttribute('name', '_' . $section->getPdfKey());
}
else {
$writer->writeAttribute('name', $section->getTitle());
}
foreach ($section->getEntries() as $entry) {
$writer->startElement('entry');
$writer->writeAttribute('name', $entry->getKey());
$writer->endElement();
}
}
$writer->endElement();
}
$writer->endElement();
return $writer->outputMemory();
}
}
/**
* PDF structure
*
* @author Roland Gruber
*/
class PDFStructure {
/** no folding marks */
public const FOLDING_NONE = 'no';
/** standard folding marks */
public const FOLDING_STANDARD = 'standard';
private $logo;
private $title = 'LDAP Account Manager';
private $foldingMarks = 'no';
private $sections = [];
/**
* Returns an array representation of the structure.
*
* @return array export data
*/
public function export() {
$data = [];
$data['title'] = $this->title;
$data['foldingMarks'] = $this->foldingMarks;
$data['logo'] = $this->logo;
$data['sections'] = [];
foreach ($this->sections as $section) {
$type = ($section instanceof PDFTextSection) ? 'text' : 'entry';
$sectionData = $section->export();
$data['sections'][] = [
'type' => $type,
'data' => $sectionData
];
}
return $data;
}
/**
* Imports an array representation of the structure.
*
* @param array $data import data
*/
public function import($data) {
if (isset($data['title'])) {
$this->title = $data['title'];
}
if (isset($data['foldingMarks'])) {
$this->foldingMarks = $data['foldingMarks'];
}
if (isset($data['logo'])) {
$this->logo = $data['logo'];
}
if (isset($data['sections'])) {
foreach ($data['sections'] as $section) {
if ($section['type'] === 'text') {
$this->sections[] = new PDFTextSection($section['data']);
}
else {
$entrySection = new PDFEntrySection('');
$entrySection->import($section['data']);
$this->sections[] = $entrySection;
}
}
}
}
/**
* Returns the logo file path.
*
* @return ?string logo
*/
public function getLogo(): ?string {
return $this->logo;
}
/**
* Sets the logo file path.
*
* @param string $logo logo
*/
public function setLogo($logo) {
$this->logo = $logo;
}
/**
* Returns the title.
*
* @return string title
*/
public function getTitle() {
return $this->title;
}
/**
* Sets the title.
*
* @param string $title title
*/
public function setTitle($title) {
$this->title = $title;
}
/**
* Returns if to print folding marks.
*
* @return ?string print folding marks
*/
public function getFoldingMarks(): ?string {
return $this->foldingMarks;
}
/**
* Sets if to print folding marks.
*
* @param string $foldingMarks print folding marks
*/
public function setFoldingMarks($foldingMarks) {
$this->foldingMarks = $foldingMarks;
}
/**
* Returns the sections.
*
* @return PDFTextSection[]|PDFEntrySection[] $sections
*/
public function getSections() {
return $this->sections;
}
/**
* Sets the sections.
*
* @param PDFTextSection[]|PDFEntrySection[] $sections sections
*/
public function setSections($sections) {
$this->sections = $sections;
}
}
/**
* Section for static text.
*
* @author Roland Gruber
*/
class PDFTextSection {
private $text = '';
/**
* Constructor.
*
* @param string $text text
*/
public function __construct($text) {
$this->text = $text;
}
/**
* Exports the section.
*
* @return string text
*/
public function export() {
return $this->getText();
}
/**
* Returns the text.
*
* @return string text
*/
public function getText() {
return $this->text;
}
}
/**
* PDF section that contains LDAP data entries.
*
* @author Roland Gruber
*/
class PDFEntrySection {
private $title;
private $entries = [];
/**
* Constructor
*
* @param string $title title
*/
public function __construct($title) {
$this->title = $title;
}
/**
* Exports the section.
*
* @return array export data
*/
public function export() {
$data = [];
$data['title'] = $this->title;
$data['entries'] = [];
foreach ($this->getEntries() as $entry) {
$data['entries'][] = $entry->getKey();
}
return $data;
}
/**
* Imports the section.
*
* @param array $data import data
*/
public function import($data) {
if (isset($data['title'])) {
$this->title = $data['title'];
}
if ($data['entries']) {
foreach ($data['entries'] as $entry) {
$this->entries[] = new PDFSectionEntry($entry);
}
}
}
/**
* Returns if the title is an attribute value.
*
* @return boolean is attribute
*/
public function isAttributeTitle() {
return (bool) preg_match('/^_([a-zA-Z0-9_-])+$/', $this->title);
}
/**
* Returns the PDF key name.
*
* @return string PDF key name
*/
public function getPdfKey() {
return substr($this->title, 1);
}
/**
* Returns the text title.
*
* @return string title
*/
public function getTitle() {
return $this->title;
}
/**
* Sets the title text.
*
* @param string $title title
*/
public function setTitle($title) {
$this->title = $title;
}
/**
* Returns the entries.
*
* @return PDFSectionEntry[] entries
*/
public function getEntries() {
return $this->entries;
}
/**
* Sets the entries.
*
* @param PDFSectionEntry[] $entries entries
*/
public function setEntries($entries) {
$this->entries = $entries;
}
}
/**
* Single PDF entry.
*
* @author Roland Gruber
*/
class PDFSectionEntry {
private $key;
/**
* Constructor
*
* @param string $key key
*/
public function __construct($key) {
$this->key = $key;
}
/**
* Returns the PDF key.
*
* @return string $key key
*/
public function getKey() {
return $this->key;
}
}
/**
* Returns a list of possible fonts.
*
* @return array list of fonts (description => font name)
*/
function getPdfFonts() {
return [
'DejaVu' => 'DejaVuSerif',
_('Chinese Traditional') => 'cid0ct',
_('Chinese Simplified') => 'cid0cs',
_('Japanese') => 'cid0jp',
_('Korean') => 'cid0kr',
];
}