local.php
948 lines
| 1 | <?php |
| 2 | |
| 3 | // Namespace |
| 4 | namespace BMI\Plugin\Staging; |
| 5 | |
| 6 | // Use |
| 7 | use BMI\Plugin\Backup_Migration_Plugin as BMP; |
| 8 | use BMI\Plugin\Checker\BMI_Checker as Checker; |
| 9 | use BMI\Plugin\Database\BMI_Search_Replace_Engine as BMISearchReplace; |
| 10 | |
| 11 | // Exit on direct access |
| 12 | if (!defined('ABSPATH')) exit; |
| 13 | |
| 14 | // Require controller |
| 15 | require_once BMI_INCLUDES . '/staging/controller.php'; |
| 16 | |
| 17 | /** |
| 18 | * Subclass of main staging controller (local handler) |
| 19 | */ |
| 20 | class BMI_StagingLocal extends BMI_Staging { |
| 21 | |
| 22 | protected $data = []; |
| 23 | protected $step = 0; |
| 24 | private $filesList = null; |
| 25 | private $dirsList = null; |
| 26 | private $rootLength = 0; |
| 27 | private $excludedDirectories = []; |
| 28 | |
| 29 | public function __construct($name, $initialize = false) { |
| 30 | |
| 31 | parent::__construct(...func_get_args()); |
| 32 | if ($initialize) $this->initialization(); |
| 33 | |
| 34 | } |
| 35 | |
| 36 | public function continue() { |
| 37 | |
| 38 | if (isset($this->siteConfig['step'])) { |
| 39 | $this->step = intval($this->siteConfig['step']); |
| 40 | } else { |
| 41 | $this->step = null; |
| 42 | } |
| 43 | |
| 44 | if (!is_numeric($this->step)) { |
| 45 | |
| 46 | // End with error |
| 47 | $this->log(__('Step code was not provided for the request, prevents continuation of the process...'), 'ERROR'); |
| 48 | $this->log('Step code was not provided for the request, prevents continuation of the process...', 'VERBOSE'); |
| 49 | $this->log('#201', 'END-CODE'); |
| 50 | return ['status' => 'error']; |
| 51 | |
| 52 | } |
| 53 | |
| 54 | // Default error |
| 55 | $translatedMainError = __('Something unexpected happened, we need to abort the process.', 'backup-backup'); |
| 56 | $englishMainError = 'Something unexpected happened, we need to abort the process (#207).'; |
| 57 | |
| 58 | // Step controller |
| 59 | if ($this->step == 1) $this->prepareFilesAndDatabase(); |
| 60 | else if ($this->step == 2) $this->duplicateDatabase(); |
| 61 | else if ($this->step == 3) $this->searchReplace(); |
| 62 | else if ($this->step == 4) $this->databaseFinishedCopyFiles(); |
| 63 | else if ($this->step == 5) $this->setupWpConfigAndLoginScript(); |
| 64 | else if ($this->step == 6) $this->performFinish(); |
| 65 | else if (isset($this->step) && is_numeric($this->step)) $this->sendSuccess(); |
| 66 | else $this->returnError($translatedMainError, $englishMainError); |
| 67 | |
| 68 | } |
| 69 | |
| 70 | private function getAllLiveTables() { |
| 71 | |
| 72 | global $wpdb; |
| 73 | $tables = []; |
| 74 | |
| 75 | $allTables = $wpdb->get_results('SHOW TABLES'); |
| 76 | |
| 77 | foreach ($allTables as $table) { |
| 78 | foreach ($table as $name) $tables[] = $name; |
| 79 | } |
| 80 | |
| 81 | return $tables; |
| 82 | |
| 83 | } |
| 84 | |
| 85 | private function checkIfPrefixCanBeUsed($tables, $prefix) { |
| 86 | |
| 87 | $sizeOfPrefix = strlen($prefix); |
| 88 | |
| 89 | for ($i = 0; $i < sizeof($tables); ++$i) { |
| 90 | $name = $tables[$i]; |
| 91 | |
| 92 | if (substr($name, 0, $sizeOfPrefix) == $prefix) return false; |
| 93 | if (strpos($name, $prefix)) return false; |
| 94 | } |
| 95 | |
| 96 | return $prefix; |
| 97 | |
| 98 | } |
| 99 | |
| 100 | private function generateUniquePrefix() { |
| 101 | return 'b' . date('m') . 'mi' . date('d') . '_stg' . substr(time(), -5) . '_'; |
| 102 | } |
| 103 | |
| 104 | private function generateDatabasePrefix() { |
| 105 | |
| 106 | $tables = $this->getAllLiveTables(); |
| 107 | $prefix = $this->generateUniquePrefix(); |
| 108 | |
| 109 | $i = 0; |
| 110 | |
| 111 | while ($this->checkIfPrefixCanBeUsed($tables, $prefix) === false && $i <= 3) { |
| 112 | sleep(1); |
| 113 | $prefix = $this->generateUniquePrefix(); |
| 114 | |
| 115 | $i++; |
| 116 | } |
| 117 | |
| 118 | if ($i >= 3) return 'error'; |
| 119 | return $prefix; |
| 120 | |
| 121 | } |
| 122 | |
| 123 | private function createDatabasePrefixAndInitDir() { |
| 124 | |
| 125 | $path = trailingslashit(ABSPATH) . $this->name; |
| 126 | |
| 127 | $this->log(__('Preparing constants and staging details', 'backup-backup'), 'STEP'); |
| 128 | |
| 129 | // Directory creation |
| 130 | $this->log(__('Creating root directory of new site', 'backup-backup')); |
| 131 | if (file_exists($path) && is_dir($path)) { |
| 132 | $translated = __('Seems like desired directory of staging site already exist, try with different staging site name.', 'backup-backup'); |
| 133 | $english = 'Seems like desired directory of staging site already exist, try with different staging site name.'; |
| 134 | $this->returnError($translated, $english); |
| 135 | } |
| 136 | |
| 137 | @mkdir($path, 0755); |
| 138 | touch($path . DIRECTORY_SEPARATOR . '.bmi_staging'); |
| 139 | |
| 140 | $this->log(__('Path of new website:', 'backup-backup') . ' ' . $path, 'SUCCESS'); |
| 141 | |
| 142 | // Generation of database prefix |
| 143 | $this->log(__('Generating database prefix', 'backup-backup')); |
| 144 | $dbPrefix = $this->generateDatabasePrefix(); |
| 145 | if ($dbPrefix == 'error') { |
| 146 | $translated = __('There was an error during database prefix generation, maybe all generated were already used, try again.', 'backup-backup'); |
| 147 | $english = 'There was an error during database prefix generation, maybe all generated were already used, try again.'; |
| 148 | $this->returnError($translated, $english); |
| 149 | } |
| 150 | $this->log(__('Prefix of new site will be:', 'backup-backup') . ' ' . $dbPrefix, 'SUCCESS'); |
| 151 | |
| 152 | $this->config[$this->name]['name'] = $this->name; |
| 153 | $this->config[$this->name]['prefix'] = $dbPrefix; |
| 154 | |
| 155 | // Password for first log in |
| 156 | $this->log(__('Generating first log in password', 'backup-backup')); |
| 157 | $password = $this->getRandomPassword(); |
| 158 | $this->log(__('Password created and saved.', 'backup-backup'), 'SUCCESS'); |
| 159 | |
| 160 | // Set initial configuration |
| 161 | $this->log(__('Initializing configuration of the website', 'backup-backup'), 'STEP'); |
| 162 | $this->initialConfiguration($dbPrefix, $password); |
| 163 | $this->log(__('Configuration created', 'backup-backup'), 'SUCCESS'); |
| 164 | |
| 165 | } |
| 166 | |
| 167 | private function initialConfiguration($dbPrefix, $password) { |
| 168 | |
| 169 | global $table_prefix; |
| 170 | |
| 171 | $ip = $this->getIpAddress(); |
| 172 | |
| 173 | // Basic for all |
| 174 | $this->siteConfig['name'] = $this->name; // Name of that staging site |
| 175 | $this->siteConfig['url'] = home_url($this->name); // URL to the website |
| 176 | $this->siteConfig['root_source'] = trailingslashit(ABSPATH); // ABSPATH of source website |
| 177 | $this->siteConfig['root_staging'] = untrailingslashit(ABSPATH) . DIRECTORY_SEPARATOR . $this->name; // ABSPATH of staging site |
| 178 | $this->siteConfig['db_prefix'] = $dbPrefix; // Database prefix of that site |
| 179 | $this->siteConfig['creation_date'] = time(); // Creation date and time of that staging site |
| 180 | $this->siteConfig['password'] = $password; // Password for password-less login |
| 181 | $this->siteConfig['creator_ip'] = $ip; // IP of user who created it |
| 182 | $this->siteConfig['login_ip'] = $ip; // IP for autologin script (limit password-less login to only that IP) |
| 183 | $this->siteConfig['login_user_id'] = get_current_user_id(); // User ID for password less authentication |
| 184 | $this->siteConfig['source_home_url'] = home_url(); // Homepage URL of source website |
| 185 | $this->siteConfig['source_site_url'] = site_url(); // Website (admin) URL of source website |
| 186 | $this->siteConfig['source_db_prefix'] = $table_prefix; // Database prefix of source website |
| 187 | |
| 188 | // TasteWP |
| 189 | $this->siteConfig['communication_secret'] = 'local'; // Local if it's not TasteWP website // Secret code to get authless access to website |
| 190 | $this->siteConfig['expiration_time'] = 'never'; // Never if it's not TasteWP website // Expiration time of the website |
| 191 | |
| 192 | } |
| 193 | |
| 194 | private function getExcludedFilesAndDirectories() { |
| 195 | |
| 196 | $excludedDirectories = $this->getAllStagingSiteDirectories(); |
| 197 | $excludedDirectories[] = '.'; |
| 198 | $excludedDirectories[] = '..'; |
| 199 | $excludedDirectories[] = 'wp-config.php'; |
| 200 | $excludedDirectories[] = '.DS_Store'; |
| 201 | $excludedDirectories[] = '.quarantine'; |
| 202 | $excludedDirectories[] = '.git'; |
| 203 | $excludedDirectories[] = '.tmb'; |
| 204 | $excludedDirectories[] = 'node_modules'; |
| 205 | $excludedDirectories[] = 'debug.log'; |
| 206 | |
| 207 | return $excludedDirectories; |
| 208 | |
| 209 | } |
| 210 | |
| 211 | private function getAbspathFiles($path) { |
| 212 | |
| 213 | $excludedDirectories = $this->getExcludedFilesAndDirectories(); |
| 214 | $topFiles = array_diff(scandir($path), $excludedDirectories); |
| 215 | |
| 216 | return $topFiles; |
| 217 | |
| 218 | } |
| 219 | |
| 220 | private function processFiles($prefixPath, &$files) { |
| 221 | |
| 222 | $dirs = []; |
| 223 | |
| 224 | foreach ($files as $index => $file) { |
| 225 | $path = $prefixPath . $file; |
| 226 | if (strpos($path, BMI_BACKUPS_ROOT) !== false) continue; |
| 227 | if (is_link($path)) continue; |
| 228 | if (is_dir($path)) { |
| 229 | if (!(is_readable($path) && is_writable($path))) continue; |
| 230 | if (file_exists(trailingslashit($path) . '.bmi_staging')) continue; |
| 231 | $this->siteConfig['total_size'] += 4096; |
| 232 | $this->siteConfig['total_directories'] += 1; |
| 233 | fwrite($this->dirsList, $file . "\n"); |
| 234 | $dirs[] = $path; |
| 235 | } else if (is_file($path)) { |
| 236 | if (!is_readable($path)) continue; |
| 237 | $this->siteConfig['total_size'] += filesize($path); |
| 238 | $this->siteConfig['total_files'] += 1; |
| 239 | fwrite($this->filesList, $file . "\n"); |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | return $dirs; |
| 244 | |
| 245 | } |
| 246 | |
| 247 | private function processDirectoryRecursively($dir) { |
| 248 | |
| 249 | if (!(is_readable($dir) && is_writable($dir))) return; |
| 250 | |
| 251 | $files = array_diff(scandir($dir), $this->excludedDirectories); |
| 252 | |
| 253 | foreach ($files as $key => $value) { |
| 254 | $path = $dir . DIRECTORY_SEPARATOR . $value; |
| 255 | if (strpos($path, BMI_BACKUPS_ROOT) !== false) continue; |
| 256 | if (is_link($path)) continue; |
| 257 | if (is_dir($path)) { |
| 258 | $this->siteConfig['total_size'] += 4096; |
| 259 | $this->siteConfig['total_directories'] += 1; |
| 260 | fwrite($this->dirsList, substr($path, $this->rootLength) . "\n"); |
| 261 | if (!file_exists(trailingslashit($path) . '.bmi_staging')) { |
| 262 | $this->processDirectoryRecursively($path); |
| 263 | } |
| 264 | } else if (is_file($path)) { |
| 265 | if (!is_readable($path)) continue; |
| 266 | $this->siteConfig['total_size'] += filesize($path); |
| 267 | $this->siteConfig['total_files'] += 1; |
| 268 | fwrite($this->filesList, substr($path, $this->rootLength) . "\n"); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | } |
| 273 | |
| 274 | private function processFilesRecursively($leftDirs) { |
| 275 | |
| 276 | $this->rootLength = strlen(trailingslashit(ABSPATH)); |
| 277 | $this->excludedDirectories = $this->getExcludedFilesAndDirectories(); |
| 278 | |
| 279 | for ($i = 0; $i < sizeof($leftDirs); ++$i) { |
| 280 | $this->processDirectoryRecursively($leftDirs[$i]); |
| 281 | } |
| 282 | |
| 283 | } |
| 284 | |
| 285 | private function getTablesForDuplication() { |
| 286 | |
| 287 | global $wpdb, $table_prefix; |
| 288 | |
| 289 | $currentPrefixes = $this->getAllStagingSitePrefixes(); |
| 290 | $relatedTables = []; |
| 291 | |
| 292 | $sql = "SELECT (DATA_LENGTH + INDEX_LENGTH) as `size`, TABLE_NAME AS `name` FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s;"; |
| 293 | $sql = $wpdb->prepare($sql, array(DB_NAME)); |
| 294 | |
| 295 | $tables = $wpdb->get_results($sql); |
| 296 | foreach ($tables as $tableObject) { |
| 297 | |
| 298 | $name = $tableObject->name; |
| 299 | $size = $tableObject->size; |
| 300 | |
| 301 | if (substr($name, 0, strlen($table_prefix)) != $table_prefix) continue; |
| 302 | |
| 303 | $tableOfStagingSite = false; |
| 304 | for ($i = 0; $i < sizeof($currentPrefixes); ++$i) { |
| 305 | $subPrefix = $currentPrefixes[$i]; |
| 306 | if (substr($name, 0, strlen($subPrefix)) == $subPrefix) { |
| 307 | $tableOfStagingSite = true; |
| 308 | break; |
| 309 | } |
| 310 | } |
| 311 | if ($tableOfStagingSite) continue; |
| 312 | |
| 313 | $this->siteConfig['total_db_size'] += intval($size); |
| 314 | $relatedTables[$name] = $this->siteConfig['db_prefix'] . substr($name, strlen($table_prefix)); |
| 315 | |
| 316 | } |
| 317 | |
| 318 | return $relatedTables; |
| 319 | |
| 320 | } |
| 321 | |
| 322 | private function updateUserRolesInOptions() { |
| 323 | |
| 324 | global $wpdb; |
| 325 | |
| 326 | // Update option name |
| 327 | $sql = "UPDATE %i SET `option_name` = %s WHERE `option_name` = %s;"; |
| 328 | $newOptionTable = $this->siteConfig['db_prefix'] . 'options'; |
| 329 | $newOptionName = $this->siteConfig['db_prefix'] . 'user_roles'; |
| 330 | $oldOptionName = $this->siteConfig['source_db_prefix'] . 'user_roles'; |
| 331 | |
| 332 | $sql = $wpdb->prepare($sql, [$newOptionTable, $newOptionName, $oldOptionName]); |
| 333 | $wpdb->query($sql); |
| 334 | |
| 335 | if ($wpdb->last_error !== '') { |
| 336 | $translated = __('There was an error during update of user roles:', 'backup-backup') . ' ' . $wpdb->last_error; |
| 337 | $english = 'There was an error during update of user roles:' . ' ' . $wpdb->last_error; |
| 338 | $this->returnError($translated, $english); |
| 339 | } |
| 340 | |
| 341 | } |
| 342 | |
| 343 | private function duplicateTable($source, $destination) { |
| 344 | |
| 345 | global $wpdb; |
| 346 | |
| 347 | // Create new table |
| 348 | $sql = "CREATE TABLE %i LIKE %i;"; |
| 349 | $sql = $wpdb->prepare($sql, [$destination, $source]); |
| 350 | $wpdb->query($sql); |
| 351 | |
| 352 | if ($wpdb->last_error !== '') { |
| 353 | $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error; |
| 354 | $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error; |
| 355 | $this->returnError($translated, $english); |
| 356 | } |
| 357 | |
| 358 | // Duplicate data |
| 359 | $sql = "INSERT INTO %i SELECT * from %i;"; |
| 360 | $sql = $wpdb->prepare($sql, [$destination, $source]); |
| 361 | $wpdb->query($sql); |
| 362 | |
| 363 | if ($wpdb->last_error !== '') { |
| 364 | $translated = __('There was an error during database table data duplication:', 'backup-backup') . ' ' . $wpdb->last_error; |
| 365 | $english = 'There was an error during database table data duplication:' . ' ' . $wpdb->last_error; |
| 366 | $this->returnError($translated, $english); |
| 367 | } |
| 368 | |
| 369 | } |
| 370 | |
| 371 | private function parseDomain($domain, $removeWWW = true) { |
| 372 | |
| 373 | if (substr($domain, 0, 8) == 'https://') $domain = substr($domain, 8); |
| 374 | if (substr($domain, 0, 7) == 'http://') $domain = substr($domain, 7); |
| 375 | if ($removeWWW === true) { |
| 376 | if (substr($domain, 0, 4) == 'www.') $domain = substr($domain, 4); |
| 377 | } |
| 378 | $domain = untrailingslashit($domain); |
| 379 | |
| 380 | return $domain; |
| 381 | |
| 382 | } |
| 383 | |
| 384 | private function performSearchReplace($table, $from, $to, $start = 0, $end = 0, $limitColumns = false) { |
| 385 | |
| 386 | $replaceEngine = new BMISearchReplace([$table], $start, $end, true); |
| 387 | |
| 388 | $stats = $replaceEngine->perform($from, $to, $limitColumns); |
| 389 | |
| 390 | $this->siteConfig['totalTables'] += $stats['tables']; |
| 391 | $this->siteConfig['totalRows'] += $stats['rows']; |
| 392 | $this->siteConfig['totalChanges'] += $stats['change']; |
| 393 | $this->siteConfig['totalUpdates'] += $stats['updates']; |
| 394 | |
| 395 | $this->siteConfig['currentTableTotalUpdates'] += $stats['updates']; |
| 396 | $this->siteConfig['currentSearchReplacePage'] = $stats['currentPage']; |
| 397 | $this->siteConfig['totalSearchReplacePages'] = $stats['totalPages']; |
| 398 | |
| 399 | $replaceEngine = null; |
| 400 | |
| 401 | } |
| 402 | |
| 403 | public function requestDelete() { |
| 404 | return $this->abort(true); |
| 405 | } |
| 406 | |
| 407 | // Step: 0 (initialization of the staging site, create entry) |
| 408 | private function initialization() { |
| 409 | |
| 410 | $this->log('Step 0 - Initialization of the process', 'VERBOSE'); |
| 411 | |
| 412 | $this->log(__('Name of subsite:', 'backup-backup') . ' ' . $this->name); |
| 413 | $this->log(__('Expected URL of subsite:', 'backup-backup') . ' ' . home_url($this->name)); |
| 414 | |
| 415 | $this->printInitialLogs(); |
| 416 | |
| 417 | $this->createDatabasePrefixAndInitDir(); |
| 418 | if ($this->wasError) return; |
| 419 | |
| 420 | // Set progress to do something |
| 421 | $this->progress(4); |
| 422 | |
| 423 | // Set next step to be 1 |
| 424 | $this->setContinuation(1); |
| 425 | |
| 426 | } |
| 427 | |
| 428 | // Step: 1 (preparation of file list and database recipes) |
| 429 | private function prepareFilesAndDatabase() { |
| 430 | |
| 431 | // Create list of all files and directories |
| 432 | if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) { |
| 433 | $this->log(__('Scanning all files on your website, it may take a while...', 'backup-backup'), 'STEP'); |
| 434 | |
| 435 | $pathDirsListFile = BMI_INCLUDES . DIRECTORY_SEPARATOR . 'htaccess' . DIRECTORY_SEPARATOR . '.staging_directories'; |
| 436 | $pathFilesListFile = BMI_INCLUDES . DIRECTORY_SEPARATOR . 'htaccess' . DIRECTORY_SEPARATOR . '.staging_files'; |
| 437 | |
| 438 | $this->siteConfig['total_size'] = 0; |
| 439 | $this->siteConfig['total_files'] = 0; |
| 440 | $this->siteConfig['total_directories'] = 0; |
| 441 | |
| 442 | if (file_exists($pathDirsListFile)) @unlink($pathDirsListFile); |
| 443 | if (file_exists($pathFilesListFile)) @unlink($pathFilesListFile); |
| 444 | |
| 445 | $this->filesList = fopen($pathFilesListFile, 'a'); |
| 446 | $this->dirsList = fopen($pathDirsListFile, 'a'); |
| 447 | |
| 448 | $topLevelFiles = $this->getAbspathFiles(ABSPATH); |
| 449 | $leftDirs = $this->processFiles(trailingslashit(ABSPATH), $topLevelFiles); |
| 450 | |
| 451 | $this->processFilesRecursively($leftDirs); |
| 452 | |
| 453 | fclose($this->filesList); |
| 454 | fclose($this->dirsList); |
| 455 | |
| 456 | $this->log(__('All files scanned and prepared for duplication...', 'backup-backup'), 'SUCCESS'); |
| 457 | $this->progress(10); |
| 458 | |
| 459 | $this->setContinuation(1, 2); |
| 460 | } |
| 461 | |
| 462 | // Display details about scan and check database size |
| 463 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) { |
| 464 | $this->log(__('Looking for database tables that should be duplicated and their sizes...', 'backup-backup'), 'STEP'); |
| 465 | |
| 466 | $this->siteConfig['total_db_size'] = 0; |
| 467 | $this->siteConfig['tables'] = $this->getTablesForDuplication(); |
| 468 | $this->siteConfig['amountOfTables'] = sizeof($this->siteConfig['tables']); |
| 469 | $this->siteConfig['sumOfTotalSize'] = intval($this->siteConfig['total_db_size']) + intval($this->siteConfig['total_size']); |
| 470 | |
| 471 | $this->log(__('Amount of files to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_files']); |
| 472 | $this->log(__('Amount of directories to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_directories']); |
| 473 | $this->log(__('Amount of tables to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['amountOfTables']); |
| 474 | $this->log(__('Size of database to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_db_size']))); |
| 475 | $this->log(__('Size of files to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_size']))); |
| 476 | $this->log(__('Total duplication size:', 'backup-backup') . ' ' . BMP::humanSize($this->siteConfig['sumOfTotalSize'])); |
| 477 | $this->log(__('Tables prepared for duplication...', 'backup-backup'), 'SUCCESS'); |
| 478 | |
| 479 | $this->log(__('Checking if there is enough space for the staging site...', 'backup-backup'), 'STEP'); |
| 480 | $this->log(__('Space checking may take a while', 'backup-backup')); |
| 481 | |
| 482 | $this->progress(15); |
| 483 | |
| 484 | $this->setContinuation(1, 3); |
| 485 | } |
| 486 | |
| 487 | // Check if there is enough space for duplication |
| 488 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) { |
| 489 | require_once BMI_INCLUDES . '/check/checker.php'; |
| 490 | $checker = new Checker($this->logger); |
| 491 | |
| 492 | $bytes = intval(intval($this->siteConfig['sumOfTotalSize']) * 1.1); |
| 493 | $this->log(str_replace('%s2', BMP::humanSize($bytes), str_replace('%s1', $bytes, __('Checking in total: %s1 bytes (%s2)', 'backup-backup')))); |
| 494 | |
| 495 | if (!$checker->check_free_space($bytes, true)) { |
| 496 | |
| 497 | $translated = __('There is not enough space on your server in order to create staging site.', 'backup-backup'); |
| 498 | $english = 'There is not enough space on your server in order to create staging site.'; |
| 499 | return $this->returnError($translated, $english); |
| 500 | |
| 501 | } else { |
| 502 | |
| 503 | $this->log(__("Confirmed, there is more than enough space, checked: ", 'backup-backup') . ($bytes) . __(" bytes", 'backup-backup'), 'SUCCESS'); |
| 504 | |
| 505 | } |
| 506 | |
| 507 | $this->progress(20); |
| 508 | |
| 509 | // Set next batch to start step 2 |
| 510 | $this->log('Setting new step for next request to 2 @ batch 1', 'VERBOSE'); |
| 511 | $this->setContinuation(2); |
| 512 | |
| 513 | } |
| 514 | |
| 515 | } |
| 516 | |
| 517 | // Step: 2 (duplicate database) |
| 518 | private function duplicateDatabase() { |
| 519 | |
| 520 | if (!isset($this->siteConfig['finishedTables'])) { |
| 521 | $this->siteConfig['finishedTables'] = []; |
| 522 | } |
| 523 | |
| 524 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) { |
| 525 | $this->log(__('Duplicating database tables...', 'backup-backup'), 'STEP'); |
| 526 | } |
| 527 | |
| 528 | $startTime = time(); |
| 529 | foreach ($this->siteConfig['tables'] as $source_table => $destination_table) { |
| 530 | |
| 531 | $this->duplicateTable($source_table, $destination_table); |
| 532 | $this->log(str_replace('%s1', $source_table, str_replace('%s2', $destination_table, __('Table %s1 cloned as %s2', 'backup-backup')))); |
| 533 | |
| 534 | $this->siteConfig['finishedTables'][] = $destination_table; |
| 535 | unset($this->siteConfig['tables'][$source_table]); |
| 536 | |
| 537 | $processPercentage = intval((sizeof($this->siteConfig['finishedTables']) / $this->siteConfig['amountOfTables']) * 20); |
| 538 | $this->progress(20 + $processPercentage); |
| 539 | |
| 540 | if ((time() - $startTime) > 5) { |
| 541 | $this->setContinuation(3, (intval($this->siteConfig['batch']) + 1)); |
| 542 | break; |
| 543 | } |
| 544 | |
| 545 | } |
| 546 | |
| 547 | if (sizeof($this->siteConfig['tables']) == 0) { |
| 548 | unset($this->siteConfig['tables']); |
| 549 | } |
| 550 | |
| 551 | if (!isset($this->siteConfig['tables'])) { |
| 552 | $this->log(__('All tables were successfully duplicated', 'backup-backup'), 'SUCCESS'); |
| 553 | $this->setContinuation(3); |
| 554 | } |
| 555 | |
| 556 | } |
| 557 | |
| 558 | // Step: 3 (search & replace of new tables) |
| 559 | private function searchReplace() { |
| 560 | |
| 561 | require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'search-replace.php'; |
| 562 | |
| 563 | // Domain |
| 564 | $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']); |
| 565 | $destinationURL = $this->parseDomain($this->siteConfig['url']); |
| 566 | |
| 567 | // Paths |
| 568 | $sourceABSPATH = untrailingslashit($this->siteConfig['root_source']); |
| 569 | $destinationABSPATH = untrailingslashit($this->siteConfig['root_staging']); |
| 570 | |
| 571 | // Items to be replaced |
| 572 | $replaces = [ |
| 573 | ['from' => $sourceABSPATH, 'to' => $destinationABSPATH], |
| 574 | ['from' => $sourceURL, 'to' => $destinationURL], |
| 575 | ['from' => $this->siteConfig['source_db_prefix'], 'to' => $this->siteConfig['db_prefix']] |
| 576 | ]; |
| 577 | |
| 578 | $variants = [ |
| 579 | __("Batch for path adjustment (%s/%s) updated: %s fields.", 'backup-backup'), |
| 580 | __("Batch for domain adjustments (%s/%s) updated: %s fields.", 'backup-backup'), |
| 581 | __("Batch for prefix key adjustments (%s/%s) updated: %s fields.", 'backup-backup') |
| 582 | ]; |
| 583 | |
| 584 | $variantsEmpty = [ |
| 585 | __("Path replacements are not required for table: %s", 'backup-backup'), |
| 586 | __("Domain replacements are not required for table: %s", 'backup-backup'), |
| 587 | __("Prefix key replacements are not required for table: %s", 'backup-backup') |
| 588 | ]; |
| 589 | |
| 590 | // Table for replacement |
| 591 | if (isset($this->siteConfig['finishedTables']) && sizeof($this->siteConfig['finishedTables']) > 0) { |
| 592 | |
| 593 | // Get first table from duplicated tables |
| 594 | $table = $this->siteConfig['finishedTables'][0]; |
| 595 | |
| 596 | } else { |
| 597 | |
| 598 | // All tables processed, search replace finished |
| 599 | unset($this->siteConfig['finishedTables']); |
| 600 | $this->setContinuation(4); |
| 601 | |
| 602 | } |
| 603 | |
| 604 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) { |
| 605 | $this->log(__('Performing database search replace on duplicated tables...', 'backup-backup'), 'STEP'); |
| 606 | |
| 607 | $pagesize = '?'; |
| 608 | if (defined('BMI_MAX_SEARCH_REPLACE_PAGE')) $pagesize = BMI_MAX_SEARCH_REPLACE_PAGE; |
| 609 | $this->log(__('Page size for that process: ', 'backup-backup') . $pagesize, 'INFO'); |
| 610 | |
| 611 | $this->siteConfig['totalTables'] = 0; |
| 612 | $this->siteConfig['totalRows'] = 0; |
| 613 | $this->siteConfig['totalChanges'] = 0; |
| 614 | $this->siteConfig['totalUpdates'] = 0; |
| 615 | |
| 616 | $this->siteConfig['currentSearchReplaceItem'] = 0; |
| 617 | $this->siteConfig['currentTableTotalUpdates'] = 0; |
| 618 | $this->siteConfig['currentSearchReplacePage'] = 0; |
| 619 | $this->siteConfig['totalSearchReplacePages'] = 0; |
| 620 | |
| 621 | $this->setContinuation(3, 2); |
| 622 | } |
| 623 | |
| 624 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) { |
| 625 | |
| 626 | // Give details of current process |
| 627 | if ($this->siteConfig['currentSearchReplaceItem'] == 0 && $this->siteConfig['currentSearchReplacePage'] == 0) { |
| 628 | $this->log(sprintf(__('Performing search replace for table: %s', 'backup-backup'), $table), 'STEP'); |
| 629 | } |
| 630 | |
| 631 | // Control time of the process |
| 632 | $startTime = time(); |
| 633 | $tableCompleted = false; |
| 634 | $maxExecutionTime = 5; |
| 635 | |
| 636 | for ($i = $this->siteConfig['currentSearchReplaceItem']; $i < sizeof($replaces); ++$i) { |
| 637 | |
| 638 | $this->siteConfig['currentSearchReplaceItem'] = $i; |
| 639 | if ((time() - $startTime) >= $maxExecutionTime) break; |
| 640 | $tableCompleted = false; |
| 641 | |
| 642 | $from = $replaces[$i]['from']; |
| 643 | $to = $replaces[$i]['to']; |
| 644 | |
| 645 | while ($tableCompleted === false && (time() - $startTime) < $maxExecutionTime) { |
| 646 | |
| 647 | // Progress |
| 648 | $start = $this->siteConfig['currentSearchReplacePage']; |
| 649 | $end = $this->siteConfig['totalSearchReplacePages']; |
| 650 | |
| 651 | // Path replace for current table |
| 652 | if ($i == 2) { |
| 653 | if (strpos($table, 'usermeta') !== false) { |
| 654 | $this->performSearchReplace($table, $from, $to, $start, $end, ['meta_key']); |
| 655 | } |
| 656 | } else { |
| 657 | $this->performSearchReplace($table, $from, $to, $start, $end); |
| 658 | } |
| 659 | |
| 660 | // Handle batch logs |
| 661 | if ($this->siteConfig['currentTableTotalUpdates'] == 0 && $this->siteConfig['totalSearchReplacePages'] == 0) { |
| 662 | $this->log(sprintf($variantsEmpty[$i], $table), 'INFO'); |
| 663 | } else { |
| 664 | $st = $this->siteConfig['currentSearchReplacePage']; |
| 665 | $en = $this->siteConfig['totalSearchReplacePages']; |
| 666 | $this->log(sprintf($variants[$i], $st, $en, $this->siteConfig['currentTableTotalUpdates'])); |
| 667 | $this->siteConfig['currentTableTotalUpdates'] = 0; |
| 668 | } |
| 669 | |
| 670 | // Done of that table |
| 671 | if ($this->siteConfig['currentSearchReplacePage'] >= $this->siteConfig['totalSearchReplacePages']) { |
| 672 | if ($i >= sizeof($replaces) - 1) { |
| 673 | unset($this->siteConfig['finishedTables'][0]); |
| 674 | $this->siteConfig['finishedTables'] = array_values($this->siteConfig['finishedTables']); |
| 675 | $this->log(sprintf(__('Search replace for table %s finished', 'backup-backup'), $table), 'SUCCESS'); |
| 676 | $this->siteConfig['currentSearchReplaceItem'] = 0; |
| 677 | |
| 678 | $processPercentage = intval((($this->siteConfig['amountOfTables'] - sizeof($this->siteConfig['finishedTables'])) / $this->siteConfig['amountOfTables']) * 20); |
| 679 | $this->progress(40 + $processPercentage); |
| 680 | } |
| 681 | |
| 682 | $this->siteConfig['currentTableTotalUpdates'] = 0; |
| 683 | $this->siteConfig['currentSearchReplacePage'] = 0; |
| 684 | $this->siteConfig['totalSearchReplacePages'] = 0; |
| 685 | $tableCompleted = true; |
| 686 | } |
| 687 | |
| 688 | } |
| 689 | |
| 690 | if (!$tableCompleted) break; |
| 691 | |
| 692 | } |
| 693 | |
| 694 | if ($tableCompleted && sizeof($this->siteConfig['finishedTables']) == 0) $this->setContinuation(3, 3); // If path replacement finished |
| 695 | else $this->setContinuation(3, 2); // If path raplecement didnt finish, continue next batch (time exceed 5 seconds) |
| 696 | |
| 697 | } |
| 698 | |
| 699 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) $this->setContinuation(4); |
| 700 | |
| 701 | } |
| 702 | |
| 703 | // Step: 4 (summary of S&R and file duplication) |
| 704 | private function databaseFinishedCopyFiles() { |
| 705 | |
| 706 | $totalBatchExecution = 5; // 5 seconds |
| 707 | $milestoneUpdate = 500; // per 500 files/directories |
| 708 | $batchLimit = $milestoneUpdate + 1; // Limit of files/directories per batch |
| 709 | |
| 710 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) { |
| 711 | |
| 712 | // Remove unused variables |
| 713 | unset($this->siteConfig['currentSearchReplaceItem']); |
| 714 | unset($this->siteConfig['currentTableTotalUpdates']); |
| 715 | unset($this->siteConfig['currentSearchReplacePage']); |
| 716 | unset($this->siteConfig['totalSearchReplacePages']); |
| 717 | |
| 718 | // Display summary of search replace |
| 719 | $this->log(__('Displaying summary of search replace process', 'backup-backup'), 'STEP'); |
| 720 | $this->log(sprintf(__('Search replace processed %s tables in total', 'backup-backup'), $this->siteConfig['totalTables'])); |
| 721 | $this->log(sprintf(__('In total it processed %s rows', 'backup-backup'), $this->siteConfig['totalRows'])); |
| 722 | $this->log(sprintf(__('After all it changed only %s columns', 'backup-backup'), $this->siteConfig['totalChanges'])); |
| 723 | $this->log(sprintf(__('Which results in %s changes in total of all cells', 'backup-backup'), $this->siteConfig['totalUpdates'])); |
| 724 | |
| 725 | // Remove unused variables |
| 726 | unset($this->siteConfig['totalTables']); |
| 727 | unset($this->siteConfig['totalRows']); |
| 728 | unset($this->siteConfig['totalChanges']); |
| 729 | unset($this->siteConfig['totalUpdates']); |
| 730 | |
| 731 | // Update user roles in options table |
| 732 | $this->updateUserRolesInOptions(); |
| 733 | |
| 734 | $this->log(__('Search replace of all tables finished successfully', 'backup-backup'), 'SUCCESS'); |
| 735 | |
| 736 | // Go to new process without new batch request |
| 737 | $this->siteConfig['batch'] = 2; |
| 738 | |
| 739 | } |
| 740 | |
| 741 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) { |
| 742 | |
| 743 | if (!isset($this->siteConfig['dirSeek']) || $this->siteConfig['dirSeek'] == 0) { |
| 744 | $this->log(__('Duplication of directories to staging site', 'backup-backup'), 'STEP'); |
| 745 | $this->siteConfig['dirSeek'] = 0; |
| 746 | } |
| 747 | |
| 748 | $pathDirsListFile = BMI_INCLUDES . DIRECTORY_SEPARATOR . 'htaccess' . DIRECTORY_SEPARATOR . '.staging_directories'; |
| 749 | |
| 750 | $file = new \SplFileObject($pathDirsListFile); |
| 751 | $file->seek($file->getSize()); |
| 752 | |
| 753 | $startTime = time(); |
| 754 | $currentSeekedElements = 0; |
| 755 | $totalLines = $file->key() + 1; |
| 756 | $stagingRoot = trailingslashit($this->siteConfig['root_staging']); |
| 757 | $file->seek($this->siteConfig['dirSeek']); |
| 758 | |
| 759 | while (!$file->eof()) { |
| 760 | |
| 761 | if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break; |
| 762 | |
| 763 | $file->seek($this->siteConfig['dirSeek']); |
| 764 | $path = $stagingRoot . trim($file->current()); |
| 765 | |
| 766 | if (!(file_exists($path) && is_dir($path))) @mkdir($path); |
| 767 | if ($this->siteConfig['dirSeek'] % $milestoneUpdate === 0 && $this->siteConfig['dirSeek'] != 0) { |
| 768 | $processPercentageTotal = number_format(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 100, 2); |
| 769 | $this->log(sprintf( |
| 770 | __('Directory creation milestone: %s/%s (%s)', 'backup-backup'), |
| 771 | $this->siteConfig['dirSeek'], |
| 772 | $this->siteConfig['total_directories'], |
| 773 | $processPercentageTotal . '%' |
| 774 | )); |
| 775 | $processPercentage = intval(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 5); |
| 776 | $this->progress(60 + $processPercentage); |
| 777 | } |
| 778 | |
| 779 | $this->siteConfig['dirSeek']++; |
| 780 | $currentSeekedElements++; |
| 781 | |
| 782 | } |
| 783 | |
| 784 | if ($this->siteConfig['dirSeek'] >= $this->siteConfig['total_directories']) { |
| 785 | unlink($pathDirsListFile); |
| 786 | unset($this->siteConfig['dirSeek']); |
| 787 | $this->log(sprintf( |
| 788 | __('Directory creation milestone: %s/%s (%s)', 'backup-backup'), |
| 789 | $this->siteConfig['total_directories'], |
| 790 | $this->siteConfig['total_directories'], |
| 791 | '100%' |
| 792 | )); |
| 793 | $this->progress(65); |
| 794 | $this->setContinuation(4, 3); |
| 795 | } else { |
| 796 | $this->setContinuation(4, 2); |
| 797 | } |
| 798 | |
| 799 | } |
| 800 | |
| 801 | if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) { |
| 802 | |
| 803 | if (!isset($this->siteConfig['fileSeek']) || $this->siteConfig['fileSeek'] == 0) { |
| 804 | $this->log(__('Duplication of files to staging site', 'backup-backup'), 'STEP'); |
| 805 | $this->log(__('Duplication of files make take a while', 'backup-backup')); |
| 806 | $this->log(__('Disclaimer: ZIP Archives are not getting cloned on staging sites', 'backup-backup'), 'WARN'); |
| 807 | $this->siteConfig['fileSeek'] = 0; |
| 808 | } |
| 809 | |
| 810 | $pathFilesListFile = BMI_INCLUDES . DIRECTORY_SEPARATOR . 'htaccess' . DIRECTORY_SEPARATOR . '.staging_files'; |
| 811 | |
| 812 | $file = new \SplFileObject($pathFilesListFile); |
| 813 | $file->seek($file->getSize()); |
| 814 | |
| 815 | $startTime = time(); |
| 816 | $currentSeekedElements = 0; |
| 817 | $totalLines = $file->key() + 1; |
| 818 | $sourceRoot = trailingslashit($this->siteConfig['root_source']); |
| 819 | $stagingRoot = trailingslashit($this->siteConfig['root_staging']); |
| 820 | $file->seek($this->siteConfig['fileSeek']); |
| 821 | $disallowedExtensions = ['zip', 'tar', 'gz', 'tmp', 'rar', '7z']; |
| 822 | |
| 823 | while (!$file->eof()) { |
| 824 | |
| 825 | if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break; |
| 826 | |
| 827 | $file->seek($this->siteConfig['fileSeek']); |
| 828 | $path = trim($file->current()); |
| 829 | |
| 830 | if (file_exists($sourceRoot . $path) && !file_exists($stagingRoot . $path)) { |
| 831 | $arrayWithExtension = explode('.', $path); |
| 832 | $ext = strtolower(array_pop($arrayWithExtension)); |
| 833 | if (!in_array($ext, $disallowedExtensions)) { |
| 834 | @copy($sourceRoot . $path, $stagingRoot . $path); |
| 835 | } |
| 836 | } |
| 837 | |
| 838 | if ($this->siteConfig['fileSeek'] % $milestoneUpdate === 0 && $this->siteConfig['fileSeek'] != 0) { |
| 839 | $processPercentageTotal = number_format(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 100, 2); |
| 840 | $this->log(sprintf( |
| 841 | __('File duplication milestone: %s/%s (%s)', 'backup-backup'), |
| 842 | $this->siteConfig['fileSeek'], |
| 843 | $this->siteConfig['total_files'], |
| 844 | $processPercentageTotal . '%' |
| 845 | )); |
| 846 | $processPercentage = intval(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 25); |
| 847 | $this->progress(65 + $processPercentage); |
| 848 | } |
| 849 | |
| 850 | $this->siteConfig['fileSeek']++; |
| 851 | $currentSeekedElements++; |
| 852 | |
| 853 | } |
| 854 | |
| 855 | if ($this->siteConfig['fileSeek'] >= $this->siteConfig['total_files']) { |
| 856 | unlink($pathFilesListFile); |
| 857 | unset($this->siteConfig['fileSeek']); |
| 858 | $this->log(sprintf( |
| 859 | __('File duplication milestone: %s/%s (%s)', 'backup-backup'), |
| 860 | $this->siteConfig['total_files'], |
| 861 | $this->siteConfig['total_files'], |
| 862 | '100%' |
| 863 | )); |
| 864 | $this->progress(90); |
| 865 | $this->setContinuation(5); |
| 866 | } else { |
| 867 | $this->setContinuation(4, 3); |
| 868 | } |
| 869 | |
| 870 | } |
| 871 | |
| 872 | } |
| 873 | |
| 874 | // Step: 5 (setup passwordless login script and paste adjusted wp-config) |
| 875 | private function setupWpConfigAndLoginScript() { |
| 876 | |
| 877 | $this->copyOverPasswordLessScript(); |
| 878 | |
| 879 | $this->log(__('Inserting wp-config.php file for staging site', 'backup-backup'), 'STEP'); |
| 880 | |
| 881 | $sourceWPConfig = trailingslashit($this->siteConfig['root_source']) . 'wp-config.php'; |
| 882 | $destinationWPConfig = trailingslashit($this->siteConfig['root_staging']) . 'wp-config.php'; |
| 883 | |
| 884 | $previousPrefix = $this->siteConfig['source_db_prefix']; |
| 885 | $destinationPrefix = $this->siteConfig['db_prefix']; |
| 886 | |
| 887 | $previousRoot = untrailingslashit($this->siteConfig['root_source']); |
| 888 | $destinationRoot = untrailingslashit($this->siteConfig['root_staging']); |
| 889 | |
| 890 | $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']); |
| 891 | $destinationURL = $this->parseDomain($this->siteConfig['url']); |
| 892 | |
| 893 | $wpconfig = file_get_contents($sourceWPConfig); |
| 894 | |
| 895 | // Table Prefix |
| 896 | $this->log(__('Replacing table prefix in wp-config.php', 'backup-backup')); |
| 897 | if (strpos($wpconfig, '"' . $previousPrefix . '";') !== false) { |
| 898 | $wpconfig = str_replace('"' . $previousPrefix . '";', '"' . $destinationPrefix . '";', $wpconfig); |
| 899 | } elseif (strpos($wpconfig, "'" . $previousPrefix . "';") !== false) { |
| 900 | $wpconfig = str_replace("'" . $previousPrefix . "';", "'" . $destinationPrefix . "';", $wpconfig); |
| 901 | } |
| 902 | |
| 903 | // Paths e.g. for wp_debug_log |
| 904 | $this->log(__('Adjusting paths in wp-config.php', 'backup-backup')); |
| 905 | if (strpos($wpconfig, '"' . $previousRoot . '";') !== false) { |
| 906 | $wpconfig = str_replace('"' . $previousRoot . '";', '"' . $destinationRoot . '";', $wpconfig); |
| 907 | } elseif (strpos($wpconfig, "'" . $previousRoot . "';") !== false) { |
| 908 | $wpconfig = str_replace("'" . $previousRoot . "';", "'" . $destinationRoot . "';", $wpconfig); |
| 909 | } |
| 910 | |
| 911 | // Paths e.g. for wp_home & wp_siteurl |
| 912 | $this->log(__('Adjusting domains in wp-config.php', 'backup-backup')); |
| 913 | $wpconfig = explode("\n", $wpconfig); |
| 914 | for ($i = 0; $i < sizeof($wpconfig); ++$i) { |
| 915 | |
| 916 | $line = $wpconfig[$i]; |
| 917 | |
| 918 | if (strpos($line, 'WP_SITEURL') !== false || strpos($line, 'WP_HOME') !== false) { |
| 919 | $wpconfig[$i] = str_replace($sourceURL, $destinationURL, $line); |
| 920 | } |
| 921 | |
| 922 | } |
| 923 | |
| 924 | $wpconfig = implode("\n", $wpconfig); |
| 925 | |
| 926 | file_put_contents($destinationWPConfig, $wpconfig); |
| 927 | $this->log(__('File adjustments for wp-config.php finished successfully', 'backup-backup'), 'SUCCESS'); |
| 928 | |
| 929 | $this->setContinuation(6); |
| 930 | |
| 931 | } |
| 932 | |
| 933 | // Step: 6 (cleanup and misc) |
| 934 | private function performFinish() { |
| 935 | |
| 936 | $this->cleanup(); |
| 937 | $this->setContinuation(7); |
| 938 | |
| 939 | } |
| 940 | |
| 941 | public function __destruct() { |
| 942 | |
| 943 | parent::__destruct(); |
| 944 | |
| 945 | } |
| 946 | |
| 947 | } |
| 948 |