Add GUI for template listener package installation plugin
authorMatthias Schmidt <gravatronics@live.com>
Sun, 6 May 2018 11:35:29 +0000 (13:35 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 6 May 2018 11:35:29 +0000 (13:35 +0200)
See #2545

wcfsetup/install/files/lib/data/acp/template/ACPTemplate.class.php
wcfsetup/install/files/lib/data/package/Package.class.php
wcfsetup/install/files/lib/system/package/plugin/TemplateListenerPackageInstallationPlugin.class.php
wcfsetup/install/lang/en.xml

index 06bd9d8e3df96944ce8484fd8f5c8643a4bbb8e2..9f421aeffbd4f0c224def90652819560cf89168b 100644 (file)
@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace wcf\data\acp\template;
 use wcf\data\DatabaseObject;
+use wcf\data\package\PackageCache;
 
 /**
  * Represents an ACP template.
@@ -21,4 +22,24 @@ class ACPTemplate extends DatabaseObject {
         * @inheritDoc
         */
        protected static $databaseTableIndexName = 'templateID';
+       
+       /**
+        * Returns the path to this template.
+        * 
+        * @return      string
+        * @since       3.2
+        */
+       public function getPath() {
+               return PackageCache::getInstance()->getPackage($this->packageID)->getAbsolutePackageDir() . 'acp/templates/' . $this->templateName . '.tpl';
+       }
+       
+       /**
+        * Returns the source of this template.
+        * 
+        * @return      string
+        * @since       3.2
+        */
+       public function getSource() {
+               return @file_get_contents($this->getPath());
+       }
 }
