Skip to content

Commit

Permalink
Word2007 Reader : Support for FormFields (#2653)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Progi1984 and vincentKool authored Aug 12, 2024
1 parent 00febf5 commit 3631936
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/changes/2.x/2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 143 additions & 3 deletions src/PhpWord/Reader/Word2007/AbstractPart.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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.
*
Expand Down
188 changes: 188 additions & 0 deletions tests/PhpWordTests/Reader/Word2007/ElementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<w:p>
<w:r>
<w:t>Reference</w:t>
</w:r>
<w:r>
<w:fldChar w:fldCharType="begin">
<w:ffData>
<w:name w:val="DropDownList1"/>
<w:enabled/>
<w:calcOnExit w:val="0"/>
<w:ddList>
<w:result w:val="2"/>
<w:listEntry w:val="TBD"/>
<w:listEntry w:val="Option One"/>
<w:listEntry w:val="Option Two"/>
<w:listEntry w:val="Option Three"/>
<w:listEntry w:val="Other"/>
</w:ddList>
</w:ffData>
</w:fldChar>
</w:r>
<w:r>
<w:instrText xml:space="preserve"> FORMDROPDOWN </w:instrText>
</w:r>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
</w:r>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:fldChar w:fldCharType="end"/>
</w:r>
</w:p>';

$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 = '<w:p>
<w:r>
<w:t>Fieldname</w:t>
</w:r>
<w:r>
<w:fldChar w:fldCharType="begin">
<w:ffData>
<w:name w:val="TextInput2"/>
<w:enabled/>
<w:calcOnExit w:val="0"/>
<w:textInput>
<w:default w:val="TBD"/>
<w:maxLength w:val="200"/>
</w:textInput>
</w:ffData>
</w:fldChar>
</w:r>
<w:r>
<w:instrText xml:space="preserve"> FORMTEXT </w:instrText>
</w:r>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
</w:r>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="00807709">
<w:rPr>
<w:noProof/>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:t>This is some sample text</w:t>
</w:r>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:fldChar w:fldCharType="end"/>
</w:r>
</w:p>';

$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 = '<w:p>
<w:pPr/>
<w:r>
<w:fldChar w:fldCharType="begin">
<w:ffData>
<w:enabled w:val="1"/>
<w:name w:val="SomeCheckbox"/>
<w:calcOnExit w:val="0"/>
<w:checkBox>
<w:sizeAuto w:val=""/>
<w:default w:val="0"/>
<w:checked w:val="0"/>
</w:checkBox>
</w:ffData>
</w:fldChar>
</w:r>
<w:r>
<w:rPr/>
<w:instrText xml:space="preserve">FORMCHECKBOX</w:instrText>
</w:r>
<w:r>
<w:rPr/>
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r>
<w:rPr/>
<w:t xml:space="preserve"> </w:t>
</w:r>
<w:r>
<w:rPr/>
<w:fldChar w:fldCharType="end"/>
</w:r>
</w:p>';

$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());
}
}

0 comments on commit 3631936

Please sign in to comment.