From 1318b90330773d852ee15c8b432a7e7175c4bd55 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 19 Feb 2021 22:03:50 +0100 Subject: [PATCH] Bugfix #1858; Apply stricter scoping rules to named range/cell access (#1866) * Apply stricter scoping rules to named range/cell access via Worksheet object * Additional unit tests --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 70 ++++++--- .../Worksheet/WorksheetNamedRangesTest.php | 143 ++++++++++++++++++ tests/data/Worksheet/namedRangeTest.xlsx | Bin 0 -> 9812 bytes 3 files changed, 190 insertions(+), 23 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php create mode 100644 tests/data/Worksheet/namedRangeTest.xlsx diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index c6f655c3be..02305b7a7f 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1195,11 +1195,12 @@ public function getCell($pCoordinate, $createIfNotExists = true) (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) && (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $pCoordinate, $matches)) ) { - $namedRange = DefinedName::resolveName($pCoordinate, $this); + $namedRange = $this->validateNamedRange($pCoordinate, true); if ($namedRange !== null) { - $pCoordinate = str_replace('$', '', $namedRange->getValue()); + $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!'); + $cellCoordinate = str_replace('$', '', $cellCoordinate); - return $namedRange->getWorksheet()->getCell($pCoordinate, $createIfNotExists); + return $namedRange->getWorksheet()->getCell($cellCoordinate, $createIfNotExists); } } @@ -1295,18 +1296,12 @@ public function cellExists($pCoordinate) (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) && (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $pCoordinate, $matches)) ) { - $namedRange = DefinedName::resolveName($pCoordinate, $this); + $namedRange = $this->validateNamedRange($pCoordinate, true); if ($namedRange !== null) { - $pCoordinate = str_replace('$', '', $namedRange->getValue()); - if ($this->getHashCode() != $namedRange->getWorksheet()->getHashCode()) { - if (!$namedRange->getLocalOnly()) { - return $namedRange->getWorksheet()->cellExists($pCoordinate); - } + $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!'); + $cellCoordinate = str_replace('$', '', $cellCoordinate); - throw new Exception('Named range ' . $namedRange->getName() . ' is not accessible from within sheet ' . $this->getTitle()); - } - } else { - return false; + return $namedRange->getWorksheet()->cellExists($cellCoordinate); } } @@ -2551,10 +2546,42 @@ public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = tr return $returnValue; } + private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName + { + $namedRange = DefinedName::resolveName($definedName, $this); + if ($namedRange === null) { + if ($returnNullIfInvalid) { + return null; + } + + throw new Exception('Named Range ' . $definedName . ' does not exist.'); + } + + if ($namedRange->isFormula()) { + if ($returnNullIfInvalid) { + return null; + } + + throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.'); + } + + if ($namedRange->getLocalOnly() && $this->getHashCode() !== $namedRange->getWorksheet()->getHashCode()) { + if ($returnNullIfInvalid) { + return null; + } + + throw new Exception( + 'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle() + ); + } + + return $namedRange; + } + /** * Create array from a range of cells. * - * @param string $pNamedRange Name of the Named Range + * @param string $definedName The Named Range that should be returned * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist * @param bool $calculateFormulas Should formulas be calculated? * @param bool $formatData Should formatting be applied to cell values? @@ -2563,17 +2590,14 @@ public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = tr * * @return array */ - public function namedRangeToArray($pNamedRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false) + public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false) { - $namedRange = DefinedName::resolveName($pNamedRange, $this); - if ($namedRange !== null) { - $pWorkSheet = $namedRange->getWorksheet(); - $pCellRange = str_replace('$', '', $namedRange->getValue()); - - return $pWorkSheet->rangeToArray($pCellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef); - } + $namedRange = $this->validateNamedRange($definedName); + $workSheet = $namedRange->getWorksheet(); + $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!'); + $cellRange = str_replace('$', '', $cellRange); - throw new Exception('Named Range ' . $pNamedRange . ' does not exist.'); + return $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef); } /** diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php new file mode 100644 index 0000000000..62238b6857 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php @@ -0,0 +1,143 @@ +spreadsheet = $reader->load('tests/data/Worksheet/namedRangeTest.xlsx'); + } + + public function testCellExists(): void + { + $namedCell = 'GREETING'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cellExists = $worksheet->cellExists($namedCell); + self::assertTrue($cellExists); + } + + public function testCellNotExists(): void + { + $namedCell = 'GOODBYE'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cellExists = $worksheet->cellExists($namedCell); + self::assertFalse($cellExists); + } + + public function testCellExistsInvalidScope(): void + { + $namedCell = 'Result'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cellExists = $worksheet->cellExists($namedCell); + self::assertFalse($cellExists); + } + + public function testCellExistsRange(): void + { + $namedRange = 'Range1'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cell coordinate can not be a range of cells'); + + $worksheet = $this->spreadsheet->getActiveSheet(); + $worksheet->cellExists($namedRange); + } + + public function testGetCell(): void + { + $namedCell = 'GREETING'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cell = $worksheet->getCell($namedCell); + self::assertSame('Hello', $cell->getValue()); + } + + public function testGetCellNotExists(): void + { + $namedCell = 'GOODBYE'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Invalid cell coordinate {$namedCell}"); + + $worksheet = $this->spreadsheet->getActiveSheet(); + $worksheet->getCell($namedCell); + } + + public function testGetCellInvalidScope(): void + { + $namedCell = 'Result'; + $ucNamedCell = strtoupper($namedCell); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Invalid cell coordinate {$ucNamedCell}"); + + $worksheet = $this->spreadsheet->getActiveSheet(); + $worksheet->getCell($namedCell); + } + + public function testGetCellLocalScoped(): void + { + $namedCell = 'Result'; + + $this->spreadsheet->setActiveSheetIndexByName('Sheet2'); + $worksheet = $this->spreadsheet->getActiveSheet(); + $cell = $worksheet->getCell($namedCell); + self::assertSame(8, $cell->getCalculatedValue()); + } + + public function testGetCellNamedFormula(): void + { + $namedCell = 'Result'; + + $this->spreadsheet->setActiveSheetIndexByName('Sheet2'); + $worksheet = $this->spreadsheet->getActiveSheet(); + $cell = $worksheet->getCell($namedCell); + self::assertSame(8, $cell->getCalculatedValue()); + } + + public function testGetCellWithNamedRange(): void + { + $namedCell = 'Range1'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cell coordinate can not be a range of cells'); + + $worksheet = $this->spreadsheet->getActiveSheet(); + $worksheet->getCell($namedCell); + } + + public function testNamedRangeToArray(): void + { + $namedRange = 'Range1'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $rangeData = $worksheet->namedRangeToArray($namedRange); + self::assertSame([[1, 2, 3]], $rangeData); + } + + public function testInvalidNamedRangeToArray(): void + { + $namedRange = 'Range2'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Named Range {$namedRange} does not exist"); + + $worksheet = $this->spreadsheet->getActiveSheet(); + $rangeData = $worksheet->namedRangeToArray($namedRange); + self::assertSame([[1, 2, 3]], $rangeData); + } +} diff --git a/tests/data/Worksheet/namedRangeTest.xlsx b/tests/data/Worksheet/namedRangeTest.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a4383bb0b1b6839889a3f7725ade8214db118e8c GIT binary patch literal 9812 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 PRLBn&B2q!>-_HIYb%x#_ literal 0 HcmV?d00001