Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update LanguageHelper.php to handle translated text with embedded new lines. #42456

Closed
wants to merge 11 commits into from
132 changes: 112 additions & 20 deletions libraries/src/Language/LanguageHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,38 +404,130 @@ public static function parseIniFile($fileName, $debug = false)
return [];
}

// Capture hidden PHP errors from the parsing.
if ($debug === true) {
// See https://www.php.net/manual/en/reserved.variables.phperrormsg.php
$php_errormsg = null;

$trackErrors = ini_get('track_errors');
ini_set('track_errors', true);
}

// This was required for https://github.com/joomla/joomla-cms/issues/17198 but not sure what server setup
// issue it is solving
$disabledFunctions = explode(',', ini_get('disable_functions'));
$isParseIniFileDisabled = \in_array('parse_ini_file', array_map('trim', $disabledFunctions));

if (!\function_exists('parse_ini_file') || $isParseIniFileDisabled) {
$contents = file_get_contents($fileName);
$strings = @parse_ini_string($contents, false, INI_SCANNER_RAW);
} else {
$strings = @parse_ini_file($fileName, false, INI_SCANNER_RAW);
}
// Capture hidden PHP errors from the parsing.
set_error_handler(static function ($errno, $err) {
throw new \Exception($err);
}, \E_WARNING);
BrainforgeUK marked this conversation as resolved.
Show resolved Hide resolved

try {
if (!\function_exists('parse_ini_file') || $isParseIniFileDisabled) {
$contents = file_get_contents($fileName);
$strings = parse_ini_string($contents, false, INI_SCANNER_RAW);
} else {
$strings = parse_ini_file($fileName, false, INI_SCANNER_RAW);
}
} catch (\Exception $e) {
// Emulate Joomla 4.4.0 and earlier handling of multi-line text strings.
// Some developers prefer to format long descriptive strings in that way for easier maintenance.
// For performance reasons we only do this as a last resort as most .ini files are not affected by this.
$strings = self::parseMultilineIni(isset($contents) ? $contents : file_get_contents($fileName));
laoneo marked this conversation as resolved.
Show resolved Hide resolved

if ($strings === false) {
if ($debug) {
throw new \RuntimeException($e->getMessage());
}

// Ini files are processed in the "RAW" mode of parse_ini_string, leaving escaped quotes untouched - lets postprocess them
$strings = str_replace('\"', '"', $strings);
return [];
}

// Restore error tracking to what it was before.
if ($debug === true) {
ini_set('track_errors', $trackErrors);
// Ini file processing has left escaped quotes untouched - let's postprocess them
$strings = str_replace('\"', '"', $strings);
} finally {
restore_error_handler();
}

return \is_array($strings) ? $strings : [];
}

/**
* Parse strings from a language file.
* See issue https://github.com/joomla/joomla-cms/issues/42416
* Emulates Joomla v4.4.0 and earlier behaviour with multi-line text strings without the potential security concerns.
*
* @param string $fileName The language ini file path.
* @param boolean $debug If set to true debug language ini file.
*
* @return array | false The strings parsed.
*
* @since __DEPLOY_VERSION__
*/
protected static function parseMultilineIni($inifileContents)
{
$lines = explode("\n", $inifileContents);

$nameValuePairs = [];

for ($l=0; $l < count($lines); $l++) {
BrainforgeUK marked this conversation as resolved.
Show resolved Hide resolved
$line = trim($lines[$l]);

if (empty($line)) {
continue;
}

if ($line[0] == ';') {
continue;
}

$lineParts = explode('=', $line);
if (count($lineParts) != 2) {
// Unexpected file contents
return false;
}

// In case there is a space before / after =
$lineParts[0] = rtrim($lineParts[0]);
$lineParts[1] = ltrim($lineParts[1]);

// We are only expecting strings for language translation
if (!str_starts_with($lineParts[1], '"')) {
// Unexpected file contents
return false;
}

if (str_ends_with($lineParts[1], '"')) {
// Just in case we have a line ending with an escaped "
if (!str_ends_with($line, '\\"')) {
if (strlen($lineParts[1]) == 2) {
$nameValuePairs[$lineParts[0]] = '';
continue;
}
$nameValuePairs[$lineParts[0]] = substr($lineParts[1], 1, strlen($lineParts[1]) - 2);

continue;
}
}

// Found a string with embedded new lines
$nameValuePair = $lineParts[1];

while (++$l < count($lines)) {
$line = trim($lines[$l]);

$nameValuePair .= "\n" . $line;

if (str_ends_with($line, '"')) {
if (!str_ends_with($line, '\\"')) {
$nameValuePairs[$lineParts[0]] = substr($nameValuePair, 1, strlen($nameValuePair) - 1);

continue 2;
}
}
}

if (!str_starts_with($lineParts[1], '"')) {
// Unexpected file contents
return false;
}
}

return $nameValuePairs;
}

/**
* Save strings to a language file.
*
Expand Down