FileBackupService::backupTables()   C
last analyzed

Complexity

Conditions 13
Paths 168

Size

Total Lines 123
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 77
c 1
b 0
f 0
nc 168
nop 2
dl 0
loc 123
rs 5.3418

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2019, Rubén Domínguez nuxsmin@$syspass.org
8
 *
9
 * This file is part of sysPass.
10
 *
11
 * sysPass is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation, either version 3 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * sysPass is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 *  along with sysPass.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace SP\Services\Backup;
26
27
use Exception;
28
use PDO;
29
use Psr\Container\ContainerExceptionInterface;
30
use Psr\Container\NotFoundExceptionInterface;
31
use SP\Config\ConfigData;
32
use SP\Core\AppInfoInterface;
33
use SP\Core\Events\Event;
34
use SP\Core\Events\EventMessage;
35
use SP\Core\Exceptions\CheckException;
36
use SP\Core\Exceptions\ConstraintException;
37
use SP\Core\Exceptions\QueryException;
38
use SP\Core\Exceptions\SPException;
39
use SP\Core\PhpExtensionChecker;
40
use SP\Services\Service;
41
use SP\Services\ServiceException;
42
use SP\Storage\Database\Database;
43
use SP\Storage\Database\DatabaseUtil;
44
use SP\Storage\Database\QueryData;
45
use SP\Storage\File\ArchiveHandler;
46
use SP\Storage\File\FileException;
47
use SP\Storage\File\FileHandler;
48
use SP\Util\Checks;
49
50
defined('APP_ROOT') || die();
51
52
/**
53
 * Esta clase es la encargada de realizar la copia de sysPass.
54
 */
