2 namespace wcf\system\package\plugin
;
3 use wcf\system\database\util\PreparedStatementConditionBuilder
;
4 use wcf\system\exception\SystemException
;
5 use wcf\system\language\LanguageFactory
;
6 use wcf\system\package\PackageArchive
;
7 use wcf\system\package\PackageInstallationDispatcher
;
13 * Abstract implementation of a package installation plugin using a XML file.
15 * @author Alexander Ebert
16 * @copyright 2001-2018 WoltLab GmbH
17 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
18 * @package WoltLabSuite\Core\System\Package\Plugin
20 abstract class AbstractXMLPackageInstallationPlugin
extends AbstractPackageInstallationPlugin
{
22 * object editor class name
25 public $className = '';
28 * xml tag name, e.g. 'acpmenuitem'
36 public function __construct(PackageInstallationDispatcher
$installation, $instruction = []) {
37 parent
::__construct($installation, $instruction);
39 // autoset 'tableName' property
40 if (empty($this->tableName
) && !empty($this->className
)) {
41 $this->tableName
= call_user_func([$this->className
, 'getDatabaseTableAlias']);
44 // autoset 'tagName' property
45 if (empty($this->tagName
) && !empty($this->tableName
)) {
46 $this->tagName
= str_replace('_', '', $this->tableName
);
53 public function install() {
57 $xml = $this->getXML($this->instruction
['value']);
58 $xpath = $xml->xpath();
60 // handle delete first
61 if ($this->installation
->getAction() == 'update') {
62 $this->deleteItems($xpath);
66 $this->importItems($xpath);
75 public function uninstall() {
85 * @param \DOMXPath $xpath
87 protected function deleteItems(\DOMXPath
$xpath) {
88 $elements = $xpath->query('/ns:data/ns:delete/ns:'.$this->tagName
);
90 foreach ($elements as $element) {
94 'value' => $element->nodeValue
98 $attributes = $xpath->query('attribute::*', $element);
99 foreach ($attributes as $attribute) {
100 $data['attributes'][$attribute->name
] = $attribute->value
;
103 // get child elements
104 $childNodes = $xpath->query('child::*', $element);
105 foreach ($childNodes as $childNode) {
106 $data['elements'][$childNode->nodeName
] = $childNode->nodeValue
;
113 if (!empty($items)) {
114 $this->handleDelete($items);
119 * @param \DOMXPath $xpath
120 * @return \DOMNodeList
122 protected function getImportElements(\DOMXPath
$xpath) {
123 return $xpath->query('/ns:data/ns:import/ns:'.$this->tagName
);
127 * Imports or updates items.
129 * @param \DOMXPath $xpath
131 protected function importItems(\DOMXPath
$xpath) {
132 foreach ($this->getImportElements($xpath) as $element) {
140 $attributes = $xpath->query('attribute::*', $element);
141 foreach ($attributes as $attribute) {
142 $data['attributes'][$attribute->name
] = $attribute->value
;
145 // fetch child elements
146 $items = $xpath->query('child::*', $element);
147 foreach ($items as $item) {
148 $this->getElement($xpath, $data['elements'], $item);
151 // include node value if item does not contain any child elements (eg. pip)
152 if (empty($data['elements'])) {
153 $data['nodeValue'] = $element->nodeValue
;
156 // map element data to database fields
157 $data = $this->prepareImport($data);
159 // validate item data
160 $this->validateImport($data);
162 // try to find an existing item for updating
163 $sqlData = $this->findExistingItem($data);
165 // handle items which do not support updating (e.g. cronjobs)
166 if ($sqlData === null) $row = false;
168 $statement = WCF
::getDB()->prepareStatement($sqlData['sql']);
169 $statement->execute($sqlData['parameters']);
170 $row = $statement->fetchArray();
173 // ensure a valid parameter for import()
174 if ($row === false) $row = [];
177 $this->import($row, $data);
185 * Sets element value from XPath.
187 * @param \DOMXPath $xpath
188 * @param array $elements
189 * @param \DOMElement $element
191 protected function getElement(\DOMXPath
$xpath, array &$elements, \DOMElement
$element) {
192 $elements[$element->tagName
] = $element->nodeValue
;
196 * Returns i18n values by validating each value against the list of installed
197 * languages, optionally returning only the best matching value.
199 * @param string[] $values list of values by language code
200 * @param boolean $singleValueOnly true to return only the best matching value
201 * @return string[]|string matching i18n values controller by `$singleValueOnly`
204 protected function getI18nValues(array $values, $singleValueOnly = false) {
205 if (empty($values)) {
206 return $singleValueOnly ?
'' : [];
209 // check for a value with an empty language code and treat it as 'en' unless 'en' exists
210 if (isset($values[''])) {
211 if (!isset($values['en'])) {
212 $values['en'] = $values[''];
218 $matchingValues = [];
219 foreach ($values as $languageCode => $value) {
220 if (LanguageFactory
::getInstance()->getLanguageByCode($languageCode) !== null) {
221 $matchingValues[$languageCode] = $value;
225 // no matching value found
226 if (empty($matchingValues)) {
227 if (isset($values['en'])) {
228 // safest route: pick English
229 $matchingValues['en'] = $values['en'];
231 else if (isset($values[''])) {
232 // fallback: use the value w/o a language code
233 $matchingValues[''] = $values[''];
236 // failsafe: just use the first found value in whatever language
237 $matchingValues = array_splice($values, 0, 1);
241 if ($singleValueOnly) {
242 if (isset($matchingValues[LanguageFactory
::getInstance()->getDefaultLanguage()->languageCode
])) {
243 return $matchingValues[LanguageFactory
::getInstance()->getDefaultLanguage()->languageCode
];
246 return array_shift($matchingValues);
249 return $matchingValues;
253 * Inserts or updates new items.
257 * @return \wcf\data\IStorableObject
259 protected function import(array $row, array $data) {
262 $this->prepareCreate($data);
264 return call_user_func([$this->className
, 'create'], $data);
267 // update existing item
268 $baseClass = call_user_func([$this->className
, 'getBaseClass']);
270 /** @var \wcf\data\DatabaseObjectEditor $itemEditor */
271 $itemEditor = new $this->className(new $baseClass(null, $row));
272 $itemEditor->update($data);
279 * Executed after all items would have been imported, use this hook if you've
280 * overwritten import() to disable insert/update.
282 protected function postImport() { }
285 * Deletes the given items.
287 * @param array $items
289 abstract protected function handleDelete(array $items);
292 * Prepares import, use this to map xml tags and attributes
293 * to their corresponding database fields.
298 abstract protected function prepareImport(array $data);
301 * Validates given item, e.g. checking for invalid values. If validation
302 * fails you should throw an exception.
306 protected function validateImport(array $data) { }
309 * Returns an array with a sql query and its parameters to find an existing item for updating
310 * or `null` if updates are not supported.
315 abstract protected function findExistingItem(array $data);
318 * Append additional fields which are not to be updated if a corresponding
319 * item exists but are required for creation.
321 * Attention: $data is passed by reference
325 protected function prepareCreate(array &$data) {
326 $data['packageID'] = $this->installation
->getPackageID();
330 * Triggered after executing all delete and/or import actions.
332 protected function cleanup() { }
335 * Loads the xml file into a string and returns this string.
337 * @param string $filename
339 * @throws SystemException
341 protected function getXML($filename = '') {
342 if (empty($filename)) {
343 $filename = $this->instruction
['value'];
346 // Search the xml-file in the package archive.
347 // Abort installation in case no file was found.
348 if (($fileIndex = $this->installation
->getArchive()->getTar()->getIndexByFilename($filename)) === false) {
349 throw new SystemException("xml file '".$filename."' not found in '".$this->installation
->getArchive()->getArchive()."'");
352 // Extract acpmenu file and parse XML
354 $tmpFile = FileUtil
::getTemporaryFilename('xml_');
356 $this->installation
->getArchive()->getTar()->extract($fileIndex, $tmpFile);
357 $xml->load($tmpFile);
359 catch (\Exception
$e) { // bugfix to avoid file caching problems
361 $this->installation
->getArchive()->getTar()->extract($fileIndex, $tmpFile);
362 $xml->load($tmpFile);
364 catch (\Exception
$e) {
365 $this->installation
->getArchive()->getTar()->extract($fileIndex, $tmpFile);
366 $xml->load($tmpFile);
375 * Returns the show order value.
377 * @param integer $showOrder
378 * @param string $parentName
379 * @param string $columnName
380 * @param string $tableNameExtension
383 protected function getShowOrder($showOrder, $parentName = null, $columnName = null, $tableNameExtension = '') {
384 if ($showOrder === null) {
385 // get greatest showOrder value
386 $conditions = new PreparedStatementConditionBuilder();
387 if ($columnName !== null) $conditions->add($columnName." = ?", [$parentName]);
389 $sql = "SELECT MAX(showOrder) AS showOrder
390 FROM ".$this->application
.WCF_N
."_".$this->tableName
.$tableNameExtension."
392 $statement = WCF
::getDB()->prepareStatement($sql);
393 $statement->execute($conditions->getParameters());
394 $maxShowOrder = $statement->fetchArray();
395 return (!$maxShowOrder) ?
1 : ($maxShowOrder['showOrder'] +
1);
398 // increase all showOrder values which are >= $showOrder
399 $sql = "UPDATE ".$this->application
.WCF_N
."_".$this->tableName
.$tableNameExtension."
400 SET showOrder = showOrder + 1
402 ".($columnName !== null ?
"AND ".$columnName." = ?" : "");
403 $statement = WCF
::getDB()->prepareStatement($sql);
405 $data = [$showOrder];
406 if ($columnName !== null) $data[] = $parentName;
408 $statement->execute($data);
410 // return the wanted showOrder level
416 * @see \wcf\system\package\plugin\IPackageInstallationPlugin::getDefaultFilename()
419 public static function getDefaultFilename() {
420 $classParts = explode('\\', get_called_class());
422 return lcfirst(str_replace('PackageInstallationPlugin', '', array_pop($classParts))).'.xml';
428 public static function isValid(PackageArchive
$archive, $instruction) {
430 $defaultFilename = static::getDefaultFilename();
431 if ($defaultFilename) {
432 $instruction = $defaultFilename;
436 if (preg_match('~\.xml$~', $instruction)) {
437 // check if file actually exists
439 if ($archive->getTar()->getIndexByFilename($instruction) === false) {
443 catch (SystemException
$e) {