Merge branch 'formBuilder' into pipGui
authorMatthias Schmidt <gravatronics@live.com>
Sun, 6 May 2018 07:26:56 +0000 (09:26 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 6 May 2018 07:26:56 +0000 (09:26 +0200)
27 files changed:
wcfsetup/install/files/acp/templates/__objectTypePipGui.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/devtoolsProjectList.tpl
wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryAdd.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryList.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/DevtoolsProjectPipEntryAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/DevtoolsProjectPipEntryEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipEntryListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationDispatcher.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPipEntryList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/devtools/pip/IDevtoolsPipEntryList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/devtools/pip/IGuiPackageInstallationPlugin.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/data/GuiPackageInstallationPluginFormFieldDataProcessor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/package/plugin/AbstractXMLPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ObjectTypeDefinitionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ObjectTypePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/PIPPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/UserNotificationEventPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/UserProfileMenuPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/util/XML.class.php
wcfsetup/install/files/lib/util/XMLWriter.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

diff --git a/wcfsetup/install/files/acp/templates/__objectTypePipGui.tpl b/wcfsetup/install/files/acp/templates/__objectTypePipGui.tpl
new file mode 100644 (file)
index 0000000..320cbad
--- /dev/null
@@ -0,0 +1,36 @@
+<script data-relocate="true">
+       require(['Language'], function(Language) {
+               Language.addObject({
+                       'wcf.acp.pip.objectType.className.description': '{lang __literal=true}wcf.acp.pip.objectType.className.description{/lang}',
+                       {implode from=$definitionNames item=definitionName}
+                               'wcf.acp.pip.objectType.definitionName.{@$definitionName}.description': '{lang __literal=true __optional=true}wcf.acp.pip.objectType.definitionName.{@$definitionName}.description{/lang}'
+                       {/implode}
+               });
+               
+               var definitionNamesWithInterface = {
+                       {implode from=$definitionNamesWithInterface key=definitionName item=interfaceName}
+                               '{@$definitionName}': '{@$interfaceName|encodeJS}'
+                       {/implode}
+               };
+               
+               var classNameDescription = elById('className').nextElementSibling;
+               var definitionName = elById('definitionName');
+               var definitionNameDescription = definitionName.nextElementSibling;
+               
+               function update() {
+                       // update description of `definitionName` field
+                       definitionNameDescription.innerHTML = Language.get('wcf.acp.pip.objectType.definitionName.' + definitionName.value + '.description');
+                       
+                       // update description of `className` field with new interface
+                       if (definitionNamesWithInterface[definitionName.value]) {
+                               classNameDescription.innerHTML = Language.get('wcf.acp.pip.objectType.className.description', {
+                                       interfaceName: definitionNamesWithInterface[definitionName.value]
+                               });
+                       }
+               }
+               
+               definitionName.addEventListener('change', update);
+               
+               update();
+       });
+</script>
\ No newline at end of file
index af6c9c8018fdc2a8de7fdef70909ffe7e6922bcc..d50f541b77c7b8842503c58eaaa9bdfcc0fc6906 100644 (file)
@@ -48,6 +48,7 @@
                                                <tr class="jsObjectRow">
                                                        <td class="columnIcon">
                                                                <a href="{link controller='DevtoolsProjectSync' id=$object->getObjectID()}{/link}" class="button small">{lang}wcf.acp.devtools.project.sync{/lang}</a>
+                                                               <a href="{link controller='DevtoolsProjectPipList' id=$object->getObjectID()}{/link}" class="button small">{lang}wcf.acp.devtools.project.pips{/lang}</a>
                                                        </td>
                                                        <td class="columnIcon">
                                                                <a href="{link controller='DevtoolsProjectEdit' id=$object->getObjectID()}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
diff --git a/wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryAdd.tpl b/wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryAdd.tpl
new file mode 100644 (file)
index 0000000..5652d9e
--- /dev/null
@@ -0,0 +1,29 @@
+{include file='header' pageTitle='wcf.acp.devtools.project.pip.entry.'|concat:$action:'.pageTitle'}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.devtools.project.pip.entry.{$action}{/lang}</h1>
+               <p class="contentHeaderDescription">{$project->name}</p>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='DevtoolsProjectPipEntryList' id=$project->projectID pip=$pip}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.devtools.project.pip.entry.list{/lang}</span></a></li>
+                       <li><a href="{link controller='DevtoolsProjectList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.devtools.project.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{include file='formError'}
+
+{if $success|isset}
+       <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+{@$pipObject->getPip()->getAdditionalTemplateCode()}
+
+{@$form->getHtml()}
+
+{include file='footer'}
diff --git a/wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryList.tpl b/wcfsetup/install/files/acp/templates/devtoolsProjectPipEntryList.tpl
new file mode 100644 (file)
index 0000000..ec41d8f
--- /dev/null
@@ -0,0 +1,58 @@
+{include file='header' pageTitle='wcf.acp.devtools.project.pip.entry.list.pageTitle'}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.devtools.project.pip.entry.list{/lang}</h1>
+               <p class="contentHeaderDescription">{$project->name}</p>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li class="dropdown">
+                               <a class="button dropdownToggle"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.devtools.project.pip.list{/lang}</span></a>
+                               <div class="dropdownMenu">
+                                       <ul class="scrollableDropdownMenu">
+                                               {foreach from=$project->getPips() item=otherPip}
+                                                       {if $otherPip->supportsGui()}
+                                                               <li{if $otherPip->pluginName === $pip} class="active"{/if}><a href="{link controller='DevtoolsProjectPipEntryList' id=$project->projectID pip=$otherPip->pluginName}{/link}">{$otherPip->pluginName}</a></li>
+                                                       {/if}
+                                               {/foreach}
+                                       </ul>
+                               </div>
+                       </li>
+                       <li><a href="{link controller='DevtoolsProjectPipEntryAdd' id=$project->projectID pip=$pip}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.devtools.project.pip.entry.button.add{/lang}</span></a></li>
+                       <li><a href="{link controller='DevtoolsProjectList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.devtools.project.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{if !$entryList->getEntries()|empty}
+       <div class="section tabularBox jsShowOnlyMatches" id="syncPipMatches">
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       {foreach from=$entryList->getKeys() item=languageItem name=entryListKeys}
+                                               <th{if $tpl[foreach][entryListKeys][first]} colspan="2"{/if}>{@$languageItem|language}</th>
+                                       {/foreach}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$entryList->getEntries() key=identifier item=entry}
+                                       <tr>
+                                               <td class="columnIcon"><a href="{link controller='DevtoolsProjectPipEntryEdit' id=$project->projectID pip=$pip identifier=$identifier}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a></td>
+                                               {foreach from=$entryList->getKeys() key=key item=languageItem}
+                                                       <td>{$entry[$key]}</td>
+                                               {/foreach}
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
diff --git a/wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl b/wcfsetup/install/files/acp/templates/devtoolsProjectPipList.tpl
new file mode 100644 (file)
index 0000000..fd08c43
--- /dev/null
@@ -0,0 +1,124 @@
+{include file='header' pageTitle='wcf.acp.devtools.project.pip.list.pageTitle'}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.devtools.project.pip.list{/lang}</h1>
+               <p class="contentHeaderDescription">{$project->name}</p>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='DevtoolsProjectList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.devtools.project.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{if $project->validate() === ''}
+       <div class="section">
+               <dl>
+                       <dt></dt>
+                       <dd>
+                               <label><input type="checkbox" id="showOnlyMatches" checked> {lang}wcf.acp.devtools.pip.showOnlyMatches{/lang}</label>
+                               <small>{lang}wcf.acp.devtools.pip.showOnlyMatches.description{/lang}</small>
+                       </dd>
+               </dl>
+               <dl>
+                       <dt></dt>
+                       <dd>
+                               <label><input type="checkbox" id="showGuiSupportingPipsOnly" checked> {lang}wcf.acp.devtools.pip.showGuiSupportingPipsOnly{/lang}</label>
+                               <small>{lang}wcf.acp.devtools.pip.showGuiSupportingPipsOnly.description{/lang}</small>
+                       </dd>
+               </dl>
+       </div>
+       
+       <div class="section tabularBox jsShowOnlyMatches" id="projectPipList">
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnText" colspan="2">{lang}wcf.acp.devtools.pip.pluginName{/lang}</th>
+                                       <th class="columnText">{lang}wcf.acp.devtools.pip.defaultFilename{/lang}</th>
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$project->getPips() item=pip}
+                                       <tr data-plugin-name="{$pip->pluginName}" data-is-supported="{if $pip->supportsGui()}true{else}false{/if}" data-is-used="{if !$pip->getTargets($project)|empty}true{else}false{/if}">
+                                               <td class="columnIcon">
+                                                       {if $pip->supportsGui()}
+                                                               <a href="{link controller='DevtoolsProjectPipEntryAdd' id=$project->projectID pip=$pip->pluginName}{/link}" title="{lang}wcf.global.button.add{/lang}" class="jsTooltip"><span class="icon icon16 fa-plus"></span></a>
+                                                               <a href="{link controller='DevtoolsProjectPipEntryList' id=$project->projectID pip=$pip->pluginName}{/link}" title="{lang}wcf.global.button.list{/lang}" class="jsTooltip"><span class="icon icon16 fa-list"></span></a>
+                                                       {else}
+                                                               <span class="icon icon16 fa-plus disabled" title="{lang}wcf.global.button.add{/lang}"></span>
+                                                               <span class="icon icon16 fa-list disabled" title="{lang}wcf.global.button.list{/lang}"></span>
+                                                       {/if}
+                                               </td>
+                                               <td class="columnText">
+                                                       {if $pip->supportsGui()}
+                                                               <a href="{link controller='DevtoolsProjectPipEntryList' id=$project->projectID pip=$pip->pluginName}{/link}">{$pip->pluginName}</a>
+                                                       {else}
+                                                               {$pip->pluginName}
+                                                       {/if}
+                                               </td>
+                                               {if $pip->supportsGui()}
+                                                       <td class="columnText pipDefaultFilename"><small>{$pip->getEffectiveDefaultFilename()}</small></td>
+                                               {else}
+                                                       <td class="columnText" colspan="3">
+                                                               {if !$pip->isSupported()}
+                                                                       {$pip->getFirstError()}
+                                                               {elseif !$pip->supportsGui()}
+                                                                       {lang}wcf.acp.devtools.pip.error.noGuiSupport{/lang}
+                                                               {/if}
+                                                       </td>
+                                               {/if}
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+       
+       <p class="info" style="display: none;">{lang}wcf.global.noItems{/lang}</p>
+{else}
+       <p class="error">{@$project->validate()}</p>
+{/if}
+
+<script data-relocate="true">
+       var showOnlyMatches = elById('showOnlyMatches');
+       var showGuiSupportingPipsOnly = elById('showGuiSupportingPipsOnly');
+       
+       function updateDisplayedPips() {
+               var pipList = elById('projectPipList');
+               var hasVisiblePips = false;
+               
+               elBySelAll('tbody > tr', pipList, function(element) {
+                       if (showOnlyMatches.checked && !elDataBool(element, 'is-used')) {
+                               elHide(element);
+                       }
+                       else if (showGuiSupportingPipsOnly.checked && !elDataBool(element, 'is-supported')) {
+                               elHide(element);
+                       }
+                       else {
+                               hasVisiblePips = true;
+                               elShow(element);
+                       }
+               });
+               
+               var info = pipList.nextElementSibling;
+               if (hasVisiblePips) {
+                       elShow(pipList);
+                       elHide(info);
+               }
+               else {
+                       elHide(pipList);
+                       elShow(info);
+               }
+       }
+       
+       showOnlyMatches.addEventListener('change', updateDisplayedPips);
+       showGuiSupportingPipsOnly.addEventListener('change', updateDisplayedPips);
+       
+       updateDisplayedPips();
+</script>
+
+{include file='footer'}
diff --git a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectPipEntryAddForm.class.php b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectPipEntryAddForm.class.php
new file mode 100644 (file)
index 0000000..9b1805d
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+declare(strict_types=1);
+namespace wcf\acp\form;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\form\AbstractForm;
+use wcf\form\AbstractFormBuilderForm;
+use wcf\system\devtools\pip\DevtoolsPip;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to add a new entry for a specific pip and project.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ * @since      3.2
+ */
+class DevtoolsProjectPipEntryAddForm extends AbstractFormBuilderForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
+       
+       /**
+        * name of the requested pip
+        * @var string
+        */
+       public $pip = '';
+       
+       /**
+        * devtools project
+        * @var DevtoolsProject
+        */
+       public $project;
+       
+       /**
+        * project id
+        * @var integer
+        */
+       public $projectID = 0;
+       
+       /**
+        * devtools pip object for the requested pip
+        * @var DevtoolsPip
+        */
+       protected $pipObject;
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->projectID = intval($_REQUEST['id']);
+               $this->project = new DevtoolsProject($this->projectID);
+               if (!$this->project->projectID) {
+                       throw new IllegalLinkException();
+               }
+               
+               $this->project->validatePackageXml();
+               
+               if (isset($_REQUEST['pip'])) $this->pip = StringUtil::trim($_REQUEST['pip']);
+               
+               $filteredPips = array_filter($this->project->getPips(), function(DevtoolsPip $pip) {
+                       return $pip->pluginName === $this->pip;
+               });
+               if (count($filteredPips) === 1) {
+                       $this->pipObject = reset($filteredPips);
+               }
+               else {
+                       throw new IllegalLinkException();
+               }
+               
+               if (!$this->pipObject->supportsGui()) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               // we have to do it here so that the pip object is available to
+               // add the pip-specific form fields
+               $this->addPipFormFields();
+               
+               parent::readData();
+       }
+       
+       /**
+        * Adds the pip-specific form fields.
+        */
+       protected function addPipFormFields() {
+               $this->form->appendChild(
+                       FormContainer::create('data')
+                               ->label('wcf.global.form.data')
+               );
+               
+               $this->pipObject->getPip()->addFormFields($this->form);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               $this->pipObject->getPip()->addEntry($this->form);
+               
+               $this->saved();
+               
+               // re-build form after having created a new object
+               if ($this->formAction === 'create') {
+                       $this->buildForm();
+                       $this->addPipFormFields();
+               }
+               
+               WCF::getTPL()->assign('success', true);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function setFormAction() {
+               $this->form->action(LinkHandler::getInstance()->getLink('DevtoolsProjectPipEntryAdd', [
+                       'id' => $this->project->projectID,
+                       'pip' => $this->pip
+               ]));
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'add',
+                       'pip' => $this->pip,
+                       'pipObject' => $this->pipObject,
+                       'project' => $this->project
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectPipEntryEditForm.class.php b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectPipEntryEditForm.class.php
new file mode 100644 (file)
index 0000000..64a6ac7
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+declare(strict_types=1);
+namespace wcf\acp\form;
+use wcf\form\AbstractForm;
+use wcf\form\AbstractFormBuilderForm;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to edit an exiting entry for a specific pip and project.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ * @since      3.2
+ */
+class DevtoolsProjectPipEntryEditForm extends DevtoolsProjectPipEntryAddForm {
+       /**
+        * identifier of the edited pip entry
+        * @var string
+        */
+       public $identifier = '';
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['identifier'])) $this->identifier = StringUtil::trim($_REQUEST['identifier']);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               if (!empty($_POST)) {
+                       $this->pipObject->getPip()->setEditedEntryIdentifier($this->identifier);
+               }
+               
+               parent::readData();
+               
+               if (empty($_POST)) {
+                       if (!$this->pipObject->getPip()->setEntryData($this->identifier, $this->form)) {
+                               throw new IllegalLinkException();
+                       }
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       protected function addPipFormFields() {
+               $this->form->formMode(IFormDocument::FORM_MODE_UPDATE);
+               
+               parent::addPipFormFields();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function setFormAction() {
+               $this->form->action(LinkHandler::getInstance()->getLink('DevtoolsProjectPipEntryEdit', [
+                       'id' => $this->project->projectID,
+                       'pip' => $this->pip,
+                       'identifier' => $this->identifier
+               ]));
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               $newIdentifier = $this->pipObject->getPip()->editEntry($this->form, $this->identifier);
+               
+               $this->saved();
+               
+               if ($this->identifier !== $newIdentifier) {
+                       // reload the page with the new identifier and store success
+                       // message in session variables
+                       WCF::getSession()->register($this->project->projectID . '-' . $this->pip . '-success', 1);
+                       
+                       HeaderUtil::redirect(LinkHandler::getInstance()->getLink('DevtoolsProjectPipEntryEdit', [
+                               'id' => $this->project->projectID,
+                               'pip' => $this->pip,
+                               'identifier' => $newIdentifier
+                       ]));
+                       exit;
+               }
+               else {
+                       WCF::getTPL()->assign('success', true);
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               // check if a success message has been stored in session variables
+               // from previous request 
+               if (WCF::getSession()->getVar($this->project->projectID . '-' . $this->pip . '-success') == 1) {
+                       WCF::getSession()->unregister($this->project->projectID . '-' . $this->pip . '-success');
+                       
+                       WCF::getTPL()->assign('success', true);
+               }
+               
+               WCF::getTPL()->assign([
+                       'action' => 'edit'
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipEntryListPage.class.php b/wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipEntryListPage.class.php
new file mode 100644 (file)
index 0000000..dbce9f1
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+declare(strict_types=1);
+namespace wcf\acp\page;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\page\AbstractPage;
+use wcf\system\devtools\pip\DevtoolsPip;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\package\plugin\BoxPackageInstallationPlugin;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the list of entries of a specific pip for a specific project.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ * @since      3.2
+ */
+class DevtoolsProjectPipEntryListPage extends AbstractPage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
+       
+       /**
+        * name of the requested pip
+        * @var string
+        */
+       public $pip = '';
+       
+       /**
+        * requested pip
+        * @var DevtoolsPip
+        */
+       protected $pipObject;
+       
+       /**
+        * devtools project
+        * @var DevtoolsProject
+        */
+       public $project;
+       
+       /**
+        * project id
+        * @var integer
+        */
+       public $projectID = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->projectID = intval($_REQUEST['id']);
+               $this->project = new DevtoolsProject($this->projectID);
+               if (!$this->project->projectID) {
+                       throw new IllegalLinkException();
+               }
+               
+               $this->project->validatePackageXml();
+               
+               if (isset($_REQUEST['pip'])) $this->pip = StringUtil::trim($_REQUEST['pip']);
+               
+               $filteredPips = array_filter($this->project->getPips(), function(DevtoolsPip $pip) {
+                       return $pip->pluginName === $this->pip;
+               });
+               if (count($filteredPips) === 1) {
+                       $this->pipObject = reset($filteredPips);
+               }
+               else {
+                       throw new IllegalLinkException();
+               }
+               
+               if (!$this->pipObject->supportsGui()) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               /** @var IDevtoolsPipEntryList entryList */
+               $this->entryList = $this->pipObject->getPip()->getEntryList();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'entryList' => $this->entryList,
+                       'pip' => $this->pip,
+                       'project' => $this->project
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipListPage.class.php b/wcfsetup/install/files/lib/acp/page/DevtoolsProjectPipListPage.class.php
new file mode 100644 (file)
index 0000000..05ff2f1
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+declare(strict_types=1);
+namespace wcf\acp\page;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\page\AbstractPage;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the pip data of a project.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ * @since      3.2
+ */
+class DevtoolsProjectPipListPage extends AbstractPage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
+       
+       /**
+        * devtools project
+        * @var DevtoolsProject
+        */
+       public $project;
+       
+       /**
+        * project id
+        * @var integer
+        */
+       public $projectID = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->projectID = intval($_REQUEST['id']);
+               $this->project = new DevtoolsProject($this->projectID);
+               if (!$this->project->projectID) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'project' => $this->project
+               ]);
+       }
+}
index 9119097d8d2d1422400d9423752ef98e8c49a338..938796c0206323e6f40a4f4b134a0a864fa30853 100644 (file)
@@ -51,7 +51,10 @@ class DevtoolsProject extends DatabaseObject {
                
                $pips = [];
                foreach ($pipList as $pip) {
-                       $pips[] = new DevtoolsPip($pip);
+                       $pip = new DevtoolsPip($pip);
+                       $pip->setProject($this);
+                       
+                       $pips[] = $pip;
                }
                
                return $pips;
index 5f541b77be1471003b5540cf1a120b1abc3083fd..2c6820eddfa2a58035ef6d4058d88517f4afa0c7 100644 (file)
@@ -17,7 +17,7 @@ use wcf\system\package\PackageInstallationDispatcher;
  */
 class DevtoolsPackageInstallationDispatcher extends PackageInstallationDispatcher {
        /**
-        * @var DevtoolsPackageArchive
+        * @var DevtoolsProject
         */
        protected $project;
        
@@ -65,4 +65,14 @@ class DevtoolsPackageInstallationDispatcher extends PackageInstallationDispatche
                /** @noinspection PhpParamsInspection */
                return new DevtoolsInstaller($this->project, $targetDir, $sourceArchive, $fileHandler);
        }
+       
+       /**
+        * Returns the project the installation dispatcher is created for.
+        * 
+        * @return      DevtoolsProject
+        * @since       3.2
+        */
+       public function getProject() {
+               return $this->project;
+       }
 }
index 71e7a8f0d934294ccac983cb7a83012cf2e3e4a1..21b914fd918cd5982458cd0525547d82d0a6ec27 100644 (file)
@@ -5,6 +5,7 @@ use wcf\data\devtools\project\DevtoolsProject;
 use wcf\data\package\installation\plugin\PackageInstallationPlugin;
 use wcf\data\DatabaseObjectDecorator;
 use wcf\system\application\ApplicationHandler;
+use wcf\system\package\plugin\IPackageInstallationPlugin;
 use wcf\system\WCF;
 use wcf\util\FileUtil;
 use wcf\util\JSON;
@@ -22,6 +23,20 @@ use wcf\util\JSON;
  * @mixin      PackageInstallationPlugin
  */
 class DevtoolsPip extends DatabaseObjectDecorator {
+       /**
+        * project the pip object belongs to
+        * @var DevtoolsProject
+        * @since       3.2
+        */
+       protected $project;
+       
+       /**
+        * package installation plugin object
+        * @var IPackageInstallationPlugin
+        * @since       3.2
+        */
+       protected $pip;
+       
        /**
         * @inheritDoc
         */
@@ -67,12 +82,63 @@ class DevtoolsPip extends DatabaseObjectDecorator {
                return $this->classExists() && $this->getDefaultFilename() && $this->isIdempotent();
        }
        
+       /**
+        * Returns `true` if this pip supports adding and editing entries via a gui.
+        * 
+        * @return      boolean
+        * @since       3.2
+        */
+       public function supportsGui() {
+               return $this->isSupported() && is_subclass_of($this->getDecoratedObject()->className, IGuiPackageInstallationPlugin::class);
+       }
+       
        public function getSyncDependencies($toJson = true) {
                $dependencies = call_user_func([$this->getDecoratedObject()->className, 'getSyncDependencies']);
                
                return ($toJson) ? JSON::encode($dependencies) : $dependencies;
        }
        
+       /**
+        * Returns the project this object belongs to.
+        * 
+        * @return      DevtoolsProject
+        * @since       3.2
+        */
+       public function getProject() {
+               return $this->project;
+       }
+       
+       /**
+        * Sets the project this object belongs to.
+        * 
+        * @param       DevtoolsProject         $project
+        * @since       3.2
+        */
+       public function setProject(DevtoolsProject $project) {
+               $this->project = $project;
+       }
+       
+       /**
+        * Returns the package installation plugin object for this pip.
+        * 
+        * Note: No target will be set for the package installation plugin object.
+        * 
+        * @return      IPackageInstallationPlugin
+        * @since       3.2
+        */
+       public function getPip() {
+               if ($this->pip === null) {
+                       $className = $this->getDecoratedObject()->className;
+                       
+                       $this->pip = new $className(
+                               new DevtoolsPackageInstallationDispatcher($this->getProject())
+                               // no target
+                       );
+               }
+               
+               return $this->pip;
+       }
+       
        /**
         * Returns the first validation error.
         * 
diff --git a/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPipEntryList.class.php b/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPipEntryList.class.php
new file mode 100644 (file)
index 0000000..b76ebaa
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\devtools\pip;
+
+/**
+ * Default implementation of a list of entries of a specific pip and specific
+ * project.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Pip
+ * @since      3.2
+ */
+class DevtoolsPipEntryList implements IDevtoolsPipEntryList {
+       /**
+        * pip entries
+        * @var array<array>
+        */
+       protected $entries = [];
+       
+       /**
+        * keys of the entries that can be used to display the entry list as a
+        * table
+        * @var string[]
+        */
+       protected $keys;
+       
+       /**
+        * @inheritDoc
+        */
+       public function addEntry(string $id, array $entry) {
+               if ($this->keys === null) {
+                       throw new \BadMethodCallException("No keys have been set.");
+               }
+               
+               if (isset($this->entries[$id])) {
+                       throw new \InvalidArgumentException("Entry with id '{$id}' already exists.");
+               }
+               
+               foreach ($entry as $key => $value) {
+                       if (!isset($this->keys[$key])) {
+                               throw new \InvalidArgumentException("Unknown key '{$key}'.");
+                       }
+               }
+               
+               $this->entries[$id] = $entry;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getEntries(): array {
+               return $this->entries;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getKeys(): array {
+               if ($this->keys === null) {
+                       throw new \BadMethodCallException("No keys have been set.");
+               }
+               
+               return $this->keys;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function setKeys(array $keys) {
+               if ($this->keys !== null) {
+                       throw new \BadMethodCallException("Keys have already been set.");
+               }
+               
+               foreach ($keys as $key => $value) {
+                       if (!is_string($key)) {
+                               throw new \InvalidArgumentException("Given key is no string, " . gettype($key) . " given.");
+                       }
+                       
+                       if (!is_string($value)) {
+                               throw new \InvalidArgumentException("Given value is no string, " . gettype($value) . " given.");
+                       }
+               }
+               
+               $this->keys = $keys;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/devtools/pip/IDevtoolsPipEntryList.class.php b/wcfsetup/install/files/lib/system/devtools/pip/IDevtoolsPipEntryList.class.php
new file mode 100644 (file)
index 0000000..2c50d85
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\devtools\pip;
+
+/**
+ * Represents a list of entries of a specific pip and specific project.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Pip
+ * @since      3.2
+ */
+interface IDevtoolsPipEntryList {
+       /**
+        * Adds an entry to the entry list.
+        * 
+        * Before adding entries, the keys must be set.
+        * 
+        * @param       string          $identifier     unique entry ident
+        * @param       array           $entry          entry data
+        * @throws      \BadMethodCallException         if no keys have been set
+        */
+       public function addEntry(string $id, array $entry);
+       
+       /**
+        * Returns all entries in the list.
+        * 
+        * @return      array
+        */
+       public function getEntries(): array;
+       
+       /**
+        * Returns the expected keys of the entries that can be used to display the
+        * entry list as a table.
+        * 
+        * The keys of the returned array are the entry keys and the array values are
+        * language items describing the value.
+        * 
+        * @return      array
+        * @throws      \BadMethodCallException         if no keys have been set
+        */
+       public function getKeys(): array;
+       
+       /**
+        * Sets the keys of the entries that can be used to display the entry list
+        * as a table.
+        * 
+        * @param       array           $keys           entry keys
+        */
+       public function setKeys(array $keys);
+}
diff --git a/wcfsetup/install/files/lib/system/devtools/pip/IGuiPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/devtools/pip/IGuiPackageInstallationPlugin.class.php
new file mode 100644 (file)
index 0000000..571bfcb
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\devtools\pip;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\system\form\builder\IFormDocument;
+
+/**
+ * Default interface for package installation plugins that support adding and editing
+ * entries via a graphical user interface in the developer tools.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Pip
+ * @since      3.2
+ */
+interface IGuiPackageInstallationPlugin extends IIdempotentPackageInstallationPlugin {
+       /**
+        * Adds a new entry of this pip based on the data provided by the given
+        * form.
+        * 
+        * @param       IFormDocument           $form
+        */
+       public function addEntry(IFormDocument $form);
+       
+       /**
+        * Adds all fields to the given form to add or edit an entry.
+        *
+        * @param       IFormDocument           $form
+        */
+       public function addFormFields(IFormDocument $form);
+       
+       /**
+        * Edits the entry of this pip with the given identifier based on the data
+        * provided by the given form and returns the new identifier of the entry
+        * (or the old identifier if it has not changed).
+        * 
+        * @param       IFormDocument           $form
+        * @param       string                  $identifier
+        * @return      string                  new identifier
+        */
+       public function editEntry(IFormDocument $form, string $identifier): string;
+       
+       /**
+        * Returns additional template code for the form to add and edit entries.
+        * 
+        * @return      string
+        */
+       public function getAdditionalTemplateCode(): string;
+       
+       /**
+        * Returns a list of all pip entries of this pip. 
+        * 
+        * @return      IDevtoolsPipEntryList
+        */
+       public function getEntryList(): IDevtoolsPipEntryList;
+       
+       /**
+        * Informs the pip of the identifier of the edited entry if the form to
+        * edit that entry has been submitted.
+        * 
+        * @param       string          $identifier
+        * 
+        * @throws      \InvalidArgumentException       if no such entry exists
+        */
+       public function setEditedEntryIdentifier(string $identifier);
+       
+       /**
+        * Adds the data of the pip entry with the given identifier into the
+        * given form and returns `true`. If no entry with the given identifier
+        * exists, `false` is returned.
+        * 
+        * @param       string                  $identifier
+        * @param       IFormDocument           $document
+        * @return      bool
+        */
+       public function setEntryData(string $identifier, IFormDocument $document): bool;
+}
diff --git a/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php
new file mode 100644 (file)
index 0000000..a63e1bc
--- /dev/null
@@ -0,0 +1,371 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\devtools\pip;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\data\IEditableCachedObject;
+use wcf\system\form\builder\field\IFormField;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\form\builder\IFormNode;
+use wcf\system\WCF;
+use wcf\util\DOMUtil;
+use wcf\util\StringUtil;
+use wcf\util\XML;
+
+/**
+ * Provides default implementations of the methods of the
+ *     `wcf\system\devtools\pip\IGuiPackageInstallationPlugin`
+ * interface for an xml-based package installation plugin.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Pip
+ * @since      3.2
+ */
+trait TXmlGuiPackageInstallationPlugin {
+       /**
+        * dom element representing the original data of the edited element
+        * @var null|\DOMElement
+        */
+       protected $editedEntry;
+       
+       /**
+        * Adds a new entry of this pip based on the data provided by the given
+        * form.
+        *
+        * @param       IFormDocument           $form
+        */
+       public function addEntry(IFormDocument $form) {
+               $xml = $this->getProjectXml();
+               $document = $xml->getDocument();
+               
+               $newElement = $this->writeEntry($document, $form);
+               $this->sortDocument($document);
+               
+               /** @var DevtoolsProject $project */
+               $project = $this->installation->getProject();
+               
+               // TODO: while creating/testing the gui, write into a temporary file
+               // $xml->write($this->getXmlFileLocation($project));
+               $xml->write($project->path . ($project->getPackage()->package === 'com.woltlab.wcf' ? 'com.woltlab.wcf/' : '') . 'tmp_' . static::getDefaultFilename());
+               
+               $this->saveObject($newElement);
+       }
+       
+       /**
+        * Edits the entry of this pip with the given identifier based on the data
+        * provided by the given form and returns the new identifier of the entry
+        * (or the old identifier if it has not changed).
+        *
+        * @param       IFormDocument           $form
+        * @param       string                  $identifier
+        * @return      string                  new identifier
+        */
+       public function editEntry(IFormDocument $form, string $identifier): string {
+               $xml = $this->getProjectXml();
+               $document = $xml->getDocument();
+               
+               // remove old element
+               $element = $this->getElementByIdentifier($xml, $identifier);
+               DOMUtil::removeNode($element);
+               
+               // add updated element
+               $newEntry = $this->writeEntry($document, $form);
+               $this->sortDocument($document);
+               
+               /** @var DevtoolsProject $project */
+               $project = $this->installation->getProject();
+               
+               // TODO: while creating/testing the gui, write into a temporary file
+               // $xml->write($this->getXmlFileLocation($project));
+               $xml->write($project->path . ($project->getPackage()->package === 'com.woltlab.wcf' ? 'com.woltlab.wcf/' : '') . 'tmp_' . static::getDefaultFilename());
+               
+               $this->saveObject($newEntry, $element);
+               
+               return $this->getElementIdentifier($newEntry);
+       }
+       
+       /**
+        * Returns additional template code for the form to add and edit entries.
+        * 
+        * @return      string
+        */
+       public function getAdditionalTemplateCode(): string {
+               return '';
+       }
+       
+       /**
+        * Checks if the given string needs to be encapsuled by cdata and does so
+        * if required.
+        * 
+        * @param       string          $value
+        * @return      string
+        */
+       protected function getAutoCdataValue(string $value): string {
+               if (strpos('<', $value) !== false || strpos('>', $value) !== false || strpos('&', $value) !== false) {
+                       $value = '<![CDATA[' . StringUtil::escapeCDATA($value) . ']]>';
+               }
+               
+               return $value;
+       }
+       
+       /**
+        * Returns the `import` element with the given identifier.
+        * 
+        * @param       XML     $xml
+        * @param       string  $identifier
+        * @return      \DOMElement|null
+        */
+       protected function getElementByIdentifier(XML $xml, string $identifier) {
+               foreach ($xml->xpath()->query('/ns:data/ns:import/ns:' . $this->tagName) as $element) {
+                       if ($this->getElementIdentifier($element) === $identifier) {
+                               return $element;
+                       }
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Extracts the PIP object data from the given XML element.
+        *
+        * @param       \DOMElement     $element
+        * @return      array
+        */
+       abstract protected function getElementData(\DOMElement $element): array;
+       
+       /**
+        * Returns the identifier of the given `import` element.
+        * 
+        * @param       \DOMElement     $element
+        * @return      string
+        */
+       abstract protected function getElementIdentifier(\DOMElement $element): string;
+       
+       /**
+        * Returns the xml code of an empty xml file with the appropriate structure
+        * present for a new entry to be added as if it was added to an existing
+        * file.
+        * 
+        * @return      string
+        */
+       protected function getEmptyXml(): string {
+               $classNamePieces = explode('\\', get_class($this));
+               $xsdFilename = lcfirst(str_replace('PackageInstallationPlugin', '', array_pop($classNamePieces)));
+               
+               return <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<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/vortex/{$xsdFilename}.xsd">
+       <import></import>
+</data>
+XML;
+       }
+       
+       /**
+        * Returns the xml object for this pip.
+        * 
+        * @return      XML
+        */
+       protected function getProjectXml(): XML {
+               $fileLocation = $this->getXmlFileLocation();
+               
+               $xml = new XML();
+               if (!file_exists($fileLocation)) {
+                       $xml->loadXML($fileLocation, $this->getEmptyXml());
+               }
+               else {
+                       $xml->load($fileLocation);
+               }
+               
+               return $xml;
+       }
+       
+       /**
+        * Returns the location of the xml file for this pip.
+        * 
+        * @return      string
+        */
+       protected function getXmlFileLocation(): string {
+               /** @var DevtoolsProject $project */
+               $project = $this->installation->getProject();
+               
+               return $project->path . ($project->getPackage()->package === 'com.woltlab.wcf' ? 'com.woltlab.wcf/' : '') . static::getDefaultFilename();
+       }
+       
+       /**
+        * Saves an object represented by an XML element in the database by either
+        * creating a new element (if `$oldElement = null`) or updating an existing
+        * element.
+        *
+        * @param       \DOMElement             $newElement     XML element with new data
+        * @param       \DOMElement|null        $oldElement     XML element with old data
+        */
+       protected function saveObject(\DOMElement $newElement, \DOMElement $oldElement = null) {
+               $newElementData = $this->getElementData($newElement);
+               
+               if ($oldElement === null) {
+                       call_user_func([$this->className, 'create'], $newElementData);
+               }
+               else {
+                       $sqlData = $this->findExistingItem($this->getElementData($oldElement));
+                       
+                       $statement = WCF::getDB()->prepareStatement($sqlData['sql']);
+                       $statement->execute($sqlData['parameters']);
+                       
+                       $baseClass = call_user_func([$this->className, 'getBaseClass']);
+                       $itemEditor = new $this->className(new $baseClass(null, $statement->fetchArray()));
+                       $itemEditor->update($newElementData);
+               }
+               
+               if (is_subclass_of($this->className, IEditableCachedObject::class)) {
+                       call_user_func([$this->className, 'resetCache']);
+               }
+       }
+       
+       /**
+        * Informs the pip of the identifier of the edited entry if the form to
+        * edit that entry has been submitted.
+        *
+        * @param       string          $identifier
+        * 
+        * @throws      \InvalidArgumentException       if no such entry exists
+        */
+       public function setEditedEntryIdentifier(string $identifier) {
+               $this->editedEntry = $this->getElementByIdentifier($this->getProjectXml(), $identifier);
+               
+               if ($this->editedEntry === null) {
+                       throw new \InvalidArgumentException("Unknown entry with identifier '{$identifier}'.");
+               }
+       }
+       
+       /**
+        * Adds the data of the pip entry with the given identifier into the
+        * given form and returns `true`. If no entry with the given identifier
+        * exists, `false` is returned.
+        *
+        * @param       string                  $identifier
+        * @param       IFormDocument           $document
+        * @return      bool
+        */
+       public function setEntryData(string $identifier, IFormDocument $document): bool {
+               $xml = $this->getProjectXml();
+               
+               $element = $this->getElementByIdentifier($xml, $identifier);
+               if ($element === null) {
+                       return false;
+               }
+               
+               $data = [];
+               /** @var \DOMNode $attribute */
+               foreach ($element->attributes as $attribute) {
+                       $data[$attribute->nodeName] = $attribute->nodeValue;
+               }
+               foreach ($element->childNodes as $childNode) {
+                       if ($childNode instanceof \DOMText) {
+                               $data['__value'] = $childNode->nodeValue;
+                       }
+                       else {
+                               $data[$childNode->nodeName] = $childNode->nodeValue;
+                       }
+               }
+               
+               /** @var IFormNode $node */
+               foreach ($document->getIterator() as $node) {
+                       // `data-tag` is used to map the field id to the xml element tag
+                       $key = $node->hasAttribute('data-tag') ? $node->getAttribute('data-tag') : $node->getId();
+                       
+                       if ($node instanceof IFormField && $node->isAvailable() && isset($data[$key])) {
+                               $node->value($data[$key]);
+                       }
+               }
+               
+               return true;
+       }
+       
+       /**
+        * Sorts the entries of this pip that are represented by the given dom
+        * document to achieve a deterministic order.
+        * 
+        * @param       \DOMDocument    $document
+        */
+       abstract protected function sortDocument(\DOMDocument $document);
+       
+       /**
+        * Sorts the given child nodes of all nodes in the given node list by
+        * applying the given sort function on the child nodes.
+        * 
+        * Internally, the old child nodes are removed and appended again in
+        * the sorted order.
+        * 
+        * @param       \DOMNodeList    $nodeList
+        * @param       callable        $sortFunction
+        */
+       protected function sortChildNodes(\DOMNodeList $nodeList, callable $sortFunction) {
+               foreach ($nodeList as $node) {
+                       $childNodes = array_filter(iterator_to_array($node->childNodes), function($element) {
+                               return $element instanceof \DOMElement;
+                       });
+                       
+                       usort($childNodes, $sortFunction);
+                       
+                       // remove old nodes
+                       while ($node->hasChildNodes()) {
+                               $node->removeChild($node->firstChild);
+                       }
+                       
+                       // add sorted nodes
+                       foreach ($childNodes as $childNode) {
+                               $node->appendChild($childNode);
+                       }
+               }
+       }
+       
+       /**
+        * Sorts the standard `import` and `delete` blocks and ensures that the
+        * `import` block is before the `delete` block.
+        * 
+        * @param       \DOMDocument    $document
+        */
+       protected function sortImportDelete(\DOMDocument $document) {
+               switch ($document->documentElement->childNodes->length) {
+                       case 0:
+                               throw new \InvalidArgumentException('Empty xml document.');
+                       
+                       case 1:
+                               // nothing to sort
+                               break;
+                       
+                       case 2:
+                               $firstChild = $document->documentElement->firstChild;
+                               $lastChild = $document->documentElement->lastChild;
+                               
+                               if (!($firstChild->nodeName === 'import' && $lastChild->nodeName === 'delete') && !($firstChild->nodeName === 'delete' && $lastChild->nodeName === 'import')) {
+                                       throw new \InvalidArgumentException('Invalid xml given.');
+                               }
+                               
+                               if ($document->documentElement->firstChild->nodeName !== 'import') {
+                                       $firstChild = $document->documentElement->firstChild;
+                                       $document->documentElement->removeChild($firstChild);
+                                       $document->documentElement->appendChild($firstChild);
+                               }
+                               break;
+                       
+                       default:
+                               throw new \InvalidArgumentException('Xml document has more than two direct children.');
+               }
+       }
+       
+       /**
+        * Writes a new entry into the xml structure represented by the given
+        * dom document using the data provided by the given form and return
+        * the new dom element.
+        * 
+        * Note: Inserting at the correct position regarding sorting is irrelevant
+        * as the dom document will be sorted after adding the entry.
+        * 
+        * @param       \DOMDocument            $document
+        * @param       IFormDocument           $form
+        * @return      \DOMElement
+        */
+       abstract protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement; 
+}
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/data/GuiPackageInstallationPluginFormFieldDataProcessor.class.php b/wcfsetup/install/files/lib/system/form/builder/field/data/GuiPackageInstallationPluginFormFieldDataProcessor.class.php
new file mode 100644 (file)
index 0000000..85a4707
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\form\builder\field\data;
+use wcf\system\form\builder\field\IFormField;
+use wcf\system\form\builder\IFormNode;
+use wcf\system\form\builder\IFormParentNode;
+
+/**
+ * Form field data processor for gui package installation plugin forms that support
+ * the `data-tag` that should be used instead of the id if present.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Form\Builder\Field\Data
+ * @since      3.2
+ */
+class GuiPackageInstallationPluginFormFieldDataProcessor extends DefaultFormFieldDataProcessor {
+       /**
+        * Fetches all data from the given node and stores it in the given array.
+        * 
+        * @param       IFormNode       $node           node whose data will be fetched
+        * @param       array           $data           data storage
+        */
+       protected function getData(IFormNode $node, array &$data) {
+               if ($node->checkDependencies()) {
+                       if ($node instanceof IFormParentNode) {
+                               foreach ($node as $childNode) {
+                                       $this->getData($childNode, $data);
+                               }
+                       }
+                       else if ($node instanceof IFormField && $node->isAvailable() && $node->hasSaveValue()) {
+                               $data[$node->hasAttribute('data-tag') ? $node->getAttribute('data-tag') : $node->getId()] = $node->getSaveValue();
+                       }
+               }
+       }
+}
index 9bbc8719a5d3e2ec566e74d6ba56b4188b1189d9..c47af55ae89a4573575972c86bda966afec1c1f3 100644 (file)
@@ -116,14 +116,21 @@ abstract class AbstractXMLPackageInstallationPlugin extends AbstractPackageInsta
                }
        }
        
+       /**
+        * @param       \DOMXPath       $xpath
+        * @return      \DOMNodeList
+        */
+       protected function getImportElements(\DOMXPath $xpath) {
+               return $xpath->query('/ns:data/ns:import/ns:'.$this->tagName);
+       }
+       
        /**
         * Imports or updates items.
         * 
         * @param       \DOMXPath       $xpath
         */
        protected function importItems(\DOMXPath $xpath) {
-               $elements = $xpath->query('/ns:data/ns:import/ns:'.$this->tagName);
-               foreach ($elements as $element) {
+               foreach ($this->getImportElements($xpath) as $element) {
                        $data = [
                                'attributes' => [],
                                'elements' => [],
index be225c07365fbc8c6f447b4151629c927a083105..ad7dc66abac0cb2ebdaf75f3a97ac070b88a8135 100644 (file)
@@ -2,7 +2,15 @@
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
 use wcf\data\object\type\definition\ObjectTypeDefinitionEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
 use wcf\system\WCF;
 
 /**
@@ -13,7 +21,9 @@ use wcf\system\WCF;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Acp\Package\Plugin
  */
-class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -68,8 +78,185 @@ class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageIn
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               $form->getNodeById('data')->appendChildren([
+                       TextFormField::create('name')
+                               ->label('wcf.acp.pip.objectTypeDefinition.definitionName')
+                               ->description('wcf.acp.pip.objectTypeDefinition.definitionName.description', ['project' => $this->installation->getProject()])
+                               ->required()
+                               ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                       if ($formField->getValue()) {
+                                               $segments = explode('.', $formField->getValue());
+                                               if (count($segments) < 4) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'tooFewSegments',
+                                                                       'wcf.acp.pip.objectTypeDefinition.definitionName.error.tooFewSegments',
+                                                                       ['segmentCount' => count($segments)]
+                                                               )
+                                                       );
+                                               }
+                                               else {
+                                                       $invalidSegments = [];
+                                                       foreach ($segments as $key => $segment) {
+                                                               if (!preg_match('~^[A-z0-9\-\_]+$~', $segment)) {
+                                                                       $invalidSegments[$key] = $segment;
+                                                               }
+                                                       }
+                                                       
+                                                       if (!empty($invalidSegments)) {
+                                                               $formField->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'invalidSegments',
+                                                                               'wcf.acp.pip.objectTypeDefinition.definitionName.error.invalidSegments',
+                                                                               ['invalidSegments' => $invalidSegments]
+                                                                       )
+                                                               );
+                                                       }
+                                               }
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       if ($formField->getValue()) {
+                                               $objectTypeDefinition = ObjectTypeCache::getInstance()->getDefinitionByName($formField->getValue());
+                                               
+                                               // the definition name is not unique if such an object type definition
+                                               // already exists and (a) a new definition is added or (b) an existing
+                                               // definition is edited but the new definition name is not the old definition
+                                               // name so that the existing definition is not the definition currently edited
+                                               if ($objectTypeDefinition !== null && (
+                                                       $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
+                                                       $this->editedEntry->getElementsByTagName('name')->item(0)->nodeValue !== $formField->getValue()
+                                               )) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'notUnique',
+                                                                       'wcf.acp.pip.objectTypeDefinition.definitionName.error.notUnique'
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       TextFormField::create('interfaceName')
+                               ->attribute('data-tag', 'interfacename')
+                               ->label('wcf.acp.pip.objectTypeDefinition.interfaceName')
+                               ->description('wcf.acp.pip.objectTypeDefinition.interfaceName.description')
+                               ->addValidator(new FormFieldValidator('interfaceExists', function(TextFormField $formField) {
+                                       if ($formField->getValue() && !interface_exists($formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'nonExistent',
+                                                               'wcf.acp.pip.objectTypeDefinition.interfaceName.error.nonExistent'
+                                                       )
+                                               );
+                                       }
+                               }))
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               $data = [
+                       'definitionName' => $element->getElementsByTagName('name')->item(0)->nodeValue,
+                       'packageID' => $this->installation->getPackage()->packageID
+               ];
+               
+               $interfaceName = $element->getElementsByTagName('interfacename')->item(0);
+               if ($interfaceName) {
+                       $data['interfaceName'] = $interfaceName->nodeValue;
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return $element->getElementsByTagName('name')->item(0)->nodeValue;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'name' => 'wcf.acp.pip.objectTypeDefinition.definitionName',
+                       'interfaceName' => 'wcf.acp.pip.objectTypeDefinition.interfaceName'
+               ]);
+               
+               /** @var \DOMElement $element */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $interfaceName = $element->getElementsByTagName('interfacename')->item(0);
+                       
+                       $entryList->addEntry($this->getElementIdentifier($element), [
+                               'name' => $element->getElementsByTagName('name')->item(0)->nodeValue,
+                               'interfaceName' => $interfaceName ? $interfaceName->nodeValue : ''
+                       ]);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp(
+                               $element1->getElementsByTagName('name')->item(0)->nodeValue,
+                               $element2->getElementsByTagName('name')->item(0)->nodeValue
+                       );
+               });
+               
+               $this->sortChildNodes($document->getElementsByTagName('delete'), function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp(
+                               $element1->getAttribute('name'),
+                               $element2->getAttribute('name')
+                       );
+               });
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               $definition = $document->createElement('definition');
+               $definition->appendChild($document->createElement('name', $form->getNodeById('name')->getSaveValue()));
+               
+               /** @var TextFormField $interfaceName */
+               $interfaceName = $form->getNodeById('interfaceName');
+               if ($interfaceName->getSaveValue()) {
+                       $definition->appendChild($document->createElement('interfacename', $interfaceName->getSaveValue()));
+               }
+               
+               $import = $document->getElementsByTagName('import')->item(0);
+               $import->appendChild($definition);
+               
+               return $definition;
+       }
 }
index 6ddf6a86a52499544241184a07dbd33ab948ae40..e20f4a76f12732dd920cb8533368157b02a7181e 100644 (file)
@@ -1,20 +1,52 @@
 <?php
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
+use wcf\data\object\type\definition\ObjectTypeDefinitionList;
+use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\object\type\ObjectTypeEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\option\Option;
+use wcf\data\user\group\option\UserGroupOptionList;
+use wcf\data\DatabaseObjectList;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\condition\AbstractIntegerCondition;
+use wcf\system\condition\UserGroupCondition;
+use wcf\system\condition\UserIntegerPropertyCondition;
+use wcf\system\condition\UserTimestampPropertyCondition;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\event\EventHandler;
 use wcf\system\exception\SystemException;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\container\IFormContainer;
+use wcf\system\form\builder\field\data\GuiPackageInstallationPluginFormFieldDataProcessor;
+use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\BooleanFormField;
+use wcf\system\form\builder\field\ClassNameFormField;
+use wcf\system\form\builder\field\FloatFormField;
+use wcf\system\form\builder\field\IntegerFormField;
+use wcf\system\form\builder\field\ItemListFormField;
+use wcf\system\form\builder\field\SingleSelectionFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\Regex;
 use wcf\system\WCF;
+use wcf\util\DirectoryUtil;
 
 /**
  * Installs, updates and deletes object types.
  * 
- * @author     Alexander Ebert
+ * @author     Alexander Ebert, Matthias Schmidt
  * @copyright  2001-2018 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Acp\Package\Plugin
  */
-class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -31,6 +63,16 @@ class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallation
         */
        public static $reservedTags = ['classname', 'definitionname', 'name'];
        
+       /**
+        * @var string[]
+        */
+       public $definitionNames = [];
+       
+       /**
+        * @var string[]
+        */
+       public $definitionNamesWithInterface = [];
+       
        /**
         * Returns the id of the object type definition with the given name.
         * 
@@ -112,4 +154,874 @@ class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallation
        public static function getSyncDependencies() {
                return ['objectTypeDefinition'];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getAdditionalTemplateCode(): string {
+               return WCF::getTPL()->fetch('__objectTypePipGui', 'wcf', [
+                       'definitionNames' => $this->definitionNames,
+                       'definitionNamesWithInterface' => $this->definitionNamesWithInterface
+               ], true);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               $data = [
+                       'definitionID' => $this->getDefinitionID($element->getElementsByTagName('definitionname')->item(0)->nodeValue),
+                       'objectType' => $element->getElementsByTagName('name')->item(0)->nodeValue,
+                       'packageID' => $this->installation->getPackage()->packageID
+               ];
+               
+               $className = $element->getElementsByTagName('classname')->item(0);
+               if ($className) {
+                       $data['classname'] = $className->nodeValue;
+               }
+               
+               $additionalData = [];
+               
+               /** @var \DOMElement $child */
+               foreach ($element->childNodes as $child) {
+                       if (!in_array($child->nodeName, self::$reservedTags)) {
+                               $additionalData[$child->nodeName] = $child->nodeValue;
+                       }
+               }
+               
+               $data['additionalData'] = serialize($additionalData);
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               // add custom data processor
+               $form->getDataHandler()->add(new GuiPackageInstallationPluginFormFieldDataProcessor());
+               
+               // read available object type definitions
+               $list = new ObjectTypeDefinitionList();
+               $list->sqlOrderBy = 'definitionName';
+               $list->readObjects();
+               
+               foreach ($list as $definition) {
+                       $this->definitionNames[$definition->definitionName] = $definition->definitionName;
+                       
+                       if ($definition->interfaceName) {
+                               $this->definitionNamesWithInterface[$definition->definitionName] = $definition->interfaceName;
+                       }
+               }
+               
+               // add default form fields
+               $form->getNodeById('data')->appendChildren([
+                       SingleSelectionFormField::create('definitionName')
+                               ->attribute('data-tag', 'definitionname')
+                               ->label('wcf.acp.pip.objectType.definitionName')
+                               ->description('<!-- will be replaced by JavaScript -->')
+                               ->options($this->definitionNames)
+                               ->required(),
+                       
+                       TextFormField::create('objectType')
+                               ->attribute('data-tag', 'name')
+                               ->label('wcf.acp.pip.objectType.objectType')
+                               ->description('wcf.acp.pip.objectType.objectType.description')
+                               ->required()
+                               ->addValidator($this->getObjectTypeAlikeValueValidator('objectType'))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       $definitionName = $formField->getDocument()->getNodeById('definitionName')->getValue();
+                                       if ($definitionName) {
+                                               $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
+                                                       ObjectTypeCache::getInstance()->getDefinitionByName($definitionName)->definitionName,
+                                                       $formField->getValue()
+                                               );
+                                               
+                                               // the object type name is not unique if such an object type already exists
+                                               // and (a) a new object type is added or (b) the existing object type is
+                                               // different from the edited object type
+                                               if ($objectType !== null && (
+                                                               $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
+                                                               $this->editedEntry->getElementsByTagName('name')->item(0)->nodeValue !== $formField->getValue() ||
+                                                               $this->editedEntry->getElementsByTagName('definitionname')->item(0)->nodeValue !== $definitionName
+                                                       )) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'notUnique',
+                                                                       'wcf.acp.pip.objectType.objectType.error.notUnique'
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       ClassNameFormField::create('className')
+                               ->attribute('data-tag', 'classname')
+                               ->label('wcf.acp.pip.objectType.className')
+                               ->description('<!-- will be replaced by JavaScript -->')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('implementsInterface', function(TextFormField $formField) {
+                                       $definitionName = $formField->getDocument()->getNodeById('definitionName')->getValue();
+                                       if ($definitionName) {
+                                               $definition = ObjectTypeCache::getInstance()->getDefinitionByName($definitionName);
+                                               
+                                               if (!is_subclass_of($formField->getValue(), $definition->interfaceName)) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'interface',
+                                                                       'wcf.form.field.className.error.interface',
+                                                                       ['interface' => $definition->interfaceName]
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+               ]);
+               
+               /** @var SingleSelectionFormField $definitionName */
+               $definitionName = $form->getNodeById('definitionName');
+               
+               // add general field dependencies
+               $form->getNodeById('className')->addDependency(
+                       ValueFormFieldDependency::create('definitionName')
+                               ->field($definitionName)
+                               ->values(array_keys($this->definitionNamesWithInterface))
+               );
+               
+               // add object type-specific fields
+               
+               // reusable validators
+               $optionValidator = new FormFieldValidator('optionsExist', function(ItemListFormField $formField) {
+                       $options = $formField->getValue();
+                       if (is_array($options)) {
+                               $definedOptions = Option::getOptions();
+                               
+                               $options = array_filter($options, function(string $option) use ($definedOptions) {
+                                       return !isset($definedOptions[strtoupper($option)]);
+                               });
+                               
+                               if (!empty($options)) {
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'nonExistent',
+                                                       'wcf.acp.pip.general.options.error.nonExistent',
+                                                       ['options' => $options]
+                                               )
+                                       );
+                               }
+                       }
+               });
+               
+               $permissionValidator = new FormFieldValidator('permissionsExist', function(ItemListFormField $formField) {
+                       $permissions = $formField->getValue();
+                       if (is_array($permissions)) {
+                               $userGroupOptionList = new UserGroupOptionList();
+                               $userGroupOptionList->getConditionBuilder()->add('optionName IN (?)', [$permissions]);
+                               $userGroupOptionList->readObjects();
+                               
+                               if (count($userGroupOptionList) !== count($permissions)) {
+                                       foreach ($userGroupOptionList as $userGroupOption) {
+                                               unset($permissions[array_search($userGroupOption->optionName, $permissions)]);
+                                       }
+                                       
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'nonExistent',
+                                                       'wcf.acp.pip.general.permissions.error.nonExistent',
+                                                       ['permissions' => $permissions]
+                                               )
+                                       );
+                               }
+                       }
+               });
+               
+               // com.woltlab.wcf.adLocation
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.adLocation')
+                       ->appendChildren([
+                               TextFormField::create('adLocationCategoryName')
+                                       ->attribute('data-tag', 'categoryname')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.description')
+                                       ->addValidator($this->getObjectTypeAlikeValueValidator('com.woltlab.wcf.adLocation.categoryName')),
+                               ItemListFormField::create('adLocationCssClassName')
+                                       ->attribute('data-tag', 'cssclassname')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.description')
+                                       ->saveValueType(ItemListFormField::SAVE_VALUE_TYPE_SSV)
+                                       ->addValidator(new FormFieldValidator('format', function(ItemListFormField $formField) {
+                                               if (!empty($formField->getValue())) {
+                                                       $invalidClasses = [];
+                                                       foreach ($formField->getValue() as $class) {
+                                                               if (preg_match('~^-?[_A-z][_A-z0-9-]*$~', $class) !== 1) {
+                                                                       $invalidClasses[] = $class;
+                                                               }
+                                                       }
+                                                       
+                                                       if (!empty($invalidClasses)) {
+                                                               $formField->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'invalid',
+                                                                               'wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.error.invalid',
+                                                                               ['invalidClasses' => $invalidClasses]
+                                                                       )
+                                                               );
+                                                       }
+                                               }
+                                       }))
+                       ]);
+               
+               // com.woltlab.wcf.attachment.objectType
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.attachment.objectType')
+                       ->appendChild(
+                               BooleanFormField::create('attachmentPrivate')
+                                       ->attribute('data-tag', 'private')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private.description')
+                       );
+               
+               // com.woltlab.wcf.bulkProcessing.user.action
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.bulkProcessing.user.action')
+                       ->appendChildren([
+                               TextFormField::create('bulkProcessingUserAction')
+                                       ->attribute('data-tag', 'action')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.description')
+                                       ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                               if (!preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'format',
+                                                                       'wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.error.format'
+                                                               )
+                                                       );
+                                               }
+                                       })),
+                               
+                               ItemListFormField::create('bulkProcessingUserOptions')
+                                       ->attribute('data-tag', 'options')
+                                       ->label('wcf.acp.pip.general.options')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.options.description')
+                                       ->addValidator($optionValidator),
+                               
+                               ItemListFormField::create('bulkProcessingUserPermissions')
+                                       ->attribute('data-tag', 'permissions')
+                                       ->label('wcf.acp.pip.general.permissions')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.permissions.description')
+                                       ->addValidator($permissionValidator)
+                       ]);
+               
+               // com.woltlab.wcf.bulkProcessing.user.condition
+               $bulkProcessingUserConditionContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.bulkProcessing.user.condition');
+               $this->addConditionFields($bulkProcessingUserConditionContainer, true, true);
+               
+               // com.woltlab.wcf.category
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.category')
+                       ->appendChild(
+                               BooleanFormField::create('categoryDefaultPermission')
+                                       ->attribute('data-tag', 'defaultpermission')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission.description')
+                       );
+               
+               // com.woltlab.wcf.clipboardItem
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.clipboardItem')
+                       ->appendChild(
+                               ClassNameFormField::create('clipboardItemListClassName')
+                                       ->attribute('data-tag', 'listclassname')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName.description')
+                                       ->required()
+                                       ->parentClass(DatabaseObjectList::class)
+                       );
+               
+               // com.woltlab.wcf.condition.ad
+               $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.ad');
+               $this->addConditionFields($conditionAdContainer, true, false);
+               
+               // com.woltlab.wcf.condition.notice
+               $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.notice');
+               $this->addConditionFields($conditionAdContainer);
+               
+               // com.woltlab.wcf.condition.trophy
+               $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.trophy');
+               $this->addConditionFields($conditionAdContainer, false, true);
+               
+               // com.woltlab.wcf.condition.userGroupAssignment
+               $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.userGroupAssignment');
+               $this->addConditionFields($conditionAdContainer, false, true);
+               
+               // com.woltlab.wcf.condition.userSearch
+               $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.userSearch');
+               $this->addConditionFields($conditionAdContainer, false, true);
+               
+               // com.woltlab.wcf.notification.objectType
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.notification.objectType')
+                       ->appendChild(
+                               TextFormField::create('notificationObjectTypeCategory')
+                                       ->attribute('data-tag', 'category')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category.description')
+                                       // TODO: validator
+                       );
+               
+               // com.woltlab.wcf.rebuildData
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.rebuildData')
+                       ->appendChild(
+                               IntegerFormField::create('rebuildDataNiceValue')
+                                       ->attribute('data-tag', 'nicevalue')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.niceValue')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.niceValue.description')
+                                       ->nullable()
+                       );
+               
+               // com.woltlab.wcf.searchableObjectType
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.searchableObjectType')
+                       ->appendChild(
+                               TextFormField::create('searchableObjectTypeSearchIndex')
+                                       ->attribute('data-tag', 'searchindex')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.description')
+                                       ->required()
+                                       ->addValidator(new FormFieldValidator('tableName', function(TextFormField $formField) {
+                                               if ($formField->getValue()) {
+                                                       if (preg_match('~^(?P<app>[A-z]+)1_[A-z_]+$~', $formField->getValue(), $match)) {
+                                                               if (!ApplicationHandler::getInstance()->getApplication($match['app'])) {
+                                                                       $formField->addValidationError(
+                                                                               new FormFieldValidationError(
+                                                                                       'unknownApp',
+                                                                                       'wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.error.unknownApp',
+                                                                                       ['app' => $match['app']]
+                                                                               )
+                                                                       );
+                                                               }
+                                                       }
+                                                       else {
+                                                               $formField->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'invalid',
+                                                                               'wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.error.invalid'
+                                                                       )
+                                                               );
+                                                       }
+                                               }
+                                       }))
+                       );
+               
+               // com.woltlab.wcf.sitemap.object
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.sitemap.object')
+                       ->appendChildren([
+                               FloatFormField::create('sitemapObjectPriority')
+                                       ->attribute('data-tag', 'priority')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.priority')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.priority.description')
+                                       ->required()
+                                       ->minimum(0.0)
+                                       ->maximum(1.0)
+                                       ->step(0.1)
+                                       ->value(0.5),
+                               
+                               SingleSelectionFormField::create('sitemapObjectchangeFreq')
+                                       ->attribute('data-tag', 'changeFreq')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.changeFreq')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.changeFreq.description')
+                                       ->options([
+                                               'always',
+                                               'hourly',
+                                               'daily',
+                                               'weekly',
+                                               'monthly',
+                                               'yearly',
+                                               'never'
+                                       ])
+                                       ->required(),
+                               
+                               IntegerFormField::create('sitemapObjectRebuildTime')
+                                       ->attribute('data-tag', 'rebuildTime')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.rebuildTime')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.rebuildTime.description')
+                                       ->suffix('wcf.acp.option.suffix.seconds')
+                                       ->required()
+                                       ->minimum(0)
+                       ]);
+               
+               // com.woltlab.wcf.statDailyHandler
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.statDailyHandler')
+                       ->appendChildren([
+                               TextFormField::create('statDailyHandlerCategoryName')
+                                       ->attribute('data-tag', 'categoryname')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.description')
+                                       ->addValidator($this->getObjectTypeAlikeValueValidator('com.woltlab.wcf.statDailyHandler.categoryName')),
+                               
+                               BooleanFormField::create('statDailyHandlerIsDefault')
+                                       ->attribute('data-tag', 'default')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault.description')
+                       ]);
+               
+               // com.woltlab.wcf.tagging.taggableObject
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.tagging.taggableObject')
+                       ->appendChildren([
+                               ItemListFormField::create('taggingTaggableObjectOptions')
+                                       ->attribute('data-tag', 'options')
+                                       ->label('wcf.acp.pip.general.options')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.options.description')
+                                       ->addValidator($optionValidator),
+                               
+                               ItemListFormField::create('taggingTaggableObjectPermissions')
+                                       ->attribute('data-tag', 'permissions')
+                                       ->label('wcf.acp.pip.general.permissions')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.permissions.description')
+                                       ->addValidator($permissionValidator)
+                       ]);
+               
+               // com.woltlab.wcf.user.activityPointEvent
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.user.activityPointEvent')
+                       ->appendChild(
+                               IntegerFormField::create('userActivityPointEventPoints')
+                                       ->attribute('data-tag', 'points')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points.description')
+                                       ->minimum(0)
+                                       ->required()
+                       );
+               
+               // com.woltlab.wcf.versionTracker.objectType
+               $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.versionTracker.objectType')
+                       ->appendChildren([
+                               TextFormField::create('versionTrackerObjectTypeTableName')
+                                       ->attribute('data-tag', 'tableName')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName.description')
+                                       ->required()
+                                       ->addValidator(new FormFieldValidator('tableExists', function(TextFormField $formField) {
+                                               if ($formField->getValue()) {
+                                                       $value = ApplicationHandler::insertRealDatabaseTableNames($formField->getValue());
+                                                       
+                                                       if (!in_array($value, WCF::getDB()->getEditor()->getTableNames())) {
+                                                               $formField->addValidationError(new FormFieldValidationError(
+                                                                       'nonExistent',
+                                                                       'wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName.error.nonExistent',
+                                                                       ['tableName' => $value]
+                                                               ));
+                                                       }
+                                               }
+                                       })),
+                               
+                               TextFormField::create('versionTrackerObjectTypeTablePrimaryKey')
+                                       ->attribute('data-tag', 'tablePrimaryKey')
+                                       ->label('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey')
+                                       ->description('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.description')
+                                       ->required()
+                                       ->addValidator(new FormFieldValidator('columnExists', function(TextFormField $formField) {
+                                               if ($formField->getValue()) {
+                                                       /** @var TextFormField $tableName */
+                                                       $tableName = $formField->getDocument()->getNodeById('versionTrackerObjectTypeTableName');
+                                                       
+                                                       if (empty($tableName->getValidationErrors())) {
+                                                               // table name has already been validated and table exists
+                                                               $columns = WCF::getDB()->getEditor()->getColumns($tableName->getValue());
+                                                               
+                                                               foreach ($columns as $column) {
+                                                                       if ($column['name'] === $formField->getValue()) {
+                                                                               if ($column['data']['key'] !== 'PRIMARY') {
+                                                                                       $formField->addValidationError(new FormFieldValidationError(
+                                                                                               'noPrimaryColumn',
+                                                                                               'wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.error.noPrimaryColumn'
+                                                                                       ));
+                                                                               }
+                                                                               
+                                                                               return;
+                                                                       }
+                                                               }
+                                                               
+                                                               $formField->addValidationError(new FormFieldValidationError(
+                                                                       'nonExistent',
+                                                                       'wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.error.nonExistent'
+                                                               ));
+                                                       }
+                                               }
+                                       })),
+                       ]);
+       }
+       
+       /**
+        * Returns a form field validator to validate a string value that has a
+        * object type-alike structure.
+        * 
+        * @param       string          $languageItemSegment    used for error language items: `wcf.acp.pip.objectType.{$languageItemSegment}.error.{errorType}`
+        * @return      FormFieldValidator
+        */
+       protected function getObjectTypeAlikeValueValidator($languageItemSegment): FormFieldValidator {
+               return new FormFieldValidator('format', function(TextFormField $formField) use ($languageItemSegment) {
+                       if ($formField->getValue()) {
+                               $segments = explode('.', $formField->getValue());
+                               if (count($segments) < 4) {
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'tooFewSegments',
+                                                       'wcf.acp.pip.objectType.' . $languageItemSegment . '.error.tooFewSegments',
+                                                       ['segmentCount' => count($segments)]
+                                               )
+                                       );
+                               }
+                               else {
+                                       $invalidSegments = [];
+                                       foreach ($segments as $key => $segment) {
+                                               if (!preg_match('~^[A-z0-9\-\_]+$~', $segment)) {
+                                                       $invalidSegments[$key] = $segment;
+                                               }
+                                       }
+                                       
+                                       if (!empty($invalidSegments)) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'invalidSegments',
+                                                               'wcf.acp.pip.objectType.' . $languageItemSegment . '.error.invalidSegments',
+                                                               ['invalidSegments' => $invalidSegments]
+                                                       )
+                                               );
+                                       }
+                               }
+                       }
+               });
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return sha1(
+                       $element->getElementsByTagName('name')->item(0)->nodeValue . '/' .
+                       $element->getElementsByTagName('definitionname')->item(0)->nodeValue
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getEmptyXml(): string {
+               return <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<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/vortex/objectType.xsd">
+       <import></import>
+</data>
+XML;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'name' => 'wcf.acp.pip.objectType.objectType',
+                       'definitionName' => 'wcf.acp.pip.objectType.definitionName'
+               ]);
+               
+               /** @var \DOMElement $element */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $entryList->addEntry($this->getElementIdentifier($element), [
+                               'name' => $element->getElementsByTagName('name')->item(0)->nodeValue,
+                               'definitionName' => $element->getElementsByTagName('definitionname')->item(0)->nodeValue
+                       ]);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * Returns a form container for the object type definition-specific fields
+        * of the the object type definition with the given name.
+        * 
+        * The returned form container is already appended to the given form and
+        * has a dependency on the `definitionName` field so that the form container
+        * is only shown for the relevant object type definition.
+        * 
+        * @param       IFormDocument   $form
+        * @param       string          $definitionName
+        * @return      FormContainer
+        * @since       3.2
+        */
+       protected function getObjectTypeDefinitionDataContainer(IFormDocument $form, string $definitionName): FormContainer {
+               /** @var SingleSelectionFormField $definitionNameField */
+               $definitionNameField = $form->getNodeById('definitionName');
+               
+               $definitionPieces = explode('.', $definitionName);
+               
+               $formContainer = FormContainer::create(lcfirst(implode('', array_map('ucfirst', $definitionPieces))) . 'Fields')
+                       ->label('wcf.acp.pip.objectType.' . $definitionName . '.data.title')
+                       ->addDependency(
+                               ValueFormFieldDependency::create('definitionName')
+                                       ->field($definitionNameField)
+                                       ->values([$definitionName])
+                       );
+               
+               $form->appendChild($formContainer);
+               
+               return $formContainer;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp(
+                               $element1->getElementsByTagName('definitionname')->item(0)->nodeValue,
+                               $element2->getElementsByTagName('definitionname')->item(0)->nodeValue
+                       ) ?: strcmp(
+                               $element1->getElementsByTagName('name')->item(0)->nodeValue,
+                               $element2->getElementsByTagName('name')->item(0)->nodeValue
+                       );
+               });
+               
+               $this->sortChildNodes($document->getElementsByTagName('import')->item(0)->childNodes, function(\DOMElement $element1, \DOMElement $element2) {
+                       // force `definitionname` to be at the first position
+                       if ($element1->nodeName === 'definitionname') {
+                               return -1;
+                       }
+                       else if ($element2->nodeName === 'definitionname') {
+                               return 1;
+                       }
+                       // force `name` to be at the second position
+                       else if ($element1->nodeName === 'name') {
+                               return -1;
+                       }
+                       else if ($element2->nodeName === 'name') {
+                               return 1;
+                       }
+                       // force `classname` to be at the third position
+                       else if ($element1->nodeName === 'classname') {
+                               return -1;
+                       }
+                       else if ($element2->nodeName === 'classname') {
+                               return 1;
+                       }
+                       else {
+                               // the rest is sorted by node name
+                               return strcmp($element1->nodeName, $element2->nodeName);
+                       }
+               });
+               
+               $this->sortChildNodes($document->getElementsByTagName('delete'), function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp(
+                               $element1->getAttribute('name'),
+                               $element2->getAttribute('name')
+                       );
+               });
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               $type = $document->createElement('type');
+               foreach ($form->getData()['data'] as $key => $value) {
+                       if ($value !== '') {
+                               if (is_string($value)) {
+                                       $type->appendChild($document->createElement($key, $this->getAutoCdataValue($value)));
+                               }
+                               else {
+                                       $type->appendChild($document->createElement($key, (string) $value));
+                               }
+                       }
+               }
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($type);
+               
+               return $type;
+       }
+       
+       /**
+        * Adds all condition specific fields to the given form container.
+        * 
+        * @param       IFormContainer          $dataContainer
+        * @param       bool                    $addConditionObject
+        * @param       bool                    $addConditionGroup
+        * @since       3.2
+        */
+       protected function addConditionFields(IFormContainer $dataContainer, bool $addConditionObject = true, bool $addConditionGroup = true) {
+               $prefix = preg_replace('~Fields$~', '', $dataContainer->getId());
+               
+               if ($addConditionObject) {
+                       $dataContainer->appendChild(
+                               TextFormField::create($prefix . 'ConditionObject')
+                                       ->attribute('data-tag', 'conditionobject')
+                                       ->label('wcf.acp.pip.objectType.condition.conditionObject')
+                                       ->description('wcf.acp.pip.objectType.condition.conditionObject.description')
+                                       ->required()
+                                       ->addValidator($this->getObjectTypeAlikeValueValidator('condition.conditionObject'))
+                       );
+               }
+               
+               if ($addConditionGroup) {
+                       $dataContainer->appendChild(
+                               TextFormField::create($prefix . 'ConditionGroup')
+                                       ->attribute('data-tag', 'conditiongroup')
+                                       ->label('wcf.acp.pip.objectType.condition.conditionGroup')
+                                       ->description('wcf.acp.pip.objectType.condition.conditionGroup.description')
+                                       ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                               if ($formField->getValue() && !preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'format',
+                                                                       'wcf.acp.pip.objectType.condition.conditionGroup.error.format'
+                                                               )
+                                                       );
+                                               }
+                                       }))
+                       );
+               }
+               
+               // classes extending `AbstractIntegerCondition`
+               $integerConditions = [];
+               foreach (ApplicationHandler::getInstance()->getApplications() as $application) {
+                       $conditionDir = $application->getPackage()->getAbsolutePackageDir() . 'lib/system/condition/';
+                       
+                       if (file_exists($conditionDir)) {
+                               $directory = DirectoryUtil::getInstance($conditionDir);
+                               $conditionList = $directory->getFiles(SORT_ASC, new Regex('Condition\.class\.php$'));
+                               
+                               /** @var string $condition */
+                               foreach ($conditionList as $condition) {
+                                       $pathPieces = explode('/', str_replace($conditionDir, '', $condition));
+                                       $filename = array_pop($pathPieces);
+                                       
+                                       $className = $application->getAbbreviation() . '\system\condition\\';
+                                       if (!empty($pathPieces)) {
+                                               $className .= implode('\\', $pathPieces) . '\\';
+                                       }
+                                       $className .= basename($filename, '.class.php');
+                                       if (class_exists($className) && is_subclass_of($className, AbstractIntegerCondition::class)) {
+                                               $reflection = new \ReflectionClass($className);
+                                               if ($reflection->isInstantiable()) {
+                                                       $integerConditions[] = $className;
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
+               /** @var TextFormField $className */
+               $className = $dataContainer->getDocument()->getNodeById('className');
+               
+               $dataContainer->appendChildren([
+                       IntegerFormField::create($prefix . 'IntegerMinValue')
+                               ->attribute('data-tag', 'minvalue')
+                               ->label('wcf.acp.pip.objectType.condition.integer.minValue')
+                               ->description('wcf.acp.pip.objectType.condition.integer.minValue.description')
+                               ->addDependency(
+                                       ValueFormFieldDependency::create('className')
+                                               ->field($className)
+                                               ->values($integerConditions)
+                               ),
+                       IntegerFormField::create($prefix . 'IntegerMaxValue')
+                               ->attribute('data-tag', 'maxvalue')
+                               ->label('wcf.acp.pip.objectType.condition.integer.maxValue')
+                               ->description('wcf.acp.pip.objectType.condition.integer.maxValue.description')
+                               ->addDependency(
+                                       ValueFormFieldDependency::create('className')
+                                               ->field($className)
+                                               ->values($integerConditions)
+                               )
+               ]);
+               
+               // `UserGroupCondition`
+               $dataContainer->appendChild(
+                       BooleanFormField::create($prefix . 'UserGroupIncludeGuests')
+                               ->attribute('data-tag', 'includeguests')
+                               ->label('wcf.acp.pip.objectType.condition.userGroup.includeGuests')
+                               ->description('wcf.acp.pip.objectType.condition.userGroup.includeGuests.description')
+                               ->addDependency(
+                                       ValueFormFieldDependency::create('className')
+                                               ->field($className)
+                                               ->values([UserGroupCondition::class])
+                               )
+               );
+               
+               // `UserIntegerPropertyCondition`
+               $dataContainer->appendChild(
+                       TextFormField::create($prefix . 'UserIntegerPropertyName')
+                               ->attribute('data-tag', 'propertyname')
+                               ->label('wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName')
+                               ->description('wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName.description')
+                               ->addDependency(
+                                       ValueFormFieldDependency::create('className')
+                                               ->field($className)
+                                               ->values([UserIntegerPropertyCondition::class])
+                               )
+                               ->addValidator(new FormFieldValidator('userTableIntegerColumn', function(TextFormField $formField) {
+                                       $columns = WCF::getDB()->getEditor()->getColumns('wcf' . WCF_N . '_user');
+                                       
+                                       foreach ($columns as $column) {
+                                               if ($column['name'] === $formField->getValue()) {
+                                                       if ($column['data']['type'] !== 'int') {
+                                                               $formField->addValidationError(new FormFieldValidationError(
+                                                                       'noIntegerColumn',
+                                                                       'wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName.error.noIntegerColumn'
+                                                               ));
+                                                       }
+                                                       
+                                                       return;
+                                               }
+                                       }
+                                       
+                                       $formField->addValidationError(new FormFieldValidationError(
+                                               'nonExistent',
+                                               'wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName.error.nonExistent'
+                                       ));
+                               }))
+               );
+               
+               // `UserTimestampPropertyCondition`
+               $dataContainer->appendChild(
+                       TextFormField::create($prefix . 'UserTimestampPropertyName')
+                               ->attribute('data-tag', 'propertyname')
+                               ->label('wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName')
+                               ->description('wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName.description')
+                               ->addDependency(
+                                       ValueFormFieldDependency::create('className')
+                                               ->field($className)
+                                               ->values([UserTimestampPropertyCondition::class])
+                               )
+                               ->addValidator(new FormFieldValidator('userTableIntegerColumn', function(TextFormField $formField) {
+                                       $columns = WCF::getDB()->getEditor()->getColumns('wcf' . WCF_N . '_user');
+                                       
+                                       foreach ($columns as $column) {
+                                               if ($column['name'] === $formField->getValue()) {
+                                                       if ($column['data']['type'] !== 'int') {
+                                                               $formField->addValidationError(new FormFieldValidationError(
+                                                                       'noIntegerColumn',
+                                                                       'wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName.error.noIntegerColumn'
+                                                               ));
+                                                       }
+                                                       
+                                                       return;
+                                               }
+                                       }
+                                       
+                                       $formField->addValidationError(new FormFieldValidationError(
+                                               'nonExistent',
+                                               'wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName.error.nonExistent'
+                                       ));
+                               }))
+               );
+               
+               $parameters = ['dataContainer' => $dataContainer];
+               EventHandler::getInstance()->fireAction($this, 'addConditionFields', $parameters);
+       }
 }
index 121ff5d1e0381067e2b1aa628de463165128da9c..7342e051137acb0e3fe842b6e905bda85ab7bba8 100644 (file)
@@ -2,7 +2,16 @@
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
 use wcf\data\package\installation\plugin\PackageInstallationPluginEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\package\installation\plugin\PackageInstallationPluginList;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
 use wcf\system\WCF;
 
 /**
@@ -13,7 +22,9 @@ use wcf\system\WCF;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Package\Plugin
  */
-class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -84,4 +95,169 @@ class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               /** @var FormContainer $dataContainer */
+               $dataContainer = $form->getNodeById('data');
+               
+               $dataContainer->appendChildren([
+                       TextFormField::create('pluginName')
+                               ->attribute('data-tag', 'name')
+                               ->label('wcf.acp.pip.pip.pluginName')
+                               ->description('wcf.acp.pip.pip.pluginName.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                       if (preg_match('~^[a-z][A-z]+$~', $formField->getValue()) !== 1) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.pip.pluginName.error.format'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       $pipList = new PackageInstallationPluginList();
+                                       $pipList->getConditionBuilder()->add('pluginName = ?', [$formField->getValue()]);
+                                       
+                                       if ($pipList->countObjects()) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.pip.pluginName.error.notUnique'
+                                                       )
+                                               );
+                                       }
+                               })),
+                       
+                       TextFormField::create('className')
+                               ->attribute('data-tag', '__value')
+                               ->label('wcf.acp.pip.pip.className')
+                               ->description('wcf.acp.pip.pip.className.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('noLeadingBackslash', function(TextFormField $formField) {
+                                       if (substr($formField->getValue(), 0, 1) === '\\') {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'leadingBackslash',
+                                                               'wcf.acp.pip.pip.className.error.leadingBackslash'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('classExists', function(TextFormField $formField) {
+                                       if (!class_exists($formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'nonExistent',
+                                                               'wcf.acp.pip.pip.className.error.nonExistent'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('implementsInterface', function(TextFormField $formField) {
+                                       if (!is_subclass_of($formField->getValue(), IPackageInstallationPlugin::class)) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'interface',
+                                                               'wcf.acp.pip.pip.className.error.interface'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('isInstantiable', function(TextFormField $formField) {
+                                       $reflection = new \ReflectionClass($formField->getValue());
+                                       if (!$reflection->isInstantiable()) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'interface',
+                                                               'wcf.acp.pip.pip.className.error.isInstantiable'
+                                                       )
+                                               );
+                                       }
+                               }))
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               return [
+                       'className' => $element->nodeValue,
+                       'pluginName' => $element->getAttribute('name'),
+                       'priority' => $this->installation->getPackage()->package == 'com.woltlab.wcf' ? 1 : 0
+               ];
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return $element->getAttribute('name');
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'pluginName' => 'wcf.acp.pip.pip.pluginName',
+                       'className' => 'wcf.acp.pip.pip.className'
+               ]);
+               
+               /** @var \DOMElement $languageItem */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $entryList->addEntry($this->getElementIdentifier($element), [
+                               'className' => $element->nodeValue,
+                               'pluginName' => $element->getAttribute('name')
+                       ]);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp($element1->getAttribute('name'), $element2->getAttribute('name'));
+               };
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               /** @var TextFormField $className */
+               $className = $form->getNodeById('className');
+               /** @var TextFormField $pluginName */
+               $pluginName = $form->getNodeById('pluginName');
+               
+               $pip = $document->createElement('pip', $className->getSaveValue());
+               $pip->setAttribute('name', $pluginName->getSaveValue());
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($pip);
+               
+               return $pip;
+       }
 }
