<?php
/**
 *	pluginbuddy_zipbuddy Class (Experimental)
 *
 *	Handles zipping and unzipping, using the best methods available and falling back to worse methods
 *	as needed for compatibility. Allows for forcing compatibility modes.
 *
 *	Version: 1.0.0
 *	Author:
 *	Author URI:
 *
 *
 */

// Test if we are loading as standard or experimental - if experimental just drop through
if ( 0 === strcmp( basename( dirname( __FILE__ ) ), 'zipbuddy' ) ) {

	// Legacy zip.
	if ( isset( pb_backupbuddy::$options['alternative_zip_1'] ) && ( '1' == pb_backupbuddy::$options['alternative_zip_1'] ) ) {

		pb_backupbuddy::$options['alternative_zip_1'] = 0;
		pb_backupbuddy::$options['alternative_zip_2'] = 0;
		pb_backupbuddy::save();


		/**
		// User enabled experimental so look for it and load it is found, otherwise log
		$legacy_zipbuddy = dirname( dirname( __FILE__ ) ) . '/zipbuddy/legacy.zipbuddy.php';
		if ( @is_readable( $legacy_zipbuddy ) ) {

			pb_backupbuddy::status( 'details', 'Loading legacy zipbuddy.' );
			require_once( $legacy_zipbuddy );
			return;

		} else {

			pb_backupbuddy::status( 'details', sprintf( __('Legacy Zip System enabled but not found/readable at: %1$s','it-l10n-backupbuddy' ), $$legacy_zipbuddy ) );


		}
		*/

	}


	// Currently loading as standard so determine if we need to load experimental
	if ( isset( pb_backupbuddy::$options['alternative_zip_2'] ) && ( '1' == pb_backupbuddy::$options['alternative_zip_2'] ) ) {

		// User enabled experimental so look for it and load it is found, otherwise log
		$experimental_zipbuddy = dirname( dirname( __FILE__ ) ) . '/xzipbuddy/zipbuddy.php';
		if ( @is_readable( $experimental_zipbuddy ) ) {

			require_once( $experimental_zipbuddy );

		} else {

			pb_backupbuddy::status( 'details', sprintf( __('Alternate Zip System enabled but not found/readable at: %1$s','it-l10n-backupbuddy' ), $experimental_zipbuddy ) );


		}

	}

}

