Passed
Push — devel-3.0 ( 40e719...463a23 )
by Rubén
03:27
created

FileBackupService::getBackupFileDb()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2018, 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 SP\Config\ConfigData;
28
use SP\Core\AppInfoInterface;
29
use SP\Core\Events\Event;
30
use SP\Core\Events\EventMessage;
31
use SP\Core\Exceptions\SPException;
32
use SP\Core\PhpExtensionChecker;
33
use SP\Services\Service;
34
use SP\Services\ServiceException;
35
use SP\Storage\Database\Database;
36
use SP\Storage\Database\DatabaseUtil;
37
use SP\Storage\Database\QueryData;
38
use SP\Storage\File\ArchiveHandler;
39
use SP\Storage\File\FileHandler;
40
use SP\Util\Checks;
41
42
defined('APP_ROOT') || die();
43
44
/**
45
 * Esta clase es la encargada de realizar la copia de sysPass.
46
 */
47
final class FileBackupService extends Service
48
{
49
    /**
50
     * @var ConfigData
51
     */
52
    private $configData;
53
    /**
54
     * @var string
55
     */
56
    private $path;
57
    /**
58
     * @var string
59
     */
60
    private $backupFileApp;
61
    /**
62
     * @var string
63
     */
64
    private $backupFileDb;
65
    /**
66
     * @var PhpExtensionChecker
67
     */
68
    private $extensionChecker;
69
    /**
70
     * @var string
71
     */
72
    private $hash;
73
74
    /**
75
     * Realizar backup de la BBDD y aplicación.
76
     *
77
     * @param string $path
78
     *
79
     * @throws ServiceException
80
     */
81
    public function doBackup(string $path)
82
    {
83
        $this->path = $path;
84
85
        $this->checkBackupDir();
86
87
        // Generar hash unico para evitar descargas no permitidas
88
        $this->hash = sha1(uniqid('sysPassBackup', true));
89
90
        $this->backupFileApp = self::getAppBackupFilename($path, $this->hash);
91
        $this->backupFileDb = self::getDbBackupFilename($path, $this->hash);
92
93
        try {
94
            $this->deleteOldBackups();
95
96
            $this->eventDispatcher->notifyEvent('run.backup.start',
97
                new Event($this,
98
                    EventMessage::factory()->addDescription(__u('Realizar Backup'))));
99
100
            $this->backupTables(new FileHandler($this->backupFileDb), '*');
101
102
            if (!$this->backupApp()
103
                && !$this->backupAppLegacyLinux()
104
            ) {
105
                throw new ServiceException(__u('Error al realizar backup en modo compatibilidad'));
106
            }
107
108
            $this->configData->setBackupHash($this->hash);
109
            $this->config->saveConfig($this->configData);
110
        } catch (ServiceException $e) {
111
            throw $e;
112
        } catch (\Exception $e) {
113
            $this->eventDispatcher->notifyEvent('exception', new Event($e));
114
115
            throw new ServiceException(
116
                __u('Error al realizar el backup'),
117
                SPException::ERROR,
118
                __u('Revise el registro de eventos para más detalles'),
119
                $e->getCode(),
120
                $e
121
            );
122
        }
123
    }
124
125
    /**
126
     * Comprobar y crear el directorio de backups.
127
     *
128
     * @throws ServiceException
129
     * @return bool
130
     */
131
    private function checkBackupDir()
132
    {
133
        if (is_dir($this->path) === false
134
            && @mkdir($this->path, 0750) === false
135
        ) {
136
            throw new ServiceException(
137
                sprintf(__('No es posible crear el directorio de backups ("%s")'), $this->path));
138
        }
139
140
        if (!is_writable($this->path)) {
141
            throw new ServiceException(
142
                __u('Compruebe los permisos del directorio de backups'));
143
        }
144
145
        return true;
146
    }
147
148
    /**
149
     * @param string $path
150
     * @param string $hash
151
     * @param bool   $compressed
152
     *
153
     * @return string
154
     */
155
    public static function getAppBackupFilename(string $path, string $hash, bool $compressed = false)
156
    {
157
        $file = $path . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME . '_app-' . $hash;
158
159
        if ($compressed) {
160
            return $file . ArchiveHandler::COMPRESS_EXTENSION;
161
        }
162
163
        return $file;
164
    }
165
166
    /**
167
     * @param string $path
168
     * @param string $hash
169
     * @param bool   $compressed
170
     *
171
     * @return string
172
     */
173
    public static function getDbBackupFilename(string $path, string $hash, bool $compressed = false)
174
    {
175
        $file = $path . DIRECTORY_SEPARATOR . AppInfoInterface::APP_NAME . '_db-' . $hash;
176
177
        if ($compressed) {
178
            return $file . ArchiveHandler::COMPRESS_EXTENSION;
179
        }
180
181
        return $file . '.sql';
182
    }
183
184
    /**
185
     * Eliminar las copias de seguridad anteriores
186
     */
187
    private function deleteOldBackups()
188
    {
189
        array_map('unlink', glob($this->path . DIRECTORY_SEPARATOR . '*' . ArchiveHandler::COMPRESS_EXTENSION));
190
        array_map('unlink', glob($this->path . DIRECTORY_SEPARATOR . '*.sql'));
191
    }
192
193
    /**
194
     * Backup de las tablas de la BBDD.
195
     * Utilizar '*' para toda la BBDD o 'table1 table2 table3...'
196
     *
197
     * @param \SP\Storage\File\FileHandler $fileHandler
198
     * @param string|array                 $tables
199
     *
200
     * @throws \SP\Core\Exceptions\ConstraintException
201
     * @throws \SP\Core\Exceptions\QueryException
202
     * @throws \SP\Storage\File\FileException
203
     * @throws \SP\Core\Exceptions\CheckException
204
     */
205
    private function backupTables(FileHandler $fileHandler, $tables = '*')
206
    {
207
        $this->eventDispatcher->notifyEvent('run.backup.process',
208
            new Event($this,
209
                EventMessage::factory()->addDescription(__u('Copiando base de datos')))
210
        );
211
212
        $fileHandler->open('w');
213
214
        $db = $this->dic->get(Database::class);
215
216
        $queryData = new QueryData();
217
218
        if ($tables === '*') {
219
            $resTables = DatabaseUtil::$tables;
220
        } else {
221
            $resTables = is_array($tables) ? $tables : explode(',', $tables);
222
        }
223
224
        $lineSeparator = PHP_EOL . PHP_EOL;
225
226
        $dbname = $db->getDbHandler()->getDatabaseName();
227
228
        $sqlOut = '-- ' . PHP_EOL;
229
        $sqlOut .= '-- sysPass DB dump generated on ' . time() . ' (START)' . PHP_EOL;
230
        $sqlOut .= '-- ' . PHP_EOL;
231
        $sqlOut .= '-- Please, do not alter this file, it could break your DB' . PHP_EOL;
232
        $sqlOut .= '-- ' . PHP_EOL;
233
        $sqlOut .= 'SET AUTOCOMMIT = 0;' . PHP_EOL;
234
        $sqlOut .= 'SET FOREIGN_KEY_CHECKS = 0;' . PHP_EOL;
235
        $sqlOut .= 'SET UNIQUE_CHECKS = 0;' . PHP_EOL;
236
        $sqlOut .= '-- ' . PHP_EOL;
237
        $sqlOut .= 'CREATE DATABASE IF NOT EXISTS `' . $dbname . '`;' . PHP_EOL . PHP_EOL;
238
        $sqlOut .= 'USE `' . $dbname . '`;' . PHP_EOL . PHP_EOL;
239
240
        $fileHandler->write($sqlOut);
241
242
        $sqlOutViews = '';
243
        // Recorrer las tablas y almacenar los datos
244
        foreach ($resTables as $table) {
245
            $tableName = is_object($table) ? $table->{'Tables_in_' . $dbname} : $table;
246
247
            $queryData->setQuery('SHOW CREATE TABLE ' . $tableName);
248
249
            // Consulta para crear la tabla
250
            $txtCreate = $db->doQuery($queryData)->getData();
251
252
            if (isset($txtCreate->{'Create Table'})) {
253
                $sqlOut = '-- ' . PHP_EOL;
254
                $sqlOut .= '-- Table ' . strtoupper($tableName) . PHP_EOL;
255
                $sqlOut .= '-- ' . PHP_EOL;
256
                $sqlOut .= 'DROP TABLE IF EXISTS `' . $tableName . '`;' . PHP_EOL . PHP_EOL;
257
                $sqlOut .= $txtCreate->{'Create Table'} . ';' . PHP_EOL . PHP_EOL;
258
259
                $fileHandler->write($sqlOut);
260
            } elseif (isset($txtCreate->{'Create View'})) {
261
                $sqlOutViews .= '-- ' . PHP_EOL;
262
                $sqlOutViews .= '-- View ' . strtoupper($tableName) . PHP_EOL;
263
                $sqlOutViews .= '-- ' . PHP_EOL;
264
                $sqlOutViews .= 'DROP TABLE IF EXISTS `' . $tableName . '`;' . PHP_EOL . PHP_EOL;
265
                $sqlOutViews .= $txtCreate->{'Create View'} . ';' . PHP_EOL . PHP_EOL;
266
            }
267
268
            $fileHandler->write($lineSeparator);
269
        }
270
271
        // Guardar las vistas
272
        $fileHandler->write($sqlOutViews);
273
274
        // Guardar los datos
275
        foreach ($resTables as $tableName) {
276
            // No guardar las vistas!
277
            if (strrpos($tableName, '_v') !== false) {
278
                continue;
279
            }
280
281
            $queryData->setQuery('SELECT * FROM `' . $tableName . '`');
282
283
            // Consulta para obtener los registros de la tabla
284
            $queryRes = $db->doQueryRaw($queryData);
285
286
            $numColumns = $queryRes->columnCount();
287
288
            while ($row = $queryRes->fetch(\PDO::FETCH_NUM)) {
289
                $fileHandler->write('INSERT INTO `' . $tableName . '` VALUES(');
290
291
                $field = 1;
292
                foreach ($row as $value) {
293
                    if (is_numeric($value)) {
294
                        $fileHandler->write($value);
295
                    } else {
296
                        $fileHandler->write(DatabaseUtil::escape($value, $db->getDbHandler()));
297
                    }
298
299
                    if ($field < $numColumns) {
300
                        $fileHandler->write(',');
301
                    }
302
303
                    $field++;
304
                }
305
306
                $fileHandler->write(');' . PHP_EOL);
307
            }
308
        }
309
310
        $sqlOut = '-- ' . PHP_EOL;
311
        $sqlOut .= 'SET AUTOCOMMIT = 1;' . PHP_EOL;
312
        $sqlOut .= 'SET FOREIGN_KEY_CHECKS = 1;' . PHP_EOL;
313
        $sqlOut .= 'SET UNIQUE_CHECKS = 1;' . PHP_EOL;
314
        $sqlOut .= '-- ' . PHP_EOL;
315
        $sqlOut .= '-- sysPass DB dump generated on ' . time() . ' (END)' . PHP_EOL;
316
        $sqlOut .= '-- ' . PHP_EOL;
317
        $sqlOut .= '-- Please, do not alter this file, it could break your DB' . PHP_EOL;
318
        $sqlOut .= '-- ' . PHP_EOL . PHP_EOL;
319
320
        $fileHandler->write($sqlOut);
321
        $fileHandler->close();
322
323
        $archive = new ArchiveHandler($fileHandler->getFile(), $this->extensionChecker);
324
        $archive->compressFile($fileHandler->getFile());
325
326
        $fileHandler->delete();
327
    }
328
329
    /**
330
     * Realizar un backup de la aplicación y comprimirlo.
331
     *
332
     * @return bool
333
     * @throws \SP\Core\Exceptions\CheckException
334
     * @throws \SP\Storage\File\FileException
335
     */
336
    private function backupApp()
337
    {
338
        $this->eventDispatcher->notifyEvent('run.backup.process',
339
            new Event($this, EventMessage::factory()
340
                ->addDescription(__u('Copiando aplicación')))
341
        );
342
343
        $archive = new ArchiveHandler($this->backupFileApp, $this->extensionChecker);
344
        $archive->compressDirectory(APP_ROOT, '#^(?!(.*backup))(.*)$#i');
345
346
        return true;
347
    }
348
349
    /**
350
     * Realizar un backup de la aplicación y comprimirlo usando aplicaciones del SO Linux.
351
     *
352
     * @return int Con el código de salida del comando ejecutado
353
     * @throws ServiceException
354
     */
355
    private function backupAppLegacyLinux()
356
    {
357
        if (Checks::checkIsWindows()) {
358
            throw new ServiceException(
359
                __u('Esta operación sólo es posible en entornos Linux'), ServiceException::INFO);
360
        }
361
362
        $this->eventDispatcher->notifyEvent('run.backup.process',
363
            new Event($this, EventMessage::factory()
364
                ->addDescription(__u('Copiando aplicación')))
365
        );
366
367
        $command = 'tar czf ' . $this->backupFileApp . ArchiveHandler::COMPRESS_EXTENSION . ' ' . BASE_PATH . ' --exclude "' . $this->path . '" 2>&1';
368
        exec($command, $resOut, $resBakApp);
369
370
        return $resBakApp;
371
    }
372
373
    /**
374
     * @return string
375
     */
376
    public function getHash(): string
377
    {
378
        return $this->hash;
379
    }
380
381
    /**
382
     * @throws \Psr\Container\ContainerExceptionInterface
383
     * @throws \Psr\Container\NotFoundExceptionInterface
384
     */
385
    protected function initialize()
386
    {
387
        $this->configData = $this->config->getConfigData();
388
        $this->extensionChecker = $this->dic->get(PhpExtensionChecker::class);
389
    }
390
}