Initial commit
This commit is contained in:
52
application/Espo/Tools/ExportCustom/Data.php
Normal file
52
application/Espo/Tools/ExportCustom/Data.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\ExportCustom;
|
||||
|
||||
class Data
|
||||
{
|
||||
/**
|
||||
* @param string[] $customEntityTypeList
|
||||
*/
|
||||
public function __construct(
|
||||
public string $folder,
|
||||
public array $customEntityTypeList,
|
||||
private string $module
|
||||
) {}
|
||||
|
||||
public function getDir(): string
|
||||
{
|
||||
return 'data/tmp/' . $this->folder;
|
||||
}
|
||||
|
||||
public function getDestDir(): string
|
||||
{
|
||||
return $this->getDir() . '/files/custom/Espo/Modules/' . $this->module;
|
||||
}
|
||||
}
|
||||
380
application/Espo/Tools/ExportCustom/ExportCustom.php
Normal file
380
application/Espo/Tools/ExportCustom/ExportCustom.php
Normal file
@@ -0,0 +1,380 @@
|
||||
<?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\ExportCustom;
|
||||
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
|
||||
use Espo\Core\FileStorage\Manager as FileStorageManager;
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\EntityManager\NameUtil;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
class ExportCustom
|
||||
{
|
||||
private string $minVersion = '8.0.0';
|
||||
|
||||
/** @var string[] */
|
||||
private array $metadataFolderList = [
|
||||
'scopes',
|
||||
'entityDefs',
|
||||
'clientDefs',
|
||||
'recordDefs',
|
||||
'selectDefs',
|
||||
'aclDefs',
|
||||
'entityAcl',
|
||||
'formula',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private FileManager $fileManager,
|
||||
private NameUtil $nameUtil,
|
||||
private FileStorageManager $fileStorageManager,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function process(Params $params): Result
|
||||
{
|
||||
$this->validate($params);
|
||||
|
||||
$data = $this->createData($params);
|
||||
|
||||
$this->createDir($data);
|
||||
$this->copy($data);
|
||||
$this->fixMetadata($params, $data);
|
||||
$this->filterLayouts($data);
|
||||
$this->createControllers($params, $data);
|
||||
$this->createModuleJson($data);
|
||||
$this->createManifest($params, $data);
|
||||
$attachmentId = $this->createAttachment($data);
|
||||
$this->cleanup($data);
|
||||
|
||||
return new Result($attachmentId);
|
||||
}
|
||||
|
||||
private function cleanup(Data $data): void
|
||||
{
|
||||
$this->fileManager->removeInDir($data->getDir(), true);
|
||||
$this->fileManager->removeFile($data->getDir() . '.zip');
|
||||
}
|
||||
|
||||
private function createDir(Data $data): void
|
||||
{
|
||||
$this->fileManager->mkdir($data->getDestDir(), 0755);
|
||||
}
|
||||
|
||||
private function copy(Data $data): void
|
||||
{
|
||||
$customDir = 'custom/Espo/Custom';
|
||||
|
||||
$this->fileManager->copy(
|
||||
$customDir . '/Resources',
|
||||
$data->getDestDir() . '/Resources',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private function fixMetadata(Params $params, Data $data): void
|
||||
{
|
||||
foreach ($data->customEntityTypeList as $scope) {
|
||||
$file = $data->getDestDir() . '/Resources/metadata/scopes/' . $scope . '.json';
|
||||
|
||||
if (!$this->fileManager->exists($file)) {
|
||||
throw new RuntimeException("File $file does not exist.");
|
||||
}
|
||||
|
||||
$defs = Json::decode($this->fileManager->getContents($file));
|
||||
|
||||
unset($defs->isCustom);
|
||||
|
||||
$defs->module = $params->getModule();
|
||||
$defs->isCustomExported = true;
|
||||
|
||||
$this->fileManager->putJsonContents($file, $defs);
|
||||
}
|
||||
|
||||
$dir = $data->getDestDir() . '/Resources/metadata/entityDefs';
|
||||
|
||||
/** @var string[] $files */
|
||||
$files = $this->fileManager->getFileList($dir);
|
||||
|
||||
$files = array_filter($files, fn ($file) => str_ends_with($file, '.json'));
|
||||
|
||||
foreach ($files as $file) {
|
||||
$fullFile = $dir . '/' . $file;
|
||||
|
||||
$defs = Json::decode($this->fileManager->getContents($fullFile));
|
||||
|
||||
foreach (get_object_vars($defs->fields ?? (object) []) as $key => $value) {
|
||||
unset($defs->fields->$key->isCustom);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($defs->links ?? (object) []) as $key => $value) {
|
||||
unset($defs->links->$key->isCustom);
|
||||
}
|
||||
|
||||
$this->fileManager->putJsonContents($fullFile, $defs);
|
||||
}
|
||||
|
||||
$metadataDir = $data->getDestDir() . '/Resources/metadata';
|
||||
|
||||
/** @var string[] $list */
|
||||
$list = $this->fileManager->getFileList($metadataDir);
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (in_array($item, $this->metadataFolderList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file = $metadataDir . '/' . $item;
|
||||
|
||||
if ($this->fileManager->isFile($file)) {
|
||||
$this->fileManager->removeFile($file);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fileManager->removeInDir($file, true);
|
||||
}
|
||||
}
|
||||
|
||||
private function filterLayouts(Data $data): void
|
||||
{
|
||||
$dir = $data->getDestDir() . '/Resources/layouts';
|
||||
|
||||
/** @var string[] $list */
|
||||
$list = $this->fileManager->getFileList($dir);
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (in_array($item, $data->customEntityTypeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file = $dir . '/' . $item;
|
||||
|
||||
if ($this->fileManager->isFile($file)) {
|
||||
$this->fileManager->removeFile($file);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fileManager->removeInDir($file, true);
|
||||
}
|
||||
}
|
||||
|
||||
private function createControllers(Params $params, Data $data): void
|
||||
{
|
||||
foreach ($data->customEntityTypeList as $scope) {
|
||||
$this->createController($params, $data, $scope);
|
||||
}
|
||||
}
|
||||
|
||||
private function createModuleJson(Data $data): void
|
||||
{
|
||||
$file = $data->getDestDir() . '/Resources/module.json';
|
||||
|
||||
$defs = (object) ['order' => 30];
|
||||
|
||||
$this->fileManager->putJsonContents($file, $defs);
|
||||
}
|
||||
|
||||
private function createController(Params $params, Data $data, string $scope): void
|
||||
{
|
||||
$shortClassName = Util::normalizeClassName($scope);
|
||||
|
||||
$module = $params->getModule();
|
||||
|
||||
$contents = "<" . "?" . "php\n\n".
|
||||
"namespace Espo\\Modules\\$module\\Controllers;\n\n".
|
||||
"class $shortClassName extends \\Espo\\Core\\Templates\\Controllers\\Base\n".
|
||||
"{}\n";
|
||||
|
||||
$file = "{$data->getDestDir()}/Controllers/$shortClassName.php";
|
||||
|
||||
$this->fileManager->putContents($file, $contents);
|
||||
}
|
||||
|
||||
private function createManifest(Params $params, Data $data): void
|
||||
{
|
||||
$file = $data->getDir() . '/manifest.json';
|
||||
|
||||
$defs = (object) [
|
||||
'name' => $params->getName(),
|
||||
'version' => $params->getVersion(),
|
||||
'author' => $params->getAuthor(),
|
||||
'skipBackup' => true,
|
||||
'releaseDate' => DateTime::getSystemTodayString(),
|
||||
'description' => $params->getDescription(),
|
||||
'acceptableVersions' => ['>=' . $this->minVersion]
|
||||
];
|
||||
|
||||
$this->fileManager->putJsonContents($file, $defs);
|
||||
}
|
||||
|
||||
private function createAttachment(Data $data): string
|
||||
{
|
||||
$zipFile = $this->createZip($data);
|
||||
|
||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getNew();
|
||||
|
||||
$attachment
|
||||
->setName($data->folder . '.zip')
|
||||
->setRole(Attachment::ROLE_EXPORT_FILE)
|
||||
->setType('application/zip')
|
||||
->setSize($this->fileManager->getSize($zipFile));
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$resource = fopen($zipFile, 'r');
|
||||
|
||||
if ($resource === false) {
|
||||
throw new RuntimeException("Could not open file $zipFile.");
|
||||
}
|
||||
|
||||
$this->fileStorageManager->putStream($attachment, Utils::streamFor($resource));
|
||||
|
||||
return $attachment->getId();
|
||||
}
|
||||
|
||||
private function createZip(Data $data): string
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
|
||||
$archiveFile = $data->getDir() . '.zip';
|
||||
|
||||
$openResult = $zip->open($archiveFile, ZipArchive::CREATE);
|
||||
|
||||
if ($openResult !== true) {
|
||||
throw new RuntimeException("Could not open zip.");
|
||||
}
|
||||
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($data->getDir() . '/'),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($files as $file => $fileDescriptor) {
|
||||
$file = str_replace("\\", "/", $file);
|
||||
|
||||
if (!$this->fileManager->isFile($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = substr($file, strlen('data/tmp/' . $data->folder . '/'));
|
||||
|
||||
$zip->addFile(
|
||||
getcwd() . '/' . $file,
|
||||
$entry
|
||||
);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $archiveFile;
|
||||
}
|
||||
|
||||
private function createData(Params $params): Data
|
||||
{
|
||||
$folder = Util::camelCaseToHyphen($params->getModule()) . '-' . $params->getVersion();
|
||||
|
||||
return new Data(
|
||||
folder: $folder,
|
||||
customEntityTypeList: $this->obtainCustomEntityTypeList(),
|
||||
module: $params->getModule(),
|
||||
);
|
||||
}
|
||||
|
||||
private function validate(Params $params): void
|
||||
{
|
||||
if (!preg_match('/^[A-Za-z][A-Za-z0-9 ]+$/', $params->getName())) {
|
||||
throw new RuntimeException("Bad extension name.");
|
||||
}
|
||||
|
||||
if (!preg_match('/^[A-Z][a-z][A-Za-z]+$/', $params->getModule())) {
|
||||
throw new RuntimeException("Bad module name. Should be in CamelCase.");
|
||||
}
|
||||
|
||||
if (!preg_match('/^[0-9]+\.[0-9]+\.[0-9]+$/', $params->getVersion())) {
|
||||
throw new RuntimeException("Bad version number.");
|
||||
}
|
||||
|
||||
if ($this->nameUtil->nameIsNotAllowed($params->getModule())) {
|
||||
throw new RuntimeException("Not allowed module name.");
|
||||
}
|
||||
|
||||
$forbiddenModuleNames = [
|
||||
'Advanced',
|
||||
'AdvancedPack',
|
||||
'Sales',
|
||||
'SalesPack',
|
||||
'Google',
|
||||
'Outlook',
|
||||
'Voip',
|
||||
];
|
||||
|
||||
if (in_array($params->getModule(), $forbiddenModuleNames)) {
|
||||
throw new RuntimeException("Not allowed module name.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function obtainCustomEntityTypeList(): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var array<string, array<string, mixed>> $scopes */
|
||||
$scopes = $this->metadata->get('scopes', []);
|
||||
|
||||
foreach ($scopes as $scope => $item) {
|
||||
$isCustom = $item['isCustom'] ?? null;
|
||||
$isEntity = $item['entity'] ?? null;
|
||||
|
||||
if (!$isCustom || !$isEntity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $scope;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
66
application/Espo/Tools/ExportCustom/Params.php
Normal file
66
application/Espo/Tools/ExportCustom/Params.php
Normal 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\ExportCustom;
|
||||
|
||||
class Params
|
||||
{
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private string $module,
|
||||
private string $version,
|
||||
private string $author,
|
||||
private ?string $description
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getModule(): string
|
||||
{
|
||||
return $this->module;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function getAuthor(): string
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
}
|
||||
40
application/Espo/Tools/ExportCustom/Result.php
Normal file
40
application/Espo/Tools/ExportCustom/Result.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\ExportCustom;
|
||||
|
||||
class Result
|
||||
{
|
||||
public function __construct(private string $attachmentId) {}
|
||||
|
||||
public function getAttachmentId(): string
|
||||
{
|
||||
return $this->attachmentId;
|
||||
}
|
||||
}
|
||||
52
application/Espo/Tools/ExportCustom/Service.php
Normal file
52
application/Espo/Tools/ExportCustom/Service.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\ExportCustom;
|
||||
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
|
||||
class Service
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigWriter $configWriter
|
||||
) {}
|
||||
|
||||
public function storeToConfig(Params $params): void
|
||||
{
|
||||
$this->configWriter->set('customExportManifest', (object) [
|
||||
'name' => $params->getName(),
|
||||
'version' => $params->getVersion(),
|
||||
'description' => $params->getDescription(),
|
||||
'author' => $params->getAuthor(),
|
||||
'module' => $params->getModule(),
|
||||
]);
|
||||
|
||||
$this->configWriter->save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user