Merge branch 'master' into htmledit

This commit is contained in:
oleibman 2020-06-09 00:39:52 -07:00 committed by GitHub
commit 360c8d8284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 826 additions and 404 deletions

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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();
``` ```

View File

@ -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

View File

@ -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'];

View File

@ -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':

View File

@ -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];
} }
/** /**

View File

@ -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);
}
}
}
} }

View File

@ -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);
}
} }

View File

@ -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.
*/ */

View File

@ -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)

View File

@ -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();
} }

View File

@ -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,
],
]; ];
} }
} }

View File

@ -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);
} }

View File

@ -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');
}
}

View File

@ -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',
], ],
[ [

View File

@ -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',
], ],
[ [
[ [

View File

@ -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,
],
]; ];