<?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' ) ) {

	// 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" ) ) {

	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 = '/\\';


        /**
         * 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();

        /**
         * The mode which the object is being created for
         * Note: This is now ignored as an object is instantiated for both zip & unzip services
         * 
         * @var string
         */
        protected $_mode = "";

        /**
         * 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 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;
		
		/**
		 * 
		 * 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
		 *	@param		string		$mode			Optional: The zip mode for the object (ignored currently - may be reused?)
		 *	@return		null
		 *
		 */
		public function __construct( $temp_dir, $zip_methods = array(), $mode = 'zip' ) {

			// 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 );
			
			// Note: this will be removed and should no longer be used - an object is instantiated for
			// both zip & unzip services
			$this->_mode = $mode;
			
			// Record where we are located (the directory name)
			$this->_whereami = basename( dirname( __FILE__ ) );
			
			// Use our location to determine which zip methods transient we should be using
			$this->_zip_methods_transient = ( 0 === strcmp( $this->_whereami, 'zipbuddy' ) ) ? self::ZIP_METHODS_TRANSIENT : self::ZIP_METHODS_TRANSIENT_EXPERIMENTAL ;

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

			// 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();
						
			// 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( ) {

		}
		
		/**
		 *	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		bool		$value		Should be bool true|false but could be null
		 *	@return		bool		Value of $_ignore_warnings
		 *
		 */
		 protected function derive_optional_bool( $option, $value, $default ) {
		 	$result = false;
		 	if ( is_bool( $value )) {
		 		$result = $value;
		 	} elseif ( isset( pb_backupbuddy::$options[ $option ] ) ) {
		 		( ( pb_backupbuddy::$options[ $option ] == '1' ) || ( pb_backupbuddy::$options[ $option ] == true ) ) ? $result = true : $result = false ;
		 	} else {
		 		$result = $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, false );
		 	
			return $this;
			
		}

		/**
		 *	get_ignore_warnings()
		 *
		 *	Returns the previously set ignore warnings flag
		 *
		 *	@return	mixed			The stored ignore warnings flag true|false|null
		 */
		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, true );
			
			return $this;
			
		}

		/**
		 *	get_ignore_symlinks()
		 *
		 *	Returns the previously set ignore symlinks flag
		 *
		 *	@return	mixed			The stored ignore symlinks flag true|false|null
		 */
		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, true );
			
			return $this;
			
		}

		/**
		 *	get_compression()
		 *
		 *	Returns the previously set compression flag
		 *
		 *	@return	mixed			The stored compression flag true|false|null
		 */
		public function get_compression() {
			
			return $this->_compression;
			
		}

		/**
		 *	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 = "";

			// Decide if we should try for cached methods or not			
			if ( $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 {
			
				$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
				if ( $this->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
				$available_methods = 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;
				
			}
		
		}
				
		/**
		 *	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' ) ) );	

			if ( $caching_disabled || $caching_unavailable ) {
			
				pb_backupbuddy::status( 'details', 'Zip method caching disabled based on settings or unavailable.' );
				$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
		 *	
		 *	@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 ) {
			
			// 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
			$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;
	
				$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;
	
				$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;
			
		}
		
		/**
		 *	add_directory_to_zip()
		 *
		 *	Adds a directory to a new or existing (TODO: not yet available) ZIP file.
		 *
		 *	@param	string				Full path & filename of ZIP file to create.
		 *	@param	string				Full directory to add to zip file.
		 *	@param	array( string )		Array of strings of paths/files to exclude from zipping
		 *	@param	string				Full directory path to directory to temporarily place ZIP
		 *	@param	boolean				True: only use PCLZip. False: try all available
		 *
		 *	@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 ) {
			
				pb_backupbuddy::status( 'message', __('Running alternative ZIP system (BETA) based on settings.','it-l10n-backupbuddy' ) );
			
			} else {
			
				pb_backupbuddy::status( '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" ;
			pb_backupbuddy::status( 'details', sprintf( __( 'Running under %1$s-bit PHP', 'it-l10n-backupbuddy' ), $php_size ) );
			
			// Make sure we tell what the sapi is
			pb_backupbuddy::status( 'details', sprintf( __( 'Server API: %1$s', 'it-l10n-backupbuddy' ), $this->get_sapi_name() ) );			
					
			$zip_methods = array();
			$sanitized_excludes = array();
			$listmaker = NULL;
			
			// Set some additional system excludes here for now - these are all from the site install root
			$additional_excludes = array( self::NORM_DIRECTORY_SEPARATOR . 'importbuddy' . self::NORM_DIRECTORY_SEPARATOR,
										  self::NORM_DIRECTORY_SEPARATOR . 'importbuddy.php',
										  self::NORM_DIRECTORY_SEPARATOR . 'wp-content' . self::NORM_DIRECTORY_SEPARATOR . 'uploads' . self::NORM_DIRECTORY_SEPARATOR . 'pb_backupbuddy' . self::NORM_DIRECTORY_SEPARATOR,
										);
			
			// 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' ) );
						pb_backupbuddy::status( 'details', __('Using Best Available zip method based on settings.','it-l10n-backupbuddy' ) );
						break;
					case "2":
						// All Available
						$zip_methods = $this->_zip_methods;
						pb_backupbuddy::status( '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();				
						pb_backupbuddy::status( '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();				
						pb_backupbuddy::status( '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();				
				pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 ) ) {
					
						pb_backupbuddy::status( 'details', __('Temporary directory could not be deleted: ','it-l10n-backupbuddy' ) . $temporary_zip_directory );
					
					}
						
				}

				return false;
				
			}
			
			pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 = 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() );
					
					pb_backupbuddy::status( '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 ( $zipper->create( $zip_file, $add_directory, $sanitized_excludes, $temporary_zip_directory ) === true ) {
					
						// Got a valid zip file so we can just return - method will have cleaned up the temporary directory
						pb_backupbuddy::status( '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;
	
					} else {
					
						// Method will have cleaned up the temporary directory				
						pb_backupbuddy::status( '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
					pb_backupbuddy::status( '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
			pb_backupbuddy::status( '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' );				
				pb_backupbuddy::status( 'message', __('Forced compatibility unzip method (ZipArchive; medium speed) based on settings.','it-l10n-backupbuddy' ) );
				
			} elseif ( $force_compatibility_mode == 'pclzip' ) {
			
				$zip_methods = array( 'pclzip' );				
				pb_backupbuddy::status( 'message', __('Forced compatibility unzip method (PCLZip; slow speed) based on settings.','it-l10n-backupbuddy' ) );			
			
			} else {
			
				$zip_methods = $this->_zip_methods;
				pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 );
					
					// 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
			pb_backupbuddy::status( '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 ) {

			$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
				pb_backupbuddy::status( '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 ) ) ) {
			
				pb_backupbuddy::status( '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 );
					
					// 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
			pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 );
					
					// 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
			pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 );
					
					// 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
			pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 );
					
					// 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
			pb_backupbuddy::status( '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
				pb_backupbuddy::status( '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 );
					
					// 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
			pb_backupbuddy::status( '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;
			
		}
				
		// FOR FUTURE USE; NOT YET IMPLEMENTED. Use to check .sql file is non-empty.
		function file_stats( $zip_file, $locate_file, $leave_open = false ) {
			if ( in_array( 'ziparchive', $this->_zip_methods ) ) {
				$this->_zip = new ZipArchive;
				if ( $this->_zip->open( $zip_file ) === true ) {
					if ( ( $stats = $this->_zip->statName( $locate_file ) ) === false ) { // File not found in zip.
						$this->_zip->close();
						pb_backupbuddy::status( 'details', __('File not found (ziparchive) for stats','it-l10n-backupbuddy' ) . ': ' . $locate_file );
						return false;
					}
					$this->_zip->close();
					return $stats;
				} else {
					pb_backupbuddy::status( 'details', sprintf( __('ZipArchive failed to open file to check stats (looking in %1$s).','it-l10n-backupbuddy' ), $zip_file ) );
					
					return false;
				}
			}
			
			// If we made it this far then ziparchive not available/failed.
			if ( in_array( 'pclzip', $this->_zip_methods ) ) {
				require_once( ABSPATH . 'wp-admin/includes/class-pclzip.php' );
				$this->_zip = new PclZip( $zip_file );
				if ( ( $file_list = $this->_zip->listContent() ) == 0 ) { // If zero, zip is corrupt or empty.
					pb_backupbuddy::status( 'details', $this->_zip->errorInfo( true ) );
				} else {
					foreach( $file_list as $file ) {
						if ( $file['filename'] == $locate_file ) { // Found file.
							return true;
						}
					}
					pb_backupbuddy::status( 'details', __('File not found (pclzip)','it-l10n-backupbuddy' ) . ': ' . $locate_file );
					return false;
				}
			} else {
				pb_backupbuddy::status( 'details', __('Unable to check if file exists: No compatible zip method found.','it-l10n-backupbuddy' ) );
				return false;
			}
		}
			
	} // End class
	
	//$pluginbuddy_zipbuddy = new pluginbuddy_zipbuddy( $this->_options['backup_directory'] );

}