55
final class FileBackupService extends Service
56
{
57
    private const BACKUP_EXCLUDE_REGEX = '#^(?!.*(backup|cache|temp|vendor|tests))(.*)$#i';
58
59
    /**
60
     * @var ConfigData
61
     */
62
    private $configData;
63
    /**
64
     * @var string
65
     */
66
    private $path;
67
    /**
68
     * @var string
69
     */
70
    private $backupFileApp;
71
    /**
72
     * @var string
73
     */
74
    private $backupFileDb;
75
    /**
76
     * @var PhpExtensionChecker
77
     */
78
    private $extensionChecker;
79
    /**
80
     * @var string
81
     */
82
    private $hash;
83
84
    /**
85
     * Realizar backup de la BBDD y aplicación.
86
     *
87
     * @param string $path
88
     *
89
     * @throws ServiceException
90
     */
91
    public function doBackup(string $path)
92
    {
93
        set_time_limit(0);
94
95
        $this->path = $path;
96
97
        $this->checkBackupDir();
98
99
        // Generar hash unico para evitar descargas no permitidas
100
        $this->hash = sha1(uniqid('sysPassBackup', true));
101
102
        $this->backupFileApp = self::getAppBackupFilename($path, $this->hash);
103
        $this->backupFileDb = self::getDbBackupFilename($path, $this->hash);
104
105
        try {
106
            $this->deleteOldBackups();
107
108
            $this->eventDispatcher->notifyEvent('run.backup.start',
109
                new Event($this,
110
                    EventMessage::factory()->addDescription(__u('Make Backup'))));
111
112
            $this->backupTables(new FileHandler($this->backupFileDb), '*');
113
114
            if (!$this->backupApp()
115
                && !$this->backupAppLegacyLinux()
116
            ) {
117
                throw new ServiceException(__u('Error while doing the backup in compatibility mode'));
118
            }
119
120
            $this->configData->setBackupHash($this->hash);
121
            $this->config->saveConfig($this->configData);
122
        } catch (ServiceException $e) {
123
            throw $e;
124
        } catch (Exception $e) {
125
            $this->eventDispatcher->notifyEvent('exception', new Event($e));
126
127
            throw new ServiceException(
128
                __u('Error while doing the backup'),
129
                SPException::ERROR,
130
                __u('Please check out the event log for more details'),
131
                $e->getCode(),
132
                $e
133
            );
134
        }
135
    }
136
137
    /**
138
     * Comprobar y crear el directorio de backups.
139
     *
140
     * @return bool
141
     * @throws ServiceException
142
     */
143
    private function checkBackupDir()
144
    {
145
        if (is_dir($this->path) === false
146
            && @mkdir($this->path, 0750) === false
147
        ) {
148
            throw new ServiceException(
149
                sprintf(__('Unable to create the backups directory ("%s")'), $this->path));
150
        }
151
152
        if (!is_writable($this->path)) {
153
            throw new ServiceException(
154
                __u('Please, check the backup directory permissions'));
155
        }
156
157
        return true;
158
    }
159
160
    /**
161
     * @param string $path
162
     * @param string $hash
163
     * @param bool   $compressed
164
     *
165
     * @return string
166
     */
167
    public static function getAppBackupFilename(string $path, string $hash, bool $compressed = false)
168
    {
169
        $file = $path . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME . '_app-' . $hash;
170
171
        if ($compressed) {
172
            return $file . ArchiveHandler::COMPRESS_EXTENSION;
173
        }
174
175
        return $file;
176
    }
177
178
    /**
179
     * @param string $path
180
     * @param string $hash
181
     * @param bool   $compressed
182
     *
183
     * @return string
184
     */
185
    public static function getDbBackupFilename(string $path, string $hash, bool $compressed = false)
186
    {
187
        $file = $path . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME . '_db-' . $hash;
188
189
        if ($compressed) {
190
            return $file . ArchiveHandler::COMPRESS_EXTENSION;
191
        }
192
193
        return $file . '.sql';
194
    }
195
196
    /**
197
     * Eliminar las copias de seguridad anteriores
198
     */
199
    private function deleteOldBackups()
200
    {
201
        $path = $this->path . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME;
202
203
        array_map(function ($file) {
204
            return @unlink($file);
205
        }, array_merge(
206
            glob($path . '_db-*'),
0 ignored issues
show
Bug introduced by
It seems like glob($path . '_db-*') can also be of type false; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

206
            /** @scrutinizer ignore-type */ glob($path . '_db-*'),
Loading history...
207
            glob($path . '_app-*'),
0 ignored issues
show
Bug introduced by
It seems like glob($path . '_app-*') can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

207
            /** @scrutinizer ignore-type */ glob($path . '_app-*'),
Loading history...
208
            glob($path . '*.sql')
0 ignored issues
show
Bug introduced by
It seems like glob($path . '*.sql') can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

208
            /** @scrutinizer ignore-type */ glob($path . '*.sql')
Loading history...
209
        ));
210
    }
211
212
    /**
213
     * Backup de las tablas de la BBDD.
214
     * Utilizar '*' para toda la BBDD o 'table1 table2 table3...'
215
     *
216
     * @param FileHandler  $fileHandler
217
     * @param string|array $tables
218
     *
219
     * @throws ConstraintException
220
     * @throws QueryException
221
     * @throws FileException
222
     * @throws CheckException
223
     */
224
    private function backupTables(FileHandler $fileHandler, $tables = '*')
225
    {
226
        $this->eventDispatcher->notifyEvent('run.backup.process',
227
            new Event($this,
228
                EventMessage::factory()->addDescription(__u('Copying database')))
229
        );
230
231
        $fileHandler->open('w');
232
233
        $db = $this->dic->get(Database::class);
234
        $databaseUtil = $this->dic->get(DatabaseUtil::class);
235
236
        $queryData = new QueryData();
237
238
        if ($tables === '*') {
239
            $resTables = DatabaseUtil::$tables;
240
        } else {
241
            $resTables = is_array($tables) ? $tables : explode(',', $tables);
242
        }
243
244
        $lineSeparator = PHP_EOL . PHP_EOL;
245
246
        $dbname = $db->getDbHandler()->getDatabaseName();
247
248
        $sqlOut = '-- ' . PHP_EOL;
249
        $sqlOut .= '-- sysPass DB dump generated on ' . time() . ' (START)' . PHP_EOL;
250
        $sqlOut .= '-- ' . PHP_EOL;
251
        $sqlOut .= '-- Please, do not alter this file, it could break your DB' . PHP_EOL;
252
        $sqlOut .= '-- ' . PHP_EOL;
253
        $sqlOut .= 'SET AUTOCOMMIT = 0;' . PHP_EOL;
254
        $sqlOut .= 'SET FOREIGN_KEY_CHECKS = 0;' . PHP_EOL;
255
        $sqlOut .= 'SET UNIQUE_CHECKS = 0;' . PHP_EOL;
256
        $sqlOut .= '-- ' . PHP_EOL;
257
        $sqlOut .= 'CREATE DATABASE IF NOT EXISTS `' . $dbname . '`;' . PHP_EOL . PHP_EOL;
258
        $sqlOut .= 'USE `' . $dbname . '`;' . PHP_EOL . PHP_EOL;
259
260
        $fileHandler->write($sqlOut);
261
262
        $sqlOutViews = '';
263
        // Recorrer las tablas y almacenar los datos
264
        foreach ($resTables as $table) {
265
            $tableName = is_object($table) ? $table->{'Tables_in_' . $dbname} : $table;
266
267
            $queryData->setQuery('SHOW CREATE TABLE ' . $tableName);
268
269
            // Consulta para crear la tabla
270
            $txtCreate = $db->doQuery($queryData)->getData();
271
272
            if (isset($txtCreate->{'Create Table'})) {
273
                $sqlOut = '-- ' . PHP_EOL;
274
                $sqlOut .= '-- Table ' . strtoupper($tableName) . PHP_EOL;
275
                $sqlOut .= '-- ' . PHP_EOL;
276
                $sqlOut .= 'DROP TABLE IF EXISTS `' . $tableName . '`;' . PHP_EOL . PHP_EOL;
277
                $sqlOut .= $txtCreate->{'Create Table'} . ';' . PHP_EOL . PHP_EOL;
278
279
                $fileHandler->write($sqlOut);
280
            } elseif (isset($txtCreate->{'Create View'})) {
281
                $sqlOutViews .= '-- ' . PHP_EOL;
282
                $sqlOutViews .= '-- View ' . strtoupper($tableName) . PHP_EOL;
283
                $sqlOutViews .= '-- ' . PHP_EOL;
284
                $sqlOutViews .= 'DROP TABLE IF EXISTS `' . $tableName . '`;' . PHP_EOL . PHP_EOL;
285
                $sqlOutViews .= $txtCreate->{'Create View'} . ';' . PHP_EOL . PHP_EOL;
286
            }
287
288
            $fileHandler->write($lineSeparator);
289
        }
290
291
        // Guardar las vistas
292
        $fileHandler->write($sqlOutViews);
293
294
        // Guardar los datos
295
        foreach ($resTables as $tableName) {
296
            // No guardar las vistas!
297
            if (strrpos($tableName, '_v') !== false) {
298
                continue;
299
            }
300
301
            $queryData->setQuery('SELECT * FROM `' . $tableName . '`');
302
303
            // Consulta para obtener los registros de la tabla
304
            $queryRes = $db->doQueryRaw($queryData, [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL], false);
305
306
            $numColumns = $queryRes->columnCount();
307
308
            while ($row = $queryRes->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_NEXT)) {
309
                $fileHandler->write('INSERT INTO `' . $tableName . '` VALUES(');
310
311
                $field = 1;
312
                foreach ($row as $value) {
313
                    if (is_numeric($value)) {
314
                        $fileHandler->write($value);
315
                    } else {
316
                        $fileHandler->write($databaseUtil->escape($value));
317
                    }
318
319
                    if ($field < $numColumns) {
320
                        $fileHandler->write(',');
321
                    }
322
323
                    $field++;
324
                }
325
326
                $fileHandler->write(');' . PHP_EOL);
327
            }
328
        }
329
330
        $sqlOut = '-- ' . PHP_EOL;
331
        $sqlOut .= 'SET AUTOCOMMIT = 1;' . PHP_EOL;
332
        $sqlOut .= 'SET FOREIGN_KEY_CHECKS = 1;' . PHP_EOL;
333
        $sqlOut .= 'SET UNIQUE_CHECKS = 1;' . PHP_EOL;
334
        $sqlOut .= '-- ' . PHP_EOL;
335
        $sqlOut .= '-- sysPass DB dump generated on ' . time() . ' (END)' . PHP_EOL;
336
        $sqlOut .= '-- ' . PHP_EOL;
337
        $sqlOut .= '-- Please, do not alter this file, it could break your DB' . PHP_EOL;
338
        $sqlOut .= '-- ' . PHP_EOL . PHP_EOL;
339
340
        $fileHandler->write($sqlOut);
341
        $fileHandler->close();
342
343
        $archive = new ArchiveHandler($fileHandler->getFile(), $this->extensionChecker);
344
        $archive->compressFile($fileHandler->getFile());
345
346
        $fileHandler->delete();
347
    }
