Merge pull request #6006 from WoltLab/file-processor-can-adopt
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / AbstractMenuPackageInstallationPlugin.class.php
CommitLineData
11ade432 1<?php
a9229942 2
11ade432 3namespace wcf\system\package\plugin;
a9229942 4
bbcfcec3
MS
5use wcf\data\acp\menu\item\ACPMenuItem;
6use wcf\data\DatabaseObjectList;
9847bda9
MS
7use wcf\page\IPage;
8use wcf\system\devtools\pip\IDevtoolsPipEntryList;
d7424422 9use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
9847bda9 10use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
11ade432 11use wcf\system\exception\SystemException;
9847bda9
MS
12use wcf\system\form\builder\container\IFormContainer;
13use wcf\system\form\builder\field\ClassNameFormField;
14use wcf\system\form\builder\field\IntegerFormField;
35d95be3 15use wcf\system\form\builder\field\option\OptionFormField;
9847bda9
MS
16use wcf\system\form\builder\field\SingleSelectionFormField;
17use wcf\system\form\builder\field\TextFormField;
35d95be3 18use wcf\system\form\builder\field\user\group\option\UserGroupOptionFormField;
9847bda9
MS
19use wcf\system\form\builder\field\validation\FormFieldValidationError;
20use wcf\system\form\builder\field\validation\FormFieldValidator;
21use wcf\system\form\builder\IFormDocument;
11ade432 22use wcf\system\WCF;
4f2d3f58 23use wcf\util\StringUtil;
9847bda9 24use wcf\util\Url;
11ade432
AE
25
26/**
a17de04e 27 * Abstract implementation of a package installation plugin for menu items.
a9229942
TD
28 *
29 * @author Alexander Ebert, Matthias Schmidt
30 * @copyright 2001-2019 WoltLab GmbH
31 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
11ade432 32 */
a9229942
TD
33abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements
34 IIdempotentPackageInstallationPlugin
35{
36 // we do no implement `IGuiPackageInstallationPlugin` but instead just
37 // provide the default implementation to ensure backwards compatibility
38 // with third-party packages containing classes that extend this abstract
39 // class
40 use TXmlGuiPackageInstallationPlugin;
41
42 /**
43 * @inheritDoc
44 */
45 protected function handleDelete(array $items)
46 {
47 $sql = "DELETE FROM " . $this->application . WCF_N . "_" . $this->tableName . "
48 WHERE menuItem = ?
49 AND packageID = ?";
50 $statement = WCF::getDB()->prepareStatement($sql);
51 foreach ($items as $item) {
52 $statement->execute([
53 $item['attributes']['name'],
54 $this->installation->getPackageID(),
55 ]);
56 }
57 }
58
59 /**
60 * @inheritDoc
61 */
62 protected function prepareImport(array $data)
63 {
64 // adjust show order
65 $showOrder = $data['elements']['showorder'] ?? null;
66 $parent = $data['elements']['parent'] ?? '';
67 $showOrder = $this->getShowOrder($showOrder, $parent, 'parentMenuItem');
68
69 // merge values and default values
70 return [
71 'menuItem' => $data['attributes']['name'],
72 'menuItemController' => $data['elements']['controller'] ?? '',
73 'menuItemLink' => $data['elements']['link'] ?? '',
74 'options' => isset($data['elements']['options']) ? StringUtil::normalizeCsv($data['elements']['options']) : '',
75 'parentMenuItem' => $data['elements']['parent'] ?? '',
76 'permissions' => isset($data['elements']['permissions']) ? StringUtil::normalizeCsv($data['elements']['permissions']) : '',
77 'showOrder' => $showOrder,
78 ];
79 }
80
81 /**
82 * @inheritDoc
83 */
84 protected function validateImport(array $data)
85 {
86 if (empty($data['parentMenuItem'])) {
87 return;
88 }
89
90 $sql = "SELECT COUNT(menuItemID)
91 FROM " . $this->application . WCF_N . "_" . $this->tableName . "
92 WHERE menuItem = ?";
93 $statement = WCF::getDB()->prepareStatement($sql);
94 $statement->execute([$data['parentMenuItem']]);
95
96 if (!$statement->fetchSingleColumn()) {
97 throw new SystemException("Unable to find parent 'menu item' with name '" . $data['parentMenuItem'] . "' for 'menu item' with name '" . $data['menuItem'] . "'.");
98 }
99 }
100
101 /**
102 * @inheritDoc
103 */
104 protected function findExistingItem(array $data)
105 {
106 $sql = "SELECT *
107 FROM " . $this->application . WCF_N . "_" . $this->tableName . "
108 WHERE menuItem = ?
109 AND packageID = ?";
110 $parameters = [
111 $data['menuItem'],
112 $this->installation->getPackageID(),
113 ];
114
115 return [
116 'sql' => $sql,
117 'parameters' => $parameters,
118 ];
119 }
120
121 /**
122 * @inheritDoc
123 * @since 3.1
124 */
125 public static function getSyncDependencies()
126 {
127 return [];
128 }
129
130 /**
131 * @inheritDoc
132 * @since 5.2
133 */
134 protected function addFormFields(IFormDocument $form)
135 {
136 /** @var IFormContainer $dataContainer */
137 $dataContainer = $form->getNodeById('data');
138
139 $dataContainer->appendChildren([
140 TextFormField::create('menuItem')
141 ->objectProperty('name')
142 ->label('wcf.acp.pip.abstractMenu.menuItem')
143 ->addValidator(new FormFieldValidator('uniqueness', function (TextFormField $formField) {
144 if (
145 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE
146 || $this->editedEntry->getAttribute('name') !== $formField->getValue()
147 ) {
148 // replace `Editor` with `List`
149 $listClassName = \substr($this->className, 0, -6) . 'List';
150
151 /** @var DatabaseObjectList $menuItemList */
152 $menuItemList = new $listClassName();
153 $menuItemList->getConditionBuilder()->add('menuItem = ?', [$formField->getValue()]);
154
155 if ($menuItemList->countObjects() > 0) {
156 $formField->addValidationError(
157 new FormFieldValidationError(
158 'notUnique',
159 'wcf.acp.pip.abstractMenu.menuItem.error.notUnique'
160 )
161 );
162 }
163 }
164 })),
165
166 SingleSelectionFormField::create('parentMenuItem')
167 ->objectProperty('parent')
168 ->label('wcf.acp.pip.abstractMenu.parentMenuItem')
169 ->filterable()
170 ->options(function () {
171 $menuStructure = $this->getMenuStructureData()['structure'];
172
173 $options = [
174 [
175 'depth' => 0,
176 'label' => 'wcf.global.noSelection',
177 'value' => '',
178 ],
179 ];
180
181 $buildOptions = static function ($parent = '', $depth = 0) use ($menuStructure, &$buildOptions) {
182 // only consider menu items until the third level (thus only parent
183 // menu items until the second level) as potential parent menu items
184 if ($depth > 2) {
185 return [];
186 }
187
188 $options = [];
189 foreach ($menuStructure[$parent] as $menuItem) {
190 $options[] = [
191 'depth' => $depth,
192 'label' => $menuItem->menuItem,
193 'value' => $menuItem->menuItem,
194 ];
195
196 if (isset($menuStructure[$menuItem->menuItem])) {
197 $options = \array_merge($options, $buildOptions($menuItem->menuItem, $depth + 1));
198 }
199 }
200
201 return $options;
202 };
203
204 return \array_merge($options, $buildOptions());
205 }, true)
206 ->value('')
207 ->addValidator(new FormFieldValidator(
208 'selfChildAsParent',
209 function (SingleSelectionFormField $formField) {
210 if (
211 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_UPDATE
212 && $formField->getSaveValue() !== ''
213 ) {
214 /** @var TextFormField $menuItemField */
215 $menuItemField = $formField->getDocument()->getNodeById('menuItem');
216 $menuItem = $menuItemField->getSaveValue();
217 $parentMenuItem = $formField->getSaveValue();
218
219 if ($menuItem === $parentMenuItem) {
220 $formField->addValidationError(new FormFieldValidationError(
221 'selfParent',
222 'wcf.acp.pip.abstractMenu.parentMenuItem.error.selfParent'
223 ));
224 } else {
225 $menuStructure = $this->getMenuStructureData()['structure'];
226
227 $checkChildren = static function ($menuItem) use (
228 $formField,
229 $menuStructure,
230 $parentMenuItem,
231 &$checkChildren
232 ) {
233 if (isset($menuStructure[$menuItem])) {
234 /** @var ACPMenuItem $childMenuItem */
235 foreach ($menuStructure[$menuItem] as $childMenuItem) {
236 if ($childMenuItem->menuItem === $parentMenuItem) {
237 $formField->addValidationError(new FormFieldValidationError(
238 'childAsParent',
239 'wcf.acp.pip.abstractMenu.parentMenuItem.error.childAsParent'
240 ));
241
242 return false;
243 } else {
244 if (!$checkChildren($childMenuItem->menuItem)) {
245 return false;
246 }
247 }
248 }
249 }
250
251 return true;
252 };
253
254 $checkChildren($menuItem);
255 }
256 }
257 }
258 )),
259
260 ClassNameFormField::create('menuItemController')
261 ->objectProperty('controller')
262 ->label('wcf.acp.pip.abstractMenu.menuItemController')
263 ->implementedInterface(IPage::class),
264
265 TextFormField::create('menuItemLink')
266 ->objectProperty('link')
267 ->label('wcf.acp.pip.abstractMenu.menuItemLink')
268 ->description('wcf.acp.pip.abstractMenu.menuItemLink.description')
269 ->objectProperty('link')
270 ->addValidator(new FormFieldValidator('linkSpecified', function (TextFormField $formField) {
271 /** @var TextFormField $menuItem */
272 $menuItem = $formField->getDocument()->getNodeById('menuItem');
273
274 /** @var ClassNameFormField $menuItemController */
275 $menuItemController = $formField->getDocument()->getNodeById('menuItemController');
276
277 // ensure that either a menu item controller is specified or a link
278 // and workaround for special ACP menu item `wcf.acp.menu.link.option.category`
279 if (
280 $formField->getSaveValue() === '' && $menuItemController->getSaveValue() === ''
281 && (!($this instanceof ACPMenuPackageInstallationPlugin) || $menuItem->getSaveValue() !== 'wcf.acp.menu.link.option.category')
282 ) {
283 $formField->addValidationError(
284 new FormFieldValidationError(
285 'noLinkSpecified',
286 'wcf.acp.pip.abstractMenu.menuItemLink.error.noLinkSpecified'
287 )
288 );
289 }
290 }))
291 ->addValidator(new FormFieldValidator('format', static function (TextFormField $formField) {
292 if ($formField->getSaveValue() !== '') {
293 /** @var ClassNameFormField $menuItemController */
294 $menuItemController = $formField->getDocument()->getNodeById('menuItemController');
295
296 if (!$menuItemController->getSaveValue() && !Url::is($formField->getSaveValue())) {
297 $formField->addValidationError(
298 new FormFieldValidationError(
299 'noLink',
300 'wcf.acp.pip.abstractMenu.menuItemLink.error.noLink'
301 )
302 );
303 }
304 }
305 })),
306
307 OptionFormField::create()
308 ->description('wcf.acp.pip.abstractMenu.options.description')
309 ->saveValueType(OptionFormField::SAVE_VALUE_TYPE_CSV)
310 ->packageIDs(\array_merge(
311 [$this->installation->getPackage()->packageID],
312 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
313 )),
314
315 UserGroupOptionFormField::create()
316 ->description('wcf.acp.pip.abstractMenu.permissions.description')
317 ->saveValueType(OptionFormField::SAVE_VALUE_TYPE_CSV)
318 ->packageIDs(\array_merge(
319 [$this->installation->getPackage()->packageID],
320 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
321 )),
322
323 IntegerFormField::create('showOrder')
324 ->objectProperty('showorder')
325 ->label('wcf.form.field.showOrder')
326 ->description('wcf.acp.pip.abstractMenu.showOrder.description')
327 ->objectProperty('showorder')
328 ->minimum(1)
329 ->nullable(),
330 ]);
331 }
332
333 /**
334 * @inheritDoc
335 * @since 5.2
336 */
337 protected function fetchElementData(\DOMElement $element, $saveData)
338 {
339 $data = [
340 'menuItem' => $element->getAttribute('name'),
341 'packageID' => $this->installation->getPackage()->packageID,
342 ];
343
344 $parentMenuItem = $element->getElementsByTagName('parent')->item(0);
345 if ($parentMenuItem !== null) {
346 $data['parentMenuItem'] = $parentMenuItem->nodeValue;
347 } elseif ($saveData) {
348 $data['parentMenuItem'] = '';
349 }
350
351 $controller = $element->getElementsByTagName('controller')->item(0);
352 if ($controller !== null) {
353 $data['menuItemController'] = $controller->nodeValue;
354 } elseif ($saveData) {
355 $data['menuItemController'] = '';
356 }
357
358 $link = $element->getElementsByTagName('link')->item(0);
359 if ($link !== null) {
360 $data['menuItemLink'] = $link->nodeValue;
361 } elseif ($saveData) {
362 $data['menuItemLink'] = '';
363 }
364
365 $options = $element->getElementsByTagName('options')->item(0);
366 if ($options !== null) {
367 $data['options'] = $options->nodeValue;
368 } elseif ($saveData) {
369 $data['options'] = '';
370 }
371
372 $permissions = $element->getElementsByTagName('permissions')->item(0);
373 if ($permissions !== null) {
374 $data['permissions'] = $permissions->nodeValue;
375 } elseif ($saveData) {
376 $data['permissions'] = '';
377 }
378
379 $showOrder = $element->getElementsByTagName('showorder')->item(0);
380 if ($showOrder !== null) {
381 $data['showOrder'] = $showOrder->nodeValue;
382 }
383 if ($saveData && $this->editedEntry === null) {
384 // only set explicit showOrder when adding new menu item
385 $data['showOrder'] = $this->getShowOrder(
386 $data['showOrder'] ?? null,
387 $data['parentMenuItem'],
388 'parentMenuItem'
389 );
390 }
391
392 return $data;
393 }
394
395 /**
396 * @inheritDoc
397 * @since 5.2
398 */
399 public function getElementIdentifier(\DOMElement $element)
400 {
401 return $element->getAttribute('name');
402 }
403
404 /**
405 * Returns data on the structure of the menu.
406 *
407 * @return array
408 */
409 protected function getMenuStructureData()
410 {
411 // replace `Editor` with `List`
412 $listClassName = \substr($this->className, 0, -6) . 'List';
413
414 /** @var DatabaseObjectList $menuItemList */
415 $menuItemList = new $listClassName();
416 $menuItemList->getConditionBuilder()->add('packageID IN (?)', [
417 \array_merge(
418 [$this->installation->getPackage()->packageID],
419 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
420 ),
421 ]);
422 $menuItemList->sqlOrderBy = 'parentMenuItem ASC, showOrder ASC';
423 $menuItemList->readObjects();
424
425 // for better IDE auto-completion, we use `ACPMenuItem`, but the
426 // menu items can also belong to other menus
427 /** @var ACPMenuItem[] $menuItems */
428 $menuItems = [];
429 /** @var ACPMenuItem[][] $menuStructure */
430 $menuStructure = [];
431 foreach ($menuItemList as $menuItem) {
432 if (!isset($menuStructure[$menuItem->parentMenuItem])) {
433 $menuStructure[$menuItem->parentMenuItem] = [];
434 }
435
436 $menuStructure[$menuItem->parentMenuItem][$menuItem->menuItem] = $menuItem;
437 $menuItems[$menuItem->menuItem] = $menuItem;
438 }
439
440 $menuItemLevels = [];
441 foreach ($menuStructure as $parentMenuItemName => $childMenuItems) {
442 $menuItemsLevel = 1;
443
444 while (($parentMenuItem = $menuItems[$parentMenuItemName] ?? null)) {
445 $menuItemsLevel++;
446 $parentMenuItemName = $parentMenuItem->parentMenuItem;
447 }
448
449 foreach ($childMenuItems as $menuItem) {
450 $menuItemLevels[$menuItem->menuItem] = $menuItemsLevel;
451 }
452 }
453
454 return [
455 'levels' => $menuItemLevels,
456 'structure' => $menuStructure,
457 ];
458 }
459
460 /**
461 * @inheritDoc
462 * @since 5.2
463 */
464 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
465 {
466 $entryList->setKeys([
467 'menuItem' => 'wcf.acp.pip.abstractMenu.menuItem',
468 'parentMenuItem' => 'wcf.acp.pip.abstractMenu.parentMenuItem',
469 ]);
470 }
471
472 /**
473 * @inheritDoc
474 * @since 5.2
475 */
476 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
477 {
478 $formData = $form->getData()['data'];
479
480 $menuItem = $document->createElement($this->tagName);
481 $menuItem->setAttribute('name', $formData['name']);
482
483 $this->appendElementChildren(
484 $menuItem,
485 [
486 'controller' => '',
487 'parent' => '',
488 'link' => '',
489 'options' => '',
490 'permissions' => '',
491 'showorder' => null,
492 ],
493 $form
494 );
495
496 return $menuItem;
497 }
11ade432 498}