Add GUI support for language package installation plugin
authorMatthias Schmidt <gravatronics@live.com>
Wed, 3 Oct 2018 14:28:54 +0000 (16:28 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Wed, 3 Oct 2018 14:28:54 +0000 (16:28 +0200)
See #2545

wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php
wcfsetup/install/files/lib/system/devtools/pip/TMultiXmlGuiPackageInstallationPlugin.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/form/builder/field/validation/FormFieldValidatorUtil.class.php
wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/util/XML.class.php
wcfsetup/install/lang/en.xml

index 4fd36e8ce01f960fd26ee413d4387d60e3bd69fe..6142352e9629338e7a5568c4f9457eae8959fd2c 100644 (file)
@@ -7,7 +7,9 @@ use wcf\data\DatabaseObject;
 use wcf\system\devtools\package\DevtoolsPackageArchive;
 use wcf\system\devtools\pip\DevtoolsPip;
 use wcf\system\package\validation\PackageValidationException;
+use wcf\system\Regex;
 use wcf\system\WCF;
+use wcf\util\DirectoryUtil;
 
 /**
  * Represents a devtools project.
@@ -158,6 +160,17 @@ class DevtoolsProject extends DatabaseObject {
                return $this->packageArchive;
        }
        
+       /**
+        * Returns the absolute paths of the language files.
+        * 
+        * @return      string[]
+        */
+       public function getLanguageFiles() {
+               $languageDirectory = $this->path . ($this->isCore() ? 'wcfsetup/install/lang/' : 'language/');
+               
+               return array_values(DirectoryUtil::getInstance($languageDirectory)->getFiles(SORT_ASC, Regex::compile('\w+\.xml')));
+       }
+       
        /**
         * Validates the provided path and returns an error code
         * if the path does not exist (`notFound`) or if there is
index 874a7b57dfc10e9aade34f73c0f16e3158e60719..1c762ecb509dd2131f14835399c522c1294c0231 100644 (file)
@@ -122,7 +122,7 @@ class DevtoolsPip extends DatabaseObjectDecorator {
         * 
         * Note: No target will be set for the package installation plugin object.
         * 
-        * @return      IPackageInstallationPlugin
+        * @return      IPackageInstallationPlugin|IGuiPackageInstallationPlugin
         * @since       3.2
         */
        public function getPip() {
diff --git a/wcfsetup/install/files/lib/system/devtools/pip/TMultiXmlGuiPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/devtools/pip/TMultiXmlGuiPackageInstallationPlugin.class.php
new file mode 100644 (file)
index 0000000..4829229
--- /dev/null
@@ -0,0 +1,188 @@
+<?php
+namespace wcf\system\devtools\pip;
+use wcf\system\form\builder\field\IFormField;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\package\PackageInstallationDispatcher;
+use wcf\util\DOMUtil;
+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 that works with multiple
+ * files at once.
+ * 
+ * @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
+ * 
+ * @property   PackageInstallationDispatcher|DevtoolsPackageInstallationDispatcher     $installation
+ */
+trait TMultiXmlGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
+       /**
+        * dom elements representing the original data of the edited entry
+        * @var \DOMElement[]
+        */
+       protected $editedEntries;
+       
+       /**
+        * Adds a new entry of this pip based on the data provided by the given
+        * form.
+        *
+        * @param       IFormDocument           $form
+        */
+       public function addEntry(IFormDocument $form) {
+               foreach ($this->getProjectXmls() as $xml) {
+                       $document = $xml->getDocument();
+                       
+                       $newElement = $this->writeEntry($document, $form);
+                       
+                       $this->saveObject($newElement);
+                       
+                       $this->sortDocument($document);
+                       
+                       // TODO: while creating/testing the gui, write into a temporary file
+                       // $xml->write($this->getXmlFileLocation($project));
+                       $xml->write(substr($xml->getPath(), 0, -4) . '_tmp.xml');
+               }
+       }
+       
+       /**
+        * 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, $identifier) {
+               $newEntry = null;
+               foreach ($this->getProjectXmls() as $xml) {
+                       $document = $xml->getDocument();
+                       
+                       // remove old element
+                       $element = $this->getElementByIdentifier($xml, $identifier);
+                       DOMUtil::removeNode($element);
+                       
+                       // add updated element
+                       $newEntry = $this->writeEntry($document, $form);
+                       
+                       $this->saveObject($newEntry, $element);
+                       
+                       $this->sortDocument($document);
+                       
+                       // TODO: while creating/testing the gui, write into a temporary file
+                       // $xml->write($this->getXmlFileLocation($project));
+                       $xml->write(substr($xml->getPath(), 0, -4) . '_tmp.xml');
+               }
+               
+               if ($newEntry === null) {
+                       throw new \UnexpectedValueException("Have not edited any entry");
+               }
+               
+               return $this->getElementIdentifier($newEntry);
+       }
+       
+       /**
+        * Returns a list of all pip entries of this pip.
+        *
+        * @return      IDevtoolsPipEntryList
+        */
+       public function getEntryList() {
+               $entryList = new DevtoolsPipEntryList();
+               $this->setEntryListKeys($entryList);
+               
+               foreach ($this->getProjectXmls() as $xml) {
+                       $xpath = $xml->xpath();
+                       
+                       /** @var \DOMElement $element */
+                       foreach ($this->getImportElements($xpath) as $element) {
+                               $entryList->addEntry(
+                                       $this->getElementIdentifier($element),
+                                       array_intersect_key($this->getElementData($element), $entryList->getKeys())
+                               );
+                       }
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * Returns the xml objects for this pip.
+        * 
+        * @return      XML[]
+        */
+       abstract protected function getProjectXmls();
+       
+       /**
+        * @inheritDoc
+        */
+       public function setEditedEntryIdentifier($identifier) {
+               $editedEntries = [];
+               foreach ($this->getProjectXmls() as $xml) {
+                       $editedEntry = $this->getElementByIdentifier($xml, $identifier);
+                       
+                       if ($editedEntry !== null) {
+                               $editedEntries[] = $editedEntry;
+                       }
+               }
+               
+               if (empty($editedEntries)) {
+                       throw new \InvalidArgumentException("Unknown entry with identifier '{$identifier}'.");
+               }
+               
+               $this->editedEntries = $editedEntries;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function setEntryData($identifier, IFormDocument $document) {
+               $xmls = $this->getProjectXmls();
+               $missingElements = 0;
+               
+               foreach ($xmls as $xml) {
+                       $element = $this->getElementByIdentifier($xml, $identifier);
+                       if ($element === null) {
+                               $missingElements++;
+                               
+                               continue;
+                       }
+                       
+                       $data = $this->getElementData($element);
+                       
+                       /** @var IFormNode $node */
+                       foreach ($document->getIterator() as $node) {
+                               if ($node instanceof IFormField && $node->isAvailable()) {
+                                       $key = $node->getId();
+                                       
+                                       if (isset($data[$key])) {
+                                               $node->value($data[$key]);
+                                       }
+                                       else if ($node->getObjectProperty() !== $node->getId()) {
+                                               $key = $node->getObjectProperty();
+                                               
+                                               try {
+                                                       if (isset($data[$key])) {
+                                                               $node->value($data[$key]);
+                                                       }
+                                               }
+                                               catch (\InvalidArgumentException $e) {
+                                                       // ignore invalid argument exceptions for fields with object property
+                                                       // as there might be multiple fields with the same object property but
+                                                       // different possible values (for example when using single selection
+                                                       // form fields to set the parent element)
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
+               return $missingElements !== count($xmls);
+       }
+}
index fb7c8e8e29b16787e5738f060e47cd734acb64f3..ffabd32be2320a129d6d3acc50d51b471650c16b 100644 (file)
@@ -26,7 +26,7 @@ use wcf\util\XML;
  */
 trait TXmlGuiPackageInstallationPlugin {
        /**
-        * dom element representing the original data of the edited element
+        * dom element representing the original data of the edited entry
         * @var null|\DOMElement
         */
        protected $editedEntry;
index bc26b7f7cd28cd7633c3744a46cb98462e1b97fc..d5bfa5bea4065b123168811486226179aebd4d74 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\system\form\builder\field\validation;
 use wcf\system\form\builder\field\IFormField;
+use wcf\system\form\builder\field\TextFormField;
 use wcf\system\Regex;
 
 /**
@@ -13,6 +14,65 @@ use wcf\system\Regex;
  * @since      3.2
  */
 abstract class FormFieldValidatorUtil {
+       /**
+        * Returns a form field validator to ensure that the value of the form field
+        * is a dot-separated string.
+        * 
+        * @param       string          $lalnguageItemPrefix            language item prefix used for error language items `{$languageItemPrefix}.error.{errorType}`
+        * @param       int             $minimumSegmentCount            minimum number of dot-separated segments, or `-1` if there is no minimum
+        * @param       int             $maximumSegmentCount            maximum number of dot-separated segments, or `-1` if there is no minimum
+        * @param       string          $segmentRegularExpression       regular expression used to validate each segment
+        * @return      FormFieldValidator
+        */
+       public static function getDotSeparatedStringValidator($languageItemPrefix, $minimumSegmentCount = 3, $maximumSegmentCount = -1, $segmentRegularExpression = '^[A-z0-9\-\_]+$') {
+               $regex = Regex::compile($segmentRegularExpression);
+               if (!$regex->isValid()) {
+                       throw new \InvalidArgumentException("Invalid regular expression '{$segmentRegularExpression}' given.");
+               }
+               
+               return new FormFieldValidator('format', function(TextFormField $formField) use ($languageItemPrefix, $minimumSegmentCount, $maximumSegmentCount, $regex) {
+                       if ($formField->getValue()) {
+                               $segments = explode('.', $formField->getValue());
+                               if ($minimumSegmentCount !== -1 && count($segments) < $minimumSegmentCount) {
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'tooFewSegments',
+                                                       $languageItemPrefix . '.error.tooFewSegments',
+                                                       ['segmentCount' => count($segments)]
+                                               )
+                                       );
+                               }
+                               else if ($maximumSegmentCount !== -1 && count($segments) > $maximumSegmentCount) {
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'tooManySegments',
+                                                       $languageItemPrefix . '.error.tooManySegments',
+                                                       ['segmentCount' => count($segments)]
+                                               )
+                                       );
+                               }
+                               else {
+                                       $invalidSegments = [];
+                                       foreach ($segments as $key => $segment) {
+                                               if (!$regex->match($segment)) {
+                                                       $invalidSegments[$key] = $segment;
+                                               }
+                                       }
+                                       
+                                       if (!empty($invalidSegments)) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'invalidSegments',
+                                                               $languageItemPrefix . '.error.invalidSegments',
+                                                               ['invalidSegments' => $invalidSegments]
+                                                       )
+                                               );
+                                       }
+                               }
+                       }
+               });
+       }
+       
        /**
         * Returns a form field validator to check the form field value against
         * the given regular expression.
index 9fb1805df38147833103c5faf29843987fa7be3b..cedc55ebac29b8ab8542cf7a7eaddccdc2bdb2ab 100644 (file)
@@ -1,12 +1,32 @@
 <?php
 namespace wcf\system\package\plugin;
+use wcf\data\IEditableCachedObject;
+use wcf\data\language\category\LanguageCategory;
+use wcf\data\language\category\LanguageCategoryAction;
+use wcf\data\language\item\LanguageItemEditor;
+use wcf\data\language\item\LanguageItemList;
 use wcf\data\language\Language;
 use wcf\data\language\LanguageEditor;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TMultiXmlGuiPackageInstallationPlugin;
 use wcf\system\exception\SystemException;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
+use wcf\system\form\builder\field\MultilineTextFormField;
+use wcf\system\form\builder\field\RadioButtonFormField;
+use wcf\system\form\builder\field\SingleSelectionFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\language\LanguageFactory;
 use wcf\system\package\PackageArchive;
 use wcf\system\WCF;
+use wcf\util\StringUtil;
 use wcf\util\XML;
 
 /**
@@ -17,12 +37,30 @@ use wcf\util\XML;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Package\Plugin
  */
-class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TMultiXmlGuiPackageInstallationPlugin;
+       
+       /**
+        * @inheritDoc
+        */
+       public $className = LanguageItemEditor::class;
+       
+       /**
+        * newly created language categories when saving language item via GUI
+        * @var LanguageCategory[]
+        */
+       public $newLanguageCategories = [];
+       
        /**
         * @inheritDoc
         */
        public $tableName = 'language_item';
        
+       /**
+        * @inheritDoc
+        */
+       public $tagName = 'item';
+       
        /**
         * @inheritDoc
         */
@@ -237,17 +275,23 @@ class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPl
        /**
         * @inheritDoc
         */
-       protected function handleDelete(array $items) { }
+       protected function handleDelete(array $items) {
+               // does nothing
+       }
        
        /**
         * @inheritDoc
         */
-       protected function prepareImport(array $data) { }
+       protected function prepareImport(array $data) {
+               // does nothing
+       }
        
        /**
         * @inheritDoc
         */
-       protected function findExistingItem(array $data) { }
+       protected function findExistingItem(array $data) {
+               wcfDebug($data);
+       }
        
        /**
         * @see \wcf\system\package\plugin\IPackageInstallationPlugin::getDefaultFilename()
@@ -265,8 +309,501 @@ class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPl
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               /** @var FormContainer $dataContainer */
+               $dataContainer = $form->getNodeById('data');
+               
+               // add fields
+               $dataContainer->appendChildren([
+                       RadioButtonFormField::create('languageCategoryIDMode')
+                               ->label('wcf.acp.language.item.languageCategoryID.mode')
+                               ->options([
+                                       'automatic' => 'wcf.acp.language.item.languageCategoryID.mode.automatic',
+                                       'selection' => 'wcf.acp.language.item.languageCategoryID.mode.selection',
+                                       'new' => 'wcf.acp.language.item.languageCategoryID.mode.new'
+                               ])
+                               ->value('automatic'),
+                       
+                       SingleSelectionFormField::create('languageCategoryID')
+                               ->label('wcf.acp.language.item.languageCategoryID')
+                               ->description('wcf.acp.language.item.languageCategoryID.description')
+                               ->options(function() {
+                                       $categories = [];
+                                       
+                                       foreach (LanguageFactory::getInstance()->getCategories() as $languageCategory) {
+                                               $categories[$languageCategory->languageCategoryID] = $languageCategory->getTitle();
+                                       }
+                                       
+                                       asort($categories);
+                                       
+                                       return $categories;
+                               }, false, false)
+                               ->filterable(),
+                       
+                       TextFormField::create('languageCategory')
+                               ->label('wcf.acp.language.item.languageCategoryID')
+                               ->description('wcf.acp.language.item.languageCategory.description')
+                               ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
+                                       'wcf.acp.language.item.languageItem',
+                                       2,
+                                       3
+                               ))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       if (LanguageFactory::getInstance()->getCategory($formField->getSaveValue()) !== null) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'notUnique',
+                                                               'wcf.acp.language.item.languageCategory.error.notUnique'
+                                                       )
+                                               );
+                                       }
+                               })),
+                       
+                       TextFormField::create('languageItem')
+                               ->label('wcf.acp.language.item.languageItem')
+                               ->description('wcf.acp.language.item.languageItem.description')
+                               ->required()
+                               ->maximumLength(191)
+                               ->addValidator(FormFieldValidatorUtil::getRegularExpressionValidator(
+                                       '^[A-z0-9-_]+(\.[A-z0-9-_]+){2,}$',
+                                       'wcf.acp.language.item.languageItem'
+                               ))
+                               ->addValidator(new FormFieldValidator('languageCategory', function(TextFormField $formField) {
+                                       /** @var RadioButtonFormField $languageCategoryIDMode */
+                                       $languageCategoryIDMode = $formField->getDocument()->getNodeById('languageCategoryIDMode');
+                                       
+                                       switch ($languageCategoryIDMode->getSaveValue()) {
+                                               case 'automatic':
+                                                       $languageItemPieces = explode('.', $formField->getSaveValue());
+                                                       
+                                                       $category = LanguageFactory::getInstance()->getCategory(
+                                                               $languageItemPieces[0] . '.' . $languageItemPieces[1] . '.' . $languageItemPieces[2]
+                                                       );
+                                                       if ($category === null) {
+                                                               $category = LanguageFactory::getInstance()->getCategory(
+                                                                       $languageItemPieces[0] . '.' . $languageItemPieces[1]
+                                                               );
+                                                       }
+                                                       
+                                                       if ($category === null) {
+                                                               $languageCategoryIDMode->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'automatic',
+                                                                               'wcf.acp.language.item.languageCategoryID.mode.error.automaticImpossible'
+                                                                       )
+                                                               );
+                                                       }
+                                                       
+                                                       break;
+                                               
+                                               case 'selection':
+                                                       /** @var SingleSelectionFormField $languageCategoryID */
+                                                       $languageCategoryID = $formField->getDocument()->getNodeById('languageCategoryID');
+                                                       
+                                                       $languageCategory = LanguageFactory::getInstance()->getCategoryByID($languageCategoryID->getSaveValue());
+                                                       
+                                                       if (strpos($formField->getSaveValue(), $languageCategory->languageCategory . '.') !== 0) {
+                                                               $formField->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'prefixMismatch',
+                                                                               'wcf.acp.language.item.languageItem.error.prefixMismatch'
+                                                                       )
+                                                               );
+                                                       }
+                                                       
+                                                       break;
+                                                       
+                                               case 'new':
+                                                       /** @var TextFormField $languageCategory */
+                                                       $languageCategory = $formField->getDocument()->getNodeById('languageCategory');
+                                                       
+                                                       if (strpos($formField->getSaveValue(), $languageCategory->getSaveValue() . '.') !== 0) {
+                                                               $formField->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'prefixMismatch',
+                                                                               'wcf.acp.language.item.languageItem.error.prefixMismatch'
+                                                                       )
+                                                               );
+                                                       }
+                                                       
+                                                       break;
+                                                       
+                                               default:
+                                                       throw new \LogicException("Unknown language category mode '{$languageCategoryIDMode->getSaveValue()}'.");
+                                       }
+                               }))
+                               ->addValidator(
+                                       new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                               if (
+                                                       $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
+                                                       $this->editedEntries[0]->getAttribute('name') !== $formField->getSaveValue()
+                                               ) {
+                                                       $languageItemList = new LanguageItemList();
+                                                       $languageItemList->getConditionBuilder()->add('languageItem = ?', [$formField->getSaveValue()]);
+                                                       
+                                                       if ($languageItemList->countObjects() > 0) {
+                                                               $formField->addValidationError(
+                                                                       new FormFieldValidationError(
+                                                                               'notUnique',
+                                                                               'wcf.acp.language.item.languageItem.error.notUnique'
+                                                                       )
+                                                               );
+                                                       }
+                                               }
+                                       }
+                               )),
+               ]);
+               
+               // add one field per language
+               foreach ($this->getProjectXmls() as $xml) {
+                       $languageCode = $xml->getDocument()->documentElement->getAttribute('languagecode');
+                       $languageName = $xml->getDocument()->documentElement->getAttribute('languagename');
+                       
+                       if ($dataContainer->getNodeById($languageCode) !== null) {
+                               throw new \LogicException("Duplicate language file with language code '{$languageCode}'.");
+                       }
+                       
+                       $dataContainer->appendChild(
+                               MultilineTextFormField::create($languageCode)
+                                       ->label($languageName)
+                       );
+               }
+               
+               // add dependencies
+               /** @var SingleSelectionFormField $languageCategoryIDMode */
+               $languageCategoryIDMode = $dataContainer->getNodeById('languageCategoryIDMode');
+               
+               $dataContainer->getNodeById('languageCategoryID')->addDependency(
+                       ValueFormFieldDependency::create('languageCategoryIDMode')
+                               ->field($languageCategoryIDMode)
+                               ->values(['selection'])
+               );
+               $dataContainer->getNodeById('languageCategory')->addDependency(
+                       ValueFormFieldDependency::create('languageCategoryIDMode')
+                               ->field($languageCategoryIDMode)
+                               ->values(['new'])
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element, $saveData = false) {
+               $data = [
+                       'languageID' => LanguageFactory::getInstance()->getLanguageByCode($element->ownerDocument->documentElement->getAttribute('languagecode'))->languageID,
+                       'languageItem' => $element->getAttribute('name'),
+                       'languageItemValue' => $element->nodeValue,
+                       'languageItemOriginIsSystem' => 1,
+                       'packageID' => $this->installation->getPackage()->packageID
+               ];
+               
+               if ($element->parentNode) {
+                       $languageCategory = $element->parentNode->getAttribute('name');
+                       
+                       if ($saveData) {
+                               if (isset($this->newLanguageCategories[$languageCategory])) {
+                                       $data['languageCategoryID'] = $this->newLanguageCategories[$languageCategory]->languageCategoryID;
+                               }
+                               else {
+                                       $languageCategoryObject = LanguageFactory::getInstance()->getCategory($languageCategory);
+                                       if ($languageCategoryObject !== null) {
+                                               $data['languageCategoryID'] = $languageCategoryObject->languageCategoryID;
+                                       }
+                                       else {
+                                               // if a new language category should be created, pass the name
+                                               // instead of the id
+                                               $data['languageCategory'] = $languageCategory;
+                                       }
+                               }
+                       }
+                       else {
+                               $data['languageCategory'] = $languageCategory;
+                       }
+               }
+               
+               if (!$saveData) {
+                       $data[$element->ownerDocument->documentElement->getAttribute('languagecode')] = $element->nodeValue;
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element) {
+               return $element->getAttribute('name');
+       }
+       
+       /**
+        * Returns a list of all pip entries of this pip.
+        * 
+        * @return      IDevtoolsPipEntryList
+        */
+       public function getEntryList() {
+               $entryList = new DevtoolsPipEntryList();
+               $this->setEntryListKeys($entryList);
+               
+               $entryData = [];
+               foreach ($this->getProjectXmls() as $xml) {
+                       $xpath = $xml->xpath();
+                       $languageCode = $xml->getDocument()->documentElement->getAttribute('languagecode');
+                       
+                       /** @var \DOMElement $element */
+                       foreach ($this->getImportElements($xpath) as $element) {
+                               $elementIdentifier = $this->getElementIdentifier($element);
+                               
+                               if (!isset($entryData[$elementIdentifier])) {
+                                       $entryData[$elementIdentifier] = [
+                                               'languageItem' => $element->getAttribute('name'),
+                                               'languageItemCategory' => $element->parentNode->getAttribute('name'),
+                                               $languageCode => 1
+                                       ];
+                               }
+                               else {
+                                       $entryData[$elementIdentifier][$languageCode] = 1;
+                               }
+                       }
+               }
+               
+               // re-sort language items as missing language items in first processed language
+               // can cause non-sorted entries even in each language file is sorted
+               uasort($entryData, function(array $item1, array $item2) {
+                       return $item1['languageItem'] <=> $item2['languageItem'];
+               });
+               
+               foreach ($entryData as $identifier => $data) {
+                       foreach ($entryList->getKeys() as $key => $label) {
+                               if (!isset($data[$key])) {
+                                       $data[$key] = 0;
+                               }
+                       }
+                       
+                       $entryList->addEntry($identifier, $data);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       protected function getImportElements(\DOMXPath $xpath) {
+               return $xpath->query('/ns:language/ns:category/ns:item');
+       }
+       
+       /**
+        * 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.
+        * 
+        * @param       string          $languageCode
+        * @return      string
+        */
+       protected function getEmptyXml($languageCode) {
+               $xsdFilename = $this->getXsdFilenlangame();
+               
+               $language = LanguageFactory::getInstance()->getLanguageByCode($languageCode);
+               if ($language === null) {
+                       throw new \InvalidArgumentException("Unknown language code '{$languageCode}'.");
+               }
+               
+               return <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<language 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" languagecode="{$language->languageCode}" languagename="{$language->languageName}" countrycode="{$language->countryCode}">
+</language>
+XML;
+       }
+       
+       /**
+        * Returns the xml objects for this pip.
+        *
+        * @return      XML[]
+        */
+       protected function getProjectXmls() {
+               $xmls = [];
+               
+               foreach ($this->installation->getProject()->getLanguageFiles() as $languageFile) {
+                       $xml = new XML();
+                       if (!file_exists($languageFile)) {
+                               $xml->loadXML($languageFile, $this->getEmptyXml(substr(basename($languageFile), 0, -4)));
+                       }
+                       else {
+                               $xml->load($languageFile);
+                       }
+                       
+                       // only consider installed languages
+                       $languageCode = $xml->getDocument()->documentElement->getAttribute('languagecode');
+                       if (LanguageFactory::getInstance()->getLanguageByCode($languageCode) !== null) {
+                               $xmls[] = $xml;
+                       }
+               }
+               
+               return $xmls;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function saveObject(\DOMElement $newElement, \DOMElement $oldElement = null) {
+               $newElementData = $this->getElementData($newElement, true);
+               
+               $existingRow = [];
+               if ($oldElement !== null) {
+                       $sql = "SELECT  *
+                               FROM    wcf" . WCF_N . "_language_item
+                               WHERE   languageItem = ?
+                                       AND languageID = ?";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute([
+                               $oldElement->getAttribute('name'),
+                               // use new element as old element has no access to parent element anymore
+                               LanguageFactory::getInstance()->getLanguageByCode(
+                                       $newElement->ownerDocument->documentElement->getAttribute('languagecode')
+                               )->languageID
+                       ]);
+                       
+                       $existingRow = $statement->fetchArray();
+               }
+               
+               if (!isset($newElementData['languageCategoryID']) && isset($newElementData['languageCategory'])) {
+                       /** @var LanguageCategory $languageCategory */
+                       $languageCategory = (new LanguageCategoryAction([], 'create', [
+                               'data' => [
+                                       'languageCategory' => $newElementData['languageCategory']
+                               ]
+                       ]))->executeAction()['returnValues'];
+                       
+                       $this->newLanguageCategories[$languageCategory->languageCategory] = $languageCategory;
+                       
+                       $newElementData['languageCategoryID'] = $languageCategory->languageCategoryID;
+                       unset($newElementData['languageCategory']);
+                       
+                       LanguageFactory::getInstance()->clearCache();
+               }
+               
+               $this->import($existingRow, $newElementData);
+               
+               $this->postImport();
+               
+               if (is_subclass_of($this->className, IEditableCachedObject::class)) {
+                       call_user_func([$this->className, 'resetCache']);
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
+               $keys = [
+                       'languageItem' => 'wcf.acp.language.item.languageItem',
+                       'languageItemCategory' => 'wcf.acp.language.item.languageCategoryID'
+               ];
+               
+               foreach ($this->getProjectXmls() as $xml) {
+                       $keys[$xml->getDocument()->documentElement->getAttribute('languagecode')] = $xml->getDocument()->documentElement->getAttribute('languagecode');
+               }
+               
+               $entryList->setKeys($keys);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $compareFunction = static::getSortFunction([
+                       [
+                               'isAttribute' => 1,
+                               'name' => 'name'
+                       ]
+               ]);
+               
+               $this->sortChildNodes($document->childNodes, $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('category'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form) {
+               $data = $form->getData()['data'];
+               
+               $languageCode = $document->documentElement->getAttribute('languagecode');
+               $languageItemValue = $data[$languageCode];
+               
+               $languageItem = $document->createElement($this->tagName);
+               $languageItem->setAttribute('name', $data['languageItem']);
+               $languageItem->appendChild($document->createCDATASection(StringUtil::escapeCDATA($languageItemValue)));
+               
+               // language category
+               $languageCategoryName = null;
+               switch ($data['languageCategoryIDMode']) {
+                       case 'automatic':
+                               $languageItemPieces = explode('.', $data['languageItem']);
+                               
+                               $category = LanguageFactory::getInstance()->getCategory(
+                                       $languageItemPieces[0] . '.' . $languageItemPieces[1] . '.' . $languageItemPieces[2]
+                               );
+                               if ($category === null) {
+                                       $category = LanguageFactory::getInstance()->getCategory(
+                                               $languageItemPieces[0] . '.' . $languageItemPieces[1]
+                                       );
+                               }
+                               
+                               if ($category === null) {
+                                       throw new \UnexpectedValueException("Cannot determine language item category for language item '{$data['languageItem']}'.");
+                               }
+                               
+                               $languageCategoryName = $category->languageCategory;
+                               
+                               break;
+                               
+                       case 'new':
+                               $languageCategoryName = $data['languageCategory'];
+                               
+                               break;
+                       
+                       case 'selection':
+                               $languageCategoryName = LanguageFactory::getInstance()->getCategoryByID($data['languageCategoryID'])->languageCategory;
+                               
+                               break;
+                       
+                       default:
+                               throw new \LogicException("Unknown language category mode '{$data['languageCategoryIDMode']}'.");
+               }
+               
+               /** @var \DOMElement $languageCategory */
+               foreach ($document->documentElement as $languageCategory) {
+                       if ($languageCategory->getAttribute('name') === $languageCategoryName) {
+                               $languageCategory->appendChild($languageItem);
+                               break;
+                       }
+               }
+               
+               if ($languageItem->parentNode === null) {
+                       $languageCategory = $document->createElement('category');
+                       $languageCategory->setAttribute('name', $languageCategoryName);
+                       $languageCategory->appendChild($languageItem);
+                       
+                       $document->documentElement->appendChild($languageCategory);
+               }
+               
+               return $languageItem;
+       }
 }
index b63829665ba219171b840ed4fed16c964395c89f..d22b19941fc969e578953057f500e531a16cbe2a 100644 (file)
@@ -217,8 +217,12 @@ class XML {
                $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]);
+               $writer->beginDocument(
+                       $this->document->documentElement->nodeName,
+                       $schemaParts[0],
+                       $schemaParts[1],
+                       $this->getAttributes($this->document->documentElement)
+               );
                foreach ($this->document->documentElement->childNodes as $childNode) {
                        $this->writeElement($writer, $childNode, $cdata);
                }
@@ -271,4 +275,13 @@ class XML {
                
                return $attributes;
        }
+       
+       /**
+        * Returns the path to the xml file.
+        * 
+        * @return      string
+        */
+       public function getPath() {
+               return $this->path;
+       }
 }
index 27eef112d64d4e9e9c3c9b2c3447b8c16b01b22f..11e2f9e309d71df66f296cd345c68b54f1649a5f 100644 (file)
                <item name="wcf.acp.language.item.languageCategoryID"><![CDATA[Language Category]]></item>
                <item name="wcf.acp.language.item.languageCategoryID.mode"><![CDATA[Language Category Mode]]></item>
                <item name="wcf.acp.language.item.languageCategoryID.mode.automatic"><![CDATA[Determined Automatically Based on Identifier]]></item>
+               <item name="wcf.acp.language.item.languageCategoryID.mode.new"><![CDATA[New Language Category]]></item>
                <item name="wcf.acp.language.item.languageCategoryID.mode.selection"><![CDATA[Manual Selection]]></item>
                <item name="wcf.acp.language.item.languageCategoryID.description"><![CDATA[The phrase will be added to the selected language category.]]></item>
                <item name="wcf.acp.language.item.languageItem"><![CDATA[Phrase Identifier]]></item>
                <item name="wcf.acp.language.item.languageItem.error.notUnique"><![CDATA[The entered identifier is already used by a different phrase.]]></item>
                <item name="wcf.acp.language.item.isCustomLanguageItem"><![CDATA[Manually added phrases]]></item>
                <item name="wcf.acp.language.item.delete.confirmMessage"><![CDATA[Do you really want to the delete the phrase? It will be deleted for all languages.]]></item>
+               <item name="wcf.acp.language.item.languageCategory"><![CDATA[Language Category Identifier]]></item>
+               <item name="wcf.acp.language.item.languageCategory.description"><![CDATA[The entered language category identifier is used to group phrases.]]></item>
+               <item name="wcf.acp.language.item.languageCategory.error.notUnique"><![CDATA[This language category identifier is already used by an existing language category.]]></item>
+               <item name="wcf.acp.language.item.languageItem.error.notUnique"><![CDATA[The entered identifier is already used by another phrase.]]></item>
        </category>
        
        <category name="wcf.acp.masterPassword">