Skip to content

Commit

Permalink
WIP Limited Support for Form Controls V2 (ListBox, Buttons, etc.)
Browse files Browse the repository at this point in the history
This is a replacement for draft PR PHPOffice#2455 and draft PR PHPOffice#3127. There is some useful commentary in those PRs which I have mostly, but not entirely, duplicated below. Fix PHPOffice#2396. Fix PHPOffice#1770. Fix PHPOffice#2388.

A related problem is that the vml files used for the form controls sometimes contain invalid xml. Fix PHPOffice#3125 (rejected previous PR PHPOffice#1181 as too risky, issue was also reported as PHPOffice#170). Vml file should be valid Xml, but Excel can generate unclosed `<br>` tags, preventing Xlsx reader from reading file correctly. I believe a very narrowly targeted fix, changing `<br>` to `<br/>`, and only when reading vml files, probably mitigates the risk. The sample file formscomments.xlsx which is part of this change shows this problem with `Button 1` on sheet `Forms`; the spreadsheet was created with Excel 365, so the problem is not restricted to Excel 2013 as originally reported. A comment on PR 3127 indicates that other tags might be involved, but, without a file demonstrating that, I will restrict this change to br tags for now.

I am starting this out in draft status, and will probably leave it that way for some time. I'm not sure where we want to go with this. It fixes some problems, but in a limited manner, and creates some others. I'm not sure the pain of the others is balanced considering the limitations of the fix. If enough interest is generated as a result of this ticket being out there, we can proceed; if not, it probably isn't worth it.

This fix allows form control elements to be read in and written out. It does not allow you to add such elements, nor even to locate them or determine their properties (so you can't modify or delete them). Although it handles reading and writing of sheets containing both form controls and comments, it will probably create a corrupt spreadsheet if you try adding a new comment to a sheet with form controls - probably quite difficult to solve. Cloning the sheet probably won't work either - probably easier than the other. It is conceivable that we want to add a new property to the Xlsx Reader which turns the reading of form elements on or off (default=off), so that negative effects will be limited to those who have explictly opted in. The change in its current form does not implement such a property.

Because of its limitations, the change isn't really testable. As in some other recent installs, I have added a sample to demonstrate that it works correctly.

As it turns out, if we have a worksheet which contains both form controls and comments (see formscomments.xlsx which is part of this PR), PhpSpreadsheet already creates a corrupt file when it tries to load and save the spreadsheet with such a worksheet. With this change, the file is saved without corruption. This tilts things in favor of proceeding. I'm still not ready, but this will be an important consideration.

A sample file for issue PHPOffice#2621 illustrated a problem with shape files. Since they are involved here, I took a look at how the sample worked with this code. In master, and with this change, a corrupt file results. Fixing that is probably easier than the general problem of handling shape files, but it's an argument against moving this forward until the corruption problem can be addressed.

Fix PHPOffice#2661. A template including checkboxes was leading to file corruption solved by this PR. Another argument for moving forward.
  • Loading branch information
oleibman committed Oct 20, 2022
1 parent f1d73d8 commit 870cf0b
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 8 deletions.
22 changes: 22 additions & 0 deletions samples/Reader/22_Reader_formscomments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use PhpOffice\PhpSpreadsheet\IOFactory;

require __DIR__ . '/../Header.php';

$helper->log('Start');

$inputFileType = 'Xlsx';
$inputFileName = __DIR__ . '/sampleData/formscomments.xlsx';

$helper->log('Loading file ' . $inputFileName . ' using IOFactory with a defined reader type of ' . $inputFileType);
$reader = IOFactory::createReader($inputFileType);
$helper->log('Loading all WorkSheets');
$reader->setLoadAllSheets();
$spreadsheet = $reader->load($inputFileName);

// Save
$helper->write($spreadsheet, __FILE__, ['Xlsx']);
$spreadsheet->disconnectWorksheets();

$helper->log('end');
1 change: 1 addition & 0 deletions samples/Reader/22_Reader_issue1767.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@

// Save
$helper->write($spreadsheet, __FILE__);
$spreadsheet->disconnectWorksheets();

$helper->log('end');
Binary file added samples/Reader/sampleData/formscomments.xlsx
Binary file not shown.
17 changes: 15 additions & 2 deletions src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,12 @@ public static function falseToArray($value): array
return is_array($value) ? $value : [];
}

