From 3631936b9e60fe817501bb7922cb220fb9bba74d Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Mon, 12 Aug 2024 14:44:08 +0200 Subject: [PATCH] Word2007 Reader : Support for FormFields (#2653) * Added code to read FormFields (text input, dropdown and checkbox) from a Word file * Fixed code style issues and added a testcase for reading a FormField of type checkbox * Fixed minor issue found by Scrutinizer * Fixed CI --------- Co-authored-by: Vincent Kool --- docs/changes/2.x/2.0.0.md | 1 + phpstan-baseline.neon | 5 - src/PhpWord/Reader/Word2007/AbstractPart.php | 146 +++++++++++++- .../Reader/Word2007/ElementTest.php | 188 ++++++++++++++++++ 4 files changed, 332 insertions(+), 8 deletions(-) diff --git a/docs/changes/2.x/2.0.0.md b/docs/changes/2.x/2.0.0.md index 06a7ba3675..e658f1a787 100644 --- a/docs/changes/2.x/2.0.0.md +++ b/docs/changes/2.x/2.0.0.md @@ -8,6 +8,7 @@ - PDF Writer : Documented how to specify a PDF renderer, when working with the PDF writer, as well as the three available choices by [@settermjd](https://github.com/settermjd) in [#2642](https://github.com/PHPOffice/PHPWord/pull/2642) - Word2007 Reader: Support for Paragraph Border Style by [@damienfa](https://github.com/damienfa) in [#2651](https://github.com/PHPOffice/PHPWord/pull/2651) - Word2007 Writer: Support for field REF by [@crystoline](https://github.com/crystoline) in [#2652](https://github.com/PHPOffice/PHPWord/pull/2652) +- Word2007 Reader : Support for FormFields by [@vincentKool](https://github.com/vincentKool) in [#2653](https://github.com/PHPOffice/PHPWord/pull/2653) ### Bug fixes diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8e47429ad9..026fd8b59a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -235,11 +235,6 @@ parameters: count: 1 path: src/PhpWord/Reader/Word2007/AbstractPart.php - - - message: "#^Parameter \\#1 \\$count of method PhpOffice\\\\PhpWord\\\\Element\\\\AbstractContainer\\:\\:addTextBreak\\(\\) expects int, null given\\.$#" - count: 1 - path: src/PhpWord/Reader/Word2007/AbstractPart.php - - message: "#^Parameter \\#1 \\$depth of method PhpOffice\\\\PhpWord\\\\Element\\\\AbstractContainer\\:\\:addListItemRun\\(\\) expects int, string\\|null given\\.$#" count: 1 diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 2741841ef3..0f3d8bf711 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -24,6 +24,7 @@ use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType; use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\AbstractElement; +use PhpOffice\PhpWord\Element\FormField; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; @@ -192,8 +193,44 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par // Paragraph style $paragraphStyle = $xmlReader->elementExists('w:pPr', $domNode) ? $this->readParagraphStyle($xmlReader, $domNode) : null; - // PreserveText - if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) { + if ($xmlReader->elementExists('w:r/w:fldChar/w:ffData', $domNode)) { + // FormField + $partOfFormField = false; + $formNodes = []; + $formType = null; + $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag', $domNode); + if ($textRunContainers > 0) { + $nodes = $xmlReader->getElements('*', $domNode); + $paragraph = $parent->addTextRun($paragraphStyle); + foreach ($nodes as $node) { + if ($xmlReader->elementExists('w:fldChar/w:ffData', $node)) { + $partOfFormField = true; + $formNodes[] = $node; + if ($xmlReader->elementExists('w:fldChar/w:ffData/w:ddList', $node)) { + $formType = 'dropdown'; + } elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:textInput', $node)) { + $formType = 'textinput'; + } elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:checkBox', $node)) { + $formType = 'checkbox'; + } + } elseif ($partOfFormField && + $xmlReader->elementExists('w:fldChar', $node) && + 'end' == $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar') + ) { + $formNodes[] = $node; + $partOfFormField = false; + // Process the form fields + $this->readFormField($xmlReader, $formNodes, $paragraph, $paragraphStyle, $formType); + } elseif ($partOfFormField) { + $formNodes[] = $node; + } else { + // normal runs + $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle); + } + } + } + } elseif ($xmlReader->elementExists('w:r/w:instrText', $domNode)) { + // PreserveText $ignoreText = false; $textContent = ''; $fontStyle = $this->readFontStyle($xmlReader, $domNode); @@ -272,7 +309,7 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par // Text and TextRun $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); if (0 === $textRunContainers) { - $parent->addTextBreak(null, $paragraphStyle); + $parent->addTextBreak(1, $paragraphStyle); } else { $nodes = $xmlReader->getElements('*', $domNode); $paragraph = $parent->addTextRun($paragraphStyle); @@ -282,6 +319,109 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par } } + /** + * @param DOMElement[] $domNodes + * @param AbstractContainer $parent + * @param mixed $paragraphStyle + * @param string $formType + */ + private function readFormField(XMLReader $xmlReader, array $domNodes, $parent, $paragraphStyle, $formType): void + { + if (!in_array($formType, ['textinput', 'checkbox', 'dropdown'])) { + return; + } + + $formField = $parent->addFormField($formType, null, $paragraphStyle); + $ffData = $xmlReader->getElement('w:fldChar/w:ffData', $domNodes[0]); + + foreach ($xmlReader->getElements('*', $ffData) as $node) { + /** @var DOMElement $node */ + switch ($node->localName) { + case 'name': + $formField->setName($node->getAttribute('w:val')); + + break; + case 'ddList': + $listEntries = []; + foreach ($xmlReader->getElements('*', $node) as $ddListNode) { + switch ($ddListNode->localName) { + case 'result': + $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode)); + + break; + case 'default': + $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode)); + + break; + case 'listEntry': + $listEntries[] = $xmlReader->getAttribute('w:val', $ddListNode); + + break; + } + } + $formField->setEntries($listEntries); + if (null !== $formField->getValue()) { + $formField->setText($listEntries[$formField->getValue()]); + } + + break; + case 'textInput': + foreach ($xmlReader->getElements('*', $node) as $ddListNode) { + switch ($ddListNode->localName) { + case 'default': + $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode)); + + break; + case 'format': + case 'maxLength': + break; + } + } + + break; + case 'checkBox': + foreach ($xmlReader->getElements('*', $node) as $ddListNode) { + switch ($ddListNode->localName) { + case 'default': + $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode)); + + break; + case 'checked': + $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode)); + + break; + case 'size': + case 'sizeAuto': + break; + } + } + + break; + } + } + + if ('textinput' == $formType) { + $ignoreText = true; + $textContent = ''; + foreach ($domNodes as $node) { + if ($xmlReader->elementExists('w:fldChar', $node)) { + $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar'); + if ('separate' == $fldCharType) { + $ignoreText = false; + } elseif ('end' == $fldCharType) { + $ignoreText = true; + } + } + + if (false === $ignoreText) { + $textContent .= $xmlReader->getValue('w:t', $node); + } + } + $formField->setValue(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8')); + $formField->setText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8')); + } + } + /** * Returns the depth of the Heading, returns 0 for a Title. * diff --git a/tests/PhpWordTests/Reader/Word2007/ElementTest.php b/tests/PhpWordTests/Reader/Word2007/ElementTest.php index f96d8ac1a2..3685cbea03 100644 --- a/tests/PhpWordTests/Reader/Word2007/ElementTest.php +++ b/tests/PhpWordTests/Reader/Word2007/ElementTest.php @@ -355,4 +355,192 @@ public function testReadDrawing(): void $elements = $phpWord->getSection(0)->getElements(); self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]); } + + /** + * Test reading FormField - DROPDOWN. + */ + public function testReadFormFieldDropdown(): void + { + $documentXml = ' + + Reference + + + + + + + + + + + + + + + + + + + + FORMDROPDOWN + + + + + + + + + + + + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(['document' => $documentXml]); + + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]); + + $subElements = $elements[0]->getElements(); + + self::assertInstanceOf('PhpOffice\PhpWord\Element\Text', $subElements[0]); + self::assertEquals('Reference', $subElements[0]->getText()); + + self::assertInstanceOf('PhpOffice\PhpWord\Element\FormField', $subElements[1]); + self::assertEquals('dropdown', $subElements[1]->getType()); + self::assertEquals('DropDownList1', $subElements[1]->getName()); + self::assertEquals('2', $subElements[1]->getValue()); + self::assertEquals('Option Two', $subElements[1]->getText()); + self::assertEquals(['TBD', 'Option One', 'Option Two', 'Option Three', 'Other'], $subElements[1]->getEntries()); + } + + /** + * Test reading FormField - textinput. + */ + public function testReadFormFieldTextinput(): void + { + $documentXml = ' + + Fieldname + + + + + + + + + + + + + + + + FORMTEXT + + + + + + + + + + + + + + + + + + This is some sample text + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(['document' => $documentXml]); + + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]); + + $subElements = $elements[0]->getElements(); + + self::assertInstanceOf('PhpOffice\PhpWord\Element\Text', $subElements[0]); + self::assertEquals('Fieldname', $subElements[0]->getText()); + + self::assertInstanceOf('PhpOffice\PhpWord\Element\FormField', $subElements[1]); + self::assertEquals('textinput', $subElements[1]->getType()); + self::assertEquals('TextInput2', $subElements[1]->getName()); + self::assertEquals('This is some sample text', $subElements[1]->getValue()); + self::assertEquals('This is some sample text', $subElements[1]->getText()); + } + + /** + * Test reading FormField - checkbox. + */ + public function testReadFormFieldCheckbox(): void + { + $documentXml = ' + + + + + + + + + + + + + + + + + + FORMCHECKBOX + + + + + + + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(['document' => $documentXml]); + + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]); + + $subElements = $elements[0]->getElements(); + +// $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $subElements[0]); +// $this->assertEquals('Fieldname', $subElements[0]->getText()); + + self::assertInstanceOf('PhpOffice\PhpWord\Element\FormField', $subElements[0]); + self::assertEquals('checkbox', $subElements[0]->getType()); + self::assertEquals('SomeCheckbox', $subElements[0]->getName()); + } }