Add delete buttons for devtools pip entries
authorMatthias Schmidt <gravatronics@live.com>
Fri, 14 Dec 2018 18:10:19 +0000 (19:10 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Fri, 14 Dec 2018 18:10:19 +0000 (19:10 +0100)
If the package installation plugin supports delete instructions, the import instruction can also be converted into a delete instruction.

See #2545

18 files changed:
wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryList.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Pip/Entry/List.js [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipEntryListPage.class.php
wcfsetup/install/files/lib/data/devtools/project/DevtoolsProjectAction.class.php
wcfsetup/install/files/lib/system/devtools/pip/IGuiPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/devtools/pip/TMultiXmlGuiPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ACLOptionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ClipboardActionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/CoreObjectPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ObjectTypeDefinitionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ObjectTypePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/TemplateListenerPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/UserNotificationEventPackageInstallationPlugin.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 790f79ab7d44534d8b97607b5dbb7116d44ab244..b3fbc352debe430b3e4c52e04cf8f787833432b2 100644 (file)
@@ -59,8 +59,8 @@
 {/hascontent}
 
 {if !$entryList->getEntries()|empty}
-       <div class="section tabularBox jsShowOnlyMatches" id="syncPipMatches">
-               <table class="table">
+       <div class="section tabularBox jsShowOnlyMatches">
+               <table class="table" id="devtoolsProjectPipEntryList">
                        <thead>
                                <tr>
                                        {foreach from=$entryList->getKeys() item=languageItem name=entryListKeys}
                        
                        <tbody>
                                {foreach from=$entryList->getEntries($startIndex-1, $itemsPerPage) key=identifier item=entry}
-                                       <tr>
-                                               <td class="columnIcon"><a href="{link controller='DevtoolsProjectPipEntryEdit' id=$project->projectID pip=$pip identifier=$identifier entryType=$entryType}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a></td>
+                                       <tr class="jsPipEntryRow" data-identifier="{@$identifier}">
+                                               <td class="columnIcon">
+                                                       <a href="{link controller='DevtoolsProjectPipEntryEdit' id=$project->projectID pip=$pip identifier=$identifier entryType=$entryType}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
+                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}"></span>
+                                               </td>
                                                {foreach from=$entryList->getKeys() key=key item=languageItem}
                                                        <td>{$entry[$key]}</td>
                                                {/foreach}
        <p class="info">{lang}wcf.global.noItems{/lang}</p>
 {/if}
 
+<script data-relocate="true">
+       require(['Language', 'WoltLabSuite/Core/Acp/Ui/Devtools/Project/Pip/Entry/List'], function(Language, DevtoolsProjectPipEntryList) {
+               Language.addObject({
+                       'wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction': '{lang}wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction{/lang}',
+                       'wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction.description': '{lang}wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction.description{/lang}',
+                       'wcf.acp.devtools.project.pip.entry.delete.confirmMessage': '{lang}wcf.acp.devtools.project.pip.entry.delete.confirmMessage{/lang}'
+               });
+               
+               new DevtoolsProjectPipEntryList('devtoolsProjectPipEntryList', '{@$project->projectID}', '{@$pip}', '{@$entryType}', {if $pipObject->getPip()->supportsDeleteInstruction()}true{else}false{/if});
+       });
+</script>
+
 {include file='footer'}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Pip/Entry/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Pip/Entry/List.js
new file mode 100644 (file)
index 0000000..cbec42d
--- /dev/null
@@ -0,0 +1,141 @@
+/**
+ * Handles the JavaScript part of the devtools project pip entry list.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Devtools/Project/Pip/Entry/List
+ */
+define([
+       'Ajax',
+       'Language',
+       'Ui/Confirmation',
+       'Ui/Notification'
+], function (
+       Ajax,
+       Language,
+       UiConfirmation,
+       UiNotification
+) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function DevtoolsProjectPipEntryList(tableId, projectId, pip, entryType, supportsDeleteInstruction) {
+               this.init(tableId, projectId, pip, entryType, supportsDeleteInstruction);
+       };
+       DevtoolsProjectPipEntryList.prototype = {
+               /**
+                * Initializes the devtools project pip entry list handler.
+                * 
+                * @param       {string}        tableId                         id of the table containing the pip entries
+                * @param       {integer}       projectId                       id of the project the listed pip entries belong to
+                * @param       {string}        pip                             name of the pip the listed entries belong to
+                * @param       {string}        entryType                       type of the listed entries
+                * @param       {boolean}       supportsDeleteInstruction       is `true` if the pip supports `<delete>`
+                */
+               init: function(tableId, projectId, pip, entryType, supportsDeleteInstruction) {
+                       this._table = elById(tableId);
+                       if (this._table === null) {
+                               throw new Error("Unknown element with id '" + tableId + "'.");
+                       }
+                       if (this._table.tagName !== 'TABLE') {
+                               throw new Error("Element with id '" + tableId + "' is no table.");
+                       }
+                       
+                       this._projectId = projectId;
+                       this._pip = pip;
+                       this._entryType = entryType;
+                       this._supportsDeleteInstruction = supportsDeleteInstruction;
+                       
+                       elBySelAll('.jsDeleteButton', this._table, function(deleteButton) {
+                               deleteButton.addEventListener('click', this._confirmDeletePipEntry.bind(this));
+                       }.bind(this));
+               },
+               
+               /**
+                * Returns the data used to setup the AJAX request object.
+                * 
+                * @return      {object}        setup data
+                */
+               _ajaxSetup: function () {
+                       return {
+                               data: {
+                                       actionName: 'deletePipEntry',
+                                       className: 'wcf\\data\\devtools\\project\\DevtoolsProjectAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX request.
+                * 
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       UiNotification.show();
+                       
+                       elBySelAll('tbody > tr', this._table, function(pipEntry) {
+                               if (elData(pipEntry, 'identifier') === data.returnValues.identifier) {
+                                       elRemove(pipEntry);
+                               }
+                       }.bind(this));
+                       
+                       // reload page if table is empty
+                       if (elBySelAll('tbody > tr', this._table).length === 0) {
+                               window.location.reload();
+                       }
+               },
+               
+               /**
+                * Shows the confirmation dialog when deleting a pip entry.
+                * 
+                * @param       {Event}         event
+                */
+               _confirmDeletePipEntry: function(event) {
+                       var pipEntry = event.currentTarget.closest('tr');
+                       
+                       UiConfirmation.show({
+                               confirm: this._deletePipEntry.bind(this),
+                               message: Language.get('wcf.acp.devtools.project.pip.entry.delete.confirmMessage'),
+                               template: this._supportsDeleteInstruction ? '' +
+                                       '<dl>' +
+                                       '       <dt></dt>' +
+                                       '       <dd>' +
+                                       '               <label><input type="checkbox" name="addDeleteInstruction" checked> ' + Language.get('wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction') + '</label>' + 
+                                       '               <small>' + Language.get('wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction.description') + '</small>' +
+                                       '       </dd>' +
+                                       '</dl>' : '',
+                               parameters: {
+                                       pipEntry: pipEntry
+                               }
+                       });
+               },
+               
+               /**
+                * Sends the AJAX request to delete a pip entry.
+                * 
+                * @param       {object}        parameters      contains the deleted pip entry element
+                * @param       {HTMLElement}   content         confirmation dialog containing the `addDeleteInstruction` instruction
+                */
+               _deletePipEntry: function(parameters, content) {
+                       var addDeleteInstruction = false;
+                       if (this._supportsDeleteInstruction) {
+                               addDeleteInstruction = ~~elBySel('input[name=addDeleteInstruction]', content).checked;
+                       }
+                       
+                       Ajax.api(this, {
+                               objectIDs: [this._projectId],
+                               parameters: {
+                                       addDeleteInstruction: addDeleteInstruction,
+                                       entryType: this._entryType,
+                                       identifier: elData(parameters.pipEntry, 'identifier'),
+                                       pip: this._pip
+                               }
+                       });
+               }
+       };
+       
+       return DevtoolsProjectPipEntryList;
+});
index 1be94b722e389f287247ec256c21a09469c3d24f..9039a20fbfbf7410befaa3415101f3b366d278ba 100644 (file)
@@ -230,6 +230,7 @@ class DevtoolsProjectPipEntryListPage extends AbstractPage {
                        'pageNo' => $this->pageNo,
                        'pages' => $this->pages,
                        'pip' => $this->pip,
+                       'pipObject' => $this->pipObject,
                        'project' => $this->project,
                        'startIndex' => $this->startIndex
                ]);
index 342b67a5959eaf8f39c6d4a26266c5ba28cd6f4d..db315e0987ab03f91f13efdbabc6ce5dde49ea23 100644 (file)
@@ -3,6 +3,7 @@ namespace wcf\data\devtools\project;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\data\package\installation\queue\PackageInstallationQueue;
 use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
+use wcf\system\devtools\pip\DevtoolsPip;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\WCF;
 use wcf\util\DirectoryUtil;
@@ -29,7 +30,7 @@ class DevtoolsProjectAction extends AbstractDatabaseObjectAction {
        /**
         * @inheritDoc
         */
-       protected $requireACP = ['delete', 'installPackage'];
+       protected $requireACP = ['delete', 'deletePipEntry', 'installPackage'];
        
        /**
         * @inheritDoc
@@ -43,6 +44,13 @@ class DevtoolsProjectAction extends AbstractDatabaseObjectAction {
         */
        public $queue;
        
+       /**
+        * package installation plugin the deleted entry belongs to
+        * @var DevtoolsPip
+        * @since       3.2
+        */
+       protected $pip;
+       
        /**
         * @inheritDoc
         * @return      DevtoolsProject
@@ -199,4 +207,73 @@ class DevtoolsProjectAction extends AbstractDatabaseObjectAction {
                        'queueID' => $this->queue->queueID
                ];
        }
+       
+       /**
+        * Checks if the `deletePipEntry` action can be executed.
+        * 
+        * @throws      IllegalLinkException
+        * @since       3.2
+        */
+       public function validateDeletePipEntry() {
+               if (!ENABLE_DEVELOPER_TOOLS) {
+                       throw new IllegalLinkException();
+               }
+               
+               WCF::getSession()->checkPermissions(['admin.configuration.package.canInstallPackage']);
+               
+               $project = $this->getSingleObject();
+               
+               // read and validate pip
+               $this->readString('pip');
+               $filteredPips = array_filter($project->getPips(), function(DevtoolsPip $pip) {
+                       return $pip->pluginName === $this->parameters['pip'];
+               });
+               if (count($filteredPips) === 1) {
+                       $this->pip = reset($filteredPips);
+               }
+               else {
+                       throw new IllegalLinkException();
+               }
+               
+               if (!$this->pip->supportsGui()) {
+                       throw new IllegalLinkException();
+               }
+               
+               // read and validate entry type
+               $this->readString('entryType', true);
+               if ($this->parameters['entryType'] !== '') {
+                       try {
+                               $this->pip->getPip()->setEntryType($this->parameters['entryType']);
+                       }
+                       catch (\InvalidArgumentException $e) {
+                               throw new IllegalLinkException();
+                       }
+               }
+               else if (!empty($this->pip->getPip()->getEntryTypes())) {
+                       throw new IllegalLinkException();
+               }
+               
+               // read and validate identifier
+               $this->readString('identifier');
+               $entryList = $this->pip->getPip()->getEntryList();
+               if (!$entryList->hasEntry($this->parameters['identifier'])) {
+                       throw new IllegalLinkException();
+               }
+               
+               $this->readBoolean('addDeleteInstruction', true);
+       }
+       
+       /**
+        * Deletes a specific pip entry.
+        *
+        * @return      string[]        identifier of the deleted pip entry
+        * @since       3.2
+        */
+       public function deletePipEntry() {
+               $this->pip->getPip()->deleteEntry($this->parameters['identifier'], $this->parameters['addDeleteInstruction']);
+               
+               return [
+                       'identifier' => $this->parameters['identifier']
+               ];
+       }
 }
index 71b67750ca5414319444e2bf898195a128723084..17aeeb518aea7121505df928db9930a000f11f25 100644 (file)
@@ -21,6 +21,16 @@ interface IGuiPackageInstallationPlugin extends IIdempotentPackageInstallationPl
         */
        public function addEntry(IFormDocument $form);
        
+       /**
+        * Deletes an existing pip entry and removes it from database.
+        * 
+        * @param       string          $identifier             identifier of deleted entry
+        * @param       bool            $addDeleteInstruction   if `true`, an explicit delete instruction is added
+        * 
+        * @throws      \InvalidArgumentException       if no such entry exists or delete instruction should be added but is not supported
+        */
+       public function deleteEntry($identifier, $addDeleteInstruction);
+       
        /**
         * Edits the entry of this pip with the given identifier based on the data
         * provided by the given form and returns the new identifier of the entry
@@ -94,4 +104,12 @@ interface IGuiPackageInstallationPlugin extends IIdempotentPackageInstallationPl
         * @throws      \InvalidArgumentException       if the given entry type is invalid (see `getEntryTypes()` method) 
         */
        public function setEntryType($entryType);
+       
+       /**
+        * Returns `true` if this package installation plugin supports delete
+        * instructions.
+        * 
+        * @return      boolean
+        */
+       public function supportsDeleteInstruction();
 }
