Fix: Add iconv fallback when mbstring extension is missing + tests
This commit is contained in:
parent
d43654d445
commit
571be1b375
|
|
@ -3693,9 +3693,14 @@ class PHPMailer
|
|||
if (function_exists('mb_strlen')) {
|
||||
return strlen($str) > mb_strlen($str, $this->CharSet);
|
||||
}
|
||||
|
||||
// Fallback to iconv if available
|
||||
if (function_exists('iconv_strlen')) {
|
||||
return strlen($str) > iconv_strlen($str, $this->CharSet);
|
||||
}
|
||||
|
||||
//Assume no multibytes (we can't handle without mbstring functions anyway)
|
||||
return false;
|
||||
// If neither mbstring nor iconv is available, make a basic check for non-ASCII characters
|
||||
return preg_match('/[^\x00-\x7F]/', $str) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3713,7 +3718,7 @@ class PHPMailer
|
|||
/**
|
||||
* Encode and wrap long multibyte strings for mail headers
|
||||
* without breaking lines within a character.
|
||||
* Adapted from a function by paravoid.
|
||||
* Uses mbstring if available, with fallback to iconv or native functions.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
|
||||
*
|
||||
|
|
@ -3731,28 +3736,54 @@ class PHPMailer
|
|||
$linebreak = static::$LE;
|
||||
}
|
||||
|
||||
$mb_length = mb_strlen($str, $this->CharSet);
|
||||
//Each line must have length <= 75, including $start and $end
|
||||
$length = 75 - strlen($start) - strlen($end);
|
||||
//Average multi-byte ratio
|
||||
$ratio = $mb_length / strlen($str);
|
||||
//Base64 has a 4:3 ratio
|
||||
$avgLength = floor($length * $ratio * .75);
|
||||
// Use mbstring if available
|
||||
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
|
||||
$mb_length = mb_strlen($str, $this->CharSet);
|
||||
// Each line must have length <= 75, including $start and $end
|
||||
$length = 75 - strlen($start) - strlen($end);
|
||||
// Average multi-byte ratio
|
||||
$ratio = $mb_length / strlen($str);
|
||||
// Base64 has a 4:3 ratio
|
||||
$avgLength = floor($length * $ratio * .75);
|
||||
|
||||
$offset = 0;
|
||||
for ($i = 0; $i < $mb_length; $i += $offset) {
|
||||
$lookBack = 0;
|
||||
do {
|
||||
$offset = $avgLength - $lookBack;
|
||||
$chunk = mb_substr($str, $i, $offset, $this->CharSet);
|
||||
$chunk = base64_encode($chunk);
|
||||
++$lookBack;
|
||||
} while (strlen($chunk) > $length);
|
||||
$encoded .= $chunk . $linebreak;
|
||||
$offset = 0;
|
||||
for ($i = 0; $i < $mb_length; $i += $offset) {
|
||||
$lookBack = 0;
|
||||
do {
|
||||
$offset = $avgLength - $lookBack;
|
||||
$chunk = mb_substr($str, $i, $offset, $this->CharSet);
|
||||
$chunk = base64_encode($chunk);
|
||||
++$lookBack;
|
||||
} while (strlen($chunk) > $length);
|
||||
$encoded .= $chunk . $linebreak;
|
||||
}
|
||||
}
|
||||
// Fallback to iconv if mbstring is not available
|
||||
elseif (function_exists('iconv_strlen') && function_exists('iconv_substr')) {
|
||||
$iconv_length = iconv_strlen($str, $this->CharSet);
|
||||
$length = 75 - strlen($start) - strlen($end);
|
||||
$ratio = $iconv_length / strlen($str);
|
||||
$avgLength = floor($length * $ratio * .75);
|
||||
|
||||
$offset = 0;
|
||||
for ($i = 0; $i < $iconv_length; $i += $offset) {
|
||||
$lookBack = 0;
|
||||
do {
|
||||
$offset = $avgLength - $lookBack;
|
||||
$chunk = iconv_substr($str, $i, $offset, $this->CharSet);
|
||||
$chunk = base64_encode($chunk);
|
||||
++$lookBack;
|
||||
} while (strlen($chunk) > $length);
|
||||
$encoded .= $chunk . $linebreak;
|
||||
}
|
||||
}
|
||||
// Fallback to basic implementation if neither mbstring nor iconv is available
|
||||
else {
|
||||
$encoded = chunk_split(base64_encode($str), 76, $linebreak);
|
||||
$encoded = trim($encoded, $linebreak);
|
||||
}
|
||||
|
||||
//Chomp the last linefeed
|
||||
return substr($encoded, 0, -strlen($linebreak));
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
namespace PHPMailer\PHPMailer;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Test fallback behavior when mbstring extension is not available
|
||||
*/
|
||||
class NoMbstringTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var PHPMailer
|
||||
*/
|
||||
protected $mail;
|
||||
|
||||
/**
|
||||
* Backup of mbstring functions
|
||||
*/
|
||||
protected static $mbstringFunctions = [
|
||||
'mb_strlen' => null,
|
||||
'mb_substr' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Set up before each test
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mail = new PHPMailer();
|
||||
$this->mail->CharSet = PHPMailer::CHARSET_UTF8;
|
||||
|
||||
// Backup mbstring functions if they exist
|
||||
foreach (array_keys(self::$mbstringFunctions) as $function) {
|
||||
if (function_exists($function)) {
|
||||
self::$mbstringFunctions[$function] = $function;
|
||||
$this->disableFunction($function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore mbstring functions after each test
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Restore mbstring functions
|
||||
foreach (self::$mbstringFunctions as $function => $original) {
|
||||
if ($original !== null) {
|
||||
$this->restoreFunction($function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that hasMultiBytes works without mbstring
|
||||
*/
|
||||
public function testHasMultiBytesWithoutMbstring()
|
||||
{
|
||||
// Test with ASCII string (should return false)
|
||||
$this->assertFalse($this->mail->hasMultiBytes('ASCII string'));
|
||||
|
||||
// Test with multibyte string (should return true)
|
||||
$this->assertTrue($this->mail->hasMultiBytes('Multibyte string: ñáéíóú'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that base64EncodeWrapMB works without mbstring
|
||||
*/
|
||||
public function testBase64EncodeWrapMBWithoutMbstring()
|
||||
{
|
||||
$testString = 'This is a test string with multibyte characters: ñáéíóú';
|
||||
$encoded = $this->mail->base64EncodeWrapMB($testString);
|
||||
|
||||
// The encoded string should not be empty
|
||||
$this->assertNotEmpty($encoded, 'Encoded string is empty');
|
||||
|
||||
// When decoded, it should match the original string
|
||||
$normalized = preg_replace('/\s+/', '', $encoded);
|
||||
$decoded = base64_decode($normalized, true);
|
||||
$this->assertNotFalse($decoded, 'Failed to decode base64 string');
|
||||
$this->assertEquals($testString, $decoded, 'Decoded string does not match original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that encodeString works without mbstring
|
||||
*/
|
||||
public function testEncodeStringWithoutMbstring()
|
||||
{
|
||||
$testString = 'This is a test string with multibyte characters: ñáéíóú';
|
||||
$encoded = $this->mail->encodeString($testString, PHPMailer::ENCODING_BASE64);
|
||||
|
||||
// The encoded string should not be empty
|
||||
$this->assertNotEmpty($encoded, 'Encoded string is empty');
|
||||
|
||||
// When decoded, it should match the original string
|
||||
$normalized = preg_replace('/\s+/', '', $encoded);
|
||||
$decoded = base64_decode($normalized, true);
|
||||
$this->assertNotFalse($decoded, 'Failed to decode base64 string');
|
||||
$this->assertEquals($testString, $decoded, 'Decoded string does not match original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a function for testing purposes
|
||||
*
|
||||
* @param string $functionName Name of the function to disable
|
||||
*/
|
||||
private function disableFunction($functionName)
|
||||
{
|
||||
$namespace = __NAMESPACE__;
|
||||
$code = <<<EOT
|
||||
namespace {$namespace};
|
||||
if (!function_exists('{$functionName}')) {
|
||||
function {$functionName}() {
|
||||
throw new \RuntimeException('{$functionName} should not be called in this test');
|
||||
}
|
||||
}
|
||||
EOT;
|
||||
eval($code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a function that was previously disabled
|
||||
*
|
||||
* @param string $functionName Name of the function to restore
|
||||
*/
|
||||
private function restoreFunction($functionName)
|
||||
{
|
||||
$namespace = __NAMESPACE__;
|
||||
$code = <<<EOT
|
||||
namespace {$namespace};
|
||||
if (function_exists('{$functionName}_backup')) {
|
||||
function {$functionName}() {
|
||||
return call_user_func_array('{$functionName}_backup', func_get_args());
|
||||
}
|
||||
}
|
||||
EOT;
|
||||
eval($code);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue