Passed
Push — release-2.x ( c21d8d...204b60 )
by Slye
01:58
created

System::getInstance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
c 0
b 0
f 0
rs 10
cc 2
nc 2
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 NDC\DatabaseBackup\Exception\NotAllowedException;
18
use NDC\DatabaseBackup\Exception\UnsupportedPHPVersionException;
19
use PDO;
20
use RuntimeException;
21
use SplFileObject;
22
use Swift_Mailer;
23
use Swift_Message;
24
use Swift_SmtpTransport;
25
26
/**
27
 * Class System
28
 * @package NDC\DatabaseBackup
29
 */
30
class System
31
{
32
    /**
33
     * @var Dotenv
34
     */
35
    private $env;
36
    /**
37
     * @var array
38
     */
39
    private $errors = [];
40
    /**
41
     * @var Lang
42
     */
43
    private $l10n;
44
45
    /**
46
     * @var System|null
47
     */
48
    private static $_instance;
49
    /**
50
     * @var AdapterInterface
51
     */
52
    private $adapter;
53
    /**
54
     * @var string
55
     */
56
    private $localDir;
57
58
    /**
59
     * @param AdapterInterface $adapter
60
     * @param array $adapterOptions
61
     * @return System|null
62
     * @throws FileNotFoundException
63
     * @throws NotAllowedException
64
     * @throws UnsupportedPHPVersionException
65
     * @throws \JBZoo\Lang\Exception
66
     * @throws \JBZoo\Path\Exception
67
     */
68
    public static function getInstance(?AdapterInterface $adapter = null, array $adapterOptions = []): ?System
69
    {
70
71
        if (self::$_instance === null) {
72
            self::$_instance = new System($adapter, $adapterOptions);
73
        }
74
        return self::$_instance;
75
    }
76
77
    /**
78
     * System constructor.
79
     * @param AdapterInterface|null $adapter
80
     * @param array $adapterOptions
81
     * @throws FileNotFoundException
82
     * @throws \JBZoo\Lang\Exception
83
     * @throws \JBZoo\Path\Exception
84
     * @throws UnsupportedPHPVersionException
85
     * @throws NotAllowedException
86
     */
87
    private function __construct(?AdapterInterface $adapter, array $adapterOptions)
88
    {
89
        $this->localDir = dirname(__DIR__) . DS;
90
        $this->loadConfigurationEnvironment();
91
        $this->l10n = new Lang(env('LANGUAGE', 'en'));
92
        $this->l10n->load($this->localDir . 'i18n', null, 'yml');
93
        if (PHP_SAPI !== 'cli' && !(bool)env('ALLOW_EXECUTE_IN_WEB_BROWSER', false)) {
94
            throw new NotAllowedException($this->l10n->translate('unauthorized_browser'));
95
        }
96
        if ((PHP_MAJOR_VERSION . PHP_MINOR_VERSION) < 72) {
97
            throw new UnsupportedPHPVersionException($this->l10n->translate('unsupported_php_version'));
98
        }
99
        if ($adapter === null) {
100
            $adapter = new Local($this->localDir . env('DIRECTORY_TO_SAVE_LOCAL_BACKUP', 'MySQLBackups') . DS);
101
        }
102
        CliFormatter::output($this->l10n->translate('app_started'), CliFormatter::COLOR_BLUE);
103
        $this->adapter = new Filesystem($adapter, $adapterOptions);
0 ignored issues
show
Documentation Bug introduced by
It seems like new League\Flysystem\Fil...apter, $adapterOptions) of type League\Flysystem\Filesystem is incompatible with the declared type League\Flysystem\AdapterInterface of property $adapter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
104
        (int)env('FILES_DAYS_HISTORY', 3) > 0 ? $this->removeOldFilesByIntervalDays() : null;
105
    }
106
107
    /**
108
     * Start System initialization
109
     * @return void
110
     * @throws RuntimeException
111
     */
