Commit | Line | Data |
---|---|---|
11ade432 | 1 | <?php |
a9229942 | 2 | |
11ade432 | 3 | namespace wcf\system\package\plugin; |
a9229942 | 4 | |
bbcfcec3 MS |
5 | use wcf\data\acp\menu\item\ACPMenuItem; |
6 | use wcf\data\DatabaseObjectList; | |
9847bda9 MS |
7 | use wcf\page\IPage; |
8 | use wcf\system\devtools\pip\IDevtoolsPipEntryList; | |
d7424422 | 9 | use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin; |
9847bda9 | 10 | use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin; |
11ade432 | 11 | use wcf\system\exception\SystemException; |
9847bda9 MS |
12 | use wcf\system\form\builder\container\IFormContainer; |
13 | use wcf\system\form\builder\field\ClassNameFormField; | |
14 | use wcf\system\form\builder\field\IntegerFormField; | |
35d95be3 | 15 | use wcf\system\form\builder\field\option\OptionFormField; |
9847bda9 MS |
16 | use wcf\system\form\builder\field\SingleSelectionFormField; |
17 | use wcf\system\form\builder\field\TextFormField; | |
35d95be3 | 18 | use wcf\system\form\builder\field\user\group\option\UserGroupOptionFormField; |
9847bda9 MS |
19 | use wcf\system\form\builder\field\validation\FormFieldValidationError; |
20 | use wcf\system\form\builder\field\validation\FormFieldValidator; | |
21 | use wcf\system\form\builder\IFormDocument; | |
11ade432 | 22 | use wcf\system\WCF; |
4f2d3f58 | 23 | use wcf\util\StringUtil; |
9847bda9 | 24 | use 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 |
33 | abstract 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 | } |