f08936193689b9a9ae265b563b868de1c163b027
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 namespace wcf\system\package\plugin;
4
5 use wcf\data\application\Application;
6 use wcf\data\package\Package;
7 use wcf\data\package\PackageCache;
8 use wcf\system\application\ApplicationHandler;
9 use wcf\system\devtools\pip\IDevtoolsPipEntryList;
10 use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
11 use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
12 use wcf\system\form\builder\container\FormContainer;
13 use wcf\system\form\builder\field\SingleSelectionFormField;
14 use wcf\system\form\builder\field\TextFormField;
15 use wcf\system\form\builder\IFormDocument;
16 use wcf\system\WCF;
17 use wcf\util\DOMUtil;
18 use wcf\util\XML;
19
20 /**
21 * Abstract implementation of a package installation plugin deleting a certain type of files.
22 *
23 * @author Matthias Schmidt
24 * @copyright 2001-2021 WoltLab GmbH
25 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
26 * @package WoltLabSuite\Core\System\Package\Plugin
27 * @since 5.5
28 */
29 abstract class AbstractFileDeletePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements
30 IGuiPackageInstallationPlugin
31 {
32 use TXmlGuiPackageInstallationPlugin;
33
34 /**
35 * Returns the name of the database table that logs the installed files.
36 */
37 abstract protected function getLogTableName(): string;
38
39 /**
40 * Returns the name of the column in the log table returned by `getLogTableName()` that contains
41 * the names of the relevant files.
42 */
43 abstract protected function getFilenameTableColumn(): string;
44
45 protected function getPipName(): string
46 {
47 return $this->getXsdFilename();
48 }
49
50 /**
51 * Returns the actual absolute path of the given file.
52 */
53 protected function getFilePath(string $filename, string $application): string
54 {
55 return Application::getDirectory($application) . $filename;
56 }
57
58 /**
59 * @inheritDoc
60 */
61 protected function handleDelete(array $items)
62 {
63 $sql = "SELECT packageID
64 FROM {$this->getLogTableName()}
65 WHERE {$this->getFilenameTableColumn()} = ?
66 AND application = ?
67 AND packageID = ?";
68 $searchStatement = WCF::getDB()->prepare($sql);
69
70 $sql = "DELETE FROM {$this->getLogTableName()}
71 WHERE packageID = ?
72 AND {$this->getFilenameTableColumn()} = ?";
73 $deleteStatement = WCF::getDB()->prepare($sql);
74
75 foreach ($items as $item) {
76 $file = $item['value'];
77 $application = 'wcf';
78 if (!empty($item['attributes']['application'])) {
79 $application = $item['attributes']['application'];
80 } elseif ($this->installation->getPackage()->isApplication) {
81 $application = Package::getAbbreviation($this->installation->getPackage()->package);
82 }
83
84 $searchStatement->execute([
85 $file,
86 $application,
87 $this->installation->getPackageID(),
88 ]);
89
90 $filePackageID = $searchStatement->fetchSingleColumn();
91 if ($filePackageID !== false && $filePackageID != $this->installation->getPackageID()) {
92 throw new \UnexpectedValueException(
93 "'{$file}' does not belong to package '{$this->installation->getPackage()->package}'
94 but to package '" . PackageCache::getInstance()->getPackage($filePackageID)->package . "'."
95 );
96 }
97
98 $filePath = $this->getFilePath($file, $application);
99 if (\file_exists($filePath)) {
100 \unlink($filePath);
101 }
102
103 $deleteStatement->execute([
104 $this->installation->getPackageID(),
105 $file,
106 ]);
107 }
108 }
109
110 /**
111 * @inheritDoc
112 */
113 final protected function import(array $row, array $data)
114 {
115 // Does nothing, imports are not supported.
116 }
117
118 /**
119 * @inheritDoc
120 */
121 final protected function prepareImport(array $data)
122 {
123 return $data;
124 }
125
126 /**
127 * @inheritDoc
128 */
129 final protected function findExistingItem(array $data)
130 {
131 return null;
132 }
133
134 /**
135 * @inheritDoc
136 */
137 public static function getSyncDependencies()
138 {
139 return [];
140 }
141
142 /**
143 * Returns the language item with the description of the file field or `null` if no description
144 * should be shown.
145 */
146 protected function getFileFieldDescription(): ?string
147 {
148 $languageItem = "wcf.acp.pip.{$this->getPipName()}.{$this->tagName}.description";
149
150 return WCF::getLanguage()->get($languageItem, true) ?: null;
151 }
152
153 /**
154 * @inheritDoc
155 */
156 protected function addFormFields(IFormDocument $form)
157 {
158 /** @var FormContainer $dataContainer */
159 $dataContainer = $form->getNodeById('data');
160
161 $dataContainer->appendChildren([
162 TextFormField::create($this->tagName)
163 ->label("wcf.acp.pip.{$this->getPipName()}.{$this->tagName}")
164 ->description($this->getFileFieldDescription())
165 ->required(),
166 SingleSelectionFormField::create('application')
167 ->label("wcf.acp.pip.{$this->getPipName()}.application")
168 ->options(static function (): array {
169 $options = [
170 '' => 'wcf.global.noSelection',
171 ];
172
173 $apps = ApplicationHandler::getInstance()->getApplications();
174 \usort($apps, static function (Application $a, Application $b) {
175 return $a->getPackage()->getTitle() <=> $b->getPackage()->getTitle();
176 });
177
178 foreach ($apps as $application) {
179 $options[$application->getAbbreviation()] = $application->getPackage()->getTitle();
180 }
181
182 return $options;
183 })
184 ->nullable(),
185 ]);
186 }
187
188 /**
189 * @inheritDoc
190 */
191 protected function fetchElementData(\DOMElement $element, $saveData)
192 {
193 return [
194 'application' => $element->getAttribute('application') ?? 'wcf',
195 $this->tagName => $element->nodeValue,
196 'packageID' => $this->installation->getPackage()->packageID,
197 ];
198 }
199
200 /**
201 * @inheritDoc
202 */
203 public function getElementIdentifier(\DOMElement $element)
204 {
205 $app = $element->getAttribute('application') ?? 'wcf';
206
207 return \sha1($app . '_' . $element->nodeValue);
208 }
209
210 /**
211 * @inheritDoc
212 */
213 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
214 {
215 $entryList->setKeys([
216 $this->tagName => "wcf.acp.pip.{$this->getPipName()}.{$this->tagName}",
217 'application' => "wcf.acp.pip.{$this->getPipName()}.application",
218 ]);
219 }
220
221 /**
222 * @inheritDoc
223 */
224 protected function insertNewXmlElement(XML $xml, \DOMElement $newElement)
225 {
226 $delete = $xml->xpath()->query('/ns:data/ns:delete')->item(0);
227 if ($delete === null) {
228 $data = $xml->xpath()->query('/ns:data')->item(0);
229 $delete = $xml->getDocument()->createElement('delete');
230 DOMUtil::prepend($delete, $data);
231 }
232
233 $delete->appendChild($newElement);
234 }
235
236 /**
237 * @inheritDoc
238 */
239 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
240 {
241 $file = $document->createElement($this->tagName);
242
243 $data = $form->getData()['data'];
244 if (!empty($data['application'])) {
245 $file->setAttribute('application', $data['application']);
246 }
247 $file->nodeValue = $data[$this->tagName];
248
249 return $file;
250 }
251
252 /**
253 * @inheritDoc
254 */
255 final protected function prepareDeleteXmlElement(\DOMElement $element)
256 {
257 return null;
258 }
259
260 /**
261 * @inheritDoc
262 */
263 protected function saveObject(\DOMElement $newElement, ?\DOMElement $oldElement = null)
264 {
265 $newElementData = $this->getElementData($newElement, true);
266
267 $this->handleDelete([[
268 'attributes' => [
269 'application' => $newElementData['application'],
270 ],
271 'value' => $newElementData[$this->tagName],
272 ]]);
273 }
274
275 /**
276 * @inheritDoc
277 */
278 final protected function deleteObject(\DOMElement $element)
279 {
280 // Reverting file deletions is not supported. Use the `file` PIP instead.
281 }
282
283 /**
284 * @inheritDoc
285 */
286 protected function getImportElements(\DOMXPath $xpath)
287 {
288 return $xpath->query('/ns:data/ns:delete/ns:' . $this->tagName);
289 }
290
291 /**
292 * @inheritDoc
293 */
294 protected function getEmptyXml()
295 {
296 $xsdFilename = $this->getXsdFilename();
297 $apiVersion = WSC_API_VERSION;
298
299 return <<<XML
300 <?xml version="1.0" encoding="UTF-8"?>
301 <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">
302 <delete></delete>
303 </data>
304 XML;
305 }
306 }