<?php

/**
 * DOCXTemplate class 0.1.10 
 * 
 * @author sergey.shuchkin@gmail.com
 */

/**
 * Replace {var} in MS Word 2007+ documents (*.docx)
 */
class DOCXTemplate {

    private $data;
    private $package;
    private $error;

    public function __construct($template_filename, $is_data = false, $debug = false) {
        $this->data = array();
        $this->error = false;
        $this->debug = $debug;
        $this->_unzip($template_filename, $is_data);
    }

    public function set($var, $value = NULL) {

        if (is_array($var) || is_object($var)) {
            foreach ($var as $k => $v)
                $this->data[$k] = $v;
        } else {
            $this->data[$var] = $value;
        }
    }

    public function error($set = false) {
        if ($set) {
            $this->error = $set;
            if ($this->debug)
                trigger_error(__CLASS__ . ': ' . $set, E_USER_WARNING);
        } else {
            return $this->error;
        }
    }

    public function success() {
        return !$this->error;
    }

    private function _parse() {
        if ($doc = $this->getEntryData('word/document.xml')) {
            if (preg_match_all('/\{[^}]+\}/', $doc, $m)) {
                foreach ($m[0] as $v) {
                    $var = preg_replace('/[\s\{\}]/', '', strip_tags($v));
                    if (isset($this->data[$var])) {
                        if (is_scalar($this->data[$var]))
                            $doc = str_replace($v, $this->_esc($this->data[$var]), $doc);
                    } else {
                        $doc = str_replace($v, '{!404_' . __CLASS__ . '_' . $var . '}', $doc);
                    }
                }
            }
            $this->setEntryData('word/document.xml', $doc);
            return true;
        } else
            return false;
    }

    private function _unzip($filename, $is_data = false) {

        // Clear current file
        $this->datasec = array();

        if ($is_data) {

            $this->package['filename'] = 'default.xlsx';
            $this->package['mtime'] = time();
            $this->package['size'] = strlen($filename);

            $vZ = $filename;
        } else {

            if (!is_readable($filename)) {
                $this->error('File not found');
                return false;
            }

            // Package information
            $this->package['filename'] = $filename;
            $this->package['mtime'] = filemtime($filename);
            $this->package['size'] = filesize($filename);

            // Read file
            $oF = fopen($filename, 'rb');
            $vZ = fread($oF, $this->package['size']);
            fclose($oF);
        }
        // Cut end of central directory
        /* 		$aE = explode("\x50\x4b\x05\x06", $vZ);

          if (count($aE) == 1) {
          $this->error('Unknown format');
          return false;
          }
         */
        if (($pcd = strrpos($vZ, "\x50\x4b\x05\x06")) === false) {
            $this->error('Unknown format');
            return false;
        }
        $aE = array(
            0 => substr($vZ, 0, $pcd),
            1 => substr($vZ, $pcd + 3)
        );

        // Normal way
        $aP = unpack('x16/v1CL', $aE[1]);
        $this->package['comment'] = substr($aE[1], 18, $aP['CL']);

        // Translates end of line from other operating systems
        $this->package['comment'] = strtr($this->package['comment'], array("\r\n" => "\n", "\r" => "\n"));

        // Cut the entries from the central directory
        $aE = explode("\x50\x4b\x01\x02", $vZ);
        // Explode to each part
        $aE = explode("\x50\x4b\x03\x04", $aE[0]);
        // Shift out spanning signature or empty entry
        array_shift($aE);

        // Loop through the entries
        foreach ($aE as $vZ) {
            $aI = array();
            $aI['E'] = 0;
            $aI['EM'] = '';
            // Retrieving local file header information
//			$aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ);
            $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ);

            // Check if data is encrypted
//			$bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE;
            $bE = false;
            $nF = $aP['FNL'];
            $mF = $aP['EFL'];

            // Special case : value block after the compressed data
            if ($aP['GPF'] & 0x0008) {
                $aP1 = unpack('V1CRC/V1CS/V1UCS', substr($vZ, -12));

                $aP['CRC'] = $aP1['CRC'];
                $aP['CS'] = $aP1['CS'];
                $aP['UCS'] = $aP1['UCS'];
                // 2013-08-10
                $vZ = substr($vZ, 0, -12);
                if (substr($vZ, -4) === "\x50\x4b\x07\x08")
                    $vZ = substr($vZ, 0, -4);
            }

            // Getting stored filename
            $aI['N'] = substr($vZ, 26, $nF);

            if (substr($aI['N'], -1) == '/') {
                // is a directory entry - will be skipped
                continue;
            }

            // Truncate full filename in path and filename
            $aI['P'] = dirname($aI['N']);
            $aI['P'] = $aI['P'] == '.' ? '' : $aI['P'];
            $aI['N'] = basename($aI['N']);

            $vZ = substr($vZ, 26 + $nF + $mF);

            if (strlen($vZ) != $aP['CS']) { // check only if availabled
                $aI['E'] = 1;
                $aI['EM'] = 'Compressed size is not equal with the value in header information.';
            } else {
                if ($bE) {
                    $aI['E'] = 5;
                    $aI['EM'] = 'File is encrypted, which is not supported from this class.';
                } else {
                    switch ($aP['CM']) {
                        case 0: // Stored
                            // Here is nothing to do, the file ist flat.
                            break;
                        case 8: // Deflated
                            $vZ = gzinflate($vZ);
                            break;
                        case 12: // BZIP2
                            if (!extension_loaded('bz2')) {
                                if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                                    @dl('php_bz2.dll');
                                } else {
                                    @dl('bz2.so');
                                }
                            }
                            if (extension_loaded('bz2')) {
                                $vZ = bzdecompress($vZ);
                            } else {
                                $aI['E'] = 7;
                                $aI['EM'] = "PHP BZIP2 extension not available.";
                            }
                            break;
                        default:
                            $aI['E'] = 6;
                            $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported.";
                    }
                    if (!$aI['E']) {
                        if ($vZ === FALSE) {
                            $aI['E'] = 2;
                            $aI['EM'] = 'Decompression of data failed.';
                        } else {
                            if (strlen($vZ) != $aP['UCS']) {
                                $aI['E'] = 3;
                                $aI['EM'] = 'Uncompressed size is not equal with the value in header information.';
                            } else {
                                if (crc32($vZ) != $aP['CRC']) {
                                    $aI['E'] = 4;
                                    $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.';
                                }
                            }
                        }
                    }
                }
            }

