3, 'excludes' => array(), 'type' => 'complete', 'reoccurrence' => 'manually', ); /** * Setup the schedule object * Loads the options from the database and populates properties * * @param string $id * * @throws Exception */ public function __construct( $id ) { // Verify the schedule id if ( ! is_string( $id ) || ! trim( $id ) ) { throw new \Exception( 'Argument 1 for ' . __METHOD__ . ' must be a non-empty string' ); } // Store id for later $this->id = $id; // Load the options $this->options = array_merge( $this->options, array_filter( (array) get_option( 'hmbkp_schedule_' . $this->get_id() ) ) ); if ( defined( 'HMBKP_SCHEDULE_START_TIME' ) && strtotime( 'HMBKP_SCHEDULE_START_TIME' ) ) { $this->set_schedule_start_time( strtotime( 'HMBKP_SCHEDULE_START_TIME' ) ); } // Setup the schedule if it isn't set if ( ( ! $this->is_cron_scheduled() && $this->get_reoccurrence() !== 'manually' ) ) { $this->schedule(); } $this->backup_filename = implode( '-', array( sanitize_title( str_ireplace( array( 'http://', 'https://', 'www', ), '', home_url() ) ), $this->get_id(), $this->get_type(), current_time( 'Y-m-d-H-i-s' ), ) ) . '.zip'; $this->database_dump_filename = implode( '-', array( 'database', sanitize_title( str_ireplace( array( 'http://', 'https://', 'www' ), '', home_url() ) ), $this->get_id(), ) ) . '.sql'; $this->status = new Backup_Status( $this->get_id() ); } /** * Get the id for this schedule */ public function get_id() { return esc_attr( $this->id ); } /** * Get a slugified version of name */ public function get_slug() { // We cache slug in $this to save expensive calls to sanitize_title if ( ! empty( $this->slug ) ) { return $this->slug; } return $this->slug = sanitize_title( $this->get_name() ); } /** * Returns the given option value * * @param $option_name * @return mixed The option value */ public function get_schedule_option( $option_name ) { if ( isset( $this->options[ $option_name ] ) ) { return $this->options[ $option_name ]; } } /** * Get the name of this backup schedule * * @return string */ public function get_name() { return ucwords( $this->get_type() ) . ' ' . $this->get_reoccurrence(); } /** * Get the type of backup * * @return string */ public function get_type() { return $this->options['type']; } /** * Set the type of backup * * @param string $type */ public function set_type( $type ) { if ( ! isset( $this->options['type'] ) || $this->options['type'] !== $type ) { $this->options['type'] = $type; } } /** * Get the exclude rules * * @return array */ public function get_excludes() { return new Excludes( $this->options['excludes'] ); } /** * Set the exclude rules * * @param mixed $excludes A comma separated list or array of exclude rules * @param bool $append Whether to replace or append to existing rules * * @return string */ public function set_excludes( $exclude_rules, $append = false ) { // Normalize the exclude rules before we save them $excludes = new Excludes; $excludes = $excludes->normalize( (array) $exclude_rules ); // If these are valid excludes and they are different save them if ( empty( $this->options['excludes'] ) || $this->options['excludes'] !== $excludes ) { $this->options['excludes'] = $append && ! empty( $this->options['excludes'] ) ? array_merge( (array) $this->options['excludes'], (array) $excludes ) : (array) $excludes; } } /** * Get the maximum number of backups to keep * * @return int */ public function get_max_backups() { return (int) $this->options['max_backups']; } /** * Set the maximum number of backups to keep * * @param int $max * * @return WP_Error|boolean */ public function set_max_backups( $max ) { $this->options['max_backups'] = $max; } public function get_status() { return $this->status; } /** * Back compat with old set_status mathod * * @deprecated 3.4 Backup->status->set_status() */ public function set_status( $message ) { _deprecated_function( __FUNCTION__, '3.4', 'Backup->status->set_status()' ); $this->status->set_status( $message ); } /** * Get the array of services options for this schedule * * @param $service * @param null $option * * @return array */ public function get_service_options( $service, $option = null ) { if ( ! is_null( $option ) ) { if ( isset( $this->options[ $service ][ $option ] ) ) { return $this->options[ $service ][ $option ]; } return array(); } if ( isset( $this->options[ $service ] ) ) { return $this->options[ $service ]; } return array(); } /** * Set the service options for this schedule * * @param $service * @param array $options */ public function set_service_options( $service, array $options ) { $this->options[ $service ] = $options; } /** * Get the start time for the schedule * * @return int timestamp || 0 for manual only schedules */ public function get_schedule_start_time( $gmt = true ) { if ( 'manually' === $this->get_reoccurrence() ) { return 0; } if ( ! $gmt ) { $offset = get_option( 'gmt_offset' ) * 3600; } else { $offset = 0; } if ( ! empty( $this->options['schedule_start_time'] ) ) { return $this->options['schedule_start_time'] + $offset; } $this->set_schedule_start_time( time() ); return time() + $offset; } /** * Set the schedule start time. * * @param Int $time A valid timestamp */ public function set_schedule_start_time( $time ) { $this->options['schedule_start_time'] = $time; $this->schedule(); } /** * Get the schedule reoccurrence * */ public function get_reoccurrence() { return $this->options['reoccurrence']; } /** * Set the schedule reoccurrence * * @param string $reoccurrence * * @return \WP_Error|null|boolean */ public function set_reoccurrence( $reoccurrence ) { $hmbkp_schedules = cron_schedules(); // Check it's valid if ( ! is_string( $reoccurrence ) || ! trim( $reoccurrence ) || ( ! in_array( $reoccurrence, array_keys( $hmbkp_schedules ) ) ) && 'manually' !== $reoccurrence ) { return new \WP_Error( 'hmbkp_invalid_argument_error', sprintf( __( 'Argument 1 for %s must be a valid cron recurrence or "manually"', 'backupwordpress' ), __METHOD__ ) ); } // If the recurrence is already set to the same thing then there's no need to continue if ( isset( $this->options['reoccurrence'] ) && $this->options['reoccurrence'] === $reoccurrence && $this->is_cron_scheduled() ) { return; } $this->options['reoccurrence'] = $reoccurrence; if ( 'manually' === $reoccurrence ) { $this->unschedule(); } else { $this->schedule(); } return true; } /** * Get the interval between backups * * @return int */ public function get_interval() { $hmbkp_schedules = cron_schedules(); if ( 'manually' === $this->get_reoccurrence() ) { return 0; } return $hmbkp_schedules[ $this->get_reoccurrence() ]['interval']; } /** * Get the next occurrence of this scheduled backup * */ public function get_next_occurrence( $gmt = true ) { $time = wp_next_scheduled( 'hmbkp_schedule_hook', array( 'id' => $this->get_id() ) ); if ( ! $time ) { $time = 0; } if ( ! $gmt ) { $time += get_option( 'gmt_offset' ) * 3600; } return $time; } public function is_cron_scheduled() { return (bool) $this->get_next_occurrence(); } /** * Schedule the backup cron * */ public function schedule() { // Clear any existing hooks $this->unschedule(); $schedule_timestamp = $this->get_schedule_start_time(); wp_schedule_event( $schedule_timestamp, $this->get_reoccurrence(), 'hmbkp_schedule_hook', array( 'id' => $this->get_id() ) ); } /** * Unschedule the backup cron. * * @return void */ public function unschedule() { wp_clear_scheduled_hook( 'hmbkp_schedule_hook', array( 'id' => $this->get_id() ) ); } /** * Run the backup * */ public function run() { // Don't run if this schedule is already running if ( $this->status->is_started() ) { return; } // Setup our Site Backup Object $backup = new Backup( $this->get_backup_filename(), $this->get_database_dump_filename() ); $backup->set_type( $this->get_type() ); $backup->set_excludes( $this->get_excludes() ); $backup->set_status( $this->status ); $this->do_action( 'hmbkp_backup_started', $backup ); $this->status->start( $this->get_backup_filename(), __( 'Starting backup...', 'backupwordpress' ) ); $this->status->set_status( __( 'Deleting old backups...', 'backupwordpress' ) ); // Delete old backups now in-case we fatal error during the backup process $this->delete_old_backups(); $backup->run(); $errors = array_merge( $backup->errors, $backup->warnings ); $notices = array(); foreach ( $errors as $key => $error ) { $key = str_replace( array( __NAMESPACE__ . '\\', '_File_Backup_Engine', '_Database_Backup_Engine' ), array( '', '', '' ), $key ); $notices[] = $key . ': ' . implode( ', ', $error ); } Notices::get_instance()->set_notices( 'backup_errors', $notices ); $this->status->set_status( __( 'Deleting old backups...', 'backupwordpress' ) ); // Delete old backups again $this->delete_old_backups(); $this->do_action( 'hmbkp_backup_complete', $backup ); $this->status->finish(); $this->update_average_schedule_run_time( $this->status->get_start_time(), time() ); } public function get_backup_filename() { if ( $this->status->is_started() ) { $this->backup_filename = $this->status->get_backup_filename(); } return $this->backup_filename; } public function get_database_dump_filename() { return $this->database_dump_filename; } /** * Hook into the actions fired in the Backup class and set the status * * @param $action */ public function do_action( $action, Backup $backup ) { // Pass the actions to all the services // Todo should be decoupled into the service class foreach ( Services::get_services( $this ) as $service ) { if ( is_wp_error( $service ) ) { return $service; } $service->action( $action, $backup ); } } /** * Calculate schedule run time. * * @param int Timestamp $end */ public function update_average_schedule_run_time( $start, $end ) { if ( $end <= $start ) { // Something went wrong, ignore. return; } $diff = (int) abs( $end - $start ); if ( isset( $this->options['duration_total'] ) && isset( $this->options['backup_run_count'] ) ) { $this->options['duration_total'] += $diff; $this->options['backup_run_count'] ++; } else { $this->options['duration_total'] = $diff; $this->options['backup_run_count'] = 1; } $this->save(); } /** * Calculates the average run time for this schedule. * * @return string */ public function get_schedule_average_duration() { $duration = 'Unknown'; if ( ! isset( $this->options['duration_total'] ) || ! isset( $this->options['backup_run_count'] ) ) { return $duration; } if ( 0 === (int) $this->options['backup_run_count'] ) { return $duration; } $average_run_time = (int) $this->options['duration_total'] / (int) $this->options['backup_run_count']; if ( $average_run_time < HOUR_IN_SECONDS ) { $mins = round( $average_run_time / MINUTE_IN_SECONDS ); if ( $mins <= 1 ) { $mins = 1; } /* translators: min=minute */ $duration = sprintf( _n( '%s min', '%s mins', $mins, 'backupwordpress' ), $mins ); } elseif ( $average_run_time < DAY_IN_SECONDS && $average_run_time >= HOUR_IN_SECONDS ) { $hours = round( $average_run_time / HOUR_IN_SECONDS ); if ( $hours <= 1 ) { $hours = 1; } $duration = sprintf( _n( '%s hour', '%s hours', $hours, 'backupwordpress' ), $hours ); } return $duration; } /** * Get the backups created by this schedule * * @todo look into using recursiveDirectoryIterator and recursiveRegexIterator * @return string[] - file paths of the backups */ public function get_backups() { $files = array(); if ( $handle = @opendir( Path::get_path() ) ) { while ( false !== ( $file = readdir( $handle ) ) ) { if ( pathinfo( $file, PATHINFO_EXTENSION ) === 'zip' && strpos( $file, $this->get_id() ) !== false && ( isset( $this->status ) && $this->get_backup_filename() !== $file ) ) { $files[ @filemtime( trailingslashit( Path::get_path() ) . $file ) ] = trailingslashit( Path::get_path() ) . $file; } } closedir( $handle ); } krsort( $files ); return $files; } /** * Delete old backups * * @access private */ public function delete_old_backups() { if ( count( $this->get_backups() ) <= $this->get_max_backups() ) { return; } array_map( array( $this, 'delete_backup' ), array_slice( $this->get_backups(), $this->get_max_backups() ) ); } /** * Delete a specific back up file created by this schedule * * @param string $filepath * * @return \WP_Error|boolean */ public function delete_backup( $filepath ) { // Check that it's a valid filepath if ( empty( $filepath ) || ! is_string( $filepath ) ) { return new \WP_Error( 'hmbkp_empty_string_error', sprintf( __( 'Argument 1 for %s must be a non-empty string', 'backupwordpress' ), __METHOD__ ) ); } // Make sure it exists if ( ! file_exists( $filepath ) ) { return new \WP_Error( 'hmbkp_file_error', sprintf( __( '%s doesn\'t exist', 'backupwordpress' ), $filepath ) ); } // Make sure it was created by this schedule if ( strpos( $filepath, $this->get_id() ) === false ) { return new \WP_Error( 'hmbkp_backup_error', __( 'That backup wasn\'t created by this schedule', 'backupwordpress' ) ); } unlink( $filepath ); return true; } /** * Delete all back up files created by this schedule * */ public function delete_backups() { array_map( array( $this, 'delete_backup' ), $this->get_backups() ); } /** * Save the schedules options. * */ public function save() { // Only save them if they have changed if ( get_option( 'hmbkp_schedule_' . $this->get_id() ) !== $this->options ) { update_option( 'hmbkp_schedule_' . $this->get_id(), $this->options ); // Delete the schedule cache. delete_transient( 'hmbkp_schedules' ); } } /** * Cancel this schedule * * Cancels the cron job, removes the schedules options * and optionally deletes all backups created by * this schedule. * */ public function cancel( $delete_backups = false ) { // Delete the schedule options delete_option( 'hmbkp_schedule_' . $this->get_id() ); // Delete the schedule cache. delete_transient( 'hmbkp_schedules' ); // Clear any existing schedules $this->unschedule(); // Delete it's backups if ( $delete_backups ) { $this->delete_backups(); } } }