index 4e2a2af6f8801df2bcc0d88b303a6de15340ad4d..2b6a9aea21ef5d5222f0f0d5d6457ed9601bbb83 100644 (file)
@@ -4,6 +4,7 @@ use wcf\system\form\builder\field\IFormField;
 use wcf\system\form\builder\IFormDocument;
 use wcf\system\form\builder\IFormNode;
 use wcf\system\package\PackageInstallationDispatcher;
+use wcf\system\WCF;
 use wcf\util\DOMUtil;
 use wcf\util\XML;
 
@@ -204,4 +205,51 @@ trait TMultiXmlGuiPackageInstallationPlugin {
                
                return $missingElements !== count($xmls);
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public function deleteEntry($identifier, $addDeleteInstruction) {
+               foreach ($this->getProjectXmls() as $xml) {
+                       $element = $this->getElementByIdentifier($xml, $identifier);
+                       
+                       if ($element === null) {
+                               throw new \InvalidArgumentException("Unknown entry with identifier '{$identifier}'.");
+                       }
+                       
+                       if (!$this->supportsDeleteInstruction() && $addDeleteInstruction) {
+                               throw new \InvalidArgumentException("This package installation plugin does not support delete instructions.");
+                       }
+                       
+                       $this->deleteObject($element);
+                       
+                       if ($addDeleteInstruction) {
+                               $this->addDeleteElement($element);
+                       }
+                       
+                       $document = $element->ownerDocument;
+                       
+                       DOMUtil::removeNode($element);
+                       
+                       $deleteFile = $this->sanitizeXmlFileAfterDeleteEntry($document);
+                       
+                       if ($deleteFile) {
+                               unlink($xml->getPath());
+                       }
+                       else {
+                               $xml->write($xml->getPath());
+                       }
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $sql = "DELETE FROM     wcf" . WCF_N . "_language_item
+                       WHERE           languageItem = ?
+                                       AND packageID = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([$element->getAttribute('name')]);
+       }
 }
index a1c464a62bc87aff8d59577b14567053dc76414f..7251177d432aff9382eca91f103e2b2d5c7b375c 100644 (file)
@@ -26,6 +26,7 @@ use wcf\util\XML;
  * 
  * @property   PackageInstallationDispatcher|DevtoolsPackageInstallationDispatcher     $installation
  * @mixin      AbstractXMLPackageInstallationPlugin
+ * @mixin      IGuiPackageInstallationPlugin
  */
 trait TXmlGuiPackageInstallationPlugin {
        /**
@@ -40,6 +41,26 @@ trait TXmlGuiPackageInstallationPlugin {
         */
        protected $entryType;
        
+       /**
+        * Adds a delete element to the xml file based on the given installation
+        * element.
+        * 
+        * @param       \DOMElement     $element        installation element
+        */
+       protected function addDeleteElement(\DOMElement $element) {
+               $document = $element->ownerDocument;
+               
+               $data = $document->getElementsByTagName('data')->item(0);
+               $delete = $data->getElementsByTagName('delete')->item(0);
+               
+               if ($delete === null) {
+                       $delete = $document->createElement('delete');
+                       $data->appendChild($delete);
+               }
+               
+               $delete->appendChild($document->importNode($this->prepareDeleteXmlElement($element)));
+       }
+       
        /**
         * Adds a new entry of this pip based on the data provided by the given
         * form.
@@ -157,6 +178,95 @@ trait TXmlGuiPackageInstallationPlugin {
                return $data['element'];
        }
        
+       /**
+        * Deletes the entry of this pip with the given identifier and, based
+        * on the value of `$addDeleteInstruction`, adds a delete instruction.
+        * 
+        * @param       string          $identifier
+        * @param       boolean         $addDeleteInstruction
+        */
+       public function deleteEntry($identifier, $addDeleteInstruction) {
+               $xml = $this->getProjectXml();
+               
+               $element = $this->getElementByIdentifier($xml, $identifier);
+               
+               if ($element === null) {
+                       throw new \InvalidArgumentException("Unknown entry with identifier '{$identifier}'.");
+               }
+               
+               if (!$this->supportsDeleteInstruction() && $addDeleteInstruction) {
+                       throw new \InvalidArgumentException("This package installation plugin does not support delete instructions.");
+               }
+               
+               $this->deleteObject($element);
+               
+               if ($addDeleteInstruction) {
+                       $this->addDeleteElement($element);
+               }
+               
+               $document = $element->ownerDocument;
+               
+               DOMUtil::removeNode($element);
+               
+               /** @var DevtoolsProject $project */
+               $project = $this->installation->getProject();
+               
+               $deleteFile = $this->sanitizeXmlFileAfterDeleteEntry($document);
+               
+               if ($deleteFile) {
+                       unlink($this->getXmlFileLocation($project));
+               }
+               else {
+                       $xml->write($this->getXmlFileLocation($project));
+               }
+       }
+       
+       /**
+        * Sanitizes the given document after an entry has been deleted by removing
+        * empty parent elements and returns `true` if the xml file should be deleted
+        * because there is no content left.
+        * 
+        * @param       \DOMDocument    $document       sanitized document
+        * @return      boolean
+        */
+       protected function sanitizeXmlFileAfterDeleteEntry(\DOMDocument $document) {
+               $data = $document->getElementsByTagName('data')->item(0);
+               $import = $data->getElementsByTagName('import')->item(0);
+               
+               // remove empty import node
+               if ($import->childNodes->length === 0) {
+                       DOMUtil::removeNode($import);
+                       
+                       // delete file if empty
+                       if ($data->childNodes->length === 0) {
+                               return true;
+                       }
+               }
+               
+               return false;
+       }
+       
+       /**
+        * Deletes the given element from database.
+        * 
+        * @param       \DOMElement     $element
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $name = $element->getAttribute('name');
+               if ($name !== '') {
+                       $this->handleDelete([['attributes' => ['name' => $name]]]);
+               }
+               else {
+                       $identifier = $element->getAttribute('identifier');
+                       if ($identifier !== '') {
+                               $this->handleDelete([['attributes' => ['identifier' => $identifier]]]);
+                       }
+                       else {
+                               throw new \LogicException("Cannot delete object using the default implementations.");
+                       }
+               }
+       }
+       
        /**
         * Edits the entry of this pip with the given identifier based on the data
         * provided by the given form and returns the new identifier of the entry
@@ -421,6 +531,36 @@ XML;
                EventHandler::getInstance()->fireAction($this, 'afterAddFormFields', $eventParameters);
        }
        
+       /**
+        * Returns a delete xml element based on the given import element.
+        * 
+        * @param       \DOMElement     $element
+        * @return      \DOMElement
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               if (!$this->supportsDeleteInstruction()) {
+                       throw new \BadMethodCallException("Cannot prepare delete xml element if delete instructions are not supported.");
+               }
+               
+               $name = $element->getAttribute('name');
+               if ($name !== '') {
+                       $element = $element->ownerDocument->createElement($this->tagName);
+                       $element->setAttribute('name', $name);
+               }
+               else {
+                       $identifier = $element->getAttribute('identifier');
+                       if ($identifier !== '') {
+                               $element = $element->ownerDocument->createElement($this->tagName);
+                               $element->setAttribute('identifier', $identifier);
+                       }
+                       else {
+                               throw new \LogicException("Cannot prepare delete xml element using the default implementations.");
+                       }
+               }
+               
+               return $element;
+       }
+       
        /**
         * Saves an object represented by an XML element in the database by either
         * creating a new element (if `$oldElement = null`) or updating an existing
@@ -542,4 +682,14 @@ XML;
                
                $this->entryType = $entryType;
        }
+       
+       /**
+        * Returns `true` if this package installation plugin supports delete
+        * instructions.
+        * 
+        * @return      boolean
+        */
+       public function supportsDeleteInstruction() {
+               return true;
+       }
 }
index 52ac209d583e7aa2e1de55cc9b4ae1e4d7c9fa8c..1d203417b7cd693eecb0520d504fa0decf9c9924 100644 (file)
@@ -18,6 +18,7 @@ use wcf\system\form\builder\field\validation\FormFieldValidator;
 use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
 use wcf\system\form\builder\IFormDocument;
 use wcf\system\WCF;
+use wcf\util\DOMUtil;
 
 /**
  * This PIP installs, updates or deletes acl options.
@@ -611,4 +612,97 @@ class ACLOptionPackageInstallationPlugin extends AbstractOptionPackageInstallati
                
                throw new \LogicException('Unreachable');
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $deleteElement = parent::prepareDeleteXmlElement($element);
+               
+               $deleteElement->appendChild($element->ownerDocument->createElement(
+                       'objecttype',
+                       $element->getElementsByTagName('objecttype')->item(0)->nodeValue
+               ));
+               
+               return $deleteElement;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $name = $element->getAttribute('name');
+               $objectType = $element->getElementsByTagName('objecttype')->item(0)->nodeValue;
+               
+               switch ($this->entryType) {
+                       case 'categories':
+                               // also delete options
+                               $sql = "DELETE FROM     " . $this->application . WCF_N . "_" . $this->tableName . "
+                                       WHERE           categoryName = ?
+                                                       AND objectTypeID = ?
+                                                       AND packageID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute([
+                                       $name,
+                                       $this->getObjectTypeID($objectType),
+                                       $this->installation->getPackageID()
+                               ]);
+                               
+                               $sql = "DELETE FROM     " . $this->application . WCF_N . "_" . $this->tableName . "_category
+                                       WHERE           categoryName = ?
+                                                       AND objectTypeID = ?
+                                                       AND packageID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute([
+                                       $name,
+                                       $this->getObjectTypeID($objectType),
+                                       $this->installation->getPackageID()
+                               ]);
+                               
+                               break;
+                       
+                       case 'options':
+                               $sql = "DELETE FROM     ".$this->application . WCF_N . "_". $this->tableName ."
+                                       WHERE           optionName = ?
+                                                       AND objectTypeID = ?
+                                                       AND packageID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute([
+                                       $name,
+                                       $this->getObjectTypeID($objectType),
+                                       $this->installation->getPackageID()
+                               ]);
+                               
+                               break;
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function addDeleteElement(\DOMElement $element) {
+               $this->defaultAddDeleteElement($element);
+               
+               // remove install instructions for options in delete categories;
+               // explicitly adding delete instructions for these options is not
+               // necessary as they will be deleted automatically 
+               if ($this->entryType === 'categories') {
+                       $categoryName = $element->getAttribute('name');
+                       
+                       $xpath = new \DOMXPath($element->ownerDocument);
+                       $xpath->registerNamespace('ns', $element->ownerDocument->documentElement->getAttribute('xmlns'));
+                       
+                       $options = $xpath->query('/ns:data/ns:import/ns:options')->item(0);
+                       
+                       /** @var \DOMElement $option */
+                       foreach (DOMUtil::getElements($options, 'option') as $option) {
+                               if ($option->getElementsByTagName('categoryname')->item(0)->nodeValue === $categoryName) {
+                                       DOMUtil::removeNode($option);
+                               }
+                       }
+               }
+       }
 }
