<?php
/**
 *	pluginbuddy_zbzipziparchive Class
 *
 *  Extends the zip capability core class with proc specific capability
 *	
 *	Version: 1.0.0
 *	Author:
 *	Author URI:
 *
 *	@param		$parent		object		Optional parent object which can provide functions for reporting, etc.
 *	@return		null
 *
 */
if ( !class_exists( "pluginbuddy_zbzipziparchive" ) ) {

	/**
	 *	pluginbuddy_ZipArchive Class
	 *
	 *	Wrapper for ZipArchive to handle error situations and provide additional functions
	 *
	 *	@param	none
	 *	@return	null
	 *
	 */
	class pluginbuddy_ZipArchive {

        /**
         * The created ZipArchive object if it can be created
         * 
         * @var $_za 	object
         */
		private $_za = NULL;
		
		/**
		 *	__construct()
		 *	
		 *	Default constructor.
		 *	TODO: Consider having a "suppress warnings" parameter to determine whether methods
		 *	should be invoked with warnings suppressed or not. For is_available() usage we would
		 *	want to so as not to potentially flood the PHP error log. For other functions that
		 *	are not called frequently we might not want to suppress the warnings.
		 *	
		 *	@param		none
		 *	@return		null
		 *
		 */
		public function __construct() {
			
			if ( class_exists( 'ZipArchive', false ) ) {
			
				// It's available so create the private instance
				$this->_za = new ZipArchive();
				
			} else {
			
				// Not available so throw the exception for the caller to handle
				throw new Exception( 'ZipArchive class does not exist.' );
			
			}
			
			return;
		
		}
		
		/**
		 *	__destruct()
		 *	
		 *	Default destructor.
		 *	
		 *	@return		null
		 *
		 */
		public function __destruct() {
		
			if ( NULL != $this->_za ) { unset ( $this->_za ); }
			
			return;
		
		}
		
		/**
		 *	__call()
		 *	
		 *	Magic method intercepting calls to unknown methods. This allows us to intercept
		 *	all method calls and add additional processing
		 *	
		 *	@param		string	$method		The name of the intercepted method
		 *	@param		array	$arguments	Array of the arguments associated with the method call
		 *	@return		mixed	$result		Whatever the invoked wrapper method call returns
		 *
		 */
		public function __call( $method, $arguments ) {
		
			$result = false;
		
			$result = @call_user_func_array( array( $this->_za, $method ), $arguments );
			
			return $result;
		
		}
		
		/**
		 *	__get()
		 *	
		 *	Magic method intercepting calls to unknown properties. This means we have to
		 *	provide the properties of the wrapped object but that's ok as there aren't many.
		 *	Note: Maybe we can get the object properties and automate this?
		 *	
		 *	@param		string	$name		The name of the property
		 *	@return		mixed	$result		Whatever the wrapped object property returns
		 *
		 */
		public function __get( $name ) {
		
			switch ( $name ) {
			
				case "comment":
				
					$result = $this->_za->comment;
					break;
				
				case "numFiles":
				
					$result = $this->_za->numFiles;
					break;
				
				case "filename":
				
					$result = $this->_za->filename;
					break;
				
				case "status":
				
					$result = $this->_za->status;
					break;
					
				case "statusSys":
				
					$result = $this->_za->statusSys;
					break;
					
				default:
				
					// Hmm, not quite sure what we should return here...
					$result = false;
	
			}
			
			return $result;
		
		}
	
		/**
		 *	errorInfo()
		 *	
		 *	Translate a ZipArchive error code into an informative string description
		 *	and returns the string. An error number can be passed in, otherwise will
		 *	get the status property and use that (if not indicating no error) or the
		 *	statusSys property and use that (if not indicating no error). In the case
		 *	of the statusSys property will get the error string from the getStatusString()
		 *	method, otherwise for status property or passed in value use the mappings
		 *	below.
		 *	Note: This does mean that a statusSys error no should not be passed in
		 *	presently. In future may choose to use an offset mapping to handle that.
		 *	
		 *	@param		integer	$error	Optional: The error code
		 *	@param		bool	$full	Optional: True to provide a name/value/description string
		 *	@return		string			Informative error string
		 *
		 */
		public function errorInfo( $error = PHP_INT_MAX, $full = true ) {
		
			// Get statusSys property value in case we need it
			$error_sys = $this->_za->statusSys;
			
			// Check whether we have been given or need to get a status value
			if ( PHP_INT_MAX == $error) {
			
				// No error number passed in, lets get one
				$error = $this->_za->status;
			
			}
			
			if ( ( ZIPARCHIVE::ER_OK == $error ) && ( 0 < $error_sys ) ) {
			
				// No basic error AND we have a system error
				$error_string = "ZLIB" . "(" . $error_sys . ") : " . $this->_za->getStatusString();
			
			} else {
			
				$error_name = '';
				$error_description = '';
			
				// It's either a basic error OR no system error
				// We can check the symbolic values
				switch( (int) $error ) {
				 case ZIPARCHIVE::ER_OK:
				 	$error_name = "ZIPARCHIVE::ERR_OK";
				 	$error_description = "No error";
				 	break;
				 case ZIPARCHIVE::ER_OPEN:
				 	$error_name = "ZIPARCHIVE::ER_OPEN";
				 	$error_description = "Can't open file";
				 	break;
				 case ZIPARCHIVE::ER_MEMORY:
				 	$error_name = "ZIPARCHIVE::ER_MEMORY";
				 	$error_description = "Memory allocation failure";
				 	break;
				 case ZIPARCHIVE::ER_EXISTS:
				 	$error_name = "ZIPARCHIVE::ERR_EXISTS";
				 	$error_description = "File already exists";
				 	break;
				 case ZIPARCHIVE::ER_INCONS:
				 	$error_name = "ZIPARCHIVE::ER_INCONS";
				 	$error_description = "Zip archive inconsistent";
				 	break;
				 case ZIPARCHIVE::ER_INVAL:
				 	$error_name = "ZIPARCHIVE::ER_INVAL";
				 	$error_description = "Invalid argument";
				 	break;
				 case ZIPARCHIVE::ER_NOENT:
				 	$error_name = "ZIPARCHIVE::ER_NOENT";
				 	$error_description = "No such file";
				 	break;
				 case ZIPARCHIVE::ER_NOZIP:
				 	$error_name = "ZIPARCHIVE::ER_NOZIP";
				 	$error_description = "Not a zip archive";
				 	break;
				 case ZIPARCHIVE::ER_READ:
				 	$error_name = "ZIPARCHIVE::ER_READ";
				 	$error_description = "Read error";
				 	break;
				 case ZIPARCHIVE::ER_SEEK:
				 	$error_name = "ZIPARCHIVE::ER_SEEK";
				 	$error_description = "Seek error";
				 	break;
				 case ZIPARCHIVE::ER_MULTIDISK:
				 	$error_name = "ZIPARCHIVE::ER_MULTIDISK";
				 	$error_description = "Multi-disk zip archives not supported";
				 	break;
				 case ZIPARCHIVE::ER_RENAME:
				 	$error_name = "ZIPARCHIVE::ER_RENAME";
				 	$error_description = "Renaming temporary file failed";
				 	break;
				 case ZIPARCHIVE::ER_CLOSE:
				 	$error_name = "ZIPARCHIVE::ER_CLOSE";
				 	$error_description = "Closing zip archive failed";
				 	break;
				 case ZIPARCHIVE::ER_WRITE:
				 	$error_name = "ZIPARCHIVE::ER_WRITE";
				 	$error_description = "Write error";
				 	break;
				 case ZIPARCHIVE::ER_CRC:
				 	$error_name = "ZIPARCHIVE::ER_CRC";
				 	$error_description = "CRC error";
				 	break;
				 case ZIPARCHIVE::ER_ZIPCLOSED:
				 	$error_name = "ZIPARCHIVE::ER_ZIPCLOSED";
				 	$error_description = "Containing zip archive was closed";
				 	break;
				 case ZIPARCHIVE::ER_TMPOPEN:
				 	$error_name = "ZIPARCHIVE::ER_TMPOPEN";
				 	$error_description = "Failure to create temporary file";
				 	break;
				 case ZIPARCHIVE::ER_ZLIB:
				 	$error_name = "ZIPARCHIVE::ER_ZLIB";
				 	$error_description = "Zlib error";
				 	break;
				 case ZIPARCHIVE::ER_CHANGED:
				 	$error_name = "ZIPARCHIVE::ER_CHANGED";
				 	$error_description = "Entry has been changed";
				 	break;
				 case ZIPARCHIVE::ER_COMPNOTSUPP:
				 	$error_name = "ZIPARCHIVE::ER_COMPNOTSUPP";
				 	$error_description = "Compression method not supported";
				 	break;
				 case ZIPARCHIVE::ER_EOF:
				 	$error_name = "ZIPARCHIVE::ER_EOF";
				 	$error_description = "Premature EOF";
				 	break;
				 case ZIPARCHIVE::ER_INTERNAL:
				 	$error_name = "ZIPARCHIVE::ER_INTERNAL";
				 	$error_description = "Internal error";
				 	break;
				 case ZIPARCHIVE::ER_REMOVE:
				 	$error_name = "ZIPARCHIVE::ER_REMOVE";
				 	$error_description = "Can't remove file";
				 	break;
				 case ZIPARCHIVE::ER_DELETED:
				 	$error_name = "ZIPARCHIVE::ER_DELETED";
				 	$error_description = "Entry has been deleted";
				 	break;
				 default:
				 	$error_name = "ZIPARCHIVE::ERR_UNKNOWN";
				 	$error_description = "Unkmown error";
				}
				
				$error_string = $error_name . "(" . $error . ") : " . $error_description;
				
			}
			
			// One way or another we have a string to return
			return $error_string;
		
		}
		
	}

	class pluginbuddy_zbzipziparchive extends pluginbuddy_zbzipcore {
	
        /**
         * method tag used to refer to the method and entities associated with it such as class name
         * 
         * @var $_method_tag 	string
         */
		public static $_method_tag = 'ziparchive';
			
        /**
         * This tells us whether this method is regarded as a "compatibility" method
         * 
         * @var bool
         */
		public static $_is_compatibility_method = false;
			
        /**
         * This tells us the dependencies of this method so they can be check to see if the method can be supported
         * 
         * @var array
         */
		public static $_method_dependencies = array( 'classes' => array( 'ZipArchive' ),
											  		 'functions' => array(),
											  		 'extensions' => array(),
											  		 'files' => array()
													);
			
		/**
		 * 
		 * get_method_tag_static()
		 *
		 * Get the static method tag in a static context
		 *
		 * @return		string	The method tag
		 *
		 */
		public static function get_method_tag_static() {
		
			return self::$_method_tag;
			
		}

		/**
		 * 
		 * get_is_compatibility_method_static()
		 *
		 * Get the compatibility method indicator in a static context
		 *
		 * @return		bool	True if is a compatibility method
		 *
		 */
		public static function get_is_compatibility_method_static() {
		
			return self::$_is_compatibility_method;
		}

		/**
		 * 
		 * get_method_dependencies_static()
		 *
		 * Get the method dependencies array in a static context
		 *
		 * @return		array	The dependencies of the method that is requires to be a supported method
		 *
		 */
		public static function get_method_dependencies_static() {
		
			return self::$_method_dependencies;
		}

		/**
		 *	__construct()
		 *	
		 *	Default constructor.
		 *	
		 *	@param		reference	&$parent		[optional] Reference to the object containing the status() function for status updates.
		 *	@return		null
		 *
		 */
		public function __construct( &$parent = NULL ) {

			parent::__construct( $parent );
			
			// Override some of parent defaults
			$this->_method_details[ 'attr' ] = array_merge( $this->_method_details[ 'attr' ],
															array( 'name' => 'ZipArchive Method',
													  			   'compatibility' => pluginbuddy_zbzipziparchive::$_is_compatibility_method )
													  	   );

			// No relevant parameters for this method
			$this->_method_details[ 'param' ] = array();
			
		}
		
		/**
		 *	__destruct()
		 *	
		 *	Default destructor.
		 *	
		 *	@return		null
		 *
		 */
		public function __destruct( ) {
		
			parent::__destruct();

		}
		
		/**
		 *	get_method_tag()
		 *	
		 *	Returns the (static) method tag
		 *	
		 *	@return		string The method tag
		 *
		 */
		public function get_method_tag() {
		
			return pluginbuddy_zbzipziparchive::$_method_tag;
			
		}
		
		/**
		 *	get_is_compatibility_method()
		 *	
		 *	Returns the (static) is_compatibility_method boolean
		 *	
		 *	@return		bool
		 *
		 */
		public function get_is_compatibility_method() {
		
			return pluginbuddy_zbzipziparchive::$_is_compatibility_method;
			
		}
		
		/**
		 *	is_available()
		 *	
		 *	A function that tests for the availability of the specific method and its available modes. Will test for
		 *  multiple modes (zip & unzip) and only return false if neither is available. Actual available modes will
		 *  be indicated in the method attributes.
		 *
		 *  Note: in this case as the zip and unzip capabilities are all wrapped up in the same class then if we
		 *  can zip then we'll assume (for now) that we can unzip as well so attributes are set accordingly.
		 *	
		 *	@param		string	$tempdir	Temporary directory to use for any test files (must be writeable)
		 *	@return		bool				True if the method is available for at least one mode, false otherwise
		 *
		 */
		public function is_available( $tempdir ) {
		
			$result = false;
			$za = NULL;
			
			$test_file = $tempdir . 'temp_test_' . uniqid() . '.zip';
			
			// This should give us a new archive object, of not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ZipArchive test FAILED: %1$s','it-l10n-backupbuddy' ), $error_string ) );
				$result = false;

			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				// This returns true on success, false for some error scenarios or an error code
				// If it return false we don't really know why - there may have been a Warning generated
				// but we need to suppress warnings because of the frequency this function is called
				// so we can only indicate a general failure
				$res = $za->open( $test_file, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE );
				
				if ( true === $res ) {
				
					if ( !$za->addFile( __FILE__, 'this_is_a_test.txt') ) {
					
						$this->log( 'details',  __('ZipArchive test FAILED: Unable to add file to zip file.','it-l10n-backupbuddy' ) );
						$error_string = $za->errorInfo();
						$this->log( 'details',  __('ZipArchive Error: ','it-l10n-backupbuddy' ) . $error_string );
						
					}
					
					if ( !$za->close() ) {
					
						$this->log( 'details',  __('ZipArchive test FAILED: Problem creating/closing zip file.','it-l10n-backupbuddy' ) );
						$error_string = $za->errorInfo();
						$this->log( 'details',  __('ZipArchive Error: ','it-l10n-backupbuddy' ) . $error_string );
						
					}
					
					if ( @file_exists( $test_file ) ) {
					
						if ( !@unlink( $test_file ) ) {
					
							$this->log( 'details', 'Error #564634. Unable to delete test file `' . $test_file . '`.' );
						
						}
					
						// The zip operation was successful - implies can zip and unzip and hence check and list
						// Note: we actually don't want to do archiving with this method yet
						$this->_method_details[ 'attr' ][ 'is_zipper' ] = true;
						$this->_method_details[ 'attr' ][ 'is_unzipper' ] = true;
						$this->_method_details[ 'attr' ][ 'is_checker' ] = true;
						$this->_method_details[ 'attr' ][ 'is_lister' ] = true;
						$this->_method_details[ 'attr' ][ 'is_commenter' ] = true;
						$this->_method_details[ 'attr' ][ 'is_unarchiver' ] = true;
						$this->_method_details[ 'attr' ][ 'is_extractor' ] = true;
						
						$this->log( 'details', __('ZipArchive test PASSED.','it-l10n-backupbuddy' ) );
						$result = true;
						
					} else {
					
						$this->log( 'details', __('ZipArchive test FAILED: Zip file not found.','it-l10n-backupbuddy' ) );
						$result = false;
						
					}
					
				} else {
				
					$this->log( 'details',  __('ZipArchive test FAILED: Unable to create/open zip file.','it-l10n-backupbuddy' ) );
					
					// If we got an error code (rather than simply a false failure indication) then translate it
					// It seems that in these cases the internal status doesn't indicate anything so we cannot use that
					if ( false !== $res ) {
					
						$error_string = $za->errorInfo( $res );
						$this->log( 'details',  __('ZipArchive Error: ','it-l10n-backupbuddy' ) . $error_string );
					
					}
					
					$za->close();
					
					$result = false;
					
				}
				
			}
		  	
		  	if ( NULL != $za ) { unset( $za ); }
		  	
		  	return $result;
		  	
		}
		
		/**
		 *	add_virtual_file()
		 *	
		 *	A function that allows a virtual file to be added to a zip archive
		 *	
		 *	The virtual file descriptor is an array of descriptor array(s) that provide a filename
		 *	for the virtual file to be added (can include path prefix) and a string that contains the
		 *	file content (can be empty but must be present). For example:
		 *	array( array( 'filename' => 'somefile.txt', 'content' => 'A Text String' ) )
		 *	Note that the actual zip file to which the file is added is formed from the filenaem from
		 *	the $zip parameter appaended to the $tempdir directory path. This is slightly clunky and
		 *	may be changed at some later date.
		 *	
		 *	@param		string	$zip						Full path & filename of ZIP Archive file to add file to
		 *	@param		array	$virtual_file_descriptor	Full descriptor of file(s) to add to ZIP Archive file
		 *	@param		string	$tempdir					Full path of directory for temporary usage
		 *	@return		bool								True if the addition was successful, false otherwise
		 *
		 */
		public function add_virtual_file( $zip, $virtual_file_descriptor, $tempdir ) {
				
			$this->log( 'details', __('The ','it-l10n-backupbuddy' ) . $this->get_method_tag() . __(' method is not currently supported for backup.','it-l10n-backupbuddy' ) );
			return false;
		
		}
		
		/**
		 *	create()
		 *	
		 *	A function that creates an archive file
		 *	
		 *	The $excludes will be a list or relative path excludes if the $listmaker object is NULL otehrwise
		 *	will be absolute path excludes and relative path excludes can be had from the $listmaker object
		 *	
		 *	@param		string	$zip			Full path & filename of ZIP Archive file to create
		 *	@param		string	$dir			Full path of directory to add to ZIP Archive file
		 *	@parame		array	$excludes		List of either absolute path exclusions or relative exclusions
		 *	@param		string	$tempdir		Full path of directory for temporary usage
		 *	@param		object	$listmaker		The object from which we can get an inclusions list
		 *	@return		bool					True if the creation was successful, false otherwise
		 *
		 */
		public function create( $zip, $dir, $excludes, $tempdir, $listmaker = NULL ) {
		
			$this->log( 'details', __('The ','it-l10n-backupbuddy' ) . $this->get_method_tag() . __(' method is not currently supported for backup.','it-l10n-backupbuddy' ) );
			return false;
		
		}
		
		/**
		 *	grow()
		 *	
		 *	A function that grows an archive file from already calculated contet list
		 *	Always cleans up after itself
		 *	
		 *	
		 *	@param		string	$zip			Full path & filename of ZIP Archive file to grow
		 *	@param		string	$tempdir		Full path of directory for temporary usage
		 *	@param		array	$state
		 *	@return		bool					True if the creation was successful, array for continuation, false otherwise
		 *
		 */
		public function grow( $zip, $tempdir, $state ) {
		
			$this->log( 'details', __('The ','it-l10n-backupbuddy' ) . $this->get_method_tag() . __(' method is not currently supported for backup.','it-l10n-backupbuddy' ) );
			return false;
		
		}
		
		/**
		 *	extract()
		 *
		 *	Extracts the contents of a zip file to the specified directory using the best unzip methods possible.
		 *	If no specific items given to extract then it's a complete unzip
		 *
		 *	@param	string		$zip_file					Full path & filename of ZIP file to extract from.
		 *	@param	string		$destination_directory		Full directory path to extract into.
		 *	@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() ) {
		
			$result = false;
		
			switch ( $this->get_os_type() ) {
				case self::OS_TYPE_NIX:
					if ( empty( $items ) ) {
						$result = $this->extract_generic_full( $zip_file, $destination_directory );
					} else {
						$result = $this->extract_generic_selected( $zip_file, $destination_directory, $items );					
					}
					break;
				case self::OS_TYPE_WIN:
					if ( empty( $items ) ) {
						$result = $this->extract_generic_full( $zip_file, $destination_directory );
					} else {
						$result = $this->extract_generic_selected( $zip_file, $destination_directory, $items );					
					}
					break;
				default:
					$result = false;
			}
			
			return $result;
			
		}

		/**
		 *	extract_generic_full()
		 *
		 *	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.
		 *	@return	bool									true on success, false otherwise
		 */
		protected function extract_generic_full( $zip_file, $destination_directory = '' ) {
		
			$result = false;
			$za = NULL;
			$stat = array();
			
			// This should give us a new archive object, if not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ziparchive indicated as available method but error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );
				$result = false;
				
			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				$result = $za->open( $zip_file );
				
				// Make sure we opened the zip ok
				if ( true === $result ) {
				
					// How many files - could be 0 if we had an empty zip file
					$file_count = $za->numFiles;
					
					// Only returns true for success or false for failure - no indication of why failed
					$result = $za->extractTo( $destination_directory );
									
					// Currently we can only distinguish between success and failure but no finer grain
					if ( true === $result ) {
					
						$this->log( 'details', sprintf( __('ziparchive extracted file contents (%1$s to %2$s)','it-l10n-backupbuddy' ), $zip_file, $destination_directory ) );

					} else {
					
						$error_string = $za->errorInfo();
						$this->log( 'details', sprintf( __('ziparchive failed to extract file contents (%1$s to %2$s) - Error Info: %3$s.','it-l10n-backupbuddy' ), $zip_file, $destination_directory, $error_string ) );
					
						// May seem redundant but belt'n'braces
						$result = false;
					}
					
					$this->log_archive_file_stats( $zip_file );
					
				} else {
				
					// Couldn't open archive - will return for maybe another method to try
					$error_string = $za->errorInfo( $result );
					$this->log( 'details', sprintf( __('ZipArchive failed to open file to extract contents (%1$s to %2$s) - Error Info: %3$s.','it-l10n-backupbuddy' ), $zip_file, $destination_directory, $error_string ) );

					// Return an error code and a description - this needs to be handled more generically
					//$result = array( 1, "Unable to get archive contents" );
					// Currently as we are returning an array as a valid result we just return false on failure
					$result = false;

				}
				
				$za->close();
			
			}
			
		  	if ( NULL != $za ) { unset( $za ); }		
			
			return $result;
						
		}

		/**
		 *	extract_generic_selected()
		 *
		 *	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	array		$items						Mapping of what to extract and to what
		 *	@return	bool									true on success (all extractions successful), false otherwise
		 */
		protected function extract_generic_selected( $zip_file, $destination_directory = '', $items ) {
		
			$result = false;
			$za = NULL;
			
			// This should give us a new archive object, if not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ziparchive indicated as available method but error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );
				$result = false;
				
			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				$result = $za->open( $zip_file );
				
				// Make sure we opened the zip ok
				if ( true === $result ) {
				
					// Now we need to take each item and run an unzip for it - unfortunately there is no easy way of combining
					// arbitrary extractions into a single command if some might be to a 
					foreach ( $items as $what => $where ) {
			
						$rename_required = false;
						$result = false;
				
						// Decide how to extract based on where
						if ( empty( $where) ) {
					
							// First we'll extract and then junk the path
							$result = $za->extractTo( $destination_directory, $what );
								
							// Unlike exec zip we have to effectively junk the path after the extraction
							// Do this by renaming the file to the destination directory and then getting rid of any directory
							// structure it was under. If dirname is not . then we know there is a directry path and not
							// just a simple file name (remember that $what should _not_ have any leading slash whether
							// it is a filepath or a simple filename)
							if ( "." != dirname( $what ) ) {
							
								rename( $destination_directory . DIRECTORY_SEPARATOR . $what,
										$destination_directory . DIRECTORY_SEPARATOR . basename( $what) );
										
								// Get the path component of $what - note that dirname() adds a leading slash
								// even if none was present originally. We must get the first directory component only
								// so we can do a recursive delete on it. This is a bit klunky but functional.
								$whatpath = $what;
								do {
									$whatpath = dirname( $whatpath );
								} while ( 1 < strlen( dirname( $whatpath ) ) );

								// Now we can do the recursive delete from that top level component
								$this->delete_directory_recursive( $destination_directory . $whatpath );

							}
															
					
						} elseif ( !empty( $where ) ) {
					
							if ( $what === $where ) {
							
								// Check for wildcard directory extraction like dir/* => dir/*
								if ( "*" == substr( trim( $what ), -1 ) ) {
								
									// Get our path match string (just clip off the wildcard)
									$whatroot = substr( trim( $what ), 0, -1 );
									$file_count = $za->numFiles;
								
									// Crikey, it's a directory tree extraction - don't panic
									// We need to go through the whole zip and extract each file that matches
									for ( $i = 0; $i < $file_count; $i++ ) {
									
										// Get the filename by index and see if it's in the tree
										$filename = $za->getNameIndex( $i );
										if ( 0 === strpos( $filename, $whatroot ) ) {
										
											// $what matched the root of this filename so extract it
											$result = $za->extractTo( $destination_directory, $filename );

											if ( false === $result ) {
												
												// An extraction failed so bail out here - this should just
												// drop us through to the post-processing of $result which on
												// a false should then drop us out of the foreach loop
												break;
												
											}
										
										}
									
									}
								
								} else {
								
									// It's just a single file extraction - breath a sign of relief
									// Extract to same directory structure - don't junk path, no need to add where to destnation as automatic
									$result = $za->extractTo( $destination_directory, $what );

								}
						
							} else {
						
								// First we'll extract and then junk the path
								$result = $za->extractTo( $destination_directory, $what );
								
								// Unlike exec zip we have to effectively junk the path after the extraction
								// Do this by renaming the file to the destination directory and then getting rid of any directory
								// structure it was under. If dirname is not . then we know there is a directry path and not
								// just a simple file name (remember that $what should _not_ have any leading slash whether
								// it is a filepath or a simple filename)
								if ( "." != dirname( $what ) ) {
								
									rename( $destination_directory . DIRECTORY_SEPARATOR . $what,
											$destination_directory . DIRECTORY_SEPARATOR . basename( $what) );
											
									// Get the path component of $what - note that dirname() adds a leading slash
									// even if none was present originally. We must get the first directory component only
									// so we can do a recursive delete on it. This is a bit klunky but functional.
									$whatpath = $what;
									do {
										$whatpath = dirname( $whatpath );
									} while ( 1 < strlen( dirname( $whatpath ) ) );

									// Now we can do the recursive delete from that top level component
									$this->delete_directory_recursive( $destination_directory . $whatpath );

								}
															
								// Will need to rename if the extract is ok
								$rename_required = true;
						
							}
					
						}
				
						// Note: we don't open the file and then do stuff but it's all done in one action
						// so we need to interpret the return code to dedide what to do
						// Currently we can only distinguish between success and failure but no finer grain
						if ( true === $result ) {
					
							$this->log( 'details', sprintf( __('ziparchive extracted file contents (%1$s from %2$s to %3$s%4$s)','it-l10n-backupbuddy' ), $what, $zip_file, $destination_directory, $where ) );

							// Rename if we have to
							if ( true === $rename_required) {
							
								// Note: we junked the path on the extraction so just the filename of $what is the source but
								// $where could be a simple file name or a file path 
								$result = $result && rename( $destination_directory . DIRECTORY_SEPARATOR . basename( $what ),
															 $destination_directory . DIRECTORY_SEPARATOR . $where );
							
							}

						} else {
					
							// For now let's just print the error code and drop through
							$error_string = $za->errorInfo();
							$this->log( 'details', sprintf( __('ziparchive failed to open/process file to extract file contents (%1$s from %2$s to %3$s%4$s) - Error Info: %5$s.','it-l10n-backupbuddy' ), $what, $zip_file, $destination_directory, $where, $error_string ) );
					
							// May seem redundant but belt'n'braces
							$result = false;
							
						}
					
						// If the extraction failed (or rename after extraction) then break out of the foreach and simply return false
						if ( false === $result ) {
					
							break;
						
						}
					
					}
				
				} else {
				
					// Couldn't open archive - will return for maybe another method to try
					$error_string = $za->errorInfo( $result );
					$this->log( 'details', sprintf( __('ZipArchive failed to open file to extract contents (%1$s to %2$s) - Error Info: %3$s.','it-l10n-backupbuddy' ), $zip_file, $destination_directory, $error_string ) );

					// Return an error code and a description - this needs to be handled more generically
					//$result = array( 1, "Unable to get archive contents" );
					// Currently as we are returning an array as a valid result we just return false on failure
					$result = false;

				}
				
				$za->close();
			
			}
			
		  	if ( NULL != $za ) { unset( $za ); }		
			
			return $result;
						
		}
		
		/**
		 *	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/array				True if the file is found in the zip and false if not, array for other problem
		 *
		 */
		public function file_exists( $zip_file, $locate_file, $leave_open = false ) {
		
			$result = array( 1, "Generic failure indication" );
			$za = NULL;
			
			// This should give us a new archive object, of not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ziparchive indicated as available method but error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );

				// Return an error code and a description - this needs to be handled more generically
				$result = array( 1, "Class not available to match method" );
				
			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				$result = $za->open( $zip_file );
				
				// Make sure we opened the zip ok
				if ( true === $result ) {
				
					// Now try and find the index of the file
					$index = $za->locateName( $locate_file );
					
					// If we got an index we found it otherwise not found
					if ( false !== $index ) {
					
						$this->log( 'details', __('File found (ziparchive)','it-l10n-backupbuddy' ) . ': ' . $locate_file );
						$result = true;
						
					} else {
					
						$this->log( 'details', __('File not found (ziparchive)','it-l10n-backupbuddy' ) . ': ' . $locate_file );
						$result = false;
						
					}
					
				} else {
				
					// Couldn't open archive - will return for maybe another method to try
					$error_string = $za->errorInfo( $result );
					$this->log( 'details', sprintf( __('ZipArchive failed to open file to check if file exists (looking for %1$s in %2$s) - Error Info: %3$s.','it-l10n-backupbuddy' ), $locate_file , $zip_file, $error_string ) );

					// Return an error code and a description - this needs to be handled more generically
					$result = array( 1, "Failed to open/process file" );

				}
			
				// We have finished with the archive (leave_open ignored for now)
				$za->close();
				
			}
			
		  	if ( NULL != $za ) { unset( $za ); }		
			
			return $result;
				
		}
		
		/*	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 ) {
		
			$file_list = array();
			$result = false;
			$za = NULL;
			$stat = array();
			
			// This should give us a new archive object, of not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ziparchive indicated as available method but error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );
				$result = false;
				
			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				$result = $za->open( $zip_file );
				
				// Make sure we opened the zip ok
				if ( true === $result ) {
				
					if ( ( $file_count = $za->numFiles ) > 0 ) {
				
						// Get each file in sequence by index and get the properties
						for( $i = 0; $i < $file_count; $i++ ){
						
							$stat = $za->statIndex( $i );
							
							// Assume all these keys do exist (consider testing)
							$file_list[] = array(
								$stat['name'],
								$stat['size'],
								$stat['comp_size'],
								$stat['mtime'],
							);
							
						}
						
					}
					
					
					$this->log( 'details', sprintf( __('ziparchive listed file contents (%1$s)','it-l10n-backupbuddy' ), $zip_file ) );

					$this->log_archive_file_stats( $zip_file );	

					$result = &$file_list;
					
				} else {
				
					// Couldn't open archive - will return for maybe another method to try
					$error_string = $za->errorInfo( $result );
					$this->log( 'details', sprintf( __('ZipArchive failed to open file to list contents (%1$s) - Error Info: %2$s.','it-l10n-backupbuddy' ), $zip_file, $error_string ) );

					// Return an error code and a description - this needs to be handled more generically
					//$result = array( 1, "Unable to get archive contents" );
					// Currently as we are returning an array as a valid result we just return false on failure
					$result = false;

				}
				
				$za->close();
			
			}
			
		  	if ( NULL != $za ) { unset( $za ); }		
			
			return $result;
			
		}
		
		/*	set_comment()
		 *	
		 *	Retrieve archive comment.
		 *	
		 *	@param		string			$zip_file		Filename of archive to set comment on.
		 *	@param		string			$comment		Comment to apply to archive.
		 *	@return		bool							true on success, otherwise false.
		 */
		public function set_comment( $zip_file, $comment ) {
		
			$result = false;
			$za = NULL;
			
			// This should give us a new archive object, of not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ziparchive indicated as available method but error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );
				$result = false;
				
			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				$result = $za->open( $zip_file );
				
				// Make sure at least the zip file opened ok
				if ( true === $result ) {
				
					// Set the comment - true on success, false on failure
					$result = $za->setArchiveComment( $comment );
					
					// If we got back true then all is well with the world
					if ( true === $result ) {
					
						$this->log( 'details', sprintf( __('ZipArchive set comment in file %1$s','it-l10n-backupbuddy' ), $zip_file ) );
						$result = true;
						
					} else {
					
						// If we failed to set the commnent then log it (?) and drop through
						$this->log( 'details', sprintf( __('ZipArchive failed to set comment in file %1$s','it-l10n-backupbuddy' ), $zip_file ) );
						$result = false;
						
					}
						
				} else {
				
					// If we couldn't open the zip file then log it (?) and drop through
					$error_string = $za->errorInfo( $result );
					$this->log( 'details', sprintf( __('ZipArchive failed to open file to set comment in file %1$s - Error Info: %2$s','it-l10n-backupbuddy' ), $zip_file, $error_string ) );
					$result = false;
										
				}
								
				$za->close();
				
			}
			
		  	if ( NULL != $za ) { unset( $za ); }		
			
			return $result;
			
		}

		/*	get_comment()
		 *	
		 *	Retrieve archive comment.
		 *	
		 *	@param		string		$zip_file		Filename of archive to retrieve comment from.
		 *	@return		bool|string					false on failure, Zip comment otherwise.
		 */
		public function get_comment( $zip_file ) {
		
			$result = false;
			$za = NULL;
			
			// This should give us a new archive object, of not catch it and bail out
			try {
			
				$za = new pluginbuddy_ZipArchive();
				$result = true;
				
			} catch ( Exception $e ) {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$error_string = $e->getMessage();
				$this->log( 'details', sprintf( __('ziparchive indicated as available method but error reported: %1$s','it-l10n-backupbuddy' ), $error_string ) );
				$result = false;
				
			}
			
			// Only continue if we have a valid archive object
			if ( true === $result ) {
				
				$result = $za->open( $zip_file );
				
				// Make sure at least the zip file opened ok
				if ( true === $result ) {
				
					// Get the comment or false on failure for some reason
					// Note: Currently, due to a bug in ZipArchive, getArchiveComment()
					// returns false for an empty comment whereas it should just return an
					// empty string. We'll live with this for now as it should only happen
					// when an archive is fresh and has the Integrity Check run (or when the
					// check is rerun). Once a comment is added the function behaves.
					// If any problems are thrown up then there is the option to use the
					// archive property but that has a downside in that it can only ever
					// return a string so if there really is an error in reading the comment
					// it is not possible to know (AFAIK). Perhaps an error status value might
					// be set somewhere?
					// The bug will be reported to PHP developers but we will still have to
					// live with this for a while because it takes hosts ages to catch up to
					// updated PHP versions.
					$comment = $za->getArchiveComment( ZIPARCHIVE::FL_UNCHANGED );
					//$comment = $za->comment;
					
					// If we have a comment (even if empty) then return it
					if ( false !== $comment ) {

						// Because comment may have been added by zip utility it may have been split over
						// multiple lines so we need to "unsplit" it - need to check for different possible
						// line endings
						$lines = preg_split( "/\r\n|\n|\r/", $comment );

						// Now convert back to a string but with line endings removed
						$comment = implode( "", $lines );

						// Note: new archives will return an empty comment if one was not added at creation
						$this->log( 'details', sprintf( __('ZipArchive retrieved comment in file %1$s','it-l10n-backupbuddy' ), $zip_file ) );
						$result = $comment;

					} else {

						// If we failed to get the commnent then log it (?) and drop through
						$this->log( 'details', sprintf( __('ZipArchive failed to retrieve comment in file %1$s','it-l10n-backupbuddy' ), $zip_file ) );
						$result = false;

					}	

				} else {
				
					// If we couldn't open the zip file then log it (?) and drop through
					$error_string = $za->errorInfo( $result );
					$this->log( 'details', sprintf( __('ZipArchive failed to open file to get comment in file %1$s - Error Info: %2$s','it-l10n-backupbuddy' ), $zip_file, $error_string ) );
					$result = false;
										
				}
				
				$za->close();
								
			} else {
			
				// Something fishy - the methods indicated ziparchive but we couldn't find the class
				$this->log( 'details', __('ziparchive indicated as available method but ZipArchive class non-existent','it-l10n-backupbuddy' ) );
				$result = false;
				
			}
			
		  	if ( NULL != $za ) { unset( $za ); }		
			
			return $result;
			
		}
		
	} // end pluginbuddy_zbzipziparchive class.	
	
}
?>
