diff --git a/.gitignore b/.gitignore index b085703006..fd3fe3fbe6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /data/cache/* /data/upload/* /data/preferences/* +/data/.backup/* /data/config.php /custom /application/Espo/Resources/metadata/scopes/CustomTest.json diff --git a/application/Espo/Controllers/Admin.php b/application/Espo/Controllers/Admin.php index f9b82cd5fd..9aa225f8a1 100644 --- a/application/Espo/Controllers/Admin.php +++ b/application/Espo/Controllers/Admin.php @@ -72,7 +72,7 @@ class Admin extends \Espo\Core\Controllers\Base { $upgradeManager = new \Espo\Core\UpgradeManager($this->getContainer()); - $upgradeManager->run($data['id']); + $upgradeManager->install($data['id']); return true; } diff --git a/application/Espo/Controllers/Extension.php b/application/Espo/Controllers/Extension.php index 3c59d2499b..e943bd3012 100644 --- a/application/Espo/Controllers/Extension.php +++ b/application/Espo/Controllers/Extension.php @@ -33,13 +33,13 @@ class Extension extends \Espo\Core\Controllers\Record throw new Forbidden(); } } - + public function actionUpload($params, $data, $request) { if (!$request->isPost()) { - throw new Forbidden(); + throw new Forbidden(); } - + $manager = new \Espo\Core\ExtensionManager($this->getContainer()); $id = $manager->upload($data); @@ -56,22 +56,22 @@ class Extension extends \Espo\Core\Controllers\Record public function actionInstall($params, $data, $request) { if (!$request->isPost()) { - throw new Forbidden(); + throw new Forbidden(); } - + $manager = new \Espo\Core\ExtensionManager($this->getContainer()); - $manager->run($data['id']); + $manager->install($data['id']); return true; } - + public function actionUninstall($params, $data, $request) { if (!$request->isPost()) { - throw new Forbidden(); - } - + throw new Forbidden(); + } + $manager = new \Espo\Core\ExtensionManager($this->getContainer()); $manager->uninstall($data['id']); @@ -79,6 +79,19 @@ class Extension extends \Espo\Core\Controllers\Record return true; } + public function actionDeletePackage($params, $data, $request) + { + if (!$request->isPost()) { + throw new Forbidden(); + } + + $manager = new \Espo\Core\ExtensionManager($this->getContainer()); + + $manager->delete($data['id']); + + return true; + } + public function actionCreate() { throw new Forbidden(); @@ -98,30 +111,30 @@ class Extension extends \Espo\Core\Controllers\Record { throw new Forbidden(); } - + public function actionDelete() { throw new Forbidden(); } - + public function actionMassUpdate() { throw new Forbidden(); } - + public function actionMassDelete() { throw new Forbidden(); } - + public function actionCreateLink() { throw new Forbidden(); } - + public function actionRemoveLink() { throw new Forbidden(); - } + } } diff --git a/application/Espo/Core/ExtensionManager.php b/application/Espo/Core/ExtensionManager.php index 11dbebfe49..d9b848c0ed 100644 --- a/application/Espo/Core/ExtensionManager.php +++ b/application/Espo/Core/ExtensionManager.php @@ -22,15 +22,20 @@ namespace Espo\Core; -use Espo\Core\Exceptions\Error; - class ExtensionManager extends Upgrades\Base { - protected $packagePath = 'data/upload/extensions'; + protected $name = 'Extension'; - protected $scriptNames = array( - 'before' => 'BeforeUpgrade', - 'after' => 'AfterUpgrade', + protected $params = array( + 'packagePath' => 'data/upload/extensions', + + 'backupPath' => 'data/.backup/extensions', + + 'scriptNames' => array( + 'before' => 'BeforeInstall', + 'after' => 'AfterInstall', + 'beforeUninstall' => 'BeforeUninstall', + 'afterUninstall' => 'AfterUninstall', + ) ); - } diff --git a/application/Espo/Core/UpgradeManager.php b/application/Espo/Core/UpgradeManager.php index 3c2102c8e3..5d42afe3d7 100644 --- a/application/Espo/Core/UpgradeManager.php +++ b/application/Espo/Core/UpgradeManager.php @@ -26,11 +26,14 @@ use Espo\Core\Exceptions\Error; class UpgradeManager extends Upgrades\Base { - protected $packagePath = 'data/upload/upgrades'; + protected $name = 'Upgrade'; - protected $scriptNames = array( - 'before' => 'BeforeUpgrade', - 'after' => 'AfterUpgrade', + protected $params = array( + 'packagePath' => 'data/upload/upgrades', + + 'scriptNames' => array( + 'before' => 'BeforeUpgrade', + 'after' => 'AfterUpgrade', + ) ); - } \ No newline at end of file diff --git a/application/Espo/Core/Upgrades/ActionManager.php b/application/Espo/Core/Upgrades/ActionManager.php new file mode 100644 index 0000000000..babb676a8e --- /dev/null +++ b/application/Espo/Core/Upgrades/ActionManager.php @@ -0,0 +1,102 @@ +managerName = $managerName; + $this->container = $container; + + $params['name'] = $managerName; + $this->params = $params; + } + + protected function getManagerName() + { + return $this->managerName; + } + + protected function getContainer() + { + return $this->container; + } + + public function setAction($action) + { + $this->currentAction = $action; + } + + protected function getAction() + { + return $this->currentAction; + } + + protected function getParams() + { + return $this->params; + } + + public function run($data) + { + $object = $this->getObject(); + + return $object->run($data); + } + + public function getManifest() + { + return $this->getObject()->getManifest(); + } + + protected function getObject() + { + $managerName = $this->getManagerName(); + $actionName = $this->getAction(); + + if (!isset($this->objects[$managerName][$actionName])) { + $class = '\Espo\Core\Upgrades\Actions\\' . ucfirst($managerName) . '\\' . ucfirst($actionName); + + if (!class_exists($class)) { + throw new Error('Could not find class ['.$class.'].'); + } + + $this->objects[$managerName][$actionName] = new $class($this->container, $this->getParams()); + } + + return $this->objects[$managerName][$actionName]; + } +} \ No newline at end of file diff --git a/application/Espo/Core/Upgrades/Actions/Base.php b/application/Espo/Core/Upgrades/Actions/Base.php new file mode 100644 index 0000000000..ba106cb34a --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Base.php @@ -0,0 +1,445 @@ + 'upgrade', + 'extension' => 'extension', + ); + + /** + * Default package type + */ + protected $defaultPackageType = 'extension'; + + + public function __construct($container, $params) + { + $this->container = $container; + $this->params = $params; + + $this->zipUtil = new \Espo\Core\Utils\File\ZipArchive($container->get('fileManager')); + } + + public function __destruct() + { + $this->processId = null; + $this->data = null; + } + + protected function getContainer() + { + return $this->container; + } + + protected function getParams($name = null) + { + if (isset($this->params[$name])) { + return $this->params[$name]; + } + + return $this->params; + } + + protected function getZipUtil() + { + return $this->zipUtil; + } + + protected function getFileManager() + { + if (!isset($this->fileManager)) { + $this->fileManager = $this->getContainer()->get('fileManager'); + } + return $this->fileManager; + } + + protected function getConfig() + { + if (!isset($this->config)) { + $this->config = $this->getContainer()->get('config'); + } + return $this->config; + } + + protected function getEntityManager() + { + if (!isset($this->entityManager)) { + $this->entityManager = $this->getContainer()->get('entityManager'); + } + return $this->entityManager; + } + + protected function throwErrorWithDetelePackage($errorMessage = '') + { + $this->deletePackageFiles(); + $this->deletePackageArchive(); + throw new Error($errorMessage); + } + + abstract public function run($data); + + protected function createProcessId() + { + if (isset($this->processId)) { + throw new Error('Another installation process is currently running.'); + } + + $this->processId = uniqid(); + + return $this->processId; + } + + protected function getProcessId() + { + if (!isset($this->processId)) { + throw new Error('Installation ID was not specified.'); + } + + return $this->processId; + } + + protected function setProcessId($processId) + { + $this->processId = $processId; + } + + /** + * Check if version of upgrade/extension is acceptable to current version of EspoCRM + * + * @param string $version + * @return boolean + */ + protected function isAcceptable() + { + $res = $this->checkPackageType(); + $res &= $this->checkVersions(); + + return $res; + } + + protected function checkVersions() + { + $manifest = $this->getManifest(); + + /** check acceptable versions */ + $version = $manifest['acceptableVersions']; + $currentVersion = $this->getConfig()->get('version'); + + if (is_string($version)) { + $version = (array) $version; + } + + foreach ($version as $strVersion) { + + $strVersion = trim($strVersion); + + if ($strVersion == $currentVersion) { + return true; + } + + $strVersion = str_replace('\\', '', $strVersion); + $strVersion = preg_quote($strVersion); + $strVersion = str_replace('\\*', '+', $strVersion); + + if (preg_match('/^'.$strVersion.'/', $currentVersion)) { + return true; + } + } + + $this->throwErrorWithDetelePackage('Your EspoCRM version doesn\'t match for this installation package.'); + } + + protected function checkPackageType() + { + $manifest = $this->getManifest(); + + /** check package type */ + $type = strtolower( $this->getParams('name') ); + $manifestType = isset($manifest['type']) ? strtolower($manifest['type']) : $this->defaultPackageType; + + if (!in_array($manifestType, $this->packageTypes)) { + $this->throwErrorWithDetelePackage('Unknown package type.'); + } + + if ($type != $manifestType) { + $this->throwErrorWithDetelePackage('Wrong package type. You cannot install '.$manifestType.' package via '.ucfirst($type).' Manager.'); + } + + return true; + } + + /** + * Run scripts by type + * @param string $type Ex. "before", "after" + * @return void + */ + protected function runScript($type) + { + $packagePath = $this->getPath('packagePath'); + $scriptNames = $this->getParams('scriptNames'); + + $scriptName = $scriptNames[$type]; + if (!isset($scriptName)) { + return; + } + + $beforeInstallScript = Util::concatPath( array($packagePath, self::SCRIPTS, $scriptName) ) . '.php'; + + if (file_exists($beforeInstallScript)) { + require_once($beforeInstallScript); + $script = new $scriptName(); + $script->run($this->getContainer()); + } + } + + /** + * Get package path + * + * @param string $processId + * @return string + */ + protected function getPath($name = 'packagePath', $isPackage = false) + { + $postfix = $isPackage ? $this->packagePostfix : ''; + + $processId = $this->getProcessId(); + $path = Util::concatPath($this->getParams($name), $processId); + + return $path . $postfix; + } + + /** + * Get a list of files defined in manifest.json + * + * @return [type] [description] + */ + protected function getDeleteFileList() + { + $manifest = $this->getManifest(); + + if (!empty($manifest['delete'])) { + return $manifest['delete']; + } + + return array(); + } + + /** + * Delete files defined in a manifest + * + * @return boolen + */ + protected function deleteFiles() + { + $deleteFileList = $this->getDeleteFileList(); + + if (!empty($deleteFileList)) { + return $this->getFileManager()->remove($deleteFileList); + } + + return true; + } + + protected function getCopyFileList() + { + if (!isset($this->data['fileList'])) { + $packagePath = $this->getPath('packagePath'); + $filesPath = Util::concatPath($packagePath, self::FILES); + + $this->data['fileList'] = $this->getFileManager()->getFileList($filesPath, true, '', 'all', true); + } + + return $this->data['fileList']; + } + + /** + * Copy files from upgrade/extension package + * + * @param string $processId + * @return boolean + */ + protected function copyFiles() + { + $packagePath = $this->getPath('packagePath'); + $filesPath = Util::concatPath($packagePath, self::FILES); + + return $this->getFileManager()->copy($filesPath, '', true); + } + + public function getManifest() + { + if (!isset($this->data['manifest'])) { + $packagePath = $this->getPath('packagePath'); + + $manifestPath = Util::concatPath($packagePath, $this->manifestName); + if (!file_exists($manifestPath)) { + throw new Error('It\'s not an Installation package.'); + } + + $manifestJson = $this->getFileManager()->getContents($manifestPath); + $this->data['manifest'] = Json::decode($manifestJson, true); + + if (!$this->data['manifest']) { + throw new Error('Syntax error in manifest.json.'); + } + + if (!$this->checkManifest($this->data['manifest'])) { + throw new Error('Unsupported package.'); + } + } + + return $this->data['manifest']; + } + + /** + * Check if the manifest is correct + * + * @param array $manifest + * @return boolean + */ + protected function checkManifest(array $manifest) + { + $requiredFields = array( + 'name', + 'version', + 'acceptableVersions', + ); + + foreach ($requiredFields as $fieldName) { + if (empty($manifest[$fieldName])) { + return false; + } + } + + return true; + } + + /** + * Unzip a package archieve + * + * @return void + */ + protected function unzipArchive() + { + $packagePath = $this->getPath('packagePath'); + $packageArchivePath = $this->getPath('packagePath', true); + + $res = $this->getZipUtil()->unzip($packageArchivePath, $packagePath); + if ($res === false) { + throw new Error('Unnable to unzip the file - '.$packagePath.'.'); + } + } + + /** + * Delete temporary package files + * + * @return boolean + */ + protected function deletePackageFiles() + { + $packagePath = $this->getPath('packagePath'); + $res = $this->getFileManager()->removeInDir($packagePath, true); + + return $res; + } + + /** + * Delete temporary package archive + * + * @return boolean + */ + protected function deletePackageArchive() + { + $packageArchive = $this->getPath('packagePath', true); + $res = $this->getFileManager()->removeFile($packageArchive); + + return $res; + } + + protected function systemRebuild() + { + return $this->getContainer()->get('dataManager')->rebuild(); + } + + protected function beforeRunAction() + { + + } + + protected function afterRunAction() + { + + } + + +} diff --git a/application/Espo/Core/Upgrades/Actions/Base/Delete.php b/application/Espo/Core/Upgrades/Actions/Base/Delete.php new file mode 100644 index 0000000000..60a95b0fe5 --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Base/Delete.php @@ -0,0 +1,55 @@ +debug('Delete package process ['.$processId.']: start run.'); + + if (empty($processId)) { + throw new Error('Delete package package ID was not specified.'); + } + + $this->setProcessId($processId); + + $this->beforeRunAction(); + + /* delete a package */ + $this->deletePackage(); + + $this->afterRunAction(); + + $GLOBALS['log']->debug('Delete package process ['.$processId.']: end run.'); + } + + protected function deletePackage() + { + $packageArchivePath = $this->getPath('packagePath', true); + $res = $this->getFileManager()->removeFile($packageArchivePath); + + return $res; + } + +} \ No newline at end of file diff --git a/application/Espo/Core/Upgrades/Actions/Base/Install.php b/application/Espo/Core/Upgrades/Actions/Base/Install.php new file mode 100644 index 0000000000..c21d3f67e5 --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Base/Install.php @@ -0,0 +1,81 @@ +debug('Installation process ['.$processId.']: start run.'); + + if (empty($processId)) { + throw new Error('Installation package ID was not specified.'); + } + + $this->setProcessId($processId); + + /** check if an archive is unzipped, if no then unzip */ + $packagePath = $this->getPath('packagePath'); + if (!file_exists($packagePath)) { + $this->unzipArchive(); + $this->isAcceptable(); + } + + $this->beforeRunAction(); + + /* run before install script */ + $this->runScript('before'); + + /* remove files defined in a manifest */ + if (!$this->deleteFiles()) { + throw new Error('Permission denied to delete files.'); + } + + /* copy files from directory "Files" to EspoCRM files */ + if (!$this->copyFiles()) { + throw new Error('Cannot copy files.'); + } + + if (!$this->systemRebuild()) { + throw new Error('Error occurred while EspoCRM rebuild.'); + } + + /* run before install script */ + $this->runScript('after'); + + $this->afterRunAction(); + + /* delete unziped files */ + $this->deletePackageFiles(); + + $GLOBALS['log']->debug('Installation process ['.$processId.']: end run.'); + } +} diff --git a/application/Espo/Core/Upgrades/Actions/Base/Uninstall.php b/application/Espo/Core/Upgrades/Actions/Base/Uninstall.php new file mode 100644 index 0000000000..8d0730ed4a --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Base/Uninstall.php @@ -0,0 +1,103 @@ +debug('Uninstallation process ['.$processId.']: start run.'); + + if (empty($processId)) { + throw new Error('Uninstallation package ID was not specified.'); + } + + $this->setProcessId($processId); + + $this->beforeRunAction(); + + /* run before install script */ + $this->runScript('beforeUninstall'); + + /* remove extension files, saved in fileList */ + if (!$this->deleteFiles()) { + throw new Error('Permission denied to delete files.'); + } + + /* copy core files */ + if (!$this->copyFiles()) { + throw new Error('Cannot copy files.'); + } + + if (!$this->systemRebuild()) { + throw new Error('Error occurred while EspoCRM rebuild.'); + } + + /* run before install script */ + $this->runScript('afterUninstall'); + + $this->afterRunAction(); + + /* delete backup files */ + $this->deletePackageFiles(); + + $GLOBALS['log']->debug('Uninstallation process ['.$processId.']: end run.'); + } + + protected function getDeleteFileList() + { + $extensionEntity = $this->getExtensionEntity(); + return $extensionEntity->get('fileList'); + } + + protected function copyFiles() + { + $backupPath = $this->getPath('backupPath'); + $res = $this->getFileManager()->copy(array($backupPath, self::FILES), '', true); + + return $res; + } + + /** + * Get backup path + * + * @param string $processId + * @return string + */ + protected function getPath($name = 'packagePath', $isPackage = false) + { + $name = ($name == 'packagePath') ? 'backupPath' : $name; + return parent::getPath($name, $isPackage); + } + + protected function deletePackageFiles() + { + $backupPath = $this->getPath('backupPath'); + $res = $this->getFileManager()->removeInDir($backupPath, true); + + return $res; + } + +} diff --git a/application/Espo/Core/Upgrades/Actions/Base/Upload.php b/application/Espo/Core/Upgrades/Actions/Base/Upload.php new file mode 100644 index 0000000000..ee679cd13a --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Base/Upload.php @@ -0,0 +1,62 @@ +createProcessId(); + + $GLOBALS['log']->debug('Installation process ['.$processId.']: start upload the package.'); + + $packagePath = $this->getPath('packagePath'); + $packageArchivePath = $this->getPath('packagePath', true); + + if (!empty($data)) { + list($prefix, $contents) = explode(',', $data); + $contents = base64_decode($contents); + } + + $res = $this->getFileManager()->putContents($packageArchivePath, $contents); + if ($res === false) { + throw new Error('Could not upload the package.'); + } + + $this->unzipArchive(); + + $this->isAcceptable(); + + $GLOBALS['log']->debug('Installation process ['.$processId.']: end upload the package.'); + + return $processId; + } +} \ No newline at end of file diff --git a/application/Espo/Core/Upgrades/Actions/Extension/Delete.php b/application/Espo/Core/Upgrades/Actions/Extension/Delete.php new file mode 100644 index 0000000000..daadecef66 --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Extension/Delete.php @@ -0,0 +1,75 @@ +extensionEntity; + } + + /** + * Set Extension Entity + * + * @param \Espo\Entities\Extension $extensionEntity + */ + protected function setExtensionEntity(\Espo\Entities\Extension $extensionEntity) + { + $this->extensionEntity = $extensionEntity; + } + + protected function beforeRunAction() + { + $processId = $this->getProcessId(); + + /** get extension entity */ + $extensionEntity = $this->getEntityManager()->getEntity('Extension', $processId); + if (!isset($extensionEntity)) { + throw new Error('Extension Entity not found.'); + } + $this->setExtensionEntity($extensionEntity); + + /** check if extension archive exsists */ + $packageArchivePath = $this->getPath('packagePath', true); + if (!file_exists($packageArchivePath)) { + throw new Error('Extension Archive doesn\'t exist.'); + } + } + + protected function afterRunAction() + { + /** Delete extension entity */ + $extensionEntity = $this->getExtensionEntity(); + $this->getEntityManager()->removeEntity($extensionEntity); + } +} \ No newline at end of file diff --git a/application/Espo/Core/Upgrades/Actions/Extension/Install.php b/application/Espo/Core/Upgrades/Actions/Extension/Install.php new file mode 100644 index 0000000000..9d46603707 --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Extension/Install.php @@ -0,0 +1,166 @@ +findExtension(); + $this->compareVersion(); + + $this->copyExistingFiles(); + } + + protected function afterRunAction() + { + $this->storeExtension(); + } + + /** + * Copy Existing files to backup directory + * + * @return bool + */ + protected function copyExistingFiles() + { + $fileList = $this->getCopyFileList(); + $backupPath = $this->getPath('backupPath'); + + $res = $this->getFileManager()->copy('', array($backupPath, self::FILES), false, $fileList); + + /** copy scripts files */ + $packagePath = $this->getPath('packagePath'); + $res &= $this->getFileManager()->copy(array($packagePath, self::SCRIPTS), array($backupPath, self::SCRIPTS), true); + + return $res; + } + + protected function isNew() + { + $extensionEntity = $this->getExtensionEntity(); + + if (isset($extensionEntity)) { + $id = $this->getExtensionEntity()->get('id'); + } + + return isset($id) ? false : true; + } + + /** + * Get extension ID. It's an ID of existing entity (if available) or Installation ID + * + * @return string + */ + protected function getExtensionId() + { + $extensionEntity = $this->getExtensionEntity(); + if (isset($extensionEntity)) { + $extensionEntityId = $extensionEntity->get('id'); + } + + if (!isset($extensionEntityId)) { + return $this->getProcessId(); + } + + return $extensionEntityId; + } + + /** + * Get entity of this extension + * + * @return \Espo\Entities\Extension + */ + protected function getExtensionEntity() + { + return $this->extensionEntity; + } + + /** + * Find Extension entity + * + * @return \Espo\Entities\Extension + */ + protected function findExtension() + { + $manifest = $this->getManifest(); + + $this->extensionEntity = $this->getEntityManager()->getRepository('Extension')->where(array( + 'name' => $manifest['name'], + 'isInstalled' => true, + ))->findOne(); + + return $this->extensionEntity; + } + + /** + * Create a record of Extension Entity + * + * @return bool + */ + protected function storeExtension() + { + $entityManager = $this->getEntityManager(); + $extensionEntity = $entityManager->getEntity('Extension'); + + $manifest = $this->getManifest(); + $fileList = $this->getCopyFileList(); + + $data = array( + 'id' => $this->getProcessId(), + 'name' => $manifest['name'], + 'status' => self::ENABLED, + 'isInstalled' => true, + 'version' => $manifest['version'], + 'fileList' => $fileList, + 'description' => $manifest['description'], + ); + $extensionEntity->set($data); + + return $entityManager->saveEntity($extensionEntity); + } + + /** + * Compare version between installed and a new extensions + * + * @return void + */ + protected function compareVersion() + { + $manifest = $this->getManifest(); + $extensionEntity = $this->getExtensionEntity(); + + if (isset($extensionEntity)) { + $comparedVersion = version_compare($manifest['version'], $extensionEntity->get('version')); + if ($comparedVersion <= 0) { + $this->throwErrorWithDetelePackage('You cannot install an older version of this extension.'); + } + } + } + + +} diff --git a/application/Espo/Core/Upgrades/Actions/Extension/Uninstall.php b/application/Espo/Core/Upgrades/Actions/Extension/Uninstall.php new file mode 100644 index 0000000000..acc277b914 --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Extension/Uninstall.php @@ -0,0 +1,78 @@ +extensionEntity; + } + + /** + * Set Extension Entity + * + * @param \Espo\Entities\Extension $extensionEntity [description] + */ + protected function setExtensionEntity(\Espo\Entities\Extension $extensionEntity) + { + $this->extensionEntity = $extensionEntity; + } + + protected function beforeRunAction() + { + $processId = $this->getProcessId(); + + /** get extension entity */ + $extensionEntity = $this->getEntityManager()->getEntity('Extension', $processId); + if (!isset($extensionEntity)) { + throw new Error('Extension Entity not found.'); + } + $this->setExtensionEntity($extensionEntity); + + /** check if backup of the extension exsists */ + $backupPath = $this->getPath('backupPath'); + if (!file_exists($backupPath)) { + throw new Error('Backup files don\'t exist.'); + } + } + + protected function afterRunAction() + { + /** Set extension entity, isInstalled = false */ + $extensionEntity = $this->getExtensionEntity(); + + $extensionEntity->set('status', self::DISABLED); + $extensionEntity->set('isInstalled', false); + $this->getEntityManager()->saveEntity($extensionEntity); + } +} \ No newline at end of file diff --git a/application/Espo/Core/Upgrades/Actions/Extension/Upload.php b/application/Espo/Core/Upgrades/Actions/Extension/Upload.php new file mode 100644 index 0000000000..e54b1a67c2 --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Extension/Upload.php @@ -0,0 +1,28 @@ +getManifest(); + + $res = $this->getConfig()->set('version', $manifest['version']); + if (method_exists($this->getConfig(), 'save')) { + $res = $this->getConfig()->save(); + } + $res &= parent::systemRebuild(); + + return $res; + } + + /** + * Delete temporary package files + * + * @return boolean + */ + protected function deletePackageFiles() + { + $res = parent::deletePackageFiles(); + $res &= $this->deletePackageArchive(); + + return $res; + } +} diff --git a/application/Espo/Core/Upgrades/Actions/Upgrade/Upload.php b/application/Espo/Core/Upgrades/Actions/Upgrade/Upload.php new file mode 100644 index 0000000000..a1ee4491be --- /dev/null +++ b/application/Espo/Core/Upgrades/Actions/Upgrade/Upload.php @@ -0,0 +1,28 @@ + 'Before', - 'after' => 'After', - ); - - protected $paths = array( - 'files' => 'files', - 'scripts' => 'scripts', - ); + const UNINSTALL = 'uninstall'; + const DELETE = 'delete'; public function __construct($container) { $this->container = $container; - $this->zipUtil = new \Espo\Core\Utils\File\ZipArchive($container->get('fileManager')); - } - - public function __destruct() - { - $this->upgradeId = null; - $this->data = null; + $this->actionManager = new ActionManager($this->name, $container, $this->params); } protected function getContainer() @@ -75,319 +54,41 @@ abstract class Base return $this->container; } - protected function getZipUtil() + protected function getActionManager() { - return $this->zipUtil; - } - - protected function getFileManager() - { - if (!isset($this->fileManager)) { - $this->fileManager = $this->getContainer()->get('fileManager'); - } - return $this->fileManager; - } - - protected function getConfig() - { - if (!isset($this->config)) { - $this->config = $this->getContainer()->get('config'); - } - return $this->config; - } - - - /** - * Upload an upgrade package - * - * @param [type] $contents - * @return string ID of upgrade process - */ - public function upload($data) - { - $upgradeId = $this->createUpgradeId(); - - $GLOBALS['log']->debug('Upgrade process ['.$upgradeId.']: start upload the package.'); - - $upgradePath = $this->getUpgradePath(); - $upgradePackagePath = $this->getUpgradePath(true); - - if (!empty($data)) { - list($prefix, $contents) = explode(',', $data); - $contents = base64_decode($contents); - } - - $res = $this->getFileManager()->putContents($upgradePackagePath, $contents); - if ($res === false) { - throw new Error('Could not upload the package.'); - } - - $res = $this->getZipUtil()->unzip($upgradePackagePath, $upgradePath); - if ($res === false) { - throw new Error('Unnable to unzip the file - '.$upgradePath.'.'); - } - - if (!$this->isAcceptable()) { - throw new Error("Your EspoCRM version doesn't match for this upgrade package."); - } - - $GLOBALS['log']->debug('Upgrade process ['.$upgradeId.']: end upload the package.'); - - return $upgradeId; - } - - /** - * Main upgrade process - * - * @param string $upgradeId Upgrade ID, gotten in upload stage - * @return bool - */ - public function run($upgradeId) - { - $GLOBALS['log']->debug('Upgrade process ['.$upgradeId.']: start run.'); - - if (empty($upgradeId)) { - throw new Error('Upgrade ID was not specified.'); - } - - $this->setUpgradeId($upgradeId); - - /* run before install script */ - $this->runScript('before'); - - /* remove files defined in a manifest */ - if (!$this->deleteFiles()) { - throw new Error('Permission denied to delete files.'); - } - - /* copy files from directory "Files" to EspoCRM files */ - if (!$this->copyFiles()) { - throw new Error('Cannot copy files.'); - } - - if (!$this->systemRebuild()) { - throw new Error('Error occurred while EspoCRM rebuild.'); - } - - /* run before install script */ - $this->runScript('after'); - - /* delete unziped files */ - $this->deletePackageFiles(); - - $GLOBALS['log']->debug('Upgrade process ['.$upgradeId.']: end run.'); - } - - - protected function createUpgradeId() - { - if (isset($this->upgradeId)) { - throw new Error('Another upgrade process is currently running.'); - } - - $this->upgradeId = uniqid(); - - return $this->upgradeId; - } - - protected function getUpgradeId() - { - if (!isset($this->upgradeId)) { - throw new Error("Upgrade ID was not specified."); - } - - return $this->upgradeId; - } - - protected function setUpgradeId($upgradeId) - { - $this->upgradeId = $upgradeId; - } - - /** - * Check if version of upgrade is acceptable to current version of EspoCRM - * - * @param string $version - * @return boolean - */ - protected function isAcceptable() - { - $manifest = $this->getManifest(); - $version = $manifest['acceptableVersions']; - - $currentVersion = $this->getConfig()->get('version'); - - if (is_string($version)) { - $version = (array) $version; - } - - foreach ($version as $strVersion) { - - $strVersion = trim($strVersion); - - if ($strVersion == $currentVersion) { - return true; - } - - $strVersion = str_replace('\\', '', $strVersion); - $strVersion = preg_quote($strVersion); - $strVersion = str_replace('\\*', '+', $strVersion); - - if (preg_match('/^'.$strVersion.'/', $currentVersion)) { - return true; - } - } - - return false; - } - - /** - * Run scripts by type - * @param string $type Ex. "before", "after" - * @return void - */ - protected function runScript($type) - { - $upgradePath = $this->getUpgradePath(); - - $scriptName = $this->scriptNames[$type]; - if (!isset($scriptName)) { - return; - } - - $beforeInstallScript = Util::concatPath( array($upgradePath, $this->paths['scripts'], $scriptName) ) . '.php'; - - if (file_exists($beforeInstallScript)) { - require_once($beforeInstallScript); - $script = new $scriptName(); - $script->run($this->getContainer()); - } - } - - /** - * Get upgrade path - * - * @param string $upgradeId - * @return string - */ - protected function getUpgradePath($isPackage = false) - { - $postfix = $isPackage ? $this->packagePostfix : ''; - - if (!isset($this->data['upgradePath'])) { - $upgradeId = $this->getUpgradeId(); - $this->data['upgradePath'] = Util::concatPath($this->packagePath, $upgradeId); - } - - return $this->data['upgradePath'] . $postfix; - } - - /** - * Delete files defined in a manifest - * - * @return boolen - */ - protected function deleteFiles() - { - $manifest = $this->getManifest(); - - if (!empty($manifest['delete'])) { - return $this->getFileManager()->remove($manifest['delete']); - } - - return true; - } - - /** - * Copy files from upgrade package - * - * @param string $upgradeId - * @return boolean - */ - protected function copyFiles() - { - $upgradePath = $this->getUpgradePath(); - $filesPath = Util::concatPath($upgradePath, $this->paths['files']); - - return $this->getFileManager()->copy($filesPath, '', true); + return $this->actionManager; } public function getManifest() { - if (!isset($this->data['manifest'])) { - $upgradePath = $this->getUpgradePath(); - - $manifestPath = Util::concatPath($upgradePath, $this->manifestName); - if (!file_exists($manifestPath)) { - throw new Error('It\'s not an upgrade package.'); - } - - $manifestJson = $this->getFileManager()->getContents($manifestPath); - $this->data['manifest'] = Json::decode($manifestJson, true); - - if (!$this->data['manifest']) { - throw new Error('Syntax error in manifest.json.'); - } - - if (!$this->checkManifest($this->data['manifest'])) { - throw new Error('Unsupported package.'); - } - } - - return $this->data['manifest']; + return $this->getActionManager()->getManifest(); } - /** - * Check if the manifest is correct - * - * @param array $manifest - * @return boolean - */ - protected function checkManifest(array $manifest) + public function upload($data) { - $requiredFields = array( - 'name', - 'version', - 'acceptableVersions', - ); + $this->getActionManager()->setAction(self::UPLOAD); - foreach ($requiredFields as $fieldName) { - if (empty($manifest[$fieldName])) { - return false; - } - } - - return true; + return $this->getActionManager()->run($data); } - /** - * Delete temporary package files - * - * @return boolean - */ - protected function deletePackageFiles() + public function install($processId) { - $upgradePath = $this->getUpgradePath(); - $upgradePackagePath = $this->getUpgradePath(true); + $this->getActionManager()->setAction(self::INSTALL); - $res = $this->getFileManager()->removeInDir($upgradePath, true); - $res &= $this->getFileManager()->removeFile($upgradePackagePath); - - return $res; + return $this->getActionManager()->run($processId); } - protected function systemRebuild() + public function uninstall($processId) { - $manifest = $this->getManifest(); + $this->getActionManager()->setAction(self::UNINSTALL); - $res = $this->getConfig()->set('version', $manifest['version']); - if (method_exists($this->getConfig(), 'save')) { - $res = $this->getConfig()->save(); - } - $res &= $this->getContainer()->get('dataManager')->rebuild(); - - return $res; + return $this->getActionManager()->run($processId); } + public function delete($processId) + { + $this->getActionManager()->setAction(self::DELETE); + return $this->getActionManager()->run($processId); + } } diff --git a/application/Espo/Core/Utils/File/Manager.php b/application/Espo/Core/Utils/File/Manager.php index 7534e9e45b..f60a8aac3d 100644 --- a/application/Espo/Core/Utils/File/Manager.php +++ b/application/Espo/Core/Utils/File/Manager.php @@ -362,40 +362,67 @@ class Manager /** * Copy files from one direcoty to another + * Ex. $sourcePath = 'data/uploads/extensions/file.json', $destPath = 'data/uploads/backup', result will be data/uploads/backup/data/uploads/backup/file.json. * * @param string $sourcePath * @param string $destPath * @param boolean $recursively + * @param array $fileList - list of files that should be copied + * @param boolean $copyOnlyFiles - copy only files, instead of full path with directories, Ex. $sourcePath = 'data/uploads/extensions/file.json', $destPath = 'data/uploads/backup', result will be 'data/uploads/backup/file.json' * @return boolen */ - public function copy($sourcePath, $destPath, $recursively = false) + public function copy($sourcePath, $destPath, $recursively = false, array $fileList = null, $copyOnlyFiles = false) { $sourcePath = $this->concatPaths($sourcePath); $destPath = $this->concatPaths($destPath); - if (is_file($sourcePath)) { - $fileList = (array) $sourcePath; + if (isset($fileList)) { + if (!empty($sourcePath)) { + foreach ($fileList as &$fileName) { + $fileName = $this->concatPaths(array($sourcePath, $fileName)); + } + } } else { - $fileList = $this->getFileList($sourcePath, $recursively, '', 'all', true); + $fileList = is_file($sourcePath) ? (array) $sourcePath : $this->getFileList($sourcePath, $recursively, '', 'all', true); + } + + /** Check permission before copying */ + $permissionDeniedList = array(); + foreach ($fileList as $file) { + + if ($copyOnlyFiles) { + $file = pathinfo($file, PATHINFO_BASENAME); + } + + $destFile = $this->concatPaths(array($destPath, $file)); + + if ($this->checkCreateFile($destFile) === false) { + $permissionDeniedList[] = $destFile; + } + } + /** END */ + + if (!empty($permissionDeniedList)) { + $betterPermissionList = $this->getPermissionUtils()->arrangePermissionList($permissionDeniedList); + throw new Error("Permission denied in
". implode(",
", $betterPermissionList)); } $res = true; foreach ($fileList as $file) { - $sourceFile = $this->concatPaths(array($sourcePath, $file)); - $destFile = $this->concatPaths(array($destPath, $file)); - - if ($this->checkCreateFile($destFile) === false) { - throw new Error('Permission denied in '. $destFile); + if ($copyOnlyFiles) { + $file = pathinfo($file, PATHINFO_BASENAME); } + $sourceFile = is_file($sourcePath) ? $sourcePath : $this->concatPaths(array($sourcePath, $file)); + $destFile = $this->concatPaths(array($destPath, $file)); + $res &= copy($sourceFile, $destFile); } return $res; } - /** * Create a new file if not exists with all folders in the path. * @@ -566,7 +593,6 @@ class Manager return $pathInfo['dirname']; } - /** * Return content of PHP file * diff --git a/application/Espo/Core/Utils/File/Permission.php b/application/Espo/Core/Utils/File/Permission.php index b1fc85554f..d9597f5cb0 100644 --- a/application/Espo/Core/Utils/File/Permission.php +++ b/application/Espo/Core/Utils/File/Permission.php @@ -535,6 +535,71 @@ class Permission return $this->permissionErrorRules; } + /** + * Arrange permission file list + * e.g. array('application/Espo/Controllers/Email.php', 'application/Espo/Controllers/Import.php'), result is array('application/Espo/Controllers') + * + * @param array $fileList + * @return array + */ + public function arrangePermissionList($fileList) + { + $betterList = array(); + foreach ($fileList as $fileName) { + + $pathInfo = pathinfo($fileName); + $dirname = $pathInfo['dirname']; + + $currentPath = $fileName; + if ($this->getSearchCount($dirname, $fileList) > 1) { + $currentPath = $dirname; + } + + if (!$this->isItemIncludes($currentPath, $betterList)) { + $betterList[] = $currentPath; + } + } + + return $betterList; + } + + /** + * Get count of a search string in a array + * + * @param string $search + * @param array $array + * @return bool + */ + protected function getSearchCount($search, array $array) + { + $search = $this->getPregQuote($search); + + $number = 0; + foreach ($array as $value) { + if (preg_match('/^'.$search.'/', $value)) { + $number++; + } + } + + return $number; + } + + protected function isItemIncludes($item, $array) + { + foreach ($array as $value) { + $value = $this->getPregQuote($value); + if (preg_match('/^'.$value.'/', $item)) { + return true; + } + } + + return false; + } + + protected function getPregQuote($string) + { + return preg_quote($string, '/-+=.'); + } } diff --git a/application/Espo/Resources/metadata/entityDefs/Extension.json b/application/Espo/Resources/metadata/entityDefs/Extension.json index 20bb6f1847..f5af8b3114 100644 --- a/application/Espo/Resources/metadata/entityDefs/Extension.json +++ b/application/Espo/Resources/metadata/entityDefs/Extension.json @@ -2,7 +2,7 @@ "fields": { "name": { "type": "varchar", - "required": true + "required": true }, "status": { "type": "enum", @@ -11,13 +11,17 @@ "version": { "type": "varchar", "required": true, - "maxLength": 50 + "maxLength": 50 }, "fileList": { - "type": "jsonArray" - }, + "type": "jsonArray" + }, "description": { - "type": "text" + "type": "text" + }, + "isInstalled": { + "type": "bool", + "default": false }, "createdAt": { "type": "datetime", diff --git a/tests/Espo/Core/Utils/File/PermissionTest.php b/tests/Espo/Core/Utils/File/PermissionTest.php new file mode 100644 index 0000000000..f8208a260e --- /dev/null +++ b/tests/Espo/Core/Utils/File/PermissionTest.php @@ -0,0 +1,156 @@ +objects['fileManager'] = $this->getMockBuilder('\Espo\Core\Utils\File\Manager')->disableOriginalConstructor()->getMock(); + + $this->object = new \Espo\Core\Utils\File\Permission($this->objects['fileManager']); + + $this->reflection = new ReflectionHelper($this->object); + + $this->fileList = array( + 'application/Espo/Controllers/Email.php', + 'application/Espo/Controllers/EmailAccount.php', + 'application/Espo/Controllers/EmailAddress.php', + 'application/Espo/Controllers/ExternalAccount.php', + 'application/Espo/Controllers/Import.php', + 'application/Espo/Controllers/Integration.php', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL/Calendar.json', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL/Call.json', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL/Case.json', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL/Contact.json', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL/Global.json', + 'application/Espo/Resources/layouts/User/filters.json', + 'application/Espo/Resources/metadata/app/acl.json', + 'application/Espo/Resources/metadata/app/defaultDashboardLayout.json' + ); + } + + protected function tearDown() + { + $this->object = NULL; + } + + public function testGetSearchCount() + { + $search = 'application/Espo/Controllers/'; + $methodResult = $this->reflection->invokeMethod('getSearchCount', array($search, $this->fileList)); + $result = 6; + $this->assertEquals($result, $methodResult); + + + $search = 'application/Espo/Controllers/Email.php'; + $methodResult = $this->reflection->invokeMethod('getSearchCount', array($search, $this->fileList)); + $result = 1; + $this->assertEquals($result, $methodResult); + + $search = 'application/Espo/Controllers/NotReal'; + $methodResult = $this->reflection->invokeMethod('getSearchCount', array($search, $this->fileList)); + $result = 0; + $this->assertEquals($result, $methodResult); + } + + public function testArrangePermissionList() + { + $result = array( + 'application/Espo/Controllers', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL', + 'application/Espo/Resources/layouts/User/filters.json', + 'application/Espo/Resources/metadata/app', + ); + $this->assertEquals( $result, $this->object->arrangePermissionList($this->fileList) ); + } + + /*public function bestPossibleList() + { + $fileList = array( + 'application/Espo/Controllers', + 'application/Espo/Core', + 'application/Espo/Core/Cron', + 'application/Espo/Core/Loaders', + 'application/Espo/Core/Mail', + 'application/Espo/Core/Mail/Storage/Imap.php', + 'application/Espo/Core/SelectManagers/Base.php', + 'application/Espo/Core/Utils/Database/Orm', + 'application/Espo/Core/Utils/Database/Orm/Fields', + 'application/Espo/Core/Utils/Database/Orm/Relations', + 'application/Espo/Core/Utils', + 'application/Espo/Core/defaults/config.php', + 'application/Espo/Entities', + 'application/Espo/Hooks/Common/Stream.php', + 'application/Espo/Modules/Crm/Controllers/Opportunity.php', + 'application/Espo/Modules/Crm/Jobs/CheckInboundEmails.php', + 'application/Espo/Modules/Crm/Resources/i18n/de_DE', + 'application/Espo/Modules/Crm/Resources/i18n/en_US', + 'application/Espo/Modules/Crm/Resources/i18n/nl_NL', + 'application/Espo/Modules/Crm/Resources/i18n/pl_PL', + 'application/Espo/Modules/Crm/Resources/layouts/InboundEmail', + 'application/Espo/Modules/Crm/Resources/metadata/clientDefs/InboundEmail.json', + 'application/Espo/Modules/Crm/Resources/metadata/entityDefs', + 'application/Espo/Modules/Crm/Services', + 'application/Espo/Repositories', + 'application/Espo/Resources/i18n/de_DE', + 'application/Espo/Resources/i18n/en_US', + 'application/Espo/Resources/i18n/nl_NL', + 'application/Espo/Resources/i18n/pl_PL', + 'application/Espo/Resources/layouts/Email', + 'application/Espo/Resources/layouts/EmailAccount', + 'application/Espo/Resources/layouts/User/filters.json', + 'application/Espo/Resources/metadata/app', + 'application/Espo/Resources/metadata/clientDefs', + 'application/Espo/Resources/metadata/entityDefs', + 'application/Espo/Resources/metadata/integrations/Google.json', + 'application/Espo/Resources/metadata/scopes', + 'application/Espo/SelectManagers/EmailAccount.php', + 'application/Espo/Services', + 'install/core', + 'install/core/actions/settingsTest.php', + 'install/core/i18n/de_DE/install.json', + 'install/core/i18n/en_US/install.json', + 'install/core/i18n/es_ES/install.json', + 'install/core/i18n/nl_NL/install.json', + 'install/core/i18n/pl_PL/install.json', + 'install/core/i18n/ro_RO/install.json', + 'install/core/i18n/tr_TR/install.json', + 'install/js/install.js', + ); + + $result = array( + 'application/Espo/Controllers', + 'application/Espo/Core', + 'application/Espo/Entities', + 'application/Espo/Hooks/Common/Stream.php', + 'application/Espo/Modules/Crm', + 'application/Espo/Repositories', + 'application/Espo/Resources', + 'application/Espo/SelectManagers/EmailAccount.php', + 'application/Espo/Services', + 'install/core', + 'install/js/install.js', + ); + }*/ + + + + + + + +} + +?>