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 PDO; |
14
|
|
|
use RuntimeException; |
15
|
|
|
use Swift_Attachment; |
16
|
|
|
use Swift_Mailer; |
17
|
|
|
use Swift_Message; |
18
|
|
|
use Swift_SmtpTransport; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Class System |
22
|
|
|
* @package NDC\DatabaseBackup |
23
|
|
|
*/ |
24
|
|
|
class System |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* @var Dotenv |
28
|
|
|
*/ |
29
|
|
|
private $env; |
30
|
|
|
/** |
31
|
|
|
* @var array |
32
|
|
|
*/ |
33
|
|
|
protected $errors = []; |
34
|
|
|
/** |
35
|
|
|
* @var bool |
36
|
|
|
*/ |
37
|
|
|
private $isCli; |
38
|
|
|
/** |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $files = []; |
42
|
|
|
/** |
43
|
|
|
* @var Lang |
44
|
|
|
*/ |
45
|
|
|
protected $l10n; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var System|null |
49
|
|
|
*/ |
50
|
|
|
private static $_instance; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @return System|null |
54
|
|
|
*/ |
55
|
|
|
public static function getInstance(): ?System |
56
|
|
|
{ |
57
|
|
|
|
58
|
|
|
if (self::$_instance === null) { |
59
|
|
|
self::$_instance = new System(); |
60
|
|
|
} |
61
|
|
|
return self::$_instance; |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* System constructor. |
66
|
|
|
*/ |
67
|
|
|
private function __construct() |
68
|
|
|
{ |
69
|
|
|
$this->isCli = PHP_SAPI === 'cli'; |
70
|
|
|
try { |
71
|
|
|
$this->loadConfigurationEnvironment(); |
72
|
|
|
} catch (\JBZoo\Lang\Exception $e) { |
73
|
|
|
throw new RuntimeException($e); |
74
|
|
|
} catch (\JBZoo\Path\Exception $e) { |
75
|
|
|
throw new RuntimeException($e); |
76
|
|
|
} |
77
|
|
|
FileManager::getInstance(); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Start System initialization |
82
|
|
|
* @return void |
83
|
|
|
* @throws RuntimeException |
84
|
|
|
* @throws \JBZoo\Lang\Exception |
85
|
|
|
* @throws \JBZoo\Path\Exception |
86
|
|
|
*/ |
87
|
|
|
private function loadConfigurationEnvironment(): void |
88
|
|
|
{ |
89
|
|
|
if (getenv('APP_TEST') !== '1') { |
90
|
|
|
if (!file_exists(dirname(__DIR__) . DIRECTORY_SEPARATOR . '.env')) { |
91
|
|
|
throw new RuntimeException('Please configure this script with .env file'); |
92
|
|
|
} |
93
|
|
|
$this->env = Dotenv::create(dirname(__DIR__), '.env'); |
94
|
|
|
$this->env->overload(); |
95
|
|
|
$this->checkRequirements(); |
96
|
|
|
} |
97
|
|
|
$this->l10n = new Lang(env('LANGUAGE', 'en')); |
98
|
|
|
$this->l10n->load(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'i18n', null, 'yml'); |
99
|
|
|
if (!$this->isCli && !(bool)env('ALLOW_EXECUTE_IN_WEB_BROWSER', false)) { |
100
|
|
|
die($this->l10n->translate('unauthorized_browser')); |
|
|
|
|
101
|
|
|
} |
102
|
|
|
if ((PHP_MAJOR_VERSION . PHP_MINOR_VERSION) < 72) { |
103
|
|
|
die($this->l10n->translate('unsupported_php_version')); |
|
|
|
|
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @return Validator |
109
|
|
|
*/ |
110
|
|
|
private function checkRequirements(): Validator |
111
|
|
|
{ |
112
|
|
|
return $this->env->required([ |
113
|
|
|
'DB_HOST', |
114
|
|
|
'DB_USER', |
115
|
|
|
'DB_PASSWORD', |
116
|
|
|
'MAIL_FROM', |
117
|
|
|
'MAIL_FROM_NAME', |
118
|
|
|
'MAIL_TO', |
119
|
|
|
'MAIL_TO_NAME', |
120
|
|
|
'MAIL_SEND_ON_ERROR', |
121
|
|
|
'MAIL_SEND_ON_SUCCESS', |
122
|
|
|
'MAIL_SMTP_HOST', |
123
|
|
|
'MAIL_SMTP_PORT', |
124
|
|
|
'FILES_DAYS_HISTORY', |
125
|
|
|
'FILES_PATH_TO_SAVE_BACKUP', |
126
|
|
|
'LANGUAGE', |
127
|
|
|
'ALLOW_EXECUTE_IN_WEB_BROWSER' |
128
|
|
|
])->notEmpty(); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @return array|string |
133
|
|
|
*/ |
134
|
|
|
private function getExcludedDatabases() |
135
|
|
|
{ |
136
|
|
|
if (empty(trim(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema')))) { |
137
|
|
|
return []; |
138
|
|
|
} |
139
|
|
|
return $this->parseAndSanitize(env('DB_EXCLUDE_DATABASES', 'information_schema,mysql,performance_schema')); |
|
|
|
|
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @return array |
144
|
|
|
*/ |
145
|
|
|
private function getDatabases(): array |
146
|
|
|
{ |
147
|
|
|
$pdo = new PDO('mysql:host=' . env('DB_HOST', 'localhost') . ';charset=UTF8', env('DB_USER', 'root'), |
148
|
|
|
env('DB_PASSWORD', 'root'), [ |
149
|
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, |
150
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION |
151
|
|
|
]); |
152
|
|
|
return $pdo->query('SHOW DATABASES')->fetchAll(); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Process to backup databases |
157
|
|
|
*/ |
158
|
|
|
public function processBackup(): void |
159
|
|
|
{ |
160
|
|
|
foreach ($this->getDatabases() as $database) { |
161
|
|
|
if (!\in_array($database->Database, $this->getExcludedDatabases(), true)) { |
|
|
|
|
162
|
|
|
$file_format = $database->Database . '-' . date($this->l10n->translate('date_format')) . '.sql'; |
163
|
|
|
try { |
164
|
|
|
$dumper = new Mysqldump('mysql:host=' . env('DB_HOST', |
165
|
|
|
'localhost') . ';dbname=' . $database->Database . ';charset=UTF8', |
166
|
|
|
env('DB_USER', 'root'), env('DB_PASSWORD', '')); |
167
|
|
|
$dumper->start(env('FILES_PATH_TO_SAVE_BACKUP', './Backups') . DIRECTORY_SEPARATOR . $file_format); |
168
|
|
|
$this->files[] = env('FILES_PATH_TO_SAVE_BACKUP', './Backups') . DIRECTORY_SEPARATOR . $file_format; |
169
|
|
|
} catch (Exception $e) { |
170
|
|
|
$this->errors[] = [ |
171
|
|
|
'dbname' => $database->Database, |
172
|
|
|
'error_message' => $e->getMessage(), |
173
|
|
|
'error_code' => $e->getCode() |
174
|
|
|
]; |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
$this->sendMail(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @param string $data |
183
|
|
|
* @return array|string |
184
|
|
|
*/ |
185
|
|
|
private function parseAndSanitize(string $data) |
186
|
|
|
{ |
187
|
|
|
$results = explode(',', $data); |
188
|
|
|
if (\count($results) > 1) { |
189
|
|
|
foreach ($results as $k => $v) { |
190
|
|
|
$results[$k] = trim($v); |
191
|
|
|
if (empty($v)) { |
192
|
|
|
unset($results[$k]); |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
return $results; |
196
|
|
|
} |
197
|
|
|
return trim($results[0]); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Send a mail if error or success backup database |
202
|
|
|
*/ |
203
|
|
|
private function sendMail(): void |
204
|
|
|
{ |
205
|
|
|
$smtpTransport = new Swift_SmtpTransport(env('MAIL_SMTP_HOST', 'localhost'), env('MAIL_SMTP_PORT', 25)); |
|
|
|
|
206
|
|
|
$smtpTransport->setUsername(env('MAIL_SMTP_USER', ''))->setPassword(env('MAIL_SMTP_PASSWORD', '')); |
207
|
|
|
$mailer = new Swift_Mailer($smtpTransport); |
208
|
|
|
if (empty($this->errors)) { |
209
|
|
|
if ((bool)env('MAIL_SEND_ON_SUCCESS', false)) { |
210
|
|
|
$body = "<strong>{$this->l10n->translate('mail_db_backup_successfull')}</strong>"; |
211
|
|
|
if ((bool)env('MAIL_SEND_WITH_BACKUP_FILE', false)) { |
212
|
|
|
$body .= "<br><br>{$this->l10n->translate('mail_db_backup_file')}"; |
213
|
|
|
} |
214
|
|
|
$message = (new Swift_Message($this->l10n->translate('mail_subject_on_success')))->setFrom(env('MAIL_FROM', |
215
|
|
|
'[email protected]'), |
216
|
|
|
env('MAIL_FROM_NAME', 'Website Mailer for Database Backup')) |
217
|
|
|
->setTo(env('MAIL_TO'), |
218
|
|
|
env('MAIL_TO_NAME', 'Webmaster of my website')) |
219
|
|
|
->setBody($body) |
220
|
|
|
->setCharset('utf-8') |
221
|
|
|
->setContentType('text/html'); |
222
|
|
|
if ((bool)env('MAIL_SEND_WITH_BACKUP_FILE', false)) { |
223
|
|
|
foreach ($this->files as $file) { |
224
|
|
|
$attachment = Swift_Attachment::fromPath($file)->setContentType('application/sql'); |
225
|
|
|
$message->attach($attachment); |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
$mailer->send($message); |
229
|
|
|
} |
230
|
|
|
} elseif ((bool)env('MAIL_SEND_ON_ERROR', false)) { |
231
|
|
|
$body = "<strong>{$this->l10n->translate('mail_db_backup_failed')}}:</strong><br><br><ul>"; |
232
|
|
|
foreach ($this->errors as $error) { |
233
|
|
|
$body .= "<li> |
234
|
|
|
<ul> |
235
|
|
|
<li>{$this->l10n->translate('database')}: {$error['dbname']}</li> |
236
|
|
|
<li>{$this->l10n->translate('error_code')}: {$error['error_code']}</li> |
237
|
|
|
<li>{$this->l10n->translate('error_message')}: {$error['error_message']}</li> |
238
|
|
|
</ul> |
239
|
|
|
</li>"; |
240
|
|
|
} |
241
|
|
|
$body .= '</ul>'; |
242
|
|
|
$message = (new Swift_Message($this->l10n->translate('mail_subject_on_error')))->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
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.