index 7da4df461f271d52fecb5b04d37850b7e7e6242c..9f5f5bcfe6d73fa6f4a9630de0ba08c3649fc29f 100644 (file)
@@ -48,7 +48,10 @@ abstract class AbstractOptionPackageInstallationPlugin extends AbstractXMLPackag
        // provide the default implementation to ensure backwards compatibility
        // with third-party packages containing classes that extend this abstract
        // class
-       use TXmlGuiPackageInstallationPlugin;
+       use TXmlGuiPackageInstallationPlugin {
+               addDeleteElement as defaultAddDeleteElement;
+               sanitizeXmlFileAfterDeleteEntry as defaultSanitizeXmlFileAfterDeleteEntry;
+       }
        
        /**
         * list of option types with i18n support
@@ -1117,4 +1120,121 @@ abstract class AbstractOptionPackageInstallationPlugin extends AbstractXMLPackag
                
                return array_combine($options, $options);
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $elementName = 'option';
+               
+               if ($this->entryType === 'categories') {
+                       $elementName .= 'category';
+               }
+               
+               $deleteElement = $element->ownerDocument->createElement($elementName);
+               $deleteElement->setAttribute('name', $element->getAttribute('name'));
+               
+               return $deleteElement;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $name = $element->getAttribute('name');
+               
+               switch ($this->entryType) {
+                       case 'categories':
+                               // also delete options
+                               $sql = "DELETE FROM     " . $this->application . WCF_N . "_" . $this->tableName . "
+                                       WHERE           categoryName = ?
+                                                       AND packageID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute([
+                                       $name,
+                                       $this->installation->getPackageID()
+                               ]);
+                               
+                               $sql = "DELETE FROM     " . $this->application . WCF_N . "_" . $this->tableName . "_category
+                                       WHERE           categoryName = ?
+                                                       AND packageID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute([
+                                       $name,
+                                       $this->installation->getPackageID()
+                               ]);
+                               
+                               break;
+                               
+                       case 'options':
+                               $sql = "DELETE FROM     ".$this->application . WCF_N . "_". $this->tableName ."
+                                       WHERE           optionName = ?
+                                                       AND packageID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute([
+                                       $name,
+                                       $this->installation->getPackageID()
+                               ]);
+                               
+                               break;
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function addDeleteElement(\DOMElement $element) {
+               $this->defaultAddDeleteElement($element);
+               
+               // remove install instructions for options in delete categories;
+               // explicitly adding delete instructions for these options is not
+               // necessary as they will be deleted automatically 
+               if ($this->entryType === 'categories') {
+                       $categoryName = $element->getAttribute('name');
+                       $objectType = $element->getElementsByTagName('objecttype')->item(0)->nodeValue;
+                       
+                       $xpath = new \DOMXPath($element->ownerDocument);
+                       $xpath->registerNamespace('ns', $element->ownerDocument->documentElement->getAttribute('xmlns'));
+                       
+                       $options = $xpath->query('/ns:data/ns:import/ns:options')->item(0);
+                       
+                       /** @var \DOMElement $option */
+                       foreach (DOMUtil::getElements($options, 'option') as $option) {
+                               $optionCategoryName = $option->getElementsByTagName('categoryname')->item(0);
+                               
+                               if ($optionCategoryName !== null) {
+                                       $optionObjectType = $option->getElementsByTagName('objectType')->item(0);
+                                       if ($optionCategoryName->nodeValue === $categoryName && $optionObjectType->nodeValue === $objectType) {
+                                               DOMUtil::removeNode($option);
+                                       }
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sanitizeXmlFileAfterDeleteEntry(\DOMDocument $document) {
+               $xpath = new \DOMXPath($document);
+               $xpath->registerNamespace('ns', $document->documentElement->getAttribute('xmlns'));
+               
+               // remove empty categories and options elements
+               foreach (['options'] as $type) {
+                       $element = $xpath->query('/ns:data/ns:import/ns:' . $type)->item(0);
+                       
+                       // remove empty options node
+                       if ($element !== null) {
+                               if ($element->childNodes->length === 0) {
+                                       DOMUtil::removeNode($element);
+                               }
+                       }
+               }
+               
+               return $this->defaultSanitizeXmlFileAfterDeleteEntry($document);
+       }
 }
index a9bbd5a6db4ff9ca375d07f7873922421ed98b7c..4d904fd7c247c1f26593a199d355b44cf14a4efb 100644 (file)
@@ -331,4 +331,33 @@ class ClipboardActionPackageInstallationPlugin extends AbstractXMLPackageInstall
                
                return $clipboardAction;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $clipboardAction = $element->ownerDocument->createElement($this->tagName);
+               $clipboardAction->setAttribute('name', $element->getAttribute('name'));
+               
+               $clipboardAction->appendChild($element->ownerDocument->createElement(
+                       'actionclassname',
+                       $element->getElementsByTagName('actionclassname')->item(0)->nodeValue
+               ));
+               
+               return $clipboardAction;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $actionClassName = $element->getElementsByTagName('actionclassname')->item(0)->nodeValue;
+               
+               $this->handleDelete([[
+                       'attributes' => ['name' => $element->getAttribute('name')],
+                       'elements' => ['actionclassname' => $actionClassName]
+               ]]);
+       }
 }
