JetBackupLinux.php
| 1 | <?php |
| 2 | |
| 3 | namespace JetBackup\JetBackupLinux; |
| 4 | |
| 5 | use JetBackup\BackupJob\BackupJob; |
| 6 | use JetBackup\Config\System; |
| 7 | use JetBackup\Data\Engine; |
| 8 | use JetBackup\Entities\Util; |
| 9 | use JetBackup\Exception\DBException; |
| 10 | use JetBackup\Exception\JetBackupLinuxException; |
| 11 | use JetBackup\Exception\QueueException; |
| 12 | use JetBackup\Factory; |
| 13 | use JetBackup\JetBackup; |
| 14 | use JetBackup\Queue\Queue; |
| 15 | use JetBackup\Queue\QueueItem; |
| 16 | use JetBackup\Queue\QueueItemReindex; |
| 17 | use JetBackup\Snapshot\Snapshot; |
| 18 | use JetBackup\SocketAPI\Client\Client; |
| 19 | use JetBackup\SocketAPI\Exception\SocketAPIException; |
| 20 | use JetBackup\SocketAPI\SocketAPI; |
| 21 | use JetBackup\Wordpress\Wordpress; |
| 22 | use SleekDB\Exceptions\InvalidArgumentException; |
| 23 | use SleekDB\Exceptions\IOException; |
| 24 | |
| 25 | if (!defined( '__JETBACKUP__')) die('Direct access is not allowed'); |
| 26 | |
| 27 | class JetBackupLinux { |
| 28 | |
| 29 | const BACKUP_TYPE_ACCOUNT = 1; |
| 30 | |
| 31 | const BACKUP_TYPE_ACCOUNT_CONFIG = 1<<0; |
| 32 | const BACKUP_TYPE_ACCOUNT_HOMEDIR = 1<<1; |
| 33 | const BACKUP_TYPE_ACCOUNT_DATABASES = 1<<2; |
| 34 | const BACKUP_TYPE_ACCOUNT_EMAILS = 1<<3; |
| 35 | const BACKUP_TYPE_ACCOUNT_CRON_JOBS = 1<<4; |
| 36 | const BACKUP_TYPE_ACCOUNT_DOMAINS = 1<<5; |
| 37 | const BACKUP_TYPE_ACCOUNT_CERTIFICATES = 1<<6; |
| 38 | const BACKUP_TYPE_ACCOUNT_DATABASE_USERS = 1<<7; |
| 39 | const BACKUP_TYPE_ACCOUNT_FTP = 1<<8; |
| 40 | const BACKUP_TYPE_ACCOUNT_FULL = |
| 41 | self::BACKUP_TYPE_ACCOUNT_CONFIG | |
| 42 | self::BACKUP_TYPE_ACCOUNT_HOMEDIR | |
| 43 | self::BACKUP_TYPE_ACCOUNT_DATABASES | |
| 44 | self::BACKUP_TYPE_ACCOUNT_EMAILS | |
| 45 | self::BACKUP_TYPE_ACCOUNT_CRON_JOBS | |
| 46 | self::BACKUP_TYPE_ACCOUNT_DOMAINS | |
| 47 | self::BACKUP_TYPE_ACCOUNT_CERTIFICATES | |
| 48 | self::BACKUP_TYPE_ACCOUNT_DATABASE_USERS | |
| 49 | self::BACKUP_TYPE_ACCOUNT_FTP; |
| 50 | |
| 51 | const BACKUP_STRUCTURE_INCREMENTAL = 1; |
| 52 | const BACKUP_STRUCTURE_ARCHIVED = 2; |
| 53 | const BACKUP_STRUCTURE_COMPRESSED = 4; |
| 54 | |
| 55 | const BACKUP_STRUCTURE_DEDUPLICATION = 8; |
| 56 | |
| 57 | |
| 58 | |
| 59 | const QUEUE_STATUS_RESTORE_ACCOUNT_CONFIG = 30; |
| 60 | const QUEUE_STATUS_RESTORE_ACCOUNT_DOMAINS = 31; |
| 61 | const QUEUE_STATUS_RESTORE_ACCOUNT_CERTIFICATES = 32; |
| 62 | const QUEUE_STATUS_RESTORE_ACCOUNT_FTP = 33; |
| 63 | const QUEUE_STATUS_RESTORE_ACCOUNT_CRON_JOBS = 34; |
| 64 | const QUEUE_STATUS_RESTORE_ACCOUNT_IMPORTING_DATABASES = 35; |
| 65 | const QUEUE_STATUS_RESTORE_ACCOUNT_DATABASES = 36; |
| 66 | const QUEUE_STATUS_RESTORE_ACCOUNT_DATABASE_USERS = 37; |
| 67 | const QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR = 38; |
| 68 | const QUEUE_STATUS_RESTORE_ACCOUNT_EMAILS = 39; |
| 69 | const QUEUE_STATUS_RESTORE_ACCOUNT_POST_RESTORE = 40; |
| 70 | const QUEUE_STATUS_RESTORE_ACCOUNT_PRE_RESTORE = 41; |
| 71 | |
| 72 | const QUEUE_STATUS_RESTORE_MAPPING = [ |
| 73 | self::QUEUE_STATUS_RESTORE_ACCOUNT_IMPORTING_DATABASES => Queue::STATUS_RESTORE_JB_IMPORTING_DB, |
| 74 | self::QUEUE_STATUS_RESTORE_ACCOUNT_DATABASES => Queue::STATUS_RESTORE_JB_DATABASES, |
| 75 | self::QUEUE_STATUS_RESTORE_ACCOUNT_DATABASE_USERS => Queue::STATUS_RESTORE_JB_DATABASE_USERS, |
| 76 | self::QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR => Queue::STATUS_RESTORE_JB_HOMEDIR, |
| 77 | self::QUEUE_STATUS_RESTORE_ACCOUNT_POST_RESTORE => Queue::STATUS_RESTORE_JB_POST, |
| 78 | self::QUEUE_STATUS_RESTORE_ACCOUNT_PRE_RESTORE => Queue::STATUS_RESTORE_JB_PRE, |
| 79 | ]; |
| 80 | |
| 81 | const QUEUE_STATUS_RESTORE_ACCOUNT_NAMES = [ |
| 82 | self::QUEUE_STATUS_RESTORE_ACCOUNT_CONFIG => "Restoring Panel Configurations", |
| 83 | self::QUEUE_STATUS_RESTORE_ACCOUNT_DOMAINS => "Restoring Domains and DNS", |
| 84 | self::QUEUE_STATUS_RESTORE_ACCOUNT_CERTIFICATES => "Restoring SSL Certificates", |
| 85 | self::QUEUE_STATUS_RESTORE_ACCOUNT_FTP => "Restoring FTP Accounts", |
| 86 | self::QUEUE_STATUS_RESTORE_ACCOUNT_CRON_JOBS => "Restoring Cron Jobs", |
| 87 | self::QUEUE_STATUS_RESTORE_ACCOUNT_IMPORTING_DATABASES => "Importing databases data", |
| 88 | self::QUEUE_STATUS_RESTORE_ACCOUNT_DATABASES => "Restoring Databases", |
| 89 | self::QUEUE_STATUS_RESTORE_ACCOUNT_DATABASE_USERS => "Restoring Database Users", |
| 90 | self::QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR => "Restoring Home Directory files", |
| 91 | self::QUEUE_STATUS_RESTORE_ACCOUNT_EMAILS => "Restoring Email Accounts", |
| 92 | self::QUEUE_STATUS_RESTORE_ACCOUNT_POST_RESTORE => "Performing post restore actions", |
| 93 | self::QUEUE_STATUS_RESTORE_ACCOUNT_PRE_RESTORE => "Performing pre restore actions", |
| 94 | ]; |
| 95 | |
| 96 | |
| 97 | const QUEUE_STATUS_COMPLETED = 100; |
| 98 | const QUEUE_STATUS_PARTIALLY_COMPLETED = 101; |
| 99 | const QUEUE_STATUS_FAILED = 102; |
| 100 | const QUEUE_STATUS_ABORTED = 103; |
| 101 | const QUEUE_STATUS_NEVER_FINISHED = 104; |
| 102 | |
| 103 | const QUEUE_STATUS_MAPPING = [ |
| 104 | self::QUEUE_STATUS_COMPLETED => Queue::STATUS_DONE, |
| 105 | self::QUEUE_STATUS_PARTIALLY_COMPLETED => Queue::STATUS_PARTIALLY, |
| 106 | self::QUEUE_STATUS_FAILED => Queue::STATUS_FAILED, |
| 107 | self::QUEUE_STATUS_ABORTED => Queue::STATUS_ABORTED, |
| 108 | self::QUEUE_STATUS_NEVER_FINISHED => Queue::STATUS_NEVER_FINISHED, |
| 109 | ]; |
| 110 | |
| 111 | const QUEUE_TYPE_RESTORE = 1<<1; |
| 112 | |
| 113 | const MINIMUM_VERSION = '5.3.15'; |
| 114 | |
| 115 | private function __construct() {} |
| 116 | |
| 117 | public static function isEnabled():bool { |
| 118 | return Factory::getSettingsGeneral()->isJBIntegrationEnabled() && self::isInstalled(); |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Return true will allow to check if socket file exist, we will not continue to check for socket file is this return false |
| 123 | * @return bool |
| 124 | */ |
| 125 | private static function isOpenBaseDir(): bool |
| 126 | { |
| 127 | $openBasedir = ini_get('open_basedir'); |
| 128 | if (!$openBasedir) return true; |
| 129 | |
| 130 | $socket = realpath(Client::SOCKET_FILE) ?: Client::SOCKET_FILE; |
| 131 | |
| 132 | foreach (explode(PATH_SEPARATOR, $openBasedir) as $allowed) { |
| 133 | $allowed = trim($allowed); |
| 134 | if ($allowed === '') continue; |
| 135 | |
| 136 | // Normalize to "dir/" form |
| 137 | $allowedReal = realpath($allowed) ?: $allowed; |
| 138 | $allowedReal = rtrim($allowedReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
| 139 | |
| 140 | if (str_starts_with($socket, $allowedReal)) return true; |
| 141 | } |
| 142 | |
| 143 | return false; |
| 144 | } |
| 145 | |
| 146 | |
| 147 | private static function isSocket(): bool { |
| 148 | return file_exists(Client::SOCKET_FILE); |
| 149 | } |
| 150 | |
| 151 | |
| 152 | /** |
| 153 | * Using this in the background for different implementations, just to verify if JBLinux is installed |
| 154 | * @return bool |
| 155 | */ |
| 156 | public static function isInstalled():bool { |
| 157 | return !System::isWindowsOS() && self::isOpenBaseDir() && self::isSocket(); |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * Using this for Ajax validation |
| 162 | * @return void |
| 163 | * @throws JetBackupLinuxException |
| 164 | */ |
| 165 | public static function checkRequirements():void { |
| 166 | // Check for blocking requirements first (these prevent further checks) |
| 167 | if(System::isWindowsOS()) throw new JetBackupLinuxException("This feature is not supported in Windows operating system"); |
| 168 | if(!self::isOpenBaseDir()) throw new JetBackupLinuxException("'open_basedir' restriction is enabled, make sure " . dirname(Client::SOCKET_FILE) . " is available"); |
| 169 | |
| 170 | // Collect all missing function errors at once for better user experience |
| 171 | $missingFunctions = []; |
| 172 | if(!Util::has_posix_getpwuid()) $missingFunctions[] = 'posix_getpwuid'; |
| 173 | if(!Util::has_posix_getgrgid()) $missingFunctions[] = 'posix_getgrgid'; |
| 174 | if(!Util::has_posix_geteuid()) $missingFunctions[] = 'posix_geteuid'; |
| 175 | if(!function_exists('socket_connect')) $missingFunctions[] = 'socket_connect'; |
| 176 | |
| 177 | if(!empty($missingFunctions)) { |
| 178 | $functionList = implode(', ', $missingFunctions); |
| 179 | $plural = count($missingFunctions) > 1 ? 's' : ''; |
| 180 | throw new JetBackupLinuxException("Required function{$plural} '{$functionList}' disabled or not installed. Enable to allow Socket API."); |
| 181 | } |
| 182 | |
| 183 | if (!self::isSocket()) throw new JetBackupLinuxException("Cannot find JetBackup Linux socket"); |
| 184 | |
| 185 | try { |
| 186 | $response = SocketAPI::api('getMyAccount')->execute(); |
| 187 | } catch(SocketAPIException $e) { |
| 188 | throw new JetBackupLinuxException($e->getMessage()); |
| 189 | } |
| 190 | |
| 191 | if(!isset($response['system']['version'])) throw new JetBackupLinuxException("Failed fetching JetBackup linux version"); |
| 192 | if(version_compare(self::MINIMUM_VERSION, $response['system']['version'], '>')) throw new JetBackupLinuxException("JetBackup linux version is not compatible ({$response['system']['version']}), Minimum required version is " . self::MINIMUM_VERSION); |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * @return array|mixed |
| 197 | * @throws JetBackupLinuxException |
| 198 | * @throws SocketAPIException |
| 199 | */ |
| 200 | public static function getAccountInfo() { |
| 201 | return Query::api('getMyAccount')->execute(); |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * @param string $id |
| 206 | * @param string $path |
| 207 | * @param int $limit |
| 208 | * @param int $skip |
| 209 | * @param array $sort |
| 210 | * |
| 211 | * @return array|mixed |
| 212 | * @throws JetBackupLinuxException |
| 213 | * @throws SocketAPIException |
| 214 | */ |
| 215 | public static function fileManager(string $id, string $path, int $limit=0, int $skip=0, array $sort=[]) { |
| 216 | $query = Query::api('fileManager') |
| 217 | ->arg('type', 'backup') |
| 218 | ->arg('_id', $id) |
| 219 | ->arg('path', $path); |
| 220 | |
| 221 | if($limit || $skip) $query->limit($limit, $skip); |
| 222 | |
| 223 | foreach($sort as $key => $direction) { |
| 224 | if($direction > 0) $query->sortAsc($key); |
| 225 | else if($direction < 0) $query->sortDesc($key); |
| 226 | } |
| 227 | |
| 228 | return $query->execute(); |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * @param array $items |
| 233 | * @param array $files |
| 234 | * @param array $options |
| 235 | * |
| 236 | * @return array|mixed |
| 237 | * @throws JetBackupLinuxException |
| 238 | * @throws SocketAPIException |
| 239 | */ |
| 240 | public static function addQueueItems(array $items, array $files=[], array $options=[]) { |
| 241 | |
| 242 | // Force exclude plugin's main folder and datadir |
| 243 | if(!isset($options['exclude'])) $options['exclude'] = []; |
| 244 | $options['exclude'][] = Factory::getWPHelper()->getWordPressRelativePublicDir() . |
| 245 | JetBackup::SEP . Wordpress::WP_CONTENT . |
| 246 | JetBackup::SEP . Wordpress::WP_PLUGINS . |
| 247 | JetBackup::SEP . JetBackup::PLUGIN_NAME; |
| 248 | if (Factory::getLocations()->getPublicDataDir()) { |
| 249 | $options['exclude'][] = Factory::getWPHelper()->getWordPressRelativePublicDir() . JetBackup::SEP . Factory::getLocations()->getPublicDataDir(); |
| 250 | } else { |
| 251 | $options['exclude'][] = Factory::getLocations()->getRelativeDataDir(); |
| 252 | } |
| 253 | |
| 254 | return Query::api('addQueueItems') |
| 255 | ->arg('type', self::QUEUE_TYPE_RESTORE) |
| 256 | ->arg('items', $items) |
| 257 | ->arg('files', $files) |
| 258 | ->arg('options', $options) |
| 259 | ->execute(); |
| 260 | } |
| 261 | |
| 262 | |
| 263 | /** |
| 264 | * @param array $sort |
| 265 | * |
| 266 | * @return array |
| 267 | * @throws JetBackupLinuxException|SocketAPIException |
| 268 | */ |
| 269 | public static function listQueueGroups(array $sort=[]):array { |
| 270 | $query = Query::api('listQueueGroups') |
| 271 | ->arg('type', self::QUEUE_TYPE_RESTORE) |
| 272 | ->limit(9999999); |
| 273 | |
| 274 | foreach($sort as $key => $direction) { |
| 275 | if($direction > 0) $query->sortAsc($key); |
| 276 | else if($direction < 0) $query->sortDesc($key); |
| 277 | } |
| 278 | |
| 279 | $response = $query->execute(); |
| 280 | |
| 281 | return $response['groups']; |
| 282 | } |
| 283 | |
| 284 | /** |
| 285 | * @param string $group_id |
| 286 | * |
| 287 | * @return array |
| 288 | * @throws JetBackupLinuxException|SocketAPIException |
| 289 | */ |
| 290 | public static function getQueueGroup(string $group_id) { |
| 291 | return Query::api('getQueueGroup')->arg('_id', $group_id)->execute(); |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * @param string $group_id |
| 296 | * @param array $sort |
| 297 | * |
| 298 | * @return array |
| 299 | * @throws JetBackupLinuxException|SocketAPIException |
| 300 | */ |
| 301 | public static function listQueueItems(string $group_id, array $sort=[]):array { |
| 302 | |
| 303 | $query = Query::api('listQueueItems') |
| 304 | ->arg('group_id', $group_id) |
| 305 | ->limit(9999999); |
| 306 | |
| 307 | foreach($sort as $key => $direction) { |
| 308 | if($direction > 0) $query->sortAsc($key); |
| 309 | else if($direction < 0) $query->sortDesc($key); |
| 310 | } |
| 311 | |
| 312 | $response = $query->execute(); |
| 313 | |
| 314 | return $response['items']; |
| 315 | } |
| 316 | |
| 317 | /** |
| 318 | * @param array $sort |
| 319 | * |
| 320 | * @return array |
| 321 | * @throws JetBackupLinuxException |
| 322 | * @throws SocketAPIException |
| 323 | */ |
| 324 | public static function listBackups(array $sort=[], int $limit=25, int $skip=0):array { |
| 325 | |
| 326 | $query = Query::api('listBackupsWithItems') |
| 327 | ->arg('type', self::BACKUP_TYPE_ACCOUNT) |
| 328 | ->arg('structure', self::BACKUP_STRUCTURE_INCREMENTAL) |
| 329 | ->limit($limit, $skip); |
| 330 | |
| 331 | foreach($sort as $key => $direction) { |
| 332 | if($direction > 0) $query->sortAsc($key); |
| 333 | else if($direction < 0) $query->sortDesc($key); |
| 334 | } |
| 335 | |
| 336 | $response = $query->execute(); |
| 337 | |
| 338 | return $response['backups']; |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * @return void |
| 343 | * @throws IOException |
| 344 | * @throws InvalidArgumentException |
| 345 | * @throws DBException |
| 346 | */ |
| 347 | public static function deleteSnapshots():void { |
| 348 | |
| 349 | $list = Snapshot::query() |
| 350 | ->where([Engine::ENGINE, '=', Engine::ENGINE_JB]) |
| 351 | ->getQuery() |
| 352 | ->fetch(); |
| 353 | |
| 354 | if (empty($list)) return; |
| 355 | |
| 356 | foreach ($list as $item) { |
| 357 | $snapshot = new Snapshot($item[JetBackup::ID_FIELD]); |
| 358 | if(!$snapshot->getId()) continue; |
| 359 | $snapshot->delete(); |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | /** |
| 364 | * @return void |
| 365 | * @throws IOException |
| 366 | * @throws InvalidArgumentException |
| 367 | * @throws QueueException |
| 368 | * @throws JetBackupLinuxException |
| 369 | */ |
| 370 | public static function addToQueue():void { |
| 371 | |
| 372 | if(!Factory::getSettingsGeneral()->isJBIntegrationEnabled()) throw new JetBackupLinuxException("JetBackup Linux integration is disabled"); |
| 373 | if(!self::isInstalled()) throw new JetBackupLinuxException("JetBackup Linux integration is not installed"); |
| 374 | |
| 375 | $reindex = new QueueItemReindex(); |
| 376 | $queue_item = QueueItem::prepare(); |
| 377 | $queue_item->setType(Queue::QUEUE_TYPE_REINDEX); |
| 378 | $queue_item->setItemId(0); |
| 379 | $queue_item->setItemData($reindex); |
| 380 | |
| 381 | Queue::addToQueue($queue_item); |
| 382 | } |
| 383 | |
| 384 | public static function prepareFilesForRestore($homedir_snapID, $fileManger): array { |
| 385 | |
| 386 | $public_dir = trim(Factory::getWPHelper()->getWordPressHomedir(true), JetBackup::SEP); |
| 387 | $files = []; |
| 388 | if(!sizeof($fileManger)) return $files; |
| 389 | |
| 390 | foreach ($fileManger as $file) { |
| 391 | $path = JetBackup::SEP . $public_dir . JetBackup::SEP . trim($file['path'], JetBackup::SEP); |
| 392 | $files[$homedir_snapID][$path] = $file['type']; |
| 393 | } |
| 394 | |
| 395 | return $files; |
| 396 | |
| 397 | } |
| 398 | } |