Merge branch 'master' into htmledit
This commit is contained in:
commit
360c8d8284
|
@ -38,7 +38,7 @@ jobs:
|
||||||
- php ocular.phar code-coverage:upload --format=php-clover tests/coverage-clover.xml
|
- php ocular.phar code-coverage:upload --format=php-clover tests/coverage-clover.xml
|
||||||
|
|
||||||
- stage: API documentations
|
- stage: API documentations
|
||||||
if: tag is present AND branch = master
|
if: branch = master
|
||||||
php: 7.4
|
php: 7.4
|
||||||
before_script:
|
before_script:
|
||||||
- curl -LO https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.0.0-rc/phpDocumentor.phar
|
- curl -LO https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.0.0-rc/phpDocumentor.phar
|
||||||
|
|
|
@ -5,12 +5,13 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com)
|
The format is based on [Keep a Changelog](https://keepachangelog.com)
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org).
|
and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
## [Unreleased]
|
## [1.13.0] - 2020-05-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292)
|
- Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292)
|
||||||
- Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468)
|
- Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468)
|
||||||
|
- Support protection of worksheet by a specific hash algorithm [#1485](https://github.com/PHPOffice/PhpSpreadsheet/pull/1485)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
- Several improvements in HTML writer [#1464](https://github.com/PHPOffice/PhpSpreadsheet/pull/1464)
|
- Several improvements in HTML writer [#1464](https://github.com/PHPOffice/PhpSpreadsheet/pull/1464)
|
||||||
- Fix incorrect behaviour when saving XLSX file with drawings [#1462](https://github.com/PHPOffice/PhpSpreadsheet/pull/1462),
|
- Fix incorrect behaviour when saving XLSX file with drawings [#1462](https://github.com/PHPOffice/PhpSpreadsheet/pull/1462),
|
||||||
- Fix Crash while trying setting a cell the value "123456\n" [#1476](https://github.com/PHPOffice/PhpSpreadsheet/pull/1481)
|
- Fix Crash while trying setting a cell the value "123456\n" [#1476](https://github.com/PHPOffice/PhpSpreadsheet/pull/1481)
|
||||||
|
- Improved DATEDIF() function and reduced errors for Y and YM units [#1466](https://github.com/PHPOffice/PhpSpreadsheet/pull/1466)
|
||||||
|
- Stricter typing for mergeCells [#1494](https://github.com/PHPOffice/PhpSpreadsheet/pull/1494)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ the cell object will still retain its data values.
|
||||||
|
|
||||||
What does this mean? Consider the following code:
|
What does this mean? Consider the following code:
|
||||||
|
|
||||||
```
|
```php
|
||||||
$spreadSheet = new Spreadsheet();
|
$spreadSheet = new Spreadsheet();
|
||||||
$workSheet = $spreadSheet->getActiveSheet();
|
$workSheet = $spreadSheet->getActiveSheet();
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ was a formula.
|
||||||
|
|
||||||
To do this, you need to "escape" the value by setting it as "quoted text".
|
To do this, you need to "escape" the value by setting it as "quoted text".
|
||||||
|
|
||||||
```
|
```php
|
||||||
// Set cell A4 with a formula
|
// Set cell A4 with a formula
|
||||||
$spreadsheet->getActiveSheet()->setCellValue(
|
$spreadsheet->getActiveSheet()->setCellValue(
|
||||||
'A4',
|
'A4',
|
||||||
|
|
|
@ -55,7 +55,7 @@ However, there may be times when you don't want this, perhaps you've changed
|
||||||
the underlying data and need to re-evaluate the same formula with that new
|
the underlying data and need to re-evaluate the same formula with that new
|
||||||
data.
|
data.
|
||||||
|
|
||||||
```
|
```php
|
||||||
Calculation::getInstance($spreadsheet)->disableCalculationCache();
|
Calculation::getInstance($spreadsheet)->disableCalculationCache();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ Will disable calculation caching, and flush the current calculation cache.
|
||||||
|
|
||||||
If you want only to flush the cache, then you can call
|
If you want only to flush the cache, then you can call
|
||||||
|
|
||||||
```
|
```php
|
||||||
Calculation::getInstance($spreadsheet)->clearCalculationCache();
|
Calculation::getInstance($spreadsheet)->clearCalculationCache();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -919,29 +919,53 @@ disallow inserting rows on a specific sheet, disallow sorting, ...
|
||||||
- Cell: offers the option to lock/unlock a cell as well as show/hide
|
- Cell: offers the option to lock/unlock a cell as well as show/hide
|
||||||
the internal formula.
|
the internal formula.
|
||||||
|
|
||||||
|
**Make sure you enable worksheet protection if you need any of the
|
||||||
|
worksheet or cell protection features!** This can be done using the following
|
||||||
|
code:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$spreadsheet->getActiveSheet()->getProtection()->setSheet(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Document
|
||||||
|
|
||||||
An example on setting document security:
|
An example on setting document security:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$spreadsheet->getSecurity()->setLockWindows(true);
|
$security = $spreadsheet->getSecurity();
|
||||||
$spreadsheet->getSecurity()->setLockStructure(true);
|
$security->setLockWindows(true);
|
||||||
$spreadsheet->getSecurity()->setWorkbookPassword("PhpSpreadsheet");
|
$security->setLockStructure(true);
|
||||||
|
$security->setWorkbookPassword("PhpSpreadsheet");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Worksheet
|
||||||
|
|
||||||
An example on setting worksheet security:
|
An example on setting worksheet security:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$spreadsheet->getActiveSheet()
|
$protection = $spreadsheet->getActiveSheet()->getProtection();
|
||||||
->getProtection()->setPassword('PhpSpreadsheet');
|
$protection->setPassword('PhpSpreadsheet');
|
||||||
$spreadsheet->getActiveSheet()
|
$protection->setSheet(true);
|
||||||
->getProtection()->setSheet(true);
|
$protection->setSort(true);
|
||||||
$spreadsheet->getActiveSheet()
|
$protection->setInsertRows(true);
|
||||||
->getProtection()->setSort(true);
|
$protection->setFormatCells(true);
|
||||||
$spreadsheet->getActiveSheet()
|
|
||||||
->getProtection()->setInsertRows(true);
|
|
||||||
$spreadsheet->getActiveSheet()
|
|
||||||
->getProtection()->setFormatCells(true);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If writing Xlsx files you can specify the algorithm used to hash the password
|
||||||
|
before calling `setPassword()` like so:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$protection = $spreadsheet->getActiveSheet()->getProtection();
|
||||||
|
$protection->setAlgorithm(Protection::ALGORITHM_SHA_512);
|
||||||
|
$protection->setSpinCount(20000);
|
||||||
|
$protection->setPassword('PhpSpreadsheet');
|
||||||
|
```
|
||||||
|
|
||||||
|
The salt should **not** be set manually and will be automatically generated
|
||||||
|
when setting a new password.
|
||||||
|
|
||||||
|
### Cell
|
||||||
|
|
||||||
An example on setting cell security:
|
An example on setting cell security:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
@ -950,14 +974,30 @@ $spreadsheet->getActiveSheet()->getStyle('B1')
|
||||||
->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED);
|
->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Make sure you enable worksheet protection if you need any of the
|
## Reading protected spreadsheet
|
||||||
worksheet protection features!** This can be done using the following
|
|
||||||
code:
|
Spreadsheets that are protected as described above can always be read by
|
||||||
|
PhpSpreadsheet. There is no need to know the password or do anything special in
|
||||||
|
order to read a protected file.
|
||||||
|
|
||||||
|
However if you need to implement a password verification mechanism, you can use the
|
||||||
|
following helper method:
|
||||||
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$spreadsheet->getActiveSheet()->getProtection()->setSheet(true);
|
$protection = $spreadsheet->getActiveSheet()->getProtection();
|
||||||
|
$allowed = $protection->verify('my password');
|
||||||
|
|
||||||
|
if ($allowed) {
|
||||||
|
doSomething();
|
||||||
|
} else {
|
||||||
|
throw new Exception('Incorrect password');
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you need to completely prevent reading a file by any tool, including PhpSpreadsheet,
|
||||||
|
then you are looking for "encryption", not "protection".
|
||||||
|
|
||||||
## Setting data validation on a cell
|
## Setting data validation on a cell
|
||||||
|
|
||||||
Data validation is a powerful feature of Xlsx. It allows to specify an
|
Data validation is a powerful feature of Xlsx. It allows to specify an
|
||||||
|
|
|
@ -3456,10 +3456,8 @@ class Calculation
|
||||||
if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
|
if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
|
||||||
$opCharacter .= $formula[++$index];
|
$opCharacter .= $formula[++$index];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
|
// Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
|
||||||
$isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
|
$isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
|
||||||
|
|
||||||
if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus?
|
if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus?
|
||||||
// Put a negation on the stack
|
// Put a negation on the stack
|
||||||
$stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
$stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||||
|
@ -3627,7 +3625,6 @@ class Calculation
|
||||||
$expectingOperand = false;
|
$expectingOperand = false;
|
||||||
$val = $match[1];
|
$val = $match[1];
|
||||||
$length = strlen($val);
|
$length = strlen($val);
|
||||||
|
|
||||||
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
|
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
|
||||||
$val = preg_replace('/\s/u', '', $val);
|
$val = preg_replace('/\s/u', '', $val);
|
||||||
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
||||||
|
@ -3662,7 +3659,6 @@ class Calculation
|
||||||
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
|
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
|
||||||
// Watch for this case-change when modifying to allow cell references in different worksheets...
|
// Watch for this case-change when modifying to allow cell references in different worksheets...
|
||||||
// Should only be applied to the actual cell column, not the worksheet name
|
// Should only be applied to the actual cell column, not the worksheet name
|
||||||
|
|
||||||
// If the last entry on the stack was a : operator, then we have a cell range reference
|
// If the last entry on the stack was a : operator, then we have a cell range reference
|
||||||
$testPrevOp = $stack->last(1);
|
$testPrevOp = $stack->last(1);
|
||||||
if ($testPrevOp !== null && $testPrevOp['value'] == ':') {
|
if ($testPrevOp !== null && $testPrevOp['value'] == ':') {
|
||||||
|
@ -3719,6 +3715,8 @@ class Calculation
|
||||||
}
|
}
|
||||||
|
|
||||||
$localeConstant = false;
|
$localeConstant = false;
|
||||||
|
$stackItemType = 'Value';
|
||||||
|
$stackItemReference = null;
|
||||||
if ($opCharacter == self::FORMULA_STRING_QUOTE) {
|
if ($opCharacter == self::FORMULA_STRING_QUOTE) {
|
||||||
// UnEscape any quotes within the string
|
// UnEscape any quotes within the string
|
||||||
$val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
|
$val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
|
||||||
|
@ -3729,12 +3727,17 @@ class Calculation
|
||||||
$val = (int) $val;
|
$val = (int) $val;
|
||||||
}
|
}
|
||||||
} elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
|
} elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
|
||||||
|
$stackItemType = 'Constant';
|
||||||
$excelConstant = trim(strtoupper($val));
|
$excelConstant = trim(strtoupper($val));
|
||||||
$val = self::$excelConstants[$excelConstant];
|
$val = self::$excelConstants[$excelConstant];
|
||||||
} elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
|
} elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
|
||||||
|
$stackItemType = 'Constant';
|
||||||
$val = self::$excelConstants[$localeConstant];
|
$val = self::$excelConstants[$localeConstant];
|
||||||
|
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', $val, $match)) {
|
||||||
|
$stackItemType = 'Named Range';
|
||||||
|
$stackItemReference = $val;
|
||||||
}
|
}
|
||||||
$details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
$details = $stack->getStackItem($stackItemType, $val, $stackItemReference, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||||
if ($localeConstant) {
|
if ($localeConstant) {
|
||||||
$details['localeValue'] = $localeConstant;
|
$details['localeValue'] = $localeConstant;
|
||||||
}
|
}
|
||||||
|
@ -3776,8 +3779,12 @@ class Calculation
|
||||||
}
|
}
|
||||||
// If we're expecting an operator, but only have a space between the previous and next operands (and both are
|
// If we're expecting an operator, but only have a space between the previous and next operands (and both are
|
||||||
// Cell References) then we have an INTERSECTION operator
|
// Cell References) then we have an INTERSECTION operator
|
||||||
if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
|
if (($expectingOperator) &&
|
||||||
($output[count($output) - 1]['type'] == 'Cell Reference')) {
|
((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
|
||||||
|
($output[count($output) - 1]['type'] == 'Cell Reference') ||
|
||||||
|
(preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', substr($formula, $index), $match)) &&
|
||||||
|
($output[count($output) - 1]['type'] == 'Named Range' || $output[count($output) - 1]['type'] == 'Value')
|
||||||
|
)) {
|
||||||
while ($stack->count() > 0 &&
|
while ($stack->count() > 0 &&
|
||||||
($o2 = $stack->last()) &&
|
($o2 = $stack->last()) &&
|
||||||
isset(self::$operators[$o2['value']]) &&
|
isset(self::$operators[$o2['value']]) &&
|
||||||
|
@ -3840,7 +3847,6 @@ class Calculation
|
||||||
$fakedForBranchPruning = [];
|
$fakedForBranchPruning = [];
|
||||||
// help us to know when pruning ['branchTestId' => true/false]
|
// help us to know when pruning ['branchTestId' => true/false]
|
||||||
$branchStore = [];
|
$branchStore = [];
|
||||||
|
|
||||||
// Loop through each token in turn
|
// Loop through each token in turn
|
||||||
foreach ($tokens as $tokenData) {
|
foreach ($tokens as $tokenData) {
|
||||||
$token = $tokenData['value'];
|
$token = $tokenData['value'];
|
||||||
|
|
|
@ -668,30 +668,19 @@ class DateTime
|
||||||
$endMonths = $PHPEndDateObject->format('n');
|
$endMonths = $PHPEndDateObject->format('n');
|
||||||
$endYears = $PHPEndDateObject->format('Y');
|
$endYears = $PHPEndDateObject->format('Y');
|
||||||
|
|
||||||
|
$PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject);
|
||||||
|
|
||||||
switch ($unit) {
|
switch ($unit) {
|
||||||
case 'D':
|
case 'D':
|
||||||
$retVal = (int) $difference;
|
$retVal = (int) $difference;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'M':
|
case 'M':
|
||||||
$retVal = (int) ($endMonths - $startMonths) + ((int) ($endYears - $startYears) * 12);
|
$retVal = (int) 12 * $PHPDiffDateObject->format('%y') + $PHPDiffDateObject->format('%m');
|
||||||
// We're only interested in full months
|
|
||||||
if ($endDays < $startDays) {
|
|
||||||
--$retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'Y':
|
case 'Y':
|
||||||
$retVal = (int) ($endYears - $startYears);
|
$retVal = (int) $PHPDiffDateObject->format('%y');
|
||||||
// We're only interested in full months
|
|
||||||
if ($endMonths < $startMonths) {
|
|
||||||
--$retVal;
|
|
||||||
} elseif (($endMonths == $startMonths) && ($endDays < $startDays)) {
|
|
||||||
// Remove start month
|
|
||||||
--$retVal;
|
|
||||||
// Remove end month
|
|
||||||
--$retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'MD':
|
case 'MD':
|
||||||
|
@ -701,19 +690,12 @@ class DateTime
|
||||||
$adjustDays = $PHPEndDateObject->format('j');
|
$adjustDays = $PHPEndDateObject->format('j');
|
||||||
$retVal += ($adjustDays - $startDays);
|
$retVal += ($adjustDays - $startDays);
|
||||||
} else {
|
} else {
|
||||||
$retVal = $endDays - $startDays;
|
$retVal = (int) $PHPDiffDateObject->format('%d');
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'YM':
|
case 'YM':
|
||||||
$retVal = (int) ($endMonths - $startMonths);
|
$retVal = (int) $PHPDiffDateObject->format('%m');
|
||||||
if ($retVal < 0) {
|
|
||||||
$retVal += 12;
|
|
||||||
}
|
|
||||||
// We're only interested in full months
|
|
||||||
if ($endDays < $startDays) {
|
|
||||||
--$retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'YD':
|
case 'YD':
|
||||||
|
|
|
@ -312,32 +312,59 @@ abstract class Coordinate
|
||||||
/**
|
/**
|
||||||
* Extract all cell references in range, which may be comprised of multiple cell ranges.
|
* Extract all cell references in range, which may be comprised of multiple cell ranges.
|
||||||
*
|
*
|
||||||
* @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25)
|
* @param string $cellRange Range: e.g. 'A1' or 'A1:C10' or 'A1:E10,A20:E25' or 'A1:E5 C3:G7' or 'A1:C1,A3:C3 B1:C3'
|
||||||
*
|
*
|
||||||
* @return array Array containing single cell references
|
* @return array Array containing single cell references
|
||||||
*/
|
*/
|
||||||
public static function extractAllCellReferencesInRange($pRange)
|
public static function extractAllCellReferencesInRange($cellRange): array
|
||||||
{
|
{
|
||||||
$returnValue = [];
|
[$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange);
|
||||||
|
|
||||||
// Explode spaces
|
$cells = [];
|
||||||
$cellBlocks = self::getCellBlocksFromRangeString($pRange);
|
foreach ($ranges as $range) {
|
||||||
foreach ($cellBlocks as $cellBlock) {
|
$cells[] = self::getReferencesForCellBlock($range);
|
||||||
$returnValue = array_merge($returnValue, self::getReferencesForCellBlock($cellBlock));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cells = self::processRangeSetOperators($operators, $cells);
|
||||||
|
|
||||||
|
if (empty($cells)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$cellList = array_merge(...$cells);
|
||||||
|
$cellList = self::sortCellReferenceArray($cellList);
|
||||||
|
|
||||||
|
return $cellList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function processRangeSetOperators(array $operators, array $cells): array
|
||||||
|
{
|
||||||
|
for ($offset = 0; $offset < count($operators); ++$offset) {
|
||||||
|
$operator = $operators[$offset];
|
||||||
|
if ($operator !== ' ') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cells[$offset] = array_intersect($cells[$offset], $cells[$offset + 1]);
|
||||||
|
unset($operators[$offset], $cells[$offset + 1]);
|
||||||
|
$operators = array_values($operators);
|
||||||
|
$cells = array_values($cells);
|
||||||
|
--$offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function sortCellReferenceArray(array $cellList): array
|
||||||
|
{
|
||||||
// Sort the result by column and row
|
// Sort the result by column and row
|
||||||
$sortKeys = [];
|
$sortKeys = [];
|
||||||
foreach (array_unique($returnValue) as $coord) {
|
foreach ($cellList as $coord) {
|
||||||
$column = '';
|
[$column, $row] = sscanf($coord, '%[A-Z]%d');
|
||||||
$row = 0;
|
|
||||||
|
|
||||||
sscanf($coord, '%[A-Z]%d', $column, $row);
|
|
||||||
$sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
|
$sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
|
||||||
}
|
}
|
||||||
ksort($sortKeys);
|
ksort($sortKeys);
|
||||||
|
|
||||||
// Return value
|
|
||||||
return array_values($sortKeys);
|
return array_values($sortKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,15 +509,25 @@ abstract class Coordinate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the individual cell blocks from a range string, splitting by space and removing any $ characters.
|
* Get the individual cell blocks from a range string, removing any $ characters.
|
||||||
|
* then splitting by operators and returning an array with ranges and operators.
|
||||||
*
|
*
|
||||||
* @param string $pRange
|
* @param string $rangeString
|
||||||
*
|
*
|
||||||
* @return string[]
|
* @return array[]
|
||||||
*/
|
*/
|
||||||
private static function getCellBlocksFromRangeString($pRange)
|
private static function getCellBlocksFromRangeString($rangeString)
|
||||||
{
|
{
|
||||||
return explode(' ', str_replace('$', '', strtoupper($pRange)));
|
$rangeString = str_replace('$', '', strtoupper($rangeString));
|
||||||
|
|
||||||
|
// split range sets on intersection (space) or union (,) operators
|
||||||
|
$tokens = preg_split('/([ ,])/', $rangeString, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
// separate the range sets and the operators into arrays
|
||||||
|
$split = array_chunk($tokens, 2);
|
||||||
|
$ranges = array_column($split, 0);
|
||||||
|
$operators = array_column($split, 1);
|
||||||
|
|
||||||
|
return [$ranges, $operators];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -763,13 +763,8 @@ class Xlsx extends BaseReader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) {
|
if ($xmlSheet) {
|
||||||
$docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true);
|
$this->readSheetProtection($docSheet, $xmlSheet);
|
||||||
if ($xmlSheet->protectedRanges->protectedRange) {
|
|
||||||
foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
|
|
||||||
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) {
|
if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) {
|
||||||
|
@ -2031,4 +2026,29 @@ class Xlsx extends BaseReader
|
||||||
|
|
||||||
return $workbookBasename;
|
return $workbookBasename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
|
||||||
|
{
|
||||||
|
if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
|
||||||
|
$protection = $docSheet->getProtection();
|
||||||
|
$protection->setAlgorithm($algorithmName);
|
||||||
|
|
||||||
|
if ($algorithmName) {
|
||||||
|
$protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
|
||||||
|
$protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
|
||||||
|
$protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
|
||||||
|
} else {
|
||||||
|
$protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($xmlSheet->protectedRanges->protectedRange) {
|
||||||
|
foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
|
||||||
|
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,41 @@
|
||||||
|
|
||||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Exception;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;
|
||||||
|
|
||||||
class PasswordHasher
|
class PasswordHasher
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Get algorithm name for PHP.
|
||||||
|
*/
|
||||||
|
private static function getAlgorithm(string $algorithmName): string
|
||||||
|
{
|
||||||
|
if (!$algorithmName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping between algorithm name in Excel and algorithm name in PHP
|
||||||
|
$mapping = [
|
||||||
|
Protection::ALGORITHM_MD2 => 'md2',
|
||||||
|
Protection::ALGORITHM_MD4 => 'md4',
|
||||||
|
Protection::ALGORITHM_MD5 => 'md5',
|
||||||
|
Protection::ALGORITHM_SHA_1 => 'sha1',
|
||||||
|
Protection::ALGORITHM_SHA_256 => 'sha256',
|
||||||
|
Protection::ALGORITHM_SHA_384 => 'sha384',
|
||||||
|
Protection::ALGORITHM_SHA_512 => 'sha512',
|
||||||
|
Protection::ALGORITHM_RIPEMD_128 => 'ripemd128',
|
||||||
|
Protection::ALGORITHM_RIPEMD_160 => 'ripemd160',
|
||||||
|
Protection::ALGORITHM_WHIRLPOOL => 'whirlpool',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (array_key_exists($algorithmName, $mapping)) {
|
||||||
|
return $mapping[$algorithmName];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Unsupported password algorithm: ' . $algorithmName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a password hash from a given string.
|
* Create a password hash from a given string.
|
||||||
*
|
*
|
||||||
|
@ -12,10 +45,8 @@ class PasswordHasher
|
||||||
* Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
|
* Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
|
||||||
*
|
*
|
||||||
* @param string $pPassword Password to hash
|
* @param string $pPassword Password to hash
|
||||||
*
|
|
||||||
* @return string Hashed password
|
|
||||||
*/
|
*/
|
||||||
public static function hashPassword($pPassword)
|
private static function defaultHashPassword(string $pPassword): string
|
||||||
{
|
{
|
||||||
$password = 0x0000;
|
$password = 0x0000;
|
||||||
$charPos = 1; // char position
|
$charPos = 1; // char position
|
||||||
|
@ -34,4 +65,36 @@ class PasswordHasher
|
||||||
|
|
||||||
return strtoupper(dechex($password));
|
return strtoupper(dechex($password));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a password hash from a given string by a specific algorithm.
|
||||||
|
*
|
||||||
|
* 2.4.2.4 ISO Write Protection Method
|
||||||
|
*
|
||||||
|
* @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f
|
||||||
|
*
|
||||||
|
* @param string $password Password to hash
|
||||||
|
* @param string $algorithm Hash algorithm used to compute the password hash value
|
||||||
|
* @param string $salt Pseudorandom string
|
||||||
|
* @param int $spinCount Number of times to iterate on a hash of a password
|
||||||
|
*
|
||||||
|
* @return string Hashed password
|
||||||
|
*/
|
||||||
|
public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string
|
||||||
|
{
|
||||||
|
$phpAlgorithm = self::getAlgorithm($algorithm);
|
||||||
|
if (!$phpAlgorithm) {
|
||||||
|
return self::defaultHashPassword($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
$saltValue = base64_decode($salt);
|
||||||
|
$encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
|
||||||
|
|
||||||
|
$hashValue = hash($phpAlgorithm, $saltValue . $encodedPassword, true);
|
||||||
|
for ($i = 0; $i < $spinCount; ++$i) {
|
||||||
|
$hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($hashValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,17 @@ use PhpOffice\PhpSpreadsheet\Shared\PasswordHasher;
|
||||||
|
|
||||||
class Protection
|
class Protection
|
||||||
{
|
{
|
||||||
|
const ALGORITHM_MD2 = 'MD2';
|
||||||
|
const ALGORITHM_MD4 = 'MD4';
|
||||||
|
const ALGORITHM_MD5 = 'MD5';
|
||||||
|
const ALGORITHM_SHA_1 = 'SHA-1';
|
||||||
|
const ALGORITHM_SHA_256 = 'SHA-256';
|
||||||
|
const ALGORITHM_SHA_384 = 'SHA-384';
|
||||||
|
const ALGORITHM_SHA_512 = 'SHA-512';
|
||||||
|
const ALGORITHM_RIPEMD_128 = 'RIPEMD-128';
|
||||||
|
const ALGORITHM_RIPEMD_160 = 'RIPEMD-160';
|
||||||
|
const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sheet.
|
* Sheet.
|
||||||
*
|
*
|
||||||
|
@ -119,12 +130,40 @@ class Protection
|
||||||
private $selectUnlockedCells = false;
|
private $selectUnlockedCells = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Password.
|
* Hashed password.
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private $password = '';
|
private $password = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $algorithm = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash value.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $hash = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Salt value.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $salt = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spin count.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $spinCount = 10000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Protection.
|
* Create a new Protection.
|
||||||
*/
|
*/
|
||||||
|
@ -542,7 +581,7 @@ class Protection
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Password (hashed).
|
* Get hashed password.
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
@ -562,13 +601,86 @@ class Protection
|
||||||
public function setPassword($pValue, $pAlreadyHashed = false)
|
public function setPassword($pValue, $pAlreadyHashed = false)
|
||||||
{
|
{
|
||||||
if (!$pAlreadyHashed) {
|
if (!$pAlreadyHashed) {
|
||||||
$pValue = PasswordHasher::hashPassword($pValue);
|
$salt = $this->generateSalt();
|
||||||
|
$this->setSalt($salt);
|
||||||
|
$pValue = PasswordHasher::hashPassword($pValue, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->password = $pValue;
|
$this->password = $pValue;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a pseudorandom string.
|
||||||
|
*/
|
||||||
|
private function generateSalt(): string
|
||||||
|
{
|
||||||
|
return base64_encode(random_bytes(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get algorithm name.
|
||||||
|
*/
|
||||||
|
public function getAlgorithm(): string
|
||||||
|
{
|
||||||
|
return $this->algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set algorithm name.
|
||||||
|
*/
|
||||||
|
public function setAlgorithm(string $algorithm): void
|
||||||
|
{
|
||||||
|
$this->algorithm = $algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get salt value.
|
||||||
|
*/
|
||||||
|
public function getSalt(): string
|
||||||
|
{
|
||||||
|
return $this->salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set salt value.
|
||||||
|
*/
|
||||||
|
public function setSalt(string $salt): void
|
||||||
|
{
|
||||||
|
$this->salt = $salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get spin count.
|
||||||
|
*/
|
||||||
|
public function getSpinCount(): int
|
||||||
|
{
|
||||||
|
return $this->spinCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set spin count.
|
||||||
|
*/
|
||||||
|
public function setSpinCount(int $spinCount): void
|
||||||
|
{
|
||||||
|
$this->spinCount = $spinCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the given non-hashed password can "unlock" the protection.
|
||||||
|
*/
|
||||||
|
public function verify(string $password): bool
|
||||||
|
{
|
||||||
|
if (!$this->isProtectionEnabled()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = PasswordHasher::hashPassword($password, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount());
|
||||||
|
|
||||||
|
return $this->getPassword() === $hash;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -187,7 +187,7 @@ class Worksheet implements IComparable
|
||||||
/**
|
/**
|
||||||
* Collection of merged cell ranges.
|
* Collection of merged cell ranges.
|
||||||
*
|
*
|
||||||
* @var array
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
private $mergeCells = [];
|
private $mergeCells = [];
|
||||||
|
|
||||||
|
@ -1747,7 +1747,7 @@ class Worksheet implements IComparable
|
||||||
/**
|
/**
|
||||||
* Get merge cells array.
|
* Get merge cells array.
|
||||||
*
|
*
|
||||||
* @return array[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
public function getMergeCells()
|
public function getMergeCells()
|
||||||
{
|
{
|
||||||
|
@ -1758,6 +1758,8 @@ class Worksheet implements IComparable
|
||||||
* Set merge cells array for the entire sheet. Use instead mergeCells() to merge
|
* Set merge cells array for the entire sheet. Use instead mergeCells() to merge
|
||||||
* a single cell range.
|
* a single cell range.
|
||||||
*
|
*
|
||||||
|
* @param string[] $pValue
|
||||||
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setMergeCells(array $pValue)
|
public function setMergeCells(array $pValue)
|
||||||
|
|
|
@ -420,26 +420,33 @@ class Worksheet extends WriterPart
|
||||||
// sheetProtection
|
// sheetProtection
|
||||||
$objWriter->startElement('sheetProtection');
|
$objWriter->startElement('sheetProtection');
|
||||||
|
|
||||||
if ($pSheet->getProtection()->getPassword() !== '') {
|
$protection = $pSheet->getProtection();
|
||||||
$objWriter->writeAttribute('password', $pSheet->getProtection()->getPassword());
|
|
||||||
|
if ($protection->getAlgorithm()) {
|
||||||
|
$objWriter->writeAttribute('algorithmName', $protection->getAlgorithm());
|
||||||
|
$objWriter->writeAttribute('hashValue', $protection->getPassword());
|
||||||
|
$objWriter->writeAttribute('saltValue', $protection->getSalt());
|
||||||
|
$objWriter->writeAttribute('spinCount', $protection->getSpinCount());
|
||||||
|
} elseif ($protection->getPassword() !== '') {
|
||||||
|
$objWriter->writeAttribute('password', $protection->getPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
$objWriter->writeAttribute('sheet', ($pSheet->getProtection()->getSheet() ? 'true' : 'false'));
|
$objWriter->writeAttribute('sheet', ($protection->getSheet() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('objects', ($pSheet->getProtection()->getObjects() ? 'true' : 'false'));
|
$objWriter->writeAttribute('objects', ($protection->getObjects() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('scenarios', ($pSheet->getProtection()->getScenarios() ? 'true' : 'false'));
|
$objWriter->writeAttribute('scenarios', ($protection->getScenarios() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('formatCells', ($pSheet->getProtection()->getFormatCells() ? 'true' : 'false'));
|
$objWriter->writeAttribute('formatCells', ($protection->getFormatCells() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('formatColumns', ($pSheet->getProtection()->getFormatColumns() ? 'true' : 'false'));
|
$objWriter->writeAttribute('formatColumns', ($protection->getFormatColumns() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('formatRows', ($pSheet->getProtection()->getFormatRows() ? 'true' : 'false'));
|
$objWriter->writeAttribute('formatRows', ($protection->getFormatRows() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('insertColumns', ($pSheet->getProtection()->getInsertColumns() ? 'true' : 'false'));
|
$objWriter->writeAttribute('insertColumns', ($protection->getInsertColumns() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('insertRows', ($pSheet->getProtection()->getInsertRows() ? 'true' : 'false'));
|
$objWriter->writeAttribute('insertRows', ($protection->getInsertRows() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('insertHyperlinks', ($pSheet->getProtection()->getInsertHyperlinks() ? 'true' : 'false'));
|
$objWriter->writeAttribute('insertHyperlinks', ($protection->getInsertHyperlinks() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('deleteColumns', ($pSheet->getProtection()->getDeleteColumns() ? 'true' : 'false'));
|
$objWriter->writeAttribute('deleteColumns', ($protection->getDeleteColumns() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('deleteRows', ($pSheet->getProtection()->getDeleteRows() ? 'true' : 'false'));
|
$objWriter->writeAttribute('deleteRows', ($protection->getDeleteRows() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('selectLockedCells', ($pSheet->getProtection()->getSelectLockedCells() ? 'true' : 'false'));
|
$objWriter->writeAttribute('selectLockedCells', ($protection->getSelectLockedCells() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('sort', ($pSheet->getProtection()->getSort() ? 'true' : 'false'));
|
$objWriter->writeAttribute('sort', ($protection->getSort() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('autoFilter', ($pSheet->getProtection()->getAutoFilter() ? 'true' : 'false'));
|
$objWriter->writeAttribute('autoFilter', ($protection->getAutoFilter() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('pivotTables', ($pSheet->getProtection()->getPivotTables() ? 'true' : 'false'));
|
$objWriter->writeAttribute('pivotTables', ($protection->getPivotTables() ? 'true' : 'false'));
|
||||||
$objWriter->writeAttribute('selectUnlockedCells', ($pSheet->getProtection()->getSelectUnlockedCells() ? 'true' : 'false'));
|
$objWriter->writeAttribute('selectUnlockedCells', ($protection->getSelectUnlockedCells() ? 'true' : 'false'));
|
||||||
$objWriter->endElement();
|
$objWriter->endElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine;
|
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine;
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||||
|
use PhpOffice\PhpSpreadsheet\NamedRange;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ class RangeTest extends TestCase
|
||||||
/**
|
/**
|
||||||
* @dataProvider providerRangeEvaluation
|
* @dataProvider providerRangeEvaluation
|
||||||
*
|
*
|
||||||
* @param mixed $formula
|
* @param string $formula
|
||||||
* @param int $expectedResult
|
* @param int $expectedResult
|
||||||
*/
|
*/
|
||||||
public function testRangeEvaluation($formula, $expectedResult): void
|
public function testRangeEvaluation($formula, $expectedResult): void
|
||||||
|
@ -44,11 +45,93 @@ class RangeTest extends TestCase
|
||||||
{
|
{
|
||||||
return[
|
return[
|
||||||
['=SUM(A1:B3,A1:C2)', 48],
|
['=SUM(A1:B3,A1:C2)', 48],
|
||||||
|
['=COUNT(A1:B3,A1:C2)', 12],
|
||||||
['=SUM(A1:B3 A1:C2)', 12],
|
['=SUM(A1:B3 A1:C2)', 12],
|
||||||
|
['=COUNT(A1:B3 A1:C2)', 4],
|
||||||
['=SUM(A1:A3,C1:C3)', 30],
|
['=SUM(A1:A3,C1:C3)', 30],
|
||||||
|
['=COUNT(A1:A3,C1:C3)', 6],
|
||||||
['=SUM(A1:A3 C1:C3)', Functions::null()],
|
['=SUM(A1:A3 C1:C3)', Functions::null()],
|
||||||
|
['=COUNT(A1:A3 C1:C3)', 0],
|
||||||
['=SUM(A1:B2,B2:C3)', 40],
|
['=SUM(A1:B2,B2:C3)', 40],
|
||||||
|
['=COUNT(A1:B2,B2:C3)', 8],
|
||||||
['=SUM(A1:B2 B2:C3)', 5],
|
['=SUM(A1:B2 B2:C3)', 5],
|
||||||
|
['=COUNT(A1:B2 B2:C3)', 1],
|
||||||
|
['=SUM(A1:C1,A3:C3,B1:C3)', 63],
|
||||||
|
['=COUNT(A1:C1,A3:C3,B1:C3)', 12],
|
||||||
|
['=SUM(A1:C1,A3:C3 B1:C3)', 23],
|
||||||
|
['=COUNT(A1:C1,A3:C3 B1:C3)', 5],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider providerNamedRangeEvaluation
|
||||||
|
*
|
||||||
|
* @param string $group1
|
||||||
|
* @param string $group2
|
||||||
|
* @param string $formula
|
||||||
|
* @param int $expectedResult
|
||||||
|
*/
|
||||||
|
public function testNamedRangeEvaluation($group1, $group2, $formula, $expectedResult): void
|
||||||
|
{
|
||||||
|
$workSheet = $this->spreadSheet->getActiveSheet();
|
||||||
|
$this->spreadSheet->addNamedRange(new NamedRange('GROUP1', $workSheet, $group1));
|
||||||
|
$this->spreadSheet->addNamedRange(new NamedRange('GROUP2', $workSheet, $group2));
|
||||||
|
|
||||||
|
$workSheet->setCellValue('E1', $formula);
|
||||||
|
|
||||||
|
$sumRresult = $workSheet->getCell('E1')->getCalculatedValue();
|
||||||
|
self::assertSame($expectedResult, $sumRresult);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function providerNamedRangeEvaluation()
|
||||||
|
{
|
||||||
|
return[
|
||||||
|
['A1:B3', 'A1:C2', '=SUM(GROUP1,GROUP2)', 48],
|
||||||
|
['A1:B3', 'A1:C2', '=COUNT(GROUP1,GROUP2)', 12],
|
||||||
|
['A1:B3', 'A1:C2', '=SUM(GROUP1 GROUP2)', 12],
|
||||||
|
['A1:B3', 'A1:C2', '=COUNT(GROUP1 GROUP2)', 4],
|
||||||
|
['A1:B2', 'B2:C3', '=SUM(GROUP1,GROUP2)', 40],
|
||||||
|
['A1:B2', 'B2:C3', '=COUNT(GROUP1,GROUP2)', 8],
|
||||||
|
['A1:B2', 'B2:C3', '=SUM(GROUP1 GROUP2)', 5],
|
||||||
|
['A1:B2', 'B2:C3', '=COUNT(GROUP1 GROUP2)', 1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider providerCompositeNamedRangeEvaluation
|
||||||
|
*
|
||||||
|
* @param string $composite
|
||||||
|
* @param int $expectedSum
|
||||||
|
* @param int $expectedCount
|
||||||
|
*/
|
||||||
|
public function testCompositeNamedRangeEvaluation($composite, $expectedSum, $expectedCount): void
|
||||||
|
{
|
||||||
|
$workSheet = $this->spreadSheet->getActiveSheet();
|
||||||
|
$this->spreadSheet->addNamedRange(new NamedRange('COMPOSITE', $workSheet, $composite));
|
||||||
|
|
||||||
|
$workSheet->setCellValue('E1', '=SUM(COMPOSITE)');
|
||||||
|
$workSheet->setCellValue('E2', '=COUNT(COMPOSITE)');
|
||||||
|
|
||||||
|
$actualSum = $workSheet->getCell('E1')->getCalculatedValue();
|
||||||
|
self::assertSame($expectedSum, $actualSum);
|
||||||
|
$actualCount = $workSheet->getCell('E2')->getCalculatedValue();
|
||||||
|
self::assertSame($expectedCount, $actualCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function providerCompositeNamedRangeEvaluation()
|
||||||
|
{
|
||||||
|
return[
|
||||||
|
// Calculation engine doesn't yet handle union ranges with overlap
|
||||||
|
// 'Union with overlap' => [
|
||||||
|
// 'A1:C1,A3:C3,B1:C3',
|
||||||
|
// 63,
|
||||||
|
// 12,
|
||||||
|
// ],
|
||||||
|
'Intersection' => [
|
||||||
|
'A1:C1,A3:C3 B1:C3',
|
||||||
|
23,
|
||||||
|
5,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,10 +83,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerCoordinates
|
* @dataProvider providerCoordinates
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testCoordinateFromString($expectedResult, ...$args): void
|
public function testCoordinateFromString($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::coordinateFromString(...$args);
|
$result = Coordinate::coordinateFromString($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,11 +144,12 @@ class CoordinateTest extends TestCase
|
||||||
/**
|
/**
|
||||||
* @dataProvider providerAbsoluteCoordinates
|
* @dataProvider providerAbsoluteCoordinates
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param string $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testAbsoluteCoordinateFromString($expectedResult, ...$args): void
|
public function testAbsoluteCoordinateFromString($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::absoluteCoordinate(...$args);
|
$result = Coordinate::absoluteCoordinate($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,10 +177,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerAbsoluteReferences
|
* @dataProvider providerAbsoluteReferences
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testAbsoluteReferenceFromString($expectedResult, ...$args): void
|
public function testAbsoluteReferenceFromString($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::absoluteReference(...$args);
|
$result = Coordinate::absoluteReference($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,10 +209,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerSplitRange
|
* @dataProvider providerSplitRange
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testSplitRange($expectedResult, ...$args): void
|
public function testSplitRange($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::splitRange(...$args);
|
$result = Coordinate::splitRange($rangeSet);
|
||||||
foreach ($result as $key => $split) {
|
foreach ($result as $key => $split) {
|
||||||
if (!is_array($expectedResult[$key])) {
|
if (!is_array($expectedResult[$key])) {
|
||||||
self::assertEquals($expectedResult[$key], $split[0]);
|
self::assertEquals($expectedResult[$key], $split[0]);
|
||||||
|
@ -252,10 +256,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerRangeBoundaries
|
* @dataProvider providerRangeBoundaries
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testRangeBoundaries($expectedResult, ...$args): void
|
public function testRangeBoundaries($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::rangeBoundaries(...$args);
|
$result = Coordinate::rangeBoundaries($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,10 +273,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerRangeDimension
|
* @dataProvider providerRangeDimension
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testRangeDimension($expectedResult, ...$args): void
|
public function testRangeDimension($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::rangeDimension(...$args);
|
$result = Coordinate::rangeDimension($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,10 +290,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerGetRangeBoundaries
|
* @dataProvider providerGetRangeBoundaries
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testGetRangeBoundaries($expectedResult, ...$args): void
|
public function testGetRangeBoundaries($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::getRangeBoundaries(...$args);
|
$result = Coordinate::getRangeBoundaries($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,11 +306,12 @@ class CoordinateTest extends TestCase
|
||||||
/**
|
/**
|
||||||
* @dataProvider providerExtractAllCellReferencesInRange
|
* @dataProvider providerExtractAllCellReferencesInRange
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param array $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testExtractAllCellReferencesInRange($expectedResult, ...$args): void
|
public function testExtractAllCellReferencesInRange($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::extractAllCellReferencesInRange(...$args);
|
$result = Coordinate::extractAllCellReferencesInRange($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,10 +358,11 @@ class CoordinateTest extends TestCase
|
||||||
* @dataProvider providerCoordinateIsRange
|
* @dataProvider providerCoordinateIsRange
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @param mixed $expectedResult
|
||||||
|
* @param string $rangeSet
|
||||||
*/
|
*/
|
||||||
public function testCoordinateIsRange($expectedResult, ...$args): void
|
public function testCoordinateIsRange($expectedResult, $rangeSet): void
|
||||||
{
|
{
|
||||||
$result = Coordinate::coordinateIsRange(...$args);
|
$result = Coordinate::coordinateIsRange($rangeSet);
|
||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ProtectionTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testVerifyPassword(): void
|
||||||
|
{
|
||||||
|
$protection = new Protection();
|
||||||
|
self::assertTrue($protection->verify('foo'), 'non-protected always pass');
|
||||||
|
|
||||||
|
$protection->setSheet(true);
|
||||||
|
self::assertFalse($protection->verify('foo'), 'protected will fail');
|
||||||
|
|
||||||
|
$protection->setPassword('foo', true);
|
||||||
|
self::assertSame('foo', $protection->getPassword(), 'was not stored as-is, without hashing');
|
||||||
|
self::assertFalse($protection->verify('foo'), 'setting already hashed password will not match');
|
||||||
|
|
||||||
|
$protection->setPassword('foo');
|
||||||
|
self::assertSame('CC40', $protection->getPassword(), 'was hashed');
|
||||||
|
self::assertTrue($protection->verify('foo'), 'setting non-hashed password will hash it and not match');
|
||||||
|
|
||||||
|
$protection->setAlgorithm(Protection::ALGORITHM_MD5);
|
||||||
|
self::assertFalse($protection->verify('foo'), 'changing algorithm will not match anymore');
|
||||||
|
|
||||||
|
$protection->setPassword('foo');
|
||||||
|
$hash1 = $protection->getPassword();
|
||||||
|
$protection->setPassword('foo');
|
||||||
|
$hash2 = $protection->getPassword();
|
||||||
|
|
||||||
|
self::assertSame(24, mb_strlen($hash1));
|
||||||
|
self::assertSame(24, mb_strlen($hash2));
|
||||||
|
self::assertNotSame($hash1, $hash2, 'was hashed with automatic salt');
|
||||||
|
self::assertTrue($protection->verify('foo'), 'setting password again, will hash with proper algorithm and will match');
|
||||||
|
}
|
||||||
|
}
|
|
@ -393,6 +393,10 @@ return [
|
||||||
1,
|
1,
|
||||||
'19-12-1960', '26-01-2012', 'YM',
|
'19-12-1960', '26-01-2012', 'YM',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
11,
|
||||||
|
'19-12-1960', '26-11-1962', 'YM',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
38,
|
38,
|
||||||
'19-12-1960', '26-01-2012', 'YD',
|
'19-12-1960', '26-01-2012', 'YD',
|
||||||
|
@ -402,7 +406,15 @@ return [
|
||||||
'19-12-1960', '26-01-2012', 'MD',
|
'19-12-1960', '26-01-2012', 'MD',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
50,
|
0,
|
||||||
|
'19-12-1960', '12-12-1961', 'Y',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
'19-12-1960', '12-12-1962', 'Y',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51,
|
||||||
'19-12-1960', '12-12-2012', 'Y',
|
'19-12-1960', '12-12-2012', 'Y',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -22,12 +22,6 @@ return [
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
'B4',
|
|
||||||
'B5',
|
|
||||||
'B6',
|
|
||||||
'D4',
|
|
||||||
'D5',
|
|
||||||
'D6',
|
|
||||||
],
|
],
|
||||||
'B4:B6 D4:D6',
|
'B4:B6 D4:D6',
|
||||||
],
|
],
|
||||||
|
@ -66,20 +60,10 @@ return [
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
'B4',
|
|
||||||
'B5',
|
|
||||||
'B6',
|
|
||||||
'C4',
|
|
||||||
'C5',
|
'C5',
|
||||||
'C6',
|
'C6',
|
||||||
'C7',
|
|
||||||
'D4',
|
|
||||||
'D5',
|
'D5',
|
||||||
'D6',
|
'D6',
|
||||||
'D7',
|
|
||||||
'E5',
|
|
||||||
'E6',
|
|
||||||
'E7',
|
|
||||||
],
|
],
|
||||||
'B4:D6 C5:E7',
|
'B4:D6 C5:E7',
|
||||||
],
|
],
|
||||||
|
@ -105,7 +89,7 @@ return [
|
||||||
'F5',
|
'F5',
|
||||||
'F6',
|
'F6',
|
||||||
],
|
],
|
||||||
'B2:D4 C5:D5 E3:E5 D6:E6 F4:F6',
|
'B2:D4,C5:D5,E3:E5,D6:E6,F4:F6',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -129,16 +113,13 @@ return [
|
||||||
'F5',
|
'F5',
|
||||||
'F6',
|
'F6',
|
||||||
],
|
],
|
||||||
'B2:D4 C3:E5 D4:F6',
|
'B2:D4,C3:E5,D4:F6',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
'B4',
|
|
||||||
'B5',
|
'B5',
|
||||||
'B6',
|
|
||||||
'B8',
|
|
||||||
],
|
],
|
||||||
'B4:B6 B8',
|
'B4:B6 B5',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
|
|
@ -25,4 +25,30 @@ return [
|
||||||
'CE4B',
|
'CE4B',
|
||||||
'',
|
'',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'O6EXRLpLEDNJDL/AzYtnnA4O4bY=',
|
||||||
|
'',
|
||||||
|
'SHA-1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'GYvlIMljDI1Czc4jfWrGaxU5pxl9n5Og0KUzyAfYxwk=',
|
||||||
|
'PhpSpreadsheet',
|
||||||
|
'SHA-256',
|
||||||
|
'Php_salt',
|
||||||
|
1000,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'sSHdxQv9qgpkr4LDT0bYQxM9hOQJFRhJ4D752/NHQtDDR1EVy67NCEW9cPd6oWvCoBGd96MqKpuma1A7pN1nEA==',
|
||||||
|
'Mark Baker',
|
||||||
|
'SHA-512',
|
||||||
|
'Mark_salt',
|
||||||
|
10000,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'r9KVLLCKIYOILvE2rcby+g==',
|
||||||
|
'!+&=()~§±æþ',
|
||||||
|
'MD5',
|
||||||
|
'Symbols_salt',
|
||||||
|
100000,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue