Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 8 additions & 74 deletions src/DBmysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use Glpi\DBAL\QueryUnion;
use Glpi\Debug\Profile;
use Glpi\System\Requirement\DbTimezones;
use Glpi\Toolbox\SanitizedStringsDecoder;
use Safe\DateTime;

use function Safe\filesize;
Expand All @@ -53,17 +54,6 @@
**/
class DBmysql
{
private const CHARS_MAPPING = [
'&' => '&',
'<' => '&#60;',
'>' => '&#62;',
];

private const LEGACY_CHARS_MAPPING = [
'<' => '&lt;',
'>' => '&gt;',
];

//! Database Host - string or Array of string (round-robin)
public $dbhost = "";
//! Database User
Expand Down Expand Up @@ -2188,68 +2178,6 @@ public function getComputedConfigBooleanFlags(): array
return $config_flags;
}

/**
* Check if special chars are encoded.
*
* @param string $value
*
* @return bool
*/
private function isHtmlEncoded(string $value): bool
{
// A value is Html Encoded if it does not contains
// - `<`;
// - `>`;
// - `&` not followed by an HTML entity identifier;
// and if it contains any entity used to encode HTML special chars during sanitization process.
$special_chars_pattern = '/(<|>|(&(?!#?[a-z0-9]+;)))/i';
$sanitized_chars = array_merge(
array_values(self::CHARS_MAPPING),
array_values(self::LEGACY_CHARS_MAPPING)
);
$sanitized_chars_pattern = '/(' . implode('|', $sanitized_chars) . ')/';

return preg_match($special_chars_pattern, $value) === 0
&& preg_match($sanitized_chars_pattern, $value) === 1;
}

/**
* Decode HTML special chars.
*
* @param string $value
*
* @return string
*/
private function decodeHtmlSpecialChars(string $value): string
{
if (!$this->isHtmlEncoded($value)) {
return $value;
}

$mapping = null;
foreach (self::CHARS_MAPPING as $htmlentity) {
if (str_contains($value, $htmlentity)) {
// Value was cleaned using new char mapping, so it must be uncleaned with same mapping
$mapping = self::CHARS_MAPPING;
break;
}
}
if ($mapping === null) {
$mapping = self::LEGACY_CHARS_MAPPING; // Fallback to legacy chars mapping

if (preg_match('/&lt;img\s+(alt|src|width)=&quot;/', $value)) {
// In some cases (at least on some ITIL followups, quotes have been converted too,
// probably due to a misusage of encoding process.
// Result is that quotes were encoded too (i.e. `&lt:img src=&quot;/front/document.send.php`)
// and should be decoded too.
$mapping['"'] = '&quot;';
}
}

$mapping = array_reverse($mapping);
return str_replace(array_values($mapping), array_keys($mapping), $value);
}

/**
* Decode HTML special chars on fetch operation result.
*/
Expand All @@ -2265,7 +2193,13 @@ private function decodeFetchResult(array|object|false|null $values): array|objec
continue;
}

$value = $this->decodeHtmlSpecialChars($value);
$decoder = new SanitizedStringsDecoder();

if ($key === 'completename') {
$value = $decoder->decodeHtmlSpecialCharsInCompletename($value);
} else {
$value = $decoder->decodeHtmlSpecialChars($value);
}

