Skip to content

Commit 016ad48

Browse files
authored
Merge pull request #3956 from oleibman/issue3951
Protect Sheet But Allow Sort
2 parents a6ef9a7 + 6b09ee9 commit 016ad48

File tree

12 files changed

+319
-20
lines changed

12 files changed

+319
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3535
- Formula Misidentifying Text as Cell After Insertion/Deletion [Issue #3907](https://github.com/PHPOffice/PhpSpreadsheet/issues/3907) [PR #3915](https://github.com/PHPOffice/PhpSpreadsheet/pull/3915)
3636
- Unexpected Absolute Address in Xlsx Rels [Issue #3730](https://github.com/PHPOffice/PhpSpreadsheet/issues/3730) [PR #3923](https://github.com/PHPOffice/PhpSpreadsheet/pull/3923)
3737
- Unallocated Cells Affected by Column/Row Insert/Delete [Issue #3933](https://github.com/PHPOffice/PhpSpreadsheet/issues/3933) [PR #3940](https://github.com/PHPOffice/PhpSpreadsheet/pull/3940)
38+
- Invalid Builtin Defined Name in Xls Reader [Issue #3935](https://github.com/PHPOffice/PhpSpreadsheet/issues/3935) [PR #3942](https://github.com/PHPOffice/PhpSpreadsheet/pull/3942)
39+
- Hidden Rows and Columns Tcpdf/Mpdf [PR #3945](https://github.com/PHPOffice/PhpSpreadsheet/pull/3945)
40+
- Protect Sheet But Allow Sort [Issue #3951](https://github.com/PHPOffice/PhpSpreadsheet/issues/3951) [PR #3956](https://github.com/PHPOffice/PhpSpreadsheet/pull/3956)
3841

3942
## 2.0.0 - 2024-01-04
4043

docs/topics/recipes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,15 @@ $protection->setInsertRows(false);
12951295
$protection->setFormatCells(false);
12961296
```
12971297

1298+
Note that allowing sort without providing the sheet password
1299+
(similarly with autoFilter) requires that you explicitly
1300+
enable the cell ranges for which sort is permitted,
1301+
with or without a range password:
1302+
```php
1303+
$sheet->protectCells('A:A'); // column A can be sorted without password
1304+
$sheet->protectCells('B:B', 'sortpw'); // column B can be sorted if the range password sortpw is supplied
1305+
```
1306+
12981307
If writing Xlsx files you can specify the algorithm used to hash the password
12991308
before calling `setPassword()` like so:
13001309

samples/Basic4/51_ProtectedSort.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
require __DIR__ . '/../Header.php';
4+
5+
use PhpOffice\PhpSpreadsheet\RichText\RichText;
6+
use PhpOffice\PhpSpreadsheet\RichText\TextElement;
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
9+
$spreadsheet = new Spreadsheet();
10+
11+
$helper->log('First sheet - protected, sorts not allowed');
12+
$sheet = $spreadsheet->getActiveSheet();
13+
$sheet->setTitle('sorttrue');
14+
$sheet->getCell('A1')->setValue(10);
15+
$sheet->getCell('A2')->setValue(5);
16+
$sheet->getCell('B1')->setValue(15);
17+
$protection = $sheet->getProtection();
18+
$protection->setPassword('testpassword');
19+
$protection->setSheet(true);
20+
$protection->setInsertRows(true);
21+
$protection->setFormatCells(true);
22+
$protection->setObjects(true);
23+
$protection->setAutoFilter(false);
24+
$protection->setSort(true);
25+
$comment = $sheet->getComment('A1');
26+
$text = new RichText();
27+
$text->addText(new TextElement('Sort options should be grayed out. Sheet password to remove protections is testpassword for all sheets.'));
28+
$comment->setText($text)->setHeight('120pt')->setWidth('120pt');
29+
30+
$helper->log('Second sheet - protected, sorts allowed, but no permitted range defined');
31+
$sheet = $spreadsheet->createSheet();
32+
$sheet->setTitle('sortfalse');
33+
$sheet->getCell('A1')->setValue(10);
34+
$sheet->getCell('A2')->setValue(5);
35+
$sheet->getCell('B1')->setValue(15);
36+
$protection = $sheet->getProtection();
37+
$protection->setPassword('testpassword');
38+
$protection->setSheet(true);
39+
$protection->setInsertRows(true);
40+
$protection->setFormatCells(true);
41+
$protection->setObjects(true);
42+
$protection->setAutoFilter(false);
43+
$protection->setSort(false);
44+
$comment = $sheet->getComment('A1');
45+
$text = new RichText();
46+
$text->addText(new TextElement('Sort options not grayed out, but no permissible sort range.'));
47+
$comment->setText($text)->setHeight('120pt')->setWidth('120pt');
48+
49+
$helper->log('Third sheet - protected, sorts allowed, but only on permitted range A:A, no range password needed');
50+
$sheet = $spreadsheet->createSheet();
51+
$sheet->setTitle('sortfalsenocolpw');
52+
$sheet->getCell('A1')->setValue(10);
53+
$sheet->getCell('A2')->setValue(5);
54+
$sheet->getCell('C1')->setValue(15);
55+
$protection = $sheet->getProtection();
56+
$protection->setPassword('testpassword');
57+
$protection->setSheet(true);
58+
$protection->setInsertRows(true);
59+
$protection->setFormatCells(true);
60+
$protection->setObjects(true);
61+
$protection->setAutoFilter(false);
62+
$protection->setSort(false);
63+
$sheet->protectCells('A:A');
64+
$comment = $sheet->getComment('A1');
65+
$text = new RichText();
66+
$text->addText(new TextElement('Column A may be sorted without a password. No sort for any other column.'));
67+
$comment->setText($text)->setHeight('120pt')->setWidth('120pt');
68+
69+
$helper->log('Fourth sheet - protected, sorts allowed, but only on permitted range A:A, and range password needed');
70+
$sheet = $spreadsheet->createSheet();
71+
$sheet->setTitle('sortfalsecolpw');
72+
$sheet->getCell('A1')->setValue(10);
73+
$sheet->getCell('A2')->setValue(5);
74+
$sheet->getCell('C1')->setValue(15);
75+
$protection = $sheet->getProtection();
76+
$protection->setPassword('testpassword');
77+
$protection->setSheet(true);
78+
$protection->setInsertRows(true);
79+
$protection->setFormatCells(true);
80+
$protection->setObjects(true);
81+
$protection->setAutoFilter(false);
82+
$protection->setSort(false);
83+
$sheet->protectCells('A:A', 'sortpw', false, 'sortrange');
84+
$comment = $sheet->getComment('A1');
85+
$text = new RichText();
86+
$text->addText(new TextElement('Column A may be sorted with password sortpw. No sort for any other column.'));
87+
$comment->setText($text)->setHeight('120pt')->setWidth('120pt');
88+
89+
// Save
90+
$helper->write($spreadsheet, __FILE__, ['Xls', 'Xlsx']);
91+
$spreadsheet->disconnectWorksheets();

src/PhpSpreadsheet/Reader/Xls.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4973,7 +4973,7 @@ private function readRangeProtection(): void
49734973

49744974
// Apply range protection to sheet
49754975
if ($cellRanges) {
4976-
$this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
4976+
$this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true);
49774977
}
49784978
}
49794979
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2175,7 +2175,7 @@ private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlS
21752175

21762176
if ($xmlSheet->protectedRanges->protectedRange) {
21772177
foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
2178-
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
2178+
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']);
21792179
}
21802180
}
21812181
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4+
5+
class ProtectedRange
6+
{
7+
private string $name = '';
8+
9+
private string $password = '';
10+
11+
private string $sqref;
12+
13+
private string $securityDescriptor = '';
14+
15+
/**
16+
* No setters aside from constructor.
17+
*/
18+
public function __construct(string $sqref, string $password = '', string $name = '', string $securityDescriptor = '')
19+
{
20+
$this->sqref = $sqref;
21+
$this->name = $name;
22+
$this->password = $password;
23+
$this->securityDescriptor = $securityDescriptor;
24+
}
25+
26+
public function getSqref(): string
27+
{
28+
return $this->sqref;
29+
}
30+
31+
public function getName(): string
32+
{
33+
return $this->name ?: ('p' . md5($this->sqref));
34+
}
35+
36+
public function getPassword(): string
37+
{
38+
return $this->password;
39+
}
40+
41+
public function getSecurityDescriptor(): string
42+
{
43+
return $this->securityDescriptor;
44+
}
45+
}

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ class Worksheet implements IComparable
192192
/**
193193
* Collection of protected cell ranges.
194194
*
195-
* @var string[]
195+
* @var ProtectedRange[]
196196
*/
197197
private array $protectedCells = [];
198198

@@ -1866,14 +1866,14 @@ public function setMergeCells(array $mergeCells): static
18661866
*
18671867
* @return $this
18681868
*/
1869-
public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password, bool $alreadyHashed = false): static
1869+
public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password = '', bool $alreadyHashed = false, string $name = '', string $securityDescriptor = ''): static
18701870
{
18711871
$range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
18721872

1873-
if (!$alreadyHashed) {
1873+
if (!$alreadyHashed && $password !== '') {
18741874
$password = Shared\PasswordHasher::hashPassword($password);
18751875
}
1876-
$this->protectedCells[$range] = $password;
1876+
$this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
18771877

18781878
return $this;
18791879
}
@@ -1901,11 +1901,29 @@ public function unprotectCells(AddressRange|CellAddress|int|string|array $range)
19011901
}
19021902

19031903
/**
1904-
* Get protected cells.
1904+
* Get password for protected cells.
19051905
*
19061906
* @return string[]
1907+
*
1908+
* @deprecated 2.0.1 use getProtectedCellRanges instead
1909+
* @see Worksheet::getProtectedCellRanges()
19071910
*/
19081911
public function getProtectedCells(): array
1912+
{
1913+
$array = [];
1914+
foreach ($this->protectedCells as $key => $protectedRange) {
1915+
$array[$key] = $protectedRange->getPassword();
1916+
}
1917+
1918+
return $array;
1919+
}
1920+
1921+
/**
1922+
* Get protected cells.
1923+
*
1924+
* @return ProtectedRange[]
1925+
*/
1926+
public function getProtectedCellRanges(): array
19091927
{
19101928
return $this->protectedCells;
19111929
}

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,8 @@ private function writeSheetProtection(): void
14961496
*/
14971497
private function writeRangeProtection(): void
14981498
{
1499-
foreach ($this->phpSheet->getProtectedCells() as $range => $password) {
1499+
foreach ($this->phpSheet->getProtectedCellRanges() as $range => $protectedCells) {
1500+
$password = $protectedCells->getPassword();
15001501
// number of ranges, e.g. 'A1:B3 C20:D25'
15011502
$cellRanges = explode(' ', $range);
15021503
$cref = count($cellRanges);

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -974,19 +974,20 @@ private function writeHyperlinks(XMLWriter $objWriter, PhpspreadsheetWorksheet $
974974
*/
975975
private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
976976
{
977-
if (count($worksheet->getProtectedCells()) > 0) {
977+
if (count($worksheet->getProtectedCellRanges()) > 0) {
978978
// protectedRanges
979979
$objWriter->startElement('protectedRanges');
980980

981981
// Loop protectedRanges
982-
foreach ($worksheet->getProtectedCells() as $protectedCell => $passwordHash) {
982+
foreach ($worksheet->getProtectedCellRanges() as $protectedCell => $protectedRange) {
983983
// protectedRange
984984
$objWriter->startElement('protectedRange');
985-
$objWriter->writeAttribute('name', 'p' . md5($protectedCell));
985+
$objWriter->writeAttribute('name', $protectedRange->getName());
986986
$objWriter->writeAttribute('sqref', $protectedCell);
987-
if (!empty($passwordHash)) {
988-
$objWriter->writeAttribute('password', $passwordHash);
989-
}
987+
$passwordHash = $protectedRange->getPassword();
988+
$this->writeAttributeIf($objWriter, $passwordHash !== '', 'password', $passwordHash);
989+
$securityDescriptor = $protectedRange->getSecurityDescriptor();
990+
$this->writeAttributeIf($objWriter, $securityDescriptor !== '', 'securityDescriptor', $securityDescriptor);
990991
$objWriter->endElement();
991992
}
992993

tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,10 @@ public function testProtectCellsByColumnAndRow(): void
133133
$sheet->fromArray($data, null, 'B2', true);
134134

135135
$sheet->protectCells([2, 2, 3, 3], 'secret', false);
136-
$protectedRanges = $sheet->getProtectedCells();
136+
$protectedRanges = $sheet->/** @scrutinizer ignore-deprecated*/ getProtectedCells();
137137
self::assertArrayHasKey('B2:C3', $protectedRanges);
138+
$protectedRanges2 = $sheet->getProtectedCellRanges();
139+
self::assertArrayHasKey('B2:C3', $protectedRanges2);
138140
$spreadsheet->disconnectWorksheets();
139141
}
140142

@@ -147,11 +149,11 @@ public function testUnprotectCellsByColumnAndRow(): void
147149
$sheet->fromArray($data, null, 'B2', true);
148150

149151
$sheet->protectCells('B2:C3', 'secret', false);
150-
$protectedRanges = $sheet->getProtectedCells();
152+
$protectedRanges = $sheet->getProtectedCellRanges();
151153
self::assertArrayHasKey('B2:C3', $protectedRanges);
152154

153155
$sheet->unprotectCells([2, 2, 3, 3]);
154-
$protectedRanges = $sheet->getProtectedCells();
156+
$protectedRanges = $sheet->getProtectedCellRanges();
155157
self::assertEmpty($protectedRanges);
156158
$spreadsheet->disconnectWorksheets();
157159
}

0 commit comments

Comments
 (0)