if ( !class_exists( "pluginbuddy_zipbuddy" ) ) {

	// Currently just a wrapper for pb_backupbuddy::status()
	// TODO: Would prefer to have a generic logger and this would
	// extend it if required (we may not even need this dependent
	// on how logging evolves)
	class pluginbuddy_zipbuddy_logger {

		protected $_prefix = '';
		protected $_suffix = '';

		public function __construct( $prefix = "", $suffix = "" ) {

			if ( !empty( $prefix ) ) {

				$this->set_prefix( $prefix );

			}

			if ( !empty( $suffix ) ) {

				$this->set_suffix( $suffix );

			}

		}

		public function __destruct() {

		}

		public function set_prefix( $prefix = "" ) {

			$this->_prefix = $prefix;

			return $this;

		}

		public function get_prefix() {

			return $this->_prefix;

		}

		public function set_suffix( $suffix = "" ) {

			$this->_suffix = $suffix;

			return $this;

		}

		public function get_suffix() {

			return $this->_suffix;

		}

		public function log( $level, $message, $prefix = null, $suffix = null ) {

			$prefix_to_use = ( is_null( $prefix ) ) ? $this->_prefix : ( ( is_string( $prefix ) ) ? $prefix : "" ) ;
			$suffix_to_use = ( is_null( $suffix ) ) ? $this->_suffix : ( ( is_string( $suffix ) ) ? $suffix : "" ) ;

			pb_backupbuddy::status( $level, $prefix_to_use . $message . $suffix_to_use );

			return $this;

		}

	}

	class pluginbuddy_zipbuddy_null_object {

		public function __construct() {

		}

		public function __destruct() {

		}

		public function __call( $method, $arguments ) {

		}

	}

	/**
	 *	pluginbuddy_zipbuddy_process_monitor Class
	 *
	 *	Class that is used monitor the progress of any process and take actions to try and
	 *	ensure the process execution is prolonged as much as possible.
	 *
	 */
	 class pluginbuddy_zipbuddy_process_monitor {

		const ZIP_DEFAULT_EXECUTION_MAX_PERIOD = 30;
		const ZIP_DEFAULT_EXECUTION_THRESHOLD_PERIOD = 10;

		const ZIP_DEFAULT_MONITOR_THRESHOLD_PERIOD = 10;

		const ZIP_DEFAULT_TICKLE_THRESHOLD_PERIOD = 10;

		protected $_execution_threshold_period = 0;
		protected $_execution_start_time = 0;
		protected $_execution_max_period = 0;

		protected $_monitor_threshold_period = 0;
		protected $_monitor_start_time = 0;
		protected $_monitoring_usage = false;
		protected $_start_user_time = 0;
		protected $_elapsed_user_time = 0;

		protected $_tickle_threshold_period = 0;
		protected $_tickle_start_time = 0;
		protected $_server_tickling = false;
		protected $_server_tickler = '';

		protected $_creation_time = 0;

		protected $_report_connection_status = false;

         /**
         * The logger we will use
         *
         * @var logger	object
         */
		protected $_logger = null;

		public function __construct( &$parent = null ) {

			// Inherit the parent logger by default (if it has one), caller may override after instantiated
			if ( $parent && method_exists( $parent, 'get_logger' ) ) {

				$this->set_logger( $parent->get_logger() );

			}

			$now = time();

			$this->set_creation_time( $now );

			// We can try and derive the configured execution_max_period
			// and the execution_threshold_period is set based on that. If
			// this is overridden by a given period then we can also choose
			// whether to set the threshold period based on that or set a
			// specific threshold period (this is up to the caller).
			$this->set_execution_start_time( $now );
			$this->set_execution_max_period( 'auto' );

			// The monitor threshold period cannot be derived
			$this->set_monitor_start_time( $now );
			$this->set_monitor_threshold_period( self::ZIP_DEFAULT_MONITOR_THRESHOLD_PERIOD );

			// The tickle threshold period cannot be derived
			$this->set_tickle_start_time( $now );
			$this->set_tickle_threshold_period( self::ZIP_DEFAULT_TICKLE_THRESHOLD_PERIOD );

			$this->initialize_monitoring_usage();

			$this->set_server_tickling( true );
			$this->_server_tickler = '<!--' . str_shuffle( substr( str_repeat( implode( '', range( 'a', 'z' ) ), 40 ), 0, 1024 ) ) . '-->' . chr(13) . chr(10);

			$this->log_parameters();

		}

		public function __destruct() {

		}

		/**
		 *
		 *	checkpoint()
		 *
		 *	Check how we are doing and whether we need to take any steps to prolong
		 *	execution at this time. If any of these report anythng then connection
		 *	status is also reported.
		 *
		 *	@return		object			Return reference to this object
		 *
		 */
		public function checkpoint() {

			$this->monitor_usage();
			$this->monitor_execution_time();
			$this->tickle_server();

			return $this;

		}

		public function get_creation_time() {

			return $this->_creation_time;

		}

		public function set_creation_time( $time = 0 ) {

			$this->_creation_time = ( 0 === $time ) ? time() : $time ;
			return $this;

		}

		public function get_elapsed_time() {

			return ( time() - $this->get_creation_time() );

		}

		// Methods for handling the execution time management

		public function get_execution_threshold_period() {

			return $this->_execution_threshold_period;

		}

		public function set_execution_threshold_period( $period = self::ZIP_DEFAULT_EXECUTION_THRESHOLD_PERIOD ) {

			$execution_max_period = 0;

			if ( true === is_string( $period ) ) {

				switch ( $period ) {

					case 'auto':
						// If auto then set based on execution max period
						if ( 0 === ( $execution_max_period = $this->get_execution_max_period() ) ) {

							// Not set yet so we need to set it with auto
							// Ensure we don't get into a recursive loop...
							$execution_max_period = $this->set_execution_max_period( 'auto', false )->get_execution_max_period();

						}

						// Bit of an arbitrary proportion...
						$this->_execution_threshold_period = (int)( $execution_max_period / 3 );
						break;

					default:
						// Unknown mode so use default value
						$this->_execution_threshold_period = self::ZIP_DEFAULT_EXECUTION_THRESHOLD_PERIOD;

				}

			} elseif ( is_numeric( $period ) && ( 0 < $period ) ) {

				$this->_execution_threshold_period = $period;

			} else {

				$this->_execution_threshold_period = self::ZIP_DEFAULT_EXECUTION_THRESHOLD_PERIOD;

			}

			return $this;

		}

		public function get_execution_start_time() {

			return $this->_execution_start_time;

		}

		public function set_execution_start_time( $time = 0 ) {

			( 0 === $time ) ? $this->_execution_start_time = time() : $this->_execution_start_time = $time ;
			return $this;

		}

		public function get_execution_max_period() {

			return $this->_execution_max_period;

		}

		/**
		 *
		 *	set_execution_max_period()
		 *
		 *	Set, or try to derive, what the maximum execution period should be.
		 *	Possibly also set the threshold if max was derived.
		 *	For deriving try to get configured values to use, otherwise use the
		 *	default.
		 *	The 0 value needs to be specially handled since it implies unlimited
		 *	and so we map this to the PHP maximum integer value. In practice,
		 *	unlimited doesn't really mean much since hosting will have other
		 *	timeouts that kick in - but we need to honour whatever the user
		 *	has configured.
		 *
		 *	@param		string|int		$period				Integer for specific value, 'auto' for derived
		 *	@param		bool			$auto_set_threshold	Whether to auto set threshold period or not
		 *	@return		object								This object instance
		 *
		 */
		public function set_execution_max_period( $period = self::ZIP_DEFAULT_EXECUTION_MAX_PERIOD, $auto_set_threshold = true ) {

			// Get the originally configured value if we can.
			// Returned values may be false or an integer as a string.
			// If false we want to keep it as false and we'll use our
			// default; if 0 | -1 then we'll set as larger integer;
			// otherwise if it is a numeric value and +ve we'll use it
			// otherwise set to false.
			// Note: we must translate boolean false to string false
			// because we can only use integer or string keys in the
			// mapping array otherwise array_key_exists() complains.
			$configured_execution_time = @get_cfg_var( 'max_execution_time' );
			$configured_execution_time = ( false === $configured_execution_time ) ? "false" : $configured_execution_time;

			$cfg_var_map = array(	"false" => false,
									"-1" => (int)PHP_INT_MAX,
									"0" => (int)PHP_INT_MAX,
									"" => false,
								);
			if ( ( is_string( $configured_execution_time ) || is_int( $configured_execution_time ) ) && array_key_exists( $configured_execution_time, $cfg_var_map ) ) {

				// "Special" value, map it
				$configured_execution_time = $cfg_var_map[ $configured_execution_time ];

			} else {

				// "Ordinary" value but must be numeric and +ve
				if ( is_numeric( $configured_execution_time ) && ( 0 < (int)$configured_execution_time )) {

					$configured_execution_time = (int)$configured_execution_time;

				} else {

					$configured_execution_time = false;

				}

			}

			// Get the current value if we can.
			// Returned values may be false or an integer as a string.
			// If false we want to keep it as false and we'll use our
			// default; if 0 | -1 then we'll set as larger integer;
			// if "7200" we have to assume it's because we set that
			// so make it false; otherwsie if it is a numeric value
			// and +ve we'll use it; otherwise set false.
			// Note: we must translate boolean false to string false
			// because we can only use integer or string keys in the
			// mapping array otherwise array_key_exists() complains.
			$current_execution_time = @ini_get( 'max_execution_time' );
			$current_execution_time = ( false === $current_execution_time ) ? "false" : $current_execution_time;

			$ini_get_map = array(	"false" => false,
									"-1" => (int)PHP_INT_MAX,
									"0" => (int)PHP_INT_MAX,
									"7200" => false,
									"" => false,
								);
			if ( ( is_string( $current_execution_time ) || is_int( $current_execution_time ) ) && array_key_exists( $current_execution_time, $ini_get_map ) ) {

				// "Special" value, map it
				$current_execution_time = $ini_get_map[ $current_execution_time ];

			} else {

				// "Ordinary" value but must be numeric and +ve
				if ( is_numeric( $current_execution_time ) && ( 0 < (int)$current_execution_time )) {

					$current_execution_time = (int)$current_execution_time;

				} else {

					$current_execution_time = false;

				}

			}

			if ( true === is_string( $period ) ) {

				switch ( $period ) {

					case 'auto':
						// Try for the currently set execution time
						if ( false === $current_execution_time ) {

							// Couldn't get a value so try for configured value
							if ( false === $configured_execution_time ) {

								// Couldn't get a configured value for some reason so use default
								$this->_execution_max_period = self::ZIP_DEFAULT_EXECUTION_MAX_PERIOD;

							} else {

								// Got a configured value so use it
								$this->_execution_max_period = (int) $configured_execution_time;

							}

						} else {

							// Got a non-zero current execution time so use it
							$this->_execution_max_period = (int) $current_execution_time;

						}

						// If set by auto then make sure we (re)set threshold as auto _unless_
						// told not to...
						if ( true === $auto_set_threshold ) {

							$this->set_execution_threshold_period( 'auto' );

						}
						break;

					default:
						// Unknown mode so use default value
						$this->_execution_max_period = self::ZIP_DEFAULT_EXECUTION_MAX_PERIOD;

				}

			} elseif ( is_numeric( $period ) && ( 0 < $period ) ) {

				$this->_execution_max_period = (int)$period;

			} else {

				$this->_execution_max_period = self::ZIP_DEFAULT_EXECUTION_MAX_PERIOD;

			}

			return $this;

		}

		// Methods for handling the monitoring action

		public function get_monitor_threshold_period() {

			return $this->_monitor_threshold_period;

		}

		public function set_monitor_threshold_period( $period = self::ZIP_DEFAULT_MONITOR_THRESHOLD_PERIOD ) {

			$this->_monitor_threshold_period = ( is_numeric( $period ) && ( 0 < $period ) ) ? $period : self::ZIP_DEFAULT_MONITOR_THRESHOLD_PERIOD ;
			return $this;

		}

		public function get_monitor_start_time() {

			return $this->_monitor_start_time;

		}

		public function set_monitor_start_time( $time = 0 ) {

			$this->_monitor_start_time = ( 0 === $time ) ? time() : $time ;
			return $this;

		}

		public function initialize_monitoring_usage( $reset = true ) {

			$usage_data = array();

			// Must determine if we can monitor usage data
			if ( function_exists( 'getrusage' ) && is_callable( 'getrusage' ) ) {

				$this->_monitoring_usage = true;

				// We need to know the user time value at the start so we can monitor how
				// much actual user time we are using (which counts against max_execution_time)
				// We call this when object is created to maek sure the init is done but we can
				// make a later call closer to when we wnt to start monitoring it and that will
				// reset the start time.
				if ( ( 0 === $this->_start_user_time ) || ( true === $reset ) ) {

					$usage_data = getrusage();
					$this->_start_user_time = $usage_data[ 'ru_utime.tv_sec' ];

				}

			}

		}

		public function is_monitoring_usage() {

			return ( true === $this->_monitoring_usage );

		}

		// Methods for handling the server tickling action

		public function get_tickle_threshold_period() {

			return $this->_tickle_threshold_period;

		}

		public function set_tickle_threshold_period( $period = self::ZIP_DEFAULT_TICKLE_THRESHOLD_PERIOD ) {

			$this->_tickle_threshold_period = ( is_numeric( $period ) && ( 0 < $period ) ) ? $period : self::ZIP_DEFAULT_TICKLE_THRESHOLD_PERIOD ;
			return $this;

		}

		public function get_tickle_start_time() {

			return $this->_tickle_start_time;

		}

		public function set_tickle_start_time( $time = 0 ) {

			$this->_tickle_start_time = ( 0 === $time ) ? time() : $time ;
			return $this;

		}

		public function set_server_tickling( $tickle = true ) {

			$this->_server_tickling = $tickle;
			return $this;

		}

		public function get_server_tickling() {

			return $this->_server_tickling;

		}

		public function is_server_tickling() {

			return ( true === $this->_server_tickling );

		}

		public function get_server_tickler() {

			return $this->_server_tickler;

		}

		public function connection_status_tostring( $status ) {

			$status_name = '';

			switch ( $status ) {

				case CONNECTION_NORMAL:
					$status_name = 'Normal';
					break;
				case CONNECTION_ABORTED:
					$status_name = 'Aborted';
					break;
				case CONNECTION_TIMEOUT:
					$status_name = 'Timeout';
					break;
				default:
					$status_name = 'Unknown';

			}

			return $status_name;

		}

		protected function monitor_usage() {

			$usage_data = array();
			$current_monitor_period = 0;

			// Would expect to monitor except on Windows which doesn't support getrusage()
			if ( true === $this->is_monitoring_usage() ) {

				// Decide if we need to log usage data
				$current_monitor_period = ( time() - $this->get_monitor_start_time() );
				if ( $this->get_monitor_threshold_period() < $current_monitor_period ) {

					// Get some usage data from the server (check that function available) and log it
					$usage_data = getrusage();

					// Determine the total user space time since we initialized the monitoring (relative)
					$this->_elapsed_user_time = ( $usage_data[ 'ru_utime.tv_sec' ] - $this->_start_user_time );
					$this->get_logger()->log( 'details', sprintf( __('Zip process reported: Usage data (raw/relative): ( %1$s, %2$s, %3$s, %4$s, %5$s )/( -, %6$s, -, -, - )','it-l10n-backupbuddy' ), $usage_data[ 'ru_stime.tv_sec' ], $usage_data[ 'ru_utime.tv_sec' ], $usage_data[ 'ru_majflt' ], $usage_data[ 'ru_nvcsw' ], $usage_data[ 'ru_nivcsw' ], $this->_elapsed_user_time ) );

					// Reset the monitoring period start time
					$this->set_monitor_start_time( time() );

					// We reported something so report the connection status as well
					$this->_report_connection_status = true;

				}

			}

			return $this;

		}

		protected function monitor_execution_time() {

			$current_execution_period = 0;

			// Decide if we have been running long enough to need to reset time limit
			$current_execution_period = ( time() - $this->get_execution_start_time() );
			if ( $this->get_execution_threshold_period() < $current_execution_period ) {

				// Log how long we ran for and then reset the start time and max period
				$this->get_logger()->log( 'details', sprintf( __('Zip process reported: %1$s seconds elapsed - resetting timebase to %2$s seconds','it-l10n-backupbuddy' ), $current_execution_period, $this->get_execution_max_period() ) );

				// Reset the execution period start time
				$this->set_execution_start_time( time() );

				// Reset the execution time timer (if the server honours this)
				// Belt and braces in case some server disables set_time_limit()
				// for some reason, hope that init_set() works - if neither works
				// there isn't much we can do about it - user will just have to
				// use a step period that matches the configured max_execution_time.
				@set_time_limit( $this->get_execution_max_period() );
				@ini_set( 'max_execution_time', $this->get_execution_max_period );

				// We reported something so report the connection status as well
				$this->_report_connection_status = true;

			}

			return $this;

		}

		protected function tickle_server() {

			$current_tickle_period = 0;

			// Only bother with server tickling if it is selected
			if ( true === $this->is_server_tickling() ) {

				// Decide if we have been running long enough to need to tickle the server
				$current_tickle_period = ( time() - $this->get_tickle_start_time() );
				if ( $this->get_tickle_threshold_period() < $current_tickle_period ) {

					// Log how long since we last tickled and indicate tickling
					$this->get_logger()->log( 'details', sprintf( __('Zip process reported: %1$s seconds elapsed - tickling server','it-l10n-backupbuddy' ), $current_tickle_period ) );
					$this->set_tickle_start_time( time() );

					// Output the tickler to give something to flush
					echo $this->get_server_tickler();

					// Force flushing and end of buffering
					// Possibly should need to do this because nothing should have started buffering
					// should it? It's possible that PHP config could have enabled one level of buffering
					// so at least handle that with the method as exemplified in the PHP manual. Also do
					// a staright flush as that should cause a flush at least to the server which is
					// actually all we want in this particular case.
					while ( @ob_end_flush() );
					flush();

					// We reported something so report the connection status as well
					$this->_report_connection_status = true;

				}

			}

			return $this;

		}

		protected function report_connection_status() {

			if ( true === $this->_report_connection_status ) {

				// Log where we are at and connection status for information
				$this->get_logger()->log( 'details', sprintf( __('Zip process reported: Connection status: %1$s (%2$s)','it-l10n-backupbuddy' ), $this->connection_status_tostring( connection_status() ), connection_status() ) );
				$this->_report_connection_status = false;

			}

			return $this;

		}

		// Log our setup parameters
		public function log_parameters() {

			$this->get_logger()->log( 'details', sprintf( __('Zip process reported: Execution max/threshold periods: %1$ss/%2$ss','it-l10n-backupbuddy' ), $this->get_execution_max_period(), $this->get_execution_threshold_period() ) );
			$this->get_logger()->log( 'details', sprintf( __('Zip process reported: Monitor threshold period: %1$ss','it-l10n-backupbuddy' ), $this->get_monitor_threshold_period() ) );
			$this->get_logger()->log( 'details', sprintf( __('Zip process reported: Tickle threshold period: %1$ss','it-l10n-backupbuddy' ), $this->get_tickle_threshold_period() ) );

			return $this;

		}

		public function set_logger( $logger ) {

			$this->_logger = $logger;

			return $this;

		}

		public function get_logger() {

			if ( is_null( $this->_logger ) ) {

				$logger = new pluginbuddy_zipbuddy_null_object();
				$this->set_logger( $logger );

			}

			return $this->_logger;

		}

	}

	class pluginbuddy_zipbuddy {

		const ZIP_METHODS_TRANSIENT = 'pb_backupbuddy_avail_zip_methods';
		const ZIP_METHODS_TRANSIENT_EXPERIMENTAL = 'pb_backupbuddy_avail_xzip_methods';
		const ZIP_METHODS_TRANSIENT_LIFE = 43200; // 12 Hours - really shouldn't change unless server problem
		const NORM_DIRECTORY_SEPARATOR = '/';
		const DIRECTORY_SEPARATORS = '/\\';

		const STATE_NAME_BEGIN = 'begin';
		const STATE_ID_BEGIN = 1;
		const STATE_NAME_IN_PROGRESS = 'in_progress';
		const STATE_ID_IN_PROGRESS = 2;
		const STATE_NAME_END = 'end';
		const STATE_ID_END = 3;

		const ZIP_DEFAULT_IGNORE_WARNINGS = false;
		const ZIP_DEFAULT_IGNORE_SYMLINKS = true;
		const ZIP_DEFAULT_COMPRESSION = true;
		const ZIP_DEFAULT_STEP_PERIOD = 30;
		const ZIP_DEFAULT_BURST_GAP = 2;
		const ZIP_DEFAULT_MIN_BURST_CONTENT = 10485760; // 10MB
		const ZIP_DEFAULT_MAX_BURST_CONTENT = 104857600; // 100MB
		const ZIP_DEFAULT_BURST_THRESHOLD_PERIOD = 30;

        /**
         * The plugin path for this plugin
         *
         * @var string
         */
        public $_pluginPath = '';

        /**
         * The path of the temporary directory that can be used for creating files and stuff
         *
         * @var string
         */
        protected $_tempdir = "";

        /**
         * The list of zip methods that are requested to be used
         *
         * @var array of string
         */
        protected $_requested_zip_methods = array();

        /**
         * Status message array used when calling other methods to get status information back
         *
         * @var array of string
         */
        public $_status = array();

        /**
         * The list of zip methods that are to be used or are available
         * Had to make this public for now because something accesses it directly - bad karma
         *
         * @var array of string
         */
        public $_zip_methods = array();

        /**
         * The details of the various zip methods that are available
         * Have to make this a separate array indexed by the method tag. Ideally would be combined
         * with the zip methods array but that would involve more general changes elsewhere so that
         * refactoring can be done later - main problem is the direct access to the zip methods
         * array that is made rather than through a function.
         *
         * @var array of array of array
         */
        protected $_zip_methods_details = array();

        /**
         * The list of zip methods that are supported, i.e., there is a supporting class defined
         *
         * @var array of string
         */
        protected $_supported_zip_methods = array();

        /**
         * Whether or not we are force overriding zip method caching for this instance of zipbuddy
         *
         * @var boolean
         */
		protected $_zip_method_caching_force_disabled = false;

        /**
         * Whether or not we can call a status calback
         *
         * @var bool
         */
		protected $_have_status_callback = false;

        /**
         * Object->method array for status function
         *
         * @var array
         */
		protected $_status_callback = array();

        /**
         * The directory name that we are loaded from (not: not path)
         *
         * @var string
         */
		protected $_whereami = "";

        /**
         * Whether we are loaded as the experimental zipbuddy
         *
         * @var bool
         */
		protected $_is_experimental = false;

        /**
         * The name of the zip methods transient will be dependent on if we are standard or experimental
         *
         * @var string
         */
		protected $_zip_methods_transient = "";

        /**
         * The Server API that is in use
         *
         * @var string
         */
		protected $_sapi_name = "";

        /**
         * Convenience boolean indicating if Warnings should be ignored when building archives
         *
         * @var ignore_warnings	bool
         */
		protected $_ignore_warnings = null;

        /**
         * Convenience boolean indicating if symlinks should be ignored/not-followed when building archives
         *
         * @var ignore_symlinks	bool
         */
		protected $_ignore_symlinks = null;

         /**
         * Convenience boolean indicating if compression shoul dbe used when building archives
         *
         * @var compression	bool
         */
		protected $_compression = null;

         /**
         * Convenience integer indicating the maximum period for any action during a step
         *
         * @var step_period	int
         */
		protected $_step_period = null;

         /**
         * Convenience integer indicating the gap to delay between burst
         *
         * @var burst_gap	int
         */
		protected $_burst_gap = null;

          /**
         * Convenience double indicating the minimum burst content size
         *
         * @var min_burst_content	double
         */
		protected $_min_burst_content = null;

          /**
         * Convenience double indicating the maximum burst content size
         *
         * @var max_burst_content	double
         */
		protected $_max_burst_content = null;

          /**
         * Convenience integer indicating the burst threshold period
         *
         * @var burst_threshold_period	int
         */
		protected $_burst_threshold_period = null;

        /**
         * The logger to use
         *
         * @var logger	object
         */
		protected $_logger = null;

         /**
         * The default logger to give to any children
         *
         * @var default_child_logger	object
         */
		protected $_default_child_logger = null;

         /**
         * The process monitor to use
         *
         * @var process_monitor	object
         */
		protected $_process_monitor = null;

		/**
		 *
		 * get_transient_names_static()
		 *
		 * Get the transient name(s) that may be in use
		 *
		 * @return		array	The transient name(s)
		 *
		 */
		public static function get_transient_names_static() {

			return array( self::ZIP_METHODS_TRANSIENT,
						  self::ZIP_METHODS_TRANSIENT_EXPERIMENTAL );

		}

		/**
		 *	__construct()
		 *
		 *	Default constructor.
		 *
		 *	@param		string		$temp_dir		The path of the temporary directory to use
		 *	@param		array		$zip_methods	Optional: The set of zip methods requested to use
		 *	@return		null
		 *
		 */
		public function __construct( $temp_dir, $zip_methods = array() ) {

			// Zipbuddy creates it's own default logger but can be overridden
			// Note: if user wants to override they must "get" the current logger
			// and destroy it otherwise it gets orphaned.
			$this->set_logger( new pluginbuddy_zipbuddy_logger() );

			// Use this for all our children unless they need a specific logger
			$this->_default_child_logger = new pluginbuddy_zipbuddy_logger();

			// Zipbuddy creates it's own default process monitor but can be overridden
			// Note: if user wants to override they must "get" the current process
			// monitor and destroy it otherwise it gets orphaned. We'll give it our
			// logger
			$this->set_process_monitor( new pluginbuddy_zipbuddy_process_monitor( $this ) );

			// Normalize the trailing directory separator on the path
			$temp_dir = rtrim( $temp_dir, self::DIRECTORY_SEPARATORS ) . self::NORM_DIRECTORY_SEPARATOR;

			// Normalize platform specific directory separators in path
			$this->_tempdir = str_replace( DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $temp_dir );

			// Record where we are located (the directory name)
			$this->_whereami = basename( dirname( __FILE__ ) );

			// Set a flag for easy conditional testing
			$this->_is_experimental = ( 0 === strcmp( $this->_whereami, 'zipbuddy' ) ) ? false : true ;

			// Use our experimental flag to determine which zip methods transient we should be using
			$this->_zip_methods_transient = ( $this->_is_experimental ) ? self::ZIP_METHODS_TRANSIENT_EXPERIMENTAL : self::ZIP_METHODS_TRANSIENT ;

			// Set the sapi name so we can use it later
			$this->set_sapi_name();

			// Derive whether we are ignoring Warnings or not (can be overridden by method call)
			$this->set_ignore_warnings();

			// Derive whether we are ignoring/not-following symlinks or not (can be overridden by method call)
			$this->set_ignore_symlinks();

			// Derive whether compression should be used (can be overridden by method call)
			$this->set_compression();

			// Derive what the step period should be for any action after which a new step must be initiated
			$this->set_step_period();

			// Derive what the interburst gap should be for any burst related action
			$this->set_burst_gap();

			// Derive what the min burst content size should be for any burst related action
			$this->set_min_burst_content();

			// Derive what the max burst content size should be for any burst related action
			$this->set_max_burst_content();

			// Make sure we load the core abstract class as this will always be needed
			require_once( pb_backupbuddy::plugin_path() . '/lib/' . $this->_whereami . '/zbzipcore.php' );

			// If we loaded that ok then try the method specific classes
			// Could make this more generic based on config or somesuch
			if ( class_exists( 'pluginbuddy_zbzipcore' ) ) {
//
// 				// Only provide proc mode when experimental zip enabled
// 				if ( true === $this->_is_experimental ) {
//
// 					include_once( pb_backupbuddy::plugin_path() . '/lib/' . $this->_whereami . '/zbzipproc.php' );
//
// 					if ( class_exists( 'pluginbuddy_zbzipproc' ) ) {
//
// 						if ( $this->check_method_dependencies( 'pluginbuddy_zbzipproc' ) ) {
//
// 							$this->set_supported_zip_methods( pluginbuddy_zbzipproc::get_method_tag_static() );
//
// 						}
//
// 					}
//
// 				}
//
				include_once( pb_backupbuddy::plugin_path() . '/lib/' . $this->_whereami . '/zbzipexec.php' );
				if ( class_exists( 'pluginbuddy_zbzipexec' ) ) {

					if ( $this->check_method_dependencies( 'pluginbuddy_zbzipexec' ) ) {

						$this->set_supported_zip_methods( pluginbuddy_zbzipexec::get_method_tag_static() );

					}

				}

				include_once( pb_backupbuddy::plugin_path() . '/lib/' . $this->_whereami . '/zbzipziparchive.php' );
				if ( class_exists( 'pluginbuddy_zbzipziparchive' ) ) {

					if ( $this->check_method_dependencies( 'pluginbuddy_zbzipziparchive' ) ) {

						$this->set_supported_zip_methods( pluginbuddy_zbzipziparchive::get_method_tag_static() );

					}

				}

				include_once( pb_backupbuddy::plugin_path() . '/lib/' . $this->_whereami . '/zbzippclzip.php' );
				if ( class_exists( 'pluginbuddy_zbzippclzip' ) ) {

					if ( $this->check_method_dependencies( 'pluginbuddy_zbzippclzip' ) ) {

						$this->set_supported_zip_methods( pluginbuddy_zbzippclzip::get_method_tag_static() );

					}

				}

			}

 			// Work out the list of zip methods from the requested and available along with their details
			$this->set_zip_methods( $zip_methods );

		}


		/**
		 *	__destruct()
		 *
		 *	Default destructor.
		 *
		 *	@return		null
		 *
		 */
		public function __destruct( ) {

			// TODO: Perhaps we need to check if the logger is our original one
			// that we created and if so then we should destroy it as we own it,
			// otherwise someone else owns it and we shouldn't destroy it. Same
			// goes for process monitor.

			unset( $this->_default_child_logger );

		}

		/**
		 *	set_sapi_name()
		 *
		 *	Sets the sapi name to that given or retrieves from PHP
		 *	TODO: Extend to also set a sapi id constant based on the name?
		 *
		 *	@param	string	$name	A sapi name to set (default empty)
		 *	@return	object			This object
		 */
		public function set_sapi_name( $sapi_name = "" ) {

			if ( empty( $sapi_name ) ) {

				$sapi_name = php_sapi_name();

			}

			$this->_sapi_name = $sapi_name;

			return $this;

		}

		/**
		 *	get_sapi_name()
		 *
		 *	Returns the previously set sapi name
		 *
		 *	@return	string			The stored sapi name
		 */
		public function get_sapi_name() {

			return $this->_sapi_name;

		}

		/**
		 *	derive_optional_bool()
		 *
		 *	Utility function to derive the value of an optional boolean flag based on either
		 *	a specifc value being given or the related global option being set or a given
		 *	defautl value otherise. If the provided $value is null then this forces the use
		 *	of the global option if it is set or otherwise the default value given
		 *
		 *	@param		string		$option		The option name in the global options array
		 *	@param		mixed		$value		Should be bool true|false but could be null
		 * 	@param		bool		$default	The default value to use if no other provided/available
		 *	@return		bool					Derived boolean value
		 *
		 */
		 protected function derive_optional_bool( $option, $value, $default, $empty_option_default = false ) {
		 	$result = false;
		 	if ( is_bool( $value )) {
		 		$result = $value;
		 	} elseif ( isset( pb_backupbuddy::$options[ $option ] ) ) {
		 		$result = ( ( pb_backupbuddy::$options[ $option ] == '1' ) || ( pb_backupbuddy::$options[ $option ] == true ) ) ? true : $empty_option_default ;
		 	} else {
		 		$result = $default;
		 	}
		 	return $result;
		 }

		/**
		 *	derive_optional_int()
		 *
		 *	Utility function to derive the value of an optional integer based on either
		 *	a specifc value being given or the related global option being set or a given
		 *	default value otherise.
		 *	If the provided $value is null then this forces the use of the global option if
		 *	it is set (adjusted by any factor given) or otherwise the default value given.
		 *	If the global option is set but is not numeric (generally left empty) then we
		 *	use the provided $empty_option_value (which is _not_ adjusted by the factor but
		 *	assumed to be valid as it is - for example if the option is given in MB but
		 *	the required value to use is in bytes then we would adjust by multiplying by
		 *	1024*1024 but the empty option value is assumed to be provided in bytes and so
		 *	is not adjusted.
		 *	In the case of using the adjusted option value or the empty option value we also
		 *	apply any value may given so hat, for example, an option value of 0 can be
		 *	mapped to mean "infinite".
		 *	Note: integer type options may be stored as strings so that we can use an empty
		 *	string to signal we want the default value to be used - so we need to check the
		 *	option value as being numeric rather than integer as this will return true for
		 *	any string that is a numerical value as well as the option being an integer value
		 *	in any case but crucually will return false for an empry string as this does not
		 *	represent any numerical value and hence that triggers the use of the empty option
		 *	default value.
		 *	We coerce any numeric string into an integer for assignment.
		 *
		 *	@param		string		$option					The option name in the global options array
		 *	@param		mixed		$value					Should be integer but could be null
		 * 	@param		int			$default				The deafult value to use if no other provided/available
		 *	@param		array		$option_value_map		Any special treatment for option values
		 *	@param		int			$option_value_factor	Adjustment factor if option given in units
		 *	@param		int			$empty_option_default	Default value to use if option is set but empty
		 *	@return		int									Derived integer value
		 *
		 */
		 protected function derive_optional_int( $option, $value, $default, $option_value_map = array(), $option_value_factor = 1, $empty_option_default = PHP_INT_MAX ) {
		 	$result = PHP_INT_MAX;
		 	if ( is_int( $value )) {
		 		$result = $value;
		 	} elseif ( isset( pb_backupbuddy::$options[ $option ] ) ) {
		 		$result = ( is_numeric( pb_backupbuddy::$options[ $option ] ) ) ? (int)( pb_backupbuddy::$options[ $option ] * $option_value_factor ) : $empty_option_default ;
		 		$result = ( array_key_exists( $result, $option_value_map ) ) ? $option_value_map[ $result ] : $result ;
		 	} else {
		 		$result = $default;
		 	}
		 	return $result;
		 }

		/**
		 *	derive_optional_double()
		 *
		 *	Utility function to derive the value of an optional double based on either
		 *	a specifc value being given or the related global option being set or a given
		 *	defautl value otherise. If the provided $value is null then this forces the use
		 *	of the global option if it is set or otherwise the default value given.
		 *	Note: double type options may be stored as strings so that we can use an empty
		 *	string to signal we want the default value to be used - so we need to check the
		 *	option value as being numeric rather than double as this will return true for
		 *	any string that is a numerical value as well as the option being an double value
		 *	in any case but crucually will return false for an emprt string as this does not
		 *	represent any numerical value and hence that triggers the use of the default value.
		 *	We coerce any numeric string into an double for assignment.
		 *
		 *	@param		string		$option					The option name in the global options array
		 *	@param		mixed		$value					Should be double but could be null
		 * 	@param		double		$default				The deafult value to use if no other provided/available
		 *	@param		array		$option_value_map		Any special treatment for option values
		 *	@param		int			$option_value_factor	Adjustment factor if option given in units
		 *	@param		double		$empty_option_default	Default value to use if option is set but empty
		 *	@return		double								Derived double value
		 *
		 */
		 protected function derive_optional_double( $option, $value, $default, $option_value_map = array(), $option_value_factor = 1, $empty_option_default = null ) {
			// Example of a "large" value on either a 32 or 64 bit system - we cannot set this
			// as the $empty_option_value default value in the function sig so post-process to
			// set it if indicated.
		 	$large_value = ( 4 == PHP_INT_SIZE ) ? (double)( pow(2, 63) - 1 ) : (double)PHP_INT_MAX ;
		 	$empty_option_default = ( is_null( $empty_option_default ) ) ? $large_value : $empty_option_default ;
		 	// Need to convert $value to a double only if it is numeric
		 	$value = ( is_numeric( $value ) ) ? (double)$value : $value ;
		 	if ( is_double( $value )) {
		 		$result = $value;
		 	} elseif ( isset( pb_backupbuddy::$options[ $option ] ) ) {
		 		$result = ( is_numeric( pb_backupbuddy::$options[ $option ] ) ) ? (double)( pb_backupbuddy::$options[ $option ] * $option_value_factor ) : (double)$empty_option_default ;

		 		// Array keys can only be string/int - for now we'll assume that we only map
		 		// values that can be cast as int - we may have to consider a different approach
		 		// later.
		 		$result = ( array_key_exists( (int)$result, $option_value_map ) ) ? $option_value_map[ (int)$result ] : $result ;

		 		// Special treatment - if the value was mapped to null then we want the derived "large" value
		 		$result = ( is_null( $result ) ) ? $large_value : $result ;
		 	} else {
		 		$result = (double)$default;
		 	}
		 	return $result;
		 }

		/**
		 *	set_ignore_warnings()
		 *
		 *	@param	mixed	$ignore		true|false for specific setting or null for choice
		 *	@return	object				This object
		 */
		public function set_ignore_warnings( $ignore = null ) {

		 	$this->_ignore_warnings = $this->derive_optional_bool( 'ignore_zip_warnings', $ignore, self::ZIP_DEFAULT_IGNORE_WARNINGS );

			return $this;

		}

		/**
		 *	get_ignore_warnings()
		 *
		 *	Returns the previously set ignore warnings flag
		 *
		 *	@return	mixed			The stored ignore warnings flag true|false
		 */
		public function get_ignore_warnings() {

			return $this->_ignore_warnings;

		}

		/**
		 *	set_ignore_symlinks()
		 *
		 *	@param	mixed	$ignore		true|false for specific setting or null for choice
		 *	@return	object				This object
		 */
		public function set_ignore_symlinks( $ignore = null ) {

		 	$this->_ignore_symlinks = $this->derive_optional_bool( 'ignore_zip_symlinks', $ignore, self::ZIP_DEFAULT_IGNORE_SYMLINKS );

			return $this;

		}

		/**
		 *	get_ignore_symlinks()
		 *
		 *	Returns the previously set ignore symlinks flag
		 *
		 *	@return	mixed			The stored ignore symlinks flag true|false
		 */
		public function get_ignore_symlinks() {

			return $this->_ignore_symlinks;

		}

		/**
		 *	set_compression()
		 *
		 *	@param	mixed	$compression	true|false for specific setting or null for choice
		 *	@return	object					This object
		 */
		public function set_compression( $compression = null ) {

		 	$this->_compression = $this->derive_optional_bool( 'compression', $compression, self::ZIP_DEFAULT_COMPRESSION );

			return $this;

		}

		/**
		 *	get_compression()
		 *
		 *	Returns the previously set compression flag
		 *
		 *	@return	mixed			The stored compression flag true|false
		 */
		public function get_compression() {

			return $this->_compression;

		}

		/**
		 *	set_step_period()
		 *
		 *	If the option is empty (blank) => default (30s)
		 *	If the option is 0 => infinite (PHP_MAX_INT)
		 *
		 *	@param	mixed	$set_period		integer for specific value or null for choice
		 *	@return	object					This object
		 */
		public function set_step_period( $step_period = null ) {

			$step_period_option_value_map = array( 0 => PHP_INT_MAX );

		 	$this->_step_period = $this->derive_optional_int( 'zip_step_period', $step_period, self::ZIP_DEFAULT_STEP_PERIOD, $step_period_option_value_map, 1, self::ZIP_DEFAULT_STEP_PERIOD );

			return $this;

		}

		/**
		 *	get_step_period()
		 *
		 *	Returns the previously set step period
		 *
		 *	@return	int			The stored step period int
		 */
		public function get_step_period() {

			return $this->_step_period;

		}

		/**
		 *	set_burst_gap()
		 *
		 *	If the option is empty (blank) => default (2s)
		 *
		 *	@param	mixed	$burst_gap		integer for specific value or null for choice
		 *	@return	object					This object
		 */
		public function set_burst_gap( $burst_gap = null ) {

		 	$this->_burst_gap = $this->derive_optional_int( 'zip_burst_gap', $burst_gap, self::ZIP_DEFAULT_BURST_GAP, array(), 1, self::ZIP_DEFAULT_BURST_GAP );

			return $this;

		}

		/**
		 *	get_burst_gap()
		 *
		 *	Returns the previously set burst gap
		 *
		 *	@return	int			The stored burst gap int
		 */
		public function get_burst_gap() {

			return $this->_burst_gap;

		}

		/**
		 *	set_min_burst_content()
		 *
		 *	We want this to be a double so it can have a larger value that we would
		 *	ever need to be used to represent no minimum
		 *
		 *	If the option is empty (blank) => default (10MB)
		 *	If the option is 0 => large value
		 *
		 *	@param	mixed	$min_burst_content		double for specific value or null for choice
		 *	@return	object							This object
		 */
		public function set_min_burst_content( $min_burst_content = null ) {

			// null is a special value that is further mapped to a "large" value for the
			// particular architecture
			$min_burst_content_option_value_map = array( 0 => null );

		 	$this->_min_burst_content = $this->derive_optional_double( 'zip_min_burst_content', $min_burst_content, self::ZIP_DEFAULT_MIN_BURST_CONTENT, $min_burst_content_option_value_map, (1024*1024), self::ZIP_DEFAULT_MIN_BURST_CONTENT );

			return $this;

		}

		/**
		 *	get_min_burst_content()
		 *
		 *	Returns the previously set min burst content size
		 *
		 *	@return	double			The stored min burst content size
		 */
		public function get_min_burst_content() {

			return $this->_min_burst_content;

		}

		/**
		 *	set_max_burst_content()
		 *
		 *	We want this to be a double so it can have a larger value that we would
		 *	ever need to be used to represent no minimum
		 *
		 *	If the option is empty (blank) => default (10MB)
		 *	If the option is 0 => large value
		 *
		 *	@param	mixed	$max_burst_content		double for specific value or null for choice
		 *	@return	object							This object
		 */
		public function set_max_burst_content( $max_burst_content = null ) {

			// null is a special value that is further mapped to a "large" value for the
			// particular architecture
			$max_burst_content_option_value_map = array( 0 => null );

		 	$this->_max_burst_content = $this->derive_optional_double( 'zip_max_burst_content', $max_burst_content, self::ZIP_DEFAULT_MAX_BURST_CONTENT, $max_burst_content_option_value_map, (1024*1024), self::ZIP_DEFAULT_MAX_BURST_CONTENT );

			return $this;

		}

		/**
		 *	get_max_burst_content()
		 *
		 *	Returns the previously set max burst content size
		 *
		 *	@return	double			The stored max burst content size
		 */
		public function get_max_burst_content() {

			return $this->_max_burst_content;

		}

		/**
		 *	set_supported_zip_methods()
		 *
		 *	Appends or prepends the method or methods passed to the existing supported methods array
		 *
		 *	@param	string/array	$methods	Either a (comma separated) string of methods or an array
		 *	@param	bool			$append		True if $methods should be appended to existing supported methods
		 *	@return	bool						True if set succeeded, otherwise false
		 */
		protected function set_supported_zip_methods( $methods, $append = true ) {

			$result = false;

			// If $methods is a string we need to turn it into an array (of one or more elements) or
			// otherwise assume it is an array already (but we double check in a mo)
			( is_string( $methods ) ) ? $methods_to_add = explode( ",", $methods ) : $methods_to_add = $methods;

			// Make sure we have an array and if so then either append or prepend to existing supported methods
			if ( is_array( $methods_to_add ) ) {

				( $append ) ? $this->_supported_zip_methods = array_merge( $this->_supported_zip_methods, $methods_to_add ) :
							  $this->_supported_zip_methods = array_merge( $methods_to_add, $this->_supported_zip_methods );

				$result = true;

			}

			// Will return false if we somehow didn't end up with an array to merge
			return $result;

		}

		/**
		 *	check_method_dependencies()
		 *
		 *	Checks the dependencies that a method defines for itself - this may optionally also mean
		 *	calling a given callback function that allows the method to add it's own very specific checks
		 *	beyond those that are run as standard.
		 *
		 *	@param	string		$class_name		The name of the class to check, needed because this is static checking
		 *	@return	bool						True if dependency check succeeded, otherwise false
		 */
		protected function check_method_dependencies( $class_name ) {

			// Assume dependency checks will pass - will be set false if a check fails
			$result = true;

			if ( !method_exists( $class_name, 'get_method_dependencies_static' ) ) {

				$result = false;

			} else {

				$method_dependencies = call_user_func( array( $class_name, 'get_method_dependencies_static' ) );

			}

			if ( ( $result ) && isset( $method_dependencies[ 'classes' ] ) && !empty( $method_dependencies[ 'classes' ] ) ) {

				$classes = $method_dependencies[ 'classes' ];

				$disabled_classes = array_map( "trim", explode( ',', ini_get( 'disable_classes' ) ) );

				// Check each function dependency and bail out on first failure
				foreach ( $classes as $class ) {

					$class = trim( $class );

					if ( !( ( class_exists( $class ) ) && ( !in_array( $class, $disabled_classes ) ) ) ) {

						$result = false;
						break;

					}

				}
			}

			if ( ( $result ) && isset( $method_dependencies[ 'functions' ] ) && !empty( $method_dependencies[ 'functions' ] ) ) {

				$functions = $method_dependencies[ 'functions' ];

				$disabled_functions = array_map( "trim", explode( ',', ini_get( 'disable_functions' ) ) );

				// Check each function dependency and bail out on first failure
				foreach ( $functions as $function ) {

					$function = trim( $function );

					if ( !( ( function_exists( $function ) ) && ( !in_array( $function, $disabled_functions ) ) ) ) {

						$result = false;
						break;

					}

				}

			}

			// No extension checks yet

			// No file checks yet (need to determine how this might work a bit better)

			if ( ( $result ) && isset( $method_dependencies[ 'check_func' ] ) && !empty( $method_dependencies[ 'check_func' ] ) ) {

				$result = call_user_func( array( $class_name, $method_dependencies[ 'check_func' ] ) );

			}

			return $result;

		}

		/**
		 *	deduce_zip_methods()
		 *
		 *	Returns the array of zip methods that are available (or just the best) filtered by requested methods.
		 *	Because the available methods don't really change often (rarely once stable) we use a transient
		 *	which has some defined lifetime so we don't waste time repeating the testing which involves creating
		 *	objects and processes and files which can be time consuming.
		 *	The using script can decide to have the transient refreshed by deleting it and then it will be regenerated.
		 *	Note: There is an included "signature" so that we can detect server or other moves and regenerate.
		 *	Note: filemtime() is used because this will (should) force the transient to update if the plugin is
		 *	updated because the file modification time of the file will change because the plugin is installed in a
		 *	different disk location with newly written files - the same should apply if the site is restored/migrated.
		 *
		 *	@param		array	Array reference for the deduced zip methods
		 *	@param		array	Arry reference for the details of the deduced methods
		 *	@param		array	Flat array of requested (preferred) zip methods
		 *	@param		bool	True if only the best available method wanted
		 *	@param		string	Which zip mode being tested
		 *	@return		bool	True if methods are available, False otherwise
		 *
		 */
		protected function deduce_zip_methods( array &$methods, array &$methods_details, array $requested, $best_only ) {

			$available_methods = array();
			$available_methods_details = array();
			$aggregate_available_methods = array();
			$server_signature_string = "";
			$server_signature = "";
			$use_cached_methods = false;

			// Decide if we should try for cached methods or not (save for later)
			if ( ( $use_cached_methods = $this->use_cached_methods() ) ) {

				$aggregate_available_methods = get_transient( $this->_zip_methods_transient );

				// Drop through if we didn't get a transient otherwise we'll test it for validity
				if ( false !== $aggregate_available_methods ) {

					// Generate server signature and check it matches the cached signature
					// Use current filename as component as it should be unique enough for this purpose
					$server_signature_string = __FILE__ . " : " . ( ( $filemodtime = filemtime( __FILE__ ) ) ? (string) $filemodtime : '1' );
					$server_signature = md5( $server_signature_string );

					if ( ( false === isset( $aggregate_available_methods[ 'control' ][ 'signature' ] ) ) ||
						 ( $server_signature !== $aggregate_available_methods[ 'control' ][ 'signature' ] ) ) {

						// Either no signature previously set or it has changed - either way we need to reevaluate available methods
						$aggregate_available_methods = false;

					}

				}

			} else {

				$this->log( 'details', 'Zip method caching disabled based on settings or unavailable.' );
				$aggregate_available_methods = false;

			}

			// Create new transient if we didn't have one, it was expired or we nuked it because invalid
			if ( false === $aggregate_available_methods ) {

				// Get all available methods in $available_methods - must return them in order best -> worst
				// Also getting the method details array which is keyed by method tag
				$this->get_available_zip_methods( $this->_supported_zip_methods, $available_methods, $available_methods_details );

				// Now we have to combine the two arrays into an aggregate to save
				$aggregate_available_methods[ 'methods' ] = $available_methods;
				$aggregate_available_methods[ 'details' ] = $available_methods_details;

				// Only save if we are using caching (determined earlier)
				if ( $use_cached_methods ) {

					// Add the server signature for detecting invalidated methods details on a migration or some other change
					// Note: See discussion above on derivation of signature
					// TODO: Check, probably can use the server signature calculated above
					$server_signature_string = __FILE__ . " : " . ( ( $filemodtime = filemtime( __FILE__ ) ) ? (string) $filemodtime : '1' );
					$server_signature = md5( $server_signature_string );
					$aggregate_available_methods[ 'control' ][ 'signature' ] = $server_signature;

					set_transient( $this->_zip_methods_transient, $aggregate_available_methods, self::ZIP_METHODS_TRANSIENT_LIFE );

				}

			} else {

				// We got a valid transient value so now separate the aggregate into two
				$available_methods = $aggregate_available_methods[ 'methods' ];
				$available_methods_details = $aggregate_available_methods[ 'details' ];

			}

			// Check whether these need to be filtered by requested methods
			if ( !empty( $requested ) ) {

				// Filter the available methods - result could be empty
				// Order will be retained regardless of order of requested methods
				// Renumber numeric keys fom 0
				$available_methods = array_values( array_intersect( $available_methods, $requested ) );

			}

			// If just the best available requested then slice it off
			if ( ( true === $best_only ) && ( !empty( $available_methods ) ) ) {

				$methods = array_slice( $available_methods, 0, 1 );
				$methods_details = $available_methods_details;

			} else {

				$methods = $available_methods;
				$methods_details = $available_methods_details;

			}

			if ( !empty( $methods ) ) {

				return true;

			} else {

				return false;

			}

		}

		/**
		 *	set_zip_method_caching_force_disabled()
		 *
		 *	Set whether or not zip method caching should be force disabled for this instance of zipbuddy
		 *
		 *	@param		bool	$disabled	Optional: True to disable; false (default) to not disable
		 *	@return		object				This object
		 *
		 */
		public function set_zip_method_caching_force_disabled( $disabled = false ) {

			$this->_zip_method_caching_force_disabled = $disabled;

			return $this;

		}

		/**
		 *	get_zip_method_caching_force_disabled()
		 *
		 *	Returns the boolean of whether zip methd caching for this instance of zipbuddy is disabled or not
		 *
		 *	@return		boolean		True if caching force disabled, False if not
		 *
		 */
		public function get_zip_method_caching_force_disabled() {

			return $this->_zip_method_caching_force_disabled;

		}

		/**
		 *	use_cached_methods()
		 *
		 *	Returns whether conditions/configuration indicate cached methods should be used
		 *	Note: Temporarily add the condition for whether get_/set_transient functions
		 *	exist - if not (meaning we are probably running under importbuddy) then we also
		 *	skip caching. This adds a little time when instantiating because we have to test
		 *	every time but it's acceptable for now. In the longer term we will have an
		 *	alternative way to handle the transient data outside of WordPress.
		 *
		 *	@return		bool	true if use cached methods, false otherwise
		 *
		 */
		protected function use_cached_methods() {

			// By default we'll be using caching
			$result = true;

			// Has caching been explicitly disabled
			$caching_disabled = ( isset( pb_backupbuddy::$options['disable_zipmethod_caching'] ) &&
								  ( pb_backupbuddy::$options['disable_zipmethod_caching'] == '1') );

			// Do we have the means to cache
			$caching_unavailable = ( !( function_exists( 'get_transient' ) && function_exists( 'set_transient' ) ) );

			// Has caching been force disabled for this particular instance of zipbuddy
			$caching_force_disabled = $this->get_zip_method_caching_force_disabled();

			if ( $caching_disabled || $caching_unavailable || $caching_force_disabled ) {

				$result = false;

			}

			return $result;

		}

		/**
		 *	get_zip_methods()
		 *
		 *	Returns the array of zip methods previously deduced
		 *
		 *	@return		array	Flat array of zip methods (could be empty)
		 *
		 */
		public function get_zip_methods() {

			return $this->_zip_methods;

		}

		/**
		 *	set_zip_methods()
		 *
		 *	Resets the zip methods based on new criteria and returns the array of zip methods
		 *	If the first attempt to deduce methods returns an empty result then force disable
		 *	use of cached zip methods and try again.
		 *
		 *	@param		array	$requested	Flat array of requested (preferred) zip methods
		 *	@param		bool	$best_only	Optional: True if only the best available method wanted
		 *	@return		object				This object
		 *
		 */
		public function set_zip_methods( array $requested, $best_only = false ) {

			$methods_available = false;

			// Update the memory of what zip methods were requested - make it clean
			$this->_requested_zip_methods = array_map( 'trim', $requested );

			// Work out the list of zip methods from the requested and available
			$methods_available = $this->deduce_zip_methods( $this->_zip_methods, $this->_zip_methods_details, $this->_requested_zip_methods, $best_only );

			// If we end up with no zip methods could be that caching not functional on this
			// server so we'll force override and try again for luck
			if ( true !== $methods_available ) {

				// Log that we are overriding caching
				$this->log( 'details', 'Zip method caching force disabled.' );

				$this->set_zip_method_caching_force_disabled( true );
				$this->deduce_zip_methods( $this->_zip_methods, $this->_zip_methods_details, $this->_requested_zip_methods, $best_only );

			}

			return $this;

		}

		/**
		 *	refresh_zip_methods()
		 *
		 *	Refresh the information on the available zip methods
		 *	TODO: Perhaps the transient could be deleted here rather than by the caller?
		 *
		 *	@param		array		$zip_methods	Optional: The set of zip methods requested to use
		 *	@return		object						This object
		 */
		public function refresh_zip_methods( $zip_methods = array() ) {

			$this->set_zip_methods( $zip_methods );

			return $this;

		}

		/**
		 *	sanitize_excludes()
		 *
		 *	Take an exclusion list of directories and/or files and produce a sanitized exclusion list
		 *	Directories will be recognized by having a trailing directory separator otherwise will be
		 *	treated as a file (note that here we are working with patterns and not testing to see
		 *	whether something _is_ a directory or not because we don't necessarily have the full
		 *	directory path).
		 *	Note: Anything that contains a wildcard character (* or ?) is ignored as these are not
		 *	currently supported (and maybe never will across the board). For command zip zip we can
		 *	consider an option to have these as separate exclusions (and currently we can accomodate
		 *	then through specifying environment ZIPOPTS.
		 *
		 *	@param	array		List of primary exclusions - may be empty
		 *	@param	array		List of secondary exclusions - may be empty
		 *	@param	string		The base directory to be used if normalizing
		 *
		 *	@return	mixed		array on success, false otherwise
		 */
		protected function sanitize_excludes( $primary, $secondary, $base = '' ) {

			$sanitized = array();

			$basedir = trim( $base );
			$normalize = !empty( $basedir );

			// Normalize the trailing directory separator on the path
			$basedir = rtrim( $basedir, self::DIRECTORY_SEPARATORS ) . self::NORM_DIRECTORY_SEPARATOR;

			// Normalize platform specific directory separators in path
			$basedir = str_replace( DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $basedir );

			// $primary is considered to be unclean
			foreach ( $primary as $exclude ) {

				// Reset flag for whether this exclude is a directory-like exclude
				$is_directory_path = false;

				// Get rid of standard prefix/suffix detritus
				$exclude = trim( $exclude );

				// Possible that we could end up with an empty entry
				// Also ignore if any wildcard characters present
				if ( !empty( $exclude ) && ( !preg_match( '|[?*]|', $exclude ) ) ) {

					// Remember if it has a directory separator suffix
					if ( preg_match( '|[' . addslashes( self::DIRECTORY_SEPARATORS ) . ']$|', $exclude ) ) {

						$is_directory_path = true;

					}

					// Remove what could be multiple prefix or suffix directory separators
					$exclude = trim( $exclude, self::DIRECTORY_SEPARATORS );

					// Make sure platform specific directory separators in path become normalized
					$exclude = str_replace( DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $exclude );

					// And add back a single instance prefix
					$exclude = self::NORM_DIRECTORY_SEPARATOR . $exclude;

					// And optionally a single instance suffix
					if ( $is_directory_path ) {

						$exclude .= self::NORM_DIRECTORY_SEPARATOR;

					}

					$sanitized[] = $exclude;

				}

			}

			// $secondary is considered to be clean
			if ( !empty( $secondary ) ) {

				$sanitized = array_merge( $sanitized, $secondary );

			}

			// Get unique entries and renumber numeric keys
			$sanitized = array_merge( array_unique( $sanitized ) );

			if ( true == $normalize ) {

				// Make sure the normalize base has a trailing directory separator
				$basedir = ( rtrim( $basedir, self::NORM_DIRECTORY_SEPARATOR ) ) . self::NORM_DIRECTORY_SEPARATOR;

				foreach ( $sanitized as &$exclusion ) {

					// Must remove any leading DIRECTORY_SEPARATOR because $basedir always has trailing
					$exclusion = ltrim( $exclusion, self::NORM_DIRECTORY_SEPARATOR );
					$exclusion = ( $basedir . $exclusion );

				}

			}

			return $sanitized;

		}

		/**
		 *	get_available_zip_methods()
		 *
		 *	Returns the array of zip methods that are available for the mode of this object
		 *	Libraries must have been loaded already
		 *
		 *	@param		array	The supported zip methods
		 *	@param		array	The array which will hold the available methods
		 *	@param		array	The array that will hold the available methods attributes (method tag is key)
		 *	@return		bool	True if methods available, False otherwise
		 *
		 */
		protected function get_available_zip_methods( array $supported_zip_methods, &$available_methods, &$available_methods_details ) {

			// Make sure these are cleared as the caller might not have done so
			$available_methods = array();
			$available_methods_details = array();

			// Try each supported zip method to see what it can do on this system
			foreach ( $supported_zip_methods as $method_tag ) {

				$class_name = 'pluginbuddy_zbzip' . $method_tag;

				// Let the method object inherit our logger
				$zipper = new $class_name( $this );

				if ( true === $zipper->is_available( $this->_tempdir ) ) {

					$available_methods[] = $method_tag;
					$available_methods_details[ $method_tag ] = $zipper->get_method_details();

				}

				unset( $zipper );
			}

			return ( !empty( $available_methods ) );

		}

		/**
		 *	get_compatibility_zip_methods()
		 *
		 *	Returns the array of zip methods that are regarded as "compatibility" methods
		 *	Libraries must have been loaded already
		 *
		 *	@return		array	Flat array of zip methods (could be empty)
		 *
		 */
		protected function get_compatibility_zip_methods() {

			$compatibility_methods = array();

			foreach ( $this->_zip_methods as $method_tag ) {

				$class_name = 'pluginbuddy_zbzip' . $method_tag;

				// Let the method object inherit our logger
				$zipper = new $class_name( $this );

				if ( $zipper->get_is_compatibility_method() === true ) {

					$compatibility_methods[] = $method_tag;

				}

				unset( $zipper );
			}

			return $compatibility_methods;

		}

		/**
		 *	get_best_zip_methods()
		 *
		 *	Returns the array of best zip method(s)
		 *	For now we assume only one "best" method as being the first method in the list
		 *	that has the requested attribute(s).
		 *	Libraries must have been loaded already
		 *
		 *	@param		array	$attributes	Array of attributes to check method supports
		 *	@return		array				Flat array of zip methods (could be empty)
		 *
		 */
		protected function get_best_zip_methods( $attributes = array() ) {

			$best_methods = array();

			if ( !empty( $this->_zip_methods ) ) {

				if ( empty( $attributes ) ) {

					// No attributes to test for so just take the first in the list
					$best_methods[] = $this->_zip_methods[ 0 ];

				} else {

					// Have some attributes to test so work along the list until we find a match or end of list
					foreach ( $this->_zip_methods as $method_tag ) {

						// Start afresh each time - assume success
						$match = true;
						foreach ( $attributes as $attribute ) {

							// Check each attribute in turn (precondition that all attributes set true/false)
							$match = ( $match && ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ $attribute ] === true ) );

						}

						if ( true === $match ) {

							// Found our matching method so break out of the test loop to return
							$best_methods[] = $method_tag;
							break;

						}

					}

				}


			}

			return $best_methods;

		}

		public function log( $level, $message ) {

			$this->get_logger()->log( $level, $message );

		}

		public function set_logger( $logger ) {

			$this->_logger = $logger;

			return $this;

		}

		public function get_logger() {

			if ( is_null( $this->_logger ) ) {

				$logger = new pluginbuddy_zipbuddy_null_object();
				$this->set_logger( $logger );

			}

			return $this->_logger;

		}

		public function set_process_monitor( $process_monitor ) {

			$this->_process_monitor = $process_monitor;

			return $this;

		}

		public function get_process_monitor() {

			if ( is_null( $this->_process_monitor ) ) {

				$pm = new pluginbuddy_zipbuddy_null_object();
				$this->set_process_monitor( $pm );

			}

			return $this->_process_monitor;

		}

		/**
		 *	create_empty_zip()
		 *
		 *	A function that creates an empty archive file with optional comment
		 *
		 *	Create an empty zip archive (just the end of central dir) with an optional
		 *	comment as well. This has a well known basic structure and content so we can
		 *	write it directly as binary data.
		 *
		 *	@param		string	$zip			Full path & filename of ZIP Archive file to create
		 *	@param		string	$comment		Comment to apply to archive. (may be empty)
		 *	@param		string	$tempdir		Full path of directory for temporary usage
		 *	@return		bool					True if the creation was successful, false otherwise
		 *
		 */
		public function create_empty_zip( $zip, $comment, $tempdir ) {

			$result = false;
			$zip_file_name = $zip;

			$this->log( 'details', sprintf( __( 'Zip process reported: Initializing zip archive file: %1$s', 'it-l10n-backupbuddy' ), $zip_file_name ) );

			try {

				$zip_file = new SplFileObject( $zip_file_name, "wb" );

				// Encode $comment if an array.
				if ( is_array( $comment ) ) {

					$comment = json_encode( $comment );

				}

				// Don't add an empty comment - so if the encoded string is empty or an empty
				// string was passed in originally don't do anything.
				if ( 0 != strlen( $comment ) ) {

					$comment = 'MetaData:' . $comment . 'MetaData-End:';

				}

				// ----- Packed data
				$binary_data = pack( "VvvvvVVv", 0x06054b50, 0, 0, 0, 0, 0, 0, strlen( $comment ) );

				// ----- Write the 22 bytes of the header in the zip file
				$zip_file->fwrite( $binary_data, 22 );

				// ----- Write the variable fields
				if ( 0 != strlen( $comment ) ) {

				  $zip_file->fwrite( $comment, strlen( $comment ) );

				}

				unset( $zip_file );

				$this->log( 'details', sprintf( __( 'Zip process reported: Initialized zip archive file', 'it-l10n-backupbuddy' ) ) );
				$result = true;

			} catch ( Exception $e ) {

				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('Zip process reported: Failure to initialize zip archive file - error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );
			}

			return $result;

		}

		/**
		 *	add_virtual_file_to_zip()
		 *
		 *	Adds a virtual file to a zip archive
		 *
		 *	@param	string				Full path & filename of ZIP file to add virtual file to
		 *	@param	array				Virtual file(s) descriptor
		 *	@param	string				Full directory path to directory to temporarily place ZIP
		 *
		 *	@return						true on success, false otherwise
		 *
		 */
		public function add_virtual_file_to_zip( $zip_file, $virtual_file_descriptor, $temporary_zip_directory = '' ) {

			$zip_methods = array();

			// Use whichever method supports adding virtual files
			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to add virtual file - no available methods.','it-l10n-backupbuddy' ) );

				// We should have a temporary directory, must get rid of it, can simply rmdir it as it will (should) be empty
				if ( !empty( $temporary_zip_directory ) && file_exists( $temporary_zip_directory ) ) {

					if ( !rmdir( $temporary_zip_directory ) ) {

						$this->log( 'details', __('Temporary directory could not be deleted: ','it-l10n-backupbuddy' ) . $temporary_zip_directory );

					}

				}

				return false;

			}

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can archive with this method
				if ( ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_archiver' ] === true ) &&
				     ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'can_add_virtual_file' ] === true ) ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					// Set these on specific zipper based on the values we derived at construnction or
					// overridden by subsequent method calls
					$zipper->set_compression( $this->get_compression() );
					$zipper->set_ignore_symlinks( $this->get_ignore_symlinks() );
					$zipper->set_ignore_warnings( $this->get_ignore_warnings() );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Tell the method the server api in use
					$zipper->set_sapi_name( $this->get_sapi_name() );

					// As we are looping make sure we have no stale file information
					clearstatcache();

					// The temporary zip directory _must_ exist
					if ( !empty( $temporary_zip_directory ) ) {

						if ( !file_exists( $temporary_zip_directory ) ) { // Create temp dir if it does not exist.

							mkdir( $temporary_zip_directory );

						}

					}

					// Now we are ready to try and add the virtual file
					$result = $zipper->add_virtual_file( $zip_file, $virtual_file_descriptor, $temporary_zip_directory );

					// Will be false if we couldn't add the virtual file to the archive
					if ( $result === true ) {

						// Must assume that we added the file ok
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return true;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we get here then have failed in all attempts
			$this->log( 'details', __('Failed to add virtual file with any nominated method.','it-l10n-backupbuddy' ) );

			return false;

		}

		/**
		 *	add_directory_to_zip()
		 *
		 *	Adds a directory to a new or existing (TODO: not yet available) ZIP file.
		 *
		 *	@param	string	$zip_file					Full path & filename of ZIP file to create.
		 *	@param	string	$add_directory				Full directory to add to zip file.
		 *	@param	array	$excludes					Array of strings of paths/files to exclude from zipping
		 *	@param	string	$temporary_zip_directory	Full directory path to directory to temporarily place ZIP
		 *
		 *	@return						true on success, false otherwise
		 *
		 */
		public function add_directory_to_zip( $zip_file, $add_directory, $excludes = array(), $temporary_zip_directory = '' ) {

			if ( true === $this->_is_experimental ) {

				$this->log( 'message', __('Running alternative ZIP system (BETA) based on settings.','it-l10n-backupbuddy' ) );

			} else {

				$this->log( 'message', __('Running standard ZIP system based on settings.','it-l10n-backupbuddy' ) );

			}

			// Let's just log if this is a 32 or 64 bit system
			$php_size = ( pluginbuddy_stat::is_php( pluginbuddy_stat::THIRTY_TWO_BIT ) ) ? "32" : "64" ;
			$this->log( 'details', sprintf( __( 'Running under %1$s-bit PHP', 'it-l10n-backupbuddy' ), $php_size ) );

			// Make sure we tell what the sapi is
			$this->log( 'details', sprintf( __( 'Server API: %1$s', 'it-l10n-backupbuddy' ), $this->get_sapi_name() ) );

			$zip_methods = array();
			$sanitized_excludes = array();

			// Set some additional system excludes here for now - these are all from the site install root
			$additional_excludes = array();

			// Make sure we have a valid zip method strategy setting to use otherwise fall back to emergency compatibility
			if ( isset( pb_backupbuddy::$options[ 'zip_method_strategy' ] ) && 	( '0' !== pb_backupbuddy::$options[ 'zip_method_strategy' ] ) ) {

				$zip_method_strategy = pb_backupbuddy::$options[ 'zip_method_strategy' ];
				switch ( $zip_method_strategy ) {
					case "1":
						// Best Available
						$zip_methods = $this->get_best_zip_methods( array( 'is_archiver' ) );
						$this->log( 'details', __('Using Best Available zip method based on settings.','it-l10n-backupbuddy' ) );
						break;
					case "2":
						// All Available
						$zip_methods = $this->_zip_methods;
						$this->log( 'details', __('Using All Available zip methods in preferred order based on settings.','it-l10n-backupbuddy' ) );
						break;
					case "3":
						// Force Compatibility
						$zip_methods = $this->get_compatibility_zip_methods();
						$this->log( 'message', __('Using Forced Compatibility zip method based on settings.','it-l10n-backupbuddy' ) );
						break;
					default:
						// Hmm...unrecognized value - emergency compatibility
						$zip_methods = $this->get_compatibility_zip_methods();
						$this->log( 'message', sprintf( __('Forced Compatibility Mode as Zip Method Strategy setting not recognized: %1$s','it-l10n-backupbuddy' ), $zip_method_strategy ) );
				}

			} else {

				// We got no or an invalid zip method strategy which is a bad situation - emergency compatibility is the order of the day
				$zip_methods = $this->get_compatibility_zip_methods();
				$this->log( 'message', __('Forced Compatibility Mode as Zip Method Strategy not set or setting not recognized.','it-l10n-backupbuddy' ) );

			}

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to create a Zip Archive file - no available methods.','it-l10n-backupbuddy' ) );

				// We should have a temporary directory, must get rid of it, can simply rmdir it as it will (should) be empty
				if ( !empty( $temporary_zip_directory ) && file_exists( $temporary_zip_directory ) ) {

					if ( !rmdir( $temporary_zip_directory ) ) {

						$this->log( 'details', __('Temporary directory could not be deleted: ','it-l10n-backupbuddy' ) . $temporary_zip_directory );

					}

				}

				return false;

			}

			$this->log( 'details', __('Creating ZIP file','it-l10n-backupbuddy' ) . ' `' . $zip_file . '`. ' . __('Adding directory','it-l10n-backupbuddy' ) . ' `' . $add_directory . '`. ' . __('Excludes','it-l10n-backupbuddy' ) . ': ' . implode( ',', $excludes ) );

			// We need the classes for being able to build backup file list
			require_once( pb_backupbuddy::plugin_path() . '/lib/' . $this->_whereami . '/zbdir.php' );
			if ( !class_exists( 'pluginbuddy_zbdir' ) ) {

				// Hmm, require_once() didn't bomb but we haven't got the class we expect - bail out
				$this->log( 'details', __('Unable to load classes for backup file list builder.','it-l10n-backupbuddy' ) );

				return false;

			}

			// Generate our sanitized list of directories/files to exclude as relative paths
			$sanitized_excludes = $this->sanitize_excludes( $excludes, $additional_excludes );

			// Do the same for directories/files to include
			//$sanitized_includes = $this->sanitize_excludes( $includes, $additional_includes );

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can archive with this method
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_archiver' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					// Zipper will initially inherit our logger and our
					// process monitor
					$zipper = new $class_name( $this );

					// Now override logger - will define a prefix here
					$zipper->set_logger( $this->_default_child_logger );

					// Set these on specific zipper based on the values we derived at construnction or
					// overridden by subsequent method calls
					$zipper->set_compression( $this->get_compression() );
					$zipper->set_ignore_symlinks( $this->get_ignore_symlinks() );
					$zipper->set_ignore_warnings( $this->get_ignore_warnings() );
					$zipper->set_step_period( $this->get_step_period() );
					$zipper->set_burst_gap( $this->get_burst_gap() );
					$zipper->set_min_burst_content( $this->get_min_burst_content() );
					$zipper->set_max_burst_content( $this->get_max_burst_content() );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Tell the method the server api in use
					$zipper->set_sapi_name( $this->get_sapi_name() );

					$this->log( 'details', __('Trying ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP.','it-l10n-backupbuddy' ) );

					// As we are looping make sure we have no stale file information
					clearstatcache();

					// The temporary zip directory _must_ exist
					if ( !empty( $temporary_zip_directory ) ) {

						if ( !file_exists( $temporary_zip_directory ) ) { // Create temp dir if it does not exist.

							mkdir( $temporary_zip_directory );

						}

					}

					// Now we are ready to try and produce the backup
					if (  true === ( $result = $zipper->create( $zip_file, $add_directory, $sanitized_excludes, $temporary_zip_directory ) ) ) {

						// Got a valid zip file so we can just return - method will have cleaned up the temporary directory
						$this->log( 'details', __('The ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP was successful.','it-l10n-backupbuddy' ) );
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return true;

					} elseif ( is_array( $result ) ) {

						// Didn't finish zip creation on that step so we need to set up for another step
						// Add in any addiitonal state information and simply return the state
						$this->log( 'details', __('The ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP partially completed.','it-l10n-backupbuddy' ) );
						unset( $zipper );
						return $result;

					} else {

						// We failed on the first step for one eason or another - may be an option
						// to try with another method...
						// Method will have cleaned up the temporary directory
						$this->log( 'details', __('The ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP was unsuccessful.','it-l10n-backupbuddy' ) );
						unset( $zipper );

					}

				} else {

					// This method is not considered suitable (reliable enough) for creating archives or lacked zip capability
					$this->log( 'details', __('The ','it-l10n-backupbuddy' ) . $method_tag . __(' method is not currently supported for backup.','it-l10n-backupbuddy' ) );

				}

			}

			// If we get here then have failed in all attempts
			$this->log( 'details', __('Failed to create a Zip Archive file with any nominated method.','it-l10n-backupbuddy' ) );

			return false;

		}

		/**
		 *	grow_zip()
		 *
		 *	This is called after the first step and carries on growing the zip with he already
		 *	calculated archive content
		 *
		 *	@param	string	$zip_file					Full path & filename of ZIP file to grow.
		 *	@param	string	$temporary_zip_directory	Full directory path to directory were we are working
		 *	@param	array	$state						State array that we need to pick up where we left off
		 *
		 *	@return	mixed								true on completion of archive, array for continuation, false on failure
		 *
		 */
		public function grow_zip( $zip_file, $temporary_zip_directory, $state ) {

			// Initialize the zip method tag of the method we are using
			$zip_methods = array( $state[ 'zipbuddy' ][ 'mt' ] );

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can archive with this method
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_archiver' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					// Zipper will initially inherit our logger and
					// our process monitor
					$zipper = new $class_name( $this );

					// Now override logger - will define a prefix here
					$zipper->set_logger( $this->_default_child_logger );

					// Set these on specific zipper based on the values we derived at construnction or
					// overridden by subsequent method calls
					$zipper->set_compression( $this->get_compression() );
					$zipper->set_ignore_symlinks( $this->get_ignore_symlinks() );
					$zipper->set_ignore_warnings( $this->get_ignore_warnings() );
					$zipper->set_step_period( $state[ 'zipper' ][ 'sp' ] );
					$zipper->set_burst_gap( $this->get_burst_gap() );
					$zipper->set_min_burst_content( $this->get_min_burst_content() );
					$zipper->set_max_burst_content( $this->get_max_burst_content() );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Tell the method the server api in use
					$zipper->set_sapi_name( $this->get_sapi_name() );

					$this->log( 'details', __('Trying ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP.','it-l10n-backupbuddy' ) );

					// As we are looping make sure we have no stale file information
					clearstatcache();

					// The temporary zip directory _must_ exist
					if ( !empty( $temporary_zip_directory ) ) {

						if ( !file_exists( $temporary_zip_directory ) ) { // Create temp dir if it does not exist.

							mkdir( $temporary_zip_directory );

						}

					}

					// Now we are ready to try and produce the backup
					if (  true === ( $result = $zipper->grow( $zip_file, $temporary_zip_directory, $state ) ) ) {

						// Got a valid zip file so we can just return - method will have cleaned up the temporary directory
						$this->log( 'details', __('The ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP was successful.','it-l10n-backupbuddy' ) );
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return true;

					} elseif ( is_array( $result ) ) {

						// Didn't finish zip creation on that step so we need to set up for another step
						// Add in any addiitonal state information and simply return the state
						$this->log( 'details', __('The ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP partially completed.','it-l10n-backupbuddy' ) );
						unset( $zipper );
						return $result;

					} else {

						// We failed on a continuation step so we are done really...
						// Method will have cleaned up the temporary directory
						$this->log( 'details', __('The ', 'it-l10n-backupbuddy' ) . $method_tag . __(' method for ZIP was unsuccessful.','it-l10n-backupbuddy' ) );
						unset( $zipper );

					}

				} else {

					// This method is not considered suitable (reliable enough) for creating archives or lacked zip capability
					$this->log( 'details', __('The ','it-l10n-backupbuddy' ) . $method_tag . __(' method is not currently supported for backup.','it-l10n-backupbuddy' ) );

				}

			}

			// If we get here then have failed in all attempts
			$this->log( 'details', __('Failed to create a Zip Archive file with any nominated method.','it-l10n-backupbuddy' ) );

			return false;

		}

		/**
		 *	unzip()
		 *
		 *	Extracts the contents of a zip file to the specified directory using the best unzip methods possible.
		 *
		 *	@param	string		$zip_file					Full path & filename of ZIP file to extract from.
		 *	@param	string		$destination_directory		Full directory path to extract into.
		 *	@param	bool|string $force_compatibility_mode	False: use all available, String: use that method
		 *
		 *	@return	bool									true on success, false otherwise
		 */
		public function unzip( $zip_file, $destination_directory, $force_compatibility_mode = false ) {

			$zip_methods = array();

			// The following is just to match current functionality for importbuddy - ideally would rather
			// do it by selecting available compatibility methods based on method attributes - may do that later
			// (would also need get_compatibility_zip_methods() to be updated to take parameter to check
			// whether compatibility method for that particular function.

			// Decide which methods we are going to try
			if ( $force_compatibility_mode == 'ziparchive' ) {

				$zip_methods = array( 'ziparchive' );
				$this->log( 'message', __('Forced compatibility unzip method (ZipArchive; medium speed) based on settings.','it-l10n-backupbuddy' ) );

			} elseif ( $force_compatibility_mode == 'pclzip' ) {

				$zip_methods = array( 'pclzip' );
				$this->log( 'message', __('Forced compatibility unzip method (PCLZip; slow speed) based on settings.','it-l10n-backupbuddy' ) );

			} else {

				$zip_methods = $this->_zip_methods;
				$this->log( 'details', __('Using all available unzip methods in preferred order.','it-l10n-backupbuddy' ) );
			}

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', sprintf( __('Unable to extract backup file contents (%1$s to %2$s): No available unzip methods found.','it-l10n-backupbuddy' ), $zip_file, $destination_directory ) );

				return false;

			}

			// Make sure we have a normalized directory separator suffix
			$destination_directory = rtrim( $destination_directory, self::DIRECTORY_SEPARATORS ) . self::NORM_DIRECTORY_SEPARATOR;

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can check file existence with this method (ignore silently if not)
				// Note: has to be able to unzip as well but if that functionality wasn't available in
				// the method the is_checker attribute will have been set false
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_unarchiver' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and extract the backup
					$result = $zipper->extract( $zip_file, $destination_directory );

					// Will be false if we couldn't extract the backup
					if ( $result === true ) {

						// Must assume that we extracted ok
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return true;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then no method to extract backup content was available or worked
			$this->log( 'details', sprintf( __('Unable to extract backup file contents (%1$s to %2$s): No compatible zip method found.','it-l10n-backupbuddy' ), $zip_file, $destination_directory ) );
			return false;

		}

		/**
		 *	extract()
		 *
		 *	Extracts the specified contents of a zip file to the specified destination using the best unzip methods possible.
		 *	The destination directory _must_ already exist and be writable - this function does not create it
		 *	The items are an array of mapping of what => where, e.g.
		 *	array( "dir/myfile.txt" => "", "dir/myfile.txt" => "tmpfilename", "dir/myfile.txt" => "dir/myfile.txt" )
		 *	In the first case the file is extracted to $destination_directory/myfile.txt
		 *	In the second case the file is extracted to $destination_directory/tmpfilename
		 *	In the third case the file is extracted to $destination_directory/dir/myfile.txt
		 *	Note: in the second case the file is initially extrcated as myfile.txt and then rename to tmpfilename
		 *	Another example is for directory extraction:
		 *	array( "dir/*" => "dir/*" )
		 *	Whereby the directory dir and all it's content (recursively) is extracted to $destination/dir
		 *	Note: the * is required otherwise you just get an empty directory
		 *
		 *	@param	string	$zip_file				Full path & filename of ZIP file to extract from.
		 *	@param	string	$destination_directory	Full directory path to extract to
		 *	@param	array	$items					Mapping of what to extract and to what
		 *
		 *	@return	bool							true on success (all extractions successful), false otherwise
		 */
		public function extract( $zip_file, $destination_directory = '', $items = array() ) {

			$zip_methods = array();

			// The following is just to match current functionality for importbuddy - ideally would rather
			// do it by selecting available compatibility methods based on method attributes - may do that later
			// (would also need get_compatibility_zip_methods() to be updated to take parameter to check
			// whether compatibility method for that particular function.

			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', sprintf( __('Unable to extract from backup file (%1$s to %2$s): No available unzip methods found.','it-l10n-backupbuddy' ), $zip_file, $destination ) );

				return false;

			}

			if ( !( file_exists( $destination_directory ) && is_dir( $destination_directory ) && is_writable( $destination_directory ) ) ) {

				$this->log( 'details', sprintf( __('Unable to extract from backup file (%1$s to %2$s): %2$s does not exist, is not a directory or is not writeable','it-l10n-backupbuddy' ), $zip_file, $destination_directory ) );

				return false;

			}

			// Make sure we have a normalized directory separator suffix
			$destination_directory = rtrim( $destination_directory, self::DIRECTORY_SEPARATORS ) . self::NORM_DIRECTORY_SEPARATOR;

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can check file existence with this method (ignore silently if not)
				// Note: has to be able to unzip as well but if that functionality wasn't available in
				// the method the is_checker attribute will have been set false
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_extractor' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and extract from the backup
					$result = $zipper->extract( $zip_file, $destination_directory, $items );

					// Will be false if we couldn't extract from the backup
					if ( $result === true ) {

						// Must assume that we extracted ok
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return true;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then no method to extract from backup content was available or worked
			$this->log( 'details', sprintf( __('Unable to extract from backup file (%1$s to %2$s): No compatible zip method found.','it-l10n-backupbuddy' ), $zip_file, $destination_directory ) );
			return false;

		}

		/**
		 *	file_exists()
		 *
		 *	Tests whether a file (with path) exists in the given zip file
		 *	If leave_open is true then the zip object will be left open for faster checking for subsequent files within this zip
		 *
		 *	@param		string	$zip_file		The zip file to check
		 *	@param		string	$locate_file	The file to test for
		 *	@param		bool	$leave_open		Optional: True if the zip file should be left open
		 *	@return		bool					True if the file is found in the zip otherwise false
		 *
		 */
		public function file_exists( $zip_file, $locate_file, $leave_open = false ) {

			$zip_methods = array();

			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to check file exists - no available methods.','it-l10n-backupbuddy' ) );

				return false;

			}

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can check file existence with this method (ignore silently if not)
				// Note: has to be able to unzip as well but if that functionality wasn't available in
				// the method the is_checker attribute will have been set false
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_checker' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and test for the file existence
					$result = $zipper->file_exists( $zip_file, $locate_file, $leave_open );

					// Will be true/false if file found/not-found otherwise error information array
					if ( !is_array( $result ) ) {

						// Either we found the file or we didn't but we have a valid result
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return $result;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then no method to check backup content was available or worked
			$this->log( 'details', sprintf( __('Unable to check if file exists (looking for %1$s in %2$s): No compatible zip method found.','it-l10n-backupbuddy' ), $locate_file, $zip_file ) );
			return false;

		}

		/*	get_file_list()
		 *
		 *	Get an array of all files in a zip file with some file properties.
		 *
		 *	@param		string		$zip_file	The file to list the content of
		 *	@return		bool|array				false on failure, otherwise array of file properties (may be empty)
		 */
		public function get_file_list( $zip_file ) {

			$zip_methods = array();

			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to list backup file contents - no available methods.','it-l10n-backupbuddy' ) );

				return false;

			}

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can list backup file content with this method (ignore silently if not)
				// Note: has to be able to unzip as well but if that functionality wasn't available in
				// the method the is_lister attribute will have been set false
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_lister' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and test for the file existence
					$result = $zipper->get_file_list( $zip_file );

					// Will be false if we couldn't list contents or file list array otherwise
					if ( is_array( $result ) ) {

						// We got a list so better assume it is ok
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return $result;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then no method to list backup file content was available or worked
			$this->log( 'details', sprintf( __('Unable to check file content of backup (%1$s): No compatible zip method found.','it-l10n-backupbuddy' ), $zip_file ) );
			return false;

		}

		public function get_file_contents( $zip_file, $file_path ) {
			$zip_methods = array();

			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to list backup file contents - no available methods.','it-l10n-backupbuddy' ) );

				return false;

			}

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can list backup file content with this method (ignore silently if not)
				// Note: has to be able to unzip as well but if that functionality wasn't available in
				// the method the is_lister attribute will have been set false
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_lister' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and test for the file existence
					$result = $zipper->get_file_contents( $zip_file, $file_path );

					// Will be false if we couldn't list contents or file list array otherwise
					if ( ! empty( $result ) ) {

						// We got a list so better assume it is ok
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return $result;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then no method to list backup file content was available or worked
			$this->log( 'details', sprintf( __('Unable to check file content of backup (%1$s): No compatible zip method found.','it-l10n-backupbuddy' ), $zip_file ) );
			return false;

		}

		/*	set_comment()
		 *
		 *	Retrieve archive comment.
		 *
		 *	@param		string			$zip_file		Filename of archive to set comment on.
		 *	@param		string|array	$comment		Comment to apply to archive. If array, json encoded. Deliminated with MetaData: and MetaData-End:.
		 *	@return		bool|string						true on success, error message otherwise.
		 */
		public function set_comment( $zip_file, $comment ) {

			$zip_methods = array();

			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to set comment in backup file - no available methods.','it-l10n-backupbuddy' ) );

				return false;

			}

			// Encode $comment if an array. Handle delimination.
			if ( is_array( $comment ) ) {
				$comment = json_encode( $comment );
			}
			$comment = 'MetaData:' . $comment . 'MetaData-End:';

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can manage comments with this method (ignore silently if not)
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_commenter' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and test for the file existence
					$result = $zipper->set_comment( $zip_file, $comment );

					// Will be false if we couldn't set the comment
					if ( $result === true ) {

						// Must assume that comment was set ok
						unset( $zipper );

						// We have to return here because we cannot break out of foreach
						return true;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then couldn't set a comment at all - either no available method or all method failed
			$this->log( 'details', sprintf( __('Unable to set comment in file %1$s: No compatible zip method found or all methods failed - note stored internally only.','it-l10n-backupbuddy' ), $zip_file ) );

			// Return message for display - maybe should return false and have caller display it's own message?
			$message = "\n\nUnable to set note in file.\nThe note will only be stored internally in your settings and not in the zip file itself.";
			return $message;

		}

		/*	get_comment()
		 *
		 *	Retrieve archive comment.
		 *
		 *	@param		string				$zip_file		Filename of archive to retrieve comment from.
		 *	@param		bool				$raw_comment	If true then raw comment field data returned without processing deliminators nor json. Defaults false.
		 *	@return		bool|string|array					false on failure, Zip comment otherwise. If comment is json encoded array returns array.
		 */
		public function get_comment( $zip_file, $raw_comment = false ) {

			$zip_methods = array();

			$zip_methods = $this->_zip_methods;

			// Better make sure we have some available methods
			if ( empty( $zip_methods ) ) {

				// Hmm, we don't seem to have any available methods, oops, best go no further
				$this->log( 'details', __('Failed to get comment from backup file - no available methods.','it-l10n-backupbuddy' ) );

				return false;

			}

			// Iterate over the methods - once we succeed just return directly otherwise drop through
			foreach ( $zip_methods as $method_tag ) {

				// First make sure we can manage comments with this method (ignore silently if not)
				if ( $this->_zip_methods_details[ $method_tag ][ 'attr' ][ 'is_commenter' ] === true ) {

					$class_name = 'pluginbuddy_zbzip' . $method_tag;

					$zipper = new $class_name( $this );

					$zipper->set_logger( $this->_default_child_logger );

					// We need to tell the method what details belong to it
					$zipper->set_method_details( $this->_zip_methods_details[ $method_tag ] );

					// Now we are ready to try and test for the file existence
					$result = $zipper->get_comment( $zip_file );

					// Will be false if we couldn't set the comment
					if ( is_string ( $result ) ) {

						// Format has changed and no longer encoding as htmlemtities when setting comment
						// For older backups may need to remove encoding - action _should_ be null if N/A
						// Only spanner would be if someone had put an entity in their comment but that is
						// really an outsider and in any case the correction is simply to edit and resave
						// TODO: Remove this when new format has been in use for some time
						$result = html_entity_decode( $result );

						// Must assume that comment was retrieved ok
						unset( $zipper );

						// Return raw comment as-is with no processing if specified.
						if ( true === $raw_comment ) {
							return $result;
						}

						// Handle delimination. Decode $result if json decoded (associative array mode).
						$start_deliminator = strpos( $result, 'MetaData:' );
						$end_deliminator = strpos( $result, 'MetaData-End:' );
						if ( ( false !== $start_deliminator ) && ( false !== $end_deliminator ) ) { // Found both deliminators.
							$result = substr( $result, $start_deliminator+9, $end_deliminator-9 );
							if ( NULL === ( $decoded_result = json_decode( $result, true ) ) ) { // Json decode failed so return string.
								return $result;
							} else { // Json decode success so returning variable (should be an array most likely).
								return $decoded_result;
							}

						}

						// No deliminators found if made it to this point so assuming plain text legacy comment (or deliminators missing/corrupt).

						// We have to return here because we cannot break out of foreach
						return $result;

					} else {

						// The zipper encountered an error so we need to drop through and loop round to try another
						// We'll not process the result here, just drop through silently (the method will have logged it)
						unset( $zipper );

					}

				}

			}

			// If we got this far then couldn't get a comment at all - either no available method or all method failed
			$this->log( 'details', sprintf( __('Unable to get comment in file %1$s: No compatible zip method found or all methods failed.','it-l10n-backupbuddy' ), $zip_file ) );

			return false;

		}

	} // End class

}
