Current Path : /var/www/mainsite/wp-content/plugins/duplicator-pro/src/Models/Storages/ |
Current File : /var/www/mainsite/wp-content/plugins/duplicator-pro/src/Models/Storages/LocalStorage.php |
<?php /** * * @package Duplicator * @copyright (c) 2022, Snap Creek LLC */ namespace Duplicator\Models\Storages; use DUP_PRO_Global_Entity; use DUP_PRO_Installer; use DUP_PRO_Log; use DUP_PRO_Package; use DUP_PRO_Package_Upload_Info; use Duplicator\Core\Views\TplMng; use Duplicator\Libs\Snap\SnapIO; use Duplicator\Libs\Snap\SnapUtil; use Duplicator\Package\Storage\StorageUploadChunkFiles; use Duplicator\Utils\PathUtil; use Exception; use wpdb; class LocalStorage extends AbstractStorageEntity { const LOCAL_STORAGE_CHUNK_SIZE_IN_MB = 16; /** * Get default config * * @return array<string,scalar> */ protected static function getDefaultCoinfig() { $config = parent::getDefaultCoinfig(); $config['storage_folder'] = ''; $config['purge_packages'] = true; $config['filter_protection'] = true; return $config; } /** * Wakeup method * * @return void */ public function __wakeup() { parent::__wakeup(); if ($this->legacyEntity) { // Old storage entity $this->legacyEntity = false; // Make sure the storage type is right from the old entity $this->storage_type = $this->getSType(); $this->config = [ 'storage_folder' => '/' . ltrim($this->local_storage_folder, '/\\'), 'max_packages' => $this->local_max_files, 'filter_protection' => $this->local_filter_protection, ]; // reset old values $this->local_storage_folder = ''; $this->local_max_files = 0; $this->local_filter_protection = true; } } /** * Return the storage type * * @return int */ public static function getSType() { return 0; } /** * Returns the FontAwesome storage type icon. * * @return string Returns the font-awesome icon */ public static function getStypeIcon() { return '<i class="fas fa-hdd fa-fw"></i>'; } /** * Returns the storage type name. * * @return string */ public static function getStypeName() { return __('Local', 'duplicator-pro'); } /** * Get priority, used to sort storages. * 100 is neutral value, 0 is the highest priority * * @return int */ public static function getPriority() { return 50; } /** * Returns true if storage type is local * * @return bool */ public static function isLocal() { return true; } /** * Get storage location string * * @return string */ public function getLocationString() { return $this->getStorageFolder(); } /** * Returns an html anchor tag of location * * @return string Returns an html anchor tag with the storage location as a hyperlink. * * @example * OneDrive Example return * <a target="_blank" href="https://1drv.ms/f/sAFrQtasdrewasyghg">https://1drv.ms/f/sAFrQtasdrewasyghg</a> */ public function getHtmlLocationLink() { return '<span>' . $this->getStorageFolder() . '</span>'; } /** * Check if storage is valid * * @return bool Return true if storage is valid and ready to use, false otherwise */ public function isValid() { return (is_dir($this->config['storage_folder']) && is_writable($this->config['storage_folder'])); } /** * Delete view * * @param bool $echo Echo or return * * @return string HTML string */ public function getDeleteView($echo = true) { ob_start(); ?> <div class="item"> <span class="lbl">Name:</span><?php echo esc_html($this->getName()); ?><br> <span class="lbl">Type:</span><?php echo $this->getStypeIcon(); ?> <?php echo esc_html($this->getStypeName()); ?><br> <span class="lbl">Folder:</span><?php echo esc_html($this->getLocationString()); ?><br> <span class="lbl">Note:</span><span class="maroon"> <i class="fas fa-exclamation-triangle"></i> <?php _e('By removing this storage all its packages inside it will be deleted.', 'duplicator-pro') ?> </span><br> </div> <?php if ($echo) { ob_end_flush(); return ''; } else { return (string) ob_get_clean(); } } /** * Is filter protection enabled * * @return bool */ public function isFilterProtection() { return $this->config['filter_protection']; } /** * Get action key text * * @param string $key Key name (action, pending, failed, cancelled, success) * * @return string */ protected function getActionKeyText($key) { switch ($key) { case 'action': return __('Copying to directory:', 'duplicator-pro') . '<br>' . $this->getStorageFolder(); case 'pending': return sprintf(__('Copy to directory %1$s is pending', "duplicator-pro"), $this->getStorageFolder()); case 'failed': return sprintf(__('Failed to copy to directory %1$s', "duplicator-pro"), $this->getStorageFolder()); case 'cancelled': return sprintf(__('Cancelled before could copy to directory %1$s', "duplicator-pro"), $this->getStorageFolder()); case 'success': return sprintf(__('Copied package to directory %1$s', "duplicator-pro"), $this->getStorageFolder()); default: throw new Exception('Invalid key'); } } /** * Create file * * @param string $fileName File name * @param string $content File content * * @return bool true if success, false otherwise */ protected function createFile($fileName, $content = '') { if (!$this->isValid()) { return false; } return (file_put_contents($this->getStorageFolder() . '/' . $fileName, $content) !== false); } /** * Delete file * * @param string $fileName File name * * @return bool true if success, false otherwise */ protected function deleteFile($fileName) { return SnapIO::unlink($this->getStorageFolder() . '/' . $fileName); } /** * Get file content * * @param string $fileName File name * * @return false|string false if file not found, file content otherwise */ protected function getFileContent($fileName) { if (!file_exists($this->getStorageFolder() . '/' . $fileName)) { return false; } return file_get_contents($this->getStorageFolder() . '/' . $fileName); } /** * File exists * * @param string $fileName File name * * @return bool */ protected function fileExists($fileName) { return file_exists($this->getStorageFolder() . '/' . $fileName); } /** * Render form config fields * * @param bool $echo Echo or return * * @return string */ public function renderConfigFields($echo = true) { return TplMng::getInstance()->render( 'admin_pages/storages/configs/local', [ 'storage' => $this, 'maxPackages' => $this->config['max_packages'], 'isFilderProtection' => $this->config['filter_protection'], 'storageFolder' => $this->config['storage_folder'], ], $echo ); } /** * Storages test * * @param string $message Test message * * @return bool return true if success, false otherwise */ public function test(&$message = '') { if (parent::test($message) == false) { return false; } $folder = $this->getStorageFolder(); $testFileName = 'dup_test_' . md5(uniqid((string) rand(), true)) . '.txt'; $this->testLog->addMessage(sprintf(__('Checking if directory exists "%1$s"', 'duplicator-pro'), $folder)); if (!is_dir($folder)) { $this->testLog->addMessage(sprintf(__( 'The storage path does not exists "%1$s"', 'duplicator-pro' ), $folder)); $message = __('The storage path does not exists', 'duplicator-pro'); return false; } $this->testLog->addMessage(sprintf(__('Checking if the directory is writable "%1$s"', 'duplicator-pro'), $folder)); if (!is_writable($folder)) { $this->testLog->addMessage(sprintf(__( 'The storage path is not writable "%1$s"', 'duplicator-pro' ), $folder)); $message = __('The storage path is not writable', 'duplicator-pro'); return false; } $this->testLog->addMessage(sprintf(__('Checking if the temporary file exists "%1$s"...', 'duplicator-pro'), $testFileName)); if ($this->fileExists($testFileName)) { $this->testLog->addMessage(sprintf(__( 'File with the temporary file name already exists, please try again "%1$s"', 'duplicator-pro' ), $testFileName)); $message = __('File with the temporary file name already exists, please try again', 'duplicator-pro'); return false; } $this->testLog->addMessage(sprintf(__('Creating temporary file "%1$s"...', 'duplicator-pro'), $testFileName)); if (!$this->createFile($testFileName)) { $this->testLog->addMessage( __( 'There was a problem when storing the temporary file', 'duplicator-pro' ) ); $message = __('There was a problem storing the temporary file', 'duplicator-pro'); return false; } $this->testLog->addMessage(sprintf(__('Checking if the temporary file exists "%1$s"...', 'duplicator-pro'), $testFileName)); /** @var bool */ $check = $this->fileExists($testFileName); if (!$check) { $this->testLog->addMessage(sprintf(__( 'The temporary file was not found "%1$s"', 'duplicator-pro' ), $testFileName)); $message = __('The temporary file was not found', 'duplicator-pro'); return false; } $this->testLog->addMessage(sprintf(__('Deleting temporary file "%1$s"...', 'duplicator-pro'), $testFileName)); if (!$this->deleteFile($testFileName)) { $this->testLog->addMessage(sprintf(__( 'There was a problem when deleting the temporary file "%1$s"', 'duplicator-pro' ), $testFileName)); $message = __('There was a problem deleting the temporary file', 'duplicator-pro'); return false; } $this->testLog->addMessage(__('Successfully stored and deleted file', 'duplicator-pro')); $message = __('Successfully stored and deleted file', 'duplicator-pro'); return true; } /** * Update data from http request, this method don't save data, just update object properties * * @param string $message Message * * @return bool True if success and all data is valid, false otherwise */ public function updateFromHttpRequest(&$message = '') { if ((parent::updateFromHttpRequest($message) === false)) { return false; } $this->config['filter_protection'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_local_filter_protection'); $this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'local_max_files', 10); $newFolder = self::getSanitizedInputFolder('_local_storage_folder', 'add'); if ($this->updateFolderCheck($newFolder, $message) === false) { return false; } if ($this->initStorageDirectory() == false) { $message = sprintf( __('Storage Provider Updated - Unable to init folder %1$s.', 'duplicator-pro'), $this->config['storage_folder'] ); return false; } $message = sprintf( __('Storage Provider Updated - Folder %1$s was created.', 'duplicator-pro'), $this->config['storage_folder'] ); return true; } /** * Update folder * * @param string $newFolder New folder * @param string $message Error message * * @return bool */ protected function updateFolderCheck($newFolder, &$message = '') { if ($this->config['storage_folder'] === $newFolder) { return true; } $this->config['storage_folder'] = $newFolder; if (strlen($this->config['storage_folder']) == 0) { $message = __('Local storage path can\'t be empty.', 'duplicator-pro'); return false; } if (PathUtil::isPathInCoreDirs($this->config['storage_folder'])) { $message = __( 'This storage path can\'t be used because it is a core WordPress directory or a sub-path of a core directory.', 'duplicator-pro' ); return false; } if ($this->isPathRepeated()) { $message = __( 'A local storage already exists or current folder is a child of another existing storage folder.', 'duplicator-pro' ); return false; } if (!self::isFolderEmpty($this->config['storage_folder'])) { $message = __('Selected storage path already exists and isn\'t empty select another path.', 'duplicator-pro') . ' ' . __('Select another folder or remove all files that are not backup archives.', 'duplicator-pro'); return false; } return true; } /** * Creates the snapshot directory if it doesn't already exists * * @param bool $skipIfExists If true it will skip creating the directory if it already exists * * @return bool True if success, false otherwise */ public function initStorageDirectory($skipIfExists = false) { $path = $this->getStorageFolder(); if (file_exists($path) && $skipIfExists) { if (is_dir($path)) { return true; } else { DUP_PRO_Log::infoTrace('Storage path exists but is not a directory: ' . $path); return false; } } if ((wp_mkdir_p($path) == false)) { return false; } SnapIO::chmod($path, 'u+rwx'); self::setupStorageHtaccess($path); self::setupStorageIndex($path); self::setupStorageDirRobotsFile($path); self::performHardenProcesses($path); return true; } /** * Copies the package files from the default local storage to another local storage location * * @param DUP_PRO_Package $package the package * @param DUP_PRO_Package_Upload_Info $upload_info the upload info * * @return void */ public function copyFromDefault(DUP_PRO_Package $package, DUP_PRO_Package_Upload_Info $upload_info) { DUP_PRO_Log::infoTrace("Copyng to Storage " . $this->name . '[ID: ' . $this->id . '] type:' . $this->getStypeName()); $sFolder = SnapIO::trailingslashit($this->config['storage_folder']); //must be $to => $from because array key has to be unique $replacements = array( $package->Installer->getSafeFilePath() => $sFolder . basename($package->Installer->getSafeFilePath()), $package->Archive->getSafeFilePath() => $sFolder . basename($package->Archive->getSafeFilePath()), ); $storageUpload = new StorageUploadChunkFiles( array( 'replacements' => $replacements, 'chunkSize' => DUP_PRO_Global_Entity::getInstance()->local_upload_chunksize_in_MB * MB_IN_BYTES, 'upload_info' => $upload_info, 'package' => $package, 'storage' => $this, ), 0, 1000 ); switch ($storageUpload->start()) { case StorageUploadChunkFiles::CHUNK_COMPLETE: DUP_PRO_Log::trace('LOCAL UPLOAD IN CHUNKS COMPLETED'); $upload_info->copied_installer = true; $upload_info->copied_archive = true; if ($this->config['max_packages'] > 0) { DUP_PRO_Log::trace('Purge old local packages'); $this->purgeOldPackages(); } break; case StorageUploadChunkFiles::CHUNK_STOP: DUP_PRO_Log::trace('LOCAL UPLOAD IN CHUNKS NOT COMPLETED >> CONTINUE NEXT CHUNK'); //do nothing for now break; case StorageUploadChunkFiles::CHUNK_ERROR: default: DUP_PRO_Log::infoTrace('Local upload in chunks, upload error: ' . $storageUpload->getLastErrorMessage()); $upload_info->failed = true; } $package->update(); } /** * Purge old packages * * @return bool true if success, false otherwise */ public function purgeOldPackages() { if ($this->config['max_packages'] <= 0) { return true; } DUP_PRO_Log::infoTrace("Attempting to purge old packages at " . $this->name . '[ID: ' . $this->id . '] type:' . $this->getSTypeName()); /** @var wpdb $wpdb*/ global $wpdb; try { $fileList = SnapIO::regexGlob($this->config['storage_folder'], array( 'regexFile' => array(DUPLICATOR_PRO_ARCHIVE_REGEX_PATTERN), 'regexFolder' => false, )); //Sort by creation time usort($fileList, function ($a, $b) { return filemtime($a) - filemtime($b); }); $nameHashesToPurge = array(); for ($i = 0; $i < count($fileList) - $this->config['max_packages']; $i++) { $nameHash = substr(basename($fileList[$i]), 0, -12); /** @var string */ $prepare = $wpdb->prepare("%s", $nameHash); $nameHashesToPurge[] = $prepare; DUP_PRO_Package::deletePackageFilesInDir($nameHash, $this->config['storage_folder'], true); } // Purge package record logic if ($this->config['purge_packages'] && count($nameHashesToPurge) > 0) { $table = DUP_PRO_Package::getTableName(); $max_created = $wpdb->get_var( "SELECT max(created) FROM `" . $table . "` WHERE concat_ws('_', name, hash) IN (" . implode(', ', $nameHashesToPurge) . ")" ); $sql = $wpdb->prepare("DELETE FROM " . $table . " WHERE created <= %s AND status = %d", $max_created, 100); $wpdb->query($sql); } } catch (Exception $e) { DUP_PRO_Log::infoTraceException($e, "FAIL: purge package for storage " . $this->name . '[ID: ' . $this->id . '] type:' . $this->getStypeName()); return false; } DUP_PRO_Log::infoTrace("Purge of old packages at " . $this->name . '[ID: ' . $this->id . "] storage completed."); return true; } /** * Delete this storage * * @return bool True on success, or false on error. */ public function delete() { if (parent::delete() === false) { return false; } if (self::isFolderEmpty($this->config['storage_folder'])) { SnapIO::rrmdir($this->config['storage_folder']); } else { // Don't delete the folder if it's not empty but don't show an error DUP_PRO_Log::infoTrace("Storage folder is not empty, can't delete it"); } return true; } /** * Checks if the storage path is already used by another local storage or is a child of another local storage * * @return bool Whether the storage path is already used by another local storage */ protected function isPathRepeated() { $storages = self::getAll(); foreach ($storages as $storage) { if ( !$storage->isLocal() || $storage->id == $this->id ) { continue; } if ( SnapIO::isChildPath( $this->config['storage_folder'], $storage->getStorageFolder(), false, true, true ) ) { return true; } } return false; } /** * Attempts to create a secure .htaccess file in the download directory * * @param string $path The folder path * * @return bool True if success, false otherwise */ protected static function setupStorageHtaccess($path) { try { $fileName = SnapIO::safePathTrailingslashit($path) . '.htaccess'; if (DUP_PRO_Global_Entity::getInstance()->storage_htaccess_off) { @unlink($fileName); } elseif (!file_exists($fileName)) { $fileContent = <<<FILECONTENT # Duplicator config, In case of file downloading problem, you can disable/enable it in Settings/Sotrag plugin settings Options -Indexes <IfModule mod_headers.c> <FilesMatch "\.(daf)$"> ForceType application/octet-stream Header set Content-Disposition attachment </FilesMatch> </IfModule> FILECONTENT; if (file_put_contents($fileName, $fileContent) === false) { throw new Exception('Can\'t create ' . $fileName); } } } catch (Exception $ex) { DUP_PRO_Log::Trace("Unable create file htaccess {$fileName} msg:" . $ex->getMessage()); return false; } return true; } /** * Attempts to create an index.php file in the backups directory * * @param string $path The folder path * * @return bool True if success, false otherwise */ protected static function setupStorageIndex($path) { if (!SnapIO::createSilenceIndex($path)) { DUP_PRO_Log::Trace("Unable create index.php at {$path}"); return false; } return true; } /** * Attempts to create a robots.txt file in the backups directory * to prevent search engines * * @param string $path The folder path * * @return bool True if success, false otherwise */ protected static function setupStorageDirRobotsFile($path) { try { $fileName = SnapIO::safePathTrailingslashit($path) . 'robots.txt'; if (!file_exists($fileName)) { $fileContent = <<<FILECONTENT User-agent: * Disallow: / FILECONTENT; if (file_put_contents($fileName, $fileContent) === false) { throw new Exception('Can\'t create ' . $fileName); } } } catch (Exception $ex) { DUP_PRO_Log::Trace("Unable create robots.txt {$fileName} msg:" . $ex->getMessage()); return false; } return true; } /** * Run various secure processes to harden the backups dir * * @param string $path The folder path * * @return bool True if success, false otherwise */ public static function performHardenProcesses($path) { try { $backupsDir = SnapIO::safePathTrailingslashit($path); //Edge Case: Remove any installer dirs $dupInstallFolder = $backupsDir . "dup-installer"; if (file_exists($dupInstallFolder)) { SnapIO::rrmdir($dupInstallFolder); } // Rename installer php files to .bak SnapIO::regexGlobCallback( $backupsDir, function ($path) { $parts = pathinfo($path); $newPath = $parts['dirname'] . '/' . $parts['filename'] . DUP_PRO_Installer::INSTALLER_SERVER_EXTENSION; SnapIO::rename($path, $newPath); }, array( 'regexFile' => '/^.+_installer.*\.php$/', 'regexFolder' => false, 'recursive' => true, ) ); } catch (Exception $ex) { DUP_PRO_Log::Trace("Unable to cleanup the storage folder {$path} msg:" . $ex->getMessage()); return false; } return true; } /** * Check if folder is empty or have only package files * * @param string $path The folder path * * @return bool True is ok, false otherwise */ protected static function isFolderEmpty($path) { if (!file_exists($path)) { return true; } elseif (!is_dir($path)) { return false; } $acceptFiles = [ 'index.php', 'robots.txt', '.htaccess', 'index.html', ]; try { SnapIO::regexGlobCallback( $path, function ($path) use ($acceptFiles) { if (is_dir($path)) { throw new Exception('Folder have subfolders', 10); } if (!in_array(basename($path), $acceptFiles)) { throw new Exception('Folder isn\'t empty', 10); } return; }, array( 'regexFile' => array(DUPLICATOR_PRO_GEN_FILE_REGEX_PATTERN), 'regexFolder' => false, 'recursive' => false, 'invert' => true, ) ); } catch (Exception $ex) { if ($ex->getCode() == 10) { return false; } throw $ex; } return true; } }