Fixed time zone calculation issue
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / AbstractXMLPackageInstallationPlugin.class.php
1 <?php
2 namespace wcf\system\package\plugin;
3 use wcf\system\database\util\PreparedStatementConditionBuilder;
4 use wcf\system\exception\SystemException;
5 use wcf\system\package\PackageInstallationDispatcher;
6 use wcf\system\WCF;
7 use wcf\util\FileUtil;
8 use wcf\util\XML;
9
10 /**
11 * Abstract implementation of a package installation plugin using a XML file.
12 *
13 * @author Alexander Ebert
14 * @copyright 2001-2014 WoltLab GmbH
15 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
16 * @package com.woltlab.wcf
17 * @subpackage system.package.plugin
18 * @category Community Framework
19 */
20 abstract class AbstractXMLPackageInstallationPlugin extends AbstractPackageInstallationPlugin {
21 /**
22 * object editor class name
23 * @var string
24 */
25 public $className = '';
26
27 /**
28 * xml tag name, e.g. 'acpmenuitem'
29 * @var string
30 */
31 public $tagName = '';
32
33 /**
34 * @see \wcf\system\package\plugin\AbstractPackageInstallationPlugin::install()
35 */
36 public function __construct(PackageInstallationDispatcher $installation, $instruction = array()) {
37 parent::__construct($installation, $instruction);
38
39 // autoset 'tableName' property
40 if (empty($this->tableName) && !empty($this->className)) {
41 $this->tableName = call_user_func(array($this->className, 'getDatabaseTableAlias'));
42 }
43
44 // autoset 'tagName' property
45 if (empty($this->tagName) && !empty($this->tableName)) {
46 $this->tagName = str_replace('_', '', $this->tableName);
47 }
48 }
49
50 /**
51 * @see \wcf\system\package\plugin\IPackageInstallationPlugin::install()
52 */
53 public function install() {
54 parent::install();
55
56 // get xml
57 $xml = $this->getXML($this->instruction['value']);
58 $xpath = $xml->xpath();
59
60 // handle delete first
61 if ($this->installation->getAction() == 'update') {
62 $this->deleteItems($xpath);
63 }
64
65 // handle import
66 $this->importItems($xpath);
67
68 // execute cleanup
69 $this->cleanup();
70 }
71
72 /**
73 * @see \wcf\system\package\plugin\IPackageInstallationPlugin::uninstall()
74 */
75 public function uninstall() {
76 parent::uninstall();
77
78 // execute cleanup
79 $this->cleanup();
80 }
81
82 /**
83 * Deletes items.
84 *
85 * @param \DOMXPath $xpath
86 */
87 protected function deleteItems(\DOMXPath $xpath) {
88 $elements = $xpath->query('/ns:data/ns:delete/ns:'.$this->tagName);
89 $items = array();
90 foreach ($elements as $element) {
91 $data = array(
92 'attributes' => array(),
93 'elements' => array(),
94 'value' => $element->nodeValue
95 );
96
97 // get attributes
98 $attributes = $xpath->query('attribute::*', $element);
99 foreach ($attributes as $attribute) {
100 $data['attributes'][$attribute->name] = $attribute->value;
101 }
102
103 // get child elements
104 $childNodes = $xpath->query('child::*', $element);
105 foreach ($childNodes as $childNode) {
106 $data['elements'][$childNode->nodeName] = $childNode->nodeValue;
107 }
108
109 $items[] = $data;
110 }
111
112 // delete items
113 if (!empty($items)) {
114 $this->handleDelete($items);
115 }
116 }
117
118 /**
119 * Imports or updates items.
120 *
121 * @param \DOMXPath $xpath
122 */
123 protected function importItems(\DOMXPath $xpath) {
124 $elements = $xpath->query('/ns:data/ns:import/ns:'.$this->tagName);
125 foreach ($elements as $element) {
126 $data = array(
127 'attributes' => array(),
128 'elements' => array(),
129 'nodeValue' => ''
130 );
131
132 // fetch attributes
133 $attributes = $xpath->query('attribute::*', $element);
134 foreach ($attributes as $attribute) {
135 $data['attributes'][$attribute->name] = $attribute->value;
136 }
137
138 // fetch child elements
139 $items = $xpath->query('child::*', $element);
140 foreach ($items as $item) {
141 $this->getElement($xpath, $data['elements'], $item);
142 }
143
144 // include node value if item does not contain any child elements (eg. pip)
145 if (empty($data['elements'])) {
146 $data['nodeValue'] = $element->nodeValue;
147 }
148
149 // map element data to database fields
150 $data = $this->prepareImport($data);
151
152 // validate item data
153 $this->validateImport($data);
154
155 // try to find an existing item for updating
156 $sqlData = $this->findExistingItem($data);
157
158 // handle items which do not support updating (e.g. cronjobs)
159 if ($sqlData === null) $row = false;
160 else {
161 $statement = WCF::getDB()->prepareStatement($sqlData['sql']);
162 $statement->execute($sqlData['parameters']);
163 $row = $statement->fetchArray();
164 }
165
166 // ensure a valid parameter for import()
167 if ($row === false) $row = array();
168
169 // import items
170 $this->import($row, $data);
171 }
172
173 // fire after import
174 $this->postImport();
175 }
176
177 /**
178 * Sets element value from XPath.
179 *
180 * @param \DOMXPath $xpath
181 * @param array $elements
182 * @param \DOMElement $element
183 */
184 protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element) {
185 $elements[$element->tagName] = $element->nodeValue;
186 }
187
188 /**
189 * Inserts or updates new items.
190 *
191 * @param array $row
192 * @param array $data
193 * @return \wcf\data\IStorableObject
194 */
195 protected function import(array $row, array $data) {
196 if (empty($row)) {
197 // create new item
198 $this->prepareCreate($data);
199
200 return call_user_func(array($this->className, 'create'), $data);
201 }
202 else {
203 // update existing item
204 $baseClass = call_user_func(array($this->className, 'getBaseClass'));
205
206 $itemEditor = new $this->className(new $baseClass(null, $row));
207 $itemEditor->update($data);
208
209 return $itemEditor;
210 }
211 }
212
213 /**
214 * Executed after all items would have been imported, use this hook if you've
215 * overwritten import() to disable insert/update.
216 */
217 protected function postImport() { }
218
219 /**
220 * Deletes the given items.
221 *
222 * @param array $items
223 */
224 abstract protected function handleDelete(array $items);
225
226 /**
227 * Prepares import, use this to map xml tags and attributes
228 * to their corresponding database fields.
229 *
230 * @param array $data
231 * @return array
232 */
233 abstract protected function prepareImport(array $data);
234
235 /**
236 * Validates given item, e.g. checking for invalid values. If validation
237 * fails you should throw an exception.
238 *
239 * @param array $data
240 */
241 protected function validateImport(array $data) { }
242
243 /**
244 * Find an existing item for updating, should return sql query.
245 *
246 * @param array $data
247 * @return array
248 */
249 abstract protected function findExistingItem(array $data);
250
251 /**
252 * Append additional fields which are not to be updated if a corresponding
253 * item exists but are required for creation.
254 *
255 * Attention: $data is passed by reference
256 *
257 * @param array $data
258 */
259 protected function prepareCreate(array &$data) {
260 $data['packageID'] = $this->installation->getPackageID();
261 }
262
263 /**
264 * Triggered after executing all delete and/or import actions.
265 */
266 protected function cleanup() { }
267
268 /**
269 * Loads the xml file into a string and returns this string.
270 *
271 * @param string $filename
272 * @return XML $xml
273 */
274 protected function getXML($filename = '') {
275 if (empty($filename)) {
276 $filename = $this->instruction['value'];
277 }
278
279 // Search the xml-file in the package archive.
280 // Abort installation in case no file was found.
281 if (($fileIndex = $this->installation->getArchive()->getTar()->getIndexByFilename($filename)) === false) {
282 throw new SystemException("xml file '".$filename."' not found in '".$this->installation->getArchive()->getArchive()."'");
283 }
284
285 // Extract acpmenu file and parse XML
286 $xml = new XML();
287 $tmpFile = FileUtil::getTemporaryFilename('xml_');
288 try {
289 $this->installation->getArchive()->getTar()->extract($fileIndex, $tmpFile);
290 $xml->load($tmpFile);
291 }
292 catch (\Exception $e) { // bugfix to avoid file caching problems
293 try {
294 $this->installation->getArchive()->getTar()->extract($fileIndex, $tmpFile);
295 $xml->load($tmpFile);
296 }
297 catch (\Exception $e) {
298 $this->installation->getArchive()->getTar()->extract($fileIndex, $tmpFile);
299 $xml->load($tmpFile);
300 }
301 }
302
303 @unlink($tmpFile);
304 return $xml;
305 }
306
307 /**
308 * Returns the show order value.
309 *
310 * @param integer $showOrder
311 * @param string $parentName
312 * @param string $columnName
313 * @param string $tableNameExtension
314 * @return integer
315 */
316 protected function getShowOrder($showOrder, $parentName = null, $columnName = null, $tableNameExtension = '') {
317 if ($showOrder === null) {
318 // get greatest showOrder value
319 $conditions = new PreparedStatementConditionBuilder();
320 if ($columnName !== null) $conditions->add($columnName." = ?", array($parentName));
321
322 $sql = "SELECT MAX(showOrder) AS showOrder
323 FROM ".$this->application.WCF_N."_".$this->tableName.$tableNameExtension."
324 ".$conditions;
325 $statement = WCF::getDB()->prepareStatement($sql);
326 $statement->execute($conditions->getParameters());
327 $maxShowOrder = $statement->fetchArray();
328 return (!$maxShowOrder) ? 1 : ($maxShowOrder['showOrder'] + 1);
329 }
330 else {
331 // increase all showOrder values which are >= $showOrder
332 $sql = "UPDATE ".$this->application.WCF_N."_".$this->tableName.$tableNameExtension."
333 SET showOrder = showOrder + 1
334 WHERE showOrder >= ?
335 ".($columnName !== null ? "AND ".$columnName." = ?" : "");
336 $statement = WCF::getDB()->prepareStatement($sql);
337
338 $data = array($showOrder);
339 if ($columnName !== null) $data[] = $parentName;
340
341 $statement->execute($data);
342
343 // return the wanted showOrder level
344 return $showOrder;
345 }
346 }
347 }