mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-13 11:36:24 +00:00
Protect Sheet But Allow Sort
Fix #3951. When an Excel sheet is protected, even when sorting is explicitly allowed without a password, sorts are permitted only on "protected ranges" within the sheet. PhpSpreadsheet already supports protected ranges, and only minor tinkering is necessary for that (e.g. the protected range can have, but does not require, a password). The more important part of this change is documenting the far-from-intuitive way that Excel handles this. To that end, documentation is updated, and a new sample is added. A new class, `Worksheet\ProtectedRange` is added in place of the string array which had been used. `Worksheet::getProtectedCells` is deprecated in favor of the new `Worksheet::getProtectedCellRanges`.
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../Header.php';
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\RichText\RichText;
|
||||
use PhpOffice\PhpSpreadsheet\RichText\TextElement;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$helper->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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Worksheet;
|
||||
|
||||
class ProtectedRange
|
||||
{
|
||||
private string $name = '';
|
||||
|
||||
private string $password = '';
|
||||
|
||||
private string $sqref;
|
||||
|
||||
private string $securityDescriptor = '';
|
||||
|
||||
/**
|
||||
* No setters aside from constructor.
|
||||
*/
|
||||
public function __construct(string $sqref, string $password = '', string $name = '', string $securityDescriptor = '')
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
|
||||
|
||||
class Issue3951Test extends AbstractFunctional
|
||||
{
|
||||
/**
|
||||
* @dataProvider providerType
|
||||
*/
|
||||
public function testIssue2266(string $type): void
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$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);
|
||||
|
||||
$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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user