<?php
/**
 * Handles creating the zipped files
 *
 * @package Eight_Day_Week
 */

/**
 * Zip file creation class.
 * Makes zip files.
 *
 * @access  public
 * @see     Official ZIP file format: http://www.pkware.com/support/zip-app-note
 */
class ZipFile {
	/**
	 * Whether to echo zip as it's built or return as string from -> file
	 *
	 * @var  boolean  $do_write
	 */
	public $do_write = false;

	/**
	 * Array to store compressed data
	 *
	 * @var  array    $datasec
	 */
	public $datasec = array();

	/**
	 * Central directory
	 *
	 * @var  array    $ctrl_dir
	 */
	public $ctrl_dir = array();

	/**
	 * End of central directory record
	 *
	 * @var  string   $eof_ctrl_dir
	 */
	public $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00";

	/**
	 * Last offset position
	 *
	 * @var  integer  $old_offset
	 */
	public $old_offset = 0;


	/**
	 * Sets member variable this -> do_write to true
	 * - Should be called immediately after class instantiation
	 * - If set to true, then ZIP archive are echo'ed to STDOUT as each
	 *   file is added via this -> addfile(), and central directories are
	 *   echoed to STDOUT on final call to this -> file().  Also,
	 *   this -> file() returns an empty string so it is safe to issue a
	 *   "echo $zipfile;" command
	 *
	 * @access public
	 *
	 * @return void
	 */
	public function setdo_write() {
		$this->do_write = true;
	} // end of the 'setdo_write()' method

	/**
	 * Converts an Unix timestamp to a four byte DOS date and time format (date
	 * in high two bytes, time in low two bytes allowing magnitude comparison).
	 *
	 * @param integer $unixtime The current Unix timestamp.
	 *
	 * @return integer the current date in a four byte DOS format
	 *
	 * @access private
	 */
	public function unix_2_dos_time( $unixtime = 0 ) {
		$timearray = ( 0 === $unixtime ) ? getdate() : getdate( $unixtime );

		if ( $timearray['year'] < 1980 ) {
			$timearray['year']    = 1980;
			$timearray['mon']     = 1;
			$timearray['mday']    = 1;
			$timearray['hours']   = 0;
			$timearray['minutes'] = 0;
			$timearray['seconds'] = 0;
		} // end if

		return ( ( $timearray['year'] - 1980 ) << 25 )
			| ( $timearray['mon'] << 21 )
			| ( $timearray['mday'] << 16 )
			| ( $timearray['hours'] << 11 )
			| ( $timearray['minutes'] << 5 )
			| ( $timearray['seconds'] >> 1 );
	} // end of the 'unix_2_dos_time()' method


	/**
	 * Adds "file" to archive
	 *
	 * @param string  $data File contents.
	 * @param string  $name Name of the file in the archive (may contains the path).
	 * @param integer $time The current timestamp.
	 *
	 * @access public
	 *
	 * @return void
	 */
	public function addFile( $data, $name, $time = 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
		$name = str_replace( '\\', '/', $name );

		$hexdtime = pack( 'V', $this->unix_2_dos_time( $time ) );

		$fr  = "\x50\x4b\x03\x04";
		$fr .= "\x14\x00";            // ver needed to extract.
		$fr .= "\x00\x00";            // gen purpose bit flag.
		$fr .= "\x08\x00";            // compression method.
		$fr .= $hexdtime;             // last mod time and date.

		// "local file header" segment
		$unc_len = strlen( $data );
		$crc     = crc32( $data );
		$zdata   = gzcompress( $data, 0 );
		$zdata   = substr( substr( $zdata, 0, strlen( $zdata ) - 4 ), 2 ); // fix crc bug.
		$c_len   = strlen( $zdata );
		$fr     .= pack( 'V', $crc );             // crc32.
		$fr     .= pack( 'V', $c_len );           // compressed filesize.
		$fr     .= pack( 'V', $unc_len );         // uncompressed filesize.
		$fr     .= pack( 'v', strlen( $name ) );  // length of filename.
		$fr     .= pack( 'v', 0 );                // extra field length.
		$fr     .= $name;

		// "file data" segment
		$fr .= $zdata;

		// echo this entry on the fly, ...
		if ( $this->do_write ) {
			echo $fr; // phpcs:ignore
		} else {                     // ... OR add this entry to array
			$this->datasec[] = $fr;
		}

		// Now add to central directory record.
		$cdrec  = "\x50\x4b\x01\x02";
		$cdrec .= "\x00\x00";                   // version made by.
		$cdrec .= "\x14\x00";                   // version needed to extract.
		$cdrec .= "\x00\x00";                   // gen purpose bit flag.
		$cdrec .= "\x08\x00";                   // compression method.
		$cdrec .= $hexdtime;                    // last mod time & date.
		$cdrec .= pack( 'V', $crc );            // crc32.
		$cdrec .= pack( 'V', $c_len );          // compressed filesize.
		$cdrec .= pack( 'V', $unc_len );        // uncompressed filesize.
		$cdrec .= pack( 'v', strlen( $name ) ); // length of filename.
		$cdrec .= pack( 'v', 0 );               // extra field length.
		$cdrec .= pack( 'v', 0 );               // file comment length.
		$cdrec .= pack( 'v', 0 );               // disk number start.
		$cdrec .= pack( 'v', 0 );               // internal file attributes.
		$cdrec .= pack( 'V', 32 );              // external file attributes.
		// - 'archive' bit set.

		$cdrec            .= pack( 'V', $this->old_offset ); // relative offset of local header.
		$this->old_offset += strlen( $fr );

		$cdrec .= $name;

		// optional extra field, file comment goes here
		// save to central directory.
		$this->ctrl_dir[] = $cdrec;
	} // end of the 'addFile()' method


	/**
	 * Echo central dir if ->do_write==true, else build string to return
	 *
	 * @return string  if ->do_write {empty string} else the ZIP file contents
	 *
	 * @access public
	 */
	public function file() {
		$ctrldir = implode( '', $this->ctrl_dir );
		$header  = $ctrldir .
					$this->eof_ctrl_dir .
					pack( 'v', count( $this->ctrl_dir ) ) . // total #of entries "on this disk".
					pack( 'v', count( $this->ctrl_dir ) ) . // total #of entries overall.
					pack( 'V', strlen( $ctrldir ) ) . // size of central dir.
					pack( 'V', $this->old_offset ) . // offset to start of central dir.
					"\x00\x00";                             // .zip file comment length.

		if ( $this->do_write ) { // Send central directory & end ctrl dir to STDOUT.
			echo $header; // phpcs:ignore
			return ''; // Return empty string.
		} else {  // Return entire ZIP archive as string.
			$data = implode( '', $this->datasec );

			return $data . $header;
		}
	} // end of the 'file()' method

} // end of the 'ZipFile' class
