1 | <?php |
||
2 | namespace phpbu\App\Configuration\Loader; |
||
3 | |||
4 | use DOMElement; |
||
5 | use DOMXPath; |
||
6 | use phpbu\App\Configuration; |
||
7 | use phpbu\App\Configuration\Loader; |
||
8 | use phpbu\App\Exception; |
||
9 | use phpbu\App\Util\Str; |
||
10 | |||
11 | /** |
||
12 | * Loader for a phpbu XML configuration file. |
||
13 | * |
||
14 | * Example XML configuration file: |
||
15 | * <code> |
||
16 | * <?xml version="1.0" encoding="UTF-8" ?> |
||
17 | * <phpbu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||
18 | * xsi:noNamespaceSchemaLocation="http://schema.phpbu.de/1.1/phpbu.xsd" |
||
19 | * bootstrap="backup/bootstrap.php" |
||
20 | * verbose="true"> |
||
21 | * |
||
22 | * <logging> |
||
23 | * <log type="json" target="/tmp/logfile.json" /> |
||
24 | * </logging> |
||
25 | * |
||
26 | * <backups> |
||
27 | * <backup> |
||
28 | * <source type="mysql"> |
||
29 | * <option name="databases" value="dbname" /> |
||
30 | * <option name="tables" value="" /> |
||
31 | * <option name="ignoreTables" value="" /> |
||
32 | * <option name="structureOnly" value="dbname.table1,dbname.table2" /> |
||
33 | * </source> |
||
34 | * |
||
35 | * <target dirname="/tmp/backup" filename="mysqldump-%Y%m%d-%H%i.sql" compress="bzip2" /> |
||
36 | * |
||
37 | * <check type="sizemin" value="10MB" /> |
||
38 | * |
||
39 | * <crypt type="mcrypt"> |
||
40 | * <option name="algorithm" value="blowfish"/> |
||
41 | * <option name="key" value="myKey"/> |
||
42 | * </crypt> |
||
43 | * |
||
44 | * <sync type="sftp" skipOnFailure="true"> |
||
45 | * <option name="host" value="example.com" /> |
||
46 | * <option name="user" value="user.name" /> |
||
47 | * <option name="password" value="topsecret" /> |
||
48 | * <option name="path" value="backup" /> |
||
49 | * </sync> |
||
50 | * |
||
51 | * <cleanup type="Outdated" skipOnFailure="true"> |
||
52 | * <option name="older" value="2W" /> |
||
53 | * </cleanup> |
||
54 | * </backup> |
||
55 | * </backups> |
||
56 | * </phpbu> |
||
57 | * </code> |
||
58 | * |
||
59 | * @package phpbu |
||
60 | * @subpackage App |
||
61 | * @author Sebastian Feldmann <[email protected]> |
||
62 | * @copyright Sebastian Feldmann <[email protected]> |
||
63 | * @license https://opensource.org/licenses/MIT The MIT License (MIT) |
||
64 | * @link http://phpbu.de/ |
||
65 | * @since Class available since Release 2.0.0 |
||
66 | */ |
||
67 | class Xml extends File implements Loader |
||
68 | { |
||
69 | /** |
||
70 | * Config file DOMDocument |
||
71 | * |
||
72 | * @var \DOMDocument |
||
73 | */ |
||
74 | private $document; |
||
75 | |||
76 | /** |
||
77 | * Xpath to navigate the config DOM. |
||
78 | * |
||
79 | * @var \DOMXPath |
||
80 | */ |
||
81 | private $xpath; |
||
82 | |||
83 | /** |
||
84 | * Xml constructor. |
||
85 | * |
||
86 | * @param string $file |
||
87 | * @param \phpbu\App\Configuration\Bootstrapper $bootstrapper |
||
88 | * @throws \phpbu\App\Exception |
||
89 | */ |
||
90 | 19 | public function __construct(string $file, Configuration\Bootstrapper $bootstrapper) |
|
91 | { |
||
92 | 19 | parent::__construct($file, $bootstrapper); |
|
93 | 19 | $this->document = $this->loadXmlFile($file); |
|
94 | 17 | $this->xpath = new DOMXPath($this->document); |
|
95 | |||
96 | 17 | $this->validateConfigurationAgainstSchema(); |
|
97 | 17 | } |
|
98 | |||
99 | /** |
||
100 | * Return list of adapter configs. |
||
101 | * |
||
102 | * @return array |
||
103 | * @throws \phpbu\App\Exception |
||
104 | */ |
||
105 | 17 | protected function getAdapterConfigs() |
|
106 | { |
||
107 | 17 | $adapters = []; |
|
108 | /** @var \DOMElement $adapterNode */ |
||
109 | 17 | foreach ($this->xpath->query('adapters/adapter') as $adapterNode) { |
|
110 | 5 | $type = $adapterNode->getAttribute('type'); |
|
111 | 5 | $name = $adapterNode->getAttribute('name'); |
|
112 | 5 | $options = $this->getOptions($adapterNode); |
|
113 | 5 | if (!$type) { |
|
114 | 1 | throw new Exception('invalid adapter configuration: attribute type missing'); |
|
115 | } |
||
116 | 4 | if (!$name) { |
|
117 | 1 | throw new Exception('invalid adapter configuration: attribute name missing'); |
|
118 | } |
||
119 | 3 | $adapters[] = new Configuration\Adapter($type, $name, $options); |
|
120 | } |
||
121 | 15 | return $adapters; |
|
122 | } |
||
123 | |||
124 | /** |
||
125 | * Set the phpbu application settings. |
||
126 | * |
||
127 | * @param \phpbu\App\Configuration $configuration |
||
128 | */ |
||
129 | 17 | public function setAppSettings(Configuration $configuration) |
|
130 | { |
||
131 | 17 | $root = $this->document->documentElement; |
|
132 | |||
133 | 17 | if ($root->hasAttribute('bootstrap')) { |
|
134 | 17 | $configuration->setBootstrap($this->toAbsolutePath($root->getAttribute('bootstrap'))); |
|
135 | } |
||
136 | 17 | if ($root->hasAttribute('verbose')) { |
|
137 | 16 | $configuration->setVerbose(Str::toBoolean($root->getAttribute('verbose'), false)); |
|
138 | } |
||
139 | 17 | if ($root->hasAttribute('colors')) { |
|
140 | 16 | $configuration->setColors(Str::toBoolean($root->getAttribute('colors'), false)); |
|
141 | } |
||
142 | 17 | } |
|
143 | |||
144 | /** |
||
145 | * Set the log configuration. |
||
146 | * |
||
147 | * @param \phpbu\App\Configuration $configuration |
||
148 | * @throws \phpbu\App\Exception |
||
149 | */ |
||
150 | 15 | public function setLoggers(Configuration $configuration) |
|
151 | { |
||
152 | /** @var \DOMElement $logNode */ |
||
153 | 15 | foreach ($this->xpath->query('logging/log') as $logNode) { |
|
154 | 13 | $type = $logNode->getAttribute('type'); |
|
155 | 13 | if (!$type) { |
|
156 | 1 | throw new Exception('invalid logger configuration: attribute type missing'); |
|
157 | } |
||
158 | 12 | $options = $this->getOptions($logNode); |
|
159 | 12 | if (isset($options['target'])) { |
|
160 | 1 | $options['target'] = $this->toAbsolutePath($options['target']); |
|
161 | } |
||
162 | // search for target attribute to convert to option |
||
163 | 12 | $target = $logNode->getAttribute('target'); |
|
164 | 12 | if (!empty($target)) { |
|
165 | 11 | $options['target'] = $this->getAdapterizedValue($this->toAbsolutePath($target)); |
|
166 | } |
||
167 | 12 | $configuration->addLogger(new Configuration\Logger($type, $options)); |
|
168 | } |
||
169 | 14 | } |
|
170 | |||
171 | /** |
||
172 | * Set the backup configurations. |
||
173 | * |
||
174 | * @param \phpbu\App\Configuration $configuration |
||
175 | * @throws \phpbu\App\Exception |
||
176 | */ |
||
177 | 14 | public function setBackups(Configuration $configuration) |
|
178 | { |
||
179 | 14 | foreach ($this->xpath->query('backups/backup') as $backupNode) { |
|
180 | 14 | $configuration->addBackup($this->getBackupConfig($backupNode)); |
|
181 | } |
||
182 | 7 | } |
|
183 | |||
184 | /** |
||
185 | * Get the config for a single backup node. |
||
186 | * |
||
187 | * @param \DOMElement $backupNode |
||
188 | * @throws \phpbu\App\Exception |
||
189 | * @return \phpbu\App\Configuration\Backup |
||
190 | */ |
||
191 | 14 | private function getBackupConfig(DOMElement $backupNode) |
|
192 | { |
||
193 | 14 | $stopOnFailure = Str::toBoolean($backupNode->getAttribute('stopOnFailure'), false); |
|
194 | 14 | $backupName = $this->getAdapterizedValue($backupNode->getAttribute('name')); |
|
195 | 14 | $backup = new Configuration\Backup($backupName, $stopOnFailure); |
|
196 | |||
197 | 14 | $backup->setSource($this->getSource($backupNode)); |
|
198 | 12 | $backup->setTarget($this->getTarget($backupNode)); |
|
199 | |||
200 | 11 | $this->setChecks($backup, $backupNode); |
|
201 | 11 | $this->setCrypt($backup, $backupNode); |
|
202 | 10 | $this->setSyncs($backup, $backupNode); |
|
203 | 8 | $this->setCleanup($backup, $backupNode); |
|
204 | |||
205 | 7 | return $backup; |
|
206 | } |
||
207 | |||
208 | /** |
||
209 | * Get source configuration. |
||
210 | * |
||
211 | * @param \DOMElement $node |
||
212 | * @return \phpbu\App\Configuration\Backup\Source |
||
213 | * @throws \phpbu\App\Exception |
||
214 | */ |
||
215 | 14 | protected function getSource(DOMElement $node) |
|
216 | { |
||
217 | 14 | $sources = $node->getElementsByTagName('source'); |
|
218 | 14 | if ($sources->length !== 1) { |
|
219 | 1 | throw new Exception('backup requires exactly one source config'); |
|
220 | } |
||
221 | /** @var DOMElement $sourceNode */ |
||
222 | 13 | $sourceNode = $sources->item(0); |
|
223 | 13 | $type = $sourceNode->getAttribute('type'); |
|
224 | 13 | if (!$type) { |
|
225 | 1 | throw new Exception('source requires type attribute'); |
|
226 | } |
||
227 | |||
228 | 12 | return new Configuration\Backup\Source($type, $this->getOptions($sourceNode)); |
|
229 | } |
||
230 | |||
231 | /** |
||
232 | * Get Target configuration. |
||
233 | * |
||
234 | * @param \DOMElement $node |
||
235 | * @return \phpbu\App\Configuration\Backup\Target |
||
236 | * @throws \phpbu\App\Exception |
||
237 | */ |
||
238 | 12 | protected function getTarget(DOMElement $node) |
|
239 | { |
||
240 | 12 | $targets = $node->getElementsByTagName('target'); |
|
241 | 12 | if ($targets->length !== 1) { |
|
242 | 1 | throw new Exception('backup requires exactly one target config'); |
|
243 | } |
||
244 | /** @var DOMElement $targetNode */ |
||
245 | 11 | $targetNode = $targets->item(0); |
|
246 | 11 | $compress = $targetNode->getAttribute('compress'); |
|
247 | 11 | $filename = $this->getAdapterizedValue($targetNode->getAttribute('filename')); |
|
248 | 11 | $dirname = $this->getAdapterizedValue($targetNode->getAttribute('dirname')); |
|
249 | |||
250 | 11 | if ($dirname) { |
|
251 | 11 | $dirname = $this->toAbsolutePath($dirname); |
|
252 | } |
||
253 | |||
254 | 11 | return new Configuration\Backup\Target($dirname, $filename, $compress); |
|
255 | } |
||
256 | |||
257 | /** |
||
258 | * Set backup checks. |
||
259 | * |
||
260 | * @param \phpbu\App\Configuration\Backup $backup |
||
261 | * @param \DOMElement $node |
||
262 | */ |
||
263 | 11 | protected function setChecks(Configuration\Backup $backup, DOMElement $node) |
|
264 | { |
||
265 | /** @var DOMElement $checkNode */ |
||
266 | 11 | foreach ($node->getElementsByTagName('check') as $checkNode) { |
|
267 | 10 | $type = $checkNode->getAttribute('type'); |
|
268 | 10 | $value = $checkNode->getAttribute('value'); |
|
269 | // skip invalid sanity checks |
||
270 | 10 | if (!$type || !$value) { |
|
271 | 1 | continue; |
|
272 | } |
||
273 | 9 | $backup->addCheck(new Configuration\Backup\Check($type, $value)); |
|
274 | } |
||
275 | 11 | } |
|
276 | |||
277 | /** |
||
278 | * Set the crypt configuration. |
||
279 | * |
||
280 | * @param \phpbu\App\Configuration\Backup $backup |
||
281 | * @param \DOMElement $node |
||
282 | * @throws \phpbu\App\Exception |
||
283 | */ |
||
284 | 11 | protected function setCrypt(Configuration\Backup $backup, DOMElement $node) |
|
285 | { |
||
286 | /** @var \DOMNodeList $cryptNodes */ |
||
287 | 11 | $cryptNodes = $node->getElementsByTagName('crypt'); |
|
288 | 11 | if ($cryptNodes->length > 0) { |
|
289 | /** @var \DOMElement $cryptNode */ |
||
290 | 8 | $cryptNode = $cryptNodes->item(0); |
|
291 | 8 | $type = $cryptNode->getAttribute('type'); |
|
292 | 8 | if (!$type) { |
|
293 | 1 | throw new Exception('invalid crypt configuration: attribute type missing'); |
|
294 | } |
||
295 | 7 | $skip = Str::toBoolean($cryptNode->getAttribute('skipOnFailure'), true); |
|
296 | 7 | $options = $this->getOptions($cryptNode); |
|
297 | 7 | $backup->setCrypt(new Configuration\Backup\Crypt($type, $skip, $options)); |
|
298 | } |
||
299 | 10 | } |
|
300 | |||
301 | /** |
||
302 | * Set backup sync configurations. |
||
303 | * |
||
304 | * @param \phpbu\App\Configuration\Backup $backup |
||
305 | * @param \DOMElement $node |
||
306 | * @throws \phpbu\App\Exception |
||
307 | */ |
||
308 | 10 | protected function setSyncs(Configuration\Backup $backup, DOMElement $node) |
|
309 | { |
||
310 | /** @var DOMElement $syncNode */ |
||
311 | 10 | foreach ($node->getElementsByTagName('sync') as $syncNode) { |
|
312 | 9 | $type = $syncNode->getAttribute('type'); |
|
313 | 9 | if (!$type) { |
|
314 | 1 | throw new Exception('invalid sync configuration: attribute type missing'); |
|
315 | } |
||
316 | 8 | $skip = Str::toBoolean($syncNode->getAttribute('skipOnFailure'), true); |
|
317 | 8 | $options = $this->getOptions($syncNode); |
|
318 | 7 | $backup->addSync(new Configuration\Backup\Sync($type, $skip, $options)); |
|
319 | } |
||
320 | 8 | } |
|
321 | |||
322 | /** |
||
323 | * Set the cleanup configuration. |
||
324 | * |
||
325 | * @param \phpbu\App\Configuration\Backup $backup |
||
326 | * @param \DOMElement $node |
||
327 | * @throws \phpbu\App\Exception |
||
328 | */ |
||
329 | 8 | protected function setCleanup(Configuration\Backup $backup, DOMElement $node) |
|
330 | { |
||
331 | /** @var \DOMNodeList $cleanupNodes */ |
||
332 | 8 | $cleanupNodes = $node->getElementsByTagName('cleanup'); |
|
333 | 8 | if ($cleanupNodes->length > 0) { |
|
334 | /** @var \DOMElement $cleanupNode */ |
||
335 | 7 | $cleanupNode = $cleanupNodes->item(0); |
|
336 | 7 | $type = $cleanupNode->getAttribute('type'); |
|
337 | 7 | if (!$type) { |
|
338 | 1 | throw new Exception('invalid cleanup configuration: attribute type missing'); |
|
339 | } |
||
340 | 6 | $skip = Str::toBoolean($cleanupNode->getAttribute('skipOnFailure'), true); |
|
341 | 6 | $options = $this->getOptions($cleanupNode); |
|
342 | 6 | $backup->setCleanup(new Configuration\Backup\Cleanup($type, $skip, $options)); |
|
343 | } |
||
344 | 7 | } |
|
345 | |||
346 | /** |
||
347 | * Extracts all option tags. |
||
348 | * |
||
349 | * @param \DOMElement $node |
||
350 | * @return array |
||
351 | * @throws \phpbu\App\Exception |
||
352 | */ |
||
353 | 15 | protected function getOptions(DOMElement $node) |
|
354 | { |
||
355 | 15 | $options = []; |
|
356 | /** @var \DOMElement $optionNode */ |
||
357 | 15 | foreach ($node->getElementsByTagName('option') as $optionNode) { |
|
358 | 15 | $name = $optionNode->getAttribute('name'); |
|
359 | 15 | $value = $this->getAdapterizedValue($optionNode->getAttribute('value')); |
|
360 | 15 | $options[$name] = $value; |
|
361 | } |
||
362 | 15 | return $options; |
|
363 | } |
||
364 | |||
365 | /** |
||
366 | * Load the XML-File. |
||
367 | * |
||
368 | * @param string $filename |
||
369 | * @throws \phpbu\App\Exception |
||
370 | * @return \DOMDocument |
||
371 | */ |
||
372 | 19 | private function loadXmlFile($filename) |
|
373 | { |
||
374 | 19 | $contents = $this->loadFile($filename); |
|
375 | 18 | $document = new \DOMDocument; |
|
376 | 18 | $message = ''; |
|
377 | 18 | $internal = libxml_use_internal_errors(true); |
|
378 | 18 | $reporting = error_reporting(0); |
|
379 | |||
380 | 18 | $document->documentURI = $filename; |
|
381 | 18 | $loaded = $document->loadXML($contents); |
|
382 | |||
383 | 18 | foreach (libxml_get_errors() as $error) { |
|
384 | 1 | $message .= "\n" . $error->message; |
|
385 | } |
||
386 | |||
387 | 18 | libxml_use_internal_errors($internal); |
|
388 | 18 | error_reporting($reporting); |
|
389 | |||
390 | 18 | if ($loaded === false || $message !== '') { |
|
391 | 1 | throw new Exception( |
|
392 | 1 | sprintf( |
|
393 | 1 | 'Error loading file "%s".%s', |
|
394 | $filename, |
||
395 | 1 | $message != '' ? "\n" . $message : '' |
|
396 | ) |
||
397 | ); |
||
398 | } |
||
399 | 17 | return $document; |
|
400 | } |
||
401 | |||
402 | /** |
||
403 | * Validate xml configuration against phpbu.xsd schema |
||
404 | * |
||
405 | * @return void |
||
406 | */ |
||
407 | 17 | private function validateConfigurationAgainstSchema() |
|
408 | { |
||
409 | 17 | $original = \libxml_use_internal_errors(true); |
|
410 | 17 | $xsdFilename = __DIR__ . '/../../../phpbu.xsd'; |
|
411 | 17 | if (\defined('__PHPBU_PHAR_ROOT__')) { |
|
412 | $xsdFilename = __PHPBU_PHAR_ROOT__ . '/phpbu.xsd'; |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
413 | } |
||
414 | 17 | $this->document->schemaValidate($xsdFilename); |
|
415 | 17 | foreach (\libxml_get_errors() as $error) { |
|
416 | 7 | if (!isset($this->errors[$error->line])) { |
|
417 | 7 | $this->errors[$error->line] = []; |
|
418 | } |
||
419 | 7 | $this->errors[$error->line][] = \trim($error->message); |
|
420 | } |
||
421 | 17 | \libxml_clear_errors(); |
|
422 | 17 | \libxml_use_internal_errors($original); |
|
423 | 17 | } |
|
424 | } |
||
425 |