Skip to content

Commit 13b62be

Browse files
authored
Fix for Issue #1887 - Lose Track of Selected Cells After Save (#1908)
* Fix for Issue #1887 - Lose Track of Selected Cells After Save Issue #1887 reports that selected cells are lost after saving Xlsx. Testing indicates that this applies to the object in memory, though not to the saved spreadsheet. Xlsx writer tries to save calculated values for cells which contain formulas. Calculation::_calculateFormulaValue issues a getStyle call merely to retrieve the quotePrefix property, which, if set, indicates that the cell does not contain a formula even though it looks like one. A side-effect of calls to getStyle is that selectedCell is updated. That is clearly accidental, and highly undesirable, in this case. Code is changed to save selectedCell before getStyle call and restore it afterwards. The problem was reported only for Xlsx save. To be on the safe side, test is made for output formats of Xlsx, Xls, Ods, Html (which basically includes Pdf), and Csv. For all of those, the object in memory is tested after the save. For Xlsx and Xls, the saved file is also tested. It does not make sense to test the saved file for Csv and Html. It does make sense to test it for Ods, but the necessary support is not yet present in either the Ods Reader or Ods Writer - a project for another day. * Move Logic Out of Calculation, Add Support for Ods ActiveSheet and SelectedCells Mark Baker thought logic belonged in Worksheet, not Calculation. I couldn't get it to work in Worksheet, but doing it in Cell works, and that has already been used to preserve ActiveSheet over call to getCalculatedValue, so this just extends that idea to SelectedCells. Original tests could not completely support Ods because of a lack of support for ActiveSheet and SelectedCells in Ods Reader and Writer. There's a lot missing in Ods support, but a journey of 1000 miles ... Those two particular concepts are now supported for Ods.
1 parent 70f372d commit 13b62be

File tree

6 files changed

+219
-6
lines changed

6 files changed

+219
-6
lines changed

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,11 @@ public function getCalculatedValue($resetLog = true)
252252
if ($this->dataType == DataType::TYPE_FORMULA) {
253253
try {
254254
$index = $this->getWorksheet()->getParent()->getActiveSheetIndex();
255+
$selected = $this->getWorksheet()->getSelectedCells();
255256
$result = Calculation::getInstance(
256257
$this->getWorksheet()->getParent()
257258
)->calculateCellValue($this, $resetLog);
259+
$this->getWorksheet()->setSelectedCells($selected);
258260
$this->getWorksheet()->getParent()->setActiveSheetIndex($index);
259261
// We don't yet handle array returns
260262
if (is_array($result)) {

src/PhpSpreadsheet/Reader/Ods.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PhpOffice\PhpSpreadsheet\Spreadsheet;
2424
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
2525
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
26+
use Throwable;
2627
use XMLReader;
2728
use ZipArchive;
2829

@@ -646,10 +647,81 @@ public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
646647
$this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs);
647648
}
648649
$spreadsheet->setActiveSheetIndex(0);
650+
651+
if ($zip->locateName('settings.xml') !== false) {
652+
$this->processSettings($zip, $spreadsheet);
653+
}
649654
// Return
650655
return $spreadsheet;
651656
}
652657