index d0ac593ba6a3aea5e30a965af5c14820a80d5a13..c7c44a646e4753a3b73ff1d002456cdcda6ac0bd 100644 (file)
@@ -168,4 +168,28 @@ class CoreObjectPackageInstallationPlugin extends AbstractXMLPackageInstallation
                
                return $coreObject;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $coreObject = $element->ownerDocument->createElement($this->tagName);
+               $coreObject->setAttribute(
+                       'name',
+                       $element->getElementsByTagName('objectname')->item(0)->nodeValue
+               );
+               
+               return $coreObject;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $name = $element->getElementsByTagName('objectname')->item(0)->nodeValue;
+               
+               $this->handleDelete([['attributes' => ['name' => $name]]]);
+       }
 }
index dabc6df0dffde31f94b1dd639fb0c15d8ad79850..4bf784139e06b1fe749773d4ba9b57f3ce82dd0b 100644 (file)
@@ -825,4 +825,43 @@ XML;
                
                return $newElement;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $sql = "DELETE FROM     wcf" . WCF_N . "_language_item
+                       WHERE           languageItem = ?
+                                       AND packageID = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([
+                       $element->getAttribute('name'),
+                       $this->installation->getPackageID()
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function supportsDeleteInstruction() {
+               return false;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sanitizeXmlFileAfterDeleteEntry(\DOMDocument $document) {
+               $language = $document->getElementsByTagName('language')->item(0);
+               
+               foreach (DOMUtil::getElements($language, 'category') as $category) {
+                       if ($category->childNodes->length === 0) {
+                               DOMUtil::removeNode($category);
+                       }
+               }
+               
+               return $language->childNodes->length === 0;
+       }
 }
index 6d412dc37312d6ffc8bf6900124a805f53a92120..1874e242b9ccc2fcfa0959ce133bf7d6a6729092 100644 (file)
@@ -199,4 +199,28 @@ class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageIn
                
                return $definition;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $objectTypeDefinition = $element->ownerDocument->createElement($this->tagName);
+               $objectTypeDefinition->setAttribute(
+                       'name',
+                       $element->getElementsByTagName('name')->item(0)->nodeValue
+               );
+               
+               return $objectTypeDefinition;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $this->handleDelete([['attributes' => [
+                       'name' => $element->getElementsByTagName('name')->item(0)->nodeValue
+               ]]]);
+       }
 }