348
349
    /**
350
     * Realizar un backup de la aplicación y comprimirlo.
351
     *
352
     * @return bool
353
     * @throws CheckException
354
     * @throws FileException
355
     */
356
    private function backupApp()
357
    {
358
        $this->eventDispatcher->notifyEvent('run.backup.process',
359
            new Event($this, EventMessage::factory()
360
                ->addDescription(__u('Copying application')))
361
        );
362
363
        $archive = new ArchiveHandler($this->backupFileApp, $this->extensionChecker);
364
365
        $archive->compressDirectory(APP_ROOT, self::BACKUP_EXCLUDE_REGEX);
366
367
        return true;
368
    }
369
370
    /**
371
     * Realizar un backup de la aplicación y comprimirlo usando aplicaciones del SO Linux.
372
     *
373
     * @return int Con el código de salida del comando ejecutado
374
     * @throws ServiceException
375
     */
376
    private function backupAppLegacyLinux()
377
    {
378
        if (Checks::checkIsWindows()) {
379
            throw new ServiceException(
380
                __u('This operation is only available on Linux environments'), ServiceException::INFO);
381
        }
382
383
        $this->eventDispatcher->notifyEvent('run.backup.process',
384
            new Event($this, EventMessage::factory()
385
                ->addDescription(__u('Copying application')))
386
        );
387
388
        $command = 'tar czf ' . $this->backupFileApp . ArchiveHandler::COMPRESS_EXTENSION . ' ' . BASE_PATH . ' --exclude "' . $this->path . '" 2>&1';
389
        exec($command, $resOut, $resBakApp);
390
391
        return $resBakApp;
392
    }
393
394
    /**
395
     * @return string
396
     */
397
    public function getHash(): string
398
    {
399
        return $this->hash;
400
    }
401
402
    /**
403
     * @throws ContainerExceptionInterface
404
     * @throws NotFoundExceptionInterface
405
     */
406
    protected function initialize()
407
    {
408
        $this->configData = $this->config->getConfigData();
409
        $this->extensionChecker = $this->dic->get(PhpExtensionChecker::class);
410
    }
411
}