Add PIP to delete files (#4267)
authorMatthias Schmidt <gravatronics@live.com>
Mon, 7 Jun 2021 13:08:06 +0000 (15:08 +0200)
committerGitHub <noreply@github.com>
Mon, 7 Jun 2021 13:08:06 +0000 (15:08 +0200)
See #4180

XSD/fileDelete.xsd [new file with mode: 0644]
com.woltlab.wcf/packageInstallationPlugin.xml
wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/FileDeletePackageInstallationPlugin.class.php [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

diff --git a/XSD/fileDelete.xsd b/XSD/fileDelete.xsd
new file mode 100644 (file)
index 0000000..ca5331d
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!-- This file is used for xml files which delete files. -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.woltlab.com" targetNamespace="http://www.woltlab.com" elementFormDefault="qualified">
+       <xs:include schemaLocation="types.xsd" />
+       
+       <xs:element name="data">
+               <xs:complexType>
+                       <xs:all>
+                               <xs:element name="delete" type="delete" minOccurs="0" />
+                       </xs:all>
+               </xs:complexType>
+       </xs:element>
+       
+       <xs:complexType name="delete">
+               <xs:sequence>
+                       <xs:element name="file" type="file" maxOccurs="unbounded" />
+               </xs:sequence>
+       </xs:complexType>
+       
+       <xs:complexType name="file">
+               <xs:simpleContent>
+                       <xs:extension base="xs:string">
+                               <xs:attribute name="application" type="woltlab_varchar"/>
+                       </xs:extension>
+               </xs:simpleContent>
+       </xs:complexType>
+</xs:schema>
index 943f68c06a522ab21314873c8b42afa8bc69ac3e..c64fb38ee63ab0d109a515506c4116ac31da1401 100644 (file)
@@ -32,5 +32,6 @@
                <pip name="menuItem">wcf\system\package\plugin\MenuItemPackageInstallationPlugin</pip>
                <pip name="mediaProvider">wcf\system\package\plugin\MediaProviderPackageInstallationPlugin</pip>
                <pip name="database">wcf\system\package\plugin\DatabasePackageInstallationPlugin</pip>
+               <pip name="fileDelete">wcf\system\package\plugin\FileDeletePackageInstallationPlugin</pip>
        </import>
 </data>
index f4b30c7dfe13ba52424b82accd59cabd76bda65a..9f03cd06e56167a6bb870d3c62c5b807cf328759 100644 (file)
@@ -599,10 +599,12 @@ XML;
         if ($oldElement !== null) {
             $sqlData = $this->findExistingItem($this->getElementData($oldElement, true));
 
-            $statement = WCF::getDB()->prepareStatement($sqlData['sql']);
-            $statement->execute($sqlData['parameters']);
+            if ($sqlData !== null) {
+                $statement = WCF::getDB()->prepareStatement($sqlData['sql']);
+                $statement->execute($sqlData['parameters']);
 
-            $existingRow = $statement->fetchArray();
+                $existingRow = $statement->fetchArray();
+            }
         }
 
         $this->import($existingRow, $newElementData);
diff --git a/wcfsetup/install/files/lib/system/package/plugin/FileDeletePackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/FileDeletePackageInstallationPlugin.class.php
new file mode 100644 (file)
index 0000000..89138fb
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+
+namespace wcf\system\package\plugin;
+
+use wcf\data\application\Application;
+use wcf\data\package\Package;
+use wcf\data\package\PackageCache;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\SingleSelectionFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\WCF;
+use wcf\util\DOMUtil;
+use wcf\util\XML;
+
+/**
+ * Files files installed with the `file` package installation plugin.
+ *
+ * @author  Matthias Schmidt
+ * @copyright   2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Package\Plugin
+ * @since   5.5
+ */
+class FileDeletePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin
+{
+    use TXmlGuiPackageInstallationPlugin;
+
+    /**
+     * @inheritDoc
+     */
+    public $tagName = 'file';
+
+    /**
+     * @inheritDoc
+     */
+    protected function handleDelete(array $items)
+    {
+        $sql = "SELECT  packageID
+                FROM    wcf1_package_installation_file_log
+                WHERE   filename = ?
+                    AND application = ?
+                    AND packageID = ?";
+        $searchStatement = WCF::getDB()->prepare($sql);
+
+        $sql = "DELETE FROM wcf1_package_installation_file_log
+                WHERE       packageID = ?
+                        AND filename = ?";
+        $deleteStatement = WCF::getDB()->prepare($sql);
+
+        foreach ($items as $item) {
+            $file = $item['value'];
+            $application = 'wcf';
+            if (!empty($item['attributes']['application'])) {
+                $application = $item['attributes']['application'];
+            } elseif ($this->installation->getPackage()->isApplication) {
+                $application = Package::getAbbreviation($this->installation->getPackage()->package);
+            }
+
+            $searchStatement->execute([
+                $file,
+                $application,
+                $this->installation->getPackageID(),
+            ]);
+
+            $filePackageID = $searchStatement->fetchSingleColumn();
+            if ($filePackageID !== false && $filePackageID != $this->installation->getPackageID()) {
+                throw new \UnexpectedValueException(
+                    "File '{$file}' does not belong to package '{$this->installation->getPackage()->package}'
+                    but to package '" . PackageCache::getInstance()->getPackage($filePackageID)->package . "'."
+                );
+            }
+
+            $filePath = Application::getDirectory($application) . $file;
+            if (\file_exists($filePath)) {
+                \unlink($filePath);
+            }
+
+            $deleteStatement->execute([
+                $this->installation->getPackageID(),
+                $file,
+            ]);
+        }
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function import(array $row, array $data)
+    {
+        throw new \LogicException("The `fileDelete` package installation plugin does not support imports.");
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function prepareImport(array $data)
+    {
+        return $data;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function findExistingItem(array $data)
+    {
+        return null;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public static function getSyncDependencies()
+    {
+        return [];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function addFormFields(IFormDocument $form)
+    {
+        /** @var FormContainer $dataContainer */
+        $dataContainer = $form->getNodeById('data');
+
+        $dataContainer->appendChildren([
+            TextFormField::create('file')
+                ->label('wcf.acp.pip.fileDelete.file')
+                ->required(),
+            SingleSelectionFormField::create('application')
+                ->label('wcf.acp.pip.fileDelete.application')
+                ->options(static function (): array {
+                    $options = [
+                        '' => 'wcf.global.noSelection',
+                    ];
+
+                    $apps = ApplicationHandler::getInstance()->getApplications();
+                    \usort($apps, static function (Application $a, Application $b) {
+                        return $a->getPackage()->getTitle() <=> $b->getPackage()->getTitle();
+                    });
+
+                    foreach ($apps as $application) {
+                        $options[$application->getAbbreviation()] = $application->getPackage()->getTitle();
+                    }
+
+                    return $options;
+                })
+                ->nullable(),
+        ]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function fetchElementData(\DOMElement $element, $saveData)
+    {
+        return [
+            'application' => $element->getAttribute('application') ?? 'wcf',
+            'file' => $element->nodeValue,
+            'packageID' => $this->installation->getPackage()->packageID,
+        ];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getElementIdentifier(\DOMElement $element)
+    {
+        $app = $element->getAttribute('application') ?? 'wcf';
+
+        return \sha1($app . '_' . $element->nodeValue);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
+    {
+        $entryList->setKeys([
+            'file' => 'wcf.acp.pip.fileDelete.file',
+            'application' => 'wcf.acp.pip.fileDelete.application',
+        ]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function insertNewXmlElement(XML $xml, \DOMElement $newElement)
+    {
+        $delete = $xml->xpath()->query('/ns:data/ns:delete')->item(0);
+        if ($delete === null) {
+            $data = $xml->xpath()->query('/ns:data')->item(0);
+            $delete = $xml->getDocument()->createElement('delete');
+            DOMUtil::prepend($delete, $data);
+        }
+
+        $delete->appendChild($newElement);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
+    {
+        $file = $document->createElement($this->tagName);
+
+        $data = $form->getData()['data'];
+        if (!empty($data['application'])) {
+            $file->setAttribute('application', $data['application']);
+        }
+        $file->nodeValue = $data['file'];
+
+        return $file;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function prepareDeleteXmlElement(\DOMElement $element)
+    {
+        return null;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function saveObject(\DOMElement $newElement, ?\DOMElement $oldElement = null)
+    {
+        $newElementData = $this->getElementData($newElement, true);
+
+        $this->handleDelete([[
+            'attributes' => [
+                'application' => $newElementData['application'],
+            ],
+            'value' => $newElementData['file'],
+        ]]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function deleteObject(\DOMElement $element)
+    {
+        // Reverting file deletions is not supported. Use the `file` PIP instead.
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function getImportElements(\DOMXPath $xpath)
+    {
+        return $xpath->query('/ns:data/ns:delete/ns:' . $this->tagName);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function getEmptyXml()
+    {
+        $xsdFilename = $this->getXsdFilename();
+        $apiVersion = WSC_API_VERSION;
+
+        return <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/{$apiVersion}/{$xsdFilename}.xsd">
+       <delete></delete>
+</data>
+XML;
+    }
+}
index 2f96b207b6ba0d793066774cd1b700104def7868..c2298b269b6a8aa6cfa287279784c648ea4240fc 100644 (file)
@@ -2643,6 +2643,9 @@ Kein Abschnitt darf leer sein und alle Abschnitten dürfen nur folgende Zeichen
                <item name="wcf.acp.pip.language.languageItemValue.en.description"><![CDATA[Hilfreiche Code-Fragmente: <kbd>“”</kbd>]]></item>
                <item name="wcf.acp.pip.option.optionName.error.notLowercase"><![CDATA[Der Optionsname darf keine Großbuchstaben enthalten.]]></item>
                <item name="wcf.acp.pip.option.optionName.error.pattern"><![CDATA[Der Optionsname darf ausschließlich aus kleinen lateinischen Buchstaben, Ziffern und Unterstrichen bestehen. Das erste und letzte Zeichen muss jeweils ein Buchstabe sein.]]></item>
+               <item name="wcf.acp.pip.fileDelete.info"><![CDATA[Das <kbd>fileDelete</kbd>-Package Installation Plugin löscht Dateien, die mit dem <kbd>file</kbd>-Package Installation Plugin installiert wurden. {if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst{else}Sie können{/if} mehr Informationen in der <a href="https://docs.woltlab.com/latest/package/pip/fileDelete" class="externalURL">Entwickler-Dokumentation</a> finden.]]></item>
+               <item name="wcf.acp.pip.fileDelete.file"><![CDATA[Datei]]></item>
+               <item name="wcf.acp.pip.fileDelete.application"><![CDATA[App]]></item>
        </category>
        <category name="wcf.acp.reactionType">
                <item name="wcf.acp.reactionType.type"><![CDATA[Reaktions-Typ]]></item>
index 57a335c61780f76e232348aacf006ce22d2da817..027b0b02aadfef2f66d8d05c2a4f2931463c2b2e 100644 (file)
@@ -2573,6 +2573,9 @@ If you have <strong>already bought the licenses for the listed apps</strong>, th
                <item name="wcf.acp.pip.language.languageItemValue.en.description"><![CDATA[Helpful code fragments: <kbd>“”</kbd>]]></item>
                <item name="wcf.acp.pip.option.optionName.error.notLowercase"><![CDATA[The option name may not include uppercase letters.]]></item>
                <item name="wcf.acp.pip.option.optionName.error.pattern"><![CDATA[The option name must consist of lowercase latin letters, digits and underscores only. The first and last character must be letters.]]></item>
+               <item name="wcf.acp.pip.fileDelete.info"><![CDATA[The <kbd>fileDelete</kbd> package installation plugin installs deletes files installed with the <kbd>file</kbd> package installation plugin. You can find more information in the <a href="https://docs.woltlab.com/latest/package/pip/fileDelete/" class="externalURL">developer documentation</a>.]]></item>
+               <item name="wcf.acp.pip.fileDelete.file"><![CDATA[File]]></item>
+               <item name="wcf.acp.pip.fileDelete.application"><![CDATA[App]]></item>
        </category>
        <category name="wcf.acp.reactionType">
                <item name="wcf.acp.reactionType.type"><![CDATA[Reaction Type]]></item>