if (is_object($values)) {
$values->{$key} = $value;
Expand Down
16 changes: 11 additions & 5 deletions src/Glpi/Search/Provider/SQLProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
use Glpi\Search\Input\QueryBuilder;
use Glpi\Search\SearchEngine;
use Glpi\Search\SearchOption;
use Glpi\Toolbox\SanitizedStringsDecoder;
use Group;
use Group_Item;
use Group_KnowbaseItem;
Expand Down Expand Up @@ -5598,9 +5599,11 @@ public static function giveItem(
$added = [];
$count_display = 0;
for ($k = 0; $k < $data[$ID]['count']; $k++) {
$completename = isset($data[$ID][$k]['name']) && (strlen(trim($data[$ID][$k]['name'])) > 0)
? (new SanitizedStringsDecoder())->decodeHtmlSpecialCharsInCompletename($data[$ID][$k]['name'])
: null;
if (
isset($data[$ID][$k]['name'])
&& (strlen(trim($data[$ID][$k]['name'])) > 0)
$completename !== null
&& !in_array(
$data[$ID][$k]['name'] . "-" . $data[$ID][$k]['profiles_id'],
$added
Expand Down Expand Up @@ -5634,7 +5637,7 @@ public static function giveItem(
}
return $out;
} elseif (($so["datatype"] ?? "") != "itemlink" && !empty($data[$ID][0]['name'])) {
$completename = $data[$ID][0]['name'];
$completename = (new SanitizedStringsDecoder())->decodeHtmlSpecialCharsInCompletename($data[$ID][0]['name']);
if ($html_output) {
if (!$_SESSION['glpiuse_flat_dropdowntree_on_search_result']) {
$split_name = explode(">", $completename);
Expand All @@ -5658,7 +5661,8 @@ public static function giveItem(
&& $data[$ID][0]['name'] != null //column have value in DB
&& !$_SESSION['glpiuse_flat_dropdowntree_on_search_result'] //user doesn't want the completename
) {
$split_name = explode(">", $data[$ID][0]['name']);
$completename = (new SanitizedStringsDecoder())->decodeHtmlSpecialCharsInCompletename($data[$ID][0]['name']);
$split_name = explode(">", $completename);
return htmlescape(trim(end($split_name)));
}
break;
Expand Down Expand Up @@ -6427,6 +6431,7 @@ public static function giveItem(
$name = sprintf(__('%1$s (%2$s)'), $name, $data[$ID][$k]['id']);
}
if (isset($field) && $field === 'completename') {
$name = (new SanitizedStringsDecoder())->decodeHtmlSpecialCharsInCompletename($data[$ID][0]['name']);
$chunks = \explode(' > ', $name);
$completename = '';
foreach ($chunks as $key => $element_name) {
Expand Down Expand Up @@ -6723,7 +6728,8 @@ public static function giveItem(
if (isset($field_data['trans']) && !empty($field_data['trans'])) {
$out .= \htmlescape($field_data['trans']);
} elseif (isset($field_data['trans_completename']) && !empty($field_data['trans_completename'])) {
$out .= \htmlescape($field_data['trans_completename']);
$value = (new SanitizedStringsDecoder())->decodeHtmlSpecialCharsInCompletename($field_data['trans_completename']);
$out .= \htmlescape($value);
} elseif (isset($field_data['trans_name']) && !empty($field_data['trans_name'])) {
$out .= \htmlescape($field_data['trans_name']);
} else {
Expand Down
108 changes: 108 additions & 0 deletions src/Glpi/Toolbox/SanitizedStringsDecoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2025 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

namespace Glpi\Toolbox;

use function Safe\preg_match;

class SanitizedStringsDecoder
{
private const CHARS_MAPPING = [
'<' => '&#60;',
'>' => '&#62;',
'&' => '&#38;',
];

private const LEGACY_CHARS_MAPPING = [
'<' => '&lt;',
'>' => '&gt;',
];

/**
* Decode HTML special chars.
*/
public function decodeHtmlSpecialChars(string $value): string
{
$mapping = [];

if (
// A value was HTML encoded in GLPI 10.0.x if
// - it does not contains `<`, `>` and `&` not followed by an HTML entity identifier;
// - it contains any entity used to encode HTML special chars during sanitization process.
preg_match('/(<|>|(&(?!#?[a-z0-9]+;)))/i', $value) === 0
&& preg_match('/(' . implode('|', array_values(self::CHARS_MAPPING)) . ')/', $value) === 1
) {
$mapping = self::CHARS_MAPPING;
} elseif (
// A value was HTML encoded in GLPI <= 9.5 if
// - it does not contains `<` and `>`;
// - it contains `&lt;` or `&gt;`.
preg_match('/(<|>)/i', $value) === 0
&& preg_match('/(' . implode('|', array_values(self::LEGACY_CHARS_MAPPING)) . ')/', $value) === 1
) {
$mapping = self::LEGACY_CHARS_MAPPING;

if (preg_match('/&lt;img\s+(alt|src|width)=&quot;/', $value)) {
// In some cases (at least on some ITIL followups, quotes have been converted too,
// probably due to a misusage of encoding process.
// Result is that quotes were encoded too (i.e. `&lt:img src=&quot;/front/document.send.php`)
// and should be decoded too.
$mapping['"'] = '&quot;';
}
}

if ($mapping !== []) {
$value = str_replace(array_values($mapping), array_keys($mapping), $value);
}

return $value;
}

/**
* Decode HTML special chars in completename field value.
*/
public function decodeHtmlSpecialCharsInCompletename(string $value): string
{
$separator = '>';

return implode(
$separator,
array_map(
fn(string $chunk) => $this->decodeHtmlSpecialChars($chunk),
explode($separator, $value)
)
);
}
}
38 changes: 38 additions & 0 deletions tests/functional/DBmysqlIteratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,7 @@ public function testInCriteria()
public static function resultProvider(): iterable
{
// Data from GLPI 9.5- (autosanitized)
// `&` was not encoded, `<` and `>` were encoded to `&lt;` and `&gt;`
yield [
'db_data' => [
'id' => 1,
Expand All @@ -1653,8 +1654,21 @@ public static function resultProvider(): iterable
'content' => '<p>Test</p>',
],
];
yield [
'db_data' => [
'id' => 1,
'name' => '&foo',
'completename' => 'A&B > &foo',
],
'result' => [
'id' => 1,
'name' => '&foo',
'completename' => 'A&B > &foo',
],
];

// Data from GLPI 10.0.x (autosanitized)
// `&`, `<` and `>` were encoded to `&#38;`, `&#60;` and `&#62;`
yield [
'db_data' => [
'id' => 1,
Expand All @@ -1667,6 +1681,18 @@ public static function resultProvider(): iterable
'content' => '<p>Test</p>',
],
];
yield [
'db_data' => [
'id' => 1,
'name' => '&#38;foo',
'completename' => 'A&#38;B > &#38;foo',
],
'result' => [
'id' => 1,
'name' => '&foo',
'completename' => 'A&B > &foo',
],
];

// Data from GLPI 11.0+ (not autosanitized)
yield [
Expand All @@ -1681,6 +1707,18 @@ public static function resultProvider(): iterable
'content' => '<p>Test</p>',
],
];
yield [
'db_data' => [
'id' => 1,
'name' => '&foo',
'completename' => 'A&B > &foo',
],
'result' => [
'id' => 1,
'name' => '&foo',
'completename' => 'A&B > &foo',
],
];
}


Expand Down
Loading