From c0809b0c6cb2f8ad6e1ce359cfc44219ba7c841d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 28 Jul 2022 07:03:26 -0700 Subject: [PATCH] Fix Spreadsheet Copy, Disable Clone, Improve Coverage (#2951) * Fix Spreadsheet Copy, Disable Clone, Improve Coverage This PR was supposed to be merely to increase coverage in Spreadsheet. However, in doing so, I discovered that neither clone nor copy worked correctly. Neither had been covered in the test suite. Copy not only did not work, it broke the source spreadsheet as well. I tried to debug and got nowhere; I even tried using myclabs/deep-copy which is already in use in the test suite, but it failed as well. However, write and reload ought to work just fine for copy. It can't be used for clone; however, since copy does what clone ought to do, there's no reason why clone needs to be used, so __clone is changed to throw an exception if attempted. One other source change was needed, an obvious bug where an if condition uses 'or' when it should use 'and'. Also, one docblock declaration needed a change. Aside from that, the rest of this PR is test cases, and overall coverage passes 89% for the first time. * Clone is Okay After All But copy wasn't, changing it to just return clone. Perhaps save and reload will be needed instead at some point, but not yet. * An Error I Cannot Reproduce PHP8.1 unit test says error because GdImage can't be serialized. I can't reproduce this error on any of my test systems. I have no idea why GdImage is even involved. Using try/catch to see if it helps. * Weird Failures in Github I thought restoring clone was a good idea. That left me in a state where, after one change, copy/clone no longer worked on Github (unable to reproduce on any of my test systems). After a second change, copy worked but clone didn't, again unable to reproduce. So, reverting to original version - copy does save and reload, clone throws exception. --- phpstan-baseline.neon | 15 -- src/PhpSpreadsheet/Spreadsheet.php | 35 ++- tests/PhpSpreadsheetTests/DefinedNameTest.php | 12 + .../PhpSpreadsheetTests/NamedFormulaTest.php | 6 + tests/PhpSpreadsheetTests/NamedRangeTest.php | 6 + .../Reader/Xlsx/RibbonTest.php | 30 +++ .../SpreadsheetCoverageTest.php | 209 ++++++++++++++++++ tests/PhpSpreadsheetTests/Style/FontTest.php | 17 ++ 8 files changed, 297 insertions(+), 33 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5ef5522f..024f1fbd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2860,11 +2860,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Comparison operation \"\\<\\=\" between int\\ and 1000 is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 @@ -2875,21 +2870,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#" count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 33b4fe0c..4bb93987 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet { @@ -1120,28 +1123,24 @@ class Spreadsheet */ public function copy() { - $copied = clone $this; + $filename = File::temporaryFilename(); + $writer = new XlsxWriter($this); + $writer->setIncludeCharts(true); + $writer->save($filename); - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); - $this->workSheetCollection[$i]->rebindParent($this); - } + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $reloadedSpreadsheet = $reader->load($filename); + unlink($filename); - return $copied; + return $reloadedSpreadsheet; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ public function __clone() { - // @phpstan-ignore-next-line - foreach ($this as $key => $val) { - if (is_object($val) || (is_array($val))) { - $this->{$key} = unserialize(serialize($val)); - } - } + throw new Exception( + 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.' + ); } /** @@ -1562,7 +1561,7 @@ class Spreadsheet * Workbook window is hidden and cannot be shown in the * user interface. * - * @param string $visibility visibility status of the workbook + * @param null|string $visibility visibility status of the workbook */ public function setVisibility($visibility): void { @@ -1596,7 +1595,7 @@ class Spreadsheet */ public function setTabRatio($tabRatio): void { - if ($tabRatio >= 0 || $tabRatio <= 1000) { + if ($tabRatio >= 0 && $tabRatio <= 1000) { $this->tabRatio = (int) $tabRatio; } else { throw new Exception('Tab ratio must be between 0 and 1000.'); diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 43eddc8a..82950880 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -85,6 +85,18 @@ class DefinedNameTest extends TestCase self::assertCount(1, $this->spreadsheet->getDefinedNames()); } + public function testRemoveGlobalDefinedName(): void + { + $this->spreadsheet->addDefinedName( + DefinedName::createInstance('Any', $this->spreadsheet->getActiveSheet(), '=A1') + ); + self::assertCount(1, $this->spreadsheet->getDefinedNames()); + + $this->spreadsheet->removeDefinedName('Any'); + self::assertCount(0, $this->spreadsheet->getDefinedNames()); + $this->spreadsheet->removeDefinedName('Other'); + } + public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void { $this->spreadsheet->addDefinedName( diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 4c4a6b11..02e9d818 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -133,4 +133,10 @@ class NamedFormulaTest extends TestCase $formula->getValue() ); } + + public function testRemoveNonExistentNamedFormula(): void + { + self::assertCount(0, $this->spreadsheet->getNamedFormulae()); + $this->spreadsheet->removeNamedFormula('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index c72b7b73..402e7eba 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -133,4 +133,10 @@ class NamedRangeTest extends TestCase $range->getValue() ); } + + public function testRemoveNonExistentNamedRange(): void + { + self::assertCount(0, $this->spreadsheet->getNamedRanges()); + $this->spreadsheet->removeNamedRange('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php index 197ad47f..ab304e7b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -44,4 +44,34 @@ class RibbonTest extends AbstractFunctional self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); $reloadedSpreadsheet->disconnectWorksheets(); } + + /** + * Same as above but discard macros. + */ + public function testDiscardMacros(): void + { + $filename = 'tests/data/Reader/XLSX/ribbon.donotopen.zip'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen($data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + $spreadsheet->discardMacros(); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertNull($reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php new file mode 100644 index 00000000..584c53fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -0,0 +1,209 @@ +getProperties(); + $properties->setCreator('Anyone'); + $properties->setTitle('Description'); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $spreadsheet2->getProperties()); + $properties2 = clone $properties; + $spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $spreadsheet2->getProperties()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testDocumentSecurity(): void + { + $spreadsheet = new Spreadsheet(); + $security = $spreadsheet->getSecurity(); + $security->setLockRevision(true); + $revisionsPassword = 'revpasswd'; + $security->setRevisionsPassword($revisionsPassword); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $spreadsheet2->getSecurity()); + $security2 = clone $security; + $spreadsheet2->setSecurity($security2); + self::assertEquals($security, $spreadsheet2->getSecurity()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testCellXfCollection(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $collection = $spreadsheet->getCellXfCollection(); + self::assertCount(4, $collection); + $font1Style = $collection[1]; + self::assertTrue($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + + $spreadsheet->removeCellXfByIndex(1); + self::assertFalse($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $spreadsheet->removeCellXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveDefaultStyle(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('No default style found for this workbook'); + // Removing default style probably should be disallowed. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellXfByIndex(0); + $style = $spreadsheet->getDefaultStyle(); + $spreadsheet->disconnectWorksheets(); + } + + public function testCellStyleXF(): void + { + $spreadsheet = new Spreadsheet(); + $collection = $spreadsheet->getCellStyleXfCollection(); + self::assertCount(1, $collection); + $styleXf = $collection[0]; + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0)); + $hash = $styleXf->getHashCode(); + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x')); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellStyleXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellStyleXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidFirstSheetIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('First sheet index must be a positive integer.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setFirstSheetIndex(-1); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidVisibility(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Invalid visibility value.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility('badvalue'); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidTabRatio(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setTabRatio(2000); + $spreadsheet->disconnectWorksheets(); + } + + public function testCopy(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $sheet->getCell('A1')->setValue('this is a1'); + $sheet->getCell('A2')->setValue('this is a2'); + $sheet->getCell('A3')->setValue('this is a3'); + $sheet->getCell('B1')->setValue('this is b1'); + $sheet->getCell('B2')->setValue('this is b2'); + $copied = $spreadsheet->copy(); + $copysheet = $copied->getActiveSheet(); + $copysheet->getStyle('A2')->getFont()->setName('font12'); + $copysheet->getCell('A2')->setValue('this was a2'); + + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $sheet->getCell('A1')->getValue()); + self::assertSame('this is a2', $sheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $sheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $sheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $sheet->getCell('B2')->getValue()); + + self::assertSame('font1', $copysheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font12', $copysheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $copysheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $copysheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $copysheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $copysheet->getCell('A1')->getValue()); + self::assertSame('this was a2', $copysheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); + + $spreadsheet->disconnectWorksheets(); + $copied->disconnectWorksheets(); + } + + public function testClone(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); + $spreadsheet = new Spreadsheet(); + $clone = clone $spreadsheet; + $spreadsheet->disconnectWorksheets(); + $clone->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/FontTest.php b/tests/PhpSpreadsheetTests/Style/FontTest.php index 02814afa..6cd4d950 100644 --- a/tests/PhpSpreadsheetTests/Style/FontTest.php +++ b/tests/PhpSpreadsheetTests/Style/FontTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Style; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; use PHPUnit\Framework\TestCase; class FontTest extends TestCase @@ -88,4 +89,20 @@ class FontTest extends TestCase self::assertEquals('Calibri', $font->getName(), 'Null string changed to default'); $spreadsheet->disconnectWorksheets(); } + + public function testUnderlineHash(): void + { + $font1 = new Font(); + $font2 = new Font(); + $font2aHash = $font2->getHashCode(); + self::assertSame($font1->getHashCode(), $font2aHash); + $font2->setUnderlineColor( + [ + 'type' => 'srgbClr', + 'value' => 'FF0000', + ] + ); + $font2bHash = $font2->getHashCode(); + self::assertNotEquals($font1->getHashCode(), $font2bHash); + } }