Update 2 Template processor setValue() improvements #614

This commit is contained in:
kazi Tanvir Ahsan 2015-09-10 23:50:23 +10:00
parent 873d41a872
commit b446a23b61

View File

@ -1,5 +1,4 @@
<?php <?php
/** /**
* This file is part of PHPWord - A pure PHP library for reading and writing * This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents. * word processing documents.
@ -26,41 +25,41 @@ use PhpOffice\PhpWord\Shared\ZipArchive;
class TemplateProcessor class TemplateProcessor
{ {
const MAXIMUM_REPLACEMENTS_DEFAULT = - 1; const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
/** /**
* ZipArchive object. * ZipArchive object.
* *
* @var mixed * @var mixed
*/ */
protected $zipClass; protected $zipClass;
/** /**
* @var string Temporary document filename (with path). * @var string Temporary document filename (with path).
*/ */
protected $tempDocumentFilename; protected $tempDocumentFilename;
/** /**
* Content of main document part (in XML format) of the temporary document. * Content of main document part (in XML format) of the temporary document.
* *
* @var string * @var string
*/ */
protected $tempDocumentMainPart; protected $tempDocumentMainPart;
/** /**
* Content of headers (in XML format) of the temporary document. * Content of headers (in XML format) of the temporary document.
* *
* @var string[] * @var string[]
*/ */
protected $tempDocumentHeaders = array(); protected $tempDocumentHeaders = array();
/** /**
* Content of footers (in XML format) of the temporary document. * Content of footers (in XML format) of the temporary document.
* *
* @var string[] * @var string[]
*/ */
protected $tempDocumentFooters = array(); protected $tempDocumentFooters = array();
/** /**
* @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception. * @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception.
* *
@ -69,35 +68,39 @@ class TemplateProcessor
* @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException * @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException
* @throws \PhpOffice\PhpWord\Exception\CopyFileException * @throws \PhpOffice\PhpWord\Exception\CopyFileException
*/ */
public function __construct($documentTemplate) { public function __construct($documentTemplate)
{
// Temporary document filename initialization // Temporary document filename initialization
$this->tempDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord'); $this->tempDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord');
if (false === $this->tempDocumentFilename) { if (false === $this->tempDocumentFilename) {
throw new CreateTemporaryFileException(); throw new CreateTemporaryFileException();
} }
// Template file cloning // Template file cloning
if (false === copy($documentTemplate, $this->tempDocumentFilename)) { if (false === copy($documentTemplate, $this->tempDocumentFilename)) {
throw new CopyFileException($documentTemplate, $this->tempDocumentFilename); throw new CopyFileException($documentTemplate, $this->tempDocumentFilename);
} }
// Temporary document content extraction // Temporary document content extraction
$this->zipClass = new ZipArchive(); $this->zipClass = new ZipArchive();
$this->zipClass->open($this->tempDocumentFilename); $this->zipClass->open($this->tempDocumentFilename);
$index = 1; $index = 1;
while (false !== $this->zipClass->locateName($this->getHeaderName($index))) { while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
$this->tempDocumentHeaders[$index] = $this->fixBrokenMacros($this->zipClass->getFromName($this->getHeaderName($index))); $this->tempDocumentHeaders[$index] = $this->fixBrokenMacros(
$this->zipClass->getFromName($this->getHeaderName($index))
);
$index++; $index++;
} }
$index = 1; $index = 1;
while (false !== $this->zipClass->locateName($this->getFooterName($index))) { while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
$this->tempDocumentFooters[$index] = $this->fixBrokenMacros($this->zipClass->getFromName($this->getFooterName($index))); $this->tempDocumentFooters[$index] = $this->fixBrokenMacros(
$this->zipClass->getFromName($this->getFooterName($index))
);
$index++; $index++;
} }
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml')); $this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml'));
} }
/** /**
* Applies XSL style sheet to template's parts. * Applies XSL style sheet to template's parts.
* *
@ -109,28 +112,29 @@ class TemplateProcessor
* *
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws \PhpOffice\PhpWord\Exception\Exception
*/ */
public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '') { public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '')
{
$xsltProcessor = new \XSLTProcessor(); $xsltProcessor = new \XSLTProcessor();
$xsltProcessor->importStylesheet($xslDOMDocument); $xsltProcessor->importStylesheet($xslDOMDocument);
if (false === $xsltProcessor->setParameter($xslOptionsURI, $xslOptions)) { if (false === $xsltProcessor->setParameter($xslOptionsURI, $xslOptions)) {
throw new Exception('Could not set values for the given XSL style sheet parameters.'); throw new Exception('Could not set values for the given XSL style sheet parameters.');
} }
$xmlDOMDocument = new \DOMDocument(); $xmlDOMDocument = new \DOMDocument();
if (false === $xmlDOMDocument->loadXML($this->tempDocumentMainPart)) { if (false === $xmlDOMDocument->loadXML($this->tempDocumentMainPart)) {
throw new Exception('Could not load XML from the given template.'); throw new Exception('Could not load XML from the given template.');
} }
$xmlTransformed = $xsltProcessor->transformToXml($xmlDOMDocument); $xmlTransformed = $xsltProcessor->transformToXml($xmlDOMDocument);
if (false === $xmlTransformed) { if (false === $xmlTransformed) {
throw new Exception('Could not transform the given XML document.'); throw new Exception('Could not transform the given XML document.');
} }
$this->tempDocumentMainPart = $xmlTransformed; $this->tempDocumentMainPart = $xmlTransformed;
} }
/** /**
* @param mixed $macro * @param mixed $macro
* @param mixed $replace * @param mixed $replace
@ -138,45 +142,47 @@ class TemplateProcessor
* *
* @return void * @return void
*/ */
public function setValue($macro, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT) { public function setValue($macro, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT)
{
if (substr($macro, 0, 2) !== '${' && substr($macro, -1) !== '}') { if (substr($macro, 0, 2) !== '${' && substr($macro, -1) !== '}') {
$macro = '${' . $macro . '}'; $macro = '${' . $macro . '}';
} }
if (!String::isUTF8($replace)) { if (!String::isUTF8($replace)) {
$replace = utf8_encode($replace); $replace = utf8_encode($replace);
} }
foreach ($this->tempDocumentHeaders as $index => $headerXML) { foreach ($this->tempDocumentHeaders as $index => $headerXML) {
$this->tempDocumentHeaders[$index] = $this->setValueForPart($this->tempDocumentHeaders[$index], $macro, $replace, $limit); $this->tempDocumentHeaders[$index] = $this->setValueForPart($this->tempDocumentHeaders[$index], $macro, $replace, $limit);
} }
$this->tempDocumentMainPart = $this->setValueForPart($this->tempDocumentMainPart, $macro, $replace, $limit); $this->tempDocumentMainPart = $this->setValueForPart($this->tempDocumentMainPart, $macro, $replace, $limit);
foreach ($this->tempDocumentFooters as $index => $headerXML) { foreach ($this->tempDocumentFooters as $index => $headerXML) {
$this->tempDocumentFooters[$index] = $this->setValueForPart($this->tempDocumentFooters[$index], $macro, $replace, $limit); $this->tempDocumentFooters[$index] = $this->setValueForPart($this->tempDocumentFooters[$index], $macro, $replace, $limit);
} }
} }
/** /**
* Returns array of all variables in template. * Returns array of all variables in template.
* *
* @return string[] * @return string[]
*/ */
public function getVariables() { public function getVariables()
{
$variables = $this->getVariablesForPart($this->tempDocumentMainPart); $variables = $this->getVariablesForPart($this->tempDocumentMainPart);
foreach ($this->tempDocumentHeaders as $headerXML) { foreach ($this->tempDocumentHeaders as $headerXML) {
$variables = array_merge($variables, $this->getVariablesForPart($headerXML)); $variables = array_merge($variables, $this->getVariablesForPart($headerXML));
} }
foreach ($this->tempDocumentFooters as $footerXML) { foreach ($this->tempDocumentFooters as $footerXML) {
$variables = array_merge($variables, $this->getVariablesForPart($footerXML)); $variables = array_merge($variables, $this->getVariablesForPart($footerXML));
} }
return array_unique($variables); return array_unique($variables);
} }
/** /**
* Clone a table row in a template document. * Clone a table row in a template document.
* *
@ -187,55 +193,55 @@ class TemplateProcessor
* *
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws \PhpOffice\PhpWord\Exception\Exception
*/ */
public function cloneRow($search, $numberOfClones) { public function cloneRow($search, $numberOfClones)
{
if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) { if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) {
$search = '${' . $search . '}'; $search = '${' . $search . '}';
} }
$tagPos = strpos($this->tempDocumentMainPart, $search); $tagPos = strpos($this->tempDocumentMainPart, $search);
if (!$tagPos) { if (!$tagPos) {
throw new Exception("Can not clone row, template variable not found or variable contains markup."); throw new Exception("Can not clone row, template variable not found or variable contains markup.");
} }
$rowStart = $this->findRowStart($tagPos); $rowStart = $this->findRowStart($tagPos);
$rowEnd = $this->findRowEnd($tagPos); $rowEnd = $this->findRowEnd($tagPos);
$xmlRow = $this->getSlice($rowStart, $rowEnd); $xmlRow = $this->getSlice($rowStart, $rowEnd);
// Check if there's a cell spanning multiple rows. // Check if there's a cell spanning multiple rows.
if (preg_match('#<w:vMerge w:val="restart"/>#', $xmlRow)) { if (preg_match('#<w:vMerge w:val="restart"/>#', $xmlRow)) {
// $extraRowStart = $rowEnd; // $extraRowStart = $rowEnd;
$extraRowEnd = $rowEnd; $extraRowEnd = $rowEnd;
while (true) { while (true) {
$extraRowStart = $this->findRowStart($extraRowEnd + 1); $extraRowStart = $this->findRowStart($extraRowEnd + 1);
$extraRowEnd = $this->findRowEnd($extraRowEnd + 1); $extraRowEnd = $this->findRowEnd($extraRowEnd + 1);
// If extraRowEnd is lower then 7, there was no next row found. // If extraRowEnd is lower then 7, there was no next row found.
if ($extraRowEnd < 7) { if ($extraRowEnd < 7) {
break; break;
} }
// If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row. // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row.
$tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd); $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd);
if (!preg_match('#<w:vMerge/>#', $tmpXmlRow) && !preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)) { if (!preg_match('#<w:vMerge/>#', $tmpXmlRow) &&
!preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)) {
break; break;
} }
// This row was a spanned row, update $rowEnd and search for the next row. // This row was a spanned row, update $rowEnd and search for the next row.
$rowEnd = $extraRowEnd; $rowEnd = $extraRowEnd;
} }
$xmlRow = $this->getSlice($rowStart, $rowEnd); $xmlRow = $this->getSlice($rowStart, $rowEnd);
} }
$result = $this->getSlice(0, $rowStart); $result = $this->getSlice(0, $rowStart);
for ($i = 1; $i <= $numberOfClones; $i++) { for ($i = 1; $i <= $numberOfClones; $i++) {
$result.= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow); $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
} }
$result.= $this->getSlice($rowEnd); $result .= $this->getSlice($rowEnd);
$this->tempDocumentMainPart = $result; $this->tempDocumentMainPart = $result;
} }
/** /**
* Clone a block. * Clone a block.
* *
@ -245,25 +251,34 @@ class TemplateProcessor
* *
* @return string|null * @return string|null
*/ */
public function cloneBlock($blockname, $clones = 1, $replace = true) { public function cloneBlock($blockname, $clones = 1, $replace = true)
{
$xmlBlock = null; $xmlBlock = null;
preg_match('/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is', $this->tempDocumentMainPart, $matches); preg_match(
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
$this->tempDocumentMainPart,
$matches
);
if (isset($matches[3])) { if (isset($matches[3])) {
$xmlBlock = $matches[3]; $xmlBlock = $matches[3];
$cloned = array(); $cloned = array();
for ($i = 1; $i <= $clones; $i++) { for ($i = 1; $i <= $clones; $i++) {
$cloned[] = $xmlBlock; $cloned[] = $xmlBlock;
} }
if ($replace) { if ($replace) {
$this->tempDocumentMainPart = str_replace($matches[2] . $matches[3] . $matches[4], implode('', $cloned), $this->tempDocumentMainPart); $this->tempDocumentMainPart = str_replace(
$matches[2] . $matches[3] . $matches[4],
implode('', $cloned),
$this->tempDocumentMainPart
);
} }
} }
return $xmlBlock; return $xmlBlock;
} }
/** /**
* Replace a block. * Replace a block.
* *
@ -272,14 +287,23 @@ class TemplateProcessor
* *
* @return void * @return void
*/ */
public function replaceBlock($blockname, $replacement) { public function replaceBlock($blockname, $replacement)
preg_match('/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is', $this->tempDocumentMainPart, $matches); {
preg_match(
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
$this->tempDocumentMainPart,
$matches
);
if (isset($matches[3])) { if (isset($matches[3])) {
$this->tempDocumentMainPart = str_replace($matches[2] . $matches[3] . $matches[4], $replacement, $this->tempDocumentMainPart); $this->tempDocumentMainPart = str_replace(
$matches[2] . $matches[3] . $matches[4],
$replacement,
$this->tempDocumentMainPart
);
} }
} }
/** /**
* Delete a block of text. * Delete a block of text.
* *
@ -287,10 +311,11 @@ class TemplateProcessor
* *
* @return void * @return void
*/ */
public function deleteBlock($blockname) { public function deleteBlock($blockname)
{
$this->replaceBlock($blockname, ''); $this->replaceBlock($blockname, '');
} }
/** /**
* Saves the result document. * Saves the result document.
* *
@ -298,25 +323,26 @@ class TemplateProcessor
* *
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws \PhpOffice\PhpWord\Exception\Exception
*/ */
public function save() { public function save()
{
foreach ($this->tempDocumentHeaders as $index => $headerXML) { foreach ($this->tempDocumentHeaders as $index => $headerXML) {
$this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]); $this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]);
} }
$this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart); $this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart);
foreach ($this->tempDocumentFooters as $index => $headerXML) { foreach ($this->tempDocumentFooters as $index => $headerXML) {
$this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]); $this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]);
} }
// Close zip file // Close zip file
if (false === $this->zipClass->close()) { if (false === $this->zipClass->close()) {
throw new Exception('Could not close zip file.'); throw new Exception('Could not close zip file.');
} }
return $this->tempDocumentFilename; return $this->tempDocumentFilename;
} }
/** /**
* Saves the result document to the user defined file. * Saves the result document to the user defined file.
* *
@ -326,23 +352,24 @@ class TemplateProcessor
* *
* @return void * @return void
*/ */
public function saveAs($fileName) { public function saveAs($fileName)
{
$tempFileName = $this->save(); $tempFileName = $this->save();
if (file_exists($fileName)) { if (file_exists($fileName)) {
unlink($fileName); unlink($fileName);
} }
/* /*
* Note: we do not use ``rename`` function here, because it looses file ownership data on Windows platform. * Note: we do not use ``rename`` function here, because it looses file ownership data on Windows platform.
* As a result, user cannot open the file directly getting "Access denied" message. * As a result, user cannot open the file directly getting "Access denied" message.
* *
* @see https://github.com/PHPOffice/PHPWord/issues/532 * @see https://github.com/PHPOffice/PHPWord/issues/532
*/ */
copy($tempFileName, $fileName); copy($tempFileName, $fileName);
unlink($tempFileName); unlink($tempFileName);
} }
/** /**
* Finds parts of broken macros and sticks them together. * Finds parts of broken macros and sticks them together.
* Macros, while being edited, could be implicitly broken by some of the word processors. * Macros, while being edited, could be implicitly broken by some of the word processors.
@ -353,39 +380,43 @@ class TemplateProcessor
* *
* @return string * @return string
*/ */
protected function fixBrokenMacros($documentPart) { protected function fixBrokenMacros($documentPart)
{
$fixedDocumentPart = $documentPart; $fixedDocumentPart = $documentPart;
$fixedDocumentPart = preg_replace_callback('|\$\{([^\}]+)\}|U', function ($match) { $fixedDocumentPart = preg_replace_callback(
return strip_tags($match[0]); '|\$\{([^\}]+)\}|U',
}, $fixedDocumentPart); function ($match) {
return strip_tags($match[0]);
},
$fixedDocumentPart
);
return $fixedDocumentPart; return $fixedDocumentPart;
} }
/** /**
* Find and replace macros in the given XML section. * Find and replace macros in the given XML section.
* *
* @param string $documentPartXML * @param string $documentPartXML
* @param string $searchP * @param string $search
* @param string $replace * @param string $replace
* @param integer $limit * @param integer $limit
* *
* @return string * @return string
*/ */
protected function setValueForPart($documentPartXML, $search, $replace, $limit) { protected function setValueForPart($documentPartXML, $search, $replace, $limit)
{
// Note: we can't use the same function for both cases here, because of performance considerations. // Note: we can't use the same function for both cases here, because of performance considerations.
if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) { if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) {
return str_replace($search, $replace, $documentPartXML); return str_replace($search, $replace, $documentPartXML);
} } else {
else {
$regExpDelim = '/'; $regExpDelim = '/';
$escapedSearch = preg_quote($search, $regExpDelim); $escapedSearch = preg_quote($search, $regExpDelim);
return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit); return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit);
} }
} }
/** /**
* Find all variables in $documentPartXML. * Find all variables in $documentPartXML.
* *
@ -393,12 +424,13 @@ class TemplateProcessor
* *
* @return string[] * @return string[]
*/ */
protected function getVariablesForPart($documentPartXML) { protected function getVariablesForPart($documentPartXML)
{
preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
return $matches[1]; return $matches[1];
} }
/** /**
* Get the name of the footer file for $index. * Get the name of the footer file for $index.
* *
@ -406,10 +438,11 @@ class TemplateProcessor
* *
* @return string * @return string
*/ */
protected function getFooterName($index) { protected function getFooterName($index)
{
return sprintf('word/footer%d.xml', $index); return sprintf('word/footer%d.xml', $index);
} }
/** /**
* Get the name of the header file for $index. * Get the name of the header file for $index.
* *
@ -417,10 +450,11 @@ class TemplateProcessor
* *
* @return string * @return string
*/ */
protected function getHeaderName($index) { protected function getHeaderName($index)
{
return sprintf('word/header%d.xml', $index); return sprintf('word/header%d.xml', $index);
} }
/** /**
* Find the start position of the nearest table row before $offset. * Find the start position of the nearest table row before $offset.
* *
@ -430,19 +464,20 @@ class TemplateProcessor
* *
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws \PhpOffice\PhpWord\Exception\Exception
*/ */
protected function findRowStart($offset) { protected function findRowStart($offset)
{
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr ', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); $rowStart = strrpos($this->tempDocumentMainPart, '<w:tr ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
if (!$rowStart) { if (!$rowStart) {
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr>', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); $rowStart = strrpos($this->tempDocumentMainPart, '<w:tr>', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
} }
if (!$rowStart) { if (!$rowStart) {
throw new Exception('Can not find the start position of the row to clone.'); throw new Exception('Can not find the start position of the row to clone.');
} }
return $rowStart; return $rowStart;
} }
/** /**
* Find the end position of the nearest table row after $offset. * Find the end position of the nearest table row after $offset.
* *
@ -450,10 +485,11 @@ class TemplateProcessor
* *
* @return integer * @return integer
*/ */
protected function findRowEnd($offset) { protected function findRowEnd($offset)
{
return strpos($this->tempDocumentMainPart, '</w:tr>', $offset) + 7; return strpos($this->tempDocumentMainPart, '</w:tr>', $offset) + 7;
} }
/** /**
* Get a slice of a string. * Get a slice of a string.
* *
@ -462,11 +498,12 @@ class TemplateProcessor
* *
* @return string * @return string
*/ */
protected function getSlice($startPosition, $endPosition = 0) { protected function getSlice($startPosition, $endPosition = 0)
{
if (!$endPosition) { if (!$endPosition) {
$endPosition = strlen($this->tempDocumentMainPart); $endPosition = strlen($this->tempDocumentMainPart);
} }
return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition)); return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
} }
} }