Skip to content

Commit

Permalink
Xls Writer Parser Handle Boolean Literals as Function Arguments
Browse files Browse the repository at this point in the history
Fix PHPOffice#3369. The parser had failed to account for `TRUE` and `FALSE` when supplied as arguments to a function.

I had hoped to be able to do something about its inability to handle defined names as well. I failed. I think another section might need to be added to the Writer output which specifies the defined names. I haven't yet located any suitable documentation.
  • Loading branch information
oleibman committed Feb 22, 2023
1 parent 33eefe7 commit a84684b
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 87 deletions.
205 changes: 131 additions & 74 deletions src/PhpSpreadsheet/Writer/Xls/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -499,43 +499,60 @@ private function convert($token)
{
if (preg_match('/"([^"]|""){0,255}"/', $token)) {
return $this->convertString($token);
} elseif (is_numeric($token)) {
}
if (is_numeric($token)) {
return $this->convertNumber($token);
}
// match references like A1 or $A$1
} elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) {
if (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) {
return $this->convertRef2d($token);
}
// match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1
} elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) {
if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) {
return $this->convertRef3d($token);
}
// match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1
} elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) {
if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) {
return $this->convertRef3d($token);
}
// match ranges like A1:B2 or $A$1:$B$2
} elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) {
if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) {
return $this->convertRange2d($token);
}
// match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2
} elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) {
if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) {
return $this->convertRange3d($token);
}
// match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2
} elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) {
if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) {
return $this->convertRange3d($token);
}
// operators (including parentheses)
} elseif (isset($this->ptg[$token])) {
if (isset($this->ptg[$token])) {
return pack('C', $this->ptg[$token]);
}
// match error codes
} elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') {
if (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') {
return $this->convertError($token);
} elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
}
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
return $this->convertDefinedName($token);
}
// commented so argument number can be processed correctly. See toReversePolish().
/*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token))
{
return($this->convertFunction($token, $this->_func_args));
}*/
/*if (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token))
{
return($this->convertFunction($token, $this->_func_args));
}*/
// if it's an argument, ignore the token (the argument remains)
} elseif ($token == 'arg') {
if ($token == 'arg') {
return '';
}
if (preg_match('/^true$/i', $token)) {
return $this->convertBool(1);
}
if (preg_match('/^false$/i', $token)) {
return $this->convertBool(0);
}

// TODO: use real error codes
throw new WriterException("Unknown token $token");
Expand Down Expand Up @@ -563,6 +580,11 @@ private function convertNumber($num)
return pack('Cd', $this->ptg['ptgNum'], $num);
}

private function convertBool(int $num): string
{
return pack('CC', $this->ptg['ptgBool'], $num);
}

/**
* Convert a string token to ptgStr.
*
Expand Down Expand Up @@ -747,27 +769,32 @@ private function convertError($errorCode)
return pack('C', 0xFF);
}

/** @var bool */
private $tryDefinedName = false;

private function convertDefinedName(string $name): string
{
if (strlen($name) > 255) {
throw new WriterException('Defined Name is too long');
}

throw new WriterException('Cannot yet write formulae with defined names to Xls');
/*
$nameReference = 1;
foreach ($this->spreadsheet->getDefinedNames() as $definedName) {
if ($name === $definedName->getName()) {
break;
if ($this->tryDefinedName) {
// @codeCoverageIgnoreStart
$nameReference = 1;
foreach ($this->spreadsheet->getDefinedNames() as $definedName) {
if ($name === $definedName->getName()) {
break;
}
++$nameReference;
}
++$nameReference;
}

$ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference);
$ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference);

return $ptgRef;
// @codeCoverageIgnoreEnd
}

return $ptgRef;
*/
throw new WriterException('Cannot yet write formulae with defined names to Xls');
}

