Repository: PHPOffice/PhpSpreadsheet (php)

Issue:
A mailto hyperlink emits the url text instead of the hyperlink in Html output
This is:

```
- [X] a bug report
- [ ] a feature request
- [ ] **not** a usage question (ask them on https://stackoverflow.com/questions/tagged/phpspreadsheet or https://gitter.im/PHPOffice/PhpSpreadsheet)
```
Setting a mailto hyperlink on a cell replaces the cell contents with the mailto url when writing HTML.

The bug appears to be this line in Html.php:

if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) {

The problem is corrected by adding mailto to the list of url schemes:

if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3','mailto'], true)) {

### What is the expected behavior?
Output:

<td class="column0 style0 s"><a href="mailto:me@example.com" title="">Mail Me!</a></td>

### What is the current behavior?
Outputs:

<td class="column0 style0 s">mailto:me@example.com</td>

### What are the steps to reproduce?

Please provide a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) of code that exhibits the issue without relying on an external Excel file or a web server:

```php
<?php
require __DIR__ . '/vendor/autoload.php';

$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();

$worksheet = $spreadsheet->getActiveSheet();

$worksheet->setCellValueByColumnAndRow(1, 1, 'Mail Me!');

$worksheet->getCell('A1')->getHyperlink()->setUrl('mailto:me@example.com');

$writer = PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Html');

$writer->save('php://output');
?>
```
If this is an issue with reading a specific spreadsheet file, then it may be appropriate to provide a sample file that demonstrates the problem; but please keep it as small as possible, and sanitize any confidential information before uploading.

### What features do you think are causing the issue

- [ ] Reader
- [X] Writer
- [ ] Styles
- [ ] Data Validations
- [ ] Formula Calculations
- [ ] Charts
- [ ] AutoFilter
- [ ] Form Elements

### Does an issue affect all spreadsheet file formats? If not, which formats are affected?
Only affects HTML output

### Which versions of PhpSpreadsheet and PHP are affected?
At least 1.28 onwards

Pull request:
Html Writer Allow mailto
Fix #4316. A security patch white-listed the protocols that could be used in a hyperlink. This PR adds mailto to the list.

This is:

- [x] a bugfix
- [ ] a new feature
- [ ] refactoring
- [ ] additional unit tests

Checklist:

- [x] Changes are covered by unit tests
  - [x] Changes are covered by existing unit tests
  - [x] New unit tests have been added
- [x] Code style is respected
- [x] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
- [ ] CHANGELOG.md contains a short summary of the change and a link to the pull request if applicable
- [ ] Documentation is updated as necessary

### Why this change is needed?

Provide an explanation of why this change is needed, with links to any Issues (if appropriate).
If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will make it easier to track progress with this PR.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 032bc3160..f686f3e66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
 
 - Ods Reader Sheet Names with Period. [Issue #4311](https://github.com/PHPOffice/PhpSpreadsheet/issues/4311) [PR #4313](https://github.com/PHPOffice/PhpSpreadsheet/pull/4313)
 - Mpdf and Tcpdf Hidden Columns and Merged Cells. [Issue #4319](https://github.com/PHPOffice/PhpSpreadsheet/issues/4319) [PR #4320](https://github.com/PHPOffice/PhpSpreadsheet/pull/4320)
+- Html Writer Allow mailto. [Issue #4316](https://github.com/PHPOffice/PhpSpreadsheet/issues/4316) [PR #4322](https://github.com/PHPOffice/PhpSpreadsheet/pull/4322)
 
 ## 2025-01-11 - 3.8.0
 
diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php
index 5ddabc858..1cb1288d0 100644
--- a/src/PhpSpreadsheet/Writer/Html.php
+++ b/src/PhpSpreadsheet/Writer/Html.php
@@ -1601,10 +1601,15 @@ class Html extends BaseWriter
                 $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
                 $urlTrim = preg_replace('/^\\s+/u', '', $urlDecode1) ?? $urlDecode1;
                 $parseScheme = preg_match('/^([\\w\\s]+):/u', strtolower($urlTrim), $matches);
-                if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) {
+                if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 'mailto', 's3'], true)) {
                     $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
                 } else {
-                    $cellData = '<a href="' . htmlspecialchars($url, Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>';
+                    $tooltip = $worksheet->getHyperlink($coordinate)->getTooltip();
+                    $tooltipOut = empty($tooltip) ? '' : (' title="' . htmlspecialchars($tooltip) . '"');
+                    $cellData = '<a href="'
+                        . htmlspecialchars($url) . '"'
+                        . $tooltipOut
+                        . '>' . $cellData . '</a>';
                 }
             }
 


Test patch:
diff --git a/tests/PhpSpreadsheetTests/Writer/Html/MailtoTest.php b/tests/PhpSpreadsheetTests/Writer/Html/MailtoTest.php
new file mode 100644
index 000000000..e9f250d37
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Writer/Html/MailtoTest.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Writer\Html;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;
+use PHPUnit\Framework\TestCase;
+
+class MailtoTest extends TestCase
+{
+    public function testBadHyperlink(): void
+    {
+        $spreadsheet = new Spreadsheet();
+        $worksheet = $spreadsheet->getActiveSheet();
+        $worksheet->setCellValue('A1', 'Mail Me!');
+        $worksheet->getCell('A1')
+            ->getHyperlink()
+            ->setUrl('mailto:me@example.com');
+        $worksheet->setCellValue('A2', 'Mail You!');
+        $worksheet->getCell('A2')
+            ->getHyperlink()
+            ->setTooltip('go ahead')
+            ->setUrl('mailto:you@example.com');
+        $writer = new HtmlWriter($spreadsheet);
+        $html = $writer->generateHtmlAll();
+        self::assertStringContainsString('<a href="mailto:me@example.com">Mail Me!</a>', $html);
+        self::assertStringContainsString('<a href="mailto:you@example.com" title="go ahead">Mail You!</a>', $html);
+        $spreadsheet->disconnectWorksheets();
+    }
+}
diff --git a/tests/PhpSpreadsheetTests/Writer/Html/NoJavascriptLinksTest.php b/tests/PhpSpreadsheetTests/Writer/Html/NoJavascriptLinksTest.php
index 7b1a633bc..cc0511d4f 100644
--- a/tests/PhpSpreadsheetTests/Writer/Html/NoJavascriptLinksTest.php
+++ b/tests/PhpSpreadsheetTests/Writer/Html/NoJavascriptLinksTest.php
@@ -25,7 +25,7 @@ class NoJavascriptLinksTest extends TestCase
 
         $writer = new Html($spreadsheet);
         $html = $writer->generateHTMLAll();
-        self::assertStringContainsString('<td class="column0 style0 s"><a href="http://www.example.com" title="">Click me</a></td>', $html, 'http hyperlink retained');
+        self::assertStringContainsString('<td class="column0 style0 s"><a href="http://www.example.com">Click me</a></td>', $html, 'http hyperlink retained');
         self::assertStringContainsString('<td class="column0 style0 s">javascript:alert(\'hello1\')</td>', $html, 'javascript hyperlink dropped');
         self::assertStringContainsString('<td class="column0 style0 f">javascript:alert(\'hello2\')</td>', $html, 'javascript hyperlink function dropped');
         $spreadsheet->disconnectWorksheets();