tastewp.php
354 lines
| 1 | <?php |
| 2 | |
| 3 | // Namespace |
| 4 | namespace BMI\Plugin\Staging; |
| 5 | |
| 6 | // Use |
| 7 | use BMI\Plugin\Backup_Migration_Plugin as BMP; |
| 8 | |
| 9 | // Exit on direct access |
| 10 | if (!defined('ABSPATH')) exit; |
| 11 | |
| 12 | // Require controller |
| 13 | require_once BMI_INCLUDES . '/staging/controller.php'; |
| 14 | |
| 15 | /** |
| 16 | * Subclass of main staging controller (tastewp/remote handler) |
| 17 | */ |
| 18 | class BMI_Staging_TasteWP extends BMI_Staging { |
| 19 | |
| 20 | /** |
| 21 | * Initialization of TasteWP Staging Site Handler |
| 22 | */ |
| 23 | function __construct($name, $initialize = false, $backupName = false, $delete = false) { |
| 24 | |
| 25 | parent::__construct(...func_get_args()); |
| 26 | |
| 27 | $this->backupName = $backupName; |
| 28 | |
| 29 | if ($delete) $this->abort(true); |
| 30 | else { |
| 31 | if ($initialize) $this->initialization(); |
| 32 | else $this->continue(); |
| 33 | } |
| 34 | |
| 35 | } |
| 36 | |
| 37 | private function humanToBytes($num) { |
| 38 | |
| 39 | $num_split = str_split(strval(strtolower($num))); |
| 40 | $num_split_temp = $num_split; |
| 41 | $num_end = end($num_split); |
| 42 | array_pop($num_split); |
| 43 | $num_join = implode('', $num_split); |
| 44 | if ('m' === $num_end) { |
| 45 | $num = doubleval($num_join) * 1048576; |
| 46 | } elseif ('k' === $num_end) { |
| 47 | $num = doubleval($num_join) * 1024; |
| 48 | } elseif ('g' === $num_end) { |
| 49 | $num = doubleval($num_join) * 1073741824; |
| 50 | } |
| 51 | |
| 52 | return $num; |
| 53 | |
| 54 | } |
| 55 | |
| 56 | private function getTotalAvailableMemory() { |
| 57 | |
| 58 | $wpMax = $this->humanToBytes(WP_MAX_MEMORY_LIMIT); |
| 59 | $memory = $this->humanToBytes(ini_get('memory_limit')); |
| 60 | $used = memory_get_usage(false); |
| 61 | |
| 62 | $lowest = $wpMax; |
| 63 | if ($memory < $lowest) $lowest = $memory; |
| 64 | |
| 65 | $lowest1 = $wpMax - (1024 * 5) - $used; |
| 66 | $lowest2 = $memory - (1024 * 5) - $used; |
| 67 | |
| 68 | if ($lowest2 > 0) $lowest = $lowest2; |
| 69 | else if ($lowest1 > 0) $lowest = $lowest1; |
| 70 | else $lowest = $lowest - $used; |
| 71 | |
| 72 | $lowest = intval(abs($lowest)); |
| 73 | |
| 74 | $this->log('WP MAX MEMORY LIMIT: ' . $wpMax, 'VERBOSE'); |
| 75 | $this->log('MEMORY LIMIT PHP INI: ' . $memory, 'VERBOSE'); |
| 76 | $this->log('USED MEMORY: ' . $used, 'VERBOSE'); |
| 77 | $this->log('AVAILABLE MEMORY: ' . $lowest, 'VERBOSE'); |
| 78 | $this->log('Safe limit of memory divided by 2: ' . ($lowest / 2), 'VERBOSE'); |
| 79 | |
| 80 | $this->log('Safe limit of memory divided by 2 in Megabytes: ' . number_format((($lowest / 2) / 1024 / 1024), 2), 'VERBOSE'); |
| 81 | |
| 82 | return abs($lowest / 2); |
| 83 | |
| 84 | } |
| 85 | |
| 86 | private function initialization() { |
| 87 | |
| 88 | global $table_prefix; |
| 89 | $ip = $this->getIpAddress(); |
| 90 | |
| 91 | $this->printInitialLogs(); |
| 92 | $this->siteConfig['backupName'] = $this->backupName; |
| 93 | $this->siteConfig['backupPath'] = BMP::fixSlashes(BMI_BACKUPS . DIRECTORY_SEPARATOR . $this->backupName); |
| 94 | |
| 95 | $this->siteConfig['creation_date'] = time(); |
| 96 | $this->siteConfig['creator_ip'] = $ip; |
| 97 | $this->siteConfig['db_prefix'] = $table_prefix; |
| 98 | |
| 99 | $this->siteConfig['total_files'] = '---'; |
| 100 | $this->siteConfig['expiration_time'] = time() + 60*60; |
| 101 | |
| 102 | $this->log(__('Staging site remote creation process initialized', 'backup-backup'), 'success'); |
| 103 | $this->setContinuation(1); |
| 104 | |
| 105 | } |
| 106 | |
| 107 | private function sendUploadPing(&$batchData, $last) { |
| 108 | |
| 109 | $url = $this->tastewpURL . '/api/bmi/staging/upload'; |
| 110 | $headers = [ |
| 111 | 'Content-Type' => 'application/octet-stream', |
| 112 | 'tastewp-backupname' => $this->siteConfig['backupName'], |
| 113 | 'tastewp-localname' => $this->name, |
| 114 | 'tastewp-is-last' => $last, |
| 115 | 'tastewp-secret' => $this->siteConfig['communication_secret'], |
| 116 | 'tastewp-uploadid' => $this->siteConfig['uploadId'], |
| 117 | 'tastewp-entry' => $this->siteConfig['entry'] |
| 118 | ]; |
| 119 | |
| 120 | $response = $this->httpRequest($url, $batchData, 'PUT', $headers); |
| 121 | return $response; |
| 122 | |
| 123 | } |
| 124 | |
| 125 | private function continue() { |
| 126 | |
| 127 | if (isset($this->siteConfig['step'])) { |
| 128 | $this->step = intval($this->siteConfig['step']); |
| 129 | } else { |
| 130 | $this->step = null; |
| 131 | } |
| 132 | |
| 133 | if (!is_numeric($this->step)) { |
| 134 | |
| 135 | // End with error |
| 136 | $this->log(__('Step code was not provided for the request, prevents continuation of the process...'), 'ERROR'); |
| 137 | $this->log('Step code was not provided for the request, prevents continuation of the process...', 'VERBOSE'); |
| 138 | $this->log('#201', 'END-CODE'); |
| 139 | return ['status' => 'error']; |
| 140 | |
| 141 | } |
| 142 | |
| 143 | // Default error |
| 144 | $translatedMainError = __('Something unexpected happened, we need to abort the process.', 'backup-backup'); |
| 145 | $englishMainError = 'Something unexpected happened, we need to abort the process (#207).'; |
| 146 | |
| 147 | // Step controller |
| 148 | if ($this->step == 1) $this->initializeHandshakeWithTasteWP(); |
| 149 | if ($this->step == 2) $this->processWithUpload(); |
| 150 | if ($this->step == 3) $this->validateAndPrepareRecipe(); |
| 151 | else if (isset($this->step) && is_numeric($this->step)) $this->sendSuccess(true); |
| 152 | else $this->returnError($translatedMainError, $englishMainError); |
| 153 | |
| 154 | } |
| 155 | |
| 156 | private function initializeHandshakeWithTasteWP() { |
| 157 | |
| 158 | // TODO: Check if the backup is stored locally, otherwise download from cloud |
| 159 | // if () {} |
| 160 | |
| 161 | if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) { |
| 162 | |
| 163 | if (!isset($this->siteConfig['backupPath']) || !file_exists($this->siteConfig['backupPath']) || is_dir($this->siteConfig['backupPath'])) { |
| 164 | $translatedFileExistError = __('Backup path was not set or backup file does not exist.', 'backup-backup'); |
| 165 | $englishFileExistError = 'Backup path was not set or backup file does not exist (#208).'; |
| 166 | $this->returnError($translatedFileExistError, $englishFileExistError); |
| 167 | } |
| 168 | |
| 169 | $this->log(__('Getting MD5 of selected backup file...', 'backup-backup'), 'step'); |
| 170 | $this->log('Backup path: ' . $this->siteConfig['backupPath'], 'verbose'); |
| 171 | $md5 = md5_file($this->siteConfig['backupPath']); |
| 172 | $this->log('Calculated MD5 is: ' . $md5, 'verbose'); |
| 173 | $this->log(__('File MD5 checksum saved for future.', 'backup-backup'), 'success'); |
| 174 | |
| 175 | $this->log(__('Making handshake with TasteWP and asking for blessing.', 'backup-backup'), 'step'); |
| 176 | $this->log('Sending initial request to TasteWP', 'verbose'); |
| 177 | |
| 178 | $url = $this->tastewpURL . '/api/bmi/staging/init/upload'; |
| 179 | $data = [ 'name' => $this->name, 'backupName' => $this->siteConfig['backupName'], 'md5' => $md5 ]; |
| 180 | $response = $this->httpRequest($url, $data); |
| 181 | |
| 182 | if (!isset($response['status']) || $response['status'] != 'success') { |
| 183 | if (isset($response['message'])) { |
| 184 | $this->returnError($response['message'], $response['message']); |
| 185 | } else { |
| 186 | $translatedUnknownResponseError = __('Reponse from TasteWP does not have any status code, please try again.', 'backup-backup'); |
| 187 | $englishUnknownResponseError = 'Reponse from TasteWP does not have any status code, please try again (#210). ' . print_r($response, true); |
| 188 | $this->returnError($translatedUnknownResponseError, $englishUnknownResponseError); |
| 189 | } |
| 190 | } |
| 191 | $this->progress(5); |
| 192 | |
| 193 | // Error handlers validate the response |
| 194 | $translatedKeyExistError = __('One or more keys does not exist in response, please try again.', 'backup-backup'); |
| 195 | $englishKeyExistError = 'Missing required keys to continue the process (#209), the key is: '; |
| 196 | if (!isset($response['data'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'data'); |
| 197 | if (!isset($response['data']['secret'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'secret'); |
| 198 | if (!isset($response['data']['activation'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'activation'); |
| 199 | if (!isset($response['data']['uploadId'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'uploadId'); |
| 200 | if (!isset($response['data']['entry'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'entry'); |
| 201 | if (!isset($response['data']['maxUploadSize'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'maxUploadSize'); |
| 202 | |
| 203 | $this->siteConfig['name'] = $this->name; |
| 204 | $this->siteConfig['sumOfTotalSize'] = filesize($this->siteConfig['backupPath']); |
| 205 | $this->siteConfig['display_name'] = $this->siteConfig['backupName']; |
| 206 | $this->siteConfig['url'] = $this->tastewpURL . '/stg/' . $response['data']['activation']; |
| 207 | |
| 208 | $this->siteConfig['communication_secret'] = $response['data']['secret']; |
| 209 | $this->siteConfig['activation'] = $response['data']['activation']; |
| 210 | $this->siteConfig['uploadId'] = $response['data']['uploadId']; |
| 211 | $this->siteConfig['entry'] = $response['data']['entry']; |
| 212 | $this->siteConfig['maxUploadSize'] = $response['data']['maxUploadSize']; |
| 213 | $availableMemory = $this->getTotalAvailableMemory(); |
| 214 | |
| 215 | if ($availableMemory < intval($this->siteConfig['maxUploadSize'])) { |
| 216 | $this->siteConfig['maxUploadSize'] = $availableMemory; |
| 217 | } |
| 218 | |
| 219 | $this->log(__('Blessing received, upload details saved...', 'backup-backup'), 'success'); |
| 220 | $this->progress(8); |
| 221 | $this->setContinuation(2); |
| 222 | |
| 223 | } |
| 224 | |
| 225 | } |
| 226 | |
| 227 | private function processWithUpload() { |
| 228 | |
| 229 | $translatedUploadError1 = __('One or more variables are not defined, cannot calculate batching.', 'backup-backup'); |
| 230 | $translatedUploadError2 = __('There was some error during the upload process, please try again.', 'backup-backup'); |
| 231 | $englishUploadError1 = 'One or more variables are not defined, cannot calculate batching (#213).'; |
| 232 | $englishUploadError2 = 'There was some error during the upload process, please try again (#214).'; |
| 233 | |
| 234 | $sizeOfFile = intval($this->siteConfig['sumOfTotalSize']); |
| 235 | $maxUploadLimitBytes = intval($this->siteConfig['maxUploadSize']); // By Default it was: 32 MBs, but it may be different for each user |
| 236 | |
| 237 | if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) { |
| 238 | |
| 239 | $this->log(__('Calculating amount out total upload batches', 'backup-backup'), 'step'); |
| 240 | |
| 241 | if (!is_numeric($maxUploadLimitBytes) || is_nan($maxUploadLimitBytes) || !is_numeric($sizeOfFile) || is_nan($sizeOfFile)) { |
| 242 | $this->returnError($translatedUploadError1, $englishUploadError1); |
| 243 | } |
| 244 | |
| 245 | $this->log(__('Max upload size in megabytes:', 'backup-backup') . ' ' . number_format($maxUploadLimitBytes / 1024 / 1024, 2) . ' MB'); |
| 246 | $this->log(__('Size of backup file in megabytes:', 'backup-backup') . ' ' . number_format($sizeOfFile / 1024 / 1024, 2) . ' MB'); |
| 247 | $this->log(__('Expected total amount of batches:', 'backup-backup') . ' ' . ceil($sizeOfFile / $maxUploadLimitBytes)); |
| 248 | |
| 249 | $this->log('Size of backup file in bytes: ' . $sizeOfFile . ' B', 'verbose'); |
| 250 | $this->log('Size of backup file in megabytes: ' . number_format($sizeOfFile / 1024 / 1024, 2) . ' MB', 'verbose'); |
| 251 | $this->log('Max upload size in bytes: ' . $maxUploadLimitBytes . ' B', 'verbose'); |
| 252 | $this->log('Max upload size in megabytes: ' . number_format($maxUploadLimitBytes / 1024 / 1024, 2) . ' MB', 'verbose'); |
| 253 | |
| 254 | $this->log('Total amount of batched should be ceil(' . $sizeOfFile . ' / ' . $maxUploadLimitBytes . '): ' . ceil($sizeOfFile / $maxUploadLimitBytes), 'verbose'); |
| 255 | |
| 256 | $this->siteConfig['totalUploadBatches'] = ceil($sizeOfFile / $maxUploadLimitBytes); |
| 257 | $this->siteConfig['currentUploadBatch'] = 0; |
| 258 | |
| 259 | $this->log(__('Backup upload to TasteWP', 'backup-backup'), 'step'); |
| 260 | $this->progress(10); |
| 261 | $this->setContinuation(2, 2); |
| 262 | |
| 263 | } |
| 264 | |
| 265 | if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 2) { |
| 266 | |
| 267 | $totalSize = $sizeOfFile; |
| 268 | $maxBatchSize = $maxUploadLimitBytes; |
| 269 | $totalBatches = $this->siteConfig['totalUploadBatches']; |
| 270 | $currentBatch = $this->siteConfig['currentUploadBatch']; |
| 271 | |
| 272 | if (intval($totalBatches) <= (intval($currentBatch) + 1)) $isLast = 'true'; |
| 273 | else $isLast = 'false'; |
| 274 | |
| 275 | $start = $maxBatchSize * $currentBatch; |
| 276 | $end = $start + $maxBatchSize; |
| 277 | if ($end > $totalSize) $end = $totalSize; |
| 278 | $percent = number_format(($end / $totalSize) * 100, 2); |
| 279 | $this->progress(10 + intval(($end / $totalSize) * 80)); |
| 280 | |
| 281 | $backupFileHandler = fopen($this->siteConfig['backupPath'], 'r'); |
| 282 | $batchData = stream_get_contents($backupFileHandler, $maxBatchSize, $start); |
| 283 | fclose($backupFileHandler); |
| 284 | |
| 285 | $response = $this->sendUploadPing($batchData, $isLast); |
| 286 | if (!isset($response['status']) || $response['status'] != 'success') { |
| 287 | if (isset($response['message'])) $this->returnError($response['message'], $response['message']); |
| 288 | else $this->returnError($translatedUploadError2, $englishUploadError2); |
| 289 | } |
| 290 | |
| 291 | $this->log(sprintf(__('Upload milestone: %s/%s (%s) [Batch: %s/%s]', 'backup-backup'), $end, $totalSize, $percent . '%', ($currentBatch + 1), $totalBatches), 'info'); |
| 292 | $this->log('Chunk range: ' . $start . ' - ' . $end, 'verbose'); |
| 293 | |
| 294 | if ($isLast === 'true') { |
| 295 | $this->progress(90); |
| 296 | $this->setContinuation(3); |
| 297 | } else { |
| 298 | $this->siteConfig['currentUploadBatch']++; |
| 299 | $this->setContinuation(2, 2); |
| 300 | } |
| 301 | |
| 302 | } |
| 303 | |
| 304 | } |
| 305 | |
| 306 | public function validateAndPrepareRecipe() { |
| 307 | |
| 308 | $translatedUploadError2 = __('There was some error during the upload process, please try again.', 'backup-backup'); |
| 309 | $englishUploadError2 = 'There was some error during the upload process, please try again (#316).'; |
| 310 | |
| 311 | if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) { |
| 312 | |
| 313 | $this->log(__('Upload process finished successfully.', 'backup-backup'), 'success'); |
| 314 | $this->log(__('Preparing uploaded backup file for activation.', 'backup-backup'), 'step'); |
| 315 | $this->log(__('This process may take a while.', 'backup-backup'), 'warn'); |
| 316 | $this->log('At this moment TasteWP validates and prepares one-time use recipe.', 'verbose'); |
| 317 | |
| 318 | $url = $this->tastewpURL . '/api/bmi/staging/prepare'; |
| 319 | $data = [ |
| 320 | 'secret' => $this->siteConfig['communication_secret'], |
| 321 | 'upload' => $this->siteConfig['uploadId'], |
| 322 | 'entry' => $this->siteConfig['entry'] |
| 323 | ]; |
| 324 | |
| 325 | $response = $this->httpRequest($url, $data); |
| 326 | |
| 327 | if (!isset($response['status']) || $response['status'] != 'success') { |
| 328 | if (isset($response['message'])) $this->returnError($response['message'], $response['message']); |
| 329 | else $this->returnError($translatedUploadError2, $englishUploadError2); |
| 330 | } |
| 331 | |
| 332 | $this->progress(100); |
| 333 | $this->siteConfig['total_files'] = intval($response['fileAmount']); |
| 334 | $this->siteConfig['total_size'] = intval($response['recipeSize']); |
| 335 | $this->siteConfig['db_prefix'] = htmlspecialchars($response['tablePrefix']); |
| 336 | |
| 337 | sleep(2); |
| 338 | |
| 339 | $this->log(__('Your staging site is prepared for activation.', 'backup-backup'), 'success'); |
| 340 | $this->log('It means everything is fine with the backup file and user can initialize the site now with activation URL.', 'verbose'); |
| 341 | |
| 342 | } |
| 343 | |
| 344 | $this->setContinuation(4); |
| 345 | |
| 346 | } |
| 347 | |
| 348 | public function __destruct() { |
| 349 | |
| 350 | parent::__destruct(); |
| 351 | |
| 352 | } |
| 353 | |
| 354 | } |