MDL-34738 qformat blackboard: blackboard format is broken
[moodle.git] / question / format / blackboard / format.php
index 9ce23d0..1626489 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Blackboard question importer.
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package qformat_blackboard
  * @copyright  2003 Scott Elliott
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -26,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once ($CFG->libdir . '/xmlize.php');
+require_once($CFG->libdir . '/xmlize.php');
 
 
 /**
@@ -35,19 +34,67 @@ require_once ($CFG->libdir . '/xmlize.php');
  * @copyright  2003 Scott Elliott
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class qformat_blackboard extends qformat_default {
+class qformat_blackboard extends qformat_based_on_xml {
+    // Is the current question's question text escaped HTML (true for most if not all Blackboard files).
+    public $ishtml = true;
+
 
     public function provide_import() {
         return true;
     }
 
-    function readquestions($lines) {
-        /// Parses an array of lines into an array of questions,
-        /// where each item is a question object as defined by
-        /// readquestion().
+    public function mime_type() {
+        return mimeinfo('type', '.dat');
+    }
+
+    /**
+     * Some softwares put entities in exported files.
+     * This method try to clean up known problems.
+     * @param string str string to correct
+     * @return string the corrected string
+     */
+    public function cleaninput($str) {
+        if (!$this->ishtml) {
+            return $str;
+        }
+        $html_code_list = array(
+            "'" => "'",
+            "’" => "'",
+            "[" => "[",
+            "“" => "\"",
+            "”" => "\"",
+            "]" => "]",
+            "'" => "'",
+            "–" => "-",
+            "—" => "-",
+        );
+        $str = strtr($str, $html_code_list);
+        // Use textlib entities_to_utf8 function to convert only numerical entities.
+        $str = textlib::entities_to_utf8($str, false);
+        return $str;
+    }
 
-        $text = implode($lines, " ");
-        $xml = xmlize($text, 0);
+    /**
+     * Parse the array of lines into an array of questions
+     * this *could* burn memory - but it won't happen that much
+     * so fingers crossed!
+     * @param array of lines from the input file.
+     * @param stdClass $context
+     * @return array (of objects) question objects.
+     */
+    protected function readquestions($lines) {
+
+        $text = implode($lines, ' ');
+        unset($lines);
+
+        // This converts xml to big nasty data structure,
+        // the 0 means keep white space as it is.
+        try {
+            $xml = xmlize($text, 0, 'UTF-8', true);
+        } catch (xml_format_exception $e) {
+            $this->error($e->getMessage(), '');
+            return false;
+        }
 
         $questions = array();
 
@@ -61,341 +108,405 @@ class qformat_blackboard extends qformat_default {
         return $questions;
     }
 
-//----------------------------------------
-// Process Essay Questions
-//----------------------------------------
-    function process_essay($xml, &$questions ) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_ESSAY"])) {
-            $essayquestions = $xml["POOL"]["#"]["QUESTION_ESSAY"];
+    /**
+     * Do question import processing common to every qtype.
+     * @param array $questiondata the xml tree related to the current question
+     * @return object initialized question object.
+     */
+    public function process_common($questiondata) {
+        global $CFG;
+
+        // This routine initialises the question object.
+        $question = $this->defaultquestion();
+
+        // Determine if the question is already escaped html.
+        $this->ishtml = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'),
+                false, false);
+
+        // Put questiontext in question object.
+        $text = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'TEXT', 0, '#'),
+                '', true, get_string('importnotext', 'qformat_blackboard'));
+
+        if ($this->ishtml) {
+            $question->questiontext = $this->cleaninput($text);
+            $question->questiontextformat = FORMAT_HTML;
+            $question->questiontextfiles = array();
+
+        } else {
+            $question->questiontext = $text;
         }
