Skip to content

Commit 16953e2

Browse files
author
Mark Baker
authored
Adjust Cell Reference regexp in Calculation Engine to handle worksheet names containing quotes (#2617)
* Capture of worksheet name in Calculation Engine cell references modified to handle apostrophe and quote marks, and made non-greedy to avoid ensure that multiple quoted worksheet names in a formula aren't all captured in one go * Split some of the unit tests into separate test classes
1 parent 3c57d9e commit 16953e2

File tree

4 files changed

+195
-87
lines changed

4 files changed

+195
-87
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ class Calculation
3131
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
3232
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
3333
// Cell reference (cell or range of cells, with or without a sheet reference)
34-
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
34+
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
3535
// Cell reference (with or without a sheet reference) ensuring absolute/relative
36-
const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
37-
const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?(\$?[a-z]{1,3})):(?![.*])';
38-
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
36+
const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
37+
const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
38+
const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
3939
// Cell reference (with or without a sheet reference) ensuring absolute/relative
4040
// Cell ranges ensuring absolute/relative
4141
const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
4242
const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})';
4343
// Defined Names: Named Range of cells, or Named Formulae
44-
const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)';
44+
const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)';
4545
// Error
4646
const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
4747

@@ -4199,7 +4199,8 @@ private function internalParseFormula($formula, ?Cell $cell = null)
41994199
$worksheet = $pCellParent->getTitle();
42004200
$val = "'{$worksheet}'!{$val}";
42014201
}
4202-
4202+
// unescape any apostrophes or double quotes in worksheet name
4203+
$val = str_replace(["''", '""'], ["'", '"'], $val);
42034204
$outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
42044205

42054206
$output[] = $outputItem;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class CalclationFunctionListTest extends TestCase
11+
{
12+
/**
13+
* @var string
14+
*/
15+
private $compatibilityMode;
16+
17+
/**
18+
* @var string
19+
*/
20+
private $locale;
21+
22+
protected function setUp(): void
23+
{
24+
$this->compatibilityMode = Functions::getCompatibilityMode();
25+
$calculation = Calculation::getInstance();
26+
$this->locale = $calculation->getLocale();
27+
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
28+
}
29+
30+
protected function tearDown(): void
31+
{
32+
Functions::setCompatibilityMode($this->compatibilityMode);
33+
$calculation = Calculation::getInstance();
34+
$calculation->setLocale($this->locale);
35+
}
36+
37+
/**
38+
* @dataProvider providerGetFunctions
39+
*
40+
* @param string $category
41+
* @param array|string $functionCall
42+
* @param string $argumentCount
43+
*/
44+
public function testGetFunctions($category, $functionCall, $argumentCount): void
45+
{
46+
self::assertIsCallable($functionCall);
47+
}
48+
49+
public function providerGetFunctions(): array
50+
{
51+
return Calculation::getInstance()->getFunctions();
52+
}
53+
54+
public function testIsImplemented(): void
55+
{
56+
$calculation = Calculation::getInstance();
57+
self::assertFalse($calculation->isImplemented('non-existing-function'));
58+
self::assertFalse($calculation->isImplemented('AREAS'));
59+
self::assertTrue($calculation->isImplemented('coUNt'));
60+
self::assertTrue($calculation->isImplemented('abs'));
61+
}
62+
63+
public function testUnknownFunction(): void
64+
{
65+
$workbook = new Spreadsheet();
66+
$sheet = $workbook->getActiveSheet();
67+
$sheet->setCellValue('A1', '=gzorg()');
68+
$sheet->setCellValue('A2', '=mode.gzorg(1)');
69+
$sheet->setCellValue('A3', '=gzorg(1,2)');
70+
$sheet->setCellValue('A4', '=3+IF(gzorg(),1,2)');
71+
self::assertEquals('#NAME?', $sheet->getCell('A1')->getCalculatedValue());
72+
self::assertEquals('#NAME?', $sheet->getCell('A2')->getCalculatedValue());
73+
self::assertEquals('#NAME?', $sheet->getCell('A3')->getCalculatedValue());
74+
self::assertEquals('#NAME?', $sheet->getCell('A4')->getCalculatedValue());
75+
}
76+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class CalculationSettingsTest extends TestCase
10+
{
11+
/**
12+
* @var string
13+
*/
14+
private $compatibilityMode;
15+
16+
/**
17+
* @var string
18+
*/
19+
private $locale;
20+
21+
protected function setUp(): void
22+
{
23+
$this->compatibilityMode = Functions::getCompatibilityMode();
24+
$calculation = Calculation::getInstance();
25+
$this->locale = $calculation->getLocale();
26+
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
27+
}
28+
29+
protected function tearDown(): void
30+
{
31+
Functions::setCompatibilityMode($this->compatibilityMode);
32+
$calculation = Calculation::getInstance();
33+
$calculation->setLocale($this->locale);
34+
}
35+
36+
/**
37+
* @dataProvider providerCanLoadAllSupportedLocales
38+
*
39+
* @param string $locale
40+
*/
41+
public function testCanLoadAllSupportedLocales($locale): void
42+
{
43+
$calculation = Calculation::getInstance();
44+
self::assertTrue($calculation->setLocale($locale));
45+
}
46+
47+
public function testInvalidLocaleReturnsFalse(): void
48+
{
49+
$calculation = Calculation::getInstance();
50+
self::assertFalse($calculation->setLocale('xx'));
51+
}
52+
53+
public function providerCanLoadAllSupportedLocales(): array
54+
{
55+
return [
56+
['bg'],
57+
['cs'],
58+
['da'],
59+
['de'],
60+
['en_us'],
61+
['es'],
62+
['fi'],
63+
['fr'],
64+
['hu'],
65+
['it'],
66+
['nl'],
67+
['nb'],
68+
['pl'],
69+
['pt'],
70+
['pt_br'],
71+
['ru'],
72+
['sv'],
73+
['tr'],
74+
];
75+
}
76+
}

tests/PhpSpreadsheetTests/Calculation/CalculationTest.php

Lines changed: 36 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -58,73 +58,6 @@ public function providerBinaryComparisonOperation(): array
5858
return require 'tests/data/CalculationBinaryComparisonOperation.php';
5959
}
6060

