From a0a9b2be214a3dca468110e5fc53a3ca68963a50 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 2 Jun 2023 23:25:38 -0700 Subject: [PATCH] HyperlinkBase Property, and Html Handling of Properties (#3589) * HyperlinkBase Property, and Html Handling of Properties Fix #3573. The original issue concerned non-support of Document Properties in Xml spreadsheets. However, most of the Properties mentioned there were already supported. But the investigation revealed some gaps in Html coverage. HyperlinkBase is the one property mentioned in the issue that was not supported for Xml, nor indeed for any other format. All the other document properties are 'meta'; but HyperlinkBase is functional - if you supply a relative address for a link, Excel will use HyperlinkBase, if supplied, to convert to an absolute address. (Default is directory where spreadsheet is located.) Here's a summary of how this PR will handle this property for various formats: - Support is added for Xlsx read and write. - Support is added for Xml read (there is no Xml writer). Ironically, Excel messes up this processing when reading an Xml spreadsheet; however, PhpSpreadsheet will get it right. - HyperlinkBase is supported for Xls, but I have no idea how to read or write this property. For now, when writing hyperlinked cells, PhpSpreadsheet will be changed to convert any relative addresses that it can detect to absolute references by adding HyperlinkBase to the relative address. In a similar vein, Xls supports custom properties, but PhpSpreadsheet does not know how to read or write those. - Gnumeric has no equivalent property, so nothing needs to be done to its reader. Since we don't have a Gnumeric writer, that's not really a problem for us. - Odt has no equivalent property, so nothing needs to be done to its reader. The Odt writer does not have any special logic for hyperlinks, so, at least for now, will remain unchanged. - Csv has no equivalent property, so nothing needs to be done to its reader. The Csv writer does not have any special logic for hyperlinks, so, at least for now, will remain unchanged. - Html allows for an equivalent `base` tag in the head section. Support for this is added to Html reader and writer. Html Writer was only handling 8 of the 11 'core' properties. Support is added for `created`, `modified`, and `lastModifiedBy`. Custom properties were not supported at all, and now are. Html Reader did not support any properties. It will now support all of them. * Scrutinizer Remove one dead reference. --- src/PhpSpreadsheet/Document/Properties.php | 14 ++ src/PhpSpreadsheet/Reader/Html.php | 89 ++++++++- src/PhpSpreadsheet/Reader/Xlsx/Properties.php | 3 + src/PhpSpreadsheet/Reader/Xml/Properties.php | 19 +- src/PhpSpreadsheet/Writer/Html.php | 42 +++- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 7 + src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 3 + .../Reader/Xml/XmlPropertiesTest.php | 187 ++++++++++++++++++ tests/data/Reader/Xml/hyperlinkbase.xml | 91 +++++++++ 9 files changed, 445 insertions(+), 10 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php create mode 100644 tests/data/Reader/Xml/hyperlinkbase.xml diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 162e818036..302afee79e 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -107,6 +107,8 @@ class Properties */ private $customProperties = []; + private string $hyperlinkBase = ''; + /** * Create a new Document Properties instance. */ @@ -534,4 +536,16 @@ public static function convertPropertyType(string $propertyType): string { return self::PROPERTY_TYPE_ARRAY[$propertyType] ?? self::PROPERTY_TYPE_UNKNOWN; } + + public function getHyperlinkBase(): string + { + return $this->hyperlinkBase; + } + + public function setHyperlinkBase(string $hyperlinkBase): self + { + $this->hyperlinkBase = $hyperlinkBase; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 377a8add06..fa3db908ef 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -8,6 +8,7 @@ use DOMText; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -685,10 +686,94 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) if ($loaded === false) { throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null); } + self::loadProperties($dom, $spreadsheet); return $this->loadDocument($dom, $spreadsheet); } + private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void + { + $properties = $spreadsheet->getProperties(); + foreach ($dom->getElementsByTagName('meta') as $meta) { + $metaContent = (string) $meta->getAttribute('content'); + if ($metaContent !== '') { + $metaName = (string) $meta->getAttribute('name'); + switch ($metaName) { + case 'author': + $properties->setCreator($metaContent); + + break; + case 'category': + $properties->setCategory($metaContent); + + break; + case 'company': + $properties->setCompany($metaContent); + + break; + case 'created': + $properties->setCreated($metaContent); + + break; + case 'description': + $properties->setDescription($metaContent); + + break; + case 'keywords': + $properties->setKeywords($metaContent); + + break; + case 'lastModifiedBy': + $properties->setLastModifiedBy($metaContent); + + break; + case 'manager': + $properties->setManager($metaContent); + + break; + case 'modified': + $properties->setModified($metaContent); + + break; + case 'subject': + $properties->setSubject($metaContent); + + break; + case 'title': + $properties->setTitle($metaContent); + + break; + default: + if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) { + switch ($matches[1]) { + case 'bool': + $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN); + + break; + case 'float': + $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT); + + break; + case 'int': + $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER); + + break; + case 'date': + $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE); + + break; + default: // string + $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING); + } + } + } + } + } + if (!empty($dom->baseURI)) { + $properties->setHyperlinkBase($dom->baseURI); + } + } + private static function replaceNonAscii(array $matches): string { return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; @@ -719,8 +804,10 @@ public function loadFromString($content, ?Spreadsheet $spreadsheet = null): Spre if ($loaded === false) { throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null); } + $spreadsheet = $spreadsheet ?? new Spreadsheet(); + self::loadProperties($dom, $spreadsheet); - return $this->loadDocument($dom, $spreadsheet ?? new Spreadsheet()); + return $this->loadDocument($dom, $spreadsheet); } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php index 72addffd5b..0d4701afac 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -73,6 +73,9 @@ public function readExtendedProperties(string $propertyData): void if (isset($xmlCore->Manager)) { $this->docProps->setManager((string) $xmlCore->Manager); } + if (isset($xmlCore->HyperlinkBase)) { + $this->docProps->setHyperlinkBase((string) $xmlCore->HyperlinkBase); + } } } diff --git a/src/PhpSpreadsheet/Reader/Xml/Properties.php b/src/PhpSpreadsheet/Reader/Xml/Properties.php index f0346ed02f..e216c254da 100644 --- a/src/PhpSpreadsheet/Reader/Xml/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xml/Properties.php @@ -92,6 +92,10 @@ protected function processStandardProperty( case 'Manager': $docProps->setManager($stringValue); + break; + case 'HyperlinkBase': + $docProps->setHyperlinkBase($stringValue); + break; case 'Keywords': $docProps->setKeywords($stringValue); @@ -110,17 +114,10 @@ protected function processCustomProperty( ?SimpleXMLElement $propertyValue, SimpleXMLElement $propertyAttributes ): void { - $propertyType = DocumentProperties::PROPERTY_TYPE_UNKNOWN; - switch ((string) $propertyAttributes) { - case 'string': - $propertyType = DocumentProperties::PROPERTY_TYPE_STRING; - $propertyValue = trim((string) $propertyValue); - - break; case 'boolean': $propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN; - $propertyValue = (bool) $propertyValue; + $propertyValue = (bool) (string) $propertyValue; break; case 'integer': @@ -134,9 +131,15 @@ protected function processCustomProperty( break; case 'dateTime.tz': + case 'dateTime.iso8601tz': $propertyType = DocumentProperties::PROPERTY_TYPE_DATE; $propertyValue = trim((string) $propertyValue); + break; + default: + $propertyType = DocumentProperties::PROPERTY_TYPE_STRING; + $propertyValue = trim((string) $propertyValue); + break; } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 575197aecb..842998f9eb 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -7,9 +7,11 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; use PhpOffice\PhpSpreadsheet\Settings; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont; @@ -342,13 +344,21 @@ public function writeAllSheets() private static function generateMeta(?string $val, string $desc): string { - return $val + return ($val || $val === '0') ? (' ' . PHP_EOL) : ''; } public const BODY_LINE = ' ' . PHP_EOL; + private const CUSTOM_TO_META = [ + Properties::PROPERTY_TYPE_BOOLEAN => 'bool', + Properties::PROPERTY_TYPE_DATE => 'date', + Properties::PROPERTY_TYPE_FLOAT => 'float', + Properties::PROPERTY_TYPE_INTEGER => 'int', + Properties::PROPERTY_TYPE_STRING => 'string', + ]; + /** * Generate HTML header. * @@ -374,6 +384,36 @@ public function generateHTMLHeader($includeStyles = false) $html .= self::generateMeta($properties->getCategory(), 'category'); $html .= self::generateMeta($properties->getCompany(), 'company'); $html .= self::generateMeta($properties->getManager(), 'manager'); + $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy'); + $date = Date::dateTimeFromTimestamp((string) $properties->getCreated()); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $html .= self::generateMeta($date->format(DATE_W3C), 'created'); + $date = Date::dateTimeFromTimestamp((string) $properties->getModified()); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $html .= self::generateMeta($date->format(DATE_W3C), 'modified'); + + $customProperties = $properties->getCustomProperties(); + foreach ($customProperties as $customProperty) { + $propertyValue = $properties->getCustomPropertyValue($customProperty); + $propertyType = $properties->getCustomPropertyType($customProperty); + $propertyQualifier = self::CUSTOM_TO_META[$propertyType] ?? null; + if ($propertyQualifier !== null) { + if ($propertyType === Properties::PROPERTY_TYPE_BOOLEAN) { + $propertyValue = $propertyValue ? '1' : '0'; + } elseif ($propertyType === Properties::PROPERTY_TYPE_DATE) { + $date = Date::dateTimeFromTimestamp((string) $propertyValue); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $propertyValue = $date->format(DATE_W3C); + } else { + $propertyValue = (string) $propertyValue; + } + $html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty"); + } + } + + if (!empty($properties->getHyperlinkBase())) { + $html .= ' ' . PHP_EOL; + } $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 9f23bd365e..aeedd08e77 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -503,6 +503,8 @@ public function close(): void $this->writeMergedCells(); // Hyperlinks + $phpParent = $phpSheet->getParent(); + $hyperlinkbase = ($phpParent === null) ? '' : $phpParent->getProperties()->getHyperlinkBase(); foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) { [$column, $row] = Coordinate::indexesFromString($coordinate); @@ -513,6 +515,11 @@ public function close(): void $url = str_replace('sheet://', 'internal:', $url); } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) { // URL + } elseif (!empty($hyperlinkbase) && preg_match('~^([A-Za-z]:)?[/\\\\]~', $url) !== 1) { + $url = "$hyperlinkbase$url"; + if (preg_match('/^(http:|https:|ftp:|mailto:)/', $url) !== 1) { + $url = 'external:' . $url; + } } else { // external (local file) $url = 'external:' . $url; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 8902826a19..8c33f59326 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -93,6 +93,9 @@ public function writeDocPropsApp(Spreadsheet $spreadsheet) // SharedDoc $objWriter->writeElement('SharedDoc', 'false'); + // HyperlinkBase + $objWriter->writeElement('HyperlinkBase', $spreadsheet->getProperties()->getHyperlinkBase()); + // HyperlinksChanged $objWriter->writeElement('HyperlinksChanged', 'false'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php new file mode 100644 index 0000000000..8b4a225d3f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php @@ -0,0 +1,187 @@ +load($this->filename); + + $properties = $spreadsheet->getProperties(); + self::assertSame('title', $properties->getTitle()); + self::assertSame('topic', $properties->getSubject()); + self::assertSame('author', $properties->getCreator()); + self::assertSame('keyword1, keyword2', $properties->getKeywords()); + self::assertSame('no comment', $properties->getDescription()); + self::assertSame('last author', $properties->getLastModifiedBy()); + $expected = self::timestampToInt('2023-05-18T11:21:43Z'); + self::assertEquals($expected, $properties->getCreated()); + $expected = self::timestampToInt('2023-05-18T11:30:00Z'); + self::assertEquals($expected, $properties->getModified()); + self::assertSame('category', $properties->getCategory()); + self::assertSame('manager', $properties->getManager()); + self::assertSame('company', $properties->getCompany()); + + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/', $properties->getHyperlinkBase()); + + self::assertSame('TheString', $properties->getCustomPropertyValue('StringProperty')); + self::assertSame(12345, $properties->getCustomPropertyValue('NumberProperty')); + $expected = self::timestampToInt('2023-05-18T10:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty')); + $expected = self::timestampToInt('2023-05-19T11:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty2')); + self::assertTrue($properties->getCustomPropertyValue('BooleanPropertyTrue')); + self::assertFalse($properties->getCustomPropertyValue('BooleanPropertyFalse')); + self::assertEqualsWithDelta(1.2345, $properties->getCustomPropertyValue('FloatProperty'), 1E-8); + + $sheet = $spreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // It will, however, work just fine in the Xlsx and Html copies. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('references/features-cross-reference/', $hyperlink->getUrl()); + // Same comment as for cell above. + self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $properties = $reloadedSpreadsheet->getProperties(); + self::assertSame('title', $properties->getTitle()); + self::assertSame('topic', $properties->getSubject()); + self::assertSame('author', $properties->getCreator()); + self::assertSame('keyword1, keyword2', $properties->getKeywords()); + self::assertSame('no comment', $properties->getDescription()); + self::assertSame('last author', $properties->getLastModifiedBy()); + $expected = self::timestampToInt('2023-05-18T11:21:43Z'); + self::assertEquals($expected, $properties->getCreated()); + $expected = self::timestampToInt('2023-05-18T11:30:00Z'); + self::assertEquals($expected, $properties->getModified()); + self::assertSame('category', $properties->getCategory()); + self::assertSame('manager', $properties->getManager()); + self::assertSame('company', $properties->getCompany()); + + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/', $properties->getHyperlinkBase()); + + self::assertSame('TheString', $properties->getCustomPropertyValue('StringProperty')); + self::assertSame(12345, $properties->getCustomPropertyValue('NumberProperty')); + // Note that Xlsx will ignore the time part when displaying + // the property. + $expected = self::timestampToInt('2023-05-18T10:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty')); + $expected = self::timestampToInt('2023-05-19T11:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty2')); + self::assertTrue($properties->getCustomPropertyValue('BooleanPropertyTrue')); + self::assertFalse($properties->getCustomPropertyValue('BooleanPropertyFalse')); + self::assertEqualsWithDelta(1.2345, $properties->getCustomPropertyValue('FloatProperty'), 1E-8); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // It will, however, work just fine in the Xlsx and Html copies. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('references/features-cross-reference/', $hyperlink->getUrl()); + // Same comment as for cell above. + self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testPropertiesHtml(): void + { + $reader = new Xml(); + $spreadsheet = $reader->load($this->filename); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Html'); + $spreadsheet->disconnectWorksheets(); + + $properties = $reloadedSpreadsheet->getProperties(); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/', $properties->getHyperlinkBase()); + + self::assertSame('title', $properties->getTitle()); + self::assertSame('topic', $properties->getSubject()); + self::assertSame('author', $properties->getCreator()); + self::assertSame('keyword1, keyword2', $properties->getKeywords()); + self::assertSame('no comment', $properties->getDescription()); + self::assertSame('last author', $properties->getLastModifiedBy()); + $expected = self::timestampToInt('2023-05-18T11:21:43Z'); + self::assertEquals($expected, $properties->getCreated()); + $expected = self::timestampToInt('2023-05-18T11:30:00Z'); + self::assertEquals($expected, $properties->getModified()); + self::assertSame('category', $properties->getCategory()); + self::assertSame('manager', $properties->getManager()); + self::assertSame('company', $properties->getCompany()); + + self::assertSame('TheString', $properties->getCustomPropertyValue('StringProperty')); + self::assertSame(12345, $properties->getCustomPropertyValue('NumberProperty')); + $expected = self::timestampToInt('2023-05-18T10:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty')); + $expected = self::timestampToInt('2023-05-19T11:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty2')); + self::assertTrue($properties->getCustomPropertyValue('BooleanPropertyTrue')); + self::assertFalse($properties->getCustomPropertyValue('BooleanPropertyFalse')); + self::assertEqualsWithDelta(1.2345, $properties->getCustomPropertyValue('FloatProperty'), 1E-8); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // It will, however, work just fine in the Xlsx and Html copies. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('references/features-cross-reference/', $hyperlink->getUrl()); + // Same comment as for cell above. + self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testHyperlinksXls(): void + { + $reader = new Xml(); + $spreadsheet = $reader->load($this->filename); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // However, Xls Writer will convert relative to absolute. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/references/features-cross-reference/', $hyperlink->getUrl()); + // Xls writer does not get involved in function call. + // However, hyperlink does get updated somewhere. + //self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + $hyperlink = $sheet->getCell('A2')->getHyperlink(); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/topics/accessing-cells/', $hyperlink->getUrl()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + private static function timestampToInt(string $timestamp): string + { + $dto = new DateTimeImmutable($timestamp); + + return $dto->format('U'); + } +} diff --git a/tests/data/Reader/Xml/hyperlinkbase.xml b/tests/data/Reader/Xml/hyperlinkbase.xml new file mode 100644 index 0000000000..e3448ee994 --- /dev/null +++ b/tests/data/Reader/Xml/hyperlinkbase.xml @@ -0,0 +1,91 @@ + + + + + title + topic + author + keyword1, keyword2 + no comment + last author + 2023-05-18T11:21:43Z + 2023-05-18T11:30:00Z + category + manager + company + https://phpspreadsheet.readthedocs.io/en/latest/ + 16.00 + + + TheString + 12345 + 2023-05-18T10:00:00Z + 2023-05-19T11:00:00Z + 1 + 0 + 1.2345 + + + + + + 6820 + 19200 + 32767 + 32767 + False + False + + + + + + + + + references/features-cross-reference/ + + + topics/accessing-cells/ + + + https://www.google.com + + + https://www.yahoo.com + +
+ + + + + + 3 + 3 + + + False + False + +
+