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