            $aI['D'] = $vZ;

            // DOS to UNIX timestamp
            $aI['T'] = mktime(($aP['FT'] & 0xf800) >> 11, ($aP['FT'] & 0x07e0) >> 5, ($aP['FT'] & 0x001f) << 1, ($aP['FD'] & 0x01e0) >> 5, ($aP['FD'] & 0x001f), (($aP['FD'] & 0xfe00) >> 9) + 1980);

            //$this->Entries[] = &new SimpleUnzipEntry($aI);
            $this->package['entries'][] = array(
                'data' => $aI['D'],
                'error' => $aI['E'],
                'error_msg' => $aI['EM'],
                'name' => $aI['N'],
                'path' => $aI['P'],
                'time' => $aI['T']
            );
        } // end for each entries
    }

    public function getPackage() {
        return $this->package;
    }

    public function entryExists($name) { // 0.6.6
        $dir = dirname($name);
        $name = basename($name);
        foreach ($this->package['entries'] as $entry)
            if ($entry['path'] == $dir && $entry['name'] == $name)
                return true;
        return false;
    }

    public function getEntryData($name) {
        $dir = dirname($name);
        $name = basename($name);
        foreach ($this->package['entries'] as $entry)
            if ($entry['path'] == $dir && $entry['name'] == $name)
                return $entry['data'];
        $this->error('Unknown format');
        return false;
    }

    public function setEntryData($name, $data) {
        $dir = dirname($name);
        $name = basename($name);
        foreach ($this->package['entries'] as $k => $entry)
            if ($entry['path'] == $dir && $entry['name'] == $name) {
                $this->package['entries'][$k]['data'] = $data;
                return;
            }
        return false;
    }

    private function _esc($s) {
        $s = str_replace(array('&', '"', "'", "<", ">"), array('&amp;', '&quot;', '&#039;', '&lt;', '&gt;'), $s);
        $s = str_replace(array("\r\n", "\r"), "\n", $s);
        $s = str_replace("\n", '</w:t><w:br/><w:t>', $s);
        return $s;
    }

    private function _zip($fh) {

        $zipSignature = "\x50\x4b\x03\x04"; // local file header signature
        $dirSignature = "\x50\x4b\x01\x02"; // central dir header signature
        $dirSignatureE = "\x50\x4b\x05\x06"; // end of central dir signature

        $zipComments = 'Generated by ' . __CLASS__ . ' PHP class, thanks Sergey Shuchkin';

//		$fh = fopen( $filename, 'wb' );

        if (!$fh)
            return false;

        $cdrec = '';

        foreach ($this->package['entries'] as $e) {

            $cfilename = ($e['path']) ? $e['path'] . '/' . $e['name'] : $e['name'];


            $e['uncsize'] = strlen($e['data']);

            // if data to compress is too small, just store it
            if ($e['uncsize'] < 256) {
                $e['comsize'] = $e['uncsize'];
                $e['vneeded'] = 10;
                $e['cmethod'] = 0;
                $zdata = $e['data'];
            } else { // otherwise, compress it
                $zdata = gzcompress($e['data']);
                $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug (thanks to Eric Mueller)
                $e['comsize'] = strlen($zdata);
                $e['vneeded'] = 10;
                $e['cmethod'] = 8;
            }

            $e['bitflag'] = 0;
            $e['crc_32'] = crc32($e['data']);

            // Convert date and time to DOS Format, and set then
            $lastmod_timeS = str_pad(decbin(date('s') >= 32 ? date('s') - 32 : date('s')), 5, '0', STR_PAD_LEFT);
            $lastmod_timeM = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT);
            $lastmod_timeH = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT);
            $lastmod_dateD = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT);
            $lastmod_dateM = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT);
            $lastmod_dateY = str_pad(decbin(date('Y') - 1980), 7, '0', STR_PAD_LEFT);

            # echo "ModTime: $lastmod_timeS-$lastmod_timeM-$lastmod_timeH (".date("s H H").")\n";
            # echo "ModDate: $lastmod_dateD-$lastmod_dateM-$lastmod_dateY (".date("d m Y").")\n";
            $e['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS");
            $e['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD");

            $e['offset'] = ftell($fh);

            fwrite($fh, $zipSignature);
            fwrite($fh, pack('s', $e['vneeded'])); // version_needed
            fwrite($fh, pack('s', $e['bitflag'])); // general_bit_flag
            fwrite($fh, pack('s', $e['cmethod'])); // compression_method
            fwrite($fh, pack('s', $e['modtime'])); // lastmod_time
            fwrite($fh, pack('s', $e['moddate'])); // lastmod_date
            fwrite($fh, pack('V', $e['crc_32']));  // crc-32
            fwrite($fh, pack('I', $e['comsize'])); // compressed_size
            fwrite($fh, pack('I', $e['uncsize'])); // uncompressed_size
            fwrite($fh, pack('s', strlen($cfilename)));   // file_name_length
            fwrite($fh, pack('s', 0));  // extra_field_length
            fwrite($fh, $cfilename);    // file_name
            // ignoring extra_field
            fwrite($fh, $zdata);

            // Append it to central dir
            $e['external_attributes'] = (substr($cfilename, -1) == '/' && !$zdata) ? 16 : 32; // Directory or file name
            $e['comments'] = '';

            $cdrec .= $dirSignature;
            $cdrec .= "\x0\x0";                  // version made by
            $cdrec .= pack('v', $e['vneeded']); // version needed to extract
            $cdrec .= "\x0\x0";                  // general bit flag
            $cdrec .= pack('v', $e['cmethod']); // compression method
            $cdrec .= pack('v', $e['modtime']); // lastmod time
            $cdrec .= pack('v', $e['moddate']); // lastmod date
            $cdrec .= pack('V', $e['crc_32']);  // crc32
            $cdrec .= pack('V', $e['comsize']); // compressed filesize
            $cdrec .= pack('V', $e['uncsize']); // uncompressed filesize
            $cdrec .= pack('v', strlen($cfilename)); // file name length
            $cdrec .= pack('v', 0);                // extra field length
            $cdrec .= pack('v', strlen($e['comments'])); // file comment length
            $cdrec .= pack('v', 0); // disk number start
            $cdrec .= pack('v', 0); // internal file attributes
            $cdrec .= pack('V', $e['external_attributes']); // internal file attributes
            $cdrec .= pack('V', $e['offset']); // relative offset of local header
            $cdrec .= $cfilename;
            $cdrec .= $e['comments'];
        }
        $before_cd = ftell($fh);
        fwrite($fh, $cdrec);

        // end of central dir
        fwrite($fh, $dirSignatureE);
        fwrite($fh, pack('v', 0)); // number of this disk
        fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory
        fwrite($fh, pack('v', count($this->package['entries']))); // total # of entries "on this disk" 
        fwrite($fh, pack('v', count($this->package['entries']))); // total # of entries overall 
        fwrite($fh, pack('V', strlen($cdrec)));     // size of central dir 
        fwrite($fh, pack('V', $before_cd));         // offset to start of central dir
        fwrite($fh, pack('v', strlen($zipComments))); // .zip file comment length
        fwrite($fh, $zipComments);

        return true;
    }

    function saveAs($filename) {
        if (!$this->_parse())
            return false;
        $fh = fopen($filename, 'wb');
        if (!$fh)
            return false;
        if (!$this->_zip($fh)) {
            fclose($fh);
            return false;
        }
        fclose($fh);
        return true;
    }

    function downloadAs($filename, $exit = true) {
        if (!$this->_parse())
            return false;
        //php://stdin
        $fh = tmpfile();
        if (!$fh)
            return false;
        if (!$this->_zip($fh)) {
            fclose($fh);
            return false;
        }
        $size = ftell($fh);
        $filename = ($filename) ? $filename : gmdate('Ymdhi');
        header('Content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', time()));
        header('Content-Length: ' . $size);
        fseek($fh, 0);
        echo fread($fh, $size);
        fclose($fh);
        if ($exit)
            exit();
        return true;
    }

}

?>