Total Complexity | 52 |
Total Lines | 582 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like XmlExportService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use XmlExportService, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
61 | final class XmlExportService extends Service |
||
62 | { |
||
63 | /** |
||
64 | * @var ConfigData |
||
65 | */ |
||
66 | private $configData; |
||
67 | /** |
||
68 | * @var |
||
69 | */ |
||
70 | private $extensionChecker; |
||
71 | /** |
||
72 | * @var DOMDocument |
||
73 | */ |
||
74 | private $xml; |
||
75 | /** |
||
76 | * @var DOMElement |
||
77 | */ |
||
78 | private $root; |
||
79 | /** |
||
80 | * @var string |
||
81 | */ |
||
82 | private $exportPass; |
||
83 | /** |
||
84 | * @var bool |
||
85 | */ |
||
86 | private $encrypted = false; |
||
87 | /** |
||
88 | * @var string |
||
89 | */ |
||
90 | private $exportPath; |
||
91 | /** |
||
92 | * @var string |
||
93 | */ |
||
94 | private $exportFile; |
||
95 | |||
96 | /** |
||
97 | * Realiza la exportación de las cuentas a XML |
||
98 | * |
||
99 | * @param string $exportPath |
||
100 | * @param string $pass string La clave de exportación |
||
101 | * |
||
102 | * @throws ServiceException |
||
103 | * @throws FileException |
||
104 | */ |
||
105 | public function doExport(string $exportPath, string $pass = null) |
||
106 | { |
||
107 | set_time_limit(0); |
||
108 | |||
109 | if (!empty($pass)) { |
||
110 | $this->exportPass = $pass; |
||
111 | $this->encrypted = true; |
||
112 | } |
||
113 | |||
114 | $this->setExportPath($exportPath); |
||
115 | $this->exportFile = $this->generateExportFilename(); |
||
116 | $this->deleteOldExports(); |
||
117 | $this->makeXML(); |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * @param string $exportPath |
||
122 | * |
||
123 | * @throws ServiceException |
||
124 | */ |
||
125 | private function setExportPath(string $exportPath) |
||
126 | { |
||
127 | if (!is_dir($exportPath) |
||
128 | && @mkdir($exportPath, 0700, true) === false |
||
129 | ) { |
||
130 | throw new ServiceException(sprintf(__('Unable to create the directory (%s)'), $exportPath)); |
||
131 | } |
||
132 | |||
133 | $this->exportPath = $exportPath; |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * Genera el nombre del archivo usado para la exportación. |
||
138 | * |
||
139 | * @return string |
||
140 | * @throws FileException |
||
141 | */ |
||
142 | private function generateExportFilename(): string |
||
143 | { |
||
144 | // Generar hash unico para evitar descargas no permitidas |
||
145 | $hash = sha1(uniqid('sysPassExport', true)); |
||
146 | $this->configData->setExportHash($hash); |
||
147 | $this->config->saveConfig($this->configData); |
||
148 | |||
149 | return self::getExportFilename($this->exportPath, $hash); |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * @param string $path |
||
154 | * @param string $hash |
||
155 | * @param bool $compressed |
||
156 | * |
||
157 | * @return string |
||
158 | */ |
||
159 | public static function getExportFilename(string $path, string $hash, bool $compressed = false) |
||
160 | { |
||
161 | $file = $path . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME . '_export-' . $hash; |
||
162 | |||
163 | if ($compressed) { |
||
164 | return $file . ArchiveHandler::COMPRESS_EXTENSION; |
||
165 | } |
||
166 | |||
167 | return $file . '.xml'; |
||
168 | } |
||
169 | |||
170 | /** |
||
171 | * Eliminar los archivos de exportación anteriores |
||
172 | */ |
||
173 | private function deleteOldExports() |
||
174 | { |
||
175 | $path = $this->exportPath . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME; |
||
176 | |||
177 | array_map(function ($file) { |
||
178 | return @unlink($file); |
||
179 | }, array_merge(glob($path . '_export-*'), glob($path . '*.xml'))); |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * Crear el documento XML y guardarlo |
||
184 | * |
||
185 | * @throws ContainerExceptionInterface |
||
186 | * @throws NotFoundExceptionInterface |
||
187 | * @throws ServiceException |
||
188 | */ |
||
189 | private function makeXML() |
||
190 | { |
||
191 | try { |
||
192 | $this->createRoot(); |
||
193 | $this->createMeta(); |
||
194 | $this->createCategories(); |
||
195 | $this->createClients(); |
||
196 | $this->createTags(); |
||
197 | $this->createAccounts(); |
||
198 | $this->createHash(); |
||
199 | $this->writeXML(); |
||
200 | } catch (ServiceException $e) { |
||
201 | throw $e; |
||
202 | } catch (Exception $e) { |
||
203 | throw new ServiceException( |
||
204 | __u('Error while exporting'), |
||
205 | ServiceException::ERROR, |
||
206 | __u('Please check out the event log for more details'), |
||
207 | $e->getCode(), |
||
208 | $e |
||
209 | ); |
||
210 | } |
||
211 | } |
||
212 | |||
213 | /** |
||
214 | * Crear el nodo raíz |
||
215 | * |
||
216 | * @throws ServiceException |
||
217 | */ |
||
218 | private function createRoot() |
||
219 | { |
||
220 | try { |
||
221 | $this->xml = new DOMDocument('1.0', 'UTF-8'); |
||
222 | $this->root = $this->xml->appendChild($this->xml->createElement('Root')); |
||
223 | } catch (Exception $e) { |
||
224 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
225 | } |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * Crear el nodo con metainformación del archivo XML |
||
230 | * |
||
231 | * @throws ServiceException |
||
232 | */ |
||
233 | private function createMeta() |
||
234 | { |
||
235 | try { |
||
236 | $userData = $this->context->getUserData(); |
||
237 | |||
238 | $nodeMeta = $this->xml->createElement('Meta'); |
||
239 | $metaGenerator = $this->xml->createElement('Generator', 'sysPass'); |
||
240 | $metaVersion = $this->xml->createElement('Version', VersionUtil::getVersionStringNormalized()); |
||
241 | $metaTime = $this->xml->createElement('Time', time()); |
||
242 | $metaUser = $this->xml->createElement('User', $userData->getLogin()); |
||
243 | $metaUser->setAttribute('id', $userData->getId()); |
||
244 | $metaGroup = $this->xml->createElement('Group', $userData->getUserGroupName()); |
||
245 | $metaGroup->setAttribute('id', $userData->getUserGroupId()); |
||
246 | |||
247 | $nodeMeta->appendChild($metaGenerator); |
||
248 | $nodeMeta->appendChild($metaVersion); |
||
249 | $nodeMeta->appendChild($metaTime); |
||
250 | $nodeMeta->appendChild($metaUser); |
||
251 | $nodeMeta->appendChild($metaGroup); |
||
252 | |||
253 | $this->root->appendChild($nodeMeta); |
||
254 | } catch (Exception $e) { |
||
255 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
256 | } |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * Crear el nodo con los datos de las categorías |
||
261 | * |
||
262 | * @throws ContainerExceptionInterface |
||
263 | * @throws NotFoundExceptionInterface |
||
264 | * @throws ServiceException |
||
265 | */ |
||
266 | private function createCategories() |
||
267 | { |
||
268 | try { |
||
269 | $this->eventDispatcher->notifyEvent('run.export.process.category', |
||
270 | new Event($this, EventMessage::factory() |
||
271 | ->addDescription(__u('Exporting categories'))) |
||
272 | ); |
||
273 | |||
274 | $categoryService = $this->dic->get(CategoryService::class); |
||
275 | $categories = $categoryService->getAllBasic(); |
||
276 | |||
277 | // Crear el nodo de categorías |
||
278 | $nodeCategories = $this->xml->createElement('Categories'); |
||
279 | |||
280 | if (count($categories) === 0) { |
||
281 | $this->appendNode($nodeCategories); |
||
282 | |||
283 | return; |
||
284 | } |
||
285 | |||
286 | foreach ($categories as $category) { |
||
287 | /** @var $category CategoryData */ |
||
288 | $categoryName = $this->xml->createElement('name', $this->escapeChars($category->getName())); |
||
289 | $categoryDescription = $this->xml->createElement('description', $this->escapeChars($category->getDescription())); |
||
290 | |||
291 | // Crear el nodo de categoría |
||
292 | $nodeCategory = $this->xml->createElement('Category'); |
||
293 | $nodeCategory->setAttribute('id', $category->getId()); |
||
294 | $nodeCategory->appendChild($categoryName); |
||
295 | $nodeCategory->appendChild($categoryDescription); |
||
296 | |||
297 | // Añadir categoría al nodo de categorías |
||
298 | $nodeCategories->appendChild($nodeCategory); |
||
299 | } |
||
300 | |||
301 | $this->appendNode($nodeCategories); |
||
302 | } catch (Exception $e) { |
||
303 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
304 | } |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Añadir un nuevo nodo al árbol raíz |
||
309 | * |
||
310 | * @param DOMElement $node El nodo a añadir |
||
311 | * |
||
312 | * @throws ServiceException |
||
313 | */ |
||
314 | private function appendNode(DOMElement $node) |
||
315 | { |
||
316 | try { |
||
317 | // Si se utiliza clave de encriptación los datos se encriptan en un nuevo nodo: |
||
318 | // Encrypted -> Data |
||
319 | if ($this->encrypted === true) { |
||
320 | // Obtener el nodo en formato XML |
||
321 | $nodeXML = $this->xml->saveXML($node); |
||
322 | |||
323 | // Crear los datos encriptados con la información del nodo |
||
324 | $securedKey = Crypt::makeSecuredKey($this->exportPass); |
||
325 | $encrypted = Crypt::encrypt($nodeXML, $securedKey, $this->exportPass); |
||
326 | |||
327 | // Buscar si existe ya un nodo para el conjunto de datos encriptados |
||
328 | $encryptedNode = $this->root->getElementsByTagName('Encrypted')->item(0); |
||
329 | |||
330 | if (!$encryptedNode instanceof DOMElement) { |
||
331 | $encryptedNode = $this->xml->createElement('Encrypted'); |
||
332 | $encryptedNode->setAttribute('hash', Hash::hashKey($this->exportPass)); |
||
333 | } |
||
334 | |||
335 | // Crear el nodo hijo con los datos encriptados |
||
336 | $encryptedData = $this->xml->createElement('Data', base64_encode($encrypted)); |
||
337 | |||
338 | $encryptedDataKey = $this->xml->createAttribute('key'); |
||
339 | $encryptedDataKey->value = $securedKey; |
||
340 | |||
341 | // Añadir nodos de datos |
||
342 | $encryptedData->appendChild($encryptedDataKey); |
||
343 | $encryptedNode->appendChild($encryptedData); |
||
344 | |||
345 | // Añadir el nodo encriptado |
||
346 | $this->root->appendChild($encryptedNode); |
||
347 | } else { |
||
348 | $this->root->appendChild($node); |
||
349 | } |
||
350 | } catch (Exception $e) { |
||
351 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
352 | } |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * Escapar carácteres no válidos en XML |
||
357 | * |
||
358 | * @param $data string Los datos a escapar |
||
359 | * |
||
360 | * @return mixed |
||
361 | */ |
||
362 | private function escapeChars($data) |
||
363 | { |
||
364 | $arrStrFrom = ['&', '<', '>', '"', '\'']; |
||
365 | $arrStrTo = ['&', '<', '>', '"', ''']; |
||
366 | |||
367 | return str_replace($arrStrFrom, $arrStrTo, $data); |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * Crear el nodo con los datos de los clientes |
||
372 | * |
||
373 | * @throws ServiceException |
||
374 | * @throws ServiceException |
||
375 | * @throws ContainerExceptionInterface |
||
376 | * @throws NotFoundExceptionInterface |
||
377 | */ |
||
378 | private function createClients() |
||
379 | { |
||
380 | try { |
||
381 | $this->eventDispatcher->notifyEvent('run.export.process.client', |
||
382 | new Event($this, EventMessage::factory() |
||
383 | ->addDescription(__u('Exporting clients'))) |
||
384 | ); |
||
385 | |||
386 | $clientService = $this->dic->get(ClientService::class); |
||
387 | $clients = $clientService->getAllBasic(); |
||
388 | |||
389 | // Crear el nodo de clientes |
||
390 | $nodeClients = $this->xml->createElement('Clients'); |
||
391 | |||
392 | if (count($clients) === 0) { |
||
393 | $this->appendNode($nodeClients); |
||
394 | return; |
||
395 | } |
||
396 | |||
397 | foreach ($clients as $client) { |
||
398 | $clientName = $this->xml->createElement('name', $this->escapeChars($client->getName())); |
||
399 | $clientDescription = $this->xml->createElement('description', $this->escapeChars($client->getDescription())); |
||
400 | |||
401 | // Crear el nodo de clientes |
||
402 | $nodeClient = $this->xml->createElement('Client'); |
||
403 | $nodeClient->setAttribute('id', $client->getId()); |
||
404 | $nodeClient->appendChild($clientName); |
||
405 | $nodeClient->appendChild($clientDescription); |
||
406 | |||
407 | // Añadir cliente al nodo de clientes |
||
408 | $nodeClients->appendChild($nodeClient); |
||
409 | } |
||
410 | |||
411 | $this->appendNode($nodeClients); |
||
412 | } catch (Exception $e) { |
||
413 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
414 | } |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Crear el nodo con los datos de las etiquetas |
||
419 | * |
||
420 | * @throws ServiceException |
||
421 | * @throws ContainerExceptionInterface |
||
422 | * @throws NotFoundExceptionInterface |
||
423 | */ |
||
424 | private function createTags() |
||
425 | { |
||
426 | try { |
||
427 | $this->eventDispatcher->notifyEvent('run.export.process.tag', |
||
428 | new Event($this, EventMessage::factory() |
||
429 | ->addDescription(__u('Exporting tags'))) |
||
430 | ); |
||
431 | |||
432 | $tagService = $this->dic->get(TagService::class); |
||
433 | $tags = $tagService->getAllBasic(); |
||
434 | |||
435 | // Crear el nodo de etiquetas |
||
436 | $nodeTags = $this->xml->createElement('Tags'); |
||
437 | |||
438 | if (count($tags) === 0) { |
||
439 | $this->appendNode($nodeTags); |
||
440 | return; |
||
441 | } |
||
442 | |||
443 | foreach ($tags as $tag) { |
||
444 | $tagName = $this->xml->createElement('name', $this->escapeChars($tag->getName())); |
||
445 | |||
446 | // Crear el nodo de etiquetas |
||
447 | $nodeTag = $this->xml->createElement('Tag'); |
||
448 | $nodeTag->setAttribute('id', $tag->getId()); |
||
449 | $nodeTag->appendChild($tagName); |
||
450 | |||
451 | // Añadir etiqueta al nodo de etiquetas |
||
452 | $nodeTags->appendChild($nodeTag); |
||
453 | } |
||
454 | |||
455 | $this->appendNode($nodeTags); |
||
456 | } catch (Exception $e) { |
||
457 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
458 | } |
||
459 | } |
||
460 | |||
461 | /** |
||
462 | * Crear el nodo con los datos de las cuentas |
||
463 | * |
||
464 | * @throws ServiceException |
||
465 | * @throws ContainerExceptionInterface |
||
466 | * @throws NotFoundExceptionInterface |
||
467 | */ |
||
468 | private function createAccounts() |
||
469 | { |
||
470 | try { |
||
471 | $this->eventDispatcher->notifyEvent('run.export.process.account', |
||
472 | new Event($this, EventMessage::factory() |
||
473 | ->addDescription(__u('Exporting accounts'))) |
||
474 | ); |
||
475 | |||
476 | $accountService = $this->dic->get(AccountService::class); |
||
477 | $accountToTagService = $this->dic->get(AccountToTagService::class); |
||
478 | $accounts = $accountService->getAllBasic(); |
||
479 | |||
480 | // Crear el nodo de cuentas |
||
481 | $nodeAccounts = $this->xml->createElement('Accounts'); |
||
482 | |||
483 | if (count($accounts) === 0) { |
||
484 | $this->appendNode($nodeAccounts); |
||
485 | return; |
||
486 | } |
||
487 | |||
488 | foreach ($accounts as $account) { |
||
489 | $accountName = $this->xml->createElement('name', $this->escapeChars($account->getName())); |
||
490 | $accountCustomerId = $this->xml->createElement('clientId', $account->getClientId()); |
||
491 | $accountCategoryId = $this->xml->createElement('categoryId', $account->getCategoryId()); |
||
492 | $accountLogin = $this->xml->createElement('login', $this->escapeChars($account->getLogin())); |
||
493 | $accountUrl = $this->xml->createElement('url', $this->escapeChars($account->getUrl())); |
||
494 | $accountNotes = $this->xml->createElement('notes', $this->escapeChars($account->getNotes())); |
||
495 | $accountPass = $this->xml->createElement('pass', $this->escapeChars($account->getPass())); |
||
496 | $accountIV = $this->xml->createElement('key', $this->escapeChars($account->getKey())); |
||
497 | $tags = $this->xml->createElement('tags'); |
||
498 | |||
499 | foreach ($accountToTagService->getTagsByAccountId($account->getId()) as $itemData) { |
||
500 | $tag = $this->xml->createElement('tag'); |
||
501 | $tag->setAttribute('id', $itemData->getId()); |
||
502 | |||
503 | $tags->appendChild($tag); |
||
504 | } |
||
505 | |||
506 | // Crear el nodo de cuenta |
||
507 | $nodeAccount = $this->xml->createElement('Account'); |
||
508 | $nodeAccount->setAttribute('id', $account->getId()); |
||
509 | $nodeAccount->appendChild($accountName); |
||
510 | $nodeAccount->appendChild($accountCustomerId); |
||
511 | $nodeAccount->appendChild($accountCategoryId); |
||
512 | $nodeAccount->appendChild($accountLogin); |
||
513 | $nodeAccount->appendChild($accountUrl); |
||
514 | $nodeAccount->appendChild($accountNotes); |
||
515 | $nodeAccount->appendChild($accountPass); |
||
516 | $nodeAccount->appendChild($accountIV); |
||
517 | $nodeAccount->appendChild($tags); |
||
518 | |||
519 | // Añadir cuenta al nodo de cuentas |
||
520 | $nodeAccounts->appendChild($nodeAccount); |
||
521 | } |
||
522 | |||
523 | $this->appendNode($nodeAccounts); |
||
524 | } catch (Exception $e) { |
||
525 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
526 | } |
||
527 | } |
||
528 | |||
529 | /** |
||
530 | * Crear el hash del archivo XML e insertarlo en el árbol DOM |
||
531 | * |
||
532 | * @throws ServiceException |
||
533 | */ |
||
534 | private function createHash() |
||
535 | { |
||
536 | try { |
||
537 | $hash = self::generateHashFromNodes($this->xml); |
||
538 | |||
539 | $hashNode = $this->xml->createElement('Hash', $hash); |
||
540 | $hashNode->appendChild($this->xml->createAttribute('sign')); |
||
541 | |||
542 | $key = $this->exportPass ?: sha1($this->configData->getPasswordSalt()); |
||
543 | |||
544 | $hashNode->setAttribute('sign', Hash::signMessage($hash, $key)); |
||
545 | |||
546 | $this->root |
||
547 | ->getElementsByTagName('Meta') |
||
548 | ->item(0) |
||
549 | ->appendChild($hashNode); |
||
550 | } catch (Exception $e) { |
||
551 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
552 | } |
||
553 | } |
||
554 | |||
555 | /** |
||
556 | * @param DOMDocument $document |
||
557 | * |
||
558 | * @return string |
||
559 | */ |
||
560 | public static function generateHashFromNodes(DOMDocument $document): string |
||
569 | } |
||
570 | |||
571 | /** |
||
572 | * Generar el archivo XML |
||
573 | * |
||
574 | * @throws ServiceException |
||
575 | */ |
||
576 | private function writeXML() |
||
577 | { |
||
578 | try { |
||
579 | $this->xml->formatOutput = true; |
||
580 | $this->xml->preserveWhiteSpace = false; |
||
581 | |||
582 | if (!$this->xml->save($this->exportFile)) { |
||
583 | throw new ServiceException(__u('Error while creating the XML file')); |
||
584 | } |
||
585 | } catch (Exception $e) { |
||
586 | throw new ServiceException($e->getMessage(), ServiceException::ERROR, __FUNCTION__); |
||
587 | } |
||
588 | } |
||
589 | |||
590 | /** |
||
591 | * @throws CheckException |
||
592 | * @throws FileException |
||
593 | */ |
||
594 | public function createArchive() |
||
595 | { |
||
596 | $archive = new ArchiveHandler($this->exportFile, $this->extensionChecker); |
||
597 | $archive->compressFile($this->exportFile); |
||
598 | |||
599 | $file = new FileHandler($this->exportFile); |
||
600 | $file->delete(); |
||
601 | } |
||
602 | |||
603 | /** |
||
604 | * @return string |
||
605 | */ |
||
606 | public function getExportFile(): string |
||
609 | } |
||
610 | |||
611 | /** |
||
612 | * @return bool |
||
613 | */ |
||
614 | public function isEncrypted(): bool |
||
617 | } |
||
618 | |||
619 | /** |
||
620 | * @throws ContainerExceptionInterface |
||
621 | * @throws NotFoundExceptionInterface |
||
622 | */ |
||
623 | protected function initialize() |
||
624 | { |
||
625 | $this->extensionChecker = $this->dic->get(PhpExtensionChecker::class); |
||
626 | $this->configData = $this->config->getConfigData(); |
||
627 | } |
||
628 | |||
629 | /** |
||
630 | * Devuelve el código XML de un nodo |
||
631 | * |
||
632 | * @param $node string El nodo a devolver |
||
633 | * |
||
634 | * @return string |
||
635 | * @throws ServiceException |
||
636 | */ |
||
637 | private function getNodeXML($node) |
||
643 | } |
||
644 | } |
||
645 | } |