112
    private function loadConfigurationEnvironment(): void
113
    {
114
        if (!file_exists($this->localDir . '.env')) {
115
            throw new InvalidFileException('Please configure this script with .env file');
116
        }
117
        $this->env = Dotenv::create($this->localDir, '.env');
118
        $this->env->overload();
119
        $this->checkRequirements();
120
    }
121
122
    /**
123
     * @return Validator
124
     */
125
    private function checkRequirements(): Validator
126
    {
127
        return $this->env->required([
128
            'DB_HOST',
129
            'DB_USER',
130
            'DB_PASSWORD'
131
        ])->notEmpty();
132
    }
133
134
    /**
135
     * @return array|string
136
     */
137
    private function getExcludedDatabases()
138
    {
139
        if (empty(trim(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema')))) {
140
            return [];
141
        }
142
        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

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

183
            if (!\in_array($database->Database, /** @scrutinizer ignore-type */ $this->getExcludedDatabases(), true)) {
Loading history...
184
                $filename = $database->Database . '-' . date($this->l10n->translate('date_format')) . ".$ext";
185
                try {
186
                    $dumper = new Mysqldump('mysql:host=' . env('DB_HOST',
187
                            'localhost') . ';dbname=' . $database->Database . ';charset=UTF8',
188
                        env('DB_USER', 'root'), env('DB_PASSWORD', ''), $settings, [
189
                            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
190
                        ]);
191
                    $tempFile = $this->createTempFile();
192
                    $dumper->start($tempFile->getRealPath());
193
                    $this->adapter->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\AdapterInterface::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

193
                    $this->adapter->writeStream($filename, /** @scrutinizer ignore-type */ fopen($tempFile->getRealPath(), 'wb+'));
Loading history...
Bug introduced by
The call to League\Flysystem\AdapterInterface::writeStream() has too few arguments starting with config. ( Ignorable by Annotation )

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

193
                    $this->adapter->/** @scrutinizer ignore-call */ 
194
                                    writeStream($filename, fopen($tempFile->getRealPath(), 'wb+'));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
194
                    $this->deleteTempFile($tempFile);
195
                    CliFormatter::output($database->Database . ' ' . $this->l10n->translate('backuped_successfully'),
196
                        CliFormatter::COLOR_GREEN);
197
                } catch (Exception $e) {
198
                    CliFormatter::output('!! ERROR::' . $e->getMessage() . ' !!', CliFormatter::COLOR_RED);
199
                    $this->errors[] = [
200
                        'dbname' => $database->Database,
201
                        'error_message' => $e->getMessage(),
202
                        'error_code' => $e->getCode()
203
                    ];
204
                }
205
            }
206
        }
207
        $this->sendMail();
208
        CliFormatter::output($this->l10n->translate('databases_backup_successfull'), CliFormatter::COLOR_PURPLE);
209
    }
210
211
    /**
212
     * @param string $data
213
     * @return array|string
214
     */
215
    private function parseAndSanitize(string $data)
216
    {
217
        $results = explode(',', preg_replace('/\s+/', '', $data));
218
        if (\count($results) > 1) {
219
            foreach ($results as $k => $v) {
220
                $results[$k] = trim($v);
221
                if (empty($v)) {
222
                    unset($results[$k]);
223
                }
224
            }
225
            return $results;
226
        }
227
        return trim($results[0]);
228
    }
229
230
    /**
231
     * Send a mail if error or success backup database(s)
232
     */
233
    private function sendMail(): void
234
    {
235
        $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

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

298
        $name = stream_get_meta_data(/** @scrutinizer ignore-type */ $file)['uri'];
Loading history...
299
        return new SplFileObject($name, 'w+');
300
    }
301
302
    /**
303
     * @param \SplFileInfo $file
304
     * @return bool
305
     */
306
    protected function deleteTempFile(\SplFileInfo $file): bool
307
    {
308
        return unlink($file->getRealPath());
309
    }
310
}
311