Kimai time management system (PHP 5.6 / MySQL) with custom extensions: - Budget tracking (ki_budget) - Change request form (ki_changerequest) - Invoice generation (ki_invoice) - Expense tracking (ki_expenses) - Flexi time (ki_flexitime) - Export, admin panel, timesheets, tasks, summary Database: ~900K time entries, 2,000+ users, 439 projects. Config: includes/autoconf.php — localhost/kimai/kimai
1284 lines
36 KiB
PHP
1284 lines
36 KiB
PHP
<?php
|
|
/*
|
|
* This file is part of the tinyDoc package.
|
|
* (c) Olivier Loynet
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
/**
|
|
* tinyDoc class.
|
|
*
|
|
* This class extends TinyButStrong class to work with OpenDocument and Word 2007 documents.
|
|
*
|
|
* This class needs : PHP 5.2
|
|
* This class needs : TinyButStrong class
|
|
* This class needs optionally : ZipArchive from PECL
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*
|
|
* @package tinyDoc
|
|
* @subpackage tinyDoc
|
|
* @author Olivier Loynet <tinydoc@googlegroups.com>
|
|
* @version $Id$
|
|
*/
|
|
|
|
class tinyDoc extends clsTinyButStrong
|
|
{
|
|
const VERSION = '1.0.3';
|
|
|
|
const PICTURE_DPI = 96;
|
|
const INCH_TO_CM = 2.54;
|
|
const PIXEL_TO_CM = 0.0264583333; // 2.54 / 96
|
|
|
|
private
|
|
$zipMethod = 'shell',
|
|
$zipBin = 'zip',
|
|
$unzipBin = 'unzip',
|
|
|
|
$sourcePathname = '',
|
|
$processDir = '',
|
|
$processBasename = '',
|
|
$defaultExtension = 'odt',
|
|
$xmlFilename = 'content.xml',
|
|
|
|
$defaultCharset = 'UTF-8',
|
|
$defaultIsEscape = true,
|
|
$defaultCallback = '=~encodeData',
|
|
|
|
$charset = 'UTF-8',
|
|
$isEscape = true,
|
|
$callback = '=~encodeData',
|
|
|
|
$mimetype = array(
|
|
'sxw' => 'application/vnd.sun.xml.writer',
|
|
'stw' => 'application/vnd.sun.xml.writer.template',
|
|
'sxg' => 'application/vnd.sun.xml.writer.global',
|
|
'sxc' => 'application/vnd.sun.xml.calc',
|
|
'stc' => 'application/vnd.sun.xml.calc.template',
|
|
'sxi' => 'application/vnd.sun.xml.impress',
|
|
'sti' => 'application/vnd.sun.xml.impress.template',
|
|
'sxd' => 'application/vnd.sun.xml.draw',
|
|
'std' => 'application/vnd.sun.xml.draw.template',
|
|
'sxm' => 'application/vnd.sun.xml.math',
|
|
'odt' => 'application/vnd.oasis.OpenDocument.text',
|
|
'ott' => 'application/vnd.oasis.OpenDocument.text-template',
|
|
'oth' => 'application/vnd.oasis.OpenDocument.text-web',
|
|
'odm' => 'application/vnd.oasis.OpenDocument.text-master',
|
|
'odg' => 'application/vnd.oasis.OpenDocument.graphics',
|
|
'otg' => 'application/vnd.oasis.OpenDocument.graphics-template',
|
|
'odp' => 'application/vnd.oasis.OpenDocument.presentation',
|
|
'otp' => 'application/vnd.oasis.OpenDocument.presentation-template',
|
|
'ods' => 'application/vnd.oasis.OpenDocument.spreadsheet',
|
|
'ots' => 'application/vnd.oasis.OpenDocument.spreadsheet-template',
|
|
'odc' => 'application/vnd.oasis.OpenDocument.chart',
|
|
'odf' => 'application/vnd.oasis.OpenDocument.formula',
|
|
'odb' => 'application/vnd.oasis.OpenDocument.database',
|
|
'odi' => 'application/vnd.oasis.OpenDocument.image',
|
|
'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
|
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
|
|
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
|
'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
|
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
|
|
'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
|
|
'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
|
|
'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
|
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
|
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
|
|
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
|
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
|
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
|
|
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
|
);
|
|
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new unique file from a source file
|
|
*
|
|
* @param mixed $options The pathname of source document (empty array by default)
|
|
*/
|
|
public function createFrom($options = array())
|
|
{
|
|
$this->setSourcePathname($options);
|
|
|
|
// create an unique basename to process the office document
|
|
$this->newUniqueBasename();
|
|
|
|
// create an unique directory like basename to zip/unzip files
|
|
if (!mkdir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename(), 0777, true))
|
|
{
|
|
throw new tinyDocException(sprintf('Can\'t make directory "%s"', $this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename()));
|
|
}
|
|
if (!is_dir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename()))
|
|
{
|
|
throw new tinyDocException(sprintf('Directory not found "%s"', $this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename()));
|
|
}
|
|
|
|
// copy the file source into the process dir with an unique filename
|
|
if (!copy($this->getSourcePathname(), $this->getPathname()))
|
|
{
|
|
if (!file_exists($this->getSourcePathname()))
|
|
{
|
|
throw new tinyDocException(sprintf('Can\'t copy file, source file not found "%s"', $this->getSourcePathname()));
|
|
}
|
|
else
|
|
{
|
|
throw new tinyDocException(sprintf('Can\'t copy file from "%s" to "%s"', $this->getSourcePathname(), $this->getPathname()));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Load the XML file from the current process file as a TBS template
|
|
*
|
|
* @param string $xmlFilename The XML file (content.xml by default)
|
|
*/
|
|
public function loadXml($xmlFilename = 'content.xml')
|
|
{
|
|
$this->setXmlFilename($xmlFilename);
|
|
|
|
// unzip the XML file into the current basename process dir
|
|
switch($this->getZipMethod())
|
|
{
|
|
case 'ziparchive':
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($this->getPathname()) === true)
|
|
{
|
|
$zip->extractTo($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR, array($this->getXmlFilename()));
|
|
$zip->close();
|
|
}
|
|
break;
|
|
|
|
case 'pclzip':
|
|
require_once('pclzip.lib.php');
|
|
$zip = new PclZip($this->getPathname());
|
|
$zip->extract(PCLZIP_OPT_PATH,$this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR,
|
|
PCLZIP_OPT_BY_NAME,$this->getXmlFilename());
|
|
break;
|
|
|
|
case 'shell':
|
|
default:
|
|
$cmd = $this->getUnzipBinary();
|
|
$cmd.= ' '.escapeshellarg($this->getPathname());
|
|
$cmd.= ' -d';
|
|
$cmd.= ' '.escapeshellarg($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename());
|
|
$cmd.= ' '.escapeshellarg($this->getXmlFilename());
|
|
exec($cmd);
|
|
break;
|
|
}
|
|
|
|
// test if the XML file exist
|
|
if (!file_exists($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$this->getXmlFilename()))
|
|
{
|
|
throw new tinyDocException(sprintf('Xml file not found "%s"', $this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$this->getXmlFilename()));
|
|
}
|
|
|
|
// load the XML file as a TBS template
|
|
$this->ObjectRef = $this;
|
|
$this->LoadTemplate($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$this->getXmlFilename(), $this->getCallback());
|
|
|
|
// work around - convert apostrophe in XML file needed for TBS functions
|
|
$this->Source = str_replace(''', '\'', $this->Source);
|
|
}
|
|
|
|
|
|
/**
|
|
* Merge data with the template file
|
|
*
|
|
* Available options:
|
|
*
|
|
* - name: The tag name in the template ('block' by default)
|
|
* - type: The tag type in the template ('field' | 'block' - 'block' by default)
|
|
* - data_type: The data type ('array' by default)
|
|
* - charset: The data charset ('UTF-8' by default)
|
|
* - is_escape: If data are escaped (true by default)
|
|
* - callback: The callback to encode data ('=~encodeData' by default)
|
|
*
|
|
* @param array $options Options
|
|
* @param mixed $data Data
|
|
*/
|
|
public function mergeXml($options = array(), $data = array())
|
|
{
|
|
$name = isset($options['name']) && is_string($options['name']) ? $options['name'] : 'block';
|
|
$type = isset($options['type']) && is_string($options['type']) ? $options['type'] : 'block';
|
|
$data_type = isset($options['data_type']) && is_string($options['data_type']) ? $options['data_type'] : 'array';
|
|
|
|
$this->setCharset (isset($options['charset']) && is_string($options['charset']) ? $options['charset'] : $this->getDefaultCharset());
|
|
$this->setIsEscape(isset($options['is_escape']) && is_bool($options['is_escape']) ? $options['is_escape'] : $this->getDefaultIsEscape());
|
|
$this->setCallback(isset($options['callback']) && is_string($options['callback']) ? $options['callback'] : $this->getDefaultCallback());
|
|
|
|
switch($type)
|
|
{
|
|
case 'field':
|
|
$this->MergeField($name, $data);
|
|
break;
|
|
|
|
case 'block':
|
|
default:
|
|
$this->MergeBlock($name, $data_type, $data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Merge data with a TBS field in template
|
|
*
|
|
* @param string $name TBS field name in template
|
|
* @param mixed $data Data to merge
|
|
*/
|
|
public function mergeXmlField($name, $data)
|
|
{
|
|
$this->mergeXml(array('name' => $name, 'type' => 'field'), $data);
|
|
}
|
|
|
|
|
|
/**
|
|
* Merge data with a TBS block in template
|
|
*
|
|
* @param string $name TBS block name in template
|
|
* @param mixed $data Data to merge
|
|
*/
|
|
public function mergeXmlBlock($name, $data)
|
|
{
|
|
$this->mergeXml(array('name' => $name, 'type' => 'block'), $data);
|
|
}
|
|
|
|
|
|
/**
|
|
* Save the result of merged XML file into the current process file
|
|
*/
|
|
public function saveXml()
|
|
{
|
|
// get the source result (TBS method)
|
|
$this->Show(TBS_NOTHING);
|
|
|
|
// update the result into the current process file
|
|
switch($this->getZipMethod())
|
|
{
|
|
case 'ziparchive':
|
|
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($this->getPathname()) === true)
|
|
{
|
|
$zip->addFromString($this->getXmlFilename(), $this->Source);
|
|
$zip->close();
|
|
}
|
|
break;
|
|
|
|
case 'pclzip':
|
|
require_once('pclzip.lib.php');
|
|
// save the merged result
|
|
$fdw = fopen($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$this->getXmlFilename(), "w");
|
|
fwrite($fdw, $this->Source, strlen($this->Source));
|
|
fclose ($fdw);
|
|
|
|
// change the current directory to basename in process dir
|
|
$cwd = getcwd();
|
|
chdir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR);
|
|
|
|
$zip = new PclZip($this->getPathname());
|
|
$zip->add($this->getXmlFilename());
|
|
|
|
// get back current directory
|
|
chdir($cwd);
|
|
break;
|
|
|
|
case 'shell':
|
|
default:
|
|
// save the merged result
|
|
$fdw = fopen($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$this->getXmlFilename(), "w");
|
|
fwrite($fdw, $this->Source, strlen($this->Source));
|
|
fclose ($fdw);
|
|
|
|
// change the current directory to basename in process dir
|
|
$cwd = getcwd();
|
|
chdir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR);
|
|
|
|
// zip the XML file into the archive
|
|
$cmd = $this->getZipBinary();
|
|
$cmd.= ' -m';
|
|
$cmd.= ' '.escapeshellarg($this->getPathname());
|
|
$cmd.= ' '.escapeshellarg($this->getXmlFilename());
|
|
exec($cmd);
|
|
|
|
// get back current directory
|
|
chdir($cwd);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Encode the data before merge
|
|
*
|
|
* @param string $encodeData The data to merge
|
|
*
|
|
* @return string The encoded data
|
|
*/
|
|
public function encodeData($encodeData)
|
|
{
|
|
// XML charset of OpenOffice / OpenDocument / Word 2007 is utf-8
|
|
switch($this->getCharset())
|
|
{
|
|
case 'ISO 8859-1':
|
|
case 'ISO 8859-15':
|
|
$encodeData = utf8_encode($encodeData);
|
|
break;
|
|
case 'UTF-8':
|
|
case 'UTF8':
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// convert special XML chars
|
|
$encodeData = str_replace('&' ,'&', $encodeData); // the '&' has to be the first to convert
|
|
$encodeData = str_replace('\'' ,''', $encodeData);
|
|
$encodeData = str_replace('"' ,'"', $encodeData);
|
|
|
|
if ($this->getIsEscape())
|
|
{
|
|
$encodeData = str_replace('<', '<', $encodeData);
|
|
$encodeData = str_replace('>', '>', $encodeData);
|
|
}
|
|
|
|
// convert TAB & LF & CR to XML
|
|
switch($this->getExtension())
|
|
{
|
|
case 'docm':
|
|
case 'docx':
|
|
case 'dotm':
|
|
case 'dotx':
|
|
$encodeData = str_replace("\r\n", '</w:t></w:r></w:p><w:p><w:r><w:t>', $encodeData);
|
|
$encodeData = str_replace("\n", '</w:t></w:r></w:p><w:p><w:r><w:t>', $encodeData);
|
|
$encodeData = str_replace("\r", '</w:t></w:r></w:p><w:p><w:r><w:t>', $encodeData);
|
|
$encodeData = str_replace("\t", '</w:t></w:r><w:r><w:tab/><w:t>' , $encodeData);
|
|
break;
|
|
|
|
case 'ods':
|
|
case 'sxc':
|
|
$encodeData = str_replace("\r\n", '</text:p><text:p>' , $encodeData);
|
|
$encodeData = str_replace("\n", '</text:p><text:p>' , $encodeData);
|
|
$encodeData = str_replace("\r", '</text:p><text:p>' , $encodeData);
|
|
$encodeData = str_replace("\t", '<text:tab/>' , $encodeData);
|
|
|
|
// work-around for EURO caracter
|
|
$encodeData = str_replace(chr(0xC2).chr(0x80) , chr(0xE2).chr(0x82).chr(0xAC), $encodeData);
|
|
break;
|
|
|
|
case 'odt':
|
|
case 'sxw':
|
|
default:
|
|
$encodeData = str_replace("\r\n", '<text:line-break/>', $encodeData);
|
|
$encodeData = str_replace("\n", '<text:line-break/>', $encodeData);
|
|
$encodeData = str_replace("\r", '<text:line-break/>', $encodeData);
|
|
$encodeData = str_replace("\t", '<text:tab/>' , $encodeData);
|
|
|
|
// work-around for EURO caracter
|
|
$encodeData = str_replace(chr(0xC2).chr(0x80) , chr(0xE2).chr(0x82).chr(0xAC), $encodeData);
|
|
break;
|
|
}
|
|
|
|
// remove specials chars from 0x00 to 0x1F
|
|
$encodeData = preg_replace('/[\x{00}-\x{1F}]+/u', '', $encodeData);
|
|
|
|
return $encodeData;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a unique id and set the basename of process file
|
|
*/
|
|
public function newUniqueBasename()
|
|
{
|
|
$unique = md5(uniqid(rand(), true));
|
|
$this->setBasename($unique);
|
|
}
|
|
|
|
|
|
/**
|
|
* Close by remove the current process directory and subdirectory
|
|
*/
|
|
public function close()
|
|
{
|
|
// remove the current process directory
|
|
clearstatcache();
|
|
if (is_dir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename()))
|
|
{
|
|
$this->deltree($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename());
|
|
rmdir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove a directory and his contents recursively
|
|
*
|
|
* @param string $dir The pathname the directory
|
|
*/
|
|
private function deltree($dir)
|
|
{
|
|
// changed by kevin - makes no sense on shared hosting.
|
|
// for example open basedir settings restrict access to /
|
|
// if (realpath($dir) == realpath(DIRECTORY_SEPARATOR))
|
|
|
|
if (realpath($dir) == DIRECTORY_SEPARATOR)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach(glob($dir.DIRECTORY_SEPARATOR.'*') as $filename)
|
|
{
|
|
if (is_dir($filename) && !is_link($filename))
|
|
{
|
|
$this->deltree($filename);
|
|
if (is_writable($filename))
|
|
{
|
|
rmdir($filename);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (is_writable($filename))
|
|
{
|
|
unlink($filename);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove the current process file
|
|
*/
|
|
public function remove()
|
|
{
|
|
unlink($this->getPathname());
|
|
}
|
|
|
|
|
|
/**
|
|
* Add an external file into the current process file
|
|
*
|
|
* GIF images are not supported in WORD 2007
|
|
*
|
|
* @param string $sourcePathname The pathname of the source file to add
|
|
* @param string $archivePathname The pathname in the archive
|
|
*/
|
|
public function addFile($sourcePathname, $archivePathname)
|
|
{
|
|
if (file_exists($sourcePathname))
|
|
{
|
|
switch($this->getZipMethod())
|
|
{
|
|
case 'ziparchive':
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($this->getPathname()) === true)
|
|
{
|
|
// don't work - archive corrupted
|
|
// $zip->addFile($sourcePathname, $archivePathname);
|
|
|
|
$zip->addFromString($archivePathname, file_get_contents($sourcePathname, false));
|
|
$zip->close();
|
|
}
|
|
|
|
break;
|
|
|
|
case 'pclzip':
|
|
require_once('pclzip.lib.php');
|
|
$zip = new PclZip($this->getPathname());
|
|
$zip->add($sourcePathname,
|
|
PCLZIP_OPT_ADD_PATH,$archivePathname,
|
|
PCLZIP_OPT_REMOVE_ALL_PATH);
|
|
break;
|
|
|
|
case 'shell':
|
|
default:
|
|
$dir = dirname($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$archivePathname);
|
|
if (!file_exists($dir))
|
|
{
|
|
mkdir($dir, 0777, true);
|
|
}
|
|
|
|
if (copy($sourcePathname, $this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR.$archivePathname))
|
|
{
|
|
// change the current directory to basename in process dir
|
|
$cwd = getcwd();
|
|
chdir($this->getProcessDir().DIRECTORY_SEPARATOR.$this->getBasename().DIRECTORY_SEPARATOR);
|
|
|
|
// zip the file into the archive
|
|
$cmd = $this->getZipBinary();
|
|
$cmd.= ' -u';
|
|
$cmd.= ' '.escapeshellarg($this->getPathname());
|
|
$cmd.= ' '.escapeshellarg($archivePathname);
|
|
exec($cmd);
|
|
|
|
// get back current directory
|
|
chdir($cwd);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This function is obsolete, use parameter 'image' to merge pictures.
|
|
*
|
|
* Hack to change in the XML source the tags for image. Only tags with [*.src] work.
|
|
* The TBS tag need to set with OpenOffice into the title field of the image property.
|
|
*
|
|
* Experimental : only for OpenDocument/OpenOffice. Image in blocks don't work with spreadsheet.
|
|
*/
|
|
public function tagXmlImage()
|
|
{
|
|
$doc = new DOMDocument();
|
|
$doc->loadXML($this->Source);
|
|
$draw_frames = $doc->getElementsByTagName('frame'); // draw:frame
|
|
|
|
foreach ($draw_frames as $draw_frame)
|
|
{
|
|
$svg_titles = $draw_frame->getElementsByTagName('title'); //svg:title
|
|
if ($svg_titles->length > 0)
|
|
{
|
|
$tag = $svg_titles->item(0)->nodeValue;
|
|
if (preg_match('/\[.*src\]/', $tag))
|
|
{
|
|
$draw_images = $draw_frame->getElementsByTagName('image'); // draw:image
|
|
if ($draw_images->length > 0)
|
|
{
|
|
$draw_image = $draw_images->item(0);
|
|
$draw_image->removeAttribute('href'); // xlink:href
|
|
$draw_image->setAttribute('xlink:href', $tag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$this->Source = $doc->saveXML();
|
|
}
|
|
|
|
/**
|
|
* Send the response
|
|
*/
|
|
public function sendResponse($options = array())
|
|
{
|
|
header('Content-Type: '.$this->getMimetype());
|
|
header('Content-Disposition: attachment; filename="'.$this->getDownloadFilename().'"');
|
|
header('Content-Length: '.$this->getSize());
|
|
echo $this->getContent();
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the basename of the current process file
|
|
*
|
|
* @return string The basename
|
|
*/
|
|
public function getBasename()
|
|
{
|
|
return $this->processBasename;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the callback method to encode data
|
|
*
|
|
* @return string The callback method
|
|
*/
|
|
public function getCallback()
|
|
{
|
|
return $this->callback;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the charset of data to be merged
|
|
*
|
|
* @return string The charset
|
|
*/
|
|
public function getCharset()
|
|
{
|
|
return $this->charset;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the binary content of the current process file
|
|
*
|
|
* @return stream The binary content
|
|
*/
|
|
public function getContent()
|
|
{
|
|
return file_get_contents($this->getPathname(), false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the default callback method to encode data
|
|
*
|
|
* @return string The default callback method
|
|
*/
|
|
public function getDefaultCallback()
|
|
{
|
|
return $this->defaultCallback;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the default charset of data
|
|
*
|
|
* @return string The default charset
|
|
*/
|
|
public function getDefaultCharset()
|
|
{
|
|
return $this->defaultCharset;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the default extension of source file
|
|
*
|
|
* @return string The default extension
|
|
*/
|
|
public function getDefaultExtension()
|
|
{
|
|
return $this->defaultExtension;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the default if data are escaped
|
|
*
|
|
* @return string The default
|
|
*/
|
|
public function getDefaultIsEscape()
|
|
{
|
|
return $this->defaultIsEscape;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the filename to download as the current template by default
|
|
*
|
|
* @return string The download filename
|
|
*/
|
|
public function getDownloadFilename($options = array())
|
|
{
|
|
return basename($this->getSourcePathname());
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the extension of the current process file
|
|
*
|
|
* @return string The extension
|
|
*/
|
|
public function getExtension()
|
|
{
|
|
$info = pathinfo($this->getSourcePathname());
|
|
|
|
return $info['extension'];
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the filename of the current process file
|
|
*
|
|
* @return string The filename
|
|
*/
|
|
public function getFilename()
|
|
{
|
|
return $this->getBasename().'.'.$this->getExtension();
|
|
}
|
|
|
|
|
|
/**
|
|
* Get if data are escaped
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function getIsEscape()
|
|
{
|
|
return $this->isEscape;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the mimetype of the current process file
|
|
*
|
|
* @return string The mimetype
|
|
*/
|
|
public function getMimetype()
|
|
{
|
|
return isset($this->mimetype[$this->getExtension()]) ? $this->mimetype[$this->getExtension()] : null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the pathname of the current process file
|
|
*
|
|
* @return string The pathname
|
|
*/
|
|
public function getPathname()
|
|
{
|
|
return $this->getProcessDir().DIRECTORY_SEPARATOR.$this->getFilename();
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the process directory
|
|
*
|
|
* @return string The process dir pathname
|
|
*/
|
|
public function getProcessDir()
|
|
{
|
|
return $this->processDir;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the size of the current process file
|
|
*
|
|
* @return int The size
|
|
*/
|
|
public function getSize()
|
|
{
|
|
return filesize($this->getPathname());
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the source file pathname
|
|
*
|
|
* @return string The source file pathname
|
|
*/
|
|
public function getSourcePathname()
|
|
{
|
|
return $this->sourcePathname;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the unzip binary pathfile
|
|
*
|
|
* @return string The unzip binary
|
|
*/
|
|
public function getUnzipBinary()
|
|
{
|
|
return $this->unzipBin;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the current XML filename
|
|
*
|
|
* @return string The XML filename
|
|
*/
|
|
public function getXmlFilename()
|
|
{
|
|
return $this->xmlFilename;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the zip binary pathfile
|
|
*
|
|
* @return string The zip binary
|
|
*/
|
|
public function getZipBinary()
|
|
{
|
|
return $this->zipBin;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the method name to zip/unzip
|
|
*
|
|
* @return string The zip method name
|
|
*/
|
|
public function getZipMethod()
|
|
{
|
|
return $this->zipMethod;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the basename of the current process file
|
|
*
|
|
* @param string $basename The basename
|
|
*/
|
|
public function setBasename($basename)
|
|
{
|
|
return $this->processBasename = $basename;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the callback method to encode merged data
|
|
*
|
|
* @param string $callback The callback method (TBS syntax)
|
|
*/
|
|
public function setCallback($callback)
|
|
{
|
|
$this->callback = $callback;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the charset
|
|
*
|
|
* @param string $charset The charset of the data to merge (utf-8 by default)
|
|
*/
|
|
public function setCharset($charset = 'UTF-8')
|
|
{
|
|
$this->charset = strtoupper($charset);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set if '<' & '>' in merged data are escaped by '<' & '>'
|
|
*
|
|
* BE CAREFUL TO SET TO FALSE
|
|
*
|
|
* @param boolean $isEscape Escape data (true by default)
|
|
*/
|
|
public function setIsEscape($isEscape = true)
|
|
{
|
|
$this->isEscape = $isEscape;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the source file pathname
|
|
*
|
|
* @param mixed $options The sourcePathname
|
|
*/
|
|
public function setSourcePathname($options = array())
|
|
{
|
|
$sourcePathname = $options;
|
|
|
|
// test if file exist
|
|
if (!file_exists($sourcePathname))
|
|
{
|
|
throw new tinyDocException(sprintf('File not found "%s"', $sourcePathname));
|
|
}
|
|
|
|
// test if file readable
|
|
if (!is_readable($sourcePathname))
|
|
{
|
|
throw new tinyDocException(sprintf('File not readable "%s"', $sourcePathname));
|
|
}
|
|
|
|
$this->sourcePathname = $sourcePathname;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the process directory
|
|
*
|
|
* @param string $processDir The pathname of the process dir
|
|
*/
|
|
public function setProcessDir($processDir = 'tmp')
|
|
{
|
|
// changed by kevin - makes no sense on shared hosting.
|
|
// for example open basedir settings restrict access to /
|
|
// if (realpath($processDir) == realpath(DIRECTORY_SEPARATOR))
|
|
if (realpath($processDir) == DIRECTORY_SEPARATOR)
|
|
{
|
|
throw new tinyDocException(sprintf('Could not use the root for the process directory "%s"', $processDir));
|
|
}
|
|
if (!is_dir(realpath($processDir)))
|
|
{
|
|
throw new tinyDocException(sprintf('Process directory not found "%s"', $processDir));
|
|
}
|
|
if (!is_writable(realpath($processDir)))
|
|
{
|
|
throw new tinyDocException(sprintf('Process directory not writable "%s"', $processDir));
|
|
}
|
|
|
|
$this->processDir = rtrim(realpath($processDir), DIRECTORY_SEPARATOR);
|
|
}
|
|
|
|
|
|
/**
|
|
* Fix the unzip binary
|
|
*
|
|
* @param string $unzipBin The pathname of the unzip binary (unzip by default)
|
|
*/
|
|
public function setUnzipBinary($unzipBinary = 'unzip')
|
|
{
|
|
if ($this->getZipMethod() == 'shell')
|
|
{
|
|
$unzipBinary = self::escapeShellCommand($unzipBinary);
|
|
|
|
if (strlen(shell_exec($unzipBinary.' -h')) == 0)
|
|
{
|
|
throw new tinyDocException(sprintf('"%s" not executable', $unzipBinary));
|
|
}
|
|
|
|
$this->unzipBin = $unzipBinary;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the XML filename of the current template
|
|
*
|
|
* @param string The pathname of XML filename (content.xml by default)
|
|
*/
|
|
public function setXmlFilename($xmlFilename = 'content.xml')
|
|
{
|
|
$this->xmlFilename = $xmlFilename;
|
|
}
|
|
|
|
|
|
/**
|
|
* Fix the zip binary
|
|
*
|
|
* @param string $zipBin The pathname of the zip binary (zip by default)
|
|
*/
|
|
public function setZipBinary($zipBinary = 'zip')
|
|
{
|
|
if ($this->getZipMethod() == 'shell')
|
|
{
|
|
$zipBinary = self::escapeShellCommand($zipBinary);
|
|
|
|
if (strlen(shell_exec($zipBinary.' -h')) == 0)
|
|
{
|
|
throw new tinyDocException(sprintf('"%s" not executable', $zipBinary));
|
|
}
|
|
|
|
$this->zipBin = $zipBinary;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Fix the method to zip/unzip file.
|
|
*
|
|
* @param string $zipMethod The method are 'shell' or 'ziparchive' (shell by default)
|
|
*/
|
|
public function setZipMethod($zipMethod = 'shell')
|
|
{
|
|
$method = strtolower($zipMethod);
|
|
|
|
if (!in_array($method, array('shell', 'ziparchive', 'pclzip')))
|
|
{
|
|
throw new tinyDocException(sprintf('Zip method "%s" need to be \'shell\' or \'ziparchive\'', $method));
|
|
}
|
|
|
|
if ($method == 'ziparchive' && !class_exists('ZipArchive'))
|
|
{
|
|
throw new tinyDocException('Zip extension not loaded - check your php settings, PHP 5.2 minimum with zip extension is required');
|
|
}
|
|
|
|
$this->zipMethod = $method;
|
|
}
|
|
|
|
|
|
/**
|
|
* Redefinition of TBS method to make an exception when an alert occured
|
|
*
|
|
* @param string $Src TBS subject ???
|
|
* @param string $Msg TBS message ???
|
|
* @param string $NoErrMsg TBS no error message ???
|
|
* @param string $SrcType The TBS source type ???
|
|
*/
|
|
public function meth_Misc_Alert($Src, $Msg, $NoErrMsg=false, $SrcType=false)
|
|
{
|
|
throw new tinyDocException(sprintf('%s', $Msg));
|
|
}
|
|
|
|
|
|
/**
|
|
* Redefinition of TBS method to add parameter 'cell' to set value with native OpenDocument format in spreadsheet
|
|
*
|
|
* refactoring code from egroupware/perp_api/inc/report/ooo.inc.php
|
|
*
|
|
* @param string $Txt TBS source
|
|
* @param string $Loc TBS locator
|
|
* @param string $Value TBS data to merge
|
|
* @param string $CheckSub TBS checksub ???
|
|
*/
|
|
public function meth_Locator_Replace(&$Txt, &$Loc, &$Value, $CheckSub)
|
|
{
|
|
|
|
if (!isset($Loc->PrmLst['type']) && !isset($Loc->PrmLst['image']) && !isset($Loc->PrmLst['link']))
|
|
{
|
|
return parent::meth_Locator_Replace($Txt, $Loc, $Value, $CheckSub);
|
|
}
|
|
|
|
// keep 'Loc' position for the end
|
|
$posBeg = $Loc->PosBeg;
|
|
$posEnd = $Loc->PosEnd;
|
|
|
|
// get data
|
|
$data = isset($Value) ? $Value : null;
|
|
foreach($Loc->SubLst as $sub)
|
|
{
|
|
$data = isset($Value[$sub]) ? $Value[$sub] : null ;
|
|
}
|
|
|
|
// ----- parameter = type
|
|
if (isset($Loc->PrmLst['type']))
|
|
{
|
|
if ($data == '')
|
|
{
|
|
$Txt = substr_replace($Txt, '', $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1 );
|
|
$Loc->PosBeg = $posBeg;
|
|
$Loc->PosEnd = $posEnd;
|
|
return $Loc->PosBeg;
|
|
}
|
|
|
|
// get container enlarged to table:table-cell
|
|
$Loc->Enlarged = $this->f_Loc_EnlargeToStr($Txt, $Loc, '<table:table-cell' ,'/table:table-cell>');
|
|
$container = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1);
|
|
|
|
if ($container == '')
|
|
{
|
|
throw new tinyDocException(sprintf('<table:table-cell not found in document "%s"', $this->getXmlFilename()));
|
|
}
|
|
|
|
// OpenOffice attributes cell - see : http://books.evc-cit.info/odbook/ch05.html#table-cells-section
|
|
switch($Loc->PrmLst['type'])
|
|
{
|
|
case 'datetime':
|
|
case 'date':
|
|
case 'dt':
|
|
case 'd':
|
|
$attribute = sprintf('office:value-type="date" office:date-value="%s"', str_replace(' ', 'T', $data));
|
|
break;
|
|
|
|
case 'time':
|
|
case 't':
|
|
list($h, $m, $s) = split(":", $data);
|
|
$attribute = sprintf('office:value-type="time" office:time-value="PT%sH%sM%sS"', $h, $m, $s);
|
|
break;
|
|
|
|
case 'percentage':
|
|
case 'percent':
|
|
case 'p':
|
|
$attribute = sprintf('office:value-type="percentage" office:value="%s"', $data);
|
|
break;
|
|
|
|
case 'currency':
|
|
case 'cur':
|
|
case 'c':
|
|
//$attribute = sprintf('office:value-type="currency" office:currency="EUR" office:value="%s"', $data); // still not necessary to fix the currency
|
|
$attribute = sprintf('office:value-type="currency" office:value="%s"', $data);
|
|
break;
|
|
|
|
case 'number':
|
|
case 'n':
|
|
case 'float':
|
|
case 'f':
|
|
$attribute = sprintf('office:value-type="float" office:value="%s"', $data);
|
|
break;
|
|
|
|
case 'int':
|
|
case 'i':
|
|
$attribute = sprintf('office:value-type="float" office:value="%d"', $data);
|
|
break;
|
|
|
|
default:
|
|
case 'string':
|
|
case 's':
|
|
$attribute = sprintf('office:value-type="string"');
|
|
break;
|
|
}
|
|
|
|
// new container
|
|
$newContainer = preg_replace('/office:value-type="string"/', $attribute, $container);
|
|
|
|
// replace the new cell containter in the main Txt
|
|
$Txt = substr_replace($Txt, $newContainer, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1 );
|
|
|
|
// correct 'Loc' to include the change of the new cell container
|
|
$delta = strlen($newContainer) - strlen($container);
|
|
$Loc->PosBeg = $posBeg + $delta;
|
|
$Loc->PosEnd = $posEnd + $delta;
|
|
$Loc->Enlarged = null;
|
|
}
|
|
|
|
|
|
// ----- parameter = image
|
|
if (isset($Loc->PrmLst['image']))
|
|
{
|
|
// get container enlarged to draw:frame
|
|
$Loc->Enlarged = $this->f_Loc_EnlargeToStr($Txt, $Loc, '<draw:frame' ,'/draw:frame>');
|
|
$container = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1);
|
|
|
|
if ($container == '')
|
|
{
|
|
throw new tinyDocException(sprintf('<draw:frame not found in document "%s"', $this->getXmlFilename()));
|
|
}
|
|
|
|
// test if data is empty or if file not exists
|
|
if ($data == '' || !file_exists($data))
|
|
{
|
|
$Txt = substr_replace($Txt, '', $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1 );
|
|
$Loc->PosBeg = $posBeg;
|
|
$Loc->PosEnd = $posEnd;
|
|
$Loc->Enlarged = null;
|
|
return $Loc->PosBeg;
|
|
}
|
|
|
|
$picture = 'Pictures/'.basename($data);
|
|
|
|
// image size
|
|
$size = @getimagesize($data);
|
|
if ($size === false)
|
|
{
|
|
throw new tinyDocException(sprintf('Invalid image format "%"', $data));
|
|
}
|
|
else
|
|
{
|
|
list ($width, $height) = $size;
|
|
}
|
|
|
|
|
|
// image ratio
|
|
$ratio = 1;
|
|
|
|
switch(strtolower($Loc->PrmLst['image']))
|
|
{
|
|
case 'fit':
|
|
if (preg_match('/svg:width="(.*?)(cm|in|mm|px)" svg:height="(.*?)(cm|in|mm|px)"/', $container, $matches))
|
|
{
|
|
$ratio_w = self::convertToCm($matches[1], $matches[2]) / self::convertToCm($width, 'px');
|
|
$ratio_h = self::convertToCm($matches[3], $matches[4]) / self::convertToCm($height,'px');
|
|
$ratio = min($ratio_w, $ratio_h);
|
|
}
|
|
break;
|
|
case 'max':
|
|
if (preg_match('/svg:width="(.*?)(cm|in|mm|px)" svg:height="(.*?)(cm|in|mm|px)"/', $container, $matches))
|
|
{
|
|
$ratio_w = self::convertToCm($matches[1], $matches[2]) / self::convertToCm($width, 'px');
|
|
$ratio_h = self::convertToCm($matches[3], $matches[4]) / self::convertToCm($height,'px');
|
|
$ratio = min(1, $ratio_w, $ratio_h);
|
|
}
|
|
break;
|
|
default:
|
|
if (preg_match('/([0-9\.]*)%/', $Loc->PrmLst['image'], $matches) > 0)
|
|
{
|
|
$ratio = $matches[1] / 100;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// replace values
|
|
$newContainer = $container;
|
|
$newContainer = preg_replace('/svg:width="(.*?)"/' , sprintf('svg:width="%scm"' , self::convertToCm($width, 'px') * $ratio), $newContainer);
|
|
$newContainer = preg_replace('/svg:height="(.*?)"/', sprintf('svg:height="%scm"', self::convertToCm($height,'px') * $ratio), $newContainer);
|
|
$newContainer = preg_replace('/xlink:href="(.*?)"/', sprintf('xlink:href="%s"' , $picture), $newContainer);
|
|
|
|
// add file
|
|
$this->addFile($data, $picture);
|
|
|
|
// replace the new cell containter in the main Txt
|
|
$Txt = substr_replace($Txt, $newContainer, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1);
|
|
|
|
$Loc->PosBeg = $posBeg;
|
|
$Loc->PosEnd = $posEnd;
|
|
$Loc->Enlarged = null;
|
|
}
|
|
|
|
|
|
// ----- parameter = link
|
|
if (isset($Loc->PrmLst['link']))
|
|
{
|
|
if ($data == '')
|
|
{
|
|
$Txt = substr_replace($Txt, '', $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1 );
|
|
$Loc->PosBeg = $posBeg;
|
|
$Loc->PosEnd = $posEnd;
|
|
return $Loc->PosBeg;
|
|
}
|
|
|
|
$container = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1);
|
|
$title = ($Loc->PrmLst['link'] != '1' ? $Loc->PrmLst['link'] : $data);
|
|
$newContainer = sprintf('<text:a xlink:type="simple" xlink:href="%s">%s</text:a>', $data, $title);
|
|
|
|
$Txt = substr_replace($Txt, $newContainer, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1);
|
|
|
|
// before return, restore 'Loc' with beginning values (to work with block)
|
|
$Loc->PosBeg = $posBeg;
|
|
$Loc->PosEnd = $posEnd;
|
|
return $Loc->PosEnd;
|
|
}
|
|
|
|
// call the parent method to insert the value
|
|
$ret = parent::meth_Locator_Replace($Txt, $Loc, $Value, $CheckSub);
|
|
|
|
// before return, restore 'Loc' with beginning values (to work with block)
|
|
$Loc->PosBeg = $posBeg;
|
|
$Loc->PosEnd = $posEnd;
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
public static function convertToCm($value, $unit)
|
|
{
|
|
switch(strtolower($unit))
|
|
{
|
|
case 'px':
|
|
$returnValue = $value * self::INCH_TO_CM / self::PICTURE_DPI;
|
|
break;
|
|
case 'in':
|
|
$returnValue = $value * self::INCH_TO_CM;
|
|
break;
|
|
case 'mm':
|
|
$returnValue = $value / 10;
|
|
break;
|
|
default;
|
|
case 'cm':
|
|
$returnValue = $value;
|
|
break;
|
|
}
|
|
|
|
return $returnValue;
|
|
}
|
|
|
|
|
|
public static function escapeShellCommand($command)
|
|
{
|
|
if (strpos($command, ' ') !== false)
|
|
{
|
|
$command = (strpos($command, '"') === 0 ? '' : '"').$command;
|
|
$command = $command.((strrpos($command, '"') == strlen($command)-1) ? '' : '"');
|
|
}
|
|
return $command;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* tinyDocException exception class
|
|
*
|
|
* @package tinyDoc
|
|
* @subpackage tinyDocException
|
|
* @author Olivier Loynet <tinydoc@googlegroups.com>
|
|
* @version $Id$
|
|
*/
|
|
class tinyDocException extends Exception
|
|
{
|
|
}
|