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('##', $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)) { 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.*)(\${' . $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.*)(\${' . $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, '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. * * @param integer $offset * * @return integer */ protected function findRowEnd($offset) { return strpos($this->tempDocumentMainPart, '', $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)); } }