index 1fe6e13308a1f8b50d54db4cf9f27e6e9be59d31..12b9043e5d9f3b8001490d75e09a08c04699c0df 100644 (file)
@@ -1131,4 +1131,37 @@ XML;
                
                return $returnValue;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $objectType = $element->ownerDocument->createElement($this->tagName);
+               $objectType->setAttribute(
+                       'name',
+                       $element->getElementsByTagName('name')->item(0)->nodeValue
+               );
+               
+               $objectType->appendChild($element->ownerDocument->createElement(
+                       'definitionname',
+                       $element->getElementsByTagName('definitionname')->item(0)->nodeValue
+               ));
+               
+               return $objectType;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $name = $element->getElementsByTagName('name')->item(0)->nodeValue;
+               $definitionName = $element->getElementsByTagName('definitionname')->item(0)->nodeValue;
+               
+               $this->handleDelete([[
+                       'attributes' => ['name' => $name],
+                       'elements' => ['definitionname' => $definitionName]
+               ]]);
+       }
 }
index 9d9e412f60379df345f2851f026761d48aa62230..b267646e9e9d18a94c1b28fa20489f45a2dc6cf0 100644 (file)
@@ -511,4 +511,38 @@ class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstal
                
                return $listener;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $templateListener = $element->ownerDocument->createElement($this->tagName);