-        else {
-            return;
+        // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
+        $question->name = shorten_text(strip_tags($question->questiontext), 200);
+        $question->name = substr($question->name, 0, 250);
+        if (!$question->name) {
+            $id = $this->getpath($questiondata,
+                    array('@', 'id'), '',  true);
+            $question->name = get_string('defaultname', 'qformat_blackboard' , $id);
         }
 
-        foreach ($essayquestions as $essayquestion) {
+        $question->generalfeedback = '';
+        $question->generalfeedbackformat = FORMAT_HTML;
+        $question->generalfeedbackfiles = array();
 
-            $question = $this->defaultquestion();
+        // TODO : read the mark from the POOL TITLE QUESTIONLIST section.
+        $question->defaultmark = 1;
+        return $question;
+    }
+
+    /**
+     * Process Essay Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_essay($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) {
+            $essayquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_ESSAY'), false, false);
+        } else {
+            return;
+        }
 
-            $question->qtype = ESSAY;
+        foreach ($essayquestions as $thisquestion) {
 
-            // determine if the question is already escaped html
-            $ishtml = $essayquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
+            $question = $this->process_common($thisquestion);
 
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($essayquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]));
-            }
+            $question->qtype = 'essay';
 
-            // put name in question object
-            $question->name = substr($question->questiontext, 0, 254);
             $question->answer = '';
+            $answer = $this->getpath($thisquestion,
+                    array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true);
+            $question->graderinfo =  $this->text_field($this->cleaninput($answer));
             $question->feedback = '';
+            $question->responseformat = 'editor';
+            $question->responsefieldlines = 15;
+            $question->attachments = 0;
             $question->fraction = 0;
 
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process True / False Questions
-    //----------------------------------------
-    function process_tf($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_TRUEFALSE"])) {
-            $tfquestions = $xml["POOL"]["#"]["QUESTION_TRUEFALSE"];
-        }
-        else {
+    /**
+     * Process True / False Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_tf($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) {
+            $tfquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($tfquestions); $i++) {
-
-            $question = $this->defaultquestion();
+        foreach ($tfquestions as $thisquestion) {
 
-            $question->qtype = TRUEFALSE;
-            $question->single = 1; // Only one answer is allowed
+            $question = $this->process_common($thisquestion);
 
-            $thisquestion = $tfquestions[$i];
+            $question->qtype = 'truefalse';
+            $question->single = 1; // Only one answer is allowed.
 
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false);
 
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-            // put name in question object
-            $question->name = shorten_text($question->questiontext, 254);
+            $correct_answer = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                    '', true);
 
-            $choices = $thisquestion["#"]["ANSWER"];
-
-            $correct_answer = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"];
-
-            // first choice is true, second is false.
-            $id = $choices[0]["@"]["id"];
-
-            if (strcmp($id, $correct_answer) == 0) {  // true is correct
+            // First choice is true, second is false.
+            $id = $this->getpath($choices[0], array('@', 'id'), '', true);
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            if (strcmp($id,  $correct_answer) == 0) {  // True is correct.
                 $question->answer = 1;
-                $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
-                $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-            } else {  // false is correct
+                $question->feedbacktrue = $this->text_field($this->cleaninput($correctfeedback));
+                $question->feedbackfalse = $this->text_field($this->cleaninput($incorrectfeedback));
+            } else {  // False is correct.
                 $question->answer = 0;
-                $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-                $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
+                $question->feedbacktrue = $this->text_field($this->cleaninput($incorrectfeedback));
+                $question->feedbackfalse = $this->text_field($this->cleaninput($correctfeedback));
             }
             $question->correctanswer = $question->answer;
             $questions[] = $question;
-          }
+        }
     }
 
-    //----------------------------------------
-    // Process Multiple Choice Questions
-    //----------------------------------------
-    function process_mc($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"])) {
-            $mcquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"];
-        }
-        else {
+    /**
+     * Process Multiple Choice Questions with single answer
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_mc($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) {
+            $mcquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($mcquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MULTICHOICE;
-            $question->single = 1; // Only one answer is allowed
-
-            $thisquestion = $mcquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-
-            // put name of question in question object, careful of length
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $choices = $thisquestion["#"]["ANSWER"];
-            for ($j = 0; $j < sizeof ($choices); $j++) {
-
-                $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]);
-                // put this choice in the question object.
-                if ($ishtml) {
-                    $question->answer[$j] = html_entity_decode($choice,ENT_QUOTES,'UTF-8');
-                }
-                $question->answer[$j] = $question->answer[$j];
-
-                $id = $choices[$j]["@"]["id"];
-                $correct_answer_id = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"];
-                // if choice is the answer, give 100%, otherwise give 0%
-                if (strcmp ($id, $correct_answer_id) == 0) {
-                    $question->fraction[$j] = 1;
-                    if ($ishtml) {
-                        $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]),ENT_QUOTES,'UTF-8');
-                    }
-                    $question->feedback[$j] = $question->feedback[$j];
+        foreach ($mcquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            $question->partiallycorrectfeedback = $this->text_field('');
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $question->qtype = 'multichoice';
+            $question->single = 1; // Only one answer is allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correct_answer_id = $this->getpath($thisquestion,
+                        array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                        '', true);
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->text_field($this->cleaninput($choicetext));
+
+                $choice_id = $this->getpath($choice, array('@', 'id'), '', true);
+                // If choice is the right answer, give 100% mark, otherwise give 0%.
+                if (strcmp ($choice_id, $correct_answer_id) == 0) {
+                    $question->fraction[] = 1;
                 } else {
-                    $question->fraction[$j] = 0;
-                    if ($ishtml) {
-                        $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]),ENT_QUOTES,'UTF-8');
-                    }
-                    $question->feedback[$j] = $question->feedback[$j];
+                    $question->fraction[] = 0;
                 }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
             }
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Multiple Choice Questions With Multiple Answers
-    //----------------------------------------
-    function process_ma($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"])) {
-            $maquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"];
-        }
-        else {
+    /**
+     * Process Multiple Choice Questions With Multiple Answers
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_ma($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) {
+            $maquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($maquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MULTICHOICE;
+        foreach ($maquestions as $thisquestion) {
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $question->qtype = 'multichoice';
             $question->defaultmark = 1;
-            $question->single = 0; // More than one answers allowed
-            $question->image = ""; // No images with this format
-
-            $thisquestion = $maquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
+            $question->single = 0; // More than one answers allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correct_answer_ids = array();
+            foreach ($this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) {
+                if ($correctanswer) {
+                    $correct_answer_ids[] = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                }
             }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
+            $fraction = 1/count($correct_answer_ids);
 
-            $choices = $thisquestion["#"]["ANSWER"];
-            $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"];
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->text_field($this->cleaninput($choicetext));
 
-            for ($j = 0; $j < sizeof ($choices); $j++) {
+                $choice_id = $this->getpath($choice, array('@', 'id'), '', true);
 
-                $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]);
-                // put this choice in the question object.
-                $question->answer[$j] = $choice;
+                $iscorrect = in_array($choice_id, $correct_answer_ids);
 
-                $correctanswercount = sizeof($correctanswers);
-                $id = $choices[$j]["@"]["id"];
-                $iscorrect = 0;
-                for ($k = 0; $k < $correctanswercount; $k++) {
-
-                    $correct_answer_id = trim($correctanswers[$k]["@"]["answer_id"]);
-                    if (strcmp ($id, $correct_answer_id) == 0) {
-                        $iscorrect = 1;
-                    }
-
-                }
                 if ($iscorrect) {
-                    $question->fraction[$j] = floor(100000/$correctanswercount)/100000; // strange behavior if we have more than 5 decimal places
-                    $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
+                    $question->fraction[] = $fraction;
                 } else {
-                    $question->fraction[$j] = 0;
-                    $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
+                    $question->fraction[] = 0;
                 }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
             }
-
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Fill in the Blank Questions
-    //----------------------------------------
-    function process_fib($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_FILLINBLANK"])) {
-            $fibquestions = $xml["POOL"]["#"]["QUESTION_FILLINBLANK"];
-        }
-        else {
+    /**
+     * Process Fill in the Blank Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_fib($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) {
+            $fibquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($fibquestions); $i++) {
-            $question = $this->defaultquestion();
-
-            $question->qtype = SHORTANSWER;
-            $question->usecase = 0; // Ignore case
-
-            $thisquestion = $fibquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $answer = trim($thisquestion["#"]["ANSWER"][0]["#"]["TEXT"][0]["#"]);
-
-            $question->answer[] = $answer;
-            $question->fraction[] = 1;
-            $question->feedback = array();
-
-            if (is_array( $thisquestion['#']['GRADABLE'][0]['#'] )) {
-                $question->feedback[0] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
-            }
-            else {
-                $question->feedback[0] = '';
-            }
-            if (is_array( $thisquestion["#"]["GRADABLE"][0]["#"] )) {
-                $question->feedback[1] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-            }
-            else {
-                $question->feedback[1] = '';
+        foreach ($fibquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $question->qtype = 'shortanswer';
+            $question->usecase = 0; // Ignore case.
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            foreach ($answers as $answer) {
+                $question->answer[] = $this->getpath($answer,
+                        array('#', 'TEXT', 0, '#'), '', true);
+                $question->fraction[] = 1;
+                $question->feedback[] = $this->text_field($this->cleaninput($correctfeedback));
             }
+            $question->answer[] = '*';
+            $question->fraction[] = 0;
+            $question->feedback[] = $this->text_field($this->cleaninput($incorrectfeedback));
 
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Matching Questions
-    //----------------------------------------
-    function process_matching($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MATCH"])) {
-            $matchquestions = $xml["POOL"]["#"]["QUESTION_MATCH"];
-        }
-        else {
+    /**
+     * Process Matching Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_matching($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) {
+            $matchquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MATCH'), false, false);
+        } else {
             return;
         }
-
-        for ($i = 0; $i < sizeof ($matchquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MATCH;
-
-            $thisquestion = $matchquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
+        // Blackboard questions can't be imported in core Moodle without a loss in data,
+        // as core match question don't allow HTML in subanswers. The contributed ddmatch
+        // question type support HTML in subanswers.
+        // The ddmatch question type is not part of core, so we need to check if it is defined.
+        $ddmatch_is_installed = question_bank::is_qtype_installed('ddmatch');
+
+        foreach ($matchquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+            if ($ddmatch_is_installed) {
+                $question->qtype = 'ddmatch';
+            } else {
+                $question->qtype = 'match';
             }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $choices = $thisquestion["#"]["CHOICE"];
-            for ($j = 0; $j < sizeof ($choices); $j++) {
-
-                $subquestion = NULL;
-
-                $choice = $choices[$j]["#"]["TEXT"][0]["#"];
-                $choice_id = $choices[$j]["@"]["id"];
-
-                $question->subanswers[] = trim($choice);
-
-                $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"];
-                for ($k = 0; $k < sizeof ($correctanswers); $k++) {
-
-                    if (strcmp($choice_id, $correctanswers[$k]["@"]["choice_id"]) == 0) {
-
-                        $answer_id = $correctanswers[$k]["@"]["answer_id"];
 
-                        $answers = $thisquestion["#"]["ANSWER"];
-                        for ($m = 0; $m < sizeof ($answers); $m++) {
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $choices = $this->getpath($thisquestion,
+                    array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers.
+            $answers = $this->getpath($thisquestion,
+                    array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions.
+            $correctanswers = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers.
+            $mappings = array();
+            foreach ($correctanswers as $correctanswer) {
+                if ($correctanswer) {
+                    $correct_choice_id = $this->getpath($correctanswer,
+                                array('@', 'choice_id'), '', true);
+                    $correct_answer_id = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                    $mappings[$correct_answer_id] = $correct_choice_id;
+                }
+            }
 
-                            $answer = $answers[$m];
-                            $current_ans_id = $answer["@"]["id"];
-                            if (strcmp ($current_ans_id, $answer_id) == 0) {
+            foreach ($choices as $choice) {
+                if ($ddmatch_is_installed) {
+                    $choicetext = $this->text_field($this->cleaninput($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                } else {
+                    $choicetext = trim(strip_tags($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                }
 
-                                $answer = $answer["#"]["TEXT"][0]["#"];
-                                $question->subquestions[] = trim($answer);
+                if ($choicetext != '') { // Only import non empty subanswers.
+                    $subquestion = '';
+                    $choice_id = $this->getpath($choice,
+                            array('@', 'id'), '', true);
+                    $fiber = array_search($choice_id, $mappings);
+                    $fiber = array_keys ($mappings, $choice_id);
+                    foreach ($fiber as $correct_answer_id) {
+                        // We have found a correspondance for this choice so we need to take the associated answer.
+                        foreach ($answers as $answer) {
+                            $current_ans_id = $this->getpath($answer,
+                                    array('@', 'id'), '', true);
+                            if (strcmp ($current_ans_id, $correct_answer_id) == 0) {
+                                $subquestion = $this->getpath($answer,
+                                        array('#', 'TEXT', 0, '#'), '', true);
                                 break;
                             }
                         }
-                        break;
+                        $question->subquestions[] = $this->text_field($this->cleaninput($subquestion));
+                        $question->subanswers[] = $choicetext;
+                    }
+
+                    if ($subquestion == '') { // Then in this case, $choice is a distractor.
+                        $question->subquestions[] = $this->text_field('');
+                        $question->subanswers[] = $choicetext;
                     }
                 }
             }
 
-            $questions[] = $question;
+            // Verify that this matching question has enough subquestions and subanswers.
+            $subquestioncount = 0;
+            $subanswercount = 0;
+            $subanswers = $question->subanswers;
+            foreach ($question->subquestions as $key => $subquestion) {
+                $subquestion = $subquestion['text'];
+                $subanswer = $subanswers[$key];
+                if ($subquestion != '') {
+                    $subquestioncount++;
+                }
+                $subanswercount++;
+            }
+            if ($subquestioncount < 2 || $subanswercount < 3) {
+                    $this->error(get_string('notenoughtsubans', 'qformat_blackboard', $question->questiontext));
+            } else {
+                $questions[] = $question;
+            }
 
         }
     }