private function loadZip(string $filename, string $ns = ''): SimpleXMLElement
private function loadZip(string $filename, string $ns = '', bool $replaceUnclosedBr = false): SimpleXMLElement
{
$contents = $this->getFromZipArchive($this->zip, $filename);
if ($replaceUnclosedBr) {
$contents = str_replace('<br>', '<br/>', $contents);
}
$rels = simplexml_load_string(
$this->securityScanner->scan($contents),
'SimpleXMLElement',
Expand Down Expand Up @@ -1024,6 +1027,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet

// later we will remove from it real vmlComments
$unparsedVmlDrawings = $vmlComments;
$vmlDrawingContents = [];

// Loop through VML comments
foreach ($vmlComments as $relName => $relPath) {
Expand All @@ -1032,7 +1036,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet

try {
// no namespace okay - processed with Xpath
$vmlCommentsFile = $this->loadZip($relPath, '');
$vmlCommentsFile = $this->loadZip($relPath, '', true);
$vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML);
} catch (Throwable $ex) {
//Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
Expand All @@ -1042,6 +1046,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
// Locate VML drawings image relations
$drowingImages = [];
$VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels';
$vmlDrawingContents[$relName] = $this->securityScanner->scan($this->getFromZipArchive($zip, $relPath));
if ($zip->locateName($VMLDrawingsRelations)) {
$relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS);
foreach ($relsVMLDrawing->Relationship as $elex) {
Expand Down Expand Up @@ -1495,6 +1500,14 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
}
}
}
if ($xmlSheet->legacyDrawing && !$this->readDataOnly) {
foreach ($xmlSheet->legacyDrawing as $drawing) {
$drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
if (isset($vmlDrawingContents[$drawingRelId])) {
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
}
}
}

// unparsed drawing AlternateContent
$xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY);
Expand Down
13 changes: 9 additions & 4 deletions src/PhpSpreadsheet/Writer/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -455,14 +455,17 @@ public function save($filename, int $flags = 0): void
}

// Add comment relationship parts
if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) {
$legacy = $unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['legacyDrawing'] ?? null;
if (count($this->spreadSheet->getSheet($i)->getComments()) > 0 || $legacy !== null) {
// VML Comments relationships
$zipContent['xl/drawings/_rels/vmlDrawing' . ($i + 1) . '.vml.rels'] = $this->getWriterPartRels()->writeVMLDrawingRelationships($this->spreadSheet->getSheet($i));

// VML Comments
$zipContent['xl/drawings/vmlDrawing' . ($i + 1) . '.vml'] = $this->getWriterPartComments()->writeVMLComments($this->spreadSheet->getSheet($i));
$zipContent['xl/drawings/vmlDrawing' . ($i + 1) . '.vml'] = $legacy ?? $this->getWriterPartComments()->writeVMLComments($this->spreadSheet->getSheet($i));
}

// Comments
// Comments
if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) {
$zipContent['xl/comments' . ($i + 1) . '.xml'] = $this->getWriterPartComments()->writeComments($this->spreadSheet->getSheet($i));

// Media
Expand All @@ -477,7 +480,9 @@ public function save($filename, int $flags = 0): void
// Add unparsed relationship parts
if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) {
foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) {
$zipContent[$vmlDrawing['filePath']] = $vmlDrawing['content'];
if (!isset($zipContent[$vmlDrawing['filePath']])) {
$zipContent[$vmlDrawing['filePath']] = $vmlDrawing['content'];
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/PhpSpreadsheet/Writer/Xlsx/Rels.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,16 @@ public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\

// Write comments relationship?
$i = 1;
if (count($worksheet->getComments()) > 0) {
if (count($worksheet->getComments()) > 0 || isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'])) {
$this->writeRelationship(
$objWriter,
'_comments_vml' . $i,
Namespaces::VML,
'../drawings/vmlDrawing' . $worksheetId . '.vml'
);
}

if (count($worksheet->getComments()) > 0) {
$this->writeRelationship(
$objWriter,
'_comments' . $i,
Expand Down
3 changes: 2 additions & 1 deletion src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -1322,7 +1322,8 @@ private function writeDrawings(XMLWriter $objWriter, PhpspreadsheetWorksheet $wo
private function writeLegacyDrawing(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
{
// If sheet contains comments, add the relationships
if (count($worksheet->getComments()) > 0) {
$unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData();
if (count($worksheet->getComments()) > 0 || isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'])) {
$objWriter->startElement('legacyDrawing');
$objWriter->writeAttribute('r:id', 'rId_comments_vml1');
$objWriter->endElement();
Expand Down

0 comments on commit 870cf0b

Please sign in to comment.