Passed
Push — release-2.x ( 204b60...1fc07b )
by Slye
01:47
created

System::setFs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
namespace NDC\DatabaseBackup;
4
5
use const DIRECTORY_SEPARATOR as DS;
6
use function dirname;
7
use Dotenv\Dotenv;
8
use Dotenv\Exception\InvalidFileException;
9
use Dotenv\Validator;
10
use Exception;
11
use Ifsnop\Mysqldump\Mysqldump;
12
use JBZoo\Lang\Lang;
13
use League\Flysystem\Adapter\Local;
14
use League\Flysystem\AdapterInterface;
15
use League\Flysystem\FileNotFoundException;
16
use League\Flysystem\Filesystem;
17
use League\Flysystem\FilesystemInterface;
18
use NDC\DatabaseBackup\Exception\NotAllowedException;
19
use NDC\DatabaseBackup\Exception\UnsupportedPHPVersionException;
20
use PDO;
21
use RuntimeException;
22
use SplFileObject;
23
use Swift_Mailer;
24
use Swift_Message;
25
use Swift_SmtpTransport;
26
27
/**
28
 * Class System
29
 * @package NDC\DatabaseBackup
30
 * @todo Add possibility to switch year/day/hour/minute in CleanerFileSequence
31
 */
32
class System
33
{
34
    /**
35
     * @var Dotenv
36
     */
37
    private $env;
38
    /**
39
     * @var array
40
     */
41
    private $errors = [];
42
    /**
43
     * @var Lang
44
     */
45
    private $l10n;
46
    /**
47
     * @var System|null
48
     */
49
    private static $_instance;
50
    /**
51
     * @var FilesystemInterface
52
     */
53
    private $fs;
54
    /**
55
     * @var string
56
     */
57
    private $localDir;
58
59
    /**
60
     * @param AdapterInterface $adapter
61
     * @param array $adapterOptions
62
     * @return System|null
63
     * @throws FileNotFoundException
64
     * @throws NotAllowedException
65
     * @throws UnsupportedPHPVersionException
66
     * @throws \JBZoo\Lang\Exception
67
     * @throws \JBZoo\Path\Exception
68
     */
69
    public static function getInstance(?AdapterInterface $adapter = null, array $adapterOptions = []): ?System
70
    {
71
72
        if (self::$_instance === null) {
73
            self::$_instance = new System($adapter, $adapterOptions);
74
        }
75
        return self::$_instance;
76
    }
77
78
    /**
79
     * System constructor.
80
     * @param AdapterInterface|null $adapter
81
     * @param array $adapterOptions
82
     * @throws FileNotFoundException
83
     * @throws \JBZoo\Lang\Exception
84
     * @throws \JBZoo\Path\Exception
85
     * @throws UnsupportedPHPVersionException
86
     * @throws NotAllowedException
87
     */
88
    private function __construct(?AdapterInterface $adapter, array $adapterOptions)
89
    {
90
        $this->setLocalDir(dirname(__DIR__) . DS);
91
        $this->loadConfigurationEnvironment();
92
        $this->setL10n(new Lang(env('LANGUAGE', 'en')));
93
        $this->getL10n()->load($this->getLocalDir() . 'i18n', null, 'yml');
94
        if (PHP_SAPI !== 'cli' && !(bool)env('ALLOW_EXECUTE_IN_WEB_BROWSER', false)) {
95
            throw new NotAllowedException($this->getL10n()->translate('unauthorized_browser'));
96
        }
97
        if ((PHP_MAJOR_VERSION . PHP_MINOR_VERSION) < 72) {
98
            throw new UnsupportedPHPVersionException($this->getL10n()->translate('unsupported_php_version'));
99
        }
100
        if ($adapter === null) {
101
            $adapter = new Local($this->getLocalDir() . env('DIRECTORY_TO_SAVE_LOCAL_BACKUP', 'MySQLBackups') . DS);
102
        }
103
        CliFormatter::output($this->getL10n()->translate('app_started'), CliFormatter::COLOR_BLUE);
104
        $this->setFs($adapter, $adapterOptions);
105
        (int)env('FILES_DAYS_HISTORY', 3) > 0 ? $this->removeOldFilesByIntervalDays() : null;
106
    }
107
108
    /**
109
     * Start System initialization
110
     * @return void
111
     * @throws RuntimeException
112
     */
113
    private function loadConfigurationEnvironment(): void
114
    {
115
        if (!file_exists($this->getLocalDir() . '.env')) {
116
            throw new InvalidFileException('Please configure this script with .env file');
117
        }
118
        $this->setEnv(Dotenv::create($this->getLocalDir(), '.env'));
119
        $this->getEnv()->overload();
120
        $this->checkRequirements();
121
    }
122
123
    /**
124
     * @return Validator
125
     */
126
    private function checkRequirements(): Validator
127
    {
128
        return $this->getEnv()->required([
129
            'DB_HOST',
130
            'DB_USER',
131
            'DB_PASSWORD'
132
        ])->notEmpty();
133
    }
134
135
    /**
136
     * @return array|string
137
     */
138
    private function getExcludedDatabases()
139
    {
140
        if (empty(trim(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema')))) {
141
            return [];
142
        }
143
        return $this->parseAndSanitize(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema'));
0 ignored issues
show
Bug introduced by
It seems like env('DB_EXCLUDE_DATABASE...ql,performance_schema') can also be of type boolean and null; however, parameter $data of NDC\DatabaseBackup\System::parseAndSanitize() does only seem to accept string, 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

143
        return $this->parseAndSanitize(/** @scrutinizer ignore-type */ env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema'));
Loading history...
144
    }
145
146
    /**
147
     * @return array
148
     */
149
    private function getDatabases(): array
150
    {
151
        $pdo = new PDO('mysql:host=' . env('DB_HOST', 'localhost') . ';charset=UTF8', env('DB_USER', 'root'),
152
            env('DB_PASSWORD', 'root'), [
153
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
154
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
155
            ]);
156
        return $pdo->query('SHOW DATABASES')->fetchAll();
157
    }
158
159
    /**
160
     * Process to backup databases
161
     * @param array $settings
162
     */
163
    public function processBackup($settings = []): void
164
    {
165
        CliFormatter::output($this->l10n->translate('started_backup'), CliFormatter::COLOR_CYAN);
166
        $ext = 'sql';
167
        if (array_key_exists('compress', $settings)) {
168
            switch ($settings['compress']) {
169
                case 'gzip':
170
                case Mysqldump::GZIP:
171
                    $ext = 'sql.gz';
172
                    break;
173
                case 'bzip2':
174
                case Mysqldump::BZIP2:
175
                    $ext = 'sql.bz2';
176
                    break;
177
                case 'none':
178
                case Mysqldump::NONE:
179
                    $ext = 'sql';
180
                    break;
181
            }
182
        }
183
        foreach ($this->getDatabases() as $database) {
184
            if (!\in_array($database->Database, $this->getExcludedDatabases(), true)) {
0 ignored issues
show
Bug introduced by
It seems like $this->getExcludedDatabases() can also be of type string; however, parameter $haystack of in_array() 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

184
            if (!\in_array($database->Database, /** @scrutinizer ignore-type */ $this->getExcludedDatabases(), true)) {
Loading history...
185
                $filename = $database->Database . '-' . date($this->getL10n()->translate('date_format')) . ".$ext";
186
                try {
187
                    $dumper = new Mysqldump('mysql:host=' . env('DB_HOST',
188
                            'localhost') . ';dbname=' . $database->Database . ';charset=UTF8',
189
                        env('DB_USER', 'root'), env('DB_PASSWORD', ''), $settings, [
190
                            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
191
                        ]);
192
                    $tempFile = $this->createTempFile();
193
                    $dumper->start($tempFile->getRealPath());
194
                    $this->getFs()->writeStream($filename, fopen($tempFile->getRealPath(), 'wb+'));
0 ignored issues
show
Bug introduced by
It seems like fopen($tempFile->getRealPath(), 'wb+') can also be of type false; however, parameter $resource of League\Flysystem\Filesys...nterface::writeStream() does only seem to accept resource, 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

194
                    $this->getFs()->writeStream($filename, /** @scrutinizer ignore-type */ fopen($tempFile->getRealPath(), 'wb+'));
Loading history...
195
                    $this->deleteTempFile($tempFile);
196
                    CliFormatter::output($database->Database . ' ' . $this->getL10n()->translate('backuped_successfully'),
197
                        CliFormatter::COLOR_GREEN);
198
                } catch (Exception $e) {
199
                    CliFormatter::output('!! ERROR::' . $e->getMessage() . ' !!', CliFormatter::COLOR_RED);
200
                    $this->errors[] = [
201
                        'dbname' => $database->Database,
202
                        'error_message' => $e->getMessage(),
203
                        'error_code' => $e->getCode()
204
                    ];
205
                }
206
            }
207
        }
208
        $this->sendMail();
209
        CliFormatter::output($this->getL10n()->translate('databases_backup_successfull'), CliFormatter::COLOR_PURPLE);
210
    }
211
212
    /**
213
     * @param string $data
214
     * @return array|string
215
     */
216
    private function parseAndSanitize(string $data)
217
    {
218
        $results = explode(',', preg_replace('/\s+/', '', $data));
219
        if (\count($results) > 1) {
220
            foreach ($results as $k => $v) {
221
                $results[$k] = trim($v);
222
                if (empty($v)) {
223
                    unset($results[$k]);
224
                }
225
            }
226
            return $results;
227
        }
228
        return trim($results[0]);
229
    }
230
231
    /**
232
     * Send a mail if error or success backup database(s)
233
     */
234
    private function sendMail(): void
235
    {
236
        $smtpTransport = new Swift_SmtpTransport(env('MAIL_SMTP_HOST', 'localhost'), env('MAIL_SMTP_PORT', 25));
0 ignored issues
show
Bug introduced by
It seems like env('MAIL_SMTP_PORT', 25) can also be of type boolean and string; however, parameter $port of Swift_SmtpTransport::__construct() does only seem to accept integer, 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

236
        $smtpTransport = new Swift_SmtpTransport(env('MAIL_SMTP_HOST', 'localhost'), /** @scrutinizer ignore-type */ env('MAIL_SMTP_PORT', 25));
Loading history...
237
        $smtpTransport->setUsername(env('MAIL_SMTP_USER', ''))->setPassword(env('MAIL_SMTP_PASSWORD', ''));
238
        $mailer = new Swift_Mailer($smtpTransport);
239
        if (empty($this->errors)) {
240
            if ((bool)env('MAIL_SEND_ON_SUCCESS', false)) {
241
                $body = "<strong>{$this->getL10n()->translate('mail_db_backup_successfull')}</strong>";
242
                $message = (new Swift_Message($this->l10n->translate('mail_subject_on_success')))->setFrom(env('MAIL_FROM',
243
                    '[email protected]'),
244
                    env('MAIL_FROM_NAME', 'Website Mailer for Database Backup'))
245
                    ->setTo(env('MAIL_TO'),
246
                        env('MAIL_TO_NAME', 'Webmaster of my website'))
247
                    ->setBody($body)
248
                    ->setCharset('utf-8')
249
                    ->setContentType('text/html');
250
                $mailer->send($message);
251
            }
252
        } elseif ((bool)env('MAIL_SEND_ON_ERROR', false)) {
253
            $body = "<strong>{$this->getL10n()->translate('mail_db_backup_failed')}}:</strong><br><br><ul>";
254
            foreach ($this->errors as $error) {
255
                $body .= "<li>
256
                        <ul>
257
                            <li>{$this->getL10n()->translate('database')}: {$error['dbname']}</li>
258
                            <li>{$this->getL10n()->translate('error_code')}: {$error['error_code']}</li>
259
                            <li>{$this->getL10n()->translate('error_message')}: {$error['error_message']}</li>
260
                        </ul>
261
                       </li>";
262
            }
263
            $body .= '</ul>';
264
            $message = (new Swift_Message($this->getLocalDir()->translate('mail_subject_on_error')))->setFrom(env('MAIL_FROM',
265
                '[email protected]'),
266
                env('MAIL_FROM_NAME', 'Website Mailer for Database Backup'))
267
                ->setTo(env('MAIL_TO'),
268
                    env('MAIL_TO_NAME', 'Webmaster of my website'))
269
                ->setBody($body)
270
                ->setCharset('utf-8')
271
                ->setContentType('text/html');
272
            $mailer->send($message);
273
        }
274
    }
275
276
    /**
277
     * @throws FileNotFoundException
278
     */
279
    private function removeOldFilesByIntervalDays(): void
280
    {
281
        CliFormatter::output($this->l10n->translate('cleaning_files'), CliFormatter::COLOR_CYAN);
282
        $files = $this->getFs()->listContents();
283
        foreach ($files as $file) {
284
            $filetime = $this->getFs()->getTimestamp($file['path']);
285
            $daysInterval = (int)env('FILES_DAYS_HISTORY', 3);
286
            if ($filetime < strtotime("-{$daysInterval} days")) {
287
                $this->getFs()->delete($file['path']);
288
            }
289
        }
290
        CliFormatter::output($this->getL10n()->translate('cleaned_files_success'), CliFormatter::COLOR_GREEN);
291
    }
292
293
    /**
294
     * @return SplFileObject
295
     */
296
    private function createTempFile(): SplFileObject
297
    {
298
        $file = tmpfile();
299
        $name = stream_get_meta_data($file)['uri'];
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type false; however, parameter $stream of stream_get_meta_data() does only seem to accept resource, 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

299
        $name = stream_get_meta_data(/** @scrutinizer ignore-type */ $file)['uri'];
Loading history...
300
        return new SplFileObject($name, 'w+');
301
    }
302
303
    /**
304
     * @param \SplFileInfo $file
305
     * @return bool
306
     */
307
    protected function deleteTempFile(\SplFileInfo $file): bool
308
    {
309
        return unlink($file->getRealPath());
310
    }
311
312
    /**
313
     * @return string
314
     */
315
    public function getLocalDir(): string
316
    {
317
        return $this->localDir;
318
    }
319
320
    /**
321
     * @param string $localDir
322
     */
323
    public function setLocalDir(string $localDir): void
324
    {
325
        $this->localDir = $localDir;
326
    }
327
328
    /**
329
     * @return Dotenv
330
     */
331
    public function getEnv(): Dotenv
332
    {
333
        return $this->env;
334
    }
335
336
    /**
337
     * @param Dotenv $env
338
     */
339
    public function setEnv(Dotenv $env): void
340
    {
341
        $this->env = $env;
342
    }
343
344
    /**
345
     * @return array
346
     */
347
    public function getErrors(): array
348
    {
349
        return $this->errors;
350
    }
351
352
    /**
353
     * @param array $errors
354
     */
355
    public function setErrors(array $errors): void
356
    {
357
        $this->errors = $errors;
358
    }
359
360
    /**
361
     * @return Lang
362
     */
363
    public function getL10n(): Lang
364
    {
365
        return $this->l10n;
366
    }
367
368
    /**
369
     * @param Lang $l10n
370
     */
371
    public function setL10n(Lang $l10n): void
372
    {
373
        $this->l10n = $l10n;
374
    }
375
376
    /**
377
     * @return FilesystemInterface
378
     */
379
    public function getFs(): FilesystemInterface
380
    {
381
        return $this->fs;
382
    }
383
384
    /**
385
     * @param AdapterInterface|null $adapter
386
     * @param array $adapterOptions
387
     */
388
    public function setFs(?AdapterInterface $adapter, array $adapterOptions): void
389
    {
390
        $this->fs = new Filesystem($adapter, $adapterOptions);
0 ignored issues
show
Bug introduced by
It seems like $adapter can also be of type null; however, parameter $adapter of League\Flysystem\Filesystem::__construct() does only seem to accept League\Flysystem\AdapterInterface, 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

390
        $this->fs = new Filesystem(/** @scrutinizer ignore-type */ $adapter, $adapterOptions);
Loading history...
391
    }
392
}
393