From 6f5ef037be65a60259380c269da287b591904ead Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 12 Aug 2023 07:29:20 -0700 Subject: [PATCH] Hyperlink Support for Ods (#3669) Fix #3660. Code existed to read a hyperlink in a cell for Ods, but did not exist for writing. Hyperlinks pointing within a document use a different representation than for Excel (and therefore for PhpSpreadsheet); read and write will both handle the mapping from one to the other. While researching how to write the text part of a started Xml element, it transpired that writing 2 Ods properties (Company and Category) did not escape their values properly. Confirmed and fixed problem. I do not believe that there is such an exposure for any other writer. As it turns out, Ods Reader was not processing Company or Category properly; that is fixed. --- src/PhpSpreadsheet/Reader/Ods.php | 3 ++ src/PhpSpreadsheet/Reader/Ods/Properties.php | 9 +++- src/PhpSpreadsheet/Writer/Ods/Content.php | 18 ++++++- src/PhpSpreadsheet/Writer/Ods/Meta.php | 4 +- .../Reader/Ods/HyperlinkTest.php | 52 +++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/HyperlinkTest.php diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 8e52713b5..3e61f2078 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -624,6 +624,9 @@ class Ods extends BaseReader } if ($hyperlink !== null) { + if ($hyperlink[0] === '#') { + $hyperlink = 'sheet://' . substr($hyperlink, 1); + } $cell->getHyperlink() ->setUrl($hyperlink); } diff --git a/src/PhpSpreadsheet/Reader/Ods/Properties.php b/src/PhpSpreadsheet/Reader/Ods/Properties.php index 8c4ccc303..33419356e 100644 --- a/src/PhpSpreadsheet/Reader/Ods/Properties.php +++ b/src/PhpSpreadsheet/Reader/Ods/Properties.php @@ -91,7 +91,14 @@ class Properties break; case 'user-defined': - $this->setUserDefinedProperty($propertyValueAttributes, $propertyValue, $docProps); + $name2 = (string) ($propertyValueAttributes['name'] ?? ''); + if ($name2 === 'Company') { + $docProps->setCompany($propertyValue); + } elseif ($name2 === 'category') { + $docProps->setCategory($propertyValue); + } else { + $this->setUserDefinedProperty($propertyValueAttributes, $propertyValue, $docProps); + } break; } diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 311044369..f52dcfad4 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -250,7 +250,23 @@ class Content extends WriterPart // break intentionally omitted case DataType::TYPE_STRING: $objWriter->writeAttribute('office:value-type', 'string'); - $objWriter->writeElement('text:p', $cell->getValue()); + $url = $cell->getHyperlink()->getUrl(); + if (empty($url)) { + $objWriter->writeElement('text:p', $cell->getValue()); + } else { + $objWriter->startElement('text:p'); + $objWriter->startElement('text:a'); + $sheets = 'sheet://'; + $lensheets = strlen($sheets); + if (substr($url, 0, $lensheets) === $sheets) { + $url = '#' . substr($url, $lensheets); + } + $objWriter->writeAttribute('xlink:href', $url); + $objWriter->writeAttribute('xlink:type', 'simple'); + $objWriter->text($cell->getValue()); + $objWriter->endElement(); // text:a + $objWriter->endElement(); // text:p + } break; } diff --git a/src/PhpSpreadsheet/Writer/Ods/Meta.php b/src/PhpSpreadsheet/Writer/Ods/Meta.php index 16f7c8b54..75023a6da 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Meta.php +++ b/src/PhpSpreadsheet/Writer/Ods/Meta.php @@ -65,12 +65,12 @@ class Meta extends WriterPart // $objWriter->startElement('meta:user-defined'); $objWriter->writeAttribute('meta:name', 'Company'); - $objWriter->writeRaw($spreadsheet->getProperties()->getCompany()); + $objWriter->writeRawData($spreadsheet->getProperties()->getCompany()); $objWriter->endElement(); $objWriter->startElement('meta:user-defined'); $objWriter->writeAttribute('meta:name', 'category'); - $objWriter->writeRaw($spreadsheet->getProperties()->getCategory()); + $objWriter->writeRawData($spreadsheet->getProperties()->getCategory()); $objWriter->endElement(); self::writeDocPropsCustom($objWriter, $spreadsheet); diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HyperlinkTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HyperlinkTest.php new file mode 100644 index 000000000..2dc5f9cfa --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HyperlinkTest.php @@ -0,0 +1,52 @@ +getProperties()->setCompany('gzorg'); + $spreadsheetOld->getProperties()->setCategory('hzorg'); + $sheet = $spreadsheetOld->getActiveSheet(); + $sheet->getCell('A1')->setValue('Hello World'); + $sheet->getCell('A2')->setValue('http://example.org'); + $sheet->getCell('A2')->getHyperlink()->setUrl('http://example.org/'); + $sheet->getCell('A3')->setValue('pagetCell('A3')->getHyperlink()->setUrl('http://example.org/page1.html'); + $sheet2 = $spreadsheetOld->createSheet(); + $sheet2->setTitle('TargetSheet'); + $sheet2->setCellValue('B4', 'TargetCell'); + $sheet2->setCellValue('B3', 'not target'); + $sheet->getCell('A4')->setValue('go to Target'); + $sheet->getCell('A4')->getHyperlink()->setUrl('sheet://TargetSheet!B4'); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods'); + $spreadsheetOld->disconnectWorksheets(); + + $newSheet = $spreadsheet->getActiveSheet(); + self::assertSame('gzorg', $spreadsheet->getProperties()->getCompany()); + self::assertSame('hzorg', $spreadsheet->getProperties()->getCategory()); + self::assertSame('http://example.org', $newSheet->getCell('A2')->getValue()); + self::assertSame('http://example.org/', $newSheet->getCell('A2')->getHyperlink()->getUrl()); + self::assertSame('pagetCell('A3')->getValue()); + self::assertSame('http://example.org/page1.html', $newSheet->getCell('A3')->getHyperlink()->getUrl()); + self::assertSame('go to Target', $newSheet->getCell('A4')->getValue()); + self::assertSame('sheet://TargetSheet!B4', $newSheet->getCell('A4')->getHyperlink()->getUrl()); + + // Verify that http links are unchanged, + // but internal sheet link has changed. + $writer = new Ods($spreadsheet); + $content = $writer->getWriterPartContent()->write(); + self::assertStringContainsString('xlink:href="http://example.org/"', $content); + self::assertStringContainsString('xlink:href="http://example.org/page1.html"', $content); + self::assertStringContainsString('xlink:href="#TargetSheet!B4"', $content); + self::assertStringNotContainsString('sheet:', $content); + + $spreadsheet->disconnectWorksheets(); + } +}