index 4b9f2f217c78b028fbba33c04adfa4665391dc26..6bf9ed960eb418648c0a27e96eeba0ed3d820d44 100644 (file)
@@ -1,22 +1,40 @@
 <?php
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\option\Option;
+use wcf\data\user\group\option\UserGroupOptionList;
 use wcf\data\user\notification\event\UserNotificationEvent;
 use wcf\data\user\notification\event\UserNotificationEventEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\user\notification\event\UserNotificationEventList;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
 use wcf\system\exception\SystemException;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\BooleanFormField;
+use wcf\system\form\builder\field\ClassNameFormField;
+use wcf\system\form\builder\field\ItemListFormField;
+use wcf\system\form\builder\field\SingleSelectionFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\user\notification\event\IUserNotificationEvent;
 use wcf\system\WCF;
 use wcf\util\StringUtil;
 
 /**
  * Installs, updates and deletes user notification events.
  * 
- * @author     Marcel Werk
+ * @author     Matthias Schmidt, Marcel Werk
  * @copyright  2001-2018 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Package\Plugin
  */
-class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -153,8 +171,280 @@ class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageI
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return ['objectType'];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               $form->getNodeById('data')->appendChildren([
+                       TextFormField::create('name')
+                               ->label('wcf.acp.pip.userNotificationEvent.name')
+                               ->description('wcf.acp.pip.userNotificationEvent.name.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                       if (!preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.userNotificationEvent.name.error.format'
+                                                       )
+                                               );
+                                       }
+                               })),
+                       
+                       SingleSelectionFormField::create('objectType')
+                               ->attribute('data-tag', 'objecttype')
+                               ->label('wcf.acp.pip.userNotificationEvent.objectType')
+                               ->description('wcf.acp.pip.userNotificationEvent.objectType.description')
+                               ->required()
+                               ->options(function(): array {
+                                       $options = [];
+                                       foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.notification.objectType') as $objectType) {
+                                               $options[$objectType->objectType] = $objectType->objectType;
+                                       }
+                                       
+                                       asort($options);
+                                       
+                                       return $options;
+                               })
+                               // validate the uniqueness of the `name` field after knowing that the selected object type is valid
+                               ->addValidator(new FormFieldValidator('nameUniqueness', function(SingleSelectionFormField $formField) {
+                                       /** @var TextFormField $nameField */
+                                       $nameField = $formField->getDocument()->getNodeById('name');
+                                       
+                                       if ($formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE || $this->editedEntry->getAttribute('name') !== $nameField->getSaveValue()) {
+                                               $eventList = new UserNotificationEventList();
+                                               $eventList->getConditionBuilder()->add('user_notification_event.eventName = ?', [$nameField->getSaveValue()]);
+                                               $eventList->getConditionBuilder()->add(
+                                                       'user_notification_event.objectTypeID = ?',
+                                                       [ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.notification.objectType', $formField->getSaveValue())->objectTypeID]
+                                               );
+                                               
+                                               if ($eventList->countObjects() > 0) {
+                                                       $nameField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'notUnique',
+                                                                       'wcf.acp.pip.userNotificationEvent.name.error.notUnique'
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       ClassNameFormField::create('className')
+                               ->attribute('data-tag', 'classname')
+                               ->label('wcf.acp.pip.userNotificationEvent.className')
+                               ->description('wcf.acp.pip.userNotificationEvent.className.description')
+                               ->required()
+                               ->implementedInterface(IUserNotificationEvent::class),
+                       
+                       BooleanFormField::create('preset')
+                               ->label('wcf.acp.pip.userNotificationEvent.preset')
+                               ->description('wcf.acp.pip.userNotificationEvent.preset.description'),
+                       
+                       ItemListFormField::create('options')
+                               ->label('wcf.acp.pip.general.options')
+                               ->description('wcf.acp.pip.userNotificationEvent.options.description')
+                               ->addValidator(new FormFieldValidator('optionsExist', function(ItemListFormField $formField) {
+                                       $options = $formField->getValue();
+                                       if (is_array($options)) {
+                                               $definedOptions = Option::getOptions();
+                                               
+                                               $options = array_filter($options, function(string $option) use ($definedOptions) {
+                                                       return !isset($definedOptions[strtoupper($option)]);
+                                               });
+                                               
+                                               if (!empty($options)) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'nonExistent',
+                                                                       'wcf.acp.pip.general.options.error.nonExistent',
+                                                                       ['options' => $options]
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       ItemListFormField::create('permissions')
+                               ->label('wcf.acp.pip.general.permissions')
+                               ->description('wcf.acp.pip.userNotificationEvent.permissions.description')
+                               ->addValidator(new FormFieldValidator('permissionsExist', function(ItemListFormField $formField) {
+                                       $permissions = $formField->getValue();
+                                       if (is_array($permissions)) {
+                                               $userGroupOptionList = new UserGroupOptionList();
+                                               $userGroupOptionList->getConditionBuilder()->add('optionName IN (?)', [$permissions]);
+                                               $userGroupOptionList->readObjects();
+                                               
+                                               if (count($userGroupOptionList) !== count($permissions)) {
+                                                       foreach ($userGroupOptionList as $userGroupOption) {
+                                                               unset($permissions[array_search($userGroupOption->optionName, $permissions)]);
+                                                       }
+                                                       
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'nonExistent',
+                                                                       'wcf.acp.pip.general.permissions.error.nonExistent',
+                                                                       ['permissions' => $permissions]
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       SingleSelectionFormField::create('presetMailNotificationType')
+                               ->attribute('data-tag', 'presetmailnotificationtype')
+                               ->label('wcf.acp.pip.userNotificationEvent.presetMailNotificationType')
+                               ->description('wcf.acp.pip.userNotificationEvent.presetMailNotificationType.description')
+                               ->nullable()
+                               ->options([
+                                       '' => 'wcf.user.notification.mailNotificationType.none',
+                                       'daily' => 'wcf.user.notification.mailNotificationType.daily',
+                                       'instant' => 'wcf.user.notification.mailNotificationType.instant'
+                               ])
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               $data = [
+                       'className' => $element->getElementsByTagName('classname')->item(0)->nodeValue,
+                       'objectTypeID' => $this->getObjectTypeID($element->getElementsByTagName('objecttype')->item(0)->nodeValue),
+                       'eventName' => $element->getElementsByTagName('name')->item(0)->nodeValue,
+                       'packageID' => $this->installation->getPackage()->packageID,
+                       'preset' => 0
+               ];
+               
+               $options = $element->getElementsByTagName('options')->item(0);
+               if ($options) {
+                       $data['options'] = StringUtil::normalizeCsv($options->nodeValue);
+               }
+               
+               $permissions = $element->getElementsByTagName('permissions')->item(0);
+               if ($permissions) {
+                       $data['permissions'] = StringUtil::normalizeCsv($permissions->nodeValue);
+               }
+               
+               // the presence of a `preset` element is treated as `<preset>1</preset>
+               if ($element->getElementsByTagName('preset')->length === 1) {
+                       $data['preset'] = 1;
+               }
+               
+               $presetMailNotificationType = $element->getElementsByTagName('presetmailnotificationtype')->item(0);
+               if ($presetMailNotificationType && in_array($presetMailNotificationType->nodeValue, ['instant', 'daily'])) {
+                       $data['presetMailNotificationType'] = $presetMailNotificationType->nodeValue;
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return sha1(
+                       $element->getElementsByTagName('name')->item(0)->nodeValue . '/' .
+                       $element->getElementsByTagName('objecttype')->item(0)->nodeValue
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'name' => 'wcf.acp.pip.userNotificationEvent.name',
+                       'className' => 'wcf.acp.pip.userNotificationEvent.className'
+               ]);
+               
+               /** @var \DOMElement $element */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $entryList->addEntry($this->getElementIdentifier($element), [
+                               'className' => $element->getElementsByTagName('classname')->item(0)->nodeValue,
+                               'name' => $element->getElementsByTagName('name')->item(0)->nodeValue,
+                       ]);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
+                       $objectType1 = $element1->getElementsByTagName('objecttype')->item(0)->nodeValue;
+                       $objectType2 = $element2->getElementsByTagName('objecttype')->item(0)->nodeValue;
+                       
+                       if ($objectType1 !== $objectType2) {
+                               return strcmp($objectType1, $objectType2);
+                       }
+                       
+                       return strcmp(
+                               $element1->getElementsByTagName('name')->item(0)->nodeValue,
+                               $element2->getElementsByTagName('name')->item(0)->nodeValue
+                       );
+               };
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               $event = $document->createElement($this->tagName);
+               
+               $event->appendChild($document->createElement('name', $form->getNodeById('name')->getSaveValue()));
+               $event->appendChild($document->createElement('objecttype', $form->getNodeById('objectType')->getSaveValue()));
+               $event->appendChild($document->createElement('classname', $form->getNodeById('className')->getSaveValue()));
+               
+               /** @var ItemListFormField $options */
+               $options = $form->getNodeById('options');
+               if ($options->getSaveValue()) {
+                       $event->appendChild($document->createElement('options', $options->getSaveValue()));
+               }
+               
+               /** @var ItemListFormField $permissions */
+               $permissions = $form->getNodeById('permissions');
+               if ($permissions->getSaveValue()) {
+                       $event->appendChild($document->createElement('permissions', $permissions->getSaveValue()));
+               }
+               
+               /** @var BooleanFormField $permissions */
+               $preset = $form->getNodeById('preset');
+               if ($preset->getSaveValue()) {
+                       $event->appendChild($document->createElement('preset', '1'));
+               }
+               
+               /** @var BooleanFormField $permissions */
+               $presetMailNotificationType = $form->getNodeById('presetMailNotificationType');
+               if ($presetMailNotificationType->getSaveValue()) {
+                       $event->appendChild($document->createElement('presetmailnotificationtype', $presetMailNotificationType->getSaveValue()));
+               }
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($event);
+               
+               return $event;
+       }
 }
index e198991f98f345ac2e2829679b2e8284295d15b4..c6c10d2831b94292b0c2394616d227a84e2dc386 100644 (file)
@@ -1,8 +1,22 @@
 <?php
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
+use wcf\data\option\Option;
+use wcf\data\user\group\option\UserGroupOptionList;
 use wcf\data\user\profile\menu\item\UserProfileMenuItemEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\user\profile\menu\item\UserProfileMenuItemList;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\ClassNameFormField;
+use wcf\system\form\builder\field\IntegerFormField;
+use wcf\system\form\builder\field\ItemListFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\menu\user\profile\content\IUserProfileMenuContent;
 use wcf\system\WCF;
 use wcf\util\StringUtil;
 
@@ -14,7 +28,9 @@ use wcf\util\StringUtil;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Package\Plugin
  */
-class UserProfileMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class UserProfileMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -85,8 +101,241 @@ class UserProfileMenuPackageInstallationPlugin extends AbstractXMLPackageInstall
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               $form->getNodeById('data')->appendChildren([
+                       TextFormField::create('name')
+                               ->label('wcf.acp.pip.userProfileMenu.name')
+                               ->description('wcf.acp.pip.userProfileMenu.name.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                       if (!preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.userProfileMenu.name.error.format'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       if ($formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE || $this->editedEntry->getAttribute('name') !== $formField->getValue()) {
+                                               $menuItemList = new UserProfileMenuItemList();
+                                               $menuItemList->getConditionBuilder()->add('user_profile_menu_item.menuItem = ?', [$formField->getValue()]);
+                                               
+                                               if ($menuItemList->countObjects() > 0) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'notUnique',
+                                                                       'wcf.acp.pip.userProfileMenu.name.error.notUnique'
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       ClassNameFormField::create('className')
+                               ->attribute('data-tag', 'classname')
+                               ->label('wcf.acp.pip.userProfileMenu.className')
+                               ->description('wcf.acp.pip.userProfileMenu.className.description')
+                               ->required()
+                               ->implementedInterface(IUserProfileMenuContent::class),
+                       
+                       IntegerFormField::create('showOrder')
+                               ->attribute('data-tag', 'showorder')
+                               ->label('wcf.acp.pip.userProfileMenu.showOrder')
+                               ->description('wcf.acp.pip.userProfileMenu.showOrder.description')
+                               ->nullable()
+                               ->minimum(1),
+                       
+                       ItemListFormField::create('options')
+                               ->label('wcf.acp.pip.general.options')
+                               ->description('wcf.acp.pip.userProfileMenu.options.description')
+                               ->addValidator(new FormFieldValidator('optionsExist', function(ItemListFormField $formField) {
+                                       $options = $formField->getValue();
+                                       if (is_array($options)) {
+                                               $definedOptions = Option::getOptions();
+                                               
+                                               $options = array_filter($options, function(string $option) use ($definedOptions) {
+                                                       return !isset($definedOptions[strtoupper($option)]);
+                                               });
+                                               
+                                               if (!empty($options)) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'nonExistent',
+                                                                       'wcf.acp.pip.general.options.error.nonExistent',
+                                                                       ['options' => $options]
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       ItemListFormField::create('permissions')
+                               ->label('wcf.acp.pip.general.permissions')
+                               ->description('wcf.acp.pip.userProfileMenu.permissions.description')
+                               ->addValidator(new FormFieldValidator('permissionsExist', function(ItemListFormField $formField) {
+                                       $permissions = $formField->getValue();
+                                       if (is_array($permissions)) {
+                                               $userGroupOptionList = new UserGroupOptionList();
+                                               $userGroupOptionList->getConditionBuilder()->add('optionName IN (?)', [$permissions]);
+                                               $userGroupOptionList->readObjects();
+                                               
+                                               if (count($userGroupOptionList) !== count($permissions)) {
+                                                       foreach ($userGroupOptionList as $userGroupOption) {
+                                                               unset($permissions[array_search($userGroupOption->optionName, $permissions)]);
+                                                       }
+                                                       
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'nonExistent',
+                                                                       'wcf.acp.pip.general.permissions.error.nonExistent',
+                                                                       ['permissions' => $permissions]
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               }))
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               $data = [
+                       'className' => $element->getElementsByTagName('classname')->item(0)->nodeValue,
+                       'menuItem' => $element->getAttribute('name'),
+                       'packageID' => $this->installation->getPackage()->packageID
+               ];
+               
+               $options = $element->getElementsByTagName('options')->item(0);
+               if ($options) {
+                       $data['options'] = StringUtil::normalizeCsv($options->nodeValue);
+               }
+               
+               $permissions = $element->getElementsByTagName('permissions')->item(0);
+               if ($permissions) {
+                       $data['permissions'] = StringUtil::normalizeCsv($permissions->nodeValue);
+               }
+               
+               $showOrder = $element->getElementsByTagName('showorder')->item(0);
+               if ($showOrder) {
+                       $data['showOrder'] = intval($showOrder->nodeValue);
+               }
+               else {
+                       $data['showOrder'] = null;
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return $element->getAttribute('name');
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'name' => 'wcf.acp.pip.userProfileMenu.name',
+                       'className' => 'wcf.acp.pip.userProfileMenu.className'
+               ]);
+               
+               /** @var \DOMElement $element */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $entryList->addEntry($this->getElementIdentifier($element), [
+                               'className' => $element->getElementsByTagName('classname')->item(0)->nodeValue,
+                               'name' => $element->getAttribute('name'),
+                       ]);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
+                       $showOrder1 = PHP_INT_MAX;
+                       if ($element1->getElementsByTagName('showorder')->length === 1) {
+                               $showOrder1 = $element1->getElementsByTagName('showorder')->item(0)->nodeValue;
+                       }
+                       
+                       $showOrder2 = PHP_INT_MAX;
+                       if ($element2->getElementsByTagName('showorder')->length === 1) {
+                               $showOrder2 = $element2->getElementsByTagName('showorder')->item(0)->nodeValue;
+                       }
+                       
+                       if ($showOrder1 !== $showOrder2) {
+                               return $showOrder1 > $showOrder2;
+                       }
+                       
+                       return strcmp(
+                               $element1->getAttribute('name'),
+                               $element2->getAttribute('name')
+                       );
+               };
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               $userProfileMenuItem = $document->createElement('userprofilemenuitem');
+               $userProfileMenuItem->setAttribute('name', $form->getNodeById('name')->getSaveValue());
+               $userProfileMenuItem->appendChild($document->createElement('classname', $form->getNodeById('className')->getSaveValue()));
+               
+               /** @var ItemListFormField $options */
+               $options = $form->getNodeById('options');
+               if ($options->getSaveValue()) {
+                       $userProfileMenuItem->appendChild($document->createElement('options', $options->getSaveValue()));
+               }
+               
+               /** @var ItemListFormField $permissions */
+               $permissions = $form->getNodeById('permissions');
+               if ($permissions->getSaveValue()) {
+                       $userProfileMenuItem->appendChild($document->createElement('permissions', $permissions->getSaveValue()));
+               }
+               
+               /** @var IntegerFormField $showOrder */
+               $showOrder = $form->getNodeById('showOrder');
+               if ($showOrder->getSaveValue()) {
+                       $userProfileMenuItem->appendChild($document->createElement('showorder', (string) $showOrder->getSaveValue()));
+               }
+               
+               $import = $document->getElementsByTagName('import')->item(0);
+               $import->appendChild($userProfileMenuItem);
+               
+               return $userProfileMenuItem;
+       }
 }
index fcbf0efa563c0600dc85c7e4c384d8d7a76c912f..7a25af02dd6357251e06f0f8168c1e2a1c246237 100644 (file)
@@ -188,4 +188,75 @@ class XML {
                        throw new SystemException($message);
                }
        }
+       
+       /**
+        * Returns the dom document object this object is working with.
+        * 
+        * @return      \DOMDocument
+        * @since       3.2
+        */
+       public function getDocument() {
+               return $this->document;
+       }
+       
+       /**
+        * Writes the xml structure into the given file.
+        * 
+        * @param       string          $fileLocation   location of file
+        * @param       bool            $cdata          indicates of values are escaped using cdata
+        * @since       3.2
+        */
+       public function write($fileLocation, $cdata = false) {
+               $schemaParts = explode(' ', $this->document->documentElement->getAttributeNS($this->document->documentElement->lookupNamespaceUri('xsi'), 'schemaLocation'));
+               
+               $writer = new XMLWriter();
+               // TODO: additional attributes of main element
+               $writer->beginDocument($this->document->documentElement->nodeName, $schemaParts[0], $schemaParts[1]);
+               foreach ($this->document->documentElement->childNodes as $childNode) {
+                       $this->writeElement($writer, $childNode, $cdata);
+               }
+               $writer->endDocument($fileLocation);
+       }
+       
+       /**
+        * Writes the given element using the given xml writer.
+        * 
+        * @param       XMLWriter       $writer         xml writer
+        * @param       \DOMElement     $element        written element
+        * @param       bool            $cdata          indicates if element value is escaped using cdata
+        * @since       3.2
+        */
+       protected function writeElement(XMLWriter $writer, \DOMElement $element, $cdata) {
+               if ($element->childNodes->length === 1 && $element->firstChild instanceof \DOMText) {
+                       $writer->writeElement($element->nodeName, $element->firstChild->nodeValue, $this->getAttributes($element), $cdata);
+               }
+               else {
+                       $writer->startElement($element->nodeName, $this->getAttributes($element));
+                       foreach ($element->childNodes as $childNode) {
+                               // only consider dom elements, ignore comments
+                               if ($childNode instanceof \DOMElement) {
+                                       $this->writeElement($writer, $childNode, $cdata);
+                               }
+                       }
+                       $writer->endElement();
+               }
+       }
+       
+       /**
+        * Returns an array with the attribute values of the given dom element
+        * (with the attribute names as array keys).
+        * 
+        * @param       \DOMElement     $element        elements whose attributes will be returned
+        * @return      array                           attributes
+        * @since       3.2
+        */
+       protected function getAttributes(\DOMElement $element) {
+               $attributes = [];
+               /** @var \DOMNode $attribute */
+               foreach ($element->attributes as $attribute) {
+                       $attributes[$attribute->nodeName] = $attribute->nodeValue;
+               }
+               
+               return $attributes;
+       }
 }
index 70388b03f0698aae7c4f8eac70e66ffd4fda7887..da3262d1e4904cbbf0b5fd3f5a53aafd29da1136 100644 (file)
@@ -120,8 +120,9 @@ class XMLWriter {
         * @param       string          $element
         * @param       string          $cdata
         * @param       string[]        $attributes
+        * @param       bool            $writeAsCdata
         */
-       public function writeElement($element, $cdata, array $attributes = []) {
+       public function writeElement($element, $cdata, array $attributes = [], $writeAsCdata = true) {
                $this->startElement($element);
                
                // write attributes
@@ -130,7 +131,14 @@ class XMLWriter {
                }
                
                // content
-               if ($cdata !== '') $this->xml->writeCdata(StringUtil::escapeCDATA($cdata));
+               if ($cdata !== '') {
+                       if ($writeAsCdata) {
+                               $this->xml->writeCdata(StringUtil::escapeCDATA($cdata));
+                       }
+                       else {
+                               $this->xml->text($cdata);
+                       }
+               }
                
                $this->endElement();
        }
index 7f5d3b65958e1b19fc2035a9d27a7fe5453b8282..c9cd2a8eb5b5577dd96a69e08d5ca3373cddfc1f 100644 (file)
                <item name="wcf.acp.devtools.project.sync.pageTitle"><![CDATA[Daten-Abgleich - {$object->name}]]></item>
                <item name="wcf.acp.devtools.pip.defaultFilename"><![CDATA[Suchmuster]]></item>
                <item name="wcf.acp.devtools.pip.error.notIdempotent"><![CDATA[Das PIP unterstützt keinen wiederholten Import und kann nur bei einem Update verarbeitet werden.]]></item>
+               <item name="wcf.acp.devtools.pip.error.noGuiSupport"><![CDATA[Das PIP unterstützt keinen Verwaltung von Einträgen mittels einer grafischen Benutzeroberfläche.]]></item>
                <item name="wcf.acp.devtools.pip.notice"><![CDATA[Bestehende Anweisungen in der <kbd>package.xml</kbd> werden nicht berücksichtigt; es können somit auch PIPs importiert werden, für die noch keine Anweisungen hinterlegt worden sind. Es werden nur die Standardpfade bei der Suche verwendet, zusätzlich werden anwendungsbezogene Suffixe (z. B. <kbd>files_wcf.tar</kbd>) für <kbd>.tar</kbd>-basierte PIPs unterstützt.]]></item>
                <item name="wcf.acp.devtools.pip.pluginName"><![CDATA[PIP-Bezeichner]]></item>
                <item name="wcf.acp.devtools.pip.showOnlyMatches"><![CDATA[Zeige nur übereinstimmende PIPs an]]></item>
                <item name="wcf.acp.devtools.notificationTest.link"><![CDATA[Link]]></item>
                <item name="wcf.acp.devtools.notificationTest.link.exception"><![CDATA[Link-Fehlermeldung]]></item>
                <item name="wcf.acp.devtools.notificationTest.links"><![CDATA[Links]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.add"><![CDATA[{$pip}-Eintrag hinzufügen]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.add.pageTitle"><![CDATA[{$pip}-Eintrag hinzufügen - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.button.add"><![CDATA[Eintrag hinzufügen]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.list"><![CDATA[{$pip}-Einträge]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.list.pageTitle"><![CDATA[{$pip}-Einträge - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.edit"><![CDATA[{$pip}-Eintrag bearbeiten]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.edit.pageTitle"><![CDATA[{$pip}-Eintrag bearbeiten - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pip.list"><![CDATA[Package Installation Plugins]]></item>
+               <item name="wcf.acp.devtools.project.pip.list.pageTitle"><![CDATA[Package Installation Plugins - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pips"><![CDATA[PIPs]]></item>
+               <item name="wcf.acp.devtools.pip.showGuiSupportingPipsOnly"><![CDATA[Zeige nur PIPs mit GUI-Unterstützung an]]></item>
+               <item name="wcf.acp.devtools.pip.showGuiSupportingPipsOnly.description"><![CDATA[Es werden nur PIPs angeboten, die die Verwaltung von Einträgen mittels einer grafischen Benutzeroberfläche unterstützen.]]></item>
        </category>
        
        <category name="wcf.acp.email">
@@ -1730,6 +1743,96 @@ Als Benachrichtigungs-URL in der Konfiguration der sofortigen Zahlungsbestätigu
                <item name="wcf.acp.pluginStore.purchasedItems.updateServer.requireUpdate"><![CDATA[Der Paket-Server für „{$wcfMajorRelease}“ („http://store.woltlab.com/{$wcfMajorRelease}/“) wurde noch nicht abgefragt, bitte {if LANGUAGE_USE_INFORMAL_VARIANT}lass{else}lassen Sie{/if} zuerst nach Updates suchen, um den Server abzufragen.]]></item>
                <item name="wcf.acp.pluginStore.purchasedItems.wcfMajorRelease"><![CDATA[Paket-Server für „{$wcfMajorRelease}“]]></item>
        </category>
+
+       <category name="wcf.acp.pip">
+               <item name="wcf.acp.pip.general.options"><![CDATA[Einstellungen]]></item>
+               <item name="wcf.acp.pip.general.options.error.nonExistent"><![CDATA[Die folgenden Einstellungen existieren nicht: {implode from=$options item=option}<code>{$option}</code>{/implode}. {if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst{else}Sie können{/if} <a href="{link controller='DevtoolsProjectPipEntryAdd' id=$project->projectID pip='option'}{/link}">hier</a> eine neue Einstellung erstellen.]]></item>
+               <item name="wcf.acp.pip.general.permissions"><![CDATA[Berechtigungen]]></item>
+               <item name="wcf.acp.pip.general.permissions.error.nonExistent"><![CDATA[Die folgenden Berechtigungen existieren nicht: {implode from=$options item=option}<code>{$option}</code>{/implode}. {if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst{else}Sie können{/if} <a href="{link controller='DevtoolsProjectPipEntryAdd' id=$project->projectID pip='userGroupOption'}{/link}">hier</a> eine neue Berechtigung erstellen.]]></item>
+               <item name="wcf.acp.pip.objectType.className"><![CDATA[Klasse]]></item>
+               <item name="wcf.acp.pip.objectType.className.description"><![CDATA[Die angegebene Klasse (ohne Backslash als erstes Zeichen) muss das Interface <code>{$interfaceName}</code> implementieren.]]></item>
+               <item name="wcf.acp.pip.objectType.className.error.leadingBackslash"><![CDATA[Der angegebene Klassenname hat ein Backslash als erstes Zeichen.]]></item>
+               <item name="wcf.acp.pip.objectType.className.error.nonExistent"><![CDATA[Die angebene Klasse existiert nicht.]]></item>
+               <item name="wcf.acp.pip.objectType.className.error.interface"><![CDATA[Die angebene Klasse implementiert nicht das Interface <code>{$interfaceName}</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.data.title"><![CDATA[Daten des Dateianhangtyps]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private.description"><![CDATA[Private Dateianhänge werden in der Dateianhangsverwaltung im ACP ignoriert.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.data.title"><![CDATA[Daten der Benutzermassenverarbeitungsaktion]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action"><![CDATA[Aktion]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.options.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.permissions.description"><![CDATA[Der aktive Benutzer muss mindestens eine der angegebenen Berechtigungen besitzen, um die Aktion ausführen zu dürfen.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.data.title"><![CDATA[Dates des Kategorietypes]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission"><![CDATA[Standard-Kategorieberechtigung]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.data.title"><![CDATA[Daten des Clipboard-Eintrags]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName"><![CDATA[Database Object List-Klasse]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName"><![CDATA[Objekttyp-Definition]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.acl.description"><![CDATA[TODO: This object type definition is used to register types of objects for which ACL is available. ACL (Access control list) is used to set up (multiple) user and user group permissions for a specific object.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.acl.simple.description"><![CDATA[TODO: This object type definition is used to register types of objects for which simple ACL is available. Simple ACL (Access control list) is used to set up <strong>one</strong> yes/no permission user and user group permissions for a specific object.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.adLocation.description"><![CDATA[TODO: This object type definition is used to register locations at which ads can be displayed.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.attachment.objectType.description"><![CDATA[TODO: This object type definition is used to register types of objects that support attaching files to them.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.box.articleList.condition.description"><![CDATA[TODO: This object type definition is used to register available conditions/settings for boxes listing articles to determine which articles are shown in the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.box.recentActivityList.condition.description"><![CDATA[TODO: This object type definition is used to register available conditions/settings for boxes listing recent activities to determine which recent activities are shown in the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.box.userTrophyList.condition.description"><![CDATA[TODO: This object type definition is used to register available conditions/settings for boxes listing user trophies to determine which user trophies are shown in the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.boxController.description"><![CDATA[TODO: This object type definition is used to register box controllers that provide dynamic content based on the specific settings of the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.bulkProcessableObject.description"><![CDATA[TODO: This object type definition is used to register types of objects that support the bulk processing API with which a specific action can be executed on a list of object that fulfill certain conditions.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.bulkProcessing.user.action.description"><![CDATA[TODO: This object type definition is used to register actions that can be executed when processing users in bulk.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.bulkProcessing.user.condition.description"><![CDATA[TODO: This object type definition is used to register conditions for users that are processed in bulk.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.captcha.description"><![CDATA[TODO: This object type definition is used to register captcha types that administrators are able to select to protect their sites.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.category.description"><![CDATA[TODO: This object type definition is used to register types of objects that can/must be categorized.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.clipboardItem.description"><![CDATA[TODO: This object type definition is used to register clipboard items that enable users to execute actions on multiple objects that were selected via checkboxes.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.collapsibleContent.description"><![CDATA[TODO: This object type definition is used to register content that users are able to collapse persistently.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.comment.commentableContent.description"><![CDATA[TODO: This object type definition is used to register objects that can be commented.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.ad.description"><![CDATA[TODO: This object type definition is used to register available conditions/settings for ads used to determine whether a specific ad is shown.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.notice.description"><![CDATA[TODO: This object type definition is used to register available conditions/settings for notices used to determine whether a specific notice is shown.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.trophy.description"><![CDATA[TODO: This object type definition is used to register available conditions/settings for trophies used to determine whether a specific trophy is awarded.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.userGroupAssignment.description"><![CDATA[TODO: This object type definition is used to register conditions/settings for user group assignments used to determine whether a specific user is assigned to the user group.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.userSearch.description"><![CDATA[TODO: This object type definition is used to register conditions/settings used when searching for users.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.deletedContent.description"><![CDATA[TODO: This object type definition is used to register types of objects that can be deleted and whose deleted objects will be shown in a specific list of deleted contents accessible for moderators.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.edit.historySavingObject.description"><![CDATA[TODO: This object type definition is used to register messages of which different versions can be tracked using the edit history API.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.exporter.description"><![CDATA[TODO: This object type definition is used to register exporters that export data from other software and import it into WoltLab Suite Core.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.importer.description"><![CDATA[TODO: This object type definition is used to register importers for specific types of objects.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.label.object.description"><![CDATA[TODO: This object type definition is used to register types of objects to which labels can be assigned.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.label.objectType.description"><![CDATA[TODO: This object type definition is used to register types of objects for which labels can be set up in the ACP.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.like.likeableObject.description"><![CDATA[TODO: This object type definition is used to register types of objects that can be liked (and disliked).]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.message.description"><![CDATA[TODO: This object type definition is used to register messages that support the WYSIWYG editor.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.message.embeddedObject.description"><![CDATA[TODO: This object type definition is used to register types of messages in that other objects like media can be embedded.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.message.quote.description"><![CDATA[TODO: This object type definition is used to register types of messages that can be quoted.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.moderation.activation.description"><![CDATA[TODO: This object type definition is used to register types of objects that be enabled and disabled. For disabled objects, a moderation queue entry is created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.moderation.report.description"><![CDATA[TODO: This object type definition is used to register types of objects that can be reported. For reported objects, a moderation queue entry is created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.moderation.type.description"><![CDATA[TODO: This object type definition is used to register states of objects for which they are considered as under moderation.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.modifiableContent.description"><![CDATA[TODO: This object type definition is used to register types of objects for which modifications can be logged and accessed via a chronological modification list.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.notification.notificationType.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.notification.objectType.description"><![CDATA[TODO: This object type definition is used to register types of objects for which notifications can be sent.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.payment.method.description"><![CDATA[TODO: This object type definition is used to register payment methods used, for example, for subscriptions to user groups.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.payment.type.description"><![CDATA[TODO: This object type definition is used to register types of objects for which payment is possible like subscriptions to user groups.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.poll.description"><![CDATA[TODO: This object type definition is used to register types of objects that support polls.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.rebuildData.description"><![CDATA[TODO: This object type definition is used to register workers used to rebuild a specific type of data, for example, after a data import.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.searchableObjectType.description"><![CDATA[TODO: This object type definition is used to register objects that be searched.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.sitemap.object.description"><![CDATA[TODO: This object type definition is used to register types of objects for which a sitemap will be created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.statDailyHandler.description"><![CDATA[TODO: This object type definition is used to register handlers that create specific daily stats.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.tagging.taggableObject.description"><![CDATA[TODO: This object type definition is used to register types of objects that can be tagged.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.user.activityPointEvent.description"><![CDATA[TODO: This object type definition is used to register events for which users are awarded activity points.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.user.objectWatch.description"><![CDATA[TODO: This object type definition is used to register types of objects that can be watched/subscribed to resulting in notifications for updates of the watched/subscribed object.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.user.recentActivityEvent.description"><![CDATA[TODO: This object type definition is used to register events for which a recent activity entry can be created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.versionTracker.objectType.description"><![CDATA[TODO: This object type definition is used to register types of objects of which different versions can be tracked using the version tracker API.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.visitTracker.objectType.description"><![CDATA[TODO: This object type definition is used to register types of objects for which it can be tracked if and when users have already seen or accessed them (the last time).]]></item>
+               <item name="wcf.acp.pip.objectType.objectType"><![CDATA[Objekttyp-Bezeichner]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.description"><![CDATA[Text-Bezeichner des Objekttyps, der primär in PHP-Code verwendet wird]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.invalidSegments"><![CDATA[Die folgenden Abschnitte sind ungültig: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(leer){/if} (Abschnitt {#$segmentNumber + 1}){/implode}]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.notUnique"><![CDATA[Der angegebene Name wird bereits von einem anderen Objekttypen derselben Objekttyp-Definition verwendet.]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.tooFewSegments"><![CDATA[Der angegebene Bezeichner enthält nur {#$segmentCount} Abschnitt{if $segmentCount > 1}e{/if}.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName"><![CDATA[Definition-Name]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.description"><![CDATA[Der Name einer Objekttyp-Definition besteht aus mindestens vier durch Punkte abgetrennte Abschnitte. Jeder Abschnitt darf nicht leer sein und darf nur aus Buchstaben, Zahlen, Unterstrichen und Bindestrichen bestehen. Normalerweise stimmt der erste Teil des Namens mit dem Bezeichner des Paketes überein. Example: <code>{$project->getPackage()->package}.type</code>]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.invalidSegments"><![CDATA[Die folgenden Abschnitte sind ungültig: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(leer){/if} (Abschnitt {#$segmentNumber + 1}){/implode}]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.notUnique"><![CDATA[Der angegebene Name wird bereits von einer anderen Definition verwendet.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.tooFewSegments"><![CDATA[Der angegebene Name enthält nur {#$segmentCount} Abschnitt{if $segmentCount > 1}e{/if}.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName"><![CDATA[PHP-Interface]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.description"><![CDATA[Wird ein PHP-Interface angegeben, muss jeder Objekttype dieser Definition den Namen einer PHP-Klasse angeben, die dieses Interface implementiert.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.error.nonExistent"><![CDATA[Das angegebene Interface existiert nicht.]]></item>
+               <!-- TODO: add remaining language items -->
+       </category>
        
        <category name="wcf.acp.rebuildData">
                <item name="wcf.acp.rebuildData"><![CDATA[Anzeigen aktualisieren]]></item>
@@ -2907,6 +3010,7 @@ E-Mail-Adresse: {@$emailAddress} {* this line ends with a space *}
                <item name="wcf.global.button.fullscreen"><![CDATA[Vollbildmodus]]></item>
                <item name="wcf.global.button.hide"><![CDATA[Ausblenden]]></item>
                <item name="wcf.global.button.insert"><![CDATA[Einfügen]]></item>
+               <item name="wcf.global.button.list"><![CDATA[Auflistung]]></item>
                <item name="wcf.global.button.next"><![CDATA[Weiter »]]></item>
                <item name="wcf.global.button.preview"><![CDATA[Vorschau]]></item>
                <item name="wcf.global.button.refresh"><![CDATA[Aktualisieren]]></item>
index e15978ce831b994af7738c3e4b8d3ffc6ffdb773..b09dc58191a645cbaf6a90a78d2540dbe9da95db 100644 (file)
                <item name="wcf.acp.devtools.project.sync.pageTitle"><![CDATA[Sync Data - {$object->name}]]></item>
                <item name="wcf.acp.devtools.pip.defaultFilename"><![CDATA[Search Pattern]]></item>
                <item name="wcf.acp.devtools.pip.error.notIdempotent"><![CDATA[This PIP does not support repeated imports and can only be processed in regular updates.]]></item>
+               <item name="wcf.acp.devtools.pip.error.noGuiSupport"><![CDATA[This PIP does not support managing entries via a graphical user interface.]]></item>
                <item name="wcf.acp.devtools.pip.notice"><![CDATA[Any existing instructions in the <kbd>package.xml</kbd> will be ignored; This allows the import of PIPs that have no specific instructions provided for them yet. Only the suggested default paths are recognized, with an additional support for application suffixes for <kbd>.tar</kbd>-archives (e. g. <kbd>files_wcf.tar</kbd>) are supported.]]></item>
                <item name="wcf.acp.devtools.pip.pluginName"><![CDATA[PIP identifier]]></item>
                <item name="wcf.acp.devtools.pip.showOnlyMatches"><![CDATA[Show valid PIPs only]]></item>
                <item name="wcf.acp.devtools.notificationTest.link"><![CDATA[Link]]></item>
                <item name="wcf.acp.devtools.notificationTest.link.exception"><![CDATA[Link Error Message]]></item>
                <item name="wcf.acp.devtools.notificationTest.links"><![CDATA[Links]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.add"><![CDATA[Add {$pip} Entry]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.add.pageTitle"><![CDATA[Add {$pip} Entry - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.button.add"><![CDATA[Add Entry]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.list"><![CDATA[{$pip} Entries]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.list.pageTitle"><![CDATA[{$pip} Entries - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.edit"><![CDATA[Edit {$pip} Entry]]></item>
+               <item name="wcf.acp.devtools.project.pip.entry.edit.pageTitle"><![CDATA[Edit {$pip} Entry - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pip.list"><![CDATA[Package Installation Plugins]]></item>
+               <item name="wcf.acp.devtools.project.pip.list.pageTitle"><![CDATA[Package Installation Plugins - {$project->name}]]></item>
+               <item name="wcf.acp.devtools.project.pips"><![CDATA[PIPs]]></item>
+               <item name="wcf.acp.devtools.pip.showGuiSupportingPipsOnly"><![CDATA[Show PIPs supporting GUI only]]></item>
+               <item name="wcf.acp.devtools.pip.showGuiSupportingPipsOnly.description"><![CDATA[Show only PIPs that support managing entries via a graphical user interface.]]></item>
        </category>
        
        <category name="wcf.acp.email">
@@ -1671,6 +1684,206 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pluginStore.purchasedItems.wcfMajorRelease"><![CDATA[Update-Server for “{$wcfMajorRelease}”]]></item>
        </category>
        
+       <category name="wcf.acp.pip">
+               <item name="wcf.acp.pip.general.options"><![CDATA[Options]]></item>
+               <item name="wcf.acp.pip.general.options.error.nonExistent"><![CDATA[The following options do not exist: {implode from=$options item=option}<code>{$option}</code>{/implode}. You can create a new option <a href="{link controller='DevtoolsProjectPipEntryAdd' id=$project->projectID pip='option'}{/link}">here</a>.]]></item>
+               <item name="wcf.acp.pip.general.permissions"><![CDATA[Permissions]]></item>
+               <item name="wcf.acp.pip.general.permissions.error.nonExistent"><![CDATA[The following permissions do not exist: {implode from=$permissions item=permission}<code>{$permission}</code>{/implode}. You can create a new permission <a href="{link controller='DevtoolsProjectPipEntryAdd' id=$project->projectID pip='userGroupOption'}{/link}">here</a>.]]></item>
+               <item name="wcf.acp.pip.objectType.className"><![CDATA[PHP Class]]></item>
+               <item name="wcf.acp.pip.objectType.className.description"><![CDATA[The entered class (without leading backslash) must implement the interface <code>{$interfaceName}</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.className.error.leadingBackslash"><![CDATA[The entered class name has a leading backslash.]]></item>
+               <item name="wcf.acp.pip.objectType.className.error.nonExistent"><![CDATA[The entered class does not exist.]]></item>
+               <item name="wcf.acp.pip.objectType.className.error.interface"><![CDATA[The entered class does not implement the interface <code>{$interfaceName}</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.data.title"><![CDATA[Ad Location Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName"><![CDATA[Category]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.description"><![CDATA[Ad locations are grouped by their category in the ad location selection when creating and editing ads. The category consists of at least four segments that are separated by dots. Each segment may only contain the following characters: <code>[A-z0-9-_]</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.error.tooFewSegments"><![CDATA[The entered category only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName"><![CDATA[CSS Classes]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.description"><![CDATA[The entered comma-separated CSS classes are assigned to the element that wraps all ads at the specific location. ]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.error.invalid"><![CDATA[The following CSS classes are invalid: {implode from=$invalidClasses item=invalidClass}<code>{$invalidClass}</code>{/implode}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.data.title"><![CDATA[Attachment Type Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private"><![CDATA[Attachments are Private]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private.description"><![CDATA[Private attachments are ignored by the attachment management in the ACP.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.data.title"><![CDATA[User Bulk Processing Action Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action"><![CDATA[Action]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.description"><![CDATA[Unique textual identifier of the user bulk processing action that may only contain letters and must start with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.error.format"><![CDATA[The entered action is invalid.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.options.description"><![CDATA[At least one of the entered options has to be enabled for the user bulk processing action to be available.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to execute the action.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.condition.data.title"><![CDATA[User Bulk Processing Condition Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.data.title"><![CDATA[Category Type Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission"><![CDATA[Category Default Permission]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission.description"><![CDATA[This value is used for users for which no category-specific permission value has been set (either directly for the users or indirectly for their user groups).]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.data.title"><![CDATA[Clipboard Item Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName"><![CDATA[Database Object List Class Name]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName.description"><![CDATA[The entered class must extend <code>wcf\data\DatabaseObjectList</code> and is used for fetching the selected objects on which a clipboard action will be executed.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName.error.nonExistent"><![CDATA[The entered class does not exist.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName.error.parentClass"><![CDATA[The entered class does not extend <code>wcf\data\DatabaseObjectList</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.condition.ad.data.title"><![CDATA[Ad Condition Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.condition.notice.data.title"><![CDATA[Notice Condition Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.condition.trophy.data.title"><![CDATA[Trophy Condition Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.condition.userGroupAssignment.data.title"><![CDATA[User Group Asssignment Condition Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.condition.userSearch.data.title"><![CDATA[User Search Condition Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.data.title"><![CDATA[Notification Object Type Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category"><![CDATA[Category]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category.description"><![CDATA[The category is used to group events on the notification settings page.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.data.title"><![CDATA[Rebuild Data Worker Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.niceValue"><![CDATA[Nice Value]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.niceValue.description"><![CDATA[The nice value is used to determine the order in which the rebuild data workers are shown and thus in which order they should be executed. Workers with lower nice value are shown first.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.data.title"><![CDATA[Searchable Object Type Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex"><![CDATA[Search Index Database Table]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.description"><![CDATA[Name of the database table containing the search index. The database table will be automatically created by the system. For all applications, <code>app1_</code> will be replaced with <code>appN_</code> in which <code>app</code> is the abbreviation of the relevant application and <code>N</code> is the value of <code>WCF_N</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.error.invalid"><![CDATA[The entered database table name is invalid.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.error.unknownApp"><![CDATA[No app with the abbreviation <code>{@$app}</code> is installed.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.data.title"><![CDATA[Sitemap Object Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.priority"><![CDATA[Priority]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.priority.description"><![CDATA[The priority tells crawlers which of your website’s pages you consider more important than the rest.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.changeFreq"><![CDATA[Change Frequency]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.changeFreq.description"><![CDATA[The change frequency tells crawlers how often a page changes on average.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.rebuildTime"><![CDATA[Rebuild Time]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.rebuildTime.description"><![CDATA[After the entered time interval, the sitemap will be rebuilt by the system.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.data.title"><![CDATA[Daily Statistics Handler Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName"><![CDATA[Category]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.description"><![CDATA[Daily statistics handler are grouped by their category on the stats page. The category consists of at least four segments that are separated by dots. Each segment may only contain the following characters: <code>[A-z0-9-_]</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.error.tooFewSegments"><![CDATA[The entered category only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault"><![CDATA[Default Daily Statistics Handler]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault.description"><![CDATA[Default daily statistics handler are pre-selected when loading the stats page.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.data.title"><![CDATA[Taggable Object Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.options.description"><![CDATA[At least one of the entered options has to be enabled the list of all objects of this type with a specific tag to be available.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to see the list of all objects of this type with a specific tag.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.data.title"><![CDATA[User Activity Event Points Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points"><![CDATA[Points]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points.description"><![CDATA[Number of points the user is awarded for the event.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.data.title"><![CDATA[Version Tracker Object Type Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName"><![CDATA[Database Table With Original Data]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName.description"><![CDATA[Name of the database table containg the original data whose different version will be tracked. For all applications, <code>app1_</code> will be replaced with <code>appN_</code> in which <code>app</code> is the abbreviation of the relevant application and <code>N</code> is the value of <code>WCF_N</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName.error.nonExistent"><![CDATA[The database table <code>{$tableName}</code> does not exist.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey"><![CDATA[Database Table Primary Key Column]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.description"><![CDATA[Name of the column containing the primary key of the database table containg the original data whose different version will be tracked.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.error.nonExistent"><![CDATA[The entered column does not exist in the database table entered above.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.error.noPrimaryColumn"><![CDATA[The entered column has no primary key.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionObject"><![CDATA[Conditioned Object Identifier]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionObject.description"><![CDATA[The object type-alike identifier of the object this condition is related with is used to group large lists of conditions into logical groups.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionObject.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionObject.error.tooFewSegments"><![CDATA[The entered identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionGroup"><![CDATA[Condition Group]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionGroup.description"><![CDATA[The condition group is used to group conditions with the same group identifier together into, generally, one tab. The condition group may only consist of letters and must begin with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionGroup.error.format"><![CDATA[The entered condition group is invalid.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.integer.maxValue"><![CDATA[Maximum Value]]></item>
+               <item name="wcf.acp.pip.objectType.condition.integer.maxValue.description"><![CDATA[When setting up the condition, the value for this condition may not be greater than the entered value.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.integer.minValue"><![CDATA[Minimum Value]]></item>
+               <item name="wcf.acp.pip.objectType.condition.integer.minValue.description"><![CDATA[When setting up the condition, the value for this condition may not be less than the entered value.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userGroup.includeGuests"><![CDATA[Include Guest User Group]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userGroup.includeGuests.description"><![CDATA[When setting up the condition, the guest user group can also be selected.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName"><![CDATA[User Property Name]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName.description"><![CDATA[Name of the user property/user database table column used for the condition.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName.error.noIntegerColumn"><![CDATA[The entered column name of the <code>wcf{WCF_N}_user</code> database table is no <code>INT</code> column.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userIntegerProperty.propertyName.error.nonExistent"><![CDATA[The entered column does not exist in the <code>wcf{WCF_N}_user</code> database table.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName"><![CDATA[User Property Name]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName.description"><![CDATA[Name of the user property/user database table column used for the condition.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName.error.noIntegerColumn"><![CDATA[The entered column name of the <code>wcf{WCF_N}_user</code> database table is no <code>INT</code> column.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.userTimestampProperty.propertyName.error.nonExistent"><![CDATA[The entered column does not exist in the <code>wcf{WCF_N}_user</code> database table.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName"><![CDATA[Object Type Definition]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.acl.description"><![CDATA[This object type definition is used to register types of objects for which ACL is available. ACL (Access control list) is used to set up (multiple) user and user group permissions for a specific object.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.acl.simple.description"><![CDATA[This object type definition is used to register types of objects for which simple ACL is available. Simple ACL (Access control list) is used to set up <strong>one</strong> yes/no permission user and user group permissions for a specific object.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.adLocation.description"><![CDATA[This object type definition is used to register locations at which ads can be displayed.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.attachment.objectType.description"><![CDATA[This object type definition is used to register types of objects that support attaching files to them.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.box.articleList.condition.description"><![CDATA[This object type definition is used to register available conditions/settings for boxes listing articles to determine which articles are shown in the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.box.recentActivityList.condition.description"><![CDATA[This object type definition is used to register available conditions/settings for boxes listing recent activities to determine which recent activities are shown in the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.box.userTrophyList.condition.description"><![CDATA[This object type definition is used to register available conditions/settings for boxes listing user trophies to determine which user trophies are shown in the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.boxController.description"><![CDATA[This object type definition is used to register box controllers that provide dynamic content based on the specific settings of the box.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.bulkProcessableObject.description"><![CDATA[This object type definition is used to register types of objects that support the bulk processing API with which a specific action can be executed on a list of object that fulfill certain conditions.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.bulkProcessing.user.action.description"><![CDATA[This object type definition is used to register actions that can be executed when processing users in bulk.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.bulkProcessing.user.condition.description"><![CDATA[This object type definition is used to register conditions for users that are processed in bulk.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.captcha.description"><![CDATA[This object type definition is used to register captcha types that administrators are able to select to protect their sites.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.category.description"><![CDATA[This object type definition is used to register types of objects that can/must be categorized.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.clipboardItem.description"><![CDATA[This object type definition is used to register clipboard items that enable users to execute actions on multiple objects that were selected via checkboxes.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.collapsibleContent.description"><![CDATA[This object type definition is used to register content that users are able to collapse persistently.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.comment.commentableContent.description"><![CDATA[This object type definition is used to register objects that can be commented.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.ad.description"><![CDATA[This object type definition is used to register available conditions/settings for ads used to determine whether a specific ad is shown.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.notice.description"><![CDATA[This object type definition is used to register available conditions/settings for notices used to determine whether a specific notice is shown.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.trophy.description"><![CDATA[This object type definition is used to register available conditions/settings for trophies used to determine whether a specific trophy is awarded.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.userGroupAssignment.description"><![CDATA[This object type definition is used to register conditions/settings for user group assignments used to determine whether a specific user is assigned to the user group.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.condition.userSearch.description"><![CDATA[This object type definition is used to register conditions/settings used when searching for users.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.deletedContent.description"><![CDATA[This object type definition is used to register types of objects that can be deleted and whose deleted objects will be shown in a specific list of deleted contents accessible for moderators.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.edit.historySavingObject.description"><![CDATA[This object type definition is used to register messages of which different versions can be tracked using the edit history API.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.exporter.description"><![CDATA[This object type definition is used to register exporters that export data from other software and import it into WoltLab Suite Core.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.importer.description"><![CDATA[This object type definition is used to register importers for specific types of objects.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.label.object.description"><![CDATA[This object type definition is used to register types of objects to which labels can be assigned.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.label.objectType.description"><![CDATA[This object type definition is used to register types of objects for which labels can be set up in the ACP.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.like.likeableObject.description"><![CDATA[This object type definition is used to register types of objects that can be liked (and disliked).]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.message.description"><![CDATA[This object type definition is used to register messages that support the WYSIWYG editor.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.message.embeddedObject.description"><![CDATA[This object type definition is used to register types of messages in that other objects like media can be embedded.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.message.quote.description"><![CDATA[This object type definition is used to register types of messages that can be quoted.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.moderation.activation.description"><![CDATA[This object type definition is used to register types of objects that be enabled and disabled. For disabled objects, a moderation queue entry is created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.moderation.report.description"><![CDATA[This object type definition is used to register types of objects that can be reported. For reported objects, a moderation queue entry is created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.moderation.type.description"><![CDATA[This object type definition is used to register “states” of objects so that for objects in that state, a moderation queue entry is created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.modifiableContent.description"><![CDATA[This object type definition is used to register types of objects for which modifications can be logged and accessed via a chronological modification list.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.notification.objectType.description"><![CDATA[This object type definition is used to register types of objects for which notifications can be sent.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.payment.method.description"><![CDATA[This object type definition is used to register payment methods used, for example, for subscriptions to user groups.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.payment.type.description"><![CDATA[This object type definition is used to register types of objects for which payment is possible like subscriptions to user groups.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.poll.description"><![CDATA[This object type definition is used to register types of objects that support polls.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.rebuildData.description"><![CDATA[This object type definition is used to register workers used to rebuild a specific type of data, for example, after a data import.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.searchableObjectType.description"><![CDATA[This object type definition is used to register objects that be searched.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.sitemap.object.description"><![CDATA[This object type definition is used to register types of objects for which a sitemap will be created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.statDailyHandler.description"><![CDATA[This object type definition is used to register handlers that create specific daily stats.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.tagging.taggableObject.description"><![CDATA[This object type definition is used to register types of objects that can be tagged.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.user.activityPointEvent.description"><![CDATA[This object type definition is used to register events for which users are awarded activity points.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.user.objectWatch.description"><![CDATA[This object type definition is used to register types of objects that can be watched/subscribed to resulting in notifications for updates of the watched/subscribed object.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.user.recentActivityEvent.description"><![CDATA[This object type definition is used to register events for which a recent activity entry can be created.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.versionTracker.objectType.description"><![CDATA[This object type definition is used to register types of objects of which different versions can be tracked using the version tracker API.]]></item>
+               <item name="wcf.acp.pip.objectType.definitionName.com.woltlab.wcf.visitTracker.objectType.description"><![CDATA[This object type definition is used to register types of objects for which it can be tracked if and when users have already seen or accessed them (the last time).]]></item>
+               <item name="wcf.acp.pip.objectType.objectType"><![CDATA[Object Type Identifier]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.description"><![CDATA[Textual identifier of the object type that is primarily used in PHP code. The identifier consists of at least four segments that are separated by dots. Each segment may only contain the following characters: <code>[A-z0-9-_]</code>.]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.notUnique"><![CDATA[The entered name is already used by another object type of the same object type definition.]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.tooFewSegments"><![CDATA[The entered identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName"><![CDATA[Definition Name]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.description"><![CDATA[The name of an object type definitions consists of least four segments separated by dots. Each segment must not be empty and may only contain letters, numbers, underscores, and dashes. In general, the first part of the definition name matches the package identifier. Example: <code>{$project->getPackage()->package}.type</code>]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.notUnique"><![CDATA[The entered name is already used by another definition.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.tooFewSegments"><![CDATA[The entered name only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName"><![CDATA[PHP Interface]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.description"><![CDATA[If a PHP interface is entered, every object type of this definition must provide the name of a PHP class that implements the interface.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.error.nonExistent"><![CDATA[The entered interface does not exist.]]></item>
+               <item name="wcf.acp.pip.pip.pluginName"><![CDATA[Package Installation Plugin Name]]></item>
+               <item name="wcf.acp.pip.pip.pluginName.description"><![CDATA[The name of the package installation plugin is used as the value of the <code>type</code> attribute of an <code>instruction</code> element in a <code>package.xml</code> file. The name may only consist of letters and must begin with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.pip.pluginName.error.format"><![CDATA[The entered name is invalid.]]></item>
+               <item name="wcf.acp.pip.pip.pluginName.error.notUnique"><![CDATA[The entered name is already used by another package installation plugin.]]></item>
+               <item name="wcf.acp.pip.pip.className"><![CDATA[Class Name]]></item>
+               <item name="wcf.acp.pip.pip.className.description"><![CDATA[The entered class (without leading backslash) must implement the interface <code>wcf\system\package\plugin\IPackageInstallationPlugin</code>.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.leadingBackslash"><![CDATA[The entered class name has a leading backslash.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.nonExistent"><![CDATA[The entered class does not exist.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.interface"><![CDATA[The entered class does not implement the interface <code>wcf\system\package\plugin\IPackageInstallationPlugin</code>.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.isInstantiable"><![CDATA[The entered class is not instantiable.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.name"><![CDATA[Menu Item Name]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.name.description"><![CDATA[The name of the user profile may only contain letters and must begin with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.name.error.format"><![CDATA[The entered name is invalid.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.name.error.notUnique"><![CDATA[The entered name is already used by another user profile menu item.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.className"><![CDATA[Class Name]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.className.description"><![CDATA[The entered class (without leading backslash) must implement the interface <code>wcf\system\menu\user\profile\content\IUserProfileMenuContent</code>.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.showOrder"><![CDATA[Position]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.showOrder.description"><![CDATA[The entered value determines in which order the user profile menu items are shown.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.options.description"><![CDATA[At least one of the entered options has to be enabled for the menu item to be available.]]></item>
+               <item name="wcf.acp.pip.userProfileMenu.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to see the menu item.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.name"><![CDATA[Event Name]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.name.description"><![CDATA[The name of the event may only contain letters and must begin with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.name.error.format"><![CDATA[The entered name is invalid.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.name.error.notUnique"><![CDATA[The entered name is already used by another event for the same event object type.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.objectType"><![CDATA[Event Object Type]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.objectType.description"><![CDATA[Determines the type of object the notification event belongs to.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.className"><![CDATA[Class Name]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.className.description"><![CDATA[The entered class (without leading backslash) must implement the interface <code>wcf\system\user\notification\event\IUserNotificationEvent</code>.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.preset"><![CDATA[Enable By Default]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.preset.description"><![CDATA[If selected, users will automatically receive notifications for this event by default. If they do not want to receive such notifications, they have to explicitly disable them in their notification settings.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.options.description"><![CDATA[At least one of the entered options has to be enabled for the event to be visible in users’ notification settings.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to see the event in their notification settings.]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.presetMailNotificationType"><![CDATA[Default Mail Notification Type]]></item>
+               <item name="wcf.acp.pip.userNotificationEvent.presetMailNotificationType.description"><![CDATA[If a notification type is selected, users’ mail setting for this event will have the selected value by default.]]></item>
+       </category>
+       
        <category name="wcf.acp.rebuildData">
                <item name="wcf.acp.rebuildData"><![CDATA[Rebuild Data]]></item>
                <item name="wcf.acp.rebuildData.description"><![CDATA[In order to ensure consistency, e.g. after a data import, it is highly recommended to perform all the actions below in their respective order, starting from top to bottom.]]></item>
@@ -2853,6 +3066,7 @@ Email: {@$emailAddress} {* this line ends with a space *}
                <item name="wcf.global.button.fullscreen"><![CDATA[Full Screen Mode]]></item>
                <item name="wcf.global.button.hide"><![CDATA[Hide]]></item>
                <item name="wcf.global.button.insert"><![CDATA[Insert]]></item>
+               <item name="wcf.global.button.list"><![CDATA[List]]></item>
                <item name="wcf.global.button.next"><![CDATA[Next »]]></item>
                <item name="wcf.global.button.preview"><![CDATA[Preview]]></item>
                <item name="wcf.global.button.reply"><![CDATA[Reply]]></item>