From 52da8ead3e7f556ca98e156818621ef4c48b64eb Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Wed, 3 Oct 2018 16:28:54 +0200 Subject: [PATCH] Add GUI support for language package installation plugin See #2545 --- .../project/DevtoolsProject.class.php | 13 + .../system/devtools/pip/DevtoolsPip.class.php | 2 +- ...iXmlGuiPackageInstallationPlugin.class.php | 188 ++++++ ...TXmlGuiPackageInstallationPlugin.class.php | 2 +- .../FormFieldValidatorUtil.class.php | 60 ++ ...anguagePackageInstallationPlugin.class.php | 547 +++++++++++++++++- wcfsetup/install/files/lib/util/XML.class.php | 17 +- wcfsetup/install/lang/en.xml | 5 + 8 files changed, 825 insertions(+), 9 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/devtools/pip/TMultiXmlGuiPackageInstallationPlugin.class.php diff --git a/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php b/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php index 4fd36e8ce0..6142352e96 100644 --- a/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php +++ b/wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php @@ -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 diff --git a/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php b/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php index 874a7b57df..1c762ecb50 100644 --- a/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php +++ b/wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php @@ -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 index 0000000000..48292293c4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/devtools/pip/TMultiXmlGuiPackageInstallationPlugin.class.php @@ -0,0 +1,188 @@ + + * @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); + } +} diff --git a/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php index fb7c8e8e29..ffabd32be2 100644 --- a/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php @@ -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; diff --git a/wcfsetup/install/files/lib/system/form/builder/field/validation/FormFieldValidatorUtil.class.php b/wcfsetup/install/files/lib/system/form/builder/field/validation/FormFieldValidatorUtil.class.php index bc26b7f7cd..d5bfa5bea4 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/validation/FormFieldValidatorUtil.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/validation/FormFieldValidatorUtil.class.php @@ -1,6 +1,7 @@ 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. diff --git a/wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php index 9fb1805df3..cedc55ebac 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php @@ -1,12 +1,32 @@ * @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; + } + + /** + * 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; + } } diff --git a/wcfsetup/install/files/lib/util/XML.class.php b/wcfsetup/install/files/lib/util/XML.class.php index b63829665b..d22b19941f 100644 --- a/wcfsetup/install/files/lib/util/XML.class.php +++ b/wcfsetup/install/files/lib/util/XML.class.php @@ -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; + } } diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 27eef112d6..11e2f9e309 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -795,6 +795,7 @@ + @@ -805,6 +806,10 @@ + + + + -- 2.20.1