Skip to content

Commit 92d3c1b

Browse files
committed
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`.
1 parent 86cca12 commit 92d3c1b

File tree

4 files changed

+84
-8
lines changed

4 files changed

+84
-8
lines changed

src/PhpSpreadsheet/Cell/Hyperlink.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ public function setTooltip(string $tooltip): static
6868
}
6969

7070
/**
71-
* Is this hyperlink internal? (to another worksheet).
71+
* Is this hyperlink internal? (to another worksheet or a cell in this worksheet).
7272
*/
7373
public function isInternal(): bool
7474
{
75-
return str_contains($this->url, 'sheet://');
75+
return str_starts_with($this->url, 'sheet://') || str_starts_with($this->url, '#');
7676
}
7777

7878
public function getTypeHyperlink(): string

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,9 @@ public function close(): void
458458
[$column, $row] = Coordinate::indexesFromString($coordinate);
459459

460460
$url = $hyperlink->getUrl();
461-
462-
if (str_contains($url, 'sheet://')) {
461+
if ($url[0] === '#') {
462+
$url = "internal:$url";
463+
} elseif (str_starts_with($url, 'sheet://')) {
463464
// internal to current workbook
464465
$url = str_replace('sheet://', 'internal:', $url);
465466
} elseif (Preg::isMatch('/^(http:|https:|ftp:|mailto:)/', $url)) {
@@ -946,12 +947,11 @@ private function writeUrlRange(int $row1, int $col1, int $row2, int $col2, strin
946947
// Check for internal/external sheet links or default to web link
947948
if (Preg::isMatch('[^internal:]', $url)) {
948949
$this->writeUrlInternal($row1, $col1, $row2, $col2, $url);
949-
}
950-
if (Preg::isMatch('[^external:]', $url)) {
950+
} elseif (Preg::isMatch('[^external:]', $url)) {
951951
$this->writeUrlExternal($row1, $col1, $row2, $col2, $url);
952+
} else {
953+
$this->writeUrlWeb($row1, $col1, $row2, $col2, $url);
952954
}
953-
954-
$this->writeUrlWeb($row1, $col1, $row2, $col2, $url);
955955
}
956956

957957
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class HyperlinkTest extends AbstractFunctional
11+
{
12+
public function testHyperlink(): void
13+
{
14+
$spreadsheet = new Spreadsheet();
15+
$sheet1 = $spreadsheet->getActiveSheet();
16+
$sheet1->setTitle('First');
17+
$sheet2 = $spreadsheet->createSheet();
18+
$sheet2->setTitle('Second');
19+
$sheet2->setCellValue('A100', 'other sheet');
20+
$sheet1->setCellValue('A100', 'this sheet');
21+
$sheet1->setCellValue('A1', '=HYPERLINK("#A100", "here")');
22+
$sheet1->setCellValue('A2', '=HYPERLINK("#Second!A100", "there")');
23+
$sheet1->setCellValue('A3', '=HYPERLINK("http://example.com", "external")');
24+
$sheet1->setCellValue('A4', 'gotoA101');
25+
$sheet1->getCell('A4')
26+
->getHyperlink()
27+
->setUrl('#A101');
28+
29+
$robj = $this->writeAndReload($spreadsheet, 'Xls');
30+
$spreadsheet->disconnectWorksheets();
31+
$sheet0 = $robj->setActiveSheetIndex(0);
32+
self::assertSame('sheet://#A100', $sheet0->getCell('A1')->getHyperlink()->getUrl());
33+
self::assertSame('sheet://#Second!A100', $sheet0->getCell('A2')->getHyperlink()->getUrl());
34+
self::assertSame('http://example.com', $sheet0->getCell('A3')->getHyperlink()->getUrl());
35+
self::assertSame('sheet://#A101', $sheet0->getCell('A4')->getHyperlink()->getUrl());
36+
$robj->disconnectWorksheets();
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class HyperlinkTest extends AbstractFunctional
11+
{
12+
public function testHyperlink(): void
13+
{
14+
$spreadsheet = new Spreadsheet();
15+
$sheet1 = $spreadsheet->getActiveSheet();
16+
$sheet1->setTitle('First');
17+
$sheet2 = $spreadsheet->createSheet();
18+
$sheet2->setTitle('Second');
19+
$sheet2->setCellValue('A100', 'other sheet');
20+
$sheet1->setCellValue('A100', 'this sheet');
21+
$sheet1->setCellValue('A1', '=HYPERLINK("#A100", "here")');
22+
$sheet1->setCellValue('A2', '=HYPERLINK("#Second!A100", "there")');
23+
$sheet1->setCellValue('A3', '=HYPERLINK("http://example.com", "external")');
24+
$sheet1->setCellValue('A4', 'gotoA101');
25+
$sheet1->getCell('A4')
26+
->getHyperlink()
27+
->setUrl('#A101');
28+
29+
$robj = $this->writeAndReload($spreadsheet, 'Xlsx');
30+
$spreadsheet->disconnectWorksheets();
31+
$sheet0 = $robj->setActiveSheetIndex(0);
32+
self::assertSame('sheet://#A100', $sheet0->getCell('A1')->getHyperlink()->getUrl());
33+
self::assertSame('sheet://#Second!A100', $sheet0->getCell('A2')->getHyperlink()->getUrl());
34+
self::assertSame('http://example.com', $sheet0->getCell('A3')->getHyperlink()->getUrl());
35+
self::assertSame('sheet://#A101', $sheet0->getCell('A4')->getHyperlink()->getUrl());
36+
$robj->disconnectWorksheets();
37+
}
38+
}

0 commit comments

Comments
 (0)