From 873d41a8722d0101051c38ee04986100bb28da98 Mon Sep 17 00:00:00 2001 From: kazi Tanvir Ahsan Date: Thu, 10 Sep 2015 23:46:48 +1000 Subject: [PATCH] Update Template processor setValue() improvements #614 --- src/PhpWord/TemplateProcessor.php | 242 +++++++++++++----------------- 1 file changed, 102 insertions(+), 140 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index f5b934bed6..8d03201ecb 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -1,4 +1,5 @@ tempDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord'); if (false === $this->tempDocumentFilename) { throw new CreateTemporaryFileException(); } - + // Template file cloning if (false === copy($documentTemplate, $this->tempDocumentFilename)) { throw new CopyFileException($documentTemplate, $this->tempDocumentFilename); } - + // Temporary document content extraction $this->zipClass = new ZipArchive(); $this->zipClass->open($this->tempDocumentFilename); $index = 1; 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 = 1; 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++; } $this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml')); } - + /** * Applies XSL style sheet to template's parts. * @@ -112,29 +109,28 @@ public function __construct($documentTemplate) * * @throws \PhpOffice\PhpWord\Exception\Exception */ - public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '') - { + public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '') { $xsltProcessor = new \XSLTProcessor(); - + $xsltProcessor->importStylesheet($xslDOMDocument); - + if (false === $xsltProcessor->setParameter($xslOptionsURI, $xslOptions)) { throw new Exception('Could not set values for the given XSL style sheet parameters.'); } - + $xmlDOMDocument = new \DOMDocument(); if (false === $xmlDOMDocument->loadXML($this->tempDocumentMainPart)) { throw new Exception('Could not load XML from the given template.'); } - + $xmlTransformed = $xsltProcessor->transformToXml($xmlDOMDocument); if (false === $xmlTransformed) { throw new Exception('Could not transform the given XML document.'); } - + $this->tempDocumentMainPart = $xmlTransformed; } - + /** * @param mixed $macro * @param mixed $replace @@ -142,43 +138,45 @@ public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslO * * @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) !== '}') { $macro = '${' . $macro . '}'; } - + + if (!String::isUTF8($replace)) { + $replace = utf8_encode($replace); + } + foreach ($this->tempDocumentHeaders as $index => $headerXML) { $this->tempDocumentHeaders[$index] = $this->setValueForPart($this->tempDocumentHeaders[$index], $macro, $replace, $limit); } - + $this->tempDocumentMainPart = $this->setValueForPart($this->tempDocumentMainPart, $macro, $replace, $limit); - + foreach ($this->tempDocumentFooters as $index => $headerXML) { $this->tempDocumentFooters[$index] = $this->setValueForPart($this->tempDocumentFooters[$index], $macro, $replace, $limit); } } - + /** * Returns array of all variables in template. * * @return string[] */ - public function getVariables() - { + public function getVariables() { $variables = $this->getVariablesForPart($this->tempDocumentMainPart); - + foreach ($this->tempDocumentHeaders as $headerXML) { $variables = array_merge($variables, $this->getVariablesForPart($headerXML)); } - + foreach ($this->tempDocumentFooters as $footerXML) { $variables = array_merge($variables, $this->getVariablesForPart($footerXML)); } - + return array_unique($variables); } - + /** * Clone a table row in a template document. * @@ -189,55 +187,55 @@ public function getVariables() * * @throws \PhpOffice\PhpWord\Exception\Exception */ - public function cloneRow($search, $numberOfClones) - { + public function cloneRow($search, $numberOfClones) { if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) { $search = '${' . $search . '}'; } - + $tagPos = strpos($this->tempDocumentMainPart, $search); if (!$tagPos) { throw new Exception("Can not clone row, template variable not found or variable contains markup."); } - + $rowStart = $this->findRowStart($tagPos); $rowEnd = $this->findRowEnd($tagPos); $xmlRow = $this->getSlice($rowStart, $rowEnd); - + // Check if there's a cell spanning multiple rows. if (preg_match('##', $xmlRow)) { + // $extraRowStart = $rowEnd; $extraRowEnd = $rowEnd; while (true) { $extraRowStart = $this->findRowStart($extraRowEnd + 1); $extraRowEnd = $this->findRowEnd($extraRowEnd + 1); - + // If extraRowEnd is lower then 7, there was no next row found. if ($extraRowEnd < 7) { break; } - + // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row. $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd); - if (!preg_match('##', $tmpXmlRow) && - !preg_match('##', $tmpXmlRow)) { + if (!preg_match('##', $tmpXmlRow) && !preg_match('##', $tmpXmlRow)) { break; } + // This row was a spanned row, update $rowEnd and search for the next row. $rowEnd = $extraRowEnd; } $xmlRow = $this->getSlice($rowStart, $rowEnd); } - + $result = $this->getSlice(0, $rowStart); 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; } - + /** * Clone a block. * @@ -247,34 +245,25 @@ public function cloneRow($search, $numberOfClones) * * @return string|null */ - public function cloneBlock($blockname, $clones = 1, $replace = true) - { + public function cloneBlock($blockname, $clones = 1, $replace = true) { $xmlBlock = null; - preg_match( - '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', - $this->tempDocumentMainPart, - $matches - ); - + preg_match('/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, $matches); + if (isset($matches[3])) { $xmlBlock = $matches[3]; $cloned = array(); for ($i = 1; $i <= $clones; $i++) { $cloned[] = $xmlBlock; } - + 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; } - + /** * Replace a block. * @@ -283,23 +272,14 @@ public function cloneBlock($blockname, $clones = 1, $replace = true) * * @return void */ - public function replaceBlock($blockname, $replacement) - { - preg_match( - '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', - $this->tempDocumentMainPart, - $matches - ); - + public function replaceBlock($blockname, $replacement) { + preg_match('/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, $matches); + 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. * @@ -307,11 +287,10 @@ public function replaceBlock($blockname, $replacement) * * @return void */ - public function deleteBlock($blockname) - { + public function deleteBlock($blockname) { $this->replaceBlock($blockname, ''); } - + /** * Saves the result document. * @@ -319,26 +298,25 @@ public function deleteBlock($blockname) * * @throws \PhpOffice\PhpWord\Exception\Exception */ - public function save() - { + public function save() { foreach ($this->tempDocumentHeaders as $index => $headerXML) { $this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]); } - + $this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart); - + foreach ($this->tempDocumentFooters as $index => $headerXML) { $this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]); } - + // Close zip file if (false === $this->zipClass->close()) { throw new Exception('Could not close zip file.'); } - + return $this->tempDocumentFilename; } - + /** * Saves the result document to the user defined file. * @@ -348,24 +326,23 @@ public function save() * * @return void */ - public function saveAs($fileName) - { + public function saveAs($fileName) { $tempFileName = $this->save(); - + if (file_exists($fileName)) { unlink($fileName); } - + /* * 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. * * @see https://github.com/PHPOffice/PHPWord/issues/532 - */ + */ copy($tempFileName, $fileName); unlink($tempFileName); } - + /** * Finds parts of broken macros and sticks them together. * Macros, while being edited, could be implicitly broken by some of the word processors. @@ -376,48 +353,39 @@ public function saveAs($fileName) * * @return string */ - protected function fixBrokenMacros($documentPart) - { + protected function fixBrokenMacros($documentPart) { $fixedDocumentPart = $documentPart; - - $fixedDocumentPart = preg_replace_callback( - '|\$\{([^\}]+)\}|U', - function ($match) { - return strip_tags($match[0]); - }, - $fixedDocumentPart - ); - + + $fixedDocumentPart = preg_replace_callback('|\$\{([^\}]+)\}|U', function ($match) { + return strip_tags($match[0]); + }, $fixedDocumentPart); + return $fixedDocumentPart; } - + /** * Find and replace macros in the given XML section. * * @param string $documentPartXML - * @param string $search + * @param string $searchP * @param string $replace * @param integer $limit * * @return string */ - protected function setValueForPart($documentPartXML, $search, $replace, $limit) - { + protected function setValueForPart($documentPartXML, $search, $replace, $limit) { - if (!String::isUTF8($replace)) { - $replace = utf8_encode($replace); - } - // Note: we can't use the same function for both cases here, because of performance considerations. if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) { return str_replace($search, $replace, $documentPartXML); - } else { + } + else { $regExpDelim = '/'; $escapedSearch = preg_quote($search, $regExpDelim); return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit); } } - + /** * Find all variables in $documentPartXML. * @@ -425,13 +393,12 @@ protected function setValueForPart($documentPartXML, $search, $replace, $limit) * * @return string[] */ - protected function getVariablesForPart($documentPartXML) - { + protected function getVariablesForPart($documentPartXML) { preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); - + return $matches[1]; } - + /** * Get the name of the footer file for $index. * @@ -439,11 +406,10 @@ protected function getVariablesForPart($documentPartXML) * * @return string */ - protected function getFooterName($index) - { + protected function getFooterName($index) { return sprintf('word/footer%d.xml', $index); } - + /** * Get the name of the header file for $index. * @@ -451,11 +417,10 @@ protected function getFooterName($index) * * @return string */ - protected function getHeaderName($index) - { + protected function getHeaderName($index) { return sprintf('word/header%d.xml', $index); } - + /** * Find the start position of the nearest table row before $offset. * @@ -465,20 +430,19 @@ protected function getHeaderName($index) * * @throws \PhpOffice\PhpWord\Exception\Exception */ - protected function findRowStart($offset) - { + protected function findRowStart($offset) { $rowStart = strrpos($this->tempDocumentMainPart, 'tempDocumentMainPart) - $offset) * -1)); - + if (!$rowStart) { $rowStart = strrpos($this->tempDocumentMainPart, '', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); } if (!$rowStart) { throw new Exception('Can not find the start position of the row to clone.'); } - + return $rowStart; } - + /** * Find the end position of the nearest table row after $offset. * @@ -486,11 +450,10 @@ protected function findRowStart($offset) * * @return integer */ - protected function findRowEnd($offset) - { + protected function findRowEnd($offset) { return strpos($this->tempDocumentMainPart, '', $offset) + 7; } - + /** * Get a slice of a string. * @@ -499,12 +462,11 @@ protected function findRowEnd($offset) * * @return string */ - protected function getSlice($startPosition, $endPosition = 0) - { + protected function getSlice($startPosition, $endPosition = 0) { if (!$endPosition) { $endPosition = strlen($this->tempDocumentMainPart); } - + return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition)); } }