123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- <?php
- /**
- * This file is part of PHPWord - A pure PHP library for reading and writing
- * word processing documents.
- *
- * PHPWord is free software distributed under the terms of the GNU Lesser
- * General Public License version 3 as published by the Free Software Foundation.
- *
- * For the full copyright and license information, please read the LICENSE
- * file that was distributed with this source code. For the full list of
- * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
- *
- * @link https://github.com/PHPOffice/PHPWord
- * @copyright 2010-2014 PHPWord contributors
- * @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
- */
-
- namespace PhpOffice\PhpWord;
-
- use PhpOffice\PhpWord\Exception\CopyFileException;
- use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
- use PhpOffice\PhpWord\Exception\Exception;
- use PhpOffice\PhpWord\Shared\String;
- use PhpOffice\PhpWord\Shared\ZipArchive;
-
- class TemplateProcessor
- {
- const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
-
- /**
- * ZipArchive object.
- *
- * @var mixed
- */
- protected $zipClass;
-
- /**
- * @var string Temporary document filename (with path).
- */
- protected $tempDocumentFilename;
-
- /**
- * Content of main document part (in XML format) of the temporary document.
- *
- * @var string
- */
- protected $tempDocumentMainPart;
-
- /**
- * Content of headers (in XML format) of the temporary document.
- *
- * @var string[]
- */
- protected $tempDocumentHeaders = array();
-
- /**
- * Content of footers (in XML format) of the temporary document.
- *
- * @var string[]
- */
- protected $tempDocumentFooters = array();
-
- /**
- * @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception.
- *
- * @param string $documentTemplate The fully qualified template filename.
- *
- * @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException
- * @throws \PhpOffice\PhpWord\Exception\CopyFileException
- */
- public function __construct($documentTemplate)
- {
- // Temporary document filename initialization
- $this->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))
- );
- $index++;
- }
- $index = 1;
- while (false !== $this->zipClass->locateName($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.
- *
- * @param \DOMDocument $xslDOMDocument
- * @param array $xslOptions
- * @param string $xslOptionsURI
- *
- * @return void
- *
- * @throws \PhpOffice\PhpWord\Exception\Exception
- */
- 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
- * @param integer $limit
- *
- * @return void
- */
- public function setValue($macro, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT)
- {
- 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()
- {
- $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.
- *
- * @param string $search
- * @param integer $numberOfClones
- *
- * @return void
- *
- * @throws \PhpOffice\PhpWord\Exception\Exception
- */
- 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('#<w:vMerge w:val="restart"/>#', $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('#<w:vMerge/>#', $tmpXmlRow) &&
- !preg_match('#<w:vMerge w:val="continue" />#', $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 .= $this->getSlice($rowEnd);
-
- $this->tempDocumentMainPart = $result;
- }
-
- /**
- * Clone a block.
- *
- * @param string $blockname
- * @param integer $clones
- * @param boolean $replace
- *
- * @return string|null
- */
- public function cloneBlock($blockname, $clones = 1, $replace = true)
- {
- $xmlBlock = null;
- preg_match(
- '/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $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
- );
- }
- }
-
- return $xmlBlock;
- }
-
- /**
- * Replace a block.
- *
- * @param string $blockname
- * @param string $replacement
- *
- * @return void
- */
- public function replaceBlock($blockname, $replacement)
- {
- preg_match(
- '/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
- $this->tempDocumentMainPart,
- $matches
- );
-
- if (isset($matches[3])) {
- $this->tempDocumentMainPart = str_replace(
- $matches[2] . $matches[3] . $matches[4],
- $replacement,
- $this->tempDocumentMainPart
- );
- }
- }
-
- /**
- * Delete a block of text.
- *
- * @param string $blockname
- *
- * @return void
- */
- public function deleteBlock($blockname)
- {
- $this->replaceBlock($blockname, '');
- }
-
- /**
- * Saves the result document.
- *
- * @return string
- *
- * @throws \PhpOffice\PhpWord\Exception\Exception
- */
- 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.
- *
- * @since 0.8.0
- *
- * @param string $fileName
- *
- * @return void
- */
- 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.
- *
- * @since 0.13.0
- *
- * @param string $documentPart The document part in XML representation.
- *
- * @return string
- */
- protected function fixBrokenMacros($documentPart)
- {
- $fixedDocumentPart = $documentPart;
-
- $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 $replace
- * @param integer $limit
- *
- * @return string
- */
- protected function setValueForPart($documentPartXML, $search, $replace, $limit)
- {
- if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') {
- $search = '${' . $search . '}';
- }
-
- 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 {
- $regExpDelim = '/';
- $escapedSearch = preg_quote($search, $regExpDelim);
- return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit);
- }
- }
-
- /**
- * Find all variables in $documentPartXML.
- *
- * @param string $documentPartXML
- *
- * @return string[]
- */
- protected function getVariablesForPart($documentPartXML)
- {
- preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
-
- return $matches[1];
- }
-
- /**
- * Get the name of the footer file for $index.
- *
- * @param integer $index
- *
- * @return string
- */
- protected function getFooterName($index)
- {
- return sprintf('word/footer%d.xml', $index);
- }
-
- /**
- * Get the name of the header file for $index.
- *
- * @param integer $index
- *
- * @return string
- */
- protected function getHeaderName($index)
- {
- return sprintf('word/header%d.xml', $index);
- }
-
- /**
- * Find the start position of the nearest table row before $offset.
- *
- * @param integer $offset
- *
- * @return integer
- *
- * @throws \PhpOffice\PhpWord\Exception\Exception
- */
- protected function findRowStart($offset)
- {
- $rowStart = strrpos($this->tempDocumentMainPart, '<w:tr ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
-
- if (!$rowStart) {
- $rowStart = strrpos($this->tempDocumentMainPart, '<w:tr>', ((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.
- *
- * @param integer $offset
- *
- * @return integer
- */
- protected function findRowEnd($offset)
- {
- return strpos($this->tempDocumentMainPart, '</w:tr>', $offset) + 7;
- }
-
- /**
- * Get a slice of a string.
- *
- * @param integer $startPosition
- * @param integer $endPosition
- *
- * @return string
- */
- protected function getSlice($startPosition, $endPosition = 0)
- {
- if (!$endPosition) {
- $endPosition = strlen($this->tempDocumentMainPart);
- }
-
- return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
- }
- }
|