42b901729b0501e299aca834c0cdc490a0840fb4
[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 * @inheritDoc
144 */
145 public function hasUninstall()
146 {
147 // File deletions cannot be reverted.
148 return false;
149 }
150
151 /**
152 * @inheritDoc
153 */
154 public function uninstall()
155 {
156 // File deletions cannot be reverted.
157 }
158
159 /**
160 * Returns the language item with the description of the file field or `null` if no description
161 * should be shown.
162 */
163 protected function getFileFieldDescription(): ?string
164 {
165 $languageItem = "wcf.acp.pip.{$this->getPipName()}.{$this->tagName}.description";
166
167 return WCF::getLanguage()->get($languageItem, true) ?: null;
168 }
169
170 /**
171 * @inheritDoc
172 */
173 protected function addFormFields(IFormDocument $form)
174 {
175 /** @var FormContainer $dataContainer */
176 $dataContainer = $form->getNodeById('data');
177
178 $dataContainer->appendChildren([
179 TextFormField::create($this->tagName)
180 ->label("wcf.acp.pip.{$this->getPipName()}.{$this->tagName}")
181 ->description($this->getFileFieldDescription())
182 ->required(),
183 SingleSelectionFormField::create('application')
184 ->label("wcf.acp.pip.{$this->getPipName()}.application")
185 ->options(static function (): array {
186 $options = [
187 '' => 'wcf.global.noSelection',
188 ];
189
190 $apps = ApplicationHandler::getInstance()->getApplications();
191 \usort($apps, static function (Application $a, Application $b) {
192 return $a->getPackage()->getTitle() <=> $b->getPackage()->getTitle();
193 });
194
195 foreach ($apps as $application) {
196 $options[$application->getAbbreviation()] = $application->getPackage()->getTitle();
197 }
198
199 return $options;
200 })
201 ->nullable(),
202 ]);
203 }
204
205 /**
206 * @inheritDoc
207 */
208 protected function fetchElementData(\DOMElement $element, $saveData)
209 {
210 return [
211 'application' => $element->getAttribute('application') ?? 'wcf',
212 $this->tagName => $element->nodeValue,
213 'packageID' => $this->installation->getPackage()->packageID,
214 ];
215 }
216
217 /**
218 * @inheritDoc
219 */
220 public function getElementIdentifier(\DOMElement $element)
221 {
222 $app = $element->getAttribute('application') ?? 'wcf';
223
224 return \sha1($app . '_' . $element->nodeValue);
225 }
226
227 /**
228 * @inheritDoc
229 */
230 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
231 {
232 $entryList->setKeys([
233 $this->tagName => "wcf.acp.pip.{$this->getPipName()}.{$this->tagName}",
234 'application' => "wcf.acp.pip.{$this->getPipName()}.application",
235 ]);
236 }
237
238 /**
239 * @inheritDoc
240 */
241 protected function insertNewXmlElement(XML $xml, \DOMElement $newElement)
242 {
243 $delete = $xml->xpath()->query('/ns:data/ns:delete')->item(0);
244 if ($delete === null) {
245 $data = $xml->xpath()->query('/ns:data')->item(0);
246 $delete = $xml->getDocument()->createElement('delete');
247 DOMUtil::prepend($delete, $data);
248 }
249
250 $delete->appendChild($newElement);
251 }
252
253 /**
254 * @inheritDoc
255 */
256 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
257 {
258 $file = $document->createElement($this->tagName);
259
260 $data = $form->getData()['data'];
261 if (!empty($data['application'])) {
262 $file->setAttribute('application', $data['application']);
263 }
264 $file->nodeValue = $data[$this->tagName];
265
266 return $file;
267 }
268
269 /**
270 * @inheritDoc
271 */
272 final protected function prepareDeleteXmlElement(\DOMElement $element)
273 {
274 return null;
275 }
276
277 /**
278 * @inheritDoc
279 */
280 protected function saveObject(\DOMElement $newElement, ?\DOMElement $oldElement = null)
281 {
282 $newElementData = $this->getElementData($newElement, true);
283
284 $this->handleDelete([[
285 'attributes' => [
286 'application' => $newElementData['application'],
287 ],
288 'value' => $newElementData[$this->tagName],
289 ]]);
290 }
291
292 /**
293 * @inheritDoc
294 */
295 final protected function deleteObject(\DOMElement $element)
296 {
297 // Reverting file deletions is not supported. Use the `file` PIP instead.
298 }
299
300 /**
301 * @inheritDoc
302 */
303 protected function getImportElements(\DOMXPath $xpath)
304 {
305 return $xpath->query('/ns:data/ns:delete/ns:' . $this->tagName);
306 }
307
308 /**
309 * @inheritDoc
310 */
311 protected function getEmptyXml()
312 {
313 $xsdFilename = $this->getXsdFilename();
314 $apiVersion = WSC_API_VERSION;
315
316 return <<<XML
317 <?xml version="1.0" encoding="UTF-8"?>
318 <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">
319 <delete></delete>
320 </data>
321 XML;
322 }
323 }