From: Matthias Schmidt Date: Thu, 22 Nov 2018 18:50:55 +0000 (+0100) Subject: Allow installation of packages via devtools X-Git-Tag: 5.2.0_Alpha_1~512 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=89aa80c150d10a7ebef57d082dcfb20d2c2affb1;p=GitHub%2FWoltLab%2FWCF.git Allow installation of packages via devtools Close #2616 --- diff --git a/wcfsetup/install/files/acp/js/WCF.ACP.js b/wcfsetup/install/files/acp/js/WCF.ACP.js index c050f48f7c..f527b3f7e8 100644 --- a/wcfsetup/install/files/acp/js/WCF.ACP.js +++ b/wcfsetup/install/files/acp/js/WCF.ACP.js @@ -167,6 +167,12 @@ WCF.ACP.Package.Installation = Class.extend({ */ _actionName: 'InstallPackage', + /** + * additional parameters send in all requests + * @var object + */ + _additionalRequestParameters: {}, + /** * true, if rollbacks are supported * @var boolean @@ -210,20 +216,17 @@ WCF.ACP.Package.Installation = Class.extend({ * @param string actionName * @param boolean allowRollback * @param boolean isUpdate + * @param object additionalRequestParameters */ - init: function(queueID, actionName, allowRollback, isUpdate) { + init: function(queueID, actionName, allowRollback, isUpdate, additionalRequestParameters) { this._actionName = (actionName) ? actionName : 'InstallPackage'; this._allowRollback = (allowRollback === true); this._queueID = queueID; + this._additionalRequestParameters = additionalRequestParameters || {}; - switch (this._actionName) { - case 'InstallPackage': - this._dialogTitle = 'wcf.acp.package.' + (isUpdate ? 'update' : 'install') + '.title'; - break; - - case 'UninstallPackage': - this._dialogTitle = 'wcf.acp.package.uninstallation.title'; - break; + this._dialogTitle = 'wcf.acp.package.' + (isUpdate ? 'update' : 'install') + '.title'; + if (this._actionName === 'UninstallPackage') { + this._dialogTitle = 'wcf.acp.package.uninstallation.title'; } this._initProxy(); @@ -317,10 +320,10 @@ WCF.ACP.Package.Installation = Class.extend({ * @return object */ _getParameters: function() { - return { + return $.extend({}, this._additionalRequestParameters, { queueID: this._queueID, step: 'prepare' - }; + }); }, /** @@ -517,7 +520,7 @@ WCF.ACP.Package.Installation = Class.extend({ _executeStep: function(step, node, additionalData) { if (!additionalData) additionalData = { }; - var $data = $.extend({ + var $data = $.extend({}, this._additionalRequestParameters, { node: node, queueID: this._queueID, step: step diff --git a/wcfsetup/install/files/acp/templates/__devtoolsProjectInstallationJavaScript.tpl b/wcfsetup/install/files/acp/templates/__devtoolsProjectInstallationJavaScript.tpl new file mode 100644 index 0000000000..7c93c450ce --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__devtoolsProjectInstallationJavaScript.tpl @@ -0,0 +1,24 @@ +{if !$project->getPackage() && $project->getPackageArchive()->getOpenRequirements()|empty} + +{/if} + +{if !$project->getPackageArchive()->getOpenRequirements()|empty} +
+

{lang}wcf.acp.devtools.project.installPackage.error.openRequirements{/lang}