/**
Expand Down Expand Up @@ -1052,48 +1079,64 @@ private function match($token)
}

return $token;
}

default:
// if it's a reference A1 or $A$1 or $A1 or A$1
if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) {
return $token;
} elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
// If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
return $token;
} elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
// If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
return $token;
} elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) {
// if it's a range A1:A2 or $A$1:$A$2
return $token;
} elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) {
// If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2
return $token;
} elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) {
// If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2
return $token;
} elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) {
// If it's a number (check that it's not a sheet name or range)
return $token;
} elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) {
// If it's a string (of maximum 255 characters)
return $token;
} elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') {
// If it's an error code
return $token;
} elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) {
// if it's a function call
return $token;
} elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
return $token;
} elseif (substr($token, -1) === ')') {
// It's an argument of some description (e.g. a named range),
// precise nature yet to be determined
return $token;
}

return '';
// if it's a reference A1 or $A$1 or $A1 or A$1
if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) {
return $token;
}
// If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
return $token;
}
// If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
return $token;
}
// if it's a range A1:A2 or $A$1:$A$2
if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) {
return $token;
}
// If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2
if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) {
return $token;
}
// If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2
if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) {
return $token;
}
// If it's a number (check that it's not a sheet name or range)
if (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) {
return $token;
}
// If it's a string (of maximum 255 characters)
if (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) {
return $token;
}
// If it's an error code
if (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') {
return $token;
}
// if it's a function call
if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) {
return $token;
}
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
return $token;
}
if (preg_match('/^true$/i', $token) && ($this->lookAhead === ')' || $this->lookAhead === ',')) {
return $token;
}
if (preg_match('/^false$/i', $token) && ($this->lookAhead === ')' || $this->lookAhead === ',')) {
return $token;
}
if (substr($token, -1) === ')') {
// It's an argument of some description (e.g. a named range),
// precise nature yet to be determined
return $token;
}

return '';
}

/**
Expand Down Expand Up @@ -1295,19 +1338,22 @@ private function fact()
$this->advance();

return $result;
} elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) {
}
if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) {
// If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
$result = $this->createTree($this->currentToken, '', '');
$this->advance();

return $result;
} elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) {
}
if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) {
// If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
$result = $this->createTree($this->currentToken, '', '');
$this->advance();

return $result;
} elseif (
}
if (
preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) ||
preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken)
) {
Expand All @@ -1317,21 +1363,24 @@ private function fact()
$this->advance();

return $result;
} elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) {
}
if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) {
// If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2)
// must be an error?
$result = $this->createTree($this->currentToken, '', '');
$this->advance();

return $result;
} elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) {
}
if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) {
// If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2)
// must be an error?
$result = $this->createTree($this->currentToken, '', '');
$this->advance();

return $result;
} elseif (is_numeric($this->currentToken)) {
}
if (is_numeric($this->currentToken)) {
// If it's a number or a percent
if ($this->lookAhead === '%') {
$result = $this->createTree('ptgPercent', $this->currentToken, '');
Expand All @@ -1342,15 +1391,23 @@ private function fact()
$this->advance();

return $result;
} elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) {
}
if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) {
// if it's a function call
return $this->func();
} elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) {
}
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) {
$result = $this->createTree('ptgName', $this->currentToken, '');
$this->advance();

return $result;
}
if (preg_match('/^true|false$/i', $this->currentToken)) {
$result = $this->createTree($this->currentToken, '', '');
$this->advance();

return $result;
}

throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter);
}
Expand Down
17 changes: 17 additions & 0 deletions src/PhpSpreadsheet/Writer/Xls/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,19 @@ private function writeBoolErr($row, $col, $value, $isError, $xfIndex)
const WRITE_FORMULA_RANGE = -2;
const WRITE_FORMULA_EXCEPTION = -3;

/** @var bool */
private static $allowThrow = false;

public static function setAllowThrow(bool $allowThrow): void
{
self::$allowThrow = $allowThrow;
}

public static function getAllowThrow(): bool
{
return self::$allowThrow;
}

/**
* Write a formula to the specified row and column (zero indexed).
* The textual representation of the formula is passed to the parser in
Expand Down Expand Up @@ -892,6 +905,10 @@ private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue)

return self::WRITE_FORMULA_NORMAL;
} catch (PhpSpreadsheetException $e) {
if (self::$allowThrow) {
throw $e;
}

return self::WRITE_FORMULA_EXCEPTION;
}
}
Expand Down
Loading

0 comments on commit a84684b

Please sign in to comment.