658+
private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
659+
{
660+
$dom = new DOMDocument('1.01', 'UTF-8');
661+
$dom->loadXML(
662+
$this->securityScanner->scan($zip->getFromName('settings.xml')),
663+
Settings::getLibXmlLoaderOptions()
664+
);
665+
//$xlinkNs = $dom->lookupNamespaceUri('xlink');
666+
$configNs = $dom->lookupNamespaceUri('config');
667+
//$oooNs = $dom->lookupNamespaceUri('ooo');
668+
$officeNs = $dom->lookupNamespaceUri('office');
669+
$settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
670+
->item(0);
671+
$this->lookForActiveSheet($settings, $spreadsheet, $configNs);
672+
$this->lookForSelectedCells($settings, $spreadsheet, $configNs);
673+
}
674+
675+
private function lookForActiveSheet(DOMNode $settings, Spreadsheet $spreadsheet, string $configNs): void
676+
{
677+
foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
678+
if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
679+
try {
680+
$spreadsheet->setActiveSheetIndexByName($t->nodeValue);
681+
} catch (Throwable $e) {
682+
// do nothing
683+
}
684+
685+
break;
686+
}
687+
}
688+
}
689+
690+
private function lookForSelectedCells(DOMNode $settings, Spreadsheet $spreadsheet, string $configNs): void
691+
{
692+
foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
693+
if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
694+
foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
695+
$setRow = $setCol = '';
696+
$wsname = $ws->getAttributeNs($configNs, 'name');
697+
foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
698+
$attrName = $configItem->getAttributeNs($configNs, 'name');
699+
if ($attrName === 'CursorPositionX') {
700+
$setCol = $configItem->nodeValue;
701+
}
702+
if ($attrName === 'CursorPositionY') {
703+
$setRow = $configItem->nodeValue;
704+
}
705+
}
706+
$this->setSelected($spreadsheet, $wsname, $setCol, $setRow);
707+
}
708+
709+
break;
710+
}
711+
}
712+
}
713+
714+
private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
715+
{
716+
if (is_numeric($setCol) && is_numeric($setRow)) {
717+
try {
718+
$spreadsheet->getSheetByName($wsname)->setSelectedCellByColumnAndRow($setCol + 1, $setRow + 1);
719+
} catch (Throwable $e) {
720+
// do nothing
721+
}
722+
}
723+
}
724+
653725
/**
654726
* Recursively scan element.
655727
*

src/PhpSpreadsheet/Writer/Ods/Settings.php

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
44

5+
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
56
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
67
use PhpOffice\PhpSpreadsheet\Spreadsheet;
78

@@ -16,7 +17,6 @@ class Settings extends WriterPart
1617
*/
1718
public function write(?Spreadsheet $spreadsheet = null)
1819
{
19-
$objWriter = null;
2020
if ($this->getParentWriter()->getUseDiskCaching()) {
2121
$objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
2222
} else {
@@ -39,13 +39,52 @@ public function write(?Spreadsheet $spreadsheet = null)
3939
$objWriter->writeAttribute('config:name', 'ooo:view-settings');
4040
$objWriter->startElement('config:config-item-map-indexed');
4141
$objWriter->writeAttribute('config:name', 'Views');
42-
$objWriter->endElement();
43-
$objWriter->endElement();
42+
$objWriter->startElement('config:config-item-map-entry');
43+
$spreadsheet = $spreadsheet ?? $this->getParentWriter()->getSpreadsheet();
44+
45+
$objWriter->startElement('config:config-item');
46+
$objWriter->writeAttribute('config:name', 'ViewId');
47+
$objWriter->writeAttribute('config:type', 'string');
48+
$objWriter->text('view1');
49+
$objWriter->endElement(); // ViewId
50+
$objWriter->startElement('config:config-item-map-named');
51+
$objWriter->writeAttribute('config:name', 'Tables');
52+
foreach ($spreadsheet->getWorksheetIterator() as $ws) {
53+
$objWriter->startElement('config:config-item-map-entry');
54+
$objWriter->writeAttribute('config:name', $ws->getTitle());
55+
$selected = $ws->getSelectedCells();
56+
if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) {
57+
$colSel = Coordinate::columnIndexFromString($matches[1]) - 1;
58+
$rowSel = (int) $matches[2] - 1;
59+
$objWriter->startElement('config:config-item');
60+
$objWriter->writeAttribute('config:name', 'CursorPositionX');
61+
$objWriter->writeAttribute('config:type', 'int');
62+
$objWriter->text($colSel);
63+
$objWriter->endElement();
64+
$objWriter->startElement('config:config-item');
65+
$objWriter->writeAttribute('config:name', 'CursorPositionY');
66+
$objWriter->writeAttribute('config:type', 'int');
67+
$objWriter->text($rowSel);
68+
$objWriter->endElement();
69+
}
70+
$objWriter->endElement(); // config:config-item-map-entry
71+
}
72+
$objWriter->endElement(); // config:config-item-map-named
73+
$wstitle = $spreadsheet->getActiveSheet()->getTitle();
74+
$objWriter->startElement('config:config-item');
75+
$objWriter->writeAttribute('config:name', 'ActiveTable');
76+
$objWriter->writeAttribute('config:type', 'string');
77+
$objWriter->text($wstitle);
78+
$objWriter->endElement(); // config:config-item ActiveTable
79+
80+
$objWriter->endElement(); // config:config-item-map-entry
81+
$objWriter->endElement(); // config:config-item-map-indexed Views
82+
$objWriter->endElement(); // config:config-item-set ooo:view-settings
4483
$objWriter->startElement('config:config-item-set');
4584
$objWriter->writeAttribute('config:name', 'ooo:configuration-settings');
46-
$objWriter->endElement();
47-
$objWriter->endElement();
48-
$objWriter->endElement();
85+
$objWriter->endElement(); // config:config-item-set ooo:configuration-settings
86+
$objWriter->endElement(); // office:settings
87+
$objWriter->endElement(); // office:document-settings
4988

5089
return $objWriter->getData();
5190
}

tests/PhpSpreadsheetTests/Calculation/CalculationTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
66
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
7+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
78
use PhpOffice\PhpSpreadsheet\Spreadsheet;
89
use PHPUnit\Framework\TestCase;
910

@@ -159,6 +160,14 @@ public function testCellSetAsQuotedText(): void
159160
$cell->getStyle()->setQuotePrefix(true);
160161