61-
/**
62-
* @dataProvider providerGetFunctions
63-
*
64-
* @param string $category
65-
* @param array|string $functionCall
66-
* @param string $argumentCount
67-
*/
68-
public function testGetFunctions($category, $functionCall, $argumentCount): void
69-
{
70-
self::assertIsCallable($functionCall);
71-
}
72-
73-
public function providerGetFunctions(): array
74-
{
75-
return Calculation::getInstance()->getFunctions();
76-
}
77-
78-
public function testIsImplemented(): void
79-
{
80-
$calculation = Calculation::getInstance();
81-
self::assertFalse($calculation->isImplemented('non-existing-function'));
82-
self::assertFalse($calculation->isImplemented('AREAS'));
83-
self::assertTrue($calculation->isImplemented('coUNt'));
84-
self::assertTrue($calculation->isImplemented('abs'));
85-
}
86-
87-
/**
88-
* @dataProvider providerCanLoadAllSupportedLocales
89-
*
90-
* @param string $locale
91-
*/
92-
public function testCanLoadAllSupportedLocales($locale): void
93-
{
94-
$calculation = Calculation::getInstance();
95-
self::assertTrue($calculation->setLocale($locale));
96-
}
97-
98-
public function testInvalidLocaleReturnsFalse(): void
99-
{
100-
$calculation = Calculation::getInstance();
101-
self::assertFalse($calculation->setLocale('xx'));
102-
}
103-
104-
public function providerCanLoadAllSupportedLocales(): array
105-
{
106-
return [
107-
['bg'],
108-
['cs'],
109-
['da'],
110-
['de'],
111-
['en_us'],
112-
['es'],
113-
['fi'],
114-
['fr'],
115-
['hu'],
116-
['it'],
117-
['nl'],
118-
['nb'],
119-
['pl'],
120-
['pt'],
121-
['pt_br'],
122-
['ru'],
123-
['sv'],
124-
['tr'],
125-
];
126-
}
127-
12861
public function testDoesHandleXlfnFunctions(): void
12962
{
13063
$calculation = Calculation::getInstance();
@@ -195,6 +128,42 @@ public function testCellWithDdeExpresion(): void
195128
self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
196129
}
197130

131+
public function testFormulaReferencingWorksheetWithEscapedApostrophe(): void
132+
{
133+
$spreadsheet = new Spreadsheet();
134+
$workSheet = $spreadsheet->getActiveSheet();
135+
$workSheet->setTitle("Catégorie d'absence");
136+
137+
$workSheet->setCellValue('A1', 'HELLO');
138+
$workSheet->setCellValue('B1', ' ');
139+
$workSheet->setCellValue('C1', 'WORLD');
140+
$workSheet->setCellValue(
141+
'A2',
142+
"=CONCAT('Catégorie d''absence'!A1, 'Catégorie d''absence'!B1, 'Catégorie d''absence'!C1)"
143+
);
144+
145+
$cellValue = $workSheet->getCell('A2')->getCalculatedValue();
146+
self::assertSame('HELLO WORLD', $cellValue);
147+
}
148+
149+
public function testFormulaReferencingWorksheetWithUnescapedApostrophe(): void
150+
{
151+
$spreadsheet = new Spreadsheet();
152+
$workSheet = $spreadsheet->getActiveSheet();
153+
$workSheet->setTitle("Catégorie d'absence");
154+
155+
$workSheet->setCellValue('A1', 'HELLO');
156+
$workSheet->setCellValue('B1', ' ');
157+
$workSheet->setCellValue('C1', 'WORLD');
158+
$workSheet->setCellValue(
159+
'A2',
160+
"=CONCAT('Catégorie d'absence'!A1, 'Catégorie d'absence'!B1, 'Catégorie d'absence'!C1)"
161+
);
162+
163+
$cellValue = $workSheet->getCell('A2')->getCalculatedValue();
164+
self::assertSame('HELLO WORLD', $cellValue);
165+
}
166+
198167
public function testCellWithFormulaTwoIndirect(): void
199168
{
200169
$spreadsheet = new Spreadsheet();
@@ -390,18 +359,4 @@ public function dataProviderBranchPruningFullExecution(): array
390359
{
391360
return require 'tests/data/Calculation/Calculation.php';
392361
}
393-
394-
public function testUnknownFunction(): void
395-
{
396-
$workbook = new Spreadsheet();
397-
$sheet = $workbook->getActiveSheet();
398-
$sheet->setCellValue('A1', '=gzorg()');
399-
$sheet->setCellValue('A2', '=mode.gzorg(1)');
400-
$sheet->setCellValue('A3', '=gzorg(1,2)');
401-
$sheet->setCellValue('A4', '=3+IF(gzorg(),1,2)');
402-
self::assertEquals('#NAME?', $sheet->getCell('A1')->getCalculatedValue());
403-
self::assertEquals('#NAME?', $sheet->getCell('A2')->getCalculatedValue());
404-
self::assertEquals('#NAME?', $sheet->getCell('A3')->getCalculatedValue());
405-
self::assertEquals('#NAME?', $sheet->getCell('A4')->getCalculatedValue());
406-
}
407362
}

0 commit comments

Comments
 (0)