From 92d3c1bc7d6522cde3af4c8f57faba982c26193f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 30 Apr 2025 00:23:35 -0700 Subject: [PATCH 1/2] Xls Writer Treat Hyperlink Starting with # as Internal Fix #56, which had gone stale but is now reopened. The problem which it reported with Xlsx Writer was fixed long ago. However, Xls Writer has 2 problems regarding hyperlinks. First, some logic which should have been `if ... elseif ... else ...` was coded as `if ... if ... unconditional ...`, resulting in multiple writes and a corrupt worksheet (which Excel does fix correctly). Second, it treated a hyperlink url starting with `#` (pointer to a cell in the same spreadsheet) as external, when it should be treated as internal. Also changed `Hyperlink::isInternal` to recognize a starting `#`, and to use `str_starts_with('sheet://')` rather than `str_contains`. --- src/PhpSpreadsheet/Cell/Hyperlink.php | 4 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 12 +++--- .../Writer/Xls/HyperlinkTest.php | 38 +++++++++++++++++++ .../Writer/Xlsx/HyperlinkTest.php | 38 +++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/HyperlinkTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/HyperlinkTest.php diff --git a/src/PhpSpreadsheet/Cell/Hyperlink.php b/src/PhpSpreadsheet/Cell/Hyperlink.php index 3117a7d86d..5f1c521620 100644 --- a/src/PhpSpreadsheet/Cell/Hyperlink.php +++ b/src/PhpSpreadsheet/Cell/Hyperlink.php @@ -68,11 +68,11 @@ public function setTooltip(string $tooltip): static } /** - * Is this hyperlink internal? (to another worksheet). + * Is this hyperlink internal? (to another worksheet or a cell in this worksheet). */ public function isInternal(): bool { - return str_contains($this->url, 'sheet://'); + return str_starts_with($this->url, 'sheet://') || str_starts_with($this->url, '#'); } public function getTypeHyperlink(): string diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 8ca3f2f16c..3df27c13e6 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -458,8 +458,9 @@ public function close(): void [$column, $row] = Coordinate::indexesFromString($coordinate); $url = $hyperlink->getUrl(); - - if (str_contains($url, 'sheet://')) { + if ($url[0] === '#') { + $url = "internal:$url"; + } elseif (str_starts_with($url, 'sheet://')) { // internal to current workbook $url = str_replace('sheet://', 'internal:', $url); } elseif (Preg::isMatch('/^(http:|https:|ftp:|mailto:)/', $url)) { @@ -946,12 +947,11 @@ private function writeUrlRange(int $row1, int $col1, int $row2, int $col2, strin // Check for internal/external sheet links or default to web link if (Preg::isMatch('[^internal:]', $url)) { $this->writeUrlInternal($row1, $col1, $row2, $col2, $url); - } - if (Preg::isMatch('[^external:]', $url)) { + } elseif (Preg::isMatch('[^external:]', $url)) { $this->writeUrlExternal($row1, $col1, $row2, $col2, $url); + } else { + $this->writeUrlWeb($row1, $col1, $row2, $col2, $url); } - - $this->writeUrlWeb($row1, $col1, $row2, $col2, $url); } /** diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/HyperlinkTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/HyperlinkTest.php new file mode 100644 index 0000000000..6b8cbb5bf8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/HyperlinkTest.php @@ -0,0 +1,38 @@ +getActiveSheet(); + $sheet1->setTitle('First'); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Second'); + $sheet2->setCellValue('A100', 'other sheet'); + $sheet1->setCellValue('A100', 'this sheet'); + $sheet1->setCellValue('A1', '=HYPERLINK("#A100", "here")'); + $sheet1->setCellValue('A2', '=HYPERLINK("#Second!A100", "there")'); + $sheet1->setCellValue('A3', '=HYPERLINK("http://example.com", "external")'); + $sheet1->setCellValue('A4', 'gotoA101'); + $sheet1->getCell('A4') + ->getHyperlink() + ->setUrl('#A101'); + + $robj = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + $sheet0 = $robj->setActiveSheetIndex(0); + self::assertSame('sheet://#A100', $sheet0->getCell('A1')->getHyperlink()->getUrl()); + self::assertSame('sheet://#Second!A100', $sheet0->getCell('A2')->getHyperlink()->getUrl()); + self::assertSame('http://example.com', $sheet0->getCell('A3')->getHyperlink()->getUrl()); + self::assertSame('sheet://#A101', $sheet0->getCell('A4')->getHyperlink()->getUrl()); + $robj->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/HyperlinkTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/HyperlinkTest.php new file mode 100644 index 0000000000..ba7fa2087e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/HyperlinkTest.php @@ -0,0 +1,38 @@ +getActiveSheet(); + $sheet1->setTitle('First'); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Second'); + $sheet2->setCellValue('A100', 'other sheet'); + $sheet1->setCellValue('A100', 'this sheet'); + $sheet1->setCellValue('A1', '=HYPERLINK("#A100", "here")'); + $sheet1->setCellValue('A2', '=HYPERLINK("#Second!A100", "there")'); + $sheet1->setCellValue('A3', '=HYPERLINK("http://example.com", "external")'); + $sheet1->setCellValue('A4', 'gotoA101'); + $sheet1->getCell('A4') + ->getHyperlink() + ->setUrl('#A101'); + + $robj = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $sheet0 = $robj->setActiveSheetIndex(0); + self::assertSame('sheet://#A100', $sheet0->getCell('A1')->getHyperlink()->getUrl()); + self::assertSame('sheet://#Second!A100', $sheet0->getCell('A2')->getHyperlink()->getUrl()); + self::assertSame('http://example.com', $sheet0->getCell('A3')->getHyperlink()->getUrl()); + self::assertSame('sheet://#A101', $sheet0->getCell('A4')->getHyperlink()->getUrl()); + $robj->disconnectWorksheets(); + } +} From 8be18d1a49c22d48e6607c494674e86de4e08093 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 6 May 2025 20:00:22 -0700 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f13b39286..7900e663f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Micro-optimization for excelToDateTimeObject. [Issue #4438](https://github.com/PHPOffice/PhpSpreadsheet/issues/4438) [PR #4442](https://github.com/PHPOffice/PhpSpreadsheet/pull/4442) - Print Area and Row Break. [Issue #1275](https://github.com/PHPOffice/PhpSpreadsheet/issues/1275) [PR #4450](https://github.com/PHPOffice/PhpSpreadsheet/pull/4450) +- Xls Writer Treat Hyperlink Starting with # as Internal. [Issue #56](https://github.com/PHPOffice/PhpSpreadsheet/issues/56) [PR #4453](https://github.com/PHPOffice/PhpSpreadsheet/pull/4453) ## 2025-04-16 - 4.2.0