Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.50% covered (danger)
15.50%
151 / 974
14.06% covered (danger)
14.06%
9 / 64
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserFactory
15.50% covered (danger)
15.50%
151 / 974
14.06% covered (danger)
14.06%
9 / 64
64046.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setItem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 load
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 saveXML
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBodyData
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 replaceNode
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 deleteNode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 queryXPath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 queryXPathChildren
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 loadContainerStatic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseContainerStatic
44.74% covered (danger)
44.74%
34 / 76
0.00% covered (danger)
0.00%
0 / 1
140.09
 getAncestors
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 parseContainerInteractive
55.56% covered (warning)
55.56%
15 / 27
0.00% covered (danger)
0.00%
0 / 1
16.11
 setContainerElements
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
 setContainerAttributes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseContainerItemBody
40.00% covered (danger)
40.00%
10 / 25
0.00% covered (danger)
0.00%
0 / 1
21.82
 parseContainerChoice
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 parseContainerGap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseContainerHottext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 extractAttributes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 findNamespace
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
9.58
 recursivelyFindNamespace
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
38.90
 getMathNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getXIncludeNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHTML5Namespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildItem
52.83% covered (warning)
52.83%
28 / 53
0.00% covered (danger)
0.00%
0 / 1
47.33
 loadNamespaces
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 loadSchemaLocations
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 buildApipAccessibility
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 buildInteraction
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
600
 buildChoice
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 buildResponseDeclaration
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
342
 buildOutcomeDeclaration
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 buildTemplateResponseProcessing
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
90
 buildResponseProcessing
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 buildCompositeResponseProcessing
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
380
 buildCustomResponseProcessing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildExpression
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getModalFeedback
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getOutcome
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getResponse
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildTemplatedrivenResponse
0.00% covered (danger)
0.00%
0 / 152
0.00% covered (danger)
0.00%
0 / 1
870
 buildIsNullFeedbackRule
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 buildSimpleFeedbackRule
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 createResponseIdentifier
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 buildObject
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 buildImg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildFigCaption
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildTooltip
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getNodeContentAsHtml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 buildTable
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildFigure
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildMath
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 buildXInclude
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNonEmptyChildren
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 buildStylesheet
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 buildRubricBlock
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildFeedback
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getPortableElementSubclasses
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getPortableElementClass
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 getPciClass
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getPicClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildCustomInteraction
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 buildInfoControl
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2013-2025 (original work) Open Assessment Technologies SA;
19 */
20
21namespace oat\taoQtiItem\model\qti;
22
23use oat\taoQtiItem\model\qti\choice\Choice;
24use oat\taoQtiItem\model\qti\Element;
25use oat\taoQtiItem\model\qti\container\Container;
26use oat\taoQtiItem\model\qti\exception\UnsupportedQtiElement;
27use oat\taoQtiItem\model\qti\exception\ParsingException;
28use oat\taoQtiItem\model\qti\container\ContainerInteractive;
29use oat\taoQtiItem\model\qti\container\ContainerItemBody;
30use oat\taoQtiItem\model\qti\container\ContainerGap;
31use oat\taoQtiItem\model\qti\container\ContainerHottext;
32use oat\taoQtiItem\model\qti\interaction\Interaction;
33use oat\taoQtiItem\model\qti\Item;
34use oat\taoQtiItem\model\qti\response\Custom;
35use oat\taoQtiItem\model\qti\interaction\BlockInteraction;
36use oat\taoQtiItem\model\qti\interaction\ObjectInteraction;
37use oat\taoQtiItem\model\qti\interaction\CustomInteraction;
38use oat\taoQtiItem\model\qti\interaction\PortableCustomInteraction;
39use oat\taoQtiItem\model\CustomInteractionRegistry;
40use oat\taoQtiItem\model\qti\InfoControl;
41use oat\taoQtiItem\model\qti\PortableInfoControl;
42use oat\taoQtiItem\model\InfoControlRegistry;
43use oat\taoQtiItem\model\qti\choice\ContainerChoice;
44use oat\taoQtiItem\model\qti\choice\TextVariableChoice;
45use oat\taoQtiItem\model\qti\choice\GapImg;
46use oat\taoQtiItem\model\qti\response\interactionResponseProcessing\None;
47use oat\taoQtiItem\model\qti\ResponseDeclaration;
48use oat\taoQtiItem\model\qti\OutcomeDeclaration;
49use oat\taoQtiItem\model\qti\response\Template;
50use oat\taoQtiItem\model\qti\exception\UnexpectedResponseProcessing;
51use oat\taoQtiItem\model\qti\response\TemplatesDriven;
52use oat\taoQtiItem\model\qti\response\TakeoverFailedException;
53use oat\taoQtiItem\model\qti\response\Summation;
54use oat\taoQtiItem\model\qti\expression\ExpressionParserFactory;
55use oat\taoQtiItem\model\qti\response\SimpleFeedbackRule;
56use oat\taoQtiItem\model\qti\QtiObject;
57use oat\taoQtiItem\model\qti\Img;
58use oat\taoQtiItem\model\qti\Math;
59use oat\taoQtiItem\model\qti\XInclude;
60use oat\taoQtiItem\model\qti\Stylesheet;
61use oat\taoQtiItem\model\qti\RubricBlock;
62use oat\taoQtiItem\model\qti\container\ContainerFeedbackInteractive;
63use oat\taoQtiItem\model\qti\container\ContainerStatic;
64use DOMDocument;
65use DOMXPath;
66use DOMElement;
67use common_Logger;
68use SimpleXMLElement;
69use oat\oatbox\service\ServiceManager;
70use oat\taoQtiItem\model\portableElement\model\PortableModelRegistry;
71use oat\oatbox\log\LoggerAwareTrait;
72
73/**
74 * The ParserFactory provides some methods to build the QTI_Data objects from an
75 * element.
76 * SimpleXML is used as source to build the model.
77 *
78 * @access public
79 * @author Joel Bout, <joel.bout@tudor.lu>
80 * @package taoQTI
81 */
82class ParserFactory
83{
84    use LoggerAwareTrait;
85
86    protected $data = null;
87    /** @var \oat\taoQtiItem\model\qti\Item */
88    protected $item = null;
89    protected $attributeMap = ['lang' => 'xml:lang'];
90
91    public function __construct(DOMDocument $data)
92    {
93        $this->data = $data;
94        $this->xpath = new DOMXPath($data);
95    }
96
97    /**
98     * @param \oat\taoQtiItem\model\qti\Item $item
99     */
100    public function setItem(Item $item)
101    {
102        $this->item = $item;
103    }
104
105    public function load()
106    {
107
108        $item = null;
109
110        if (!is_null($this->data)) {
111            $item = $this->buildItem($this->data->documentElement);
112        }
113
114        return $item;
115    }
116
117    protected function saveXML(DOMElement $data)
118    {
119        return $data->ownerDocument->saveXML($data);
120    }
121
122
123    /**
124     * Get the body data (markups) of an element.
125     * @param DOMElement $data the element
126     * @param boolean $removeNamespace if XML namespaces should be removed
127     * @param boolean $keepEmptyTags if true, the empty tags are kept expanded (useful when tags are HTML)
128     * @return string the body data (XML markup)
129     */
130    public function getBodyData(DOMElement $data, $removeNamespace = false, $keepEmptyTags = false)
131    {
132
133        //prepare the data string
134        $bodyData = '';
135        $saveOptions = $keepEmptyTags ? LIBXML_NOEMPTYTAG : 0;
136
137        $children  = $data->childNodes;
138
139        foreach ($children as $child) {
140            $bodyData .= $data->ownerDocument->saveXML($child, $saveOptions);
141        }
142
143        if ($removeNamespace) {
144            $bodyData = preg_replace('/<(\/)?(\w*):/i', '<$1', $bodyData);
145        }
146
147        return $bodyData;
148    }
149
150    protected function replaceNode(DOMElement $node, Element $element)
151    {
152        $placeholder = $this->data->createTextNode($element->getPlaceholder());
153        $node->parentNode->replaceChild($placeholder, $node);
154    }
155
156    protected function deleteNode(DOMElement $node)
157    {
158        $node->parentNode->removeChild($node);
159    }
160
161    public function queryXPath($query, DOMElement $contextNode = null)
162    {
163        if (is_null($contextNode)) {
164            return $this->xpath->query($query);
165        } else {
166            return $this->xpath->query($query, $contextNode);
167        }
168    }
169
170    public function queryXPathChildren($paths = [], DOMElement $contextNode = null, $ns = '')
171    {
172        $query = '.';
173        $ns = empty($ns) ? '' : $ns . ':';
174        foreach ($paths as $path) {
175            $query .= "/*[name(.)='" . $ns . $path . "']";
176        }
177
178        return $this->queryXPath($query, $contextNode);
179    }
180
181    public function loadContainerStatic(DOMElement $data, Container $container)
182    {
183        $this->parseContainerStatic($data, $container);
184    }
185
186    protected function parseContainerStatic(DOMElement $data, Container $container)
187    {
188        //initialize elements array to collect all QTI elements
189        $bodyElements = [];
190
191        //parse for feedback elements
192        //warning: parse feedback elements before any other because feedback may contain them!
193        $feedbackNodes = $this->queryXPath(
194            ".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'feedback')]",
195            $data
196        );
197
198        foreach ($feedbackNodes as $feedbackNode) {
199            $feedback = $this->buildFeedback($feedbackNode);
200            if (!is_null($feedback)) {
201                $bodyElements[$feedback->getSerial()] = $feedback;
202                $this->replaceNode($feedbackNode, $feedback);
203            }
204        }
205
206        // parse for QTI elements within item body
207
208        // parse the remaining tables, those that does not contain any interaction.
209        //warning: parse table elements before any other because table may contain them!
210        $tableNodes = $this->queryXPath(".//*[not(ancestor::*[name()='table']) and name()='table']", $data);
211        foreach ($tableNodes as $tableNode) {
212            $table = $this->buildTable($tableNode);
213            if (!is_null($table)) {
214                $bodyElements[$table->getSerial()] = $table;
215                $this->replaceNode($tableNode, $table);
216            }
217        }
218
219        $figureNodes = $this->queryXPath(".//*[name(.)='" . $this->getHTML5Namespace() . "figure']", $data);
220        foreach ($figureNodes as $figureNode) {
221            $figure = $this->buildFigure($figureNode);
222            if (!is_null($figure)) {
223                $bodyElements[$figure->getSerial()] = $figure;
224                $this->replaceNode($figureNode, $figure);
225            }
226        }
227
228        $tooltipNodes = $this->queryXPath(".//*[@data-role='tooltip-target']", $data);
229        foreach ($tooltipNodes as $tooltipNode) {
230            $tooltip = $this->buildTooltip($tooltipNode, $data);
231            if (!is_null($tooltip)) {
232                $bodyElements[$tooltip->getSerial()] = $tooltip;
233
234                $this->replaceNode($tooltipNode, $tooltip);
235            }
236        }
237
238        $objectNodes = $this->queryXPath(".//*[name(.)='object']", $data);
239        foreach ($objectNodes as $objectNode) {
240            if (!in_array('object', $this->getAncestors($objectNode))) {
241                $object = $this->buildObject($objectNode);
242                if (!is_null($object)) {
243                    $bodyElements[$object->getSerial()] = $object;
244
245                    $this->replaceNode($objectNode, $object);
246                }
247            }
248        }
249
250        $imgNodes = $this->queryXPath(".//*[name(.)='img']", $data);
251        foreach ($imgNodes as $imgNode) {
252            $img = $this->buildImg($imgNode);
253            if (!is_null($img)) {
254                $bodyElements[$img->getSerial()] = $img;
255
256                $this->replaceNode($imgNode, $img);
257            }
258        }
259
260        $figCaptionNodes = $this->queryXPath(".//*[name(.)='" . $this->getHTML5Namespace() . "figcaption']", $data);
261        foreach ($figCaptionNodes as $figCaptionNode) {
262            $figCaption = $this->buildFigCaption($figCaptionNode);
263            if (!is_null($figCaption)) {
264                $bodyElements[$figCaption->getSerial()] = $figCaption;
265
266                $this->replaceNode($figCaptionNode, $figCaption);
267            }
268            //should be no more then one
269            break;
270        }
271
272        $ns = $this->getMathNamespace();
273        $ns = empty($ns) ? '' : $ns . ':';
274        $mathNodes = $this->queryXPath(".//*[name(.)='" . $ns . "math']", $data);
275        foreach ($mathNodes as $mathNode) {
276            $math = $this->buildMath($mathNode);
277            if (!is_null($math)) {
278                $bodyElements[$math->getSerial()] = $math;
279                $this->replaceNode($mathNode, $math);
280            }
281        }
282
283        $ns = $this->getXIncludeNamespace();
284        $ns = empty($ns) ? '' : $ns . ':';
285        $xincludeNodes = $this->queryXPath(".//*[name(.)='" . $ns . "include']", $data);
286        foreach ($xincludeNodes as $xincludeNode) {
287            $include = $this->buildXInclude($xincludeNode);
288            if (!is_null($include)) {
289                $bodyElements[$include->getSerial()] = $include;
290                $this->replaceNode($xincludeNode, $include);
291            }
292        }
293
294        $printedVariableNodes = $this->queryXPath(".//*[name(.)='printedVariable']", $data);
295        foreach ($printedVariableNodes as $printedVariableNode) {
296            throw new UnsupportedQtiElement($printedVariableNode);
297        }
298
299        $templateNodes = $this->queryXPath(".//*[name(.)='templateBlock'] | *[name(.)='templateInline']", $data);
300        foreach ($templateNodes as $templateNode) {
301            throw new UnsupportedQtiElement($templateNode);
302        }
303
304        //finally, add all body elements to the body
305        $bodyData = $this->getBodyData($data);
306        //there use to be $bodyData = ItemAuthoring::cleanHTML($bodyData); there
307
308        if (empty($bodyElements)) {
309            $container->edit($bodyData);
310        } elseif (!$container->setElements($bodyElements, $bodyData)) {
311            throw new ParsingException('Cannot set elements to the static container');
312        }
313
314        return $data;
315    }
316
317    protected function getAncestors(DOMElement $data, $topNode = 'itemBody')
318    {
319        $ancestors = [];
320        $parentNodeName = '';
321        $currentNode = $data;
322        $i = 0;
323        while (!is_null($currentNode->parentNode) && $parentNodeName != $topNode) {
324            if ($i > 100) {
325                throw new ParsingException('maximum recursion of 100 reached');
326            }
327
328            $parentNodeName = $currentNode->parentNode->nodeName;
329            $ancestors[] = $parentNodeName;
330            $currentNode = $currentNode->parentNode;
331            $i++;
332        }
333        return $ancestors;
334    }
335
336    protected function parseContainerInteractive(DOMElement $data, ContainerInteractive $container)
337    {
338
339        $bodyElements = [];
340
341        //parse the xml to find the interaction nodes
342        $interactionNodes = $this->queryXPath(
343            ".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'Interaction')]",
344            $data
345        );
346
347        foreach ($interactionNodes as $k => $interactionNode) {
348            if (strpos($interactionNode->nodeName, 'portableCustomInteraction') === false) {
349                //build an interaction instance
350                $interaction = $this->buildInteraction($interactionNode);
351                if (!is_null($interaction)) {
352                    $bodyElements[$interaction->getSerial()] = $interaction;
353                    $this->replaceNode($interactionNode, $interaction);
354                }
355            }
356        }
357
358        //parse for feedback elements interactive!
359        $feedbackNodes = $this->queryXPath(
360            ".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'feedback')]",
361            $data
362        );
363
364        foreach ($feedbackNodes as $feedbackNode) {
365            $feedback = $this->buildFeedback($feedbackNode, true);
366            if (!is_null($feedback)) {
367                $bodyElements[$feedback->getSerial()] = $feedback;
368                $this->replaceNode($feedbackNode, $feedback);
369            }
370        }
371
372        $bodyData = $this->getBodyData($data);
373        foreach ($bodyElements as $bodyElement) {
374            if (strpos($bodyData, $bodyElement->getPlaceholder()) === false) {
375                unset($bodyElements[$bodyElement->getSerial()]);
376            }
377        }
378        if (!$container->setElements($bodyElements, $bodyData)) {
379            throw new ParsingException('Cannot set elements to the interactive container');
380        }
381
382        return $this->parseContainerStatic($data, $container);
383    }
384
385    protected function setContainerElements(Container $container, DOMElement $data, $bodyElements = [])
386    {
387        $bodyData = $this->getBodyData($data);
388        foreach ($bodyElements as $bodyElement) {
389            if (strpos($bodyData, $bodyElement->getPlaceholder()) === false) {
390                unset($bodyElements[$bodyElement->getSerial()]);
391            }
392        }
393        if (!$container->setElements($bodyElements, $bodyData)) {
394            throw new ParsingException('Cannot set elements to the interactive container');
395        }
396    }
397
398    private function setContainerAttributes(Container $container, DOMElement $data): void
399    {
400        $languageAttribute = $data->getAttribute('dir');
401        if (!empty($languageAttribute)) {
402            $container->setAttribute('dir', $languageAttribute);
403        }
404    }
405
406    protected function parseContainerItemBody(DOMElement $data, ContainerItemBody $container)
407    {
408
409        $bodyElements = [];
410
411        //parse for rubricBlocks: rubricBlock only allowed in item body !
412        $rubricNodes = $this->queryXPath(".//*[name(.)='rubricBlock']", $data);
413        foreach ($rubricNodes as $rubricNode) {
414            $rubricBlock = $this->buildRubricBlock($rubricNode);
415            if (!is_null($rubricBlock)) {
416                $bodyElements[$rubricBlock->getSerial()] = $rubricBlock;
417                $this->replaceNode($rubricNode, $rubricBlock);
418            }
419        }
420
421        //parse for infoControls: infoControl only allowed in item body !
422        $infoControlNodes = $this->queryXPath(".//*[name(.)='infoControl']", $data);
423        foreach ($infoControlNodes as $infoControlNode) {
424            $infoControl = $this->buildInfoControl($infoControlNode);
425            if (!is_null($infoControl)) {
426                $bodyElements[$infoControl->getSerial()] = $infoControl;
427                $this->replaceNode($infoControlNode, $infoControl);
428            }
429        }
430
431        // parse for tables, but only the ones containing interactions
432        $tableNodes = $this->queryXPath(".//*[name(.)='table']", $data);
433        foreach ($tableNodes as $tableNode) {
434            $interactionsNodes = $this->queryXPath(".//*[contains(name(.), 'Interaction')]", $tableNode);
435            if ($interactionsNodes->length > 0) {
436                $table = $this->buildTable($tableNode);
437                if (!is_null($table)) {
438                    $bodyElements[$table->getSerial()] = $table;
439                    $this->replaceNode($tableNode, $table);
440                    $this->parseContainerInteractive($tableNode, $table->getBody());
441                }
442            }
443        }
444
445        $this->setContainerAttributes($container, $data);
446        $this->setContainerElements($container, $data, $bodyElements);
447
448        return $this->parseContainerInteractive($data, $container);
449    }
450
451    private function parseContainerChoice(DOMElement $data, Container $container, $tag)
452    {
453
454        $choices = [];
455        $gapNodes = $this->queryXPath(".//*[name(.)='" . $tag . "']", $data);
456        foreach ($gapNodes as $gapNode) {
457            $gap = $this->buildChoice($gapNode);
458            if (!is_null($gap)) {
459                $choices[$gap->getSerial()] = $gap;
460                $this->replaceNode($gapNode, $gap);
461            }
462        }
463        $bodyData = $this->getBodyData($data);
464        $container->setElements($choices, $bodyData);
465
466        $data = $this->parseContainerStatic($data, $container);
467
468        return $data;
469    }
470
471    protected function parseContainerGap(DOMElement $data, ContainerGap $container)
472    {
473        return $this->parseContainerChoice($data, $container, 'gap');
474    }
475
476    protected function parseContainerHottext(DOMElement $data, ContainerHottext $container)
477    {
478        return $this->parseContainerChoice($data, $container, 'hottext');
479    }
480
481    protected function extractAttributes(DOMElement $data)
482    {
483        $options = [];
484        foreach ($data->attributes as $attr) {
485            if ($attr->nodeName === 'xsi:schemaLocation') {
486                continue;
487            }
488
489            $options[$this->attributeMap[$attr->nodeName] ?? $attr->nodeName] = (string) $attr->nodeValue;
490        }
491        return $options;
492    }
493
494    public function findNamespace($nsFragment)
495    {
496        $returnValue = '';
497
498        if (is_null($this->item)) {
499            foreach ($this->queryXPath('namespace::*') as $node) {
500                $name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
501                $uri = $node->nodeValue;
502                if (strpos($uri, $nsFragment) > 0) {
503                    $returnValue = $name;
504                    break;
505                }
506            }
507        } else {
508            $namespaces = $this->item->getNamespaces();
509
510            foreach ($namespaces as $name => $uri) {
511                if (strpos($uri, $nsFragment) > 0) {
512                    $returnValue = $name;
513                    break;
514                }
515            }
516            if ($returnValue === '') {
517                $returnValue = $this->recursivelyFindNamespace($this->data, $nsFragment);
518            }
519        }
520        return $returnValue;
521    }
522
523    private function recursivelyFindNamespace($element, $nsFragment)
524    {
525        if (strpos($this->data->saveXML(), $nsFragment) === false) {
526            return '';
527        }
528
529        $returnValue = '';
530
531        foreach ($element->childNodes as $child) {
532            if ($child->nodeType === XML_ELEMENT_NODE) {
533                foreach ($this->queryXPath('namespace::*', $child) as $node) {
534                    $name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
535                    $uri = $node->nodeValue;
536                    if (strpos($uri, $nsFragment) > 0) {
537                        $returnValue = $name;
538                        break;
539                    }
540                }
541                $value = $this->recursivelyFindNamespace($child, $nsFragment);
542                if ($value !== '') {
543                    $returnValue = $value;
544                }
545            }
546        }
547
548        return $returnValue;
549    }
550
551    protected function getMathNamespace()
552    {
553        return $this->findNamespace('MathML');
554    }
555
556    protected function getXIncludeNamespace()
557    {
558        return $this->findNamespace('XInclude');
559    }
560
561    protected function getHTML5Namespace(): string
562    {
563        // qh5
564        $ns = $this->findNamespace('html5');
565
566        return empty($ns) ? '' : $ns . ':';
567    }
568
569    /**
570     * Build a QTI_Item from a DOMElement, the root tag of which is root assessmentItem
571     *
572     * @param DOMElement $data
573     * @return \oat\taoQtiItem\model\qti\Item
574     * @throws InvalidArgumentException
575     * @throws ParsingException
576     * @throws UnsupportedQtiElement
577     */
578    protected function buildItem(DOMElement $data)
579    {
580        //check on the root tag.
581        $itemId = (string) $data->getAttribute('identifier');
582
583        $this->logDebug('Started parsing of QTI item' . (isset($itemId) ? ' ' . $itemId : ''), ['TAOITEMS']);
584
585        //create the item instance
586        $this->item = new Item($this->extractAttributes($data));
587
588        //load xml ns and schema locations
589        $this->loadNamespaces();
590        $this->loadSchemaLocations($data);
591
592        //load stylesheets
593        $styleSheetNodes = $this->queryXPath("*[name(.) = 'stylesheet']", $data);
594        foreach ($styleSheetNodes as $styleSheetNode) {
595            $styleSheet = $this->buildStylesheet($styleSheetNode);
596            $this->item->addStylesheet($styleSheet);
597        }
598
599        //extract the responses
600        $responseNodes = $this->queryXPath("*[name(.) = 'responseDeclaration']", $data);
601        foreach ($responseNodes as $responseNode) {
602            $response = $this->buildResponseDeclaration($responseNode);
603            if (!is_null($response)) {
604                $this->item->addResponse($response);
605            }
606        }
607
608        //extract outcome variables
609        $outcomes = [];
610        $outComeNodes = $this->queryXPath("*[name(.) = 'outcomeDeclaration']", $data);
611        foreach ($outComeNodes as $outComeNode) {
612            $outcome = $this->buildOutcomeDeclaration($outComeNode);
613            if (!is_null($outcome)) {
614                $outcomes[] = $outcome;
615            }
616        }
617        if (count($outcomes) > 0) {
618            $this->item->setOutcomes($outcomes);
619        }
620
621        //extract modal feedbacks
622        $feedbackNodes = $this->queryXPath("*[name(.) = 'modalFeedback']", $data);
623        foreach ($feedbackNodes as $feedbackNode) {
624            $modalFeedback = $this->buildFeedback($feedbackNode);
625            if (!is_null($modalFeedback)) {
626                $this->item->addModalFeedback($modalFeedback);
627            }
628        }
629
630        //extract the item structure to separate the structural/style content to the item content
631        $itemBodies = $this->queryXPath("*[name(.) = 'itemBody']", $data); // array with 1 or zero bodies
632        if ($itemBodies === false) {
633            $errors = libxml_get_errors();
634            if (count($errors) > 0) {
635                $error = array_shift($errors);
636                $errormsg = $error->message;
637            } else {
638                $errormsg = "without errormessage";
639            }
640            throw new ParsingException(
641                'XML error(' . $errormsg . ') on itemBody read' . (isset($itemId) ? ' for item ' . $itemId : '')
642            );
643        } elseif ($itemBodies->length) {
644            $this->parseContainerItemBody($itemBodies->item(0), $this->item->getBody());
645            $this->item->addClass($itemBodies->item(0)->getAttribute('class'));
646        }
647
648
649        // warning: extract the response processing at the latest to make
650        // oat\taoQtiItem\model\qti\response\TemplatesDriven::takeOverFrom() work
651        $rpNodes = $this->queryXPath("*[name(.) = 'responseProcessing']", $data);
652        if ($rpNodes->length === 0) {
653            //no response processing node found: the template for an empty response processing is simply "NONE"
654            $rProcessing = new TemplatesDriven();
655            $rProcessing->setRelatedItem($this->item);
656            foreach ($this->item->getInteractions() as $interaction) {
657                $rProcessing->setTemplate($interaction->getResponse(), Template::NONE);
658            }
659            $this->item->setResponseProcessing($rProcessing);
660        } else {
661            //if there is a response processing node, try parsing it
662            $rpNode = $rpNodes->item(0);
663            $rProcessing = $this->buildResponseProcessing($rpNode, $this->item);
664            if (!is_null($rProcessing)) {
665                $this->item->setResponseProcessing($rProcessing);
666            }
667        }
668
669        $this->buildApipAccessibility($data);
670
671        return $this->item;
672    }
673
674    /**
675     * Load xml namespaces into the item model
676     */
677    protected function loadNamespaces()
678    {
679        $namespaces = [];
680        foreach ($this->queryXPath('namespace::*') as $node) {
681            $name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
682            if ($name !== 'xml') {//always removed the implicit xml namespace
683                $namespaces[$name] = $node->nodeValue;
684            }
685        }
686        ksort($namespaces);
687        foreach ($namespaces as $name => $uri) {
688            $this->item->addNamespace($name, $uri);
689        }
690    }
691
692    /**
693     * Load xml schema locations into the item model
694     *
695     * @param DOMElement $itemData
696     * @throws ParsingException
697     */
698    protected function loadSchemaLocations(DOMElement $itemData)
699    {
700        $schemaLoc = preg_replace(
701            '/\s+/',
702            ' ',
703            trim($itemData->getAttributeNS($itemData->lookupNamespaceURI('xsi'), 'schemaLocation'))
704        );
705        $schemaLocToken = explode(' ', $schemaLoc);
706        $schemaCount = count($schemaLocToken);
707        if ($schemaCount % 2) {
708            throw new ParsingException('invalid schema location');
709        }
710        for ($i = 0; $i < $schemaCount; $i = $i + 2) {
711            $this->item->addSchemaLocation($schemaLocToken[$i], $schemaLocToken[$i + 1]);
712        }
713    }
714
715    protected function buildApipAccessibility(DOMElement $data)
716    {
717        $ApipNodes = $this->queryXPath("*[name(.) = 'apipAccessibility']|*[name(.) = 'apip:apipAccessibility']", $data);
718        if ($ApipNodes->length > 0) {
719            common_Logger::i('is APIP item', ['QTI', 'TAOITEMS']);
720            $apipNode = $ApipNodes->item(0);
721            $apipXml = $apipNode->ownerDocument->saveXML($apipNode);
722            $this->item->setApipAccessibility($apipXml);
723        }
724    }
725
726    /**
727     * Build a QTI_Interaction from a DOMElement (the root tag of this is an 'interaction' node)
728     *
729     * @access public
730     * @author Joel Bout, <joel.bout@tudor.lu>
731     * @param DOMElement $data
732     * @return Interaction
733     * @throws ParsingException
734     * @throws UnsupportedQtiElement
735     * @throws interaction\InvalidArgumentException
736     * @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10247
737     */
738    protected function buildInteraction(DOMElement $data)
739    {
740
741        $returnValue = null;
742
743        if ($data->nodeName === 'customInteraction') {
744            $returnValue = $this->buildCustomInteraction($data);
745        } else {
746            //build one of the standard interaction
747
748            try {
749                $type = ucfirst($data->nodeName);
750
751                $interactionClass = '\\oat\\taoQtiItem\\model\\qti\\interaction\\' . $type;
752                if (!class_exists($interactionClass)) {
753                    throw new ParsingException('The interaction class cannot be found: ' . $interactionClass);
754                }
755
756                $myInteraction = new $interactionClass($this->extractAttributes($data), $this->item);
757
758                if ($myInteraction instanceof BlockInteraction) {
759                    //extract prompt:
760                    $promptNodes = $this->queryXPath("*[name(.) = 'prompt']", $data); //prompt
761
762                    foreach ($promptNodes as $promptNode) {
763                        //only block interactions have prompt
764                        $this->parseContainerStatic($promptNode, $myInteraction->getPrompt());
765                        $this->deleteNode($promptNode);
766                    }
767                }
768
769                //build the interaction's choices regarding it's type
770                switch (strtolower($type)) {
771                    case 'matchinteraction':
772                        //extract simpleMatchSet choices
773                        $matchSetNodes = $this->queryXPath("*[name(.) = 'simpleMatchSet']", $data); //simpleMatchSet
774                        $matchSetNumber = 0;
775                        foreach ($matchSetNodes as $matchSetNode) {
776                            //simpleAssociableChoice
777                            $choiceNodes = $this->queryXPath(
778                                "*[name(.) = 'simpleAssociableChoice']",
779                                $matchSetNode
780                            );
781
782                            foreach ($choiceNodes as $choiceNode) {
783                                $choice = $this->buildChoice($choiceNode);
784                                if (!is_null($choice)) {
785                                    $myInteraction->addChoice($choice, $matchSetNumber);
786                                }
787                            }
788                            if (++$matchSetNumber === 2) {
789                                //matchSet is limited to 2 maximum
790                                break;
791                            }
792                        }
793                        break;
794
795                    case 'gapmatchinteraction':
796                        //create choices with the gapText nodes
797                        $choiceNodes = $this->queryXPath("*[name(.)='gapText']", $data); //or gapImg!!
798                        $choices = [];
799                        foreach ($choiceNodes as $choiceNode) {
800                            $choice = $this->buildChoice($choiceNode);
801                            if (!is_null($choice)) {
802                                $myInteraction->addChoice($choice);
803                                $this->deleteNode($choiceNode);
804                            }
805
806                            //remove node so it does not pollute subsequent parsing data
807                            unset($choiceNode);
808                        }
809
810                        $this->parseContainerGap($data, $myInteraction->getBody());
811                        break;
812
813                    case 'hottextinteraction':
814                        $this->parseContainerHottext($data, $myInteraction->getBody());
815                        break;
816
817                    case 'graphicgapmatchinteraction':
818                        //create choices with the gapImg nodes
819                        $choiceNodes = $this->queryXPath("*[name(.)='gapImg']", $data);
820                        $choices = [];
821                        foreach ($choiceNodes as $choiceNode) {
822                            $choice = $this->buildChoice($choiceNode);
823                            if (!is_null($choice)) {
824                                $myInteraction->addGapImg($choice);
825                            }
826                        }
827                        // no break
828                    default:
829                        //parse, extract and build the choice nodes contained in the interaction
830                        $exp = "*[contains(name(.),'Choice')] | *[name(.)='associableHotspot']";
831                        $choiceNodes = $this->queryXPath($exp, $data);
832                        foreach ($choiceNodes as $choiceNode) {
833                            $choice = $this->buildChoice($choiceNode);
834                            if (!is_null($choice)) {
835                                $myInteraction->addChoice($choice);
836                            }
837                            unset($choiceNode);
838                        }
839                        break;
840                }
841
842                if ($myInteraction instanceof ObjectInteraction) {
843                    $objectNodes = $this->queryXPath("*[name(.)='object']", $data); //object
844                    foreach ($objectNodes as $objectNode) {
845                        $object = $this->buildObject($objectNode);
846                        if (!is_null($object)) {
847                            $myInteraction->setObject($object);
848                        }
849                    }
850                }
851
852                $returnValue = $myInteraction;
853            } catch (InvalidArgumentException $iae) {
854                throw new ParsingException($iae);
855            }
856        }
857
858        return $returnValue;
859    }
860
861    /**
862     * Build a QTI_Choice from a DOMElement (the root tag of this element
863     * an 'choice' node)
864     *
865     * @access public
866     * @author Joel Bout, <joel.bout@tudor.lu>
867     * @param  DOMElement $data
868     * @return Choice
869     * @throws ParsingException
870     * @throws UnsupportedQtiElement
871     * @throws choice\InvalidArgumentException
872     * @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10254
873     */
874    protected function buildChoice(DOMElement $data)
875    {
876
877        $className = '\\oat\\taoQtiItem\\model\\qti\\choice\\' . ucfirst($data->nodeName);
878        if (!class_exists($className)) {
879            throw new ParsingException("The choice class does not exist " . $className);
880        }
881
882        $myChoice = new $className($this->extractAttributes($data));
883
884        if ($myChoice instanceof ContainerChoice) {
885            $this->parseContainerStatic($data, $myChoice->getBody());
886        } elseif ($myChoice instanceof TextVariableChoice) {
887            //use getBodyData() instead of $data->nodeValue() to preserve xml entities
888            $myChoice->setContent($this->getBodyData($data));
889        } elseif ($myChoice instanceof GapImg) {
890            //extract the media object tag
891            $objectNodes = $this->queryXPath("*[name(.)='object']", $data);
892            foreach ($objectNodes as $objectNode) {
893                $object = $this->buildObject($objectNode);
894                $myChoice->setContent($object);
895                break;
896            }
897        }
898
899        return $myChoice;
900    }
901
902    /**
903     * Short description of method buildResponseDeclaration
904     *
905     * @access public
906     * @author Joel Bout, <joel.bout@tudor.lu>
907     * @param  DOMElement $data
908     * @return \oat\taoQtiItem\model\qti\ResponseDeclaration
909     * @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10074
910     */
911    protected function buildResponseDeclaration(DOMElement $data)
912    {
913
914        $myResponse = new ResponseDeclaration($this->extractAttributes($data), $this->item);
915
916        $data = simplexml_import_dom($data);
917        //set the correct responses
918        $correctResponseNodes = $data->xpath("*[name(.) = 'correctResponse']");
919        $responses = [];
920        foreach ($correctResponseNodes as $correctResponseNode) {
921            foreach ($correctResponseNode->value as $value) {
922                $correct = (string) $value;
923                $response = new Value();
924                foreach ($value->attributes() as $attrName => $attrValue) {
925                    $response->setAttribute($attrName, strval($attrValue));
926                }
927                $response->setValue($correct);
928                $responses[] = $response;
929            }
930            break;
931        }
932        $myResponse->setCorrectResponses($responses);
933
934        //set the correct responses
935        $defaultValueNodes = $data->xpath("*[name(.) = 'defaultValue']");
936        $defaultValues = [];
937        foreach ($defaultValueNodes as $defaultValueNode) {
938            foreach ($defaultValueNode->value as $value) {
939                $default = (string) $value;
940                $defaultValue = new Value();
941                foreach ($value->attributes() as $attrName => $attrValue) {
942                    $defaultValue->setAttribute($attrName, strval($attrValue));
943                }
944                $defaultValue->setValue($default);
945                $defaultValues[] = $defaultValue;
946            }
947            break;
948        }
949        $myResponse->setDefaultValue($defaultValues);
950
951        //set the mapping if defined
952        $mappingNodes = $data->xpath("*[name(.) = 'mapping']");
953        foreach ($mappingNodes as $mappingNode) {
954            if (isset($mappingNode['defaultValue'])) {
955                $myResponse->setMappingDefaultValue(floatval((string) $mappingNode['defaultValue']));
956            }
957            $mappingOptions = [];
958            foreach ($mappingNode->attributes() as $key => $value) {
959                if ($key != 'defaultValue') {
960                    $mappingOptions[$key] = (string) $value;
961                }
962            }
963            $myResponse->setAttribute('mapping', $mappingOptions);
964
965            $mapping = [];
966            foreach ($mappingNode->mapEntry as $mapEntry) {
967                $mapping[(string) htmlspecialchars($mapEntry['mapKey'])] = (string) $mapEntry['mappedValue'];
968            }
969            $myResponse->setMapping($mapping);
970
971            break;
972        }
973
974        //set the areaMapping if defined
975        $mappingNodes = $data->xpath("*[name(.) = 'areaMapping']");
976        foreach ($mappingNodes as $mappingNode) {
977            if (isset($mappingNode['defaultValue'])) {
978                $myResponse->setMappingDefaultValue(floatval((string) $mappingNode['defaultValue']));
979            }
980            $mappingOptions = [];
981            foreach ($mappingNode->attributes() as $key => $value) {
982                if ($key != 'defaultValue') {
983                    $mappingOptions[$key] = (string) $value;
984                }
985            }
986            $myResponse->setAttribute('areaMapping', $mappingOptions);
987
988            $mapping = [];
989            foreach ($mappingNode->areaMapEntry as $mapEntry) {
990                $mappingAttributes = [];
991                foreach ($mapEntry->attributes() as $key => $value) {
992                    $mappingAttributes[(string) $key] = (string) $value;
993                }
994                $mapping[] = $mappingAttributes;
995            }
996            $myResponse->setMapping($mapping, 'area');
997
998            break;
999        }
1000
1001        return $myResponse;
1002    }
1003
1004    /**
1005     * Short description of method buildOutcomeDeclaration
1006     *
1007     * @access public
1008     * @author Joel Bout, <joel.bout@tudor.lu>
1009     * @param  DOMElement data
1010     * @return oat\taoQtiItem\model\qti\OutcomeDeclaration
1011     */
1012    protected function buildOutcomeDeclaration(DOMElement $data)
1013    {
1014
1015        $outcome = new OutcomeDeclaration($this->extractAttributes($data));
1016        $data = simplexml_import_dom($data);
1017
1018        if (isset($data->defaultValue)) {
1019            if (!is_null($data->defaultValue->value)) {
1020                $outcome->setDefaultValue((string) $data->defaultValue->value);
1021            }
1022        }
1023
1024        return $outcome;
1025    }
1026
1027    /**
1028     * Short description of method buildTemplateResponseProcessing
1029     *
1030     * @access public
1031     * @author Joel Bout, <joel.bout@tudor.lu>
1032     * @param  DOMElement data
1033     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1034     */
1035    protected function buildTemplateResponseProcessing(DOMElement $data)
1036    {
1037        $returnValue = null;
1038
1039        if ($data->hasAttribute('template') && $data->childNodes->length === 0) {
1040            $templateUri = (string) $data->getAttribute('template');
1041            $returnValue = new Template($templateUri);
1042        } elseif ($data->childNodes->length === 1) {
1043            //check response declaration identifier, which must be RESPONSE in standard rp
1044            $responses = $this->item->getResponses();
1045            if (count($responses) == 1) {
1046                $response = reset($responses);
1047                if ($response->getIdentifier() !== 'RESPONSE') {
1048                    throw new UnexpectedResponseProcessing('the response declaration identifier must be RESPONSE');
1049                }
1050            } else {
1051                //invalid number of response declaration
1052                throw new UnexpectedResponseProcessing('the item must have exactly one response declaration');
1053            }
1054
1055            $patternCorrectIMS = 'responseCondition [count(./*) = 2 ] [name(./*[1]) = "responseIf" ] '
1056                . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] '
1057                . '[name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] '
1058                . '[name(./responseIf/*[2]) = "setOutcomeValue" ] '
1059                . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] [name(./*[2]) = "responseElse" ] '
1060                . '[count(./responseElse/*) = 1 ] [name(./responseElse/*[1]) = "setOutcomeValue" ] '
1061                . '[name(./responseElse/setOutcomeValue/*[1]) = "baseValue"]';
1062            $patternMappingIMS = 'responseCondition [count(./*) = 2] [name(./*[1]) = "responseIf"] '
1063                . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "isNull"] '
1064                . '[name(./responseIf/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1065                . '[name(./responseIf/setOutcomeValue/*[1]) = "variable"] [name(./*[2]) = "responseElse"] '
1066                . '[count(./responseElse/*) = 1] [name(./responseElse/*[1]) = "setOutcomeValue"] '
1067                . '[name(./responseElse/setOutcomeValue/*[1]) = "mapResponse"]';
1068            $patternMappingPointIMS = 'responseCondition [count(./*) = 2] [name(./*[1]) = "responseIf"] '
1069                . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "isNull"] '
1070                . '[name(./responseIf/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1071                . '[name(./responseIf/setOutcomeValue/*[1]) = "variable"] [name(./*[2]) = "responseElse"] '
1072                . '[count(./responseElse/*) = 1] [name(./responseElse/*[1]) = "setOutcomeValue"] '
1073                . '[name(./responseElse/setOutcomeValue/*[1]) = "mapResponsePoint"]';
1074            if (count($this->queryXPath($patternCorrectIMS)) == 1) {
1075                $returnValue = new Template(Template::MATCH_CORRECT);
1076            } elseif (count($this->queryXPath($patternMappingIMS)) == 1) {
1077                $returnValue = new Template(Template::MAP_RESPONSE);
1078            } elseif (count($this->queryXPath($patternMappingPointIMS)) == 1) {
1079                $returnValue = new Template(Template::MAP_RESPONSE_POINT);
1080            } else {
1081                throw new UnexpectedResponseProcessing('not Template, wrong rule');
1082            }
1083            $returnValue->setRelatedItem($this->item);
1084        } else {
1085            throw new UnexpectedResponseProcessing('not Template');
1086        }
1087
1088        return $returnValue;
1089    }
1090
1091    /**
1092     * Short description of method buildResponseProcessing
1093     *
1094     * @access public
1095     * @author Joel Bout, <joel.bout@tudor.lu>
1096     * @param  DOMElement data
1097     * @param  Item item
1098     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1099     */
1100    protected function buildResponseProcessing(DOMElement $data, Item $item)
1101    {
1102        $returnValue = null;
1103
1104        // try template
1105        try {
1106            $returnValue = $this->buildTemplateResponseProcessing($data);
1107
1108            try {
1109                //warning: require to add interactions to the item to make it work
1110                $returnValue = TemplatesDriven::takeOverFrom($returnValue, $item);
1111            } catch (TakeoverFailedException $e) {
1112            }
1113        } catch (UnexpectedResponseProcessing $e) {
1114        }
1115
1116        //try templatedriven
1117        if (is_null($returnValue)) {
1118            try {
1119                $returnValue = $this->buildTemplatedrivenResponse($data, $item->getInteractions());
1120            } catch (UnexpectedResponseProcessing $e) {
1121            }
1122        }
1123
1124        // build custom
1125        if (is_null($returnValue)) {
1126            try {
1127                $returnValue = $this->buildCustomResponseProcessing($data);
1128            } catch (UnexpectedResponseProcessing $e) {
1129                // not a Template
1130                common_Logger::e('custom response processing failed', ['TAOITEMS', 'QTI']);
1131            }
1132        }
1133
1134        if (is_null($returnValue)) {
1135            common_Logger::w('failed to determine ResponseProcessing');
1136        }
1137
1138        return $returnValue;
1139    }
1140
1141    /**
1142     * Short description of method buildCompositeResponseProcessing
1143     *
1144     * @access public
1145     * @author Joel Bout, <joel.bout@tudor.lu>
1146     * @param  DOMElement data
1147     * @param  Item item
1148     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1149     */
1150    protected function buildCompositeResponseProcessing(DOMElement $data, Item $item)
1151    {
1152        $returnValue = null;
1153
1154        // STRONGLY simplified summation detection
1155        $patternCorrectTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1156            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] '
1157            . '[name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] '
1158            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] [count(./responseIf/setOutcomeValue/*) = 1 ] '
1159            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
1160        $patternMapTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1161            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not/*) = 1 ] '
1162            . '[name(./responseIf/not/*[1]) = "isNull" ] [count(./responseIf/not/isNull/*) = 1 ] '
1163            . '[name(./responseIf/not/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1164            . '[count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "mapResponse"]';
1165        $patternMapPointTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1166            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not/*) = 1 ] '
1167            . '[name(./responseIf/not/*[1]) = "isNull" ] [count(./responseIf/not/isNull/*) = 1 ] '
1168            . '[name(./responseIf/not/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1169            . '[count(./responseIf/setOutcomeValue/*) = 1 ] '
1170            . '[name(./responseIf/setOutcomeValue/*[1]) = "mapResponsePoint"]';
1171        $patternNoneTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1172            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "isNull" ] [count(./responseIf/isNull/*) = 1 ] '
1173            . '[name(./responseIf/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1174            . '[count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
1175        $possibleSummation = '/setOutcomeValue [count(./*) = 1 ] [name(./*[1]) = "sum" ]';
1176
1177        $irps = [];
1178        $composition = null;
1179        $data = simplexml_import_dom($data);
1180        foreach ($data as $responseRule) {
1181            if (!is_null($composition)) {
1182                throw new UnexpectedResponseProcessing('Not composite, rules after composition');
1183            }
1184
1185            $subtree = new SimpleXMLElement($responseRule->asXML());
1186
1187            if (count($subtree->xpath($patternCorrectTAO)) > 0) {
1188                $responseIdentifier = (string) $subtree->responseIf->match->variable[0]['identifier'];
1189                $irps[$responseIdentifier] = [
1190                    'class' => 'MatchCorrectTemplate',
1191                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
1192                ];
1193            } elseif (count($subtree->xpath($patternMapTAO)) > 0) {
1194                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable[0]['identifier'];
1195                $irps[$responseIdentifier] = [
1196                    'class' => 'MapResponseTemplate',
1197                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
1198                ];
1199            } elseif (count($subtree->xpath($patternMapPointTAO)) > 0) {
1200                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable[0]['identifier'];
1201                $irps[$responseIdentifier] = [
1202                    'class' => 'MapResponsePointTemplate',
1203                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
1204                ];
1205            } elseif (count($subtree->xpath($patternNoneTAO)) > 0) {
1206                $responseIdentifier = (string) $subtree->responseIf->isNull->variable[0]['identifier'];
1207                $irps[$responseIdentifier] = [
1208                    'class' => 'None',
1209                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier'],
1210                    'default' => (string) $subtree->responseIf->setOutcomeValue[0]->baseValue[0]
1211                ];
1212            } elseif (count($subtree->xpath($possibleSummation)) > 0) {
1213                $composition = 'Summation';
1214                $outcomesUsed = [];
1215                foreach ($subtree->xpath('/setOutcomeValue/sum/variable') as $var) {
1216                    $outcomesUsed[] = (string) $var[0]['identifier'];
1217                }
1218            } else {
1219                throw new UnexpectedResponseProcessing('Not composite, unknown rule');
1220            }
1221        }
1222
1223        if (is_null($composition)) {
1224            throw new UnexpectedResponseProcessing('Not composit, Composition rule missing');
1225        }
1226
1227        $responses = [];
1228        foreach ($item->getInteractions() as $interaction) {
1229            $responses[$interaction->getResponse()->getIdentifier()] = $interaction->getResponse();
1230        }
1231
1232        if (count(array_diff(array_keys($irps), array_keys($responses))) > 0) {
1233            throw new UnexpectedResponseProcessing(
1234                'Not composite, no responses for rules: '
1235                    . implode(',', array_diff(array_keys($irps), array_keys($responses)))
1236            );
1237        }
1238        if (count(array_diff(array_keys($responses), array_keys($irps))) > 0) {
1239            throw new UnexpectedResponseProcessing('Not composite, no support for unmatched variables yet');
1240        }
1241
1242        //assuming sum is correct
1243
1244        $compositonRP = new Summation($item);
1245        foreach ($responses as $id => $response) {
1246            $outcome = null;
1247            foreach ($item->getOutcomes() as $possibleOutcome) {
1248                if ($possibleOutcome->getIdentifier() == $irps[$id]['outcome']) {
1249                    $outcome = $possibleOutcome;
1250                    break;
1251                }
1252            }
1253            if (is_null($outcome)) {
1254                throw new ParsingException('Undeclared Outcome in ResponseProcessing');
1255            }
1256            $classname = '\\oat\\taoQtiItem\\model\\qti\\response\\interactionResponseProcessing\\'
1257                . $irps[$id]['class'];
1258            $irp = new $classname($response, $outcome);
1259
1260            if ($irp instanceof None && isset($irps[$id]['default'])) {
1261                $irp->setDefaultValue($irps[$id]['default']);
1262            }
1263            $compositonRP->add($irp);
1264        }
1265        $returnValue = $compositonRP;
1266
1267        return $returnValue;
1268    }
1269
1270    /**
1271     * Short description of method buildCustomResponseProcessing
1272     *
1273     * @access public
1274     * @author Joel Bout, <joel.bout@tudor.lu>
1275     * @param  DOMElement data
1276     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1277     */
1278    protected function buildCustomResponseProcessing(DOMElement $data)
1279    {
1280
1281        // Parse to find the different response rules
1282        $responseRules = [];
1283
1284        $data = simplexml_import_dom($data);
1285
1286        $returnValue = new Custom($responseRules, $data->asXml());
1287
1288        return $returnValue;
1289    }
1290
1291    /**
1292     * Short description of method buildExpression
1293     *
1294     * @access public
1295     * @author Joel Bout, <joel.bout@tudor.lu>
1296     * @param  DOMElement data
1297     * @return oat\taoQtiItem\model\qti\response\Rule
1298     */
1299    protected function buildExpression(DOMElement $data)
1300    {
1301        $data = simplexml_import_dom($data);
1302        return ExpressionParserFactory::build($data);
1303    }
1304
1305    protected function getModalFeedback($identifier)
1306    {
1307        foreach ($this->item->getModalFeedbacks() as $feedback) {
1308            if ($feedback->getIdentifier() == $identifier) {
1309                return $feedback;
1310            }
1311        }
1312        throw new ParsingException('cannot found the modal feedback with identifier ' . $identifier);
1313    }
1314
1315    protected function getOutcome($identifier)
1316    {
1317        foreach ($this->item->getOutcomes() as $outcome) {
1318            if ($outcome->getIdentifier() == $identifier) {
1319                return $outcome;
1320            }
1321        }
1322        throw new ParsingException('cannot found the outcome with identifier ' . $identifier);
1323    }
1324
1325    protected function getResponse($identifier)
1326    {
1327        foreach ($this->item->getResponses() as $response) {
1328            if ($response->getIdentifier() == $identifier) {
1329                return $response;
1330            }
1331        }
1332        throw new ParsingException('cannot found the response with identifier ' . $identifier);
1333    }
1334
1335    /**
1336     * Short description of method buildTemplatedrivenResponse
1337     *
1338     * @access public
1339     * @author Joel Bout, <joel.bout@tudor.lu>
1340     * @param DOMElement $data
1341     * @param $interactions
1342     * @return TemplatesDriven
1343     * @throws UnexpectedResponseProcessing
1344     * @throws exception\QtiModelException
1345     * @throws response\InvalidArgumentException
1346     */
1347    protected function buildTemplatedrivenResponse(DOMElement $data, $interactions)
1348    {
1349
1350        $patternCorrectTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1351            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] '
1352            . '[name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] '
1353            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "sum" ] '
1354            . '[name(./responseIf/setOutcomeValue/sum/*[1]) = "variable" ] '
1355            . '[name(./responseIf/setOutcomeValue/sum/*[2]) = "baseValue"]';
1356        $patternMappingTAO = '/responseCondition [count(./*) = 1] [name(./*[1]) = "responseIf"] '
1357            . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "not"] [name(./responseIf/not/*[1]) = "isNull"] '
1358            . '[name(./responseIf/not/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1359            . '[name(./responseIf/setOutcomeValue/*[1]) = "sum"] '
1360            . '[name(./responseIf/setOutcomeValue/sum/*[1]) = "variable"] '
1361            . '[name(./responseIf/setOutcomeValue/sum/*[2]) = "mapResponse"]';
1362        $patternMappingPointTAO = '/responseCondition [count(./*) = 1] [name(./*[1]) = "responseIf"] '
1363            . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "not"] [name(./responseIf/not/*[1]) = "isNull"] '
1364            . '[name(./responseIf/not/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1365            . '[name(./responseIf/setOutcomeValue/*[1]) = "sum"] '
1366            . '[name(./responseIf/setOutcomeValue/sum/*[1]) = "variable"] '
1367            . '[name(./responseIf/setOutcomeValue/sum/*[2]) = "mapResponsePoint"]';
1368
1369        $subPatternFeedbackOperatorIf = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1370            . '[contains(name(./responseIf/*[1]/*[1]), "map")] [name(./responseIf/*[1]/*[2]) = "baseValue" ] '
1371            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
1372        $subPatternFeedbackElse = '[name(./*[2]) = "responseElse"] [count(./responseElse/*) = 1 ] '
1373            . '[name(./responseElse/*[1]) = "setOutcomeValue"] '
1374            . '[name(./responseElse/setOutcomeValue/*[1]) = "baseValue"]';
1375        $subPatternFeedbackCorrect = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1376            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[1]) = "variable" ] '
1377            . '[name(./responseIf/*[1]/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1378            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
1379        $subPatternFeedbackIncorrect = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1380            . '[name(./responseIf/*[1]) = "not" ] [count(./responseIf/not) = 1 ] '
1381            . '[name(./responseIf/not/*[1]) = "match" ] [name(./responseIf/not/*[1]/*[1]) = "variable" ] '
1382            . '[name(./responseIf/not/*[1]/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1383            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
1384        $subPatternFeedbackIsNull = '[name(./*[1]) = "responseIf"]'
1385              . '[count(./responseIf/*) = 2] '
1386              . '[name(./responseIf/*[1]) = "isNull"]'
1387              . '[name(./responseIf/isNull/*[1]) = "variable"]'
1388              . '[name(./responseIf/*[2]) = "setOutcomeValue"]'
1389              . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
1390        $subPatternFeedbackMatchChoices = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1391            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "multiple" ] '
1392            . '[name(./responseIf/*[1]/*[2]/*) = "baseValue" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1393            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
1394        $subPatternFeedbackMatchChoicesEmpty = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1395            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "multiple" ] '
1396            . '[count(./responseIf/*[1]/*[2]/*) = 0 ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1397            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
1398        $subPatternFeedbackMatchChoice = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1399            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "baseValue" ] '
1400            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] '
1401            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
1402        $patternFeedbackOperator = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackOperatorIf;
1403        $patternFeedbackOperatorWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackOperatorIf
1404            . $subPatternFeedbackElse;
1405        $patternFeedbackCorrect = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackCorrect;
1406        $patternFeedbackCorrectWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackCorrect
1407            . $subPatternFeedbackElse;
1408        $patternFeedbackIncorrect = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackIncorrect;
1409        $patternFeedbackIsNull = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackIsNull;
1410        $patternFeedbackIncorrectWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackIncorrect
1411            . $subPatternFeedbackElse;
1412        $patternFeedbackMatchChoices = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoices;
1413        $patternFeedbackMatchChoicesWithElse  = '/responseCondition [count(./*) = 2 ]'
1414            . $subPatternFeedbackMatchChoices . $subPatternFeedbackElse;
1415        $patternFeedbackMatchChoice = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoice;
1416        $patternFeedbackMatchChoicesEmpty = '/responseCondition [count(./*) = 1 ]'
1417            . $subPatternFeedbackMatchChoicesEmpty;
1418        $patternFeedbackMatchChoicesEmptyWithElse  = '/responseCondition [count(./*) = 2 ]'
1419            . $subPatternFeedbackMatchChoicesEmpty . $subPatternFeedbackElse;
1420        $patternFeedbackMatchChoice = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoice;
1421        $patternFeedbackMatchChoiceWithElse  = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackMatchChoice
1422            . $subPatternFeedbackElse;
1423
1424        $rules = [];
1425        $simpleFeedbackRules = [];
1426        $data = simplexml_import_dom($data);
1427
1428        foreach ($data as $responseRule) {
1429            $feedbackRule = null;
1430            $subtree = new SimpleXMLElement($responseRule->asXML());
1431
1432            if (count($subtree->xpath($patternCorrectTAO)) > 0) {
1433                $responseIdentifier = (string) $subtree->responseIf->match->variable['identifier'];
1434                $rules[$responseIdentifier] = Template::MATCH_CORRECT;
1435            } elseif (count($subtree->xpath($patternMappingTAO)) > 0) {
1436                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable['identifier'];
1437                $rules[$responseIdentifier] = Template::MAP_RESPONSE;
1438            } elseif (count($subtree->xpath($patternMappingPointTAO)) > 0) {
1439                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable['identifier'];
1440                $rules[$responseIdentifier] = Template::MAP_RESPONSE_POINT;
1441            } elseif (
1442                count($subtree->xpath($patternFeedbackCorrect)) > 0
1443                || count($subtree->xpath($patternFeedbackCorrectWithElse)) > 0
1444            ) {
1445                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'correct');
1446            } elseif (
1447                count($subtree->xpath($patternFeedbackIncorrect)) > 0
1448                || count($subtree->xpath($patternFeedbackIncorrectWithElse)) > 0
1449            ) {
1450                $responseIdentifier = (string) $subtree->responseIf->not->match->variable['identifier'];
1451                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'incorrect', null, $responseIdentifier);
1452            } elseif (
1453                count($subtree->xpath($patternFeedbackIsNull)) > 0
1454            ) {
1455                $responseIdentifier = (string) $subtree->responseIf->isNull->variable['identifier'];
1456                $feedbackRule = $this->buildIsNullFeedbackRule($subtree, null, $responseIdentifier);
1457            } elseif (
1458                count($subtree->xpath($patternFeedbackOperator)) > 0
1459                || count($subtree->xpath($patternFeedbackOperatorWithElse)) > 0
1460            ) {
1461                $operator = '';
1462                $responseIdentifier = '';
1463                $value = '';
1464                foreach ($subtree->responseIf->children() as $child) {
1465                    $operator = $child->getName();
1466                    $map = null;
1467                    foreach ($child->children() as $granChild) {
1468                        $map = $granChild->getName();
1469                        $responseIdentifier = (string) $granChild['identifier'];
1470                        break;
1471                    }
1472                    $value = (string) $child->baseValue;
1473                    break;
1474                }
1475                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, $operator, $value);
1476            } elseif (
1477                count($subtree->xpath($patternFeedbackMatchChoices)) > 0
1478                || count($subtree->xpath($patternFeedbackMatchChoicesWithElse)) > 0
1479                || count($subtree->xpath($patternFeedbackMatchChoicesEmpty)) > 0
1480                || count($subtree->xpath($patternFeedbackMatchChoicesEmptyWithElse)) > 0
1481            ) {
1482                $choices = [];
1483                foreach ($subtree->responseIf->match->multiple->baseValue as $choice) {
1484                    $choices[] = (string)$choice;
1485                }
1486                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'choices', $choices);
1487            } elseif (
1488                count($subtree->xpath($patternFeedbackMatchChoice)) > 0
1489                || count($subtree->xpath($patternFeedbackMatchChoiceWithElse)) > 0
1490            ) {
1491                $choices = [(string)$subtree->responseIf->match->baseValue];
1492                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'choices', $choices);
1493            } else {
1494                throw new UnexpectedResponseProcessing('Not template driven, unknown rule');
1495            }
1496
1497            if (!is_null($feedbackRule)) {
1498                $responseIdentifier = $feedbackRule->comparedOutcome()->getIdentifier();
1499                if (!isset($simpleFeedbackRules[$responseIdentifier])) {
1500                    $simpleFeedbackRules[$responseIdentifier] = [];
1501                }
1502                $simpleFeedbackRules[$responseIdentifier][] = $feedbackRule;
1503            }
1504        }
1505
1506        $responseIdentifiers = [];
1507        foreach ($interactions as $interaction) {
1508            $interactionResponse = $interaction->getResponse();
1509            $responseIdentifier = $interactionResponse->getIdentifier();
1510            $responseIdentifiers[] = $responseIdentifier;
1511
1512            //create and set simple feedback rule here
1513            if (isset($simpleFeedbackRules[$responseIdentifier])) {
1514                foreach ($simpleFeedbackRules[$responseIdentifier] as $rule) {
1515                    $interactionResponse->addFeedbackRule($rule);
1516                }
1517            }
1518        }
1519
1520        //all rules must have been previously identified as belonging to one interaction
1521        if (count(array_diff(array_keys($rules), $responseIdentifiers)) > 0) {
1522            throw new UnexpectedResponseProcessing(
1523                'Not template driven, responseIdentifiers are ' . implode(',', $responseIdentifiers)
1524                    . ' while rules are ' . implode(',', array_keys($rules))
1525            );
1526        }
1527
1528        $templatesDrivenRP = new TemplatesDriven();
1529        foreach ($interactions as $interaction) {
1530            //if a rule has been found for an interaction, apply it. Default to the template NONE otherwise
1531            $pattern = isset($rules[$interaction->getResponse()->getIdentifier()])
1532                ? $rules[$interaction->getResponse()->getIdentifier()]
1533                : Template::NONE;
1534            $templatesDrivenRP->setTemplate($interaction->getResponse(), $pattern);
1535        }
1536        $templatesDrivenRP->setRelatedItem($this->item);
1537        $returnValue = $templatesDrivenRP;
1538
1539        return $returnValue;
1540    }
1541
1542    private function buildIsNullFeedbackRule($subtree, $comparedValue = null, $responseId = '')
1543    {
1544        $conditionName = 'isNull';
1545        $responseIdentifier = empty($responseId)
1546            ? (string) $subtree->responseIf->isNull->variable['identifier']
1547            : $responseId;
1548
1549        $feedbackOutcomeIdentifier = (string) $subtree->responseIf->setOutcomeValue['identifier'];
1550        $feedbackIdentifier = (string) $subtree->responseIf->setOutcomeValue->baseValue;
1551
1552        try {
1553            $response = $this->getResponse($responseIdentifier);
1554            $outcome = $this->getOutcome($feedbackOutcomeIdentifier);
1555            $feedbackThen = $this->getModalFeedback($feedbackIdentifier);
1556
1557            $feedbackElse = null;
1558
1559            $feedbackRule = new SimpleFeedbackRule($outcome, $feedbackThen, $feedbackElse);
1560            $feedbackRule->setCondition($response, $conditionName, $comparedValue);
1561        } catch (ParsingException $e) {
1562            throw new UnexpectedResponseProcessing('Feedback resources not found. Not template driven, unknown rule');
1563        }
1564        return $feedbackRule;
1565    }
1566
1567    private function buildSimpleFeedbackRule($subtree, $conditionName, $comparedValue = null, $responseIdentifier = '')
1568    {
1569        $responseIdentifier = $this->createResponseIdentifier($subtree, $responseIdentifier);
1570        $feedbackOutcomeIdentifier = (string) $subtree->responseIf->setOutcomeValue['identifier'];
1571        $feedbackIdentifier = (string) $subtree->responseIf->setOutcomeValue->baseValue;
1572
1573        try {
1574            $response = $this->getResponse($responseIdentifier);
1575            $outcome = $this->getOutcome($feedbackOutcomeIdentifier);
1576            $feedbackThen = $this->getModalFeedback($feedbackIdentifier);
1577
1578            $feedbackElse = null;
1579            if ($subtree->responseElse->getName()) {
1580                $feedbackElseIdentifier = (string) $subtree->responseElse->setOutcomeValue->baseValue;
1581                $feedbackElse = $this->getModalFeedback($feedbackElseIdentifier);
1582            }
1583
1584            $feedbackRule = new SimpleFeedbackRule($outcome, $feedbackThen, $feedbackElse);
1585            $feedbackRule->setCondition($response, $conditionName, $comparedValue);
1586        } catch (ParsingException $e) {
1587            throw new UnexpectedResponseProcessing('Feedback resources not found. Not template driven, unknown rule');
1588        }
1589        return $feedbackRule;
1590    }
1591
1592    private function createResponseIdentifier(SimpleXMLElement $subtree, string $responseIdentifier): string
1593    {
1594        if (!empty($responseIdentifier)) {
1595            return $responseIdentifier;
1596        }
1597
1598        foreach ($subtree->responseIf->children() as $child) {
1599            if ($child->getName() === 'setOutcomeValue') {
1600                continue;
1601            }
1602            foreach (['variable', 'mapResponse'] as $element) {
1603                if (isset($child->{$element})) {
1604                    return (string)$child->{$element}['identifier'];
1605                }
1606            }
1607        }
1608
1609        return $responseIdentifier;
1610    }
1611
1612    /**
1613     * Short description of method buildObject
1614     *
1615     * @access private
1616     * @author Joel Bout, <joel.bout@tudor.lu>
1617     * @param  DOMElement $data
1618     * @return \oat\taoQtiItem\model\qti\QtiObject
1619     */
1620    private function buildObject(DOMElement $data)
1621    {
1622
1623        $attributes = $this->extractAttributes($data);
1624        $returnValue = new QtiObject($attributes);
1625
1626        if ($data->hasChildNodes()) {
1627            $nonEmptyChild = $this->getNonEmptyChildren($data);
1628            if (count($nonEmptyChild) == 1 && reset($nonEmptyChild)->nodeName == 'object') {
1629                $alt = $this->buildObject(reset($nonEmptyChild));
1630                $returnValue->setAlt($alt);
1631            } else {
1632                //get the node xml content
1633                $pattern = ["/^<{$data->nodeName}([^>]*)?>/i", "/<\/{$data->nodeName}([^>]*)?>$/i"];
1634                $content = preg_replace($pattern, '', trim($this->saveXML($data)));
1635                $returnValue->setAlt($content);
1636            }
1637        } else {
1638            $alt = trim($data->nodeValue);
1639            if (!empty($alt)) {
1640                $returnValue->setAlt($alt);
1641            }
1642        }
1643
1644        return $returnValue;
1645    }
1646
1647    private function buildImg(DOMElement $data)
1648    {
1649
1650        $attributes = $this->extractAttributes($data);
1651        $returnValue = new Img($attributes);
1652
1653        return $returnValue;
1654    }
1655
1656    private function buildFigCaption(DOMElement $data): FigCaption
1657    {
1658        $attributes = $this->extractAttributes($data);
1659        $figCaption = new FigCaption($attributes);
1660        $this->parseContainerStatic($data, $figCaption->getBody());
1661
1662        return $figCaption;
1663    }
1664
1665    private function buildTooltip(DOMElement $data, DOMElement $context)
1666    {
1667
1668        $tooltip = null;
1669        $attributes = $this->extractAttributes($data);
1670
1671        // Look for tooltip content
1672        $contentId = $attributes['aria-describedby'];
1673        if (!empty($contentId)) {
1674            $tooltipContentNodes = $this->queryXPath(".//*[@id='$contentId']", $context);
1675            $tooltipContent = $tooltipContentNodes[0];
1676
1677            if (!is_null($tooltipContent)) {
1678                $content = $this->getNodeContentAsHtml($this->data, $tooltipContent);
1679
1680                // Content has been found, we can build the tooltip
1681                $tooltip = new Tooltip($attributes);
1682                $tooltip->setContent($content);
1683
1684                // remove the tooltip content node so it does not pollute the markup
1685                $tooltipContent->parentNode->removeChild($tooltipContent);
1686
1687                // Set the tooltip target
1688                $this->parseContainerStatic($data, $tooltip->getBody());
1689            }
1690        }
1691        return $tooltip;
1692    }
1693
1694    private function getNodeContentAsHtml(DOMDocument $document, DOMElement $node)
1695    {
1696        $html = "";
1697        $children = $node->childNodes;
1698        foreach ($children as $childNode) {
1699            $html .= $document->saveXML($childNode);
1700        }
1701        return $html;
1702    }
1703
1704    private function buildTable(DOMElement $data)
1705    {
1706
1707        $attributes = $this->extractAttributes($data);
1708        $table = new Table($attributes);
1709        $this->parseContainerStatic($data, $table->getBody());
1710
1711        return $table;
1712    }
1713
1714    private function buildFigure(DOMElement $data): Figure
1715    {
1716
1717        $attributes = $this->extractAttributes($data);
1718        $figure = new Figure($attributes);
1719        $this->parseContainerStatic($data, $figure->getBody());
1720
1721        return $figure;
1722    }
1723
1724    private function buildMath(DOMElement $data)
1725    {
1726
1727        $ns = $this->getMathNamespace();
1728        $annotationNodes = $this->queryXPath(".//*[name(.)='" . (empty($ns) ? '' : $ns . ':') . "annotation']", $data);
1729        $annotations = [];
1730        //need to extract the namespace, and clean it in the "bodydata"
1731        foreach ($annotationNodes as $annotationNode) {
1732            $attr = $this->extractAttributes($annotationNode);
1733            $encoding = isset($attr['encoding']) ? strtolower(trim($attr['encoding'])) : '';
1734            $str = $this->getBodyData($annotationNode);
1735            if (!empty($encoding) && !empty($str)) {
1736                $annotations[$encoding] = $str;
1737                $this->deleteNode($annotationNode);
1738            }
1739        }
1740
1741        $math = new Math($this->extractAttributes($data));
1742        $body = $this->getBodyData($data, true);
1743        $math->setMathML($body);
1744        $math->setAnnotations($annotations);
1745
1746        return $math;
1747    }
1748
1749    private function buildXInclude(DOMElement $data)
1750    {
1751
1752        return  new XInclude($this->extractAttributes($data));
1753    }
1754
1755    protected function getNonEmptyChildren(DOMElement $data)
1756    {
1757        $returnValue = [];
1758        foreach ($data->childNodes as $childNode) {
1759            if ($childNode->nodeName == '#text') {
1760                if (trim($childNode->nodeValue) != '') {
1761                    $returnValue[] = $childNode;
1762                }
1763            } else {
1764                $returnValue[] = $childNode;
1765            }
1766        }
1767        return $returnValue;
1768    }
1769
1770    private function buildStylesheet(DOMElement $data)
1771    {
1772        $returnValue = new Stylesheet([
1773            'href' => (string) $data->getAttribute('href'),
1774            'title' => $data->hasAttribute('title') ? (string) $data->getAttribute('title') : '',
1775            'media' => $data->hasAttribute('media') ? (string) $data->getAttribute('media') : 'screen',
1776            'type' => $data->hasAttribute('type') ? (string) $data->getAttribute('type') : 'text/css',
1777        ]);
1778
1779        return $returnValue;
1780    }
1781
1782    private function buildRubricBlock(DOMElement $data)
1783    {
1784
1785        $returnValue = new RubricBlock($this->extractAttributes($data));
1786        $this->parseContainerStatic($data, $returnValue->getBody());
1787
1788        return $returnValue;
1789    }
1790
1791    private function buildFeedback(DOMElement $data, $interactive = false)
1792    {
1793
1794        $type = ucfirst($data->nodeName);
1795        $feedbackClass = '\\oat\\taoQtiItem\\model\\qti\\feedback\\' . $type;
1796        if (!class_exists($feedbackClass)) {
1797            throw new ParsingException('The interaction class cannot be found: ' . $feedbackClass);
1798        }
1799
1800        $attributes = $this->extractAttributes($data);
1801
1802        if ($data->nodeName == 'modalFeedback') {
1803            $myFeedback = new $feedbackClass($attributes, $this->item);
1804            $this->parseContainerStatic($data, $myFeedback->getBody());
1805        } else {
1806            throw new UnsupportedQtiElement($data);
1807        }
1808
1809        return $myFeedback;
1810    }
1811
1812    /**
1813     * Return the list of registered php portable element subclasses
1814     * @return array
1815     */
1816    private function getPortableElementSubclasses($superClassName)
1817    {
1818        $subClasses = [];
1819        foreach (PortableModelRegistry::getRegistry()->getModels() as $model) {
1820            $portableElementClass = $model->getQtiElementClassName();
1821            if (is_subclass_of($portableElementClass, $superClassName)) {
1822                $subClasses[] = $portableElementClass;
1823            }
1824        }
1825        return $subClasses;
1826    }
1827
1828    /**
1829     * Get the PCI class associated to a dom node based on its namespace
1830     * Returns null if not a known PCI model
1831     *
1832     * @param DOMElement $data
1833     * @return null
1834     */
1835    private function getPortableElementClass(DOMElement $data, $superClassName, $portableElementNodeName)
1836    {
1837
1838        $portableElementClasses = $this->getPortableElementSubclasses($superClassName);
1839
1840        //start searching from globally declared namespace
1841        foreach ($this->item->getNamespaces() as $name => $uri) {
1842            foreach ($portableElementClasses as $class) {
1843                if (
1844                    $uri === $class::NS_URI
1845                    && $this->queryXPathChildren([$portableElementNodeName], $data, $name)->length
1846                ) {
1847                    return $class;
1848                }
1849            }
1850        }
1851
1852        //not found as a global namespace definition, try local namespace
1853        if ($this->queryXPathChildren([$portableElementNodeName], $data)->length) {
1854            $pciNode = $this->queryXPathChildren([$portableElementNodeName], $data)[0];
1855            $xmlns = $pciNode->getAttribute('xmlns');
1856            foreach ($portableElementClasses as $phpClass) {
1857                if ($phpClass::NS_URI === $xmlns) {
1858                    return $phpClass;
1859                }
1860            }
1861        }
1862
1863        //not a known portable element type
1864        return null;
1865    }
1866
1867    private function getPciClass(DOMElement $data)
1868    {
1869        return $this->getPortableElementClass(
1870            $data,
1871            'oat\\taoQtiItem\\model\\qti\\interaction\\CustomInteraction',
1872            'portableCustomInteraction'
1873        );
1874    }
1875
1876    private function getPicClass(DOMElement $data)
1877    {
1878        return $this->getPortableElementClass($data, 'oat\\taoQtiItem\\model\\qti\\InfoControl', 'portableInfoControl');
1879    }
1880
1881    /**
1882     * Parse and build a custom interaction object
1883     *
1884     * @param DOMElement $data
1885     * @return CustomInteraction
1886     * @throws ParsingException
1887     */
1888    private function buildCustomInteraction(DOMElement $data)
1889    {
1890
1891        $interaction = null;
1892
1893        $pciClass = $this->getPciClass($data);
1894
1895        if (!empty($pciClass)) {
1896            $ns = null;
1897            foreach ($this->item->getNamespaces() as $name => $uri) {
1898                if ($pciClass::NS_URI === $uri) {
1899                    $ns = new QtiNamespace($uri, $name);
1900                }
1901            }
1902            if (is_null($ns)) {
1903                $pciNodes = $this->queryXPathChildren(['portableCustomInteraction'], $data);
1904                if ($pciNodes->length) {
1905                    $ns = new QtiNamespace($pciNodes->item(0)->getAttribute('xmlns'));
1906                }
1907            }
1908
1909            //use tao's implementation of portable custom interaction
1910            $interaction = new $pciClass($this->extractAttributes($data), $this->item);
1911            $interaction->feed($this, $data, $ns);
1912        } else {
1913            $ciClass = '';
1914            $classes = $data->getAttribute('class');
1915            $classeNames = preg_split('/\s+/', $classes);
1916            foreach ($classeNames as $classeName) {
1917                $ciClass = CustomInteractionRegistry::getCustomInteractionByName($classeName);
1918                if ($ciClass) {
1919                    $interaction = new $ciClass($this->extractAttributes($data), $this->item);
1920                    $interaction->feed($this, $data);
1921                    break;
1922                }
1923            }
1924
1925            if (!$ciClass) {
1926                throw new ParsingException('unknown custom interaction to be build');
1927            }
1928        }
1929
1930        return $interaction;
1931    }
1932
1933    /**
1934     * Parse and build a info control
1935     *
1936     * @param DOMElement $data
1937     * @return InfoControl
1938     * @throws ParsingException
1939     */
1940    private function buildInfoControl(DOMElement $data)
1941    {
1942
1943        $infoControl = null;
1944
1945        $picClass = $this->getPicClass($data);
1946
1947        if (!empty($picClass)) {
1948            $ns = null;
1949            foreach ($this->item->getNamespaces() as $name => $uri) {
1950                if ($picClass::NS_URI === $uri) {
1951                    $ns = new QtiNamespace($uri, $name);
1952                }
1953            }
1954            if (is_null($ns)) {
1955                $pciNodes = $this->queryXPathChildren(['portableInfoControl'], $data);
1956                if ($pciNodes->length) {
1957                    $ns = new QtiNamespace($pciNodes->item(0)->getAttribute('xmlns'));
1958                }
1959            }
1960
1961            //use tao's implementation of portable custom interaction
1962            $infoControl = new PortableInfoControl($this->extractAttributes($data), $this->item);
1963            $infoControl->feed($this, $data, $ns);
1964        } else {
1965            $ciClass = '';
1966            $classes = $data->getAttribute('class');
1967            $classeNames = preg_split('/\s+/', $classes);
1968            foreach ($classeNames as $classeName) {
1969                $ciClass = InfoControlRegistry::getInfoControlByName($classeName);
1970                if ($ciClass) {
1971                    $infoControl = new $ciClass($this->extractAttributes($data), $this->item);
1972                    $infoControl->feed($this, $data);
1973                    break;
1974                }
1975            }
1976
1977            if (!$ciClass) {
1978                throw new UnsupportedQtiElement($data);
1979            }
1980        }
1981
1982        return $infoControl;
1983    }
1984}