index a573b08755dfb821fb16d399be19de935832c427..8d1876b2c802122df6e2aa925085c852daef4dc8 100644 (file)
@@ -29,6 +29,13 @@ use wcf\util\FileUtil;
  * @property-read      string          $authorURL              external url to the website of the package author
  */
 class Package extends DatabaseObject {
+       /**
+        * recursive list of packages that were given as required packages during installation
+        * @var         Package[]
+        * @since       3.2
+        */
+       protected $allRequiredPackages;
+       
        /**
         * list of packages that this package requires
         * @var Package[]
@@ -138,6 +145,32 @@ class Package extends DatabaseObject {
                return $this->requiredPackages;
        }
        
+       /**
+        * Returns the recursive list of packages which are required by this package.
+        * The returned packages are the packages given in the <requiredpackages> tag
+        * in the package.xml of this package and recursively repeats that for all of
+        * those required packages.
+        *
+        * @return      Package[]
+        * @since       3.2
+        */
+       public function getAllRequiredPackages() {
+               if ($this->allRequiredPackages === null) {
+                       $this->allRequiredPackages = $this->getRequiredPackages();
+                       $packagesToCheck = $this->allRequiredPackages;
+                       
+                       /** @var Package $checkedPackage */
+                       while (($checkedPackage = array_pop($packagesToCheck))) {
+                               $newRequiredPackages = array_diff($checkedPackage->getRequiredPackages(), $this->allRequiredPackages);
+                               
+                               $this->allRequiredPackages += $newRequiredPackages;
+                               $packagesToCheck = array_merge($packagesToCheck, $newRequiredPackages);
+                       }
+               }
+               
+               return $this->allRequiredPackages;
+       }
+       
        /**
         * Returns true if current user can uninstall this package.
         * 
index 6ce8a75ae54384aecae419da5560df4b60971663..7642d1ba853f256ffaf6ace6dcd2c5e4cf194208 100644 (file)
@@ -1,21 +1,40 @@
 <?php
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
+use wcf\data\acp\template\ACPTemplate;
+use wcf\data\acp\template\ACPTemplateList;
+use wcf\data\package\Package;
 use wcf\data\template\listener\TemplateListenerEditor;
+use wcf\data\template\listener\TemplateListenerList;
+use wcf\data\template\Template;
+use wcf\data\template\TemplateList;
 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
+use wcf\system\form\builder\field\MultilineTextFormField;
+use wcf\system\form\builder\field\SingleSelectionFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\Regex;
 use wcf\system\WCF;
 use wcf\util\StringUtil;
 
 /**
  * Installs, updates and deletes template listeners.
  * 
- * @author     Alexander Ebert
+ * @author     Alexander Ebert, Matthias Schmidt
  * @copyright  2001-2018 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Package\Plugin
  */
-class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -102,8 +121,293 @@ class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstal
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               $ldq = preg_quote(WCF::getTPL()->getCompiler()->getLeftDelimiter(), '~');
+               $rdq = preg_quote(WCF::getTPL()->getCompiler()->getRightDelimiter(), '~');
+               
+               $getEvents = function($templateList) use ($ldq, $rdq) {
+                       $templateEvents = [];
+                       /** @var ACPTemplate|Template $template */
+                       foreach ($templateList as $template) {
+                               if (preg_match_all("~{$ldq}event\ name\=\'(?<event>[\w]+)\'{$rdq}~", $template->getSource(), $matches)) {
+                                       $templates[$template->templateName] = $template->templateName;
+                                       
+                                       foreach ($matches['event'] as $event) {
+                                               if (!isset($templateEvents[$template->templateName])) {
+                                                       $templateEvents[$template->templateName] = [];
+                                               }
+                                               
+                                               $templateEvents[$template->templateName][] = $event;
+                                       }
+                               }
+                       }
+                       
+                       foreach ($templateEvents as &$events) {
+                               sort($events);
+                       }
+                       unset($events);
+                       
+                       return $templateEvents;
+               };
+               
+               $templateList = new TemplateList();
+               $templateList->getConditionBuilder()->add(
+                       'template.packageID IN (?)',
+                       [array_keys($this->installation->getPackage()->getAllRequiredPackages())]
+               );
+               $templateList->getConditionBuilder()->add('template.templateGroupID IS NULL');
+               $templateList->sqlOrderBy = 'template.templateName ASC';
+               $templateList->readObjects();
+               
+               $templateEvents = $getEvents($templateList);
+               
+               $acpTemplateList = new ACPTemplateList();
+               $acpTemplateList->getConditionBuilder()->add(
+                       'acp_template.packageID IN (?)',
+                       [array_keys($this->installation->getPackage()->getAllRequiredPackages())]
+               );
+               $acpTemplateList->sqlOrderBy = 'acp_template.templateName ASC';
+               $acpTemplateList->readObjects();
+               
+               $acpTemplateEvents = $getEvents($acpTemplateList);
+               
+               $form->getNodeById('data')->appendChildren([
+                       TextFormField::create('name')
+                               ->label('wcf.acp.pip.templateListener.name')
+                               ->description('wcf.acp.pip.templateListener.name.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                       if (!preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.templateListener.name.error.format'
+                                                       )
+                                               );
+                                       }
+                               })),
+                       
+                       SingleSelectionFormField::create('templateName')
+                               ->attribute('data-tag', 'templatename')
+                               ->label('wcf.acp.pip.templateListener.templateName')
+                               ->description('wcf.acp.pip.templateListener.templateName.description')
+                               ->required()
+                               ->options(array_combine(array_keys($templateEvents), array_keys($templateEvents)))
+                               ->filterable(),
+                       
+                       SingleSelectionFormField::create('acpTemplateName')
+                               ->attribute('data-tag', 'templatename')
+                               ->label('wcf.acp.pip.templateListener.templateName')
+                               ->description('wcf.acp.pip.templateListener.templateName.description')
+                               ->required()
+                               ->options(array_combine(array_keys($acpTemplateEvents), array_keys($acpTemplateEvents)))
+                               ->filterable()
+               ]);
+               
+               foreach ($templateEvents as $templateName => $events) {
+                       $form->getNodeById('data')->appendChild(
+                               SingleSelectionFormField::create($templateName . '_eventName')
+                                       ->attribute('data-tag', 'eventname')
+                                       ->label('wcf.acp.pip.templateListener.eventName')
+                                       ->description('wcf.acp.pip.templateListener.eventName.description')
+                                       ->required()
+                                       ->options(array_combine($events, $events))
+                                       ->addDependency(
+                                               ValueFormFieldDependency::create('templateName')
+                                                       ->field($form->getNodeById('templateName'))
+                                                       ->values([$templateName])
+                                       )
+                       );
+               }
+               
+               foreach ($acpTemplateEvents as $templateName => $events) {
+                       $form->getNodeById('data')->appendChild(
+                               SingleSelectionFormField::create('acp_' . $templateName . '_eventName')
+                                       ->attribute('data-tag', 'eventname')
+                                       ->label('wcf.acp.pip.templateListener.eventName')
+                                       ->description('wcf.acp.pip.templateListener.eventName.description')
+                                       ->required()
+                                       ->options(array_combine($events, $events))
+                                       ->addDependency(
+                                               ValueFormFieldDependency::create('acpTemplateName')
+                                                       ->field($form->getNodeById('acpTemplateName'))
+                                                       ->values([$templateName])
+                                       )
+                       );
+               }
+               
+               $form->getNodeById('data')->appendChildren([
+                       SingleSelectionFormField::create('environment')
+                               ->label('wcf.acp.pip.templateListener.environment')
+                               ->description('wcf.acp.pip.templateListener.environment.description')
+                               ->required()
+                               ->options([
+                                       'admin' => 'admin',
+                                       'user' => 'user'
+                               ])
+                               ->value('user')
+                               ->addValidator(new FormFieldValidator('uniqueness', function(SingleSelectionFormField $formField) {
+                                       $listenerList = new TemplateListenerList();
+                                       $listenerList->getConditionBuilder()->add(
+                                               'name = ?',
+                                               [$formField->getDocument()->getNodeById('name')->getSaveValue()]
+                                       );
+                                       
+                                       if ($formField->getSaveValue() === 'admin') {
+                                               $templateName = $formField->getDocument()->getNodeById('acpTemplateName')->getSaveValue();
+                                               $eventName = $formField->getDocument()->getNodeById('acp_' . $templateName . '_eventName')->getSaveValue();
+                                       }
+                                       else {
+                                               $templateName = $formField->getDocument()->getNodeById('templateName')->getSaveValue();
+                                               $eventName = $formField->getDocument()->getNodeById($templateName . '_eventName')->getSaveValue();
+                                       }
+                                       
+                                       $listenerList->getConditionBuilder()->add('templateName = ?', [$templateName]);
+                                       
+                                       $listenerList->getConditionBuilder()->add('eventName = ?', [$eventName]);
+                                       $listenerList->getConditionBuilder()->add('environment = ?', [$formField->getSaveValue()]);
+                               })),
+                       
+                       // TODO: use field with code support
+                       MultilineTextFormField::create('templateCode')
+                               ->attribute('data-tag', 'templatecode')
+                               ->label('wcf.acp.pip.templateListener.templateCode')
+                               ->description('wcf.acp.pip.templateListener.templateCode.description')
+                               ->required()
+               ]);
+               
+               $form->getNodeById('templateName')->addDependency(
+                       ValueFormFieldDependency::create('environment')
+                               ->field($form->getNodeById('environment'))
+                               ->values(['user'])
+               );
+               $form->getNodeById('acpTemplateName')->addDependency(
+                       ValueFormFieldDependency::create('environment')
+                               ->field($form->getNodeById('environment'))
+                               ->values(['admin'])
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               return [
+                       'environment' => $element->getElementsByTagName('environment')->item(0)->nodeValue,
+                       'eventName' => $element->getElementsByTagName('eventname')->item(0)->nodeValue,
+                       'name' => $element->getAttribute('name'),
+                       'packageID' => $this->installation->getPackage()->packageID,
+                       'templateCode' => $element->getElementsByTagName('templatecode')->item(0)->nodeValue,
+                       'templateName' => $element->getElementsByTagName('templatename')->item(0)->nodeValue
+               ];
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return sha1(
+                       $element->getElementsByTagName('templatename')->item(0)->nodeValue . '/' .
+                       $element->getElementsByTagName('eventname')->item(0)->nodeValue . '/' .
+                       $element->getElementsByTagName('environment')->item(0)->nodeValue . '/' .
+                       $element->getAttribute('name')
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'name' => 'wcf.acp.pip.templateListener.name',
+                       'templateName' => 'wcf.acp.pip.templateListener.templateName',
+                       'eventName' => 'wcf.acp.pip.templateListener.eventName',
+                       'environment' => 'wcf.acp.pip.templateListener.environment'
+               ]);
+               
+               /** @var \DOMElement $element */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $entryList->addEntry($this->getElementIdentifier($element), array_intersect_key($this->getElementData($element), $entryList->getKeys()));
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
+                       $templateName1 = $element1->getElementsByTagName('templatename')->item(0)->nodeValue;
+                       $templateName2 = $element2->getElementsByTagName('templatename')->item(0)->nodeValue;
+                       
+                       if ($templateName1 !== $templateName2) {
+                               return strcmp($templateName1, $templateName2);
+                       }
+                       
+                       $eventName1 = $element1->getElementsByTagName('eventname')->item(0)->nodeValue;
+                       $eventName2 = $element2->getElementsByTagName('eventname')->item(0)->nodeValue;
+                       
+                       if ($eventName1 !== $eventName2) {
+                               return strcmp($eventName1, $eventName2);
+                       }
+                       
+                       return strcmp(
+                               $element1->getElementsByTagName('environment')->item(0)->nodeValue,
+                               $element2->getElementsByTagName('environment')->item(0)->nodeValue
+                       );
+               };
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               $listener = $document->createElement($this->tagName);
+               $listener->setAttribute('name', $form->getNodeById('name')->getSaveValue());
+               
+               $environment = $form->getNodeById('environment')->getSaveValue();
+               if ($environment === 'user') {
+                       $templateName = $form->getNodeById('templateName')->getSaveValue();
+                       
+                       $listener->appendChild($document->createElement('templatename', $templateName));
+                       $listener->appendChild($document->createElement('eventname', $form->getNodeById($templateName . '_eventName')->getSaveValue()));
+               }
+               else {
+                       $templateName = $form->getNodeById('acpTemplateName')->getSaveValue();
+                       
+                       $listener->appendChild($document->createElement('templatename', $templateName));
+                       $listener->appendChild($document->createElement('eventname', $form->getNodeById('acp_' . $templateName . '_eventName')->getSaveValue()));
+               }
+               $listener->appendChild($document->createElement('templatecode', '<![CDATA[' . StringUtil::unifyNewlines(StringUtil::escapeCDATA($form->getNodeById('templateCode')->getSaveValue())) . ']]>'));
+               $listener->appendChild($document->createElement('environment', $environment));
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($listener);
+               
+               return $listener;
+       }
 }