+ + +
+{/if} diff --git a/wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl b/wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl index 8c77cf5861..59c7edee9d 100644 --- a/wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl +++ b/wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl @@ -136,4 +136,5 @@ updateDisplayedPips(); +{include file='__devtoolsProjectInstallationJavaScript'} {include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/devtoolsProjectSync.tpl b/wcfsetup/install/files/acp/templates/devtoolsProjectSync.tpl index acb0ddfef3..ad5eaa2342 100644 --- a/wcfsetup/install/files/acp/templates/devtoolsProjectSync.tpl +++ b/wcfsetup/install/files/acp/templates/devtoolsProjectSync.tpl @@ -114,4 +114,5 @@

{@$object->validate()}

{/if} +{include file='__devtoolsProjectInstallationJavaScript'} {include file='footer'} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Installation/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Installation/Confirmation.js new file mode 100644 index 0000000000..787fc49da0 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Installation/Confirmation.js @@ -0,0 +1,68 @@ +/** + * Handles installing a project as a package. + * + * @author Matthias Schmidt + * @copyright 2001-2018 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Acp/Ui/Devtools/Project/Installation/Confirmation + */ +define(['Ajax', 'Language', 'Ui/Confirmation'], function(Ajax, Language, UiConfirmation) { + "use strict"; + + var _projectId; + var _projectName; + + return { + /** + * Initializes the confirmation to install a project as a package. + * + * @param {int} projectId id of the installed project + * @param {string} projectName name of the installed project + */ + init: function(projectId, projectName) { + _projectId = projectId; + _projectName = projectName; + + [].forEach.call(elByClass('jsDevtoolsInstallPackage'), function(element) { + element.addEventListener('click', this._showConfirmation.bind(this)); + }.bind(this)); + }, + + /** + * Starts the package installation. + */ + _installPackage: function() { + Ajax.apiOnce({ + data: { + actionName: 'installPackage', + className: 'wcf\\data\\devtools\\project\\DevtoolsProjectAction', + objectIDs: [ _projectId ] + }, + success: function(data) { + var packageInstallation = new WCF.ACP.Package.Installation( + data.returnValues.queueID, + 'DevtoolsInstallPackage', + data.returnValues.isApplication, + false, + {projectID: _projectId} + ); + + packageInstallation.prepareInstallation(); + } + }); + }, + + /** + * Shows the confirmation to start package installation. + */ + _showConfirmation: function() { + UiConfirmation.show({ + confirm: this._installPackage.bind(this), + message: Language.get('wcf.acp.devtools.project.installPackage.confirmMessage', { + packageIdentifier: _projectName + }), + messageIsHtml: true + }); + } + }; +}); \ No newline at end of file diff --git a/wcfsetup/install/files/lib/acp/action/DevtoolsInstallPackageAction.class.php b/wcfsetup/install/files/lib/acp/action/DevtoolsInstallPackageAction.class.php new file mode 100644 index 0000000000..089e6c6c85 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/action/DevtoolsInstallPackageAction.class.php @@ -0,0 +1,63 @@ + + * @package WoltLabSuite\Core\Acp\Action + * @since 3.2 + */ +class DevtoolsInstallPackageAction extends InstallPackageAction { + /** + * project whose source is installed as a package + * @var DevtoolsProject + */ + public $project; + + /** + * id of the project whose source is installed as a package + * @var int + */ + public $projectID; + + /** + * @inheritDoc + */ + protected function getRedirectLink() { + return LinkHandler::getInstance()->getLink('DevtoolsProjectList'); + } + + /** + * @inheritDoc + * @throws IllegalLinkException + */ + public function readParameters() { + AbstractDialogAction::readParameters(); + + if (isset($_POST['projectID'])) $this->projectID = intval($_POST['projectID']); + $this->project = new DevtoolsProject($this->projectID); + if (!$this->project->projectID) { + throw new IllegalLinkException(); + } + + if (isset($_POST['node'])) $this->node = StringUtil::trim($_POST['node']); + + if (isset($_POST['queueID'])) $this->queueID = intval($_POST['queueID']); + $this->queue = new PackageInstallationQueue($this->queueID); + if (!$this->queue->queueID) { + throw new IllegalLinkException(); + } + + $this->installation = new DevtoolsPackageInstallationDispatcher($this->project, $this->queue); + } +} diff --git a/wcfsetup/install/files/lib/acp/action/InstallPackageAction.class.php b/wcfsetup/install/files/lib/acp/action/InstallPackageAction.class.php index 081d76a9ee..3634f278da 100755 --- a/wcfsetup/install/files/lib/acp/action/InstallPackageAction.class.php +++ b/wcfsetup/install/files/lib/acp/action/InstallPackageAction.class.php @@ -89,27 +89,13 @@ class InstallPackageAction extends AbstractDialogAction { $this->installation->completeSetup(); $this->finalize(); - // get domain path - $sql = "SELECT * - FROM wcf".WCF_N."_application - WHERE packageID = ?"; - $statement = WCF::getDB()->prepareStatement($sql); - $statement->execute([1]); - - /** @var Application $application */ - $application = $statement->fetchObject(Application::class); - - // build redirect location - // do not use the LinkHandler here as it is sort of unreliable during WCFSetup - $location = $application->getPageURL() . 'acp/index.php?package-list/'; - WCF::resetZendOpcache(); // show success $this->data = [ 'currentAction' => $this->getCurrentAction(null), 'progress' => 100, - 'redirectLocation' => $location, + 'redirectLocation' => $this->getRedirectLink(), 'step' => 'success' ]; return; @@ -128,6 +114,28 @@ class InstallPackageAction extends AbstractDialogAction { } } + /** + * Returns the link to the page to which the user is redirected after + * the installation finished. + * + * @return string + * @since 3.2 + */ + protected function getRedirectLink() { + // get domain path + $sql = "SELECT * + FROM wcf".WCF_N."_application + WHERE packageID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([1]); + + /** @var Application $application */ + $application = $statement->fetchObject(Application::class); + + // do not use the LinkHandler here as it is sort of unreliable during WCFSetup + return $application->getPageURL() . 'acp/index.php?package-list/'; + } + /** * Prepares the installation process. */ @@ -159,9 +167,7 @@ class InstallPackageAction extends AbstractDialogAction { } /** - * Returns parameters required to perform a rollback. - * - * @return array + * Sets the parameters required to perform a rollback. */ protected function stepRollback() { $this->data = [ diff --git a/wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php b/wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php index 37925787b2..1eceaf9ca8 100644 --- a/wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php @@ -63,7 +63,8 @@ class DevtoolsProjectSyncPage extends AbstractPage { WCF::getTPL()->assign([ 'objectID' => $this->objectID, - 'object' => $this->object + 'object' => $this->object, + 'project' => $this->object ]); } } diff --git a/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php b/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php index 6142352e96..eece7ad5c4 100644 --- a/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php +++ b/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php @@ -2,8 +2,8 @@ namespace wcf\data\devtools\project; use wcf\data\package\installation\plugin\PackageInstallationPluginList; use wcf\data\package\Package; -use wcf\data\package\PackageCache; use wcf\data\DatabaseObject; +use wcf\data\package\PackageList; use wcf\system\devtools\package\DevtoolsPackageArchive; use wcf\system\devtools\pip\DevtoolsPip; use wcf\system\package\validation\PackageValidationException; @@ -25,6 +25,13 @@ use wcf\util\DirectoryUtil; * @property-read string $path file system path */ class DevtoolsProject extends DatabaseObject { + /** + * is `true` if it has already been attempted to fetch a package + * @var bool + * @since 3.2 + */ + protected $didFetchPackage = false; + /** * @var boolean */ @@ -104,8 +111,7 @@ class DevtoolsProject extends DatabaseObject { return $e->getErrorMessage(); } - $this->package = PackageCache::getInstance()->getPackageByIdentifier($this->packageArchive->getPackageInfo('name')); - if ($this->package === null) { + if ($this->getPackage() === null) { return WCF::getLanguage()->getDynamicVariable('wcf.acp.devtools.project.path.error.notInstalled', [ 'package' => $this->packageArchive->getPackageInfo('name') ]); @@ -150,6 +156,18 @@ class DevtoolsProject extends DatabaseObject { * @return Package */ public function getPackage() { + if ($this->package === null) { + $packageList = new PackageList(); + $packageList->getConditionBuilder()->add('package = ?', [$this->getPackageArchive()->getPackageInfo('name')]); + $packageList->readObjects(); + + if (count($packageList)) { + $this->package = $packageList->current(); + } + + $this->didFetchPackage = true; + } + return $this->package; } @@ -157,6 +175,18 @@ class DevtoolsProject extends DatabaseObject { * @return DevtoolsPackageArchive */ public function getPackageArchive() { + if ($this->packageArchive === null) { + $this->packageArchive = new DevtoolsPackageArchive($this->path . ($this->isCore() ? 'com.woltlab.wcf/' : '') . 'package.xml'); + + try { + $this->packageArchive->openArchive(); + } + catch (PackageValidationException $e) { + // we do not care for errors here, `validatePackageXml()` + // takes care of that + } + } + return $this->packageArchive; } @@ -171,6 +201,21 @@ class DevtoolsProject extends DatabaseObject { return array_values(DirectoryUtil::getInstance($languageDirectory)->getFiles(SORT_ASC, Regex::compile('\w+\.xml'))); } + /** + * Sets the package that belongs to this project. + * + * @param Package $package + * @throws \InvalidArgumentException if the identifier of the given package does not match + * @since 3.2 + */ + public function setPackage(Package $package) { + if ($package->package !== $this->getPackageArchive()->getPackageInfo('name')) { + throw new \InvalidArgumentException("Package identifier of given package ('{$package->package}') does not match ('{$this->packageArchive->getPackageInfo('name')}')"); + } + + $this->package = $package; + } + /** * Validates the provided path and returns an error code * if the path does not exist (`notFound`) or if there is diff --git a/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProjectAction.class.php b/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProjectAction.class.php index 9c7ee4c1aa..af975350ff 100644 --- a/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProjectAction.class.php +++ b/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProjectAction.class.php @@ -1,6 +1,8 @@ * @package WoltLabSuite\Core\Data\Devtools\Project @@ -27,13 +29,20 @@ class DevtoolsProjectAction extends AbstractDatabaseObjectAction { /** * @inheritDoc */ - protected $requireACP = ['delete']; + protected $requireACP = ['delete', 'installPackage']; /** * @inheritDoc */ protected $permissionsDelete = ['admin.configuration.package.canInstallPackage']; + /** + * package installation queue for project to be installed from source + * @var PackageInstallationQueue + * @since 3.2 + */ + public $queue; + /** * @inheritDoc */ @@ -130,4 +139,47 @@ class DevtoolsProjectAction extends AbstractDatabaseObjectAction { ]) ]; } + + /** + * Checks if the `installPackage` action can be executed. + * + * @throws IllegalLinkException + * @since 3.2 + */ + public function validateInstallPackage() { + if (!ENABLE_DEVELOPER_TOOLS) { + throw new IllegalLinkException(); + } + + WCF::getSession()->checkPermissions(['admin.configuration.package.canInstallPackage']); + + $this->getSingleObject(); + } + + /** + * Installs a package that is currently only available as a project. + * + * @return int[] id of the package installation queue for the + * @since 3.2 + */ + public function installPackage() { + $packageArchive = $this->getSingleObject()->getPackageArchive(); + $packageArchive->openArchive(); + + $this->queue = PackageInstallationQueueEditor::create([ + 'processNo' => PackageInstallationQueue::getNewProcessNo(), + 'userID' => WCF::getUser()->userID, + 'package' => $packageArchive->getPackageInfo('name'), + 'packageName' => $packageArchive->getLocalizedPackageInfo('packageName'), + 'packageID' => null, + 'archive' => '', + 'action' => 'install', + 'isApplication' => $packageArchive->getPackageInfo('isApplication') ? 1 : 0 + ]); + + return [ + 'isApplication' => $this->queue->isApplication, + 'queueID' => $this->queue->queueID + ]; + } } diff --git a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php index d3b65d25bd..534740d42e 100644 --- a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php +++ b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php @@ -1,7 +1,12 @@ project->getPackageArchive()->getTar(); + $directory = null; + + foreach ($this->project->getPackageArchive()->getInstallInstructions() as $instruction) { + $archive = null; + switch ($instruction['pip']) { + case 'acpTemplate': + $archive = $instruction['value'] ?: ACPTemplatePackageInstallationPlugin::getDefaultFilename(); + break; + + case 'file': + $archive = $instruction['value'] ?: FilePackageInstallationPlugin::getDefaultFilename(); + break; + + case 'template': + $archive = $instruction['value'] ?: TemplatePackageInstallationPlugin::getDefaultFilename(); + break; + } + + if ($archive !== null) { + $directory = FileUtil::addTrailingSlash($this->project->path . pathinfo($archive, PATHINFO_FILENAME)); + if ($source == $archive && is_dir($directory)) { + $files = $this->project->getPackageArchive()->getTar()->getFiles(); + + foreach ($this->project->getPips() as $pip) { + if ($pip->pluginName === $instruction['pip']) { + $pip->getInstructions($this->project, $source); + + $tar = new DevtoolsTar($this->project->getPackageArchive()->getTar()->getFiles()); + + $this->project->getPackageArchive()->getTar()->setFiles($files); + + return $tar; + } + } + } + } + + } + + throw new \InvalidArgumentException("Unknown file '{$source}'"); } } diff --git a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageArchive.class.php b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageArchive.class.php index 4f49ec94ce..641465a8c3 100644 --- a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageArchive.class.php +++ b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageArchive.class.php @@ -1,6 +1,12 @@ tar = new DevtoolsTar(['package.xml' => $this->packageXmlPath]); + $projectDir = FileUtil::addTrailingSlash(dirname($this->packageXmlPath)); + + $readFiles = DirectoryUtil::getInstance($projectDir)->getFiles( + SORT_ASC, + // ignore folders whose contents are delivered as archives by default + // and ignore dotfiles and dotdirectories + Regex::compile('^' . preg_quote($projectDir) . '(acptemplates|files|templates|\.)'), + true + ); + + $files = []; + foreach ($readFiles as $file) { + if (is_file($file)) { + $files[str_replace($projectDir, '', $file)] = $file; + } + } + + $this->tar = new DevtoolsTar($files); $this->readPackageInfo(); + foreach ($this->getInstallInstructions() as $instruction) { + $archive = null; + switch ($instruction['pip']) { + case 'acpTemplate': + $archive = $instruction['value'] ?: ACPTemplatePackageInstallationPlugin::getDefaultFilename(); + break; + + case 'file': + $archive = $instruction['value'] ?: FilePackageInstallationPlugin::getDefaultFilename(); + break; + + case 'template': + $archive = $instruction['value'] ?: TemplatePackageInstallationPlugin::getDefaultFilename(); + break; + } + + if ($archive !== null) { + $this->tar->registerFile($archive, $projectDir . $archive); + } + } } /** * @inheritDoc */ public function extractTar($filename, $tempPrefix = 'package_') { - return $tempPrefix . $filename . '_dummy'; + return $filename; } } diff --git a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsTar.class.php b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsTar.class.php index 781713ec8f..54000cfc31 100644 --- a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsTar.class.php +++ b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsTar.class.php @@ -95,4 +95,22 @@ class DevtoolsTar extends Tar { return $this->contentList; } + + /** + * Returns all files in the virtual file list. + * + * @return string[] + */ + public function getFiles() { + return $this->files; + } + + /** + * Sets all files in the virtual file list. + * + * @param string[] $files + */ + public function setFiles(array $files) { + $this->files = $files; + } } diff --git a/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationDispatcher.class.php b/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationDispatcher.class.php index 61dfcb84be..5ad14f732d 100644 --- a/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationDispatcher.class.php +++ b/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationDispatcher.class.php @@ -1,8 +1,10 @@ queue = $queue; + $this->nodeBuilder = new class($this) extends PackageInstallationNodeBuilder { + protected function buildOptionalNodes() { + // does nothing; optional packages are not supported + } + }; + + $this->action = $this->queue->action; $this->project = $project; } + /** + * @inheritDoc + * @since 3.2 + */ + protected function createPackage(array $packageData) { + $package = parent::createPackage($packageData); + + $this->project->setPackage($package); + + return $package; + } + /** * @inheritDoc */ diff --git a/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php b/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php index 20951dac4f..007a3ff1f1 100644 --- a/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php +++ b/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php @@ -446,7 +446,7 @@ class PackageInstallationDispatcher { } else { // create package entry - $package = PackageEditor::create($nodeData); + $package = $this->createPackage($nodeData); // update package id for current queue $queueEditor = new PackageInstallationQueueEditor($this->queue); @@ -533,6 +533,17 @@ class PackageInstallationDispatcher { return $installationStep; } + /** + * Creates a new package based on the given data and returns it. + * + * @param array $packageData + * @return Package + * @since 3.2 + */ + protected function createPackage(array $packageData) { + return PackageEditor::create($packageData); + } + /** * Saves the localized package info. */ diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index d9a091097d..b8e5934aba 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -388,7 +388,7 @@ - + getPackageArchive()->getOpenRequirements()|empty}class="jsDevtoolsInstallPackage"{else}class="jsStaticDialog" data-dialog-id="openPackageRequirements"{/if}>{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Paket installieren?]]> package.xml gefunden werden.]]> @@ -446,6 +446,10 @@ + {@$packageIdentifier} wirklich installieren?]]> + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index d2b08aae0c..b19bfa1b7a 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -365,7 +365,7 @@ - + getPackageArchive()->getOpenRequirements()|empty}class="jsDevtoolsInstallPackage"{else}class="jsStaticDialog" data-dialog-id="openPackageRequirements"{/if}>Do you want to install the package?]]> package.xml.]]> @@ -423,6 +423,10 @@ + {@$packageIdentifier}?]]> + + +