+               $templateListener->setAttribute('name', $element->getAttribute('name'));
+               
+               foreach (['environment', 'templatename', 'eventname'] as $childElement) {
+                       $templateListener->appendChild($element->ownerDocument->createElement(
+                               $childElement,
+                               $element->getElementsByTagName($childElement)->item(0)->nodeValue
+                       ));
+               }
+               
+               return $templateListener;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $elements= [];
+               foreach (['environment', 'templatename', 'eventname'] as $childElement) {
+                       $elements[$childElement] = $element->getElementsByTagName($childElement)->item(0)->nodeValue;
+               }
+               
+               $this->handleDelete([[
+                       'attributes' => ['name' => $element->getAttribute('name')],
+                       'elements' => $elements
+               ]]);
+       }
 }
index 7106ad4d36cd09bf777cee2ecb15d1ccb17844b7..d9ab586c8ba20a5d1721c45d95a37064cf20343e 100644 (file)
@@ -382,4 +382,34 @@ class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageI
                
                return $event;
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function prepareDeleteXmlElement(\DOMElement $element) {
+               $userNotificationEvent = $element->ownerDocument->createElement($this->tagName);
+               
+               foreach (['name', 'objecttype'] as $childElement) {
+                       $userNotificationEvent->appendChild($element->ownerDocument->createElement(
+                               $childElement,
+                               $element->getElementsByTagName($childElement)->item(0)->nodeValue
+                       ));
+               }
+               
+               return $userNotificationEvent;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function deleteObject(\DOMElement $element) {
+               $elements= [];
+               foreach (['name', 'objecttype'] as $childElement) {
+                       $elements[$childElement] = $element->getElementsByTagName($childElement)->item(0)->nodeValue;
+               }
+               
+               $this->handleDelete([['elements' => $elements]]);
+       }
 }