index b09dc58191a645cbaf6a90a78d2540dbe9da95db..0927235f527dd58113a16ae9a6417368c3636ace 100644 (file)
@@ -1882,6 +1882,18 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pip.userNotificationEvent.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to see the event in their notification settings.]]></item>
                <item name="wcf.acp.pip.userNotificationEvent.presetMailNotificationType"><![CDATA[Default Mail Notification Type]]></item>
                <item name="wcf.acp.pip.userNotificationEvent.presetMailNotificationType.description"><![CDATA[If a notification type is selected, users’ mail setting for this event will have the selected value by default.]]></item>
+               <item name="wcf.acp.pip.templateListener.environment"><![CDATA[Environment]]></item>
+               <item name="wcf.acp.pip.templateListener.environment.description"><![CDATA[The environment determines whether the template listener is executed in the frontend (<code>user</code>) or the ACP (<code>admin</code>).]]></item>
+               <item name="wcf.acp.pip.templateListener.eventName"><![CDATA[Event]]></item>
+               <item name="wcf.acp.pip.templateListener.eventName.description"><![CDATA[The selected event determines at which location in the template the template listeners’ code is inserted.]]></item>
+               <item name="wcf.acp.pip.templateListener.eventName.error.nonExistent"><![CDATA[The is no event with the given name in the selected template.]]></item>
+               <item name="wcf.acp.pip.templateListener.name"><![CDATA[Template Listener Name]]></item>
+               <item name="wcf.acp.pip.templateListener.name.description"><![CDATA[The name of the event may only contain letters and must begin with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.templateListener.name.error.format"><![CDATA[The entered name is invalid.]]></item>
+               <item name="wcf.acp.pip.templateListener.templateCode"><![CDATA[Template Code]]></item>
+               <item name="wcf.acp.pip.templateListener.templateCode.description"><![CDATA[The entered template code is inserted at the location of the specified template event.]]></item>
+               <item name="wcf.acp.pip.templateListener.templateName"><![CDATA[Template]]></item>
+               <item name="wcf.acp.pip.templateListener.templateName.description"><![CDATA[The template listeners’ code will be inserted into the selected template.]]></item>
        </category>
        
        <category name="wcf.acp.rebuildData">