2 namespace wcf\system\package\plugin
;
3 use wcf\data\acp\menu\item\ACPMenuItem
;
4 use wcf\data\DatabaseObjectList
;
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
;
21 use wcf\util\StringUtil
;
25 * Abstract implementation of a package installation plugin for menu items.
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
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
37 use TXmlGuiPackageInstallationPlugin
;
42 protected function handleDelete(array $items) {
43 $sql = "DELETE FROM ".$this->application
.WCF_N
."_".$this->tableName
."
46 $statement = WCF
::getDB()->prepareStatement($sql);
47 foreach ($items as $item) {
49 $item['attributes']['name'],
50 $this->installation
->getPackageID()
58 protected function prepareImport(array $data) {
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');
64 // merge values and default values
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
79 protected function validateImport(array $data) {
80 if (empty($data['parentMenuItem'])) {
84 $sql = "SELECT COUNT(menuItemID)
85 FROM ".$this->application
.WCF_N
."_".$this->tableName
."
87 $statement = WCF
::getDB()->prepareStatement($sql);
88 $statement->execute([$data['parentMenuItem']]);
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']."'.");
98 protected function findExistingItem(array $data) {
100 FROM ".$this->application
.WCF_N
."_".$this->tableName
."
105 $this->installation
->getPackageID()
110 'parameters' => $parameters
118 public static function getSyncDependencies() {
126 protected function addFormFields(IFormDocument
$form) {
127 /** @var IFormContainer $dataContainer */
128 $dataContainer = $form->getNodeById('data');
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) {
136 $formField->getDocument()->getFormMode() === IFormDocument
::FORM_MODE_CREATE ||
137 $this->editedEntry
->getAttribute('name') !== $formField->getValue()
139 // replace `Editor` with `List`
140 $listClassName = substr($this->className
, 0, -6) . 'List';
142 /** @var DatabaseObjectList $menuItemList */
143 $menuItemList = new $listClassName();
144 $menuItemList->getConditionBuilder()->add('menuItem = ?', [$formField->getValue()]);
146 if ($menuItemList->countObjects() > 0) {
147 $formField->addValidationError(
148 new FormFieldValidationError(
150 'wcf.acp.pip.abstractMenu.menuItem.error.notUnique'
157 SingleSelectionFormField
::create('parentMenuItem')
158 ->objectProperty('parent')
159 ->label('wcf.acp.pip.abstractMenu.parentMenuItem')
161 ->options(function() {
162 $menuStructure = $this->getMenuStructureData()['structure'];
166 'label' => 'wcf.global.noSelection',
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
178 foreach ($menuStructure[$parent] as $menuItem) {
181 'label' => $menuItem->menuItem
,
182 'value' => $menuItem->menuItem
185 if (isset($menuStructure[$menuItem->menuItem
])) {
186 $options = array_merge($options, $buildOptions($menuItem->menuItem
, $depth +
1));
193 return array_merge($options, $buildOptions());
197 ClassNameFormField
::create('menuItemController')
198 ->objectProperty('controller')
199 ->label('wcf.acp.pip.abstractMenu.menuItemController')
200 ->implementedInterface(IPage
::class),
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');
211 /** @var ClassNameFormField $menuItemController */
212 $menuItemController = $formField->getDocument()->getNodeById('menuItemController');
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`
217 $formField->getSaveValue() === '' && $menuItemController->getSaveValue() === '' &&
218 (!($this instanceof ACPMenuPackageInstallationPlugin
) ||
$menuItem->getSaveValue() !== 'wcf.acp.menu.link.option.category')
220 $formField->addValidationError(
221 new FormFieldValidationError(
223 'wcf.acp.pip.abstractMenu.menuItemLink.error.noLinkSpecified'
228 ->addValidator(new FormFieldValidator('format', function(TextFormField
$formField) {
229 if ($formField->getSaveValue() !== '') {
230 /** @var ClassNameFormField $menuItemController */
231 $menuItemController = $formField->getDocument()->getNodeById('menuItemController');
233 if (!$menuItemController->getSaveValue() && !Url
::is($formField->getSaveValue())) {
234 $formField->addValidationError(
235 new FormFieldValidationError(
237 'wcf.acp.pip.abstractMenu.menuItemLink.error.noLink'
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())
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())
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')
274 protected function doGetElementData(\DOMElement
$element, $saveData) {
276 'menuItem' => $element->getAttribute('name'),
277 'packageID' => $this->installation
->getPackage()->packageID
280 $parentMenuItem = $element->getElementsByTagName('parent')->item(0);
281 if ($parentMenuItem !== null) {
282 $data['parentMenuItem'] = $parentMenuItem->nodeValue
;
285 $controller = $element->getElementsByTagName('controller')->item(0);
286 if ($controller !== null) {
287 $data['menuItemController'] = $controller->nodeValue
;
290 $link = $element->getElementsByTagName('link')->item(0);
291 if ($link !== null) {
292 $data['menuItemLink'] = $link->nodeValue
;
295 $options = $element->getElementsByTagName('options')->item(0);
296 if ($options !== null) {
297 $data['options'] = $options->nodeValue
;
300 $permissions = $element->getElementsByTagName('permissions')->item(0);
301 if ($permissions !== null) {
302 $data['permissions'] = $permissions->nodeValue
;
305 $showOrder = $element->getElementsByTagName('showorder')->item(0);
306 if ($showOrder !== null) {
307 $data['showOrder'] = $showOrder->nodeValue
;
317 public function getElementIdentifier(\DOMElement
$element) {
318 return $element->getAttribute('name');
322 * Returns data on the structure of the menu.
326 protected function getMenuStructureData() {
327 // replace `Editor` with `List`
328 $listClassName = substr($this->className
, 0, -6) . 'List';
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())
336 $menuItemList->sqlOrderBy
= 'parentMenuItem ASC, showOrder ASC';
337 $menuItemList->readObjects();
339 // for better IDE auto-completion, we use `ACPMenuItem`, but the
340 // menu items can also belong to other menus
341 /** @var ACPMenuItem[] $menuItems */
343 /** @var ACPMenuItem[][] $menuStructure */
345 foreach ($menuItemList as $menuItem) {
346 if (!isset($menuStructure[$menuItem->parentMenuItem
])) {
347 $menuStructure[$menuItem->parentMenuItem
] = [];
350 $menuStructure[$menuItem->parentMenuItem
][$menuItem->menuItem
] = $menuItem;
351 $menuItems[$menuItem->menuItem
] = $menuItem;
354 $menuItemLevels = [];
355 foreach ($menuStructure as $parentMenuItemName => $childMenuItems) {
358 while (($parentMenuItem = $menuItems[$parentMenuItemName] ??
null)) {
360 $parentMenuItemName = $parentMenuItem->parentMenuItem
;
363 foreach ($childMenuItems as $menuItem) {
364 $menuItemLevels[$menuItem->menuItem
] = $menuItemsLevel;
369 'levels' => $menuItemLevels,
370 'structure' => $menuStructure
378 protected function setEntryListKeys(IDevtoolsPipEntryList
$entryList) {
379 $entryList->setKeys([
380 'menuItem' => 'wcf.acp.pip.abstractMenu.menuItem',
381 'parentMenuItem' => 'wcf.acp.pip.abstractMenu.parentMenuItem'
389 protected function doCreateXmlElement(\DOMDocument
$document, IFormDocument
$form) {
390 $formData = $form->getData()['data'];
392 $menuItem = $document->createElement($this->tagName
);
393 $menuItem->setAttribute('name', $formData['name']);
395 $this->appendElementChildren(