Passed
Push — release-2.x ( 5f707c...22f354 )
by Slye
01:51
created

System::checkRequirements()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 17
nc 1
nop 0
dl 0
loc 19
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
namespace NDC\DatabaseBackup;
4
5
use const DIRECTORY_SEPARATOR;
6
use function dirname;
7
use Dotenv\{
8
    Dotenv, Validator
9
};
10
use Exception;
11
use Ifsnop\Mysqldump\Mysqldump;
12
use JBZoo\Lang\Lang;
13
use League\Flysystem\{
14
    Adapter\Local,
15
    AdapterInterface,
16
    Filesystem
17
};
18
use PDO;
19
use \{
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '{', expecting T_STRING on line 19 at column 5
Loading history...
20
    RuntimeException,
21
    Swift_Attachment,
22
    Swift_Mailer,
23
    Swift_Message,
24
    Swift_SmtpTransport
25
};
26
27
/**
28
 * Class System
29
 * @package NDC\DatabaseBackup
30
 */
31
class System
32
{
33
    /**
34
     * @var Dotenv
35
     */
36
    private $env;
37
    /**
38
     * @var array
39
     */
40
    private $errors = [];
41
    /**
42
     * @var bool
43
     */
44
    private $isCli;
45
    /**
46
     * @var array
47
     */
48
    private $files = [];
49
    /**
50
     * @var Lang
51
     */
52
    private $l10n;
53
54
    /**
55
     * @var System|null
56
     */
57
    private static $_instance;
58
    /**
59
     * @var AdapterInterface
60
     */
61
    private $adapter;
62
63
    /**
64
     * @param AdapterInterface $adapter
65
     * @param array $adapterOptions
66
     * @return System|null
67
     * @throws \League\Flysystem\FileNotFoundException
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 $adapter
81
     * @param $adapterOptions
82
     * @throws \League\Flysystem\FileNotFoundException
83
     */
84
    private function __construct(?AdapterInterface $adapter, array $adapterOptions)
85
    {
86
        $this->isCli = PHP_SAPI === 'cli';
87
        try {
88
            $this->loadConfigurationEnvironment();
89
        } catch (\JBZoo\Lang\Exception $e) {
90
            throw new RuntimeException($e);
91
        } catch (\JBZoo\Path\Exception $e) {
92
            throw new RuntimeException($e);
93
        }
94
        if ($adapter === null) {
95
            $adapter = new Local(env('FILES_PATH_TO_SAVE_BACKUP'));
96
        }
97
        $this->adapter = new Filesystem($adapter, $adapterOptions);
98
        env('FILES_DAYS_HISTORY', 3) > 0 ?: $this->removeOldFilesByIntervalDays();
99
    }
100
101
    /**
102
     * Start System initialization
103
     * @return void
104
     * @throws RuntimeException
105
     * @throws \JBZoo\Lang\Exception
106
     * @throws \JBZoo\Path\Exception
107
     */
108
    public function loadConfigurationEnvironment(): void
109
    {
110
        if (!file_exists(dirname(__DIR__) . DIRECTORY_SEPARATOR . '.env')) {
111
            throw new RuntimeException('Please configure this script with .env file');
112
        }
113
        $this->env = Dotenv::create(dirname(__DIR__), '.env');
114
        $this->env->overload();
115
        $this->checkRequirements();
116
        $this->l10n = new Lang(env('LANGUAGE', 'en'));
117
        $this->l10n->load(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'i18n', null, 'yml');
118
        if (!$this->isCli && !(bool)env('ALLOW_EXECUTE_IN_WEB_BROWSER', false)) {
119
            die($this->l10n->translate('unauthorized_browser'));
120
        }
121
        if ((PHP_MAJOR_VERSION . PHP_MINOR_VERSION) < 72) {
122
            die($this->l10n->translate('unsupported_php_version'));
123
        }
124
    }
125
126
    /**
127
     * @return Validator
128
     */
129
    private function checkRequirements(): Validator
130
    {
131
        return $this->env->required([
132
            'DB_HOST',
133
            'DB_USER',
134
            'DB_PASSWORD'
135
        ])->notEmpty();
136
    }
137
138
    /**
139
     * @return array|string
140
     */
141
    private function getExcludedDatabases()
142
    {
143
        if (empty(trim(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema')))) {
144
            return [];
145
        }
146
        return $this->parseAndSanitize(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema'));
147
    }
148
149
    /**
150
     * @return array
151
     */
152
    private function getDatabases(): array
153
    {
154
        $pdo = new PDO('mysql:host=' . env('DB_HOST', 'localhost') . ';charset=UTF8', env('DB_USER', 'root'),
155
            env('DB_PASSWORD', 'root'), [
156
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
157
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
158
            ]);
159
        return $pdo->query('SHOW DATABASES')->fetchAll();
160
    }
161
162
    /**
163
     * Process to backup databases
164
     */
165
    public function processBackup(): void
166
    {
167
        foreach ($this->getDatabases() as $database) {
168
            if (!\in_array($database->Database, $this->getExcludedDatabases(), true)) {
169
                $file_format = $database->Database . '-' . date($this->l10n->translate('date_format')) . '.sql';
170
                try {
171
                    $dumper = new Mysqldump('mysql:host=' . env('DB_HOST',
172
                            'localhost') . ';dbname=' . $database->Database . ';charset=UTF8',
173
                        env('DB_USER', 'root'), env('DB_PASSWORD', ''));
174
                    $dumper->start('tmp' . DIRECTORY_SEPARATOR . $file_format);
175
                    $this->adapter->copy(env('DIRECTORY_TO_SAVE_BACKUP',
176
                            'MySQLBackups') . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . $file_format,
177
                        env('DIRECTORY_TO_SAVE_BACKUP', 'MySQLBackups'));
178
                    $this->adapter->delete(env('DIRECTORY_TO_SAVE_BACKUP',
179
                            'MySQLBackups') . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . $file_format);
180
                } catch (Exception $e) {
181
                    $this->errors[] = [
182
                        'dbname' => $database->Database,
183
                        'error_message' => $e->getMessage(),
184
                        'error_code' => $e->getCode()
185
                    ];
186
                }
187
            }
188
        }
189
        $this->sendMail();
190
    }
191
192
    /**
193
     * @param string $data
194
     * @return array|string
195
     */
196
    private function parseAndSanitize(string $data)
197
    {
198
        $results = explode(',', preg_replace('/\s+/', '', $data));
199
        if (\count($results) > 1) {
200
            foreach ($results as $k => $v) {
201
                $results[$k] = trim($v);
202
                if (empty($v)) {
203
                    unset($results[$k]);
204
                }
205
            }
206
            return $results;
207
        }
208
        return trim($results[0]);
209
    }
210
211
    /**
212
     * Send a mail if error or success backup database(s)
213
     */
214
    private function sendMail(): void
215
    {
216
        $smtpTransport = new Swift_SmtpTransport(env('MAIL_SMTP_HOST', 'localhost'), env('MAIL_SMTP_PORT', 25));
217
        $smtpTransport->setUsername(env('MAIL_SMTP_USER', ''))->setPassword(env('MAIL_SMTP_PASSWORD', ''));
218
        $mailer = new Swift_Mailer($smtpTransport);
219
        if (empty($this->errors)) {
220
            if ((bool)env('MAIL_SEND_ON_SUCCESS', false)) {
221
                $body = "<strong>{$this->l10n->translate('mail_db_backup_successfull')}</strong>";
222
                if ((bool)env('MAIL_SEND_WITH_BACKUP_FILE', false)) {
223
                    $body .= "<br><br>{$this->l10n->translate('mail_db_backup_file')}";
224
                }
225
                $message = (new Swift_Message($this->l10n->translate('mail_subject_on_success')))->setFrom(env('MAIL_FROM',
226
                    '[email protected]'),
227
                    env('MAIL_FROM_NAME', 'Website Mailer for Database Backup'))
228
                    ->setTo(env('MAIL_TO'),
229
                        env('MAIL_TO_NAME', 'Webmaster of my website'))
230
                    ->setBody($body)
231
                    ->setCharset('utf-8')
232
                    ->setContentType('text/html');
233
                if ((bool)env('MAIL_SEND_WITH_BACKUP_FILE', false)) {
234
                    foreach ($this->adapter->listFiles() as $file) {
235
                        $attachment = Swift_Attachment::fromPath($file)->setContentType('application/sql');
236
                        $message->attach($attachment);
237
                    }
238
                }
239
                $mailer->send($message);
240
            }
241
        } elseif ((bool)env('MAIL_SEND_ON_ERROR', false)) {
242
            $body = "<strong>{$this->l10n->translate('mail_db_backup_failed')}}:</strong><br><br><ul>";
243
            foreach ($this->errors as $error) {
244
                $body .= "<li>
245
                        <ul>
246
                            <li>{$this->l10n->translate('database')}: {$error['dbname']}</li>
247
                            <li>{$this->l10n->translate('error_code')}: {$error['error_code']}</li>
248
                            <li>{$this->l10n->translate('error_message')}: {$error['error_message']}</li>
249
                        </ul>
250
                       </li>";
251
            }
252
            $body .= '</ul>';
253
            $message = (new Swift_Message($this->l10n->translate('mail_subject_on_error')))->setFrom(env('MAIL_FROM',
254
                '[email protected]'),
255
                env('MAIL_FROM_NAME', 'Website Mailer for Database Backup'))
256
                ->setTo(env('MAIL_TO'),
257
                    env('MAIL_TO_NAME', 'Webmaster of my website'))
258
                ->setBody($body)
259
                ->setCharset('utf-8')
260
                ->setContentType('text/html');
261
            $mailer->send($message);
262
        }
263
    }
264
265
    /**
266
     * @throws \League\Flysystem\FileNotFoundException
267
     */
268
    private function removeOldFilesByIntervalDays(): void
269
    {
270
        $files = $this->adapter->listContents();
271
        foreach ($files as $file) {
272
            $absoluteFile = $file['path'] . DIRECTORY_SEPARATOR . $file['basename'];
273
            $filetime = $this->adapter->getTimestamp($absoluteFile);
274
            if ($filetime < strtotime("-{$this->params['days_interval']} days")) {
275
                $this->adapter->delete($absoluteFile);
276
            }
277
        }
278
    }
279
}
280