Skip to content

Commit

Permalink
Merge pull request #404 from oat-sa/fix/TR-6347/allow-special-html-sy…
Browse files Browse the repository at this point in the history
…mbols

fix: response values encoding to support compatibility with XML when generating the results
  • Loading branch information
wazelin authored Nov 6, 2024
2 parents 2a135d3 + c0bafda commit 6508bc9
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 2 deletions.
64 changes: 63 additions & 1 deletion src/qtism/data/storage/xml/Utils.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
Expand Down Expand Up @@ -353,11 +355,71 @@ public static function valueAsString($value, $encode = true): string
return $value === true ? 'true' : 'false';
}
if ($encode) {
return htmlspecialchars((string)$value, ENT_XML1, 'UTF-8');
return self::xmlSpecialChars((string)$value);
}
return (string)$value;
}

private static function isInCharacterRange(int $char): bool
{
return $char == 0x09
|| $char == 0x0A
|| $char == 0x0D
|| $char >= 0x20 && $char <= 0xDF77
|| $char >= 0xE000 && $char <= 0xFFFD
|| $char >= 0x10000 && $char <= 0x10FFFF;
}

public static function xmlSpecialChars(string $value): string
{
$result = '';

$last = 0;
$length = strlen($value);
$i = 0;

while ($i < $length) {
$r = mb_substr(substr($value, $i), 0, 1);
$width = strlen($r);
$i += $width;
switch ($r) {
case '"':
$esc = '&#34;';
break;
case "'":
$esc = '&#39;';
break;
case '&':
$esc = '&amp;';
break;
case '<':
$esc = '&lt;';
break;
case '>':
$esc = '&gt;';
break;
case "\t":
$esc = '&#x9;';
break;
case "\n":
$esc = '&#xA;';
break;
case "\r":
$esc = '&#xD;';
break;
default:
if (!self::isInCharacterRange(mb_ord($r)) || (mb_ord($r) === 0xFFFD && $width === 1)) {
$esc = "\u{FFFD}";
break;
}
continue 2;
}
$result .= substr($value, $last, $i - $last - $width) . $esc;
$last = $i;
}
return $result . substr($value, $last);
}

/**
* Get the child elements of a given element by tag name. This method does
* not behave like DOMElement::getElementsByTagName. It only returns the direct
Expand Down
47 changes: 46 additions & 1 deletion test/qtismtest/data/storage/xml/XmlUtilsTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace qtismtest\data\storage\xml;

use DOMDocument;
Expand Down Expand Up @@ -386,11 +388,54 @@ public function testFindCustomNamespaces(): void
');

self::assertSame(
['qh5'=>'http://www.imsglobal.org/xsd/imsqtiv2p2_html5_v1p0'],
['qh5' => 'http://www.imsglobal.org/xsd/imsqtiv2p2_html5_v1p0'],
Utils::findExternalNamespaces($xml)
);
}

public function testValueAsStringReplaceSpecialSymbols(): void
{
$this->assertEquals("160\u{FFFD}", Utils::valueAsString("160\u{0008}"));
}

public function testProcessSpecialCharsetWithoutError(): void
{
$xml = ('<assessmentResult
xmlns="http://www.imsglobal.org/xsd/imsqti_result_v2p1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<context/>
<testResult identifier="44127db28512-suomynona#903756e974e7#e94025be336b1f89159af64b1f6eda5d470ac8d61#local-dev-acc.nextgen-stack-local" datestamp="2024-10-30T12:56:32+00:00"/>
<itemResult identifier="item-1" datestamp="2024-10-30T12:56:32+00:00" sessionStatus="final">
<responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
<candidateResponse>
<value>1</value>
</candidateResponse>
</responseVariable>
<responseVariable identifier="duration" cardinality="single" baseType="duration">
<candidateResponse>
<value>PT22S</value>
</candidateResponse>
</responseVariable>
<outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
<value>completed</value>
</outcomeVariable>
<outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
<value>0</value>
</outcomeVariable>
<outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
<value>1</value>
</outcomeVariable>
<responseVariable identifier="RESPONSE" cardinality="single" baseType="string">
<candidateResponse>
<value>%s</value>
</candidateResponse>
</responseVariable>
</itemResult>
</assessmentResult>
');
$this->assertNotNull(Utils::findExternalNamespaces(sprintf($xml, Utils::valueAsString("160\u{0008}"))));
}

public function testremoveAllButFirstOccurrence(): void
{
$subject = 'abc 12 abc 345abc678abc';
Expand Down

0 comments on commit 6508bc9

Please sign in to comment.