161162
self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
163+
164+
$cell2 = $workSheet->getCell('A2');
165+
$cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA);
166+
self::assertEquals('ABC', $cell2->getCalculatedValue());
167+
168+
$cell3 = $workSheet->getCell('A3');
169+
$cell3->setValueExplicit('=', DataType::TYPE_FORMULA);
170+
self::assertEquals('', $cell3->getCalculatedValue());
162171
}
163172

164173
public function testCellWithDdeExpresion(): void

tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ public function testLoadOneWorksheet(): void
101101
self::assertEquals('Sheet1', $spreadsheet->getSheet(0)->getTitle());
102102
}
103103

104+
public function testLoadOneWorksheetNotActive(): void
105+
{
106+
$filename = 'tests/data/Reader/Ods/data.ods';
107+
108+
// Load into this instance
109+
$reader = new Ods();
110+
$reader->setLoadSheetsOnly(['Second Sheet']);
111+
$spreadsheet = $reader->load($filename);
112+
113+
self::assertEquals(1, $spreadsheet->getSheetCount());
114+
115+
self::assertEquals('Second Sheet', $spreadsheet->getSheet(0)->getTitle());
116+
}
117+
104118
public function testLoadBadFile(): void
105119
{
106120
$this->expectException(ReaderException::class);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Writer;
4+
5+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
6+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
7+
8+
class RetainSelectedCellsTest extends AbstractFunctional
9+
{
10+
public function providerFormats()
11+
{
12+
return [
13+
['Xls'],
14+
['Xlsx'],
15+
['Ods'],
16+
['Csv'],
17+
['Html'],
18+
];
19+
}
20+
21+
/**
22+
* Test selected cell is retained in memory and in file written to disk.
23+
*
24+
* @dataProvider providerFormats
25+
*/
26+
public function testRetainSelectedCells(string $format): void
27+
{
28+
$spreadsheet = new Spreadsheet();
29+
$sheet = $spreadsheet->getActiveSheet();
30+
$sheet->setCellValue('A1', '=SIN(1)')
31+
->setCellValue('A2', '=SIN(2)')
32+
->setCellValue('A3', '=SIN(3)')
33+
->setCellValue('B1', '=SIN(4)')
34+
->setCellValue('B2', '=SIN(5)')
35+
->setCellValue('B3', '=SIN(6)')
36+
->setCellValue('C1', '=SIN(7)')
37+
->setCellValue('C2', '=SIN(8)')
38+
->setCellValue('C3', '=SIN(9)');
39+
$sheet->setSelectedCell('A3');
40+
$sheet = $spreadsheet->createSheet();
41+
$sheet->setCellValue('A1', '=SIN(1)')
42+
->setCellValue('A2', '=SIN(2)')
43+
->setCellValue('A3', '=SIN(3)')
44+
->setCellValue('B1', '=SIN(4)')
45+
->setCellValue('B2', '=SIN(5)')
46+
->setCellValue('B3', '=SIN(6)')
47+
->setCellValue('C1', '=SIN(7)')
48+
->setCellValue('C2', '=SIN(8)')
49+
->setCellValue('C3', '=SIN(9)');
50+
$sheet->setSelectedCell('B1');
51+
$sheet = $spreadsheet->createSheet();
52+
$sheet->setCellValue('A1', '=SIN(1)')
53+
->setCellValue('A2', '=SIN(2)')
54+
->setCellValue('A3', '=SIN(3)')
55+
->setCellValue('B1', '=SIN(4)')
56+
->setCellValue('B2', '=SIN(5)')
57+
->setCellValue('B3', '=SIN(6)')
58+
->setCellValue('C1', '=SIN(7)')
59+
->setCellValue('C2', '=SIN(8)')
60+
->setCellValue('C3', '=SIN(9)');
61+
$sheet->setSelectedCell('C2');
62+
$spreadsheet->setActiveSheetIndex(1);
63+
64+
$reloaded = $this->writeAndReload($spreadsheet, $format);
65+
self::assertEquals('A3', $spreadsheet->getSheet(0)->getSelectedCells());
66+
self::assertEquals('B1', $spreadsheet->getSheet(1)->getSelectedCells());
67+
self::assertEquals('C2', $spreadsheet->getSheet(2)->getSelectedCells());
68+
self::assertEquals(1, $spreadsheet->getActiveSheetIndex());
69+
// SelectedCells and ActiveSheet don't make sense for Html, Csv.
70+
if ($format === 'Xlsx' || $format === 'Xls' || $format === 'Ods') {
71+
self::assertEquals('A3', $reloaded->getSheet(0)->getSelectedCells());
72+
self::assertEquals('B1', $reloaded->getSheet(1)->getSelectedCells());
73+
self::assertEquals('C2', $reloaded->getSheet(2)->getSelectedCells());
74+
self::assertEquals(1, $reloaded->getActiveSheetIndex());
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)