index 25af9782a2aed856507733da86d9936962be6f13..c7221ae4d34bf4bf52bd7ced24bcb4ad2f2cfc01 100644 (file)
                <item name="wcf.acp.devtools.project.add.info"><![CDATA[Inkompatible Pakete, Installations- und Aktualisierungsanweisungen können erst beim Bearbeiten eines vorhandenen Projekts hinzugefügt werden.]]></item>
                <item name="wcf.acp.devtools.project.optionalPackage.error.missingFiles"><![CDATA[Die folgenden Paketdateien fehlen: {implode from=$missingFiles item=missingFile}<kbd>{$missingFile}</kbd>{/implode}.]]></item>
                <item name="wcf.acp.devtools.project.requiredPackage.error.missingFiles"><![CDATA[Die folgenden Paketdateien fehlen: {implode from=$missingFiles item=missingFile}<kbd>{$missingFile}</kbd>{/implode}.]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Eintrag wirklich löschen?]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction"><![CDATA[Löschanweisung hinzufügen]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction.description"><![CDATA[Der Eintrag wird nicht nur aus der Datenbank gelöscht und und die Installationsanweisung entfernt, sondern es wird auch explizit eine Löschanweisung hinzugefügt.]]></item>
        </category>
        <category name="wcf.acp.email">
                <item name="wcf.acp.email.smtp.test"><![CDATA[SMTP-Verbindungstest]]></item>
index ceff351f8e2d193878701665b0933b0d6ca83512..4e0e809333600a41e4427f6d34b0c699790a8459 100644 (file)
                <item name="wcf.acp.devtools.project.add.info"><![CDATA[Conflicting packages, installation instructions, and update instructions can only be added when editing an existing project.]]></item>
                <item name="wcf.acp.devtools.project.optionalPackage.error.missingFiles"><![CDATA[The following package files are missing: {implode from=$missingFiles item=missingFile}<kbd>{$missingFile}</kbd>{/implode}.]]></item>
                <item name="wcf.acp.devtools.project.requiredPackage.error.missingFiles"><![CDATA[The following package files are missing: {implode from=$missingFiles item=missingFile}<kbd>{$missingFile}</kbd>{/implode}.]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.delete.confirmMessage"><![CDATA[Do you really want to delete the entry?]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction"><![CDATA[Add delete instruction]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.delete.addDeleteInstruction.description"><![CDATA[The entry will not only be deleted from database and its installation instruction removed, but a delete instruction will also be explicitly added.]]></item>
        </category>
        <category name="wcf.acp.email">
                <item name="wcf.acp.email.smtp.test"><![CDATA[SMTP Connection Test]]></item>