diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 33d218d43..ede0f34e1 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1295,6 +1295,15 @@ $protection->setInsertRows(false); $protection->setFormatCells(false); ``` +Note that allowing sort without providing the sheet password +(similarly with autoFilter) requires that you explicitly +enable the cell ranges for which sort is permitted, +with or without a range password: +```php +$sheet->protectCells('A:A'); // column A can be sorted without password +$sheet->protectCells('B:B', 'sortpw'); // column B can be sorted if the range password sortpw is supplied +``` + If writing Xlsx files you can specify the algorithm used to hash the password before calling `setPassword()` like so: diff --git a/samples/Basic4/51_ProtectedSort.php b/samples/Basic4/51_ProtectedSort.php new file mode 100644 index 000000000..cdc95d6c2 --- /dev/null +++ b/samples/Basic4/51_ProtectedSort.php @@ -0,0 +1,91 @@ +log('First sheet - protected, sorts not allowed'); +$sheet = $spreadsheet->getActiveSheet(); +$sheet->setTitle('sorttrue'); +$sheet->getCell('A1')->setValue(10); +$sheet->getCell('A2')->setValue(5); +$sheet->getCell('B1')->setValue(15); +$protection = $sheet->getProtection(); +$protection->setPassword('testpassword'); +$protection->setSheet(true); +$protection->setInsertRows(true); +$protection->setFormatCells(true); +$protection->setObjects(true); +$protection->setAutoFilter(false); +$protection->setSort(true); +$comment = $sheet->getComment('A1'); +$text = new RichText(); +$text->addText(new TextElement('Sort options should be grayed out. Sheet password to remove protections is testpassword for all sheets.')); +$comment->setText($text)->setHeight('120pt')->setWidth('120pt'); + +$helper->log('Second sheet - protected, sorts allowed, but no permitted range defined'); +$sheet = $spreadsheet->createSheet(); +$sheet->setTitle('sortfalse'); +$sheet->getCell('A1')->setValue(10); +$sheet->getCell('A2')->setValue(5); +$sheet->getCell('B1')->setValue(15); +$protection = $sheet->getProtection(); +$protection->setPassword('testpassword'); +$protection->setSheet(true); +$protection->setInsertRows(true); +$protection->setFormatCells(true); +$protection->setObjects(true); +$protection->setAutoFilter(false); +$protection->setSort(false); +$comment = $sheet->getComment('A1'); +$text = new RichText(); +$text->addText(new TextElement('Sort options not grayed out, but no permissible sort range.')); +$comment->setText($text)->setHeight('120pt')->setWidth('120pt'); + +$helper->log('Third sheet - protected, sorts allowed, but only on permitted range A:A, no range password needed'); +$sheet = $spreadsheet->createSheet(); +$sheet->setTitle('sortfalsenocolpw'); +$sheet->getCell('A1')->setValue(10); +$sheet->getCell('A2')->setValue(5); +$sheet->getCell('C1')->setValue(15); +$protection = $sheet->getProtection(); +$protection->setPassword('testpassword'); +$protection->setSheet(true); +$protection->setInsertRows(true); +$protection->setFormatCells(true); +$protection->setObjects(true); +$protection->setAutoFilter(false); +$protection->setSort(false); +$sheet->protectCells('A:A'); +$comment = $sheet->getComment('A1'); +$text = new RichText(); +$text->addText(new TextElement('Column A may be sorted without a password. No sort for any other column.')); +$comment->setText($text)->setHeight('120pt')->setWidth('120pt'); + +$helper->log('Fourth sheet - protected, sorts allowed, but only on permitted range A:A, and range password needed'); +$sheet = $spreadsheet->createSheet(); +$sheet->setTitle('sortfalsecolpw'); +$sheet->getCell('A1')->setValue(10); +$sheet->getCell('A2')->setValue(5); +$sheet->getCell('C1')->setValue(15); +$protection = $sheet->getProtection(); +$protection->setPassword('testpassword'); +$protection->setSheet(true); +$protection->setInsertRows(true); +$protection->setFormatCells(true); +$protection->setObjects(true); +$protection->setAutoFilter(false); +$protection->setSort(false); +$sheet->protectCells('A:A', 'sortpw', false, 'sortrange'); +$comment = $sheet->getComment('A1'); +$text = new RichText(); +$text->addText(new TextElement('Column A may be sorted with password sortpw. No sort for any other column.')); +$comment->setText($text)->setHeight('120pt')->setWidth('120pt'); + +// Save +$helper->write($spreadsheet, __FILE__, ['Xls', 'Xlsx']); +$spreadsheet->disconnectWorksheets(); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 6d20e1563..da12ce8a8 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4972,7 +4972,7 @@ class Xls extends BaseReader // Apply range protection to sheet if ($cellRanges) { - $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true); + $this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index c8b2de2a9..4b855ddc7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -2175,7 +2175,7 @@ class Xlsx extends BaseReader if ($xmlSheet->protectedRanges->protectedRange) { foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { - $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); + $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']); } } } diff --git a/src/PhpSpreadsheet/Worksheet/ProtectedRange.php b/src/PhpSpreadsheet/Worksheet/ProtectedRange.php new file mode 100644 index 000000000..bd4197628 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/ProtectedRange.php @@ -0,0 +1,45 @@ +sqref = $sqref; + $this->name = $name; + $this->password = $password; + $this->securityDescriptor = $securityDescriptor; + } + + public function getSqref(): string + { + return $this->sqref; + } + + public function getName(): string + { + return $this->name ?: ('p' . md5($this->sqref)); + } + + public function getPassword(): string + { + return $this->password; + } + + public function getSecurityDescriptor(): string + { + return $this->securityDescriptor; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 0e1f4dc32..7afb95b79 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -192,7 +192,7 @@ class Worksheet implements IComparable /** * Collection of protected cell ranges. * - * @var string[] + * @var ProtectedRange[] */ private array $protectedCells = []; @@ -1866,14 +1866,14 @@ class Worksheet implements IComparable * * @return $this */ - public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password, bool $alreadyHashed = false): static + public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password = '', bool $alreadyHashed = false, string $name = '', string $securityDescriptor = ''): static { $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); - if (!$alreadyHashed) { + if (!$alreadyHashed && $password !== '') { $password = Shared\PasswordHasher::hashPassword($password); } - $this->protectedCells[$range] = $password; + $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor); return $this; } @@ -1901,11 +1901,29 @@ class Worksheet implements IComparable } /** - * Get protected cells. + * Get password for protected cells. * * @return string[] + * + * @deprecated 2.0.1 use getProtectedCellRanges instead + * @see Worksheet::getProtectedCellRanges() */ public function getProtectedCells(): array + { + $array = []; + foreach ($this->protectedCells as $key => $protectedRange) { + $array[$key] = $protectedRange->getPassword(); + } + + return $array; + } + + /** + * Get protected cells. + * + * @return ProtectedRange[] + */ + public function getProtectedCellRanges(): array { return $this->protectedCells; } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 0ef9f280d..dadfc7003 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1496,7 +1496,8 @@ class Worksheet extends BIFFwriter */ private function writeRangeProtection(): void { - foreach ($this->phpSheet->getProtectedCells() as $range => $password) { + foreach ($this->phpSheet->getProtectedCellRanges() as $range => $protectedCells) { + $password = $protectedCells->getPassword(); // number of ranges, e.g. 'A1:B3 C20:D25' $cellRanges = explode(' ', $range); $cref = count($cellRanges); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index a14ff2812..7784da61b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -974,19 +974,20 @@ class Worksheet extends WriterPart */ private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void { - if (count($worksheet->getProtectedCells()) > 0) { + if (count($worksheet->getProtectedCellRanges()) > 0) { // protectedRanges $objWriter->startElement('protectedRanges'); // Loop protectedRanges - foreach ($worksheet->getProtectedCells() as $protectedCell => $passwordHash) { + foreach ($worksheet->getProtectedCellRanges() as $protectedCell => $protectedRange) { // protectedRange $objWriter->startElement('protectedRange'); - $objWriter->writeAttribute('name', 'p' . md5($protectedCell)); + $objWriter->writeAttribute('name', $protectedRange->getName()); $objWriter->writeAttribute('sqref', $protectedCell); - if (!empty($passwordHash)) { - $objWriter->writeAttribute('password', $passwordHash); - } + $passwordHash = $protectedRange->getPassword(); + $this->writeAttributeIf($objWriter, $passwordHash !== '', 'password', $passwordHash); + $securityDescriptor = $protectedRange->getSecurityDescriptor(); + $this->writeAttributeIf($objWriter, $securityDescriptor !== '', 'securityDescriptor', $securityDescriptor); $objWriter->endElement(); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php b/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php index 467071924..bc2717804 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php @@ -133,8 +133,10 @@ class ByColumnAndRowTest extends TestCase $sheet->fromArray($data, null, 'B2', true); $sheet->protectCells([2, 2, 3, 3], 'secret', false); - $protectedRanges = $sheet->getProtectedCells(); + $protectedRanges = $sheet->/** @scrutinizer ignore-deprecated*/ getProtectedCells(); self::assertArrayHasKey('B2:C3', $protectedRanges); + $protectedRanges2 = $sheet->getProtectedCellRanges(); + self::assertArrayHasKey('B2:C3', $protectedRanges2); $spreadsheet->disconnectWorksheets(); } @@ -147,11 +149,11 @@ class ByColumnAndRowTest extends TestCase $sheet->fromArray($data, null, 'B2', true); $sheet->protectCells('B2:C3', 'secret', false); - $protectedRanges = $sheet->getProtectedCells(); + $protectedRanges = $sheet->getProtectedCellRanges(); self::assertArrayHasKey('B2:C3', $protectedRanges); $sheet->unprotectCells([2, 2, 3, 3]); - $protectedRanges = $sheet->getProtectedCells(); + $protectedRanges = $sheet->getProtectedCellRanges(); self::assertEmpty($protectedRanges); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowUndeprecatedTest.php b/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowUndeprecatedTest.php index 66862e98d..2209bc3ca 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowUndeprecatedTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowUndeprecatedTest.php @@ -144,7 +144,7 @@ class ByColumnAndRowUndeprecatedTest extends TestCase $sheet->fromArray($data, null, 'B2', true); $sheet->protectCells([2, 2, 3, 3], 'secret', false); - $protectedRanges = $sheet->getProtectedCells(); + $protectedRanges = $sheet->getProtectedCellRanges(); self::assertArrayHasKey('B2:C3', $protectedRanges); } @@ -157,11 +157,11 @@ class ByColumnAndRowUndeprecatedTest extends TestCase $sheet->fromArray($data, null, 'B2', true); $sheet->protectCells('B2:C3', 'secret', false); - $protectedRanges = $sheet->getProtectedCells(); + $protectedRanges = $sheet->getProtectedCellRanges(); self::assertArrayHasKey('B2:C3', $protectedRanges); $sheet->unprotectCells([2, 2, 3, 3]); - $protectedRanges = $sheet->getProtectedCells(); + $protectedRanges = $sheet->getProtectedCellRanges(); self::assertEmpty($protectedRanges); } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue3951Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue3951Test.php new file mode 100644 index 000000000..0833301a6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue3951Test.php @@ -0,0 +1,129 @@ +getActiveSheet(); + $sheet->setTitle('sorttrue'); + $sheet->getCell('A1')->setValue(10); + $sheet->getCell('A2')->setValue(5); + $sheet->getCell('B1')->setValue(15); + $protection = $sheet->getProtection(); + $protection->setPassword('testpassword'); + $protection->setSheet(true); + $protection->setInsertRows(true); + $protection->setFormatCells(true); + $protection->setObjects(true); + $protection->setAutoFilter(false); + $protection->setSort(true); + + $sheet = $spreadsheet->createSheet(); + $sheet->setTitle('sortfalse'); + $sheet->getCell('A1')->setValue(10); + $sheet->getCell('A2')->setValue(5); + $sheet->getCell('B1')->setValue(15); + $protection = $sheet->getProtection(); + $protection->setPassword('testpassword'); + $protection->setSheet(true); + $protection->setInsertRows(true); + $protection->setFormatCells(true); + $protection->setObjects(true); + $protection->setAutoFilter(false); + $protection->setSort(false); + + $sheet = $spreadsheet->createSheet(); + $sheet->setTitle('sortfalsenocolpw'); + $sheet->getCell('A1')->setValue(10); + $sheet->getCell('A2')->setValue(5); + $sheet->getCell('C1')->setValue(15); + $protection = $sheet->getProtection(); + $protection->setPassword('testpassword'); + $protection->setSheet(true); + $protection->setInsertRows(true); + $protection->setFormatCells(true); + $protection->setObjects(true); + $protection->setAutoFilter(false); + $protection->setSort(false); + $sheet->protectCells('A:A'); + + $sheet = $spreadsheet->createSheet(); + $sheet->setTitle('sortfalsecolpw'); + $sheet->getCell('A1')->setValue(10); + $sheet->getCell('A2')->setValue(5); + $sheet->getCell('C1')->setValue(15); + $protection = $sheet->getProtection(); + $protection->setPassword('testpassword'); + $protection->setSheet(true); + $protection->setInsertRows(true); + $protection->setFormatCells(true); + $protection->setObjects(true); + $protection->setAutoFilter(false); + $protection->setSort(false); + $sheet->protectCells('A:A', 'sortpw', false, 'sortrange'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $type); + $spreadsheet->disconnectWorksheets(); + + self::assertCount(4, $reloadedSpreadsheet->getAllSheets()); + + $rsheet1 = $reloadedSpreadsheet->getSheetByNameOrThrow('sorttrue'); + $protection = $rsheet1->getProtection(); + self::assertNotEquals('', $protection->getPassword()); + self::assertTrue($protection->getSort()); + self::assertEmpty($rsheet1->getProtectedCellRanges()); + + $rsheet2 = $reloadedSpreadsheet->getSheetByNameOrThrow('sortfalse'); + $protection = $rsheet2->getProtection(); + self::assertNotEquals('', $protection->getPassword()); + self::assertFalse($protection->getSort()); + self::assertEmpty($rsheet2->getProtectedCellRanges()); + + $rsheet3 = $reloadedSpreadsheet->getSheetByNameOrThrow('sortfalsenocolpw'); + $protection = $rsheet3->getProtection(); + self::assertNotEquals('', $protection->getPassword()); + self::assertFalse($protection->getSort()); + $maxRow = ($type === 'Xlsx') ? 1048576 : 65536; + $testKey = "A1:A$maxRow"; + $protectedCells = $rsheet3->getProtectedCellRanges(); + self::assertCount(1, $protectedCells); + self::assertArrayHasKey($testKey, $protectedCells); + self::assertSame('', $protectedCells[$testKey]->getPassword()); + + $rsheet4 = $reloadedSpreadsheet->getSheetByNameOrThrow('sortfalsecolpw'); + $protection = $rsheet4->getProtection(); + self::assertNotEquals('', $protection->getPassword()); + self::assertFalse($protection->getSort()); + $maxRow = ($type === 'Xlsx') ? 1048576 : 65536; + $testKey = "A1:A$maxRow"; + $protectedCells = $rsheet4->getProtectedCellRanges(); + self::assertCount(1, $protectedCells); + self::assertArrayHasKey($testKey, $protectedCells); + self::assertNotEquals('', $protectedCells[$testKey]->getPassword()); + if ($type === 'Xlsx') { + self::assertSame('sortrange', $protectedCells[$testKey]->getName()); + } + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public static function providerType(): array + { + return [ + ['Xlsx'], + ['Xls'], + ]; + } +}