path */ private $path; /** * The path to the directory that will be backed up * * @var string $this->root */ private $root; /** * The path to the directory that backup files are stored in * * @var string $this->path */ private $custom_path; /** * Contains the instantiated Path instance * * @var Path $this->instance */ private static $instance; /** * Protected constructor to prevent creating a new instance of the * *Singleton* via the `new` operator from outside of this class. */ protected function __construct() {} /** * Private clone method to prevent cloning of the instance of the * *Singleton* instance. */ private function __clone() {} /** * Private unserialize method to prevent unserializing of the *Singleton* * instance. */ private function __wakeup() {} /** * Returns the *Singleton* instance of this class. * * @staticvar Path $instance The *Singleton* instances of this class. * * @return Path The *Singleton* instance. */ public static function get_instance() { if ( ! ( self::$instance instanceof Path ) ) { self::$instance = new Path(); } return self::$instance; } /** * Convenience method for quickly grabbing the path */ public static function get_path() { return self::get_instance()->get_calculated_path(); } /** * Convenience method for quickly grabbing the root */ public static function get_root() { return self::get_instance()->get_calculated_root(); } /** * Calculate the path to the site "home" directory. * * The home directory is the path equivalent to the home_url. That is, * the path to the true root of the website. In situations where WordPress is * installed in a subdirectory the home path is different to ABSPATH * * @param string $site_path The site_path to use when calculating the home path, defaults to ABSPATH */ public static function get_home_path( $site_path = ABSPATH ) { if ( defined( 'HMBKP_ROOT' ) && HMBKP_ROOT ) { return wp_normalize_path( HMBKP_ROOT ); } $home_path = wp_normalize_path( $site_path ); if ( path_in_php_open_basedir( dirname( $site_path ) ) ) { $home = set_url_scheme( get_option( 'home' ), 'http' ); $siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' ); if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) { $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */ $pos = strripos( wp_normalize_path( $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) ); $home_path = substr( wp_normalize_path( $_SERVER['SCRIPT_FILENAME'] ), 0, $pos ); $home_path = trailingslashit( $home_path ); } if ( is_multisite() ) { $slashed_home = trailingslashit( get_option( 'home' ) ); $base = parse_url( $slashed_home, PHP_URL_PATH ); $document_root_fix = wp_normalize_path( realpath( $_SERVER['DOCUMENT_ROOT'] ) ); $abspath_fix = wp_normalize_path( ABSPATH ); $home_path = strpos( $abspath_fix, $document_root_fix ) === 0 ? $document_root_fix . $base : $home_path; } } return wp_normalize_path( untrailingslashit( $home_path ) ); } /** * get the calculated path to the directory where backups will be stored */ private function get_calculated_path() { // Calculate the path if needed if ( empty( $this->path ) || ! wp_is_writable( $this->path ) ) { $this->calculate_path(); } // Ensure the backup directory is protected $this->protect_path(); return wp_normalize_path( $this->path ); } /** * Set the path directly, overriding the default * * @param $path */ public function set_path( $path ) { $this->custom_path = $path; // Re-calculate the backup path $this->calculate_path(); } /** * get the calculated path to the directory that will be backed up */ private function get_calculated_root() { $root = self::get_home_path(); if ( defined( 'HMBKP_ROOT' ) && HMBKP_ROOT ) { $root = HMBKP_ROOT; } if ( $this->root ) { $root = $this->root; } return wp_normalize_path( $root ); } /** * Set the root path directly, overriding the default * * @param $root */ public function set_root( $root ) { $this->root = $root; } public function reset_path() { $this->path = $this->custom_path = ''; } /** * Get the path to the default backup location in wp-content */ public function get_default_path() { return trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ) . 'backupwordpress-' . substr( HMBKP_SECURE_KEY, 0, 10 ) . '-backups'; } /** * Get the path to the fallback backup location in uploads */ public function get_fallback_path() { $upload_dir = wp_upload_dir(); return trailingslashit( wp_normalize_path( $upload_dir['basedir'] ) ) . 'backupwordpress-' . substr( HMBKP_SECURE_KEY, 0, 10 ) . '-backups'; } /** * Get the path to the custom backup location if it's been set */ public function get_custom_path() { if ( $this->custom_path ) { return $this->custom_path; } if ( defined( 'HMBKP_PATH' ) && wp_is_writable( HMBKP_PATH ) ) { return HMBKP_PATH; } return ''; } /** * Builds an array containing existing backups folders. * * @return array */ public function get_existing_paths() { if ( false === $default = glob( WP_CONTENT_DIR . '/backupwordpress-*-backups', GLOB_ONLYDIR ) ) { $default = array(); } $upload_dir = wp_upload_dir(); if ( false === $fallback = glob( $upload_dir['basedir'] . '/backupwordpress-*-backups', GLOB_ONLYDIR ) ) { $fallback = array(); } $paths = array_merge( $default, $fallback ); $paths = array_map( 'wp_normalize_path', $paths ); return $paths; } /** * Returns the first existing path if there is one * * @return string Backup path if found empty string if not */ public function get_existing_path() { $paths = $this->get_existing_paths(); if ( ! empty( $paths[0] ) ) { return $paths[0]; } return ''; } /** * Calculate the backup path and create the directory if it doesn't exist. * * Tries all possible locations and uses the first one possible. * * @return */ public function calculate_path() { $paths = $this->get_possible_paths(); // Loop through possible paths, use the first one that exists/can be created and is writable. foreach ( $paths as $path ) { // Also handles fixing perms / directory already exists. if ( wp_mkdir_p( $path ) && wp_is_writable( $path ) ) { break; } } /** * If we managed to create a writable path then use that, * otherwise just return the unwritable path. */ if ( file_exists( $path ) && wp_is_writable( $path ) ) { $this->path = $path; } else { $this->path = reset( $paths ); } } public function get_possible_paths() { $paths = array(); // If we have a custom path then try to use it. if ( $this->get_custom_path() ) { $paths[] = $this->get_custom_path(); } // If there is already a backups directory then try to use that. if ( $this->get_existing_path() ) { $paths[] = $this->get_existing_path(); } // If not then default to a new directory in wp-content. $paths[] = $this->get_default_path(); // If that didn't work then fallback to a new directory in uploads. $paths[] = $this->get_fallback_path(); return $paths; } /** * Protect the directory that backups are stored in * * - Adds an index.html file in an attempt to disable directory browsing * - Adds a .httaccess file to deny direct access if on Apache * * @param string $reset */ public function protect_path( $reset = 'no' ) { global $is_apache; // Protect against directory browsing by including an index.html file $index = $this->path . '/index.html'; if ( 'reset' === $reset && file_exists( $index ) ) { @unlink( $index ); } if ( ! file_exists( $index ) && wp_is_writable( $this->path ) ) { file_put_contents( $index, '' ); } $htaccess = $this->path . '/.htaccess'; if ( ( 'reset' === $reset ) && file_exists( $htaccess ) ) { @unlink( $htaccess ); } // Protect the directory with a .htaccess file on Apache servers if ( $is_apache && function_exists( 'insert_with_markers' ) && ! file_exists( $htaccess ) && wp_is_writable( $this->path ) ) { $contents = array(); $contents[] = '# ' . sprintf( __( 'This %s file ensures that other people cannot download your backup files.', 'backupwordpress' ), '.htaccess' ); $contents[] = ''; $contents[] = ''; $contents[] = 'RewriteEngine On'; $contents[] = 'RewriteCond %{QUERY_STRING} !key=' . HMBKP_SECURE_KEY; $contents[] = 'RewriteRule (.*) - [F]'; $contents[] = ''; $contents[] = ''; file_put_contents( $htaccess, '' ); insert_with_markers( $htaccess, 'BackUpWordPress', $contents ); } } /** * If we have more than one path then move any existing backups to the current path and remove them */ public function merge_existing_paths() { $paths = $this->get_existing_paths(); if ( ( ! empty( $paths ) && $this->get_custom_path() ) || count( $paths ) > 1 ) { foreach ( $paths as $old_path ) { $this->move_old_backups( $old_path ); } } } /** * Move backup files from an existing directory and the new * location. * * @param string $from The path to move the backups from. */ public function move_old_backups( $from ) { if ( ! is_readable( $from ) ) { return; } if ( ! wp_is_writable( Path::get_path() ) ) { return; } // Move any existing backups if ( $handle = opendir( $from ) ) { // Loop through the backup directory while ( false !== ( $file = readdir( $handle ) ) ) { // Find all zips if ( 'zip' === pathinfo( $file, PATHINFO_EXTENSION ) ) { // Try to move them if ( ! @rename( trailingslashit( $from ) . $file, trailingslashit( Path::get_path() ) . $file ) ) { // If we can't move them then try to copy them copy( trailingslashit( $from ) . $file, trailingslashit( Path::get_path() ) . $file ); } } } closedir( $handle ); } // Delete the old directory if it's inside WP_CONTENT_DIR if ( false !== strpos( $from, WP_CONTENT_DIR ) && Path::get_path() !== $from ) { rmdirtree( $from ); } } /** * Clean any temporary / incomplete backups from the backups directory */ public function cleanup() { // Don't cleanup a custom path, who knows what other stuff is there if ( Path::get_path() === $this->get_custom_path() ) { return; } foreach ( new CleanUpIterator( new \DirectoryIterator( Path::get_path() ) ) as $file ) { if ( $file->isDot() || ! $file->isReadable() || ! $file->isFile() ) { continue; } @unlink( $file->getPathname() ); } } } class CleanUpIterator extends \FilterIterator { // Don't match index.html, files with zip extension or status logfiles. public function accept() { // Don't remove existing backups if ( 'zip' === pathinfo( $this->current()->getFilename(), PATHINFO_EXTENSION ) ) { return false; } // Don't remove the index.html file if ( 'index.html' === $this->current()->getBasename() ) { return false; } // Don't remove the file manifest if ( '.files' === $this->current()->getBasename() ) { return false; } // Don't cleanup the backup running file return ! preg_match( '/(.*-running)/', $this->current() ); } }