NuBOXDevCom /
Database-Backup
| 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
|
|||||||||
| 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
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
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
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
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
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
Loading history...
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
